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
+2 -4
View File
@@ -1367,8 +1367,7 @@ mod tests {
// instead of the kernel under test. The chain still
// carries the resolve pass that finishes the render, and
// that generated source is worth compiling too.
let scale = g.render_scale(img.size(), (16, 16));
let detail = g.compose_detail(scale);
let detail = g.compose_detail(img.size(), (16, 16));
let key = g.invalidation().through(dr_pipeline::Affects::Colour);
pass.render_detailed(&img, &shader, 16, 16, None, &detail, key)
.unwrap_or_else(|e| {
@@ -1774,8 +1773,7 @@ mod tests {
// find those operations in neither stage and fail for a reason that is
// not a defect. Shadows the smaller size deliberately.
let (w, h) = g.output_size(512, 512);
let scale = g.render_scale((512, 512), (w, h));
let detail = g.compose_detail_for(scale, dr_types::ColourSpace::Srgb);
let detail = g.compose_detail_for((512, 512), (w, h), dr_types::ColourSpace::Srgb);
assert!(
!detail.is_empty(),
"the detail half composed nothing, so nothing of it was compiled"