From 9589e99b8ed5376db1eb2d21a035289322d57aa8 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 1 Nov 2026 11:09:57 +0100 Subject: [PATCH] Rotation merge on the GPU reports a merge without fulls instead of crashing When the GPU 3D combine made no full reflection (no rocking event captured whole enough - e.g. a sweep exposed 0.2 deg out of every 0.6 deg, or a lattice that does not describe the frames), the GPU scale-fulls step was skipped and the host scale-fulls loop ran instead. That loop slices the per-frame fulls CSR that only the host combine builds, so it read empty vectors and the process died on SIGSEGV in RunArm. The host loop now runs only on fulls the host combined; with no fulls the merge ends in the existing "nothing to merge" error, as the CPU path already did. Runs that make fulls are unchanged (merged and unmerged MTZ data bit-identical on two in-house sweeps). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HVeDQqN9qRD57vnShzTC37 --- image_analysis/scale_merge/RotationScaleMerge.cpp | 4 +++- tests/RotationScaleMergeSegmentsTest.cpp | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/image_analysis/scale_merge/RotationScaleMerge.cpp b/image_analysis/scale_merge/RotationScaleMerge.cpp index 87b44d2e0..117f120a9 100644 --- a/image_analysis/scale_merge/RotationScaleMerge.cpp +++ b/image_analysis/scale_merge/RotationScaleMerge.cpp @@ -7592,7 +7592,9 @@ RotationScaleMerge::Result RotationScaleMerge::RunArm(bool scale_partials, bool BuildFullsGroupCSR(n_groups); // --- 4. Scale the fulls (XDS order, Unity model). --- - if (scale_fulls && !scaled_fulls_on_gpu) { + // On the fulls the host combined: the per-frame CSR this loop slices is built by Combine(). Fulls + // combined on the GPU were scaled there - unless there were none, and then there is nothing to scale. + if (scale_fulls && !combined_on_gpu) { // On a working copy of the fulls, as the partials' loop (ScalingRef and ScalingFit in the header): // full_index[q] is the full at position q, in the group CSR's order and then the fulls in no group. const int nf = static_cast(fulls.size()); diff --git a/tests/RotationScaleMergeSegmentsTest.cpp b/tests/RotationScaleMergeSegmentsTest.cpp index f20d05f49..92fa4325e 100644 --- a/tests/RotationScaleMergeSegmentsTest.cpp +++ b/tests/RotationScaleMergeSegmentsTest.cpp @@ -229,3 +229,18 @@ TEST_CASE("RotationScaleMergeSegments_NoSegmentsIsTheEngineAsBefore", "[Rotation else if (get_gpu_description().find("RTX 5080") != std::string::npos) CHECK(hash == 6594155396455850264ULL); } + +// A sweep in which no rocking event is captured whole enough to make a full - as in one exposed 0.2 deg out +// of every 0.6 deg, where each reflection is caught on one frame and only in part. On the GPU the combine +// returned no fulls, the scale-fulls step then fell through to the host loop, which slices a per-frame CSR +// that only the host combine builds, and the process died on SIGSEGV instead of reporting the empty merge. +TEST_CASE("RotationScaleMerge_NoFullsIsAnErrorNotACrash", "[RotationScale]") { + const std::vector sweeps{{60, 1.0, 0.0}}; + auto data = MakeSweeps(sweeps, 17); + auto x = SyntheticExperiment(false); + // More than any rocking event can capture: the combine makes no full. + ScalingSettings s = x.GetScalingSettings(); + s.MinCapturedFraction(1000.0); + x.ImportScalingSettings(s); + CHECK_THROWS_AS(Merge(data, x, {}), JFJochException); +}