Drop a detail pass that changes nothing when it sits between two others

Capture sharpening at a scale too coarse to draw its radius emits one pass
with an empty body (`nothing_to_sharpen`), so that a chain still ends in
something that performs the output transform. At fit on any modern sensor
that is most of the time. When another neighbourhood operation follows it
- dehaze, clarity, texture - the pass is not last and does nothing: it
reads the rgba16float intermediate and writes the same texels to the other
one. It still cost a full render-sized read and write every frame:

  scene                           before     after
  sharpen+clarity 2560x1600 fit   18.66 ms   14.16 ms
  sharpen+clarity 3840x2160 fit   37.93 ms   27.88 ms
  every operation 2560x1600 fit   71.80 ms   67.24 ms
  clarity alone   2560x1600 fit   14.23 ms   14.20 ms  (control)
  sharpen+clarity 2560x1600 1:1   23.01 ms   22.97 ms  (control: resolves)

(Laptop RTX 3050 held at 420/810 MHz by its power cap, synthetic 60 MP
source, median of five alternated runs of forty frames each.)

`compose_detail_with` now drops such a pass where dropping it is exact:
not the last pass, whose output transform would otherwise move onto the
previous pass's f32 result and round differently; and not a pass right
after a reduced one, because a full-resolution pass is what closes the
reduced chain for the operation after it. `DetailPass::is_identity` says
what "changes nothing" means: full size, nothing bound at binding 3, and a
body with no code in it.

The rgba8 output is bit-identical: every scene above hashed the same
before and after, and the sharpen+clarity frame hashes the same as clarity
on its own, which is the claim in one line. A new dr-pipeline test pins
the three cases - dropped ahead of another operation, kept when last, kept
when alone.
This commit is contained in:
2026-09-26 07:10:41 -04:00
parent 1dc7b45cfe
commit d430ec9528
2 changed files with 107 additions and 5 deletions
+102
View File
@@ -431,6 +431,24 @@ pub struct DetailPass {
pub storage: Vec<[f32; 4]>, pub storage: Vec<[f32; 4]>,
} }
impl DetailPass {
/// Whether this pass hands on exactly what it was given: a full-size pass
/// with nothing to bind and a body with no code in it, only comments.
///
/// The composer drops such a pass where that is exact (see
/// [`compose_detail_with`]); the operation still emits it, because whether
/// dropping it is exact depends on what is around it in the chain.
pub fn is_identity(&self) -> bool {
self.output_scale <= 1
&& self.storage.is_empty()
&& self
.wgsl
.lines()
.map(|l| l.split("//").next().unwrap_or("").trim())
.all(str::is_empty)
}
}
/// TRACES: FR-DEV-3 | FR-DEV-8 /// TRACES: FR-DEV-3 | FR-DEV-8
/// An operation that reads pixels other than the one it is writing. /// An operation that reads pixels other than the one it is writing.
/// ///
@@ -646,6 +664,40 @@ pub fn compose_detail_with(
}; };
} }
// TRACES: NFR-P5
// A pass whose body is empty changes nothing but where the pixels are: it
// reads the intermediate and writes the same values to the other one.
// Capture sharpening emits exactly that at a scale too coarse to draw its
// radius (`nothing_to_sharpen`), and at fit on any modern sensor that is
// most of the time — so an edit with sharpening *and* another kernel paid
// a full render-sized read and write for it on every frame, 4.4 ms of a
// 2560 x 1600 frame on the reference laptop with its clocks held down.
//
// Dropped here, where the chain is still a list, and only where dropping
// it is exact:
//
// - **Not the last pass.** The last pass performs the output transform on
// what it read from an `rgba16float` intermediate. Moving that transform
// onto the pass before would apply it to that pass's `f32` result
// instead, which is a different rounding of the same picture.
// - **Not after a reduced pass.** A full-resolution pass ends the reduced
// chain (see `DetailRunner::encode`), so one that follows a scaled pass
// is what stops the next operation reading the last one's base. None of
// today's operations leave a reduced chain open, but a declared one may.
//
// Everywhere else the pass before and the pass after exchange the same
// `rgba16float` texels either way, `aux` included.
let mut kept: Vec<(&str, &[Helper], DetailPass, usize)> = Vec::with_capacity(planned.len());
let total = planned.len();
for (position, entry) in planned.into_iter().enumerate() {
let after_full = kept.last().is_none_or(|(_, _, p, _)| p.output_scale <= 1);
let droppable = position + 1 < total && after_full && entry.2.is_identity();
if !droppable {
kept.push(entry);
}
}
let planned = kept;
let last = planned.len().saturating_sub(1); let last = planned.len().saturating_sub(1);
let passes = planned let passes = planned
.into_iter() .into_iter()
@@ -1211,6 +1263,56 @@ mod tests {
assert_eq!(fused(&ops).output_mode, OutputMode::LinearWorking); assert_eq!(fused(&ops).output_mode, OutputMode::LinearWorking);
} }
#[test]
fn a_pass_that_changes_nothing_is_dropped_where_that_is_exact() {
// TRACES: NFR-P5
// Capture sharpening at a scale too coarse to draw its radius emits a
// pass with an empty body. Between two other passes it costs a
// render-sized read and write and changes no texel, so it goes; as the
// last pass it performs the output transform on the intermediate, and
// moving that onto the pass before would round differently, so it
// stays.
use crate::ops::{capture_sharpen, CaptureSharpen, NoiseReduction};
let sharpen = || -> Box<dyn Operation> {
let mut op = CaptureSharpen::new();
op.set_param(capture_sharpen::AMOUNT, 60.0);
Box::new(op)
};
let chroma = || -> Box<dyn Operation> { Box::new(NoiseReduction::with_amounts(0.0, 60.0)) };
// A 24 MP frame fitted to a panel: a one-source-pixel radius is a
// quarter of a render pixel.
let scale = RenderScale::new((1500, 1000), (6000, 4000));
let unresolved = sharpen().detail().expect("a detail stage").passes(scale);
assert!(
unresolved.len() == 1 && unresolved[0].is_identity(),
"the premise: sharpening at this scale is one pass that does nothing"
);
let labels = |ops: &[Box<dyn Operation>]| -> Vec<String> {
compose_detail(ops, scale, dr_types::ColourSpace::Srgb)
.passes
.iter()
.map(|p| p.label.clone())
.collect()
};
// First, ahead of the chroma passes: dropped.
let first = labels(&[sharpen(), chroma()]);
assert_eq!(
first,
[
"noise_reduction/chroma-horizontal",
"noise_reduction/chroma-vertical"
]
);
// Last, after them: kept, and it is the pass that encodes.
let last = labels(&[chroma(), sharpen()]);
assert_eq!(last.len(), 3);
assert_eq!(last[2], "capture_sharpen/unresolved");
// Alone: kept, because the fused pass stopped short and something has
// to finish the frame.
assert_eq!(labels(&[sharpen()]), ["capture_sharpen/unresolved"]);
}
#[test] #[test]
fn a_pass_can_hand_a_scalar_to_the_next_one_alongside_the_colour() { fn a_pass_can_hand_a_scalar_to_the_next_one_alongside_the_colour() {
// What makes an unsharp mask — sharpening, clarity, texture, dehaze — // What makes an unsharp mask — sharpening, clarity, texture, dehaze —
File diff suppressed because one or more lines are too long