Draw the repairs, before anything sharpens what they removed

A spot set now composes detail passes of its own, one per round, and they
go ahead of every operation's kernel. That placement is the decision worth
recording: a sharpening pass reads a neighbourhood, so sharpening a dust
mark before removing it smears its edge into pixels the repair's disc does
not cover, and what survives is a faint over-sharpened ring around an
otherwise perfect patch. It also disagrees with ARCH §5.2, which draws
spot removal after clarity — docs/spot-removal.md §5.1 is where that is
argued out.

Every length reaching the shader is in render pixels, converted here where
the framing is in scope. Both the centre and the source go through
`Framing::output_at` — the same map the fused pass applies to every pixel
— so a rotated photograph rotates the offset with no trigonometry, and the
radius is found by mapping a point one radius above the centre and
measuring, rather than by multiplying by a ratio this function has no
business knowing about. The tests turn and crop the frame and expect the
mark to stay gone, which is the property that arrangement buys.

compose_full now takes the spot set, because a photograph with a repair
and no sharpening still has a detail stage: a fused pass that encoded its
own output there would quantise twice and bind to a texture of the wrong
format. compose_detail_for takes the source size for the same kind of
reason — a RenderScale describes the region on screen, and a spot is
stored against the photograph.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 20:17:51 +02:00
co-authored by Claude Opus 5
parent 6997c0f7ac
commit 5323608051
17 changed files with 597 additions and 49 deletions
+8 -1
View File
@@ -30,6 +30,7 @@ use dr_gpu::{AdjustPass, DemosaicedImage, Demosaicer, GpuContext, MaskPass, Subj
use dr_pipeline::descriptor::ParamId;
use dr_pipeline::mask::{MaskLayer, MaskSource, MaskStack, Morphology};
use dr_pipeline::operation::compose_full;
use dr_pipeline::spot::SpotSet;
use dr_pipeline::{ops, EditGraph, Framing};
use dr_segment::{SemanticModel, SemanticOptions, Shaped};
use dr_types::ColourSpace;
@@ -315,7 +316,13 @@ fn render_stack(
.render(stack, None, Some(&subjects), pw, ph)
.expect("rasterise masks");
let shader = compose_full(&ops::chain(), &Framing::new(), ColourSpace::Srgb, stack);
let shader = compose_full(
&ops::chain(),
&Framing::new(),
ColourSpace::Srgb,
stack,
&SpotSet::new(),
);
adjust
.render_masked(source, &shader, ow, oh, Some(array))
.expect("render");