diff --git a/image_analysis/bragg_integration/BraggIntegrationEngineMetal.mm b/image_analysis/bragg_integration/BraggIntegrationEngineMetal.mm index c5cf46860..7c902dd7b 100644 --- a/image_analysis/bragg_integration/BraggIntegrationEngineMetal.mm +++ b/image_analysis/bragg_integration/BraggIntegrationEngineMetal.mm @@ -147,7 +147,9 @@ std::vector BraggIntegrationEngineMetal::Run(const ImagePreprocessor const std::vector &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 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(static_mask.size()); diff --git a/tests/BraggIntegrationEngineMetalTest.cpp b/tests/BraggIntegrationEngineMetalTest.cpp index 92b5af4eb..c602d3132 100644 --- a/tests/BraggIntegrationEngineMetalTest.cpp +++ b/tests/BraggIntegrationEngineMetalTest.cpp @@ -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 rings(400); + for (size_t i = 0; i < rings.size(); ++i) + rings[i] = 1.0f + 0.002f * static_cast(i * i); + cpu.RadialBackgroundFromRings(rings, static_cast(rings.size()), 0.05f, 0.01f); + metal.RadialBackgroundFromRings(rings, static_cast(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]") {