Metal: a frame with a handed-over radial curve in use is integrated by the CPU engine

With a curve from the whole frame (RadialBackgroundFromRings) the ring's high-side clip follows it
(RadialClipOffset) on the CPU and CUDA; the Metal clip kernel did not, so a Mac run of such a frame
differed from both. It now goes to the CPU engine, which gives that result exactly - the precedent is
the trimmed mean. Frames without a handed-over curve are unchanged.

Not compiled here (no macOS); BraggIntegrationEngineMetal_HandedOverRadialCurveStaysOnCPU checks it on
the Mac CI job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit 36be3b8f13dbde2616fe7add3acc3d073ef082af)
This commit is contained in:
leonarski_f committed 2026-10-23 21:21:46 +02:00
1 parent 6940718cf5
commit 3467c21919
2 files changed
+28 -2

No files matched your search

@@ -147,7 +147,9 @@ std::vector<Reflection> BraggIntegrationEngineMetal::Run(const ImagePreprocessor
const std::vector<Reflection> &predicted,
size_t npredicted, int64_t image_number) {
// An image of another size, and a frame with nothing predicted, the CPU engine answers as it would.
if (image.size() != npixel || npredicted == 0)
// So does a frame whose radial curve was handed over and is in use: the ring's clip then follows the
// curve (RadialClipOffset), which the clip kernel here does not, and the CPU gives that result exactly.
if (image.size() != npixel || npredicted == 0 || (bkg_radial && !radial_curve.empty()))
return BraggIntegrationEngineCPU::Run(image, predicted, npredicted, image_number);
dev->img = nil;
@@ -221,7 +223,7 @@ std::vector<Reflection> BraggIntegrationEngineMetal::Run(const ImagePreprocessor
f.use_owner = use_owner;
f.exclude = overlap == OverlapMode::Exclude && mode != IntegratorMode::BoxSum;
// The curve is added up on the GPU only where none was handed over (RadialBackgroundFromRings):
// StartPassA sizes its sums for that case alone. The clip does not follow a handed-over curve here.
// StartPassA sizes its sums for that case alone. A handed-over curve in use never gets here (above).
const bool accumulate_radial = bkg_radial && radial_curve.empty();
f.n_rad = accumulate_radial ? a.n_rad : 0;
f.static_mask_words = static_cast<uint32_t>(static_mask.size());
+24
View File
@@ -253,6 +253,30 @@ TEST_CASE("BraggIntegrationEngineMetal_BitIdenticalToCPU", "[portable][Integrati
}
}
// A radial curve handed over from the whole frame (RadialBackgroundFromRings) makes the ring's clip follow
// it, which the clip kernel does not: such a frame is integrated by the CPU engine, so the result is the
// CPU's exactly and no frame goes to the GPU.
TEST_CASE("BraggIntegrationEngineMetal_HandedOverRadialCurveStaysOnCPU", "[portable][Integration][Metal]") {
Case c;
c.radial = true;
const DiffractionExperiment x = MakeExperiment(DetJF(2), c);
const PixelMask mask(x);
BraggIntegrationEngineCPU cpu(x, mask);
BraggIntegrationEngineMetal metal(x, mask);
std::vector<float> rings(400);
for (size_t i = 0; i < rings.size(); ++i)
rings[i] = 1.0f + 0.002f * static_cast<float>(i * i);
cpu.RadialBackgroundFromRings(rings, static_cast<int32_t>(rings.size()), 0.05f, 0.01f);
metal.RadialBackgroundFromRings(rings, static_cast<int32_t>(rings.size()), 0.05f, 0.01f);
ImagePreprocessorBuffer image(x.GetPixelsNum());
const Frame f = RandomFrame(x, 7, {});
std::copy(f.image.begin(), f.image.end(), image.data());
const auto out_cpu = cpu.Run(image, f.predicted, f.predicted.size(), 0);
RequireSame(out_cpu, metal.Run(image, f.predicted, f.predicted.size(), 0));
CHECK(out_cpu.size() > 1000);
CHECK(metal.FramesOnGPU() == 0);
}
// The trimmed-mean background sorts each ring, which only the CPU engine does: such a run keeps the CPU
// engine (MXAnalysisWithoutFPGA asks Available).
TEST_CASE("BraggIntegrationEngineMetal_TrimmedMeanStaysOnCPU", "[portable][Integration][Metal]") {