Let a detail pass carry a list, not only a kernel
Every neighbourhood pass so far has been a convolution, whose whole description fits in the uniform block because its structure fixes how many numbers it needs. Spot removal is not that shape: sixty-four repairs and one repair are the same shader with a different buffer behind it. So a pass may declare `storage`, which arrives at binding 3 as `array<vec4<f32>>` with `arrayLength` in scope. The alternative — packing the list into uniforms — needs a fixed maximum paid for on every frame, a composer that can emit vec4 fields because a uniform array's stride is 16 whatever it holds, and it gives the next operation that wants a table nothing to build on. The property worth having is what stays out of the generated source: the count is in the buffer, so placing the tenth spot uploads 512 bytes and reuses the compiled pipeline, exactly as moving a slider does for the fused pass. `changing_the_list_does_not_recompile` is that, asserted. One bind group entry rather than two more layouts, and one placeholder buffer allocated in `new` rather than sixteen bytes per pass per frame — a zero-length storage buffer cannot be bound, and per-frame allocation is what this module's documentation exists to refuse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -359,6 +359,29 @@ pub struct DetailPass {
|
||||
|
||||
/// Uniform values this pass's body reads.
|
||||
pub uniforms: Vec<Uniform>,
|
||||
|
||||
/// TRACES: FR-DEV-8
|
||||
/// Per-instance data, for a pass whose work is a *list* rather than a
|
||||
/// kernel.
|
||||
///
|
||||
/// Reaches the body as `instances: array<vec4<f32>>`, with
|
||||
/// `instance_count` in scope as a `u32`. Empty for every pass that is a
|
||||
/// convolution, which is every pass that existed before spot removal.
|
||||
///
|
||||
/// # Why not the uniform block
|
||||
///
|
||||
/// Because the uniform block is fixed by the pass's *structure*, and this
|
||||
/// is not: sixty-four repairs and one repair are the same shader with a
|
||||
/// different buffer behind it. Packing the list into uniforms would need a
|
||||
/// fixed maximum, paid for on every frame whether the photograph carries
|
||||
/// one spot or none, and it would need the composer to emit `vec4` fields —
|
||||
/// a WGSL uniform array has a stride of 16 whatever it holds.
|
||||
///
|
||||
/// The property that matters more: with the list in storage the generated
|
||||
/// source does not mention how many there are, so placing the tenth spot
|
||||
/// uploads 512 bytes and reuses the compiled pipeline, exactly as moving a
|
||||
/// slider does for the fused pass.
|
||||
pub storage: Vec<[f32; 4]>,
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3 | FR-DEV-8
|
||||
@@ -396,6 +419,9 @@ pub struct ComposedDetailPass {
|
||||
pub source: String,
|
||||
/// Uniform values in the order the generated struct declares them.
|
||||
pub uniforms: Vec<f32>,
|
||||
/// TRACES: FR-DEV-8
|
||||
/// The instance list, if this pass declared one. See [`DetailPass::storage`].
|
||||
pub storage: Vec<[f32; 4]>,
|
||||
/// See [`DetailPass::radius`].
|
||||
pub radius: u32,
|
||||
/// Whether this pass writes the display/export texture rather than another
|
||||
@@ -509,6 +535,7 @@ pub fn compose_detail(
|
||||
radius: 0,
|
||||
wgsl: String::new(),
|
||||
uniforms: Vec::new(),
|
||||
storage: Vec::new(),
|
||||
},
|
||||
0,
|
||||
scale,
|
||||
@@ -651,6 +678,10 @@ struct Params {{
|
||||
@group(0) @binding(0) var source: texture_2d<f32>;
|
||||
@group(0) @binding(1) var<uniform> u: Params;
|
||||
@group(0) @binding(2) var output: texture_storage_2d<{store_format}, write>;
|
||||
// A pass whose work is a list rather than a kernel reads it here; every other
|
||||
// pass leaves this bound to a single empty element and never looks at it. See
|
||||
// `DetailPass::storage` for why the list is not in the uniform block.
|
||||
@group(0) @binding(3) var<storage, read> instances: array<vec4<f32>>;
|
||||
|
||||
// A neighbour, clamped to the edge of the image.
|
||||
//
|
||||
@@ -681,6 +712,10 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
|
||||
// What this render is, relative to the export it has to match.
|
||||
let render_dims = u.detail_base.xy;
|
||||
let render_scale = u.detail_base.z;
|
||||
// How many entries `instances` actually holds, read from the buffer itself
|
||||
// rather than from a uniform so the two cannot disagree. A pass that
|
||||
// declared no list is bound to a one-element placeholder and never asks.
|
||||
let instance_count = arrayLength(&instances);
|
||||
|
||||
var c = tap(coord, vec2<i32>(0));
|
||||
// One scalar per pixel that survives the hand-off from one pass to the
|
||||
@@ -708,6 +743,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
|
||||
label,
|
||||
source,
|
||||
uniforms: uniform_values,
|
||||
storage: pass.storage.clone(),
|
||||
radius: pass.radius,
|
||||
writes_output,
|
||||
structure_hash,
|
||||
|
||||
@@ -141,6 +141,8 @@ impl DetailStage for BoxBlur {
|
||||
.map(|(axis, _)| DetailPass {
|
||||
label: if axis == 0 { "horizontal" } else { "vertical" },
|
||||
radius: r,
|
||||
// A convolution, not a list: nothing to bind at binding 3.
|
||||
storage: Vec::new(),
|
||||
uniforms: vec![
|
||||
Uniform {
|
||||
name: "radius",
|
||||
|
||||
@@ -358,6 +358,8 @@ impl DetailStage for CaptureSharpen {
|
||||
.map(|label| DetailPass {
|
||||
label,
|
||||
radius: extent,
|
||||
// A convolution, not a list: nothing to bind at binding 3.
|
||||
storage: Vec::new(),
|
||||
uniforms: vec![
|
||||
Uniform {
|
||||
// −100…100 as a gain around zero. A hundred percent is
|
||||
@@ -410,6 +412,8 @@ fn nothing_to_sharpen() -> DetailPass {
|
||||
label: "unresolved",
|
||||
// Reads only the pixel it writes, so a tile needs no halo at all.
|
||||
radius: 0,
|
||||
// A convolution, not a list: nothing to bind at binding 3.
|
||||
storage: Vec::new(),
|
||||
uniforms: Vec::new(),
|
||||
wgsl: "// The chosen radius is finer than one pixel of this render, so the detail
|
||||
// it would act on is not in this texture — it was lost to the downscale
|
||||
|
||||
@@ -475,12 +475,16 @@ impl<B: Band> DetailStage for LocalContrast<B> {
|
||||
DetailPass {
|
||||
label: "base",
|
||||
radius,
|
||||
// A convolution, not a list: nothing to bind at binding 3.
|
||||
storage: Vec::new(),
|
||||
uniforms: shape,
|
||||
wgsl: BASE_X.to_string(),
|
||||
},
|
||||
DetailPass {
|
||||
label: "combine",
|
||||
radius,
|
||||
// A convolution, not a list: nothing to bind at binding 3.
|
||||
storage: Vec::new(),
|
||||
uniforms: combine,
|
||||
wgsl: combine_body(B::RECIPE.midtone_taper),
|
||||
},
|
||||
|
||||
@@ -436,6 +436,8 @@ impl DetailStage for NoiseReduction {
|
||||
passes.push(DetailPass {
|
||||
label: "luminance",
|
||||
radius: luma,
|
||||
// A convolution, not a list: nothing to bind at binding 3.
|
||||
storage: Vec::new(),
|
||||
uniforms: vec![
|
||||
Uniform {
|
||||
name: "radius",
|
||||
@@ -474,6 +476,8 @@ impl DetailStage for NoiseReduction {
|
||||
"chroma-vertical"
|
||||
},
|
||||
radius: chroma,
|
||||
// A convolution, not a list: nothing to bind at binding 3.
|
||||
storage: Vec::new(),
|
||||
uniforms: vec![
|
||||
Uniform {
|
||||
name: "radius",
|
||||
|
||||
Reference in New Issue
Block a user