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"); }