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:
@@ -467,7 +467,13 @@ pub fn compose_with_framing(
|
||||
framing: &Framing,
|
||||
output: ColourSpace,
|
||||
) -> ComposedShader {
|
||||
compose_full(ops, framing, output, &MaskStack::new())
|
||||
compose_full(
|
||||
ops,
|
||||
framing,
|
||||
output,
|
||||
&MaskStack::new(),
|
||||
&crate::spot::SpotSet::new(),
|
||||
)
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3
|
||||
@@ -487,6 +493,7 @@ pub fn compose_full(
|
||||
framing: &Framing,
|
||||
output: ColourSpace,
|
||||
masks: &MaskStack,
|
||||
spots: &crate::spot::SpotSet,
|
||||
) -> ComposedShader {
|
||||
// Active *point* operations. A neighbourhood operation is filtered out
|
||||
// here rather than asked for a fragment it cannot write: it reads pixels
|
||||
@@ -506,11 +513,18 @@ pub fn compose_full(
|
||||
// than from a flag the caller sets, because a caller that got the flag
|
||||
// wrong would produce a shader whose storage format does not match the
|
||||
// texture bound to it.
|
||||
let output_mode = if ops.iter().any(|o| o.is_active() && o.detail().is_some()) {
|
||||
OutputMode::LinearWorking
|
||||
} else {
|
||||
OutputMode::Encoded
|
||||
};
|
||||
//
|
||||
// TRACES: FR-DEV-8
|
||||
// The repairs count too, and they are the reason this takes a spot set at
|
||||
// all: a photograph with a spot on it and no sharpening still has a detail
|
||||
// stage, and a fused pass that encoded its own output there would quantise
|
||||
// twice and be bound to a texture of the wrong format.
|
||||
let output_mode =
|
||||
if ops.iter().any(|o| o.is_active() && o.detail().is_some()) || !spots.is_neutral() {
|
||||
OutputMode::LinearWorking
|
||||
} else {
|
||||
OutputMode::Encoded
|
||||
};
|
||||
|
||||
// Whether an operation has taken over the rendering. Decided from the
|
||||
// operations for the same reason `output_mode` is: a caller that got it
|
||||
|
||||
Reference in New Issue
Block a user