From 2459a759afa782fa32d997d96e78ec7f87e8af6c Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 22 Aug 2026 19:18:56 +0200 Subject: [PATCH] Compile the detail stage in the whole-chain GPU tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit every_operation_generates_compilable_wgsl and the_whole_chain_at_once_compiles both rendered through the fused half only. A neighbourhood operation contributes no fused fragment, so its kernels went uncompiled — and once one is active the fused pass hands on linear working values, which plain render refuses. Both now compose both halves from the one graph, and the fused block count excludes the operations the detail chain names. Co-Authored-By: Claude Opus 5 (1M context) --- core/dr-gpu/src/adjust.rs | 71 ++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/core/dr-gpu/src/adjust.rs b/core/dr-gpu/src/adjust.rs index df7c9d1..f9a261a 100644 --- a/core/dr-gpu/src/adjust.rs +++ b/core/dr-gpu/src/adjust.rs @@ -1063,12 +1063,32 @@ mod tests { let mut g = EditGraph::default_chain(); g.set_param(cap.id, p.id, value); let shader = g.compose(); - pass.render(&img, &shader, 16, 16).unwrap_or_else(|e| { - panic!( - "{}.{} at {value} generated invalid WGSL:\n{e}", - cap.id, p.id - ) - }); + + // Through the detail stage rather than through `render`, + // because a neighbourhood operation contributes no fused + // fragment: its WGSL is generated per resolution and lives + // in dispatches of its own. Compiling only the fused half + // would leave every kernel in the chain untested here — + // and worse, `render` refuses a shader composed to hand on + // linear working values, so the omission would arrive as + // "invalid WGSL" against a shader that is perfectly valid. + // + // The scale comes from the graph, so the kernel really is + // converted the way a render converts it. The image is + // 16×16 and so is the target, which puts the ratio at 1.0 + // and keeps an acutance operation from declining to draw + // (`RenderScale::resolves`) and compiling its pass-through + // instead of the kernel this test exists to check. + let scale = g.render_scale(img.size(), (16, 16)); + let detail = g.compose_detail(scale); + let key = g.invalidation().through(dr_pipeline::Affects::Colour); + pass.render_detailed(&img, &shader, 16, 16, None, &detail, key) + .unwrap_or_else(|e| { + panic!( + "{}.{} at {value} generated invalid WGSL:\n{e}", + cap.id, p.id + ) + }); } } } @@ -1423,23 +1443,44 @@ mod tests { } let shader = g.compose(); + + // Cropped, so the render is against an output size that is not the + // source size — the case where a wrong dispatch or a wrong texture + // allocation would show up. + let (w, h) = g.output_size(32, 32); + let scale = g.render_scale(img.size(), (w, h)); + let detail = g.compose_detail(scale); + + // A neighbourhood operation is active and yet emits no fused block: it + // reads pixels it is not writing, so it is a dispatch of its own. The + // ones that are come from the detail chain rather than from a list + // here, which keeps the count exact as sharpening, noise reduction and + // clarity arrive instead of loosening it to an inequality. + let neighbourhood: std::collections::BTreeSet<&str> = detail + .passes + .iter() + .map(|p| p.label.split('/').next().expect("/")) + .collect(); + assert_eq!( shader.source.matches("---- ").count(), - // Every operation, plus framing — which emits a stage of its own - // rather than an operation block, and is not in `descriptors`. - g.descriptors().len() + 1, - "every operation and the framing should be active" + // Every fusable operation, plus framing — which emits a stage of + // its own rather than an operation block, and is not in + // `descriptors` — less the ones that run after this shader. + g.descriptors().len() + 1 - neighbourhood.len(), + "every fusable operation and the framing should be active" ); assert!( shader.source.contains("---- framing ----"), "framing must reach the shader alongside the colour operations" ); - // Cropped, so the render is against an output size that is not the - // source size — the case where a wrong dispatch or a wrong texture - // allocation would show up. - let (w, h) = g.output_size(32, 32); - pass.render(&img, &shader, w, h) + // Both halves, from the one graph: with a detail stage present the + // fused pass stops at linear working values and the last detail pass + // performs the output transform, so rendering only the first half is + // not a smaller test — it is a texture format the driver rejects. + let key = g.invalidation().through(dr_pipeline::Affects::Colour); + pass.render_detailed(&img, &shader, w, h, None, &detail, key) .expect("the full chain must compile"); }