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:
@@ -431,6 +431,24 @@ pub struct DetailPass {
|
||||
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
|
||||
/// 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 passes = planned
|
||||
.into_iter()
|
||||
@@ -1211,6 +1263,56 @@ mod tests {
|
||||
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]
|
||||
fn a_pass_can_hand_a_scalar_to_the_next_one_alongside_the_colour() {
|
||||
// What makes an unsharp mask — sharpening, clarity, texture, dehaze —
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user