Teach the whole-chain shader test about neighbourhood operations

`the_whole_chain_at_once_compiles` counted one `---- ` block per
operation in the chain. That was true while every operation was a point
operation, and stopped being true the moment a neighbourhood one existed:
clarity and texture are active in that test, and still emit no fused
block, because `compose_full` filters them out and the detail stage
dispatches them separately.

Counted now by asking each operation whether it has a detail stage --
the same question the composer's own filter asks -- rather than by
subtracting a number someone has to remember to update. Capture
sharpening and noise reduction are covered by this without another edit.

The render at the end is now `render_detailed`, which is not a
concession but the stronger test: with a neighbourhood operation active
the fused pass hands on linear working values and the last detail pass
performs the output transform, so rendering the fused half alone is the
mismatch `render_detailed` exists to reject -- and the detail passes are
generated WGSL with uniform blocks of their own, which is exactly what
"everything at once" is here to collide. It renders at 512 rather than
32 because a compositional radius is a fraction of the frame, and on a
32-pixel target every detail kernel rounds away to nothing.

Also records, in `texture_contributes_nothing_where_its_scale_does_not_exist`,
the seam this uncovered: an active detail operation whose kernel rounds
away composes an empty chain while the fused pass has already been
composed to hand on linear values, and nothing can then encode the
result. That test now asserts the property on the composed chain instead
of driving the unrenderable configuration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 19:16:52 +02:00
co-authored by Claude Opus 5
parent a9baebc396
commit 88ce89428b
2 changed files with 86 additions and 12 deletions
+33 -6
View File
@@ -547,18 +547,45 @@ fn texture_contributes_nothing_where_its_scale_does_not_exist() {
let source = step_edge(&ctx, 512, [1.0, 1.0, 1.0]);
let mut graph = graph_with(TEXTURE, 100.0);
// Asserted on the composed chain rather than on a dispatch counter,
// because texture *alone* at this size is a configuration the stage as a
// whole cannot currently render, and that is a gap in the seam rather than
// in this operation.
//
// `compose_full` decides whether the fused pass should hand on linear
// working values from `is_active()`, which has no `RenderScale` to consult;
// `compose_detail` decides what to dispatch from the kernel it can actually
// draw at this scale. Almost always the two agree. They disagree exactly
// when a detail operation is active and its kernel rounds away, and then
// `render_detailed` finds an empty chain, falls through to `render_masked`,
// and is rejected for handing a linear-working shader to the plain path.
//
// Pre-existing, and not something clarity and texture introduce:
// `DetailStage::passes` documents the empty return as *the honest answer
// for an acutance operation on a heavy proxy*, so capture sharpening and
// noise reduction reach it by the same road. Fixing it means composing
// both halves of a render together, so the fused half can know whether a
// detail half survived the scale — a change at the composition boundary,
// not in this file.
let scale = graph.render_scale(source.size(), (128, 128));
assert!(
graph.compose_detail_for(scale, ColourSpace::Srgb).is_empty(),
"texture claimed a kernel it cannot draw"
);
// With clarity on as well the edit is renderable again, and the dispatch
// count says what the assertion above says: two passes, not four. Texture
// is active, and contributes nothing.
graph.set_param(CLARITY, AMOUNT, 100.0);
let mut pass = AdjustPass::new(&ctx);
render(&mut pass, &graph, &source, 128);
assert_eq!(
pass.detail_dispatches(),
0,
"texture claimed a kernel it cannot draw"
2,
"clarity survives a thumbnail, and texture added nothing beside it"
);
graph.set_param(CLARITY, AMOUNT, 100.0);
render(&mut pass, &graph, &source, 128);
assert_eq!(pass.detail_dispatches(), 2, "clarity survives a thumbnail");
// And texture comes back, exactly, as soon as the view is large enough to
// hold it — no separate path, no fade, just the kernel resolving again.
let mut zoomed = AdjustPass::new(&ctx);