diff --git a/core/dr-pipeline/src/ops/dehaze.rs b/core/dr-pipeline/src/ops/dehaze.rs index cfa0032..711730b 100644 --- a/core/dr-pipeline/src/ops/dehaze.rs +++ b/core/dr-pipeline/src/ops/dehaze.rs @@ -116,12 +116,34 @@ //! mottling across smooth gradients. This decomposition samples nothing — it //! evaluates the exact minimum over every pixel of the window, in two steps. //! +//! # Why it is now two passes and not five +//! +//! The decomposition was run as four erosion passes — run and span along x, +//! then along y — and a fifth for the recovery. On the reference laptop the +//! taps turned out not to be what a pass costs: with the memory clock held at +//! 810 MHz by the power cap, a pass that only reads the render-sized +//! `rgba16float` intermediate and writes the other one costs about 4 ms at +//! 2560 x 1600, and dehaze's five came to 22 ms, of which the taps were about +//! 2. So each axis is now one pass that takes the minimum over the whole +//! window directly — 36 texture reads per pixel at that size instead of 12, +//! nearly all of them served by the cache — and the recovery rides in the y +//! pass, which already has the veil and this pixel's colour in hand. Two +//! passes: 9.2 ms. +//! +//! It is the same picture, bit for bit. A minimum is exact in any order, so +//! the minimum over the window's pixels is one value however it is grouped, +//! and the window is the one [`Split`] always covered, surplus pixel +//! included. The veil reaching the recovery was always exactly representable +//! in the `rgba16float` lane it crossed — a minimum of channel values that +//! were themselves read from `rgba16float` — so no rounding was lost by not +//! storing it between passes. +//! //! It is also why this operation does not use the reduced chain that TD-4 gave //! clarity. The runner holds one reduced buffer, so every scaled pass in a //! chain must declare the same `output_scale`; clarity's steps down with the //! viewport, so a second operation choosing its own would disagree with it at -//! some window sizes and not others. Four cheap full-resolution passes cost -//! less than that coupling, and the decomposition is what makes them cheap. +//! some window sizes and not others. Two full-resolution passes cost less than +//! that coupling. //! //! # The artefact this does not fix //! @@ -275,22 +297,23 @@ impl Dehaze { /// /// See the module documentation: eroding by a contiguous run and then by a set /// of points spaced one run apart erodes by the sum of the two, which is the -/// whole window. This is the arithmetic of that split, in one place, because -/// both axes need it and a second copy is a second chance to get the centring -/// wrong. +/// whole window. The passes no longer run the two stages apart (see "Why it is +/// now two passes"), but the window they read is still the one this split +/// covers — [`Self::first`] and [`Self::width`] — surplus pixel included, +/// because that is the window every edit made so far was tuned against. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Split { /// Length of the contiguous run the first pass takes the minimum over. pub run: u32, /// How many runs the second pass chains together, spaced `run` apart. pub span: u32, - /// What the second pass subtracts from its offsets to centre the window. + /// How far before the pixel being written the window starts. /// /// The composite covers `run * span` pixels, which is at least the window /// asked for and can be one or two more; the surplus falls on the far side - /// rather than being trimmed, because trimming it would need a third pass - /// and a patch a pixel wider on one side is not a visible difference in a - /// field this smooth. + /// rather than being trimmed, because trimming it would have needed a + /// third pass and a patch a pixel wider on one side is not a visible + /// difference in a field this smooth. pub shift: i32, } @@ -311,14 +334,25 @@ impl Split { } } - /// The furthest the second pass reads, in pixels. + /// The window's first offset from the pixel being written: `-shift`. + pub fn first(&self) -> i32 { + -self.shift + } + + /// How many pixels the window covers, `run * span` — the patch asked for + /// and the one or two surplus pixels on the far side the split leaves. + pub fn width(&self) -> u32 { + self.run * self.span + } + + /// The furthest the window reads from the pixel being written, in pixels. /// - /// Stated rather than assumed symmetric: the composite window is centred - /// to within a pixel and not exactly, so the two directions can differ by - /// one. An understated radius is a seam at every tile boundary (ARCH - /// §5.3), which is the kind of artefact that looks like a driver bug. - pub fn reach(&self) -> u32 { - let far = (self.span.saturating_sub(1) * self.run) as i32 - self.shift; + /// Stated rather than assumed symmetric: the window is centred to within a + /// pixel and not exactly, so the two directions can differ by one. An + /// understated radius is a seam at every tile boundary (ARCH §5.3), which + /// is the kind of artefact that looks like a driver bug. + pub fn extent(&self) -> u32 { + let far = self.width() as i32 - 1 - self.shift; self.shift.max(far).max(0) as u32 } } @@ -367,83 +401,50 @@ impl DetailStage for Dehaze { fn passes(&self, scale: RenderScale) -> Vec { let split = Split::of(self.patch(scale)); - // The run stage's uniforms are the same on both axes, and so are the - // span stage's. Only the offset expression differs, which is what - // `erode_run` and `erode_span` take as an argument — each filter + // Both axes erode over the same window; only the offset expression + // differs, which is what `erode` takes as an argument — one filter // written once, so the two axes cannot drift into being different // filters. - let run = vec![Uniform { - name: "run", - value: split.run as f32, - }]; - let span = vec![ + let window = vec![ Uniform { - name: "span", - value: split.span as f32, + name: "first", + value: split.first() as f32, }, Uniform { - name: "stride", - value: split.run as f32, - }, - Uniform { - name: "shift", - value: split.shift as f32, + name: "width", + value: split.width() as f32, }, ]; + let mut recovery = window.clone(); + recovery.extend([ + Uniform { + name: "omega", + value: self.omega(), + }, + Uniform { + name: "min_transmission", + value: MIN_TRANSMISSION, + }, + ]); vec![ DetailPass { output_scale: 1, - label: "veil-run-x", - // The run starts at this pixel and walks forward, so it reads - // `run - 1` beyond itself and nothing behind. - radius: split.run.saturating_sub(1), + label: "veil-x", + radius: split.extent(), storage: Vec::new(), - uniforms: run.clone(), - wgsl: erode_run(Axis::X), + uniforms: window, + wgsl: erode(Axis::X), }, DetailPass { output_scale: 1, - label: "veil-span-x", - radius: split.reach(), + label: "veil-y-clear", + radius: split.extent(), storage: Vec::new(), - uniforms: span.clone(), - wgsl: erode_span(Axis::X), - }, - DetailPass { - output_scale: 1, - label: "veil-run-y", - radius: split.run.saturating_sub(1), - storage: Vec::new(), - uniforms: run, - wgsl: erode_run(Axis::Y), - }, - DetailPass { - output_scale: 1, - label: "veil-span-y", - radius: split.reach(), - storage: Vec::new(), - uniforms: span, - wgsl: erode_span(Axis::Y), - }, - DetailPass { - output_scale: 1, - label: "clear", - // Reads only the pixel it writes: the veil arrived in the - // scratch lane four passes ago. - radius: 0, - storage: Vec::new(), - uniforms: vec![ - Uniform { - name: "omega", - value: self.omega(), - }, - Uniform { - name: "min_transmission", - value: MIN_TRANSMISSION, - }, - ], - wgsl: CLEAR.to_string(), + uniforms: recovery, + // The erosion in a block of its own, so its locals do not + // collide with the recovery's. + wgsl: format!("{{\n{}\n}}\n\n{CLEAR}", erode(Axis::Y)), }, ] } @@ -466,33 +467,33 @@ impl Axis { } } -/// The first stage: the minimum over a contiguous run. +/// The minimum over the window along one axis. /// /// Along x it reads the colour and reduces it to the dark channel; along y the -/// dark channel is already in the scratch lane, so it reads that instead. -/// Doing the channel minimum again on the second axis would be reducing a -/// scalar and would quietly discard the x erosion. +/// dark channel's x minimum is already in the scratch lane, so it reads that +/// instead. Doing the channel minimum again on the second axis would be +/// reducing a scalar and would quietly discard the x erosion. /// -/// Neither stage touches `c`. The recovery needs the original colour *and* the -/// veil in the same place at the same time, and the ping-pong hands each pass -/// only what the pass before it wrote — so the veil travels in `aux` and the -/// colour rides through untouched. See [`DetailPass::wgsl`]. -fn erode_run(axis: Axis) -> String { +/// The x pass does not touch `c`. The recovery needs the original colour *and* +/// the veil in the same place at the same time, and the ping-pong hands each +/// pass only what the pass before it wrote — so the veil travels in `aux` and +/// the colour rides through untouched. See [`DetailPass::wgsl`]. +fn erode(axis: Axis) -> String { let source = match axis { Axis::X => "dark_channel(tap(coord, OFFSET))", Axis::Y => "tap_aux(coord, OFFSET)", }; - let first = source.replace("OFFSET", &axis.offset("0")); - let rest = source.replace("OFFSET", &axis.offset("i")); + let head = source.replace("OFFSET", &axis.offset("o")); + let rest = source.replace("OFFSET", &axis.offset("o + i")); format!( "\ -// Half of the erosion's first stage: the minimum over `run` contiguous pixels, -// walking forward from this one. The second stage chains these together, and -// the two structuring elements add up to the whole patch — which is why this -// one is not centred and does not need to be. -let n = i32(run); -var veil = {first}; +// The minimum over every pixel of the window along this axis, from `first` +// for `width` pixels. A minimum is exact in any order, so this is the same +// value, bit for bit, as chaining a run and a span over the same pixels. +let o = i32(first); +let n = i32(width); +var veil = {head}; for (var i = 1; i < n; i = i + 1) {{ veil = min(veil, {rest}); }} @@ -500,38 +501,10 @@ aux = veil;" ) } -/// The second stage: the minimum over `span` points spaced `stride` apart. -/// -/// Each of those points already holds the minimum over the run that starts -/// there, so this reads the whole window while touching `span` pixels of it. -/// `shift` is what centres the composite on the pixel being written; without -/// it the veil would be measured from a patch lying entirely to one side, and -/// the correction would appear to lag the picture by half a patch. -fn erode_span(axis: Axis) -> String { - let offset = axis.offset("j * s - o"); - let first = axis.offset("-o"); - - format!( - "\ -// The erosion's second stage. `span` taps, spaced a whole run apart, each -// standing for the run that begins at it — so the minimum over the patch costs -// `run + span` taps rather than the `run * span` pixels it covers, and it is -// the exact minimum over all of them rather than a sample of them. -let n = i32(span); -let s = i32(stride); -let o = i32(shift); -var veil = tap_aux(coord, {first}); -for (var j = 1; j < n; j = j + 1) {{ - veil = min(veil, tap_aux(coord, {offset})); -}} -aux = veil;" - ) -} - /// The recovery: invert the scattering model with the transmission the erosion /// implies. const CLEAR: &str = "\ -// The veil the four erosion passes measured: the smallest channel anywhere in +// The veil the two erosions measured: the smallest channel anywhere in // the patch around this pixel, which the dark-channel prior reads as the // airlight that has been composited over the scene here. // @@ -581,7 +554,7 @@ mod tests { #[test] fn dehaze_starts_neutral_and_costs_nothing() { // The rule the whole pipeline rests on. An unedited photograph must not - // pay for a slider nobody has touched — and this one is five dispatches + // pay for a slider nobody has touched — and this one is two dispatches // when it is on, so "nothing" here is a worthwhile amount of nothing. assert!(!Dehaze::new().is_active()); assert!(composed(0.0, RenderScale::full((2000, 1500))).is_empty()); @@ -623,7 +596,7 @@ mod tests { // develop view about it would read as a bug. let tiny = RenderScale::full((48, 32)); assert_eq!(Dehaze::with_amount(50.0).patch(tiny), 1); - assert_eq!(composed(50.0, tiny).len(), 5); + assert_eq!(composed(50.0, tiny).len(), 2); } #[test] @@ -648,7 +621,8 @@ mod tests { // Centred to within the pixel the odd surplus leaves over: the window // covers [-31, 32] around the pixel being written. assert_eq!(split.shift, 31); - assert_eq!(split.reach(), 31); + assert_eq!((split.first(), split.width()), (-31, 64)); + assert_eq!(split.extent(), 32); } #[test] @@ -662,33 +636,32 @@ mod tests { assert_eq!(split.span, 2); assert!(split.run * split.span >= 3, "the window is not covered"); assert_eq!(split.shift, 1); - assert_eq!(split.reach(), 1); + // [-1, 2]: the surplus pixel is on the far side. + assert_eq!((split.first(), split.width()), (-1, 4)); + assert_eq!(split.extent(), 2); } #[test] - fn the_chain_is_four_erosions_and_a_recovery() { + fn the_chain_is_an_erosion_per_axis_the_second_carrying_the_recovery() { // The shape of the operation, asserted where it is cheap to assert. - // The erosions leave the colour alone and hand the veil forward in the - // scratch lane; only the last pass touches `c`, which is what makes an + // The x erosion leaves the colour alone and hands the veil forward in + // the scratch lane; only the y pass touches `c`, which is what makes an // unsharp-mask-shaped operation expressible in a chain that hands each - // pass exactly one texture. + // pass exactly one texture. Two passes and not five: each extra pass + // is a render-sized read and write, which is what a pass costs (see + // "Why it is now two passes"). let composed = composed(60.0, RenderScale::full((2000, 1500))); let labels: Vec<&str> = composed.passes.iter().map(|p| p.label.as_str()).collect(); - assert_eq!( - labels, - [ - "dehaze/veil-run-x", - "dehaze/veil-span-x", - "dehaze/veil-run-y", - "dehaze/veil-span-y", - "dehaze/clear", - ] - ); + assert_eq!(labels, ["dehaze/veil-x", "dehaze/veil-y-clear"]); + + // Both declare the whole window they read. + let split = Split::of(Dehaze::with_amount(60.0).patch(RenderScale::full((2000, 1500)))); + assert!(composed.passes.iter().all(|p| p.radius == split.extent())); // Only the last writes the display texture, so the output transform // happens exactly once (FR-DEV-2). - assert!(composed.passes[..4].iter().all(|p| !p.writes_output)); - assert!(composed.passes[4].writes_output); + assert!(!composed.passes[0].writes_output); + assert!(composed.passes[1].writes_output); // Nothing here uses the reduced chain — see the module documentation // for why a second operation cannot pick its own `output_scale` while @@ -730,7 +703,8 @@ mod tests { // nothing to say so. let composed = composed(60.0, RenderScale::full((2000, 1500))); assert!(composed.passes[0].source.contains("dark_channel(tap(coord")); - assert!(!composed.passes[2].source.contains("dark_channel(tap(coord")); + assert!(!composed.passes[1].source.contains("dark_channel(tap(coord")); + assert!(composed.passes[1].source.contains("tap_aux(coord")); // The helper is still emitted for every pass of the operation, and it // must define the function it is named for or the shader fails to // compile a long way from here. diff --git a/docs/dev/traceability.md b/docs/dev/traceability.md index c93f3a9..717d290 100644 --- a/docs/dev/traceability.md +++ b/docs/dev/traceability.md @@ -67,7 +67,7 @@ _None._ | FR-DEV-12 | [`core/dr-pipeline/ops/colour_grading.yaml:1`](../../core/dr-pipeline/ops/colour_grading.yaml#L1) | | FR-DEV-16 | [`tools/traceability/src/chord.rs:1`](../../tools/traceability/src/chord.rs#L1), [`tools/traceability/src/keymap.rs:1`](../../tools/traceability/src/keymap.rs#L1), [`ui/dr-ui/src/develop_ui.rs:476`](../../ui/dr-ui/src/develop_ui.rs#L476), [`ui/dr-ui/src/lib.rs:460`](../../ui/dr-ui/src/lib.rs#L460), [`ui/dr-ui/src/masks_ui.rs:1140`](../../ui/dr-ui/src/masks_ui.rs#L1140), [`ui/dr-ui/src/masks_ui.rs:713`](../../ui/dr-ui/src/masks_ui.rs#L713), [`ui/dr-ui/ui/adjust.slint:1024`](../../ui/dr-ui/ui/adjust.slint#L1024), [`ui/dr-ui/ui/adjust.slint:1029`](../../ui/dr-ui/ui/adjust.slint#L1029), [`ui/dr-ui/ui/adjust.slint:651`](../../ui/dr-ui/ui/adjust.slint#L651), [`ui/dr-ui/ui/app.slint:2550`](../../ui/dr-ui/ui/app.slint#L2550), [`ui/dr-ui/ui/app.slint:2580`](../../ui/dr-ui/ui/app.slint#L2580), [`ui/dr-ui/ui/app.slint:2600`](../../ui/dr-ui/ui/app.slint#L2600), [`ui/dr-ui/ui/app.slint:2618`](../../ui/dr-ui/ui/app.slint#L2618), [`ui/dr-ui/ui/app.slint:2626`](../../ui/dr-ui/ui/app.slint#L2626), [`ui/dr-ui/ui/app.slint:2650`](../../ui/dr-ui/ui/app.slint#L2650), [`ui/dr-ui/ui/app.slint:2733`](../../ui/dr-ui/ui/app.slint#L2733), [`ui/dr-ui/ui/app.slint:2749`](../../ui/dr-ui/ui/app.slint#L2749), [`ui/dr-ui/ui/app.slint:2778`](../../ui/dr-ui/ui/app.slint#L2778), [`ui/dr-ui/ui/app.slint:722`](../../ui/dr-ui/ui/app.slint#L722), [`ui/dr-ui/ui/app.slint:745`](../../ui/dr-ui/ui/app.slint#L745), [`ui/dr-ui/ui/keys.slint:1`](../../ui/dr-ui/ui/keys.slint#L1), [`ui/dr-ui/ui/masks.slint:858`](../../ui/dr-ui/ui/masks.slint#L858), [`ui/dr-ui/ui/masks.slint:884`](../../ui/dr-ui/ui/masks.slint#L884) | | FR-DEV-17 | [`core/dr-pipeline/src/orphan.rs:142`](../../core/dr-pipeline/src/orphan.rs#L142), [`core/dr-pipeline/src/orphan.rs:1`](../../core/dr-pipeline/src/orphan.rs#L1), [`ui/dr-ui/src/develop/framing.rs:1201`](../../ui/dr-ui/src/develop/framing.rs#L1201), [`ui/dr-ui/src/develop/framing.rs:133`](../../ui/dr-ui/src/develop/framing.rs#L133), [`ui/dr-ui/src/develop/framing.rs:169`](../../ui/dr-ui/src/develop/framing.rs#L169), [`ui/dr-ui/src/develop/framing.rs:235`](../../ui/dr-ui/src/develop/framing.rs#L235), [`ui/dr-ui/src/develop/framing.rs:243`](../../ui/dr-ui/src/develop/framing.rs#L243), [`ui/dr-ui/src/develop/session.rs:110`](../../ui/dr-ui/src/develop/session.rs#L110), [`ui/dr-ui/src/develop_ui.rs:1073`](../../ui/dr-ui/src/develop_ui.rs#L1073), [`ui/dr-ui/src/develop_ui.rs:1099`](../../ui/dr-ui/src/develop_ui.rs#L1099), [`ui/dr-ui/src/develop_ui.rs:1162`](../../ui/dr-ui/src/develop_ui.rs#L1162), [`ui/dr-ui/src/lib.rs:2134`](../../ui/dr-ui/src/lib.rs#L2134), [`ui/dr-ui/src/lib.rs:635`](../../ui/dr-ui/src/lib.rs#L635), [`ui/dr-ui/ui/adjust.slint:704`](../../ui/dr-ui/ui/adjust.slint#L704), [`ui/dr-ui/ui/adjust.slint:708`](../../ui/dr-ui/ui/adjust.slint#L708), [`ui/dr-ui/ui/app.slint:2600`](../../ui/dr-ui/ui/app.slint#L2600), [`ui/dr-ui/ui/app.slint:3013`](../../ui/dr-ui/ui/app.slint#L3013), [`ui/dr-ui/ui/crop.slint:51`](../../ui/dr-ui/ui/crop.slint#L51) | -| FR-DEV-18 | [`core/dr-pipeline/src/ops/dehaze.rs:1`](../../core/dr-pipeline/src/ops/dehaze.rs#L1), [`core/dr-pipeline/src/ops/dehaze.rs:221`](../../core/dr-pipeline/src/ops/dehaze.rs#L221) | +| FR-DEV-18 | [`core/dr-pipeline/src/ops/dehaze.rs:1`](../../core/dr-pipeline/src/ops/dehaze.rs#L1), [`core/dr-pipeline/src/ops/dehaze.rs:243`](../../core/dr-pipeline/src/ops/dehaze.rs#L243) | | FR-DEV-19 | [`core/dr-pipeline/src/mask.rs:1341`](../../core/dr-pipeline/src/mask.rs#L1341) | | FR-DEV-19a | [`core/dr-gpu/src/mask.rs:655`](../../core/dr-gpu/src/mask.rs#L655), [`core/dr-gpu/src/mask.rs:773`](../../core/dr-gpu/src/mask.rs#L773), [`core/dr-gpu/tests/local_adjustments.rs:793`](../../core/dr-gpu/tests/local_adjustments.rs#L793), [`core/dr-gpu/tests/local_adjustments.rs:841`](../../core/dr-gpu/tests/local_adjustments.rs#L841), [`core/dr-pipeline/src/mask.rs:1022`](../../core/dr-pipeline/src/mask.rs#L1022), [`core/dr-pipeline/src/mask.rs:1633`](../../core/dr-pipeline/src/mask.rs#L1633), [`core/dr-pipeline/src/mask.rs:2964`](../../core/dr-pipeline/src/mask.rs#L2964), [`core/dr-pipeline/src/mask.rs:3085`](../../core/dr-pipeline/src/mask.rs#L3085), [`core/dr-pipeline/src/mask.rs:3120`](../../core/dr-pipeline/src/mask.rs#L3120), [`core/dr-pipeline/src/mask.rs:929`](../../core/dr-pipeline/src/mask.rs#L929), [`core/dr-pipeline/src/mask.rs:964`](../../core/dr-pipeline/src/mask.rs#L964), [`core/dr-pipeline/src/sidecar.rs:1259`](../../core/dr-pipeline/src/sidecar.rs#L1259), [`core/dr-pipeline/tests/mask_sidecar.rs:1347`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1347), [`core/dr-pipeline/tests/mask_sidecar.rs:1369`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1369), [`core/dr-pipeline/tests/mask_sidecar.rs:1395`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1395), [`ui/dr-ui/src/develop/masks.rs:432`](../../ui/dr-ui/src/develop/masks.rs#L432), [`ui/dr-ui/src/develop/masks.rs:493`](../../ui/dr-ui/src/develop/masks.rs#L493), [`ui/dr-ui/src/develop/masks.rs:512`](../../ui/dr-ui/src/develop/masks.rs#L512), [`ui/dr-ui/src/develop/masks.rs:547`](../../ui/dr-ui/src/develop/masks.rs#L547), [`ui/dr-ui/src/develop/session.rs:192`](../../ui/dr-ui/src/develop/session.rs#L192), [`ui/dr-ui/src/labels.rs:73`](../../ui/dr-ui/src/labels.rs#L73), [`ui/dr-ui/src/masks_ui.rs:1083`](../../ui/dr-ui/src/masks_ui.rs#L1083), [`ui/dr-ui/src/masks_ui.rs:1140`](../../ui/dr-ui/src/masks_ui.rs#L1140), [`ui/dr-ui/src/masks_ui.rs:1196`](../../ui/dr-ui/src/masks_ui.rs#L1196), [`ui/dr-ui/src/masks_ui.rs:1555`](../../ui/dr-ui/src/masks_ui.rs#L1555), [`ui/dr-ui/src/masks_ui.rs:1578`](../../ui/dr-ui/src/masks_ui.rs#L1578), [`ui/dr-ui/src/masks_ui.rs:270`](../../ui/dr-ui/src/masks_ui.rs#L270), [`ui/dr-ui/ui/app.slint:2626`](../../ui/dr-ui/ui/app.slint#L2626), [`ui/dr-ui/ui/masks.slint:151`](../../ui/dr-ui/ui/masks.slint#L151), [`ui/dr-ui/ui/masks.slint:176`](../../ui/dr-ui/ui/masks.slint#L176), [`ui/dr-ui/ui/masks.slint:339`](../../ui/dr-ui/ui/masks.slint#L339), [`ui/dr-ui/ui/masks.slint:379`](../../ui/dr-ui/ui/masks.slint#L379), [`ui/dr-ui/ui/masks.slint:422`](../../ui/dr-ui/ui/masks.slint#L422), [`ui/dr-ui/ui/masks.slint:468`](../../ui/dr-ui/ui/masks.slint#L468), [`ui/dr-ui/ui/masks.slint:796`](../../ui/dr-ui/ui/masks.slint#L796), [`ui/dr-ui/ui/masks.slint:881`](../../ui/dr-ui/ui/masks.slint#L881) | | FR-DEV-19b | [`ui/dr-ui/src/develop/mask_ops.rs:606`](../../ui/dr-ui/src/develop/mask_ops.rs#L606), [`ui/dr-ui/src/develop/masks.rs:595`](../../ui/dr-ui/src/develop/masks.rs#L595), [`ui/dr-ui/src/develop/session.rs:201`](../../ui/dr-ui/src/develop/session.rs#L201), [`ui/dr-ui/src/develop/session.rs:208`](../../ui/dr-ui/src/develop/session.rs#L208), [`ui/dr-ui/src/labels.rs:67`](../../ui/dr-ui/src/labels.rs#L67), [`ui/dr-ui/src/masks_ui.rs:1229`](../../ui/dr-ui/src/masks_ui.rs#L1229), [`ui/dr-ui/src/masks_ui.rs:1448`](../../ui/dr-ui/src/masks_ui.rs#L1448), [`ui/dr-ui/src/masks_ui.rs:1476`](../../ui/dr-ui/src/masks_ui.rs#L1476), [`ui/dr-ui/src/masks_ui.rs:1849`](../../ui/dr-ui/src/masks_ui.rs#L1849), [`ui/dr-ui/src/masks_ui.rs:908`](../../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/ui/app.slint:2161`](../../ui/dr-ui/ui/app.slint#L2161), [`ui/dr-ui/ui/app.slint:856`](../../ui/dr-ui/ui/app.slint#L856), [`ui/dr-ui/ui/masks.slint:1156`](../../ui/dr-ui/ui/masks.slint#L1156), [`ui/dr-ui/ui/masks.slint:479`](../../ui/dr-ui/ui/masks.slint#L479), [`ui/dr-ui/ui/masks.slint:800`](../../ui/dr-ui/ui/masks.slint#L800), [`ui/dr-ui/ui/masks.slint:874`](../../ui/dr-ui/ui/masks.slint#L874), [`ui/dr-ui/ui/masks.slint:894`](../../ui/dr-ui/ui/masks.slint#L894) | @@ -88,7 +88,7 @@ _None._ | FR-DEV-6 | [`core/dr-pipeline/src/bundled.rs:1`](../../core/dr-pipeline/src/bundled.rs#L1), [`core/dr-pipeline/src/preset.rs:1291`](../../core/dr-pipeline/src/preset.rs#L1291), [`core/dr-pipeline/src/preset.rs:1301`](../../core/dr-pipeline/src/preset.rs#L1301), [`core/dr-pipeline/src/preset.rs:1310`](../../core/dr-pipeline/src/preset.rs#L1310), [`core/dr-pipeline/src/preset.rs:1326`](../../core/dr-pipeline/src/preset.rs#L1326), [`core/dr-pipeline/src/preset.rs:1338`](../../core/dr-pipeline/src/preset.rs#L1338), [`core/dr-pipeline/src/preset.rs:1354`](../../core/dr-pipeline/src/preset.rs#L1354), [`core/dr-pipeline/src/preset.rs:1380`](../../core/dr-pipeline/src/preset.rs#L1380), [`core/dr-pipeline/src/preset.rs:1402`](../../core/dr-pipeline/src/preset.rs#L1402), [`core/dr-pipeline/src/preset.rs:1416`](../../core/dr-pipeline/src/preset.rs#L1416), [`core/dr-pipeline/src/preset.rs:1434`](../../core/dr-pipeline/src/preset.rs#L1434), [`core/dr-pipeline/src/preset.rs:1527`](../../core/dr-pipeline/src/preset.rs#L1527), [`core/dr-pipeline/src/preset.rs:1542`](../../core/dr-pipeline/src/preset.rs#L1542), [`core/dr-pipeline/src/preset.rs:1`](../../core/dr-pipeline/src/preset.rs#L1), [`core/dr-pipeline/src/preset.rs:501`](../../core/dr-pipeline/src/preset.rs#L501), [`core/dr-pipeline/src/preset.rs:534`](../../core/dr-pipeline/src/preset.rs#L534), [`core/dr-preset-xmp/src/lib.rs:1`](../../core/dr-preset-xmp/src/lib.rs#L1), [`core/dr-preset-xmp/src/lib.rs:492`](../../core/dr-preset-xmp/src/lib.rs#L492), [`core/dr-types/src/settings.rs:443`](../../core/dr-types/src/settings.rs#L443), [`ui/dr-ui/src/develop/session.rs:560`](../../ui/dr-ui/src/develop/session.rs#L560), [`ui/dr-ui/src/develop/session.rs:581`](../../ui/dr-ui/src/develop/session.rs#L581), [`ui/dr-ui/src/develop_ui.rs:167`](../../ui/dr-ui/src/develop_ui.rs#L167), [`ui/dr-ui/src/develop_ui.rs:196`](../../ui/dr-ui/src/develop_ui.rs#L196), [`ui/dr-ui/src/lib.rs:1257`](../../ui/dr-ui/src/lib.rs#L1257), [`ui/dr-ui/src/library/sidecar.rs:11`](../../ui/dr-ui/src/library/sidecar.rs#L11), [`ui/dr-ui/src/library/sidecar.rs:39`](../../ui/dr-ui/src/library/sidecar.rs#L39), [`ui/dr-ui/src/library/thumbnails_fetch.rs:413`](../../ui/dr-ui/src/library/thumbnails_fetch.rs#L413), [`ui/dr-ui/src/library_ui/controller.rs:677`](../../ui/dr-ui/src/library_ui/controller.rs#L677), [`ui/dr-ui/src/library_ui/ratings_keywords.rs:488`](../../ui/dr-ui/src/library_ui/ratings_keywords.rs#L488), [`ui/dr-ui/src/library_ui/window.rs:663`](../../ui/dr-ui/src/library_ui/window.rs#L663), [`ui/dr-ui/src/preset_store.rs:1`](../../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1586`](../../ui/dr-ui/src/presets.rs#L1586), [`ui/dr-ui/src/presets.rs:1`](../../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:707`](../../ui/dr-ui/src/presets.rs#L707), [`ui/dr-ui/src/presets.rs:741`](../../ui/dr-ui/src/presets.rs#L741), [`ui/dr-ui/ui/adjust.slint:937`](../../ui/dr-ui/ui/adjust.slint#L937), [`ui/dr-ui/ui/adjust.slint:962`](../../ui/dr-ui/ui/adjust.slint#L962), [`ui/dr-ui/ui/app.slint:1089`](../../ui/dr-ui/ui/app.slint#L1089), [`ui/dr-ui/ui/app.slint:1509`](../../ui/dr-ui/ui/app.slint#L1509), [`ui/dr-ui/ui/app.slint:2482`](../../ui/dr-ui/ui/app.slint#L2482), [`ui/dr-ui/ui/app.slint:3420`](../../ui/dr-ui/ui/app.slint#L3420), [`ui/dr-ui/ui/app.slint:3465`](../../ui/dr-ui/ui/app.slint#L3465), [`ui/dr-ui/ui/app.slint:525`](../../ui/dr-ui/ui/app.slint#L525), [`ui/dr-ui/ui/app.slint:536`](../../ui/dr-ui/ui/app.slint#L536), [`ui/dr-ui/ui/app.slint:693`](../../ui/dr-ui/ui/app.slint#L693), [`ui/dr-ui/ui/develop.slint:257`](../../ui/dr-ui/ui/develop.slint#L257), [`ui/dr-ui/ui/library.slint:1995`](../../ui/dr-ui/ui/library.slint#L1995), [`ui/dr-ui/ui/library.slint:2001`](../../ui/dr-ui/ui/library.slint#L2001), [`ui/dr-ui/ui/library.slint:3191`](../../ui/dr-ui/ui/library.slint#L3191), [`ui/dr-ui/ui/library.slint:5127`](../../ui/dr-ui/ui/library.slint#L5127), [`ui/dr-ui/ui/library.slint:5145`](../../ui/dr-ui/ui/library.slint#L5145), [`ui/dr-ui/ui/presets.slint:132`](../../ui/dr-ui/ui/presets.slint#L132), [`ui/dr-ui/ui/presets.slint:140`](../../ui/dr-ui/ui/presets.slint#L140), [`ui/dr-ui/ui/presets.slint:20`](../../ui/dr-ui/ui/presets.slint#L20), [`ui/dr-ui/ui/presets.slint:310`](../../ui/dr-ui/ui/presets.slint#L310), [`ui/dr-ui/ui/presets.slint:32`](../../ui/dr-ui/ui/presets.slint#L32), [`ui/dr-ui/ui/presets.slint:348`](../../ui/dr-ui/ui/presets.slint#L348), [`ui/dr-ui/ui/presets.slint:383`](../../ui/dr-ui/ui/presets.slint#L383), [`ui/dr-ui/ui/presets.slint:45`](../../ui/dr-ui/ui/presets.slint#L45), [`ui/dr-ui/ui/presets.slint:6`](../../ui/dr-ui/ui/presets.slint#L6), [`ui/dr-ui/ui/presets.slint:86`](../../ui/dr-ui/ui/presets.slint#L86), [`ui/dr-ui/ui/settings.slint:126`](../../ui/dr-ui/ui/settings.slint#L126), [`ui/dr-ui/ui/settings.slint:690`](../../ui/dr-ui/ui/settings.slint#L690) | | FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop/history.rs:107`](../../ui/dr-ui/src/develop/history.rs#L107), [`ui/dr-ui/src/develop/history.rs:124`](../../ui/dr-ui/src/develop/history.rs#L124), [`ui/dr-ui/src/develop/history.rs:206`](../../ui/dr-ui/src/develop/history.rs#L206), [`ui/dr-ui/src/develop/history.rs:238`](../../ui/dr-ui/src/develop/history.rs#L238), [`ui/dr-ui/src/develop/history.rs:359`](../../ui/dr-ui/src/develop/history.rs#L359), [`ui/dr-ui/src/develop/render.rs:493`](../../ui/dr-ui/src/develop/render.rs#L493), [`ui/dr-ui/src/develop/render.rs:534`](../../ui/dr-ui/src/develop/render.rs#L534), [`ui/dr-ui/src/develop/render.rs:925`](../../ui/dr-ui/src/develop/render.rs#L925), [`ui/dr-ui/src/develop/session.rs:105`](../../ui/dr-ui/src/develop/session.rs#L105), [`ui/dr-ui/src/develop_ui.rs:707`](../../ui/dr-ui/src/develop_ui.rs#L707), [`ui/dr-ui/src/develop_ui.rs:766`](../../ui/dr-ui/src/develop_ui.rs#L766), [`ui/dr-ui/src/develop_ui.rs:838`](../../ui/dr-ui/src/develop_ui.rs#L838), [`ui/dr-ui/src/lib.rs:2122`](../../ui/dr-ui/src/lib.rs#L2122), [`ui/dr-ui/src/lib.rs:2199`](../../ui/dr-ui/src/lib.rs#L2199), [`ui/dr-ui/src/lib.rs:2213`](../../ui/dr-ui/src/lib.rs#L2213), [`ui/dr-ui/src/lib.rs:447`](../../ui/dr-ui/src/lib.rs#L447), [`ui/dr-ui/ui/app.slint:224`](../../ui/dr-ui/ui/app.slint#L224), [`ui/dr-ui/ui/app.slint:2349`](../../ui/dr-ui/ui/app.slint#L2349), [`ui/dr-ui/ui/app.slint:2395`](../../ui/dr-ui/ui/app.slint#L2395), [`ui/dr-ui/ui/app.slint:2964`](../../ui/dr-ui/ui/app.slint#L2964), [`ui/dr-ui/ui/history.slint:103`](../../ui/dr-ui/ui/history.slint#L103), [`ui/dr-ui/ui/history.slint:1`](../../ui/dr-ui/ui/history.slint#L1), [`ui/dr-ui/ui/history.slint:227`](../../ui/dr-ui/ui/history.slint#L227), [`ui/dr-ui/ui/history.slint:53`](../../ui/dr-ui/ui/history.slint#L53), [`ui/dr-ui/ui/widgets.slint:72`](../../ui/dr-ui/ui/widgets.slint#L72) | | FR-DEV-8 | [`core/dr-gpu/src/detail.rs:405`](../../core/dr-gpu/src/detail.rs#L405), [`core/dr-gpu/src/detail.rs:623`](../../core/dr-gpu/src/detail.rs#L623), [`core/dr-gpu/tests/detail_instances.rs:1`](../../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:410`](../../core/dr-pipeline/src/detail.rs#L410), [`core/dr-pipeline/src/detail.rs:452`](../../core/dr-pipeline/src/detail.rs#L452), [`core/dr-pipeline/src/detail.rs:487`](../../core/dr-pipeline/src/detail.rs#L487), [`core/dr-pipeline/src/detail.rs:573`](../../core/dr-pipeline/src/detail.rs#L573), [`core/dr-pipeline/src/graph.rs:1098`](../../core/dr-pipeline/src/graph.rs#L1098), [`core/dr-pipeline/src/graph.rs:115`](../../core/dr-pipeline/src/graph.rs#L115), [`core/dr-pipeline/src/graph.rs:247`](../../core/dr-pipeline/src/graph.rs#L247), [`core/dr-pipeline/src/operation.rs:330`](../../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:749`](../../core/dr-pipeline/src/operation.rs#L749), [`core/dr-pipeline/src/sidecar.rs:1104`](../../core/dr-pipeline/src/sidecar.rs#L1104), [`core/dr-pipeline/src/sidecar.rs:1158`](../../core/dr-pipeline/src/sidecar.rs#L1158), [`core/dr-pipeline/src/sidecar.rs:1188`](../../core/dr-pipeline/src/sidecar.rs#L1188), [`core/dr-pipeline/src/sidecar.rs:233`](../../core/dr-pipeline/src/sidecar.rs#L233), [`core/dr-pipeline/src/sidecar.rs:410`](../../core/dr-pipeline/src/sidecar.rs#L410), [`core/dr-pipeline/src/sidecar.rs:910`](../../core/dr-pipeline/src/sidecar.rs#L910), [`core/dr-pipeline/src/spot.rs:115`](../../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop/repairs.rs:114`](../../ui/dr-ui/src/develop/repairs.rs#L114), [`ui/dr-ui/src/develop/repairs.rs:11`](../../ui/dr-ui/src/develop/repairs.rs#L11), [`ui/dr-ui/src/develop/repairs.rs:197`](../../ui/dr-ui/src/develop/repairs.rs#L197), [`ui/dr-ui/src/develop/repairs.rs:211`](../../ui/dr-ui/src/develop/repairs.rs#L211), [`ui/dr-ui/src/develop/repairs.rs:49`](../../ui/dr-ui/src/develop/repairs.rs#L49), [`ui/dr-ui/src/develop/session.rs:216`](../../ui/dr-ui/src/develop/session.rs#L216), [`ui/dr-ui/src/develop_ui.rs:993`](../../ui/dr-ui/src/develop_ui.rs#L993), [`ui/dr-ui/src/labels.rs:80`](../../ui/dr-ui/src/labels.rs#L80), [`ui/dr-ui/src/lib.rs:2157`](../../ui/dr-ui/src/lib.rs#L2157), [`ui/dr-ui/src/lib.rs:490`](../../ui/dr-ui/src/lib.rs#L490), [`ui/dr-ui/src/spots_ui.rs:19`](../../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/app.slint:2202`](../../ui/dr-ui/ui/app.slint#L2202), [`ui/dr-ui/ui/app.slint:2445`](../../ui/dr-ui/ui/app.slint#L2445), [`ui/dr-ui/ui/app.slint:3313`](../../ui/dr-ui/ui/app.slint#L3313), [`ui/dr-ui/ui/icons.slint:378`](../../ui/dr-ui/ui/icons.slint#L378), [`ui/dr-ui/ui/session.slint:102`](../../ui/dr-ui/ui/session.slint#L102), [`ui/dr-ui/ui/session.slint:52`](../../ui/dr-ui/ui/session.slint#L52), [`ui/dr-ui/ui/spots.slint:191`](../../ui/dr-ui/ui/spots.slint#L191), [`ui/dr-ui/ui/spots.slint:49`](../../ui/dr-ui/ui/spots.slint#L49), [`ui/dr-ui/ui/spots.slint:6`](../../ui/dr-ui/ui/spots.slint#L6), [`ui/dr-ui/ui/spots.slint:91`](../../ui/dr-ui/ui/spots.slint#L91) | -| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:1039`](../../core/dr-gpu/src/adjust.rs#L1039), [`core/dr-gpu/src/adjust.rs:2698`](../../core/dr-gpu/src/adjust.rs#L2698), [`core/dr-gpu/src/adjust.rs:2775`](../../core/dr-gpu/src/adjust.rs#L2775), [`core/dr-gpu/src/adjust.rs:2860`](../../core/dr-gpu/src/adjust.rs#L2860), [`core/dr-gpu/src/adjust.rs:54`](../../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/lib.rs:174`](../../core/dr-gpu/src/lib.rs#L174), [`core/dr-gpu/src/lib.rs:194`](../../core/dr-gpu/src/lib.rs#L194), [`core/dr-gpu/src/lib.rs:72`](../../core/dr-gpu/src/lib.rs#L72), [`core/dr-gpu/src/lib.rs:95`](../../core/dr-gpu/src/lib.rs#L95), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:264`](../../core/dr-gpu/tests/local_contrast.rs#L264), [`core/dr-gpu/tests/noise_reduction.rs:378`](../../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:542`](../../core/dr-pipeline/src/detail.rs#L542), [`core/dr-pipeline/src/graph.rs:928`](../../core/dr-pipeline/src/graph.rs#L928), [`core/dr-pipeline/src/graph.rs:958`](../../core/dr-pipeline/src/graph.rs#L958), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:659`](../../core/dr-pipeline/src/ops/capture_sharpen.rs#L659), [`core/dr-pipeline/src/ops/dehaze.rs:1`](../../core/dr-pipeline/src/ops/dehaze.rs#L1), [`core/dr-pipeline/src/ops/dehaze.rs:592`](../../core/dr-pipeline/src/ops/dehaze.rs#L592), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:971`](../../core/dr-pipeline/src/ops/local_contrast.rs#L971), [`core/dr-pipeline/src/ops/noise_reduction.rs:700`](../../core/dr-pipeline/src/ops/noise_reduction.rs#L700), [`core/dr-pipeline/src/spot.rs:673`](../../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop/framing.rs:594`](../../ui/dr-ui/src/develop/framing.rs#L594), [`ui/dr-ui/src/develop/mod.rs:55`](../../ui/dr-ui/src/develop/mod.rs#L55), [`ui/dr-ui/src/develop/render.rs:1016`](../../ui/dr-ui/src/develop/render.rs#L1016), [`ui/dr-ui/src/develop/render.rs:1154`](../../ui/dr-ui/src/develop/render.rs#L1154), [`ui/dr-ui/src/develop/render.rs:217`](../../ui/dr-ui/src/develop/render.rs#L217), [`ui/dr-ui/src/develop/render.rs:79`](../../ui/dr-ui/src/develop/render.rs#L79), [`ui/dr-ui/src/develop/render.rs:982`](../../ui/dr-ui/src/develop/render.rs#L982), [`ui/dr-ui/src/lib.rs:1077`](../../ui/dr-ui/src/lib.rs#L1077), [`ui/dr-ui/src/lib.rs:115`](../../ui/dr-ui/src/lib.rs#L115) | +| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:1039`](../../core/dr-gpu/src/adjust.rs#L1039), [`core/dr-gpu/src/adjust.rs:2698`](../../core/dr-gpu/src/adjust.rs#L2698), [`core/dr-gpu/src/adjust.rs:2775`](../../core/dr-gpu/src/adjust.rs#L2775), [`core/dr-gpu/src/adjust.rs:2860`](../../core/dr-gpu/src/adjust.rs#L2860), [`core/dr-gpu/src/adjust.rs:54`](../../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/lib.rs:174`](../../core/dr-gpu/src/lib.rs#L174), [`core/dr-gpu/src/lib.rs:194`](../../core/dr-gpu/src/lib.rs#L194), [`core/dr-gpu/src/lib.rs:72`](../../core/dr-gpu/src/lib.rs#L72), [`core/dr-gpu/src/lib.rs:95`](../../core/dr-gpu/src/lib.rs#L95), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:264`](../../core/dr-gpu/tests/local_contrast.rs#L264), [`core/dr-gpu/tests/noise_reduction.rs:378`](../../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:542`](../../core/dr-pipeline/src/detail.rs#L542), [`core/dr-pipeline/src/graph.rs:928`](../../core/dr-pipeline/src/graph.rs#L928), [`core/dr-pipeline/src/graph.rs:958`](../../core/dr-pipeline/src/graph.rs#L958), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:659`](../../core/dr-pipeline/src/ops/capture_sharpen.rs#L659), [`core/dr-pipeline/src/ops/dehaze.rs:1`](../../core/dr-pipeline/src/ops/dehaze.rs#L1), [`core/dr-pipeline/src/ops/dehaze.rs:565`](../../core/dr-pipeline/src/ops/dehaze.rs#L565), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:971`](../../core/dr-pipeline/src/ops/local_contrast.rs#L971), [`core/dr-pipeline/src/ops/noise_reduction.rs:700`](../../core/dr-pipeline/src/ops/noise_reduction.rs#L700), [`core/dr-pipeline/src/spot.rs:673`](../../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop/framing.rs:594`](../../ui/dr-ui/src/develop/framing.rs#L594), [`ui/dr-ui/src/develop/mod.rs:55`](../../ui/dr-ui/src/develop/mod.rs#L55), [`ui/dr-ui/src/develop/render.rs:1016`](../../ui/dr-ui/src/develop/render.rs#L1016), [`ui/dr-ui/src/develop/render.rs:1154`](../../ui/dr-ui/src/develop/render.rs#L1154), [`ui/dr-ui/src/develop/render.rs:217`](../../ui/dr-ui/src/develop/render.rs#L217), [`ui/dr-ui/src/develop/render.rs:79`](../../ui/dr-ui/src/develop/render.rs#L79), [`ui/dr-ui/src/develop/render.rs:982`](../../ui/dr-ui/src/develop/render.rs#L982), [`ui/dr-ui/src/lib.rs:1077`](../../ui/dr-ui/src/lib.rs#L1077), [`ui/dr-ui/src/lib.rs:115`](../../ui/dr-ui/src/lib.rs#L115) | | FR-DSP-3 | [`core/dr-gpu/tests/frame_budget.rs:101`](../../core/dr-gpu/tests/frame_budget.rs#L101), [`core/dr-gpu/tests/local_contrast.rs:329`](../../core/dr-gpu/tests/local_contrast.rs#L329), [`core/dr-pipeline/src/ops/local_contrast.rs:447`](../../core/dr-pipeline/src/ops/local_contrast.rs#L447) | | FR-DSP-4 | [`ui/dr-ui/src/lib.rs:2222`](../../ui/dr-ui/src/lib.rs#L2222), [`ui/dr-ui/src/lib.rs:2401`](../../ui/dr-ui/src/lib.rs#L2401), [`ui/dr-ui/src/refine.rs:1`](../../ui/dr-ui/src/refine.rs#L1), [`ui/dr-ui/ui/app.slint:112`](../../ui/dr-ui/ui/app.slint#L112), [`ui/dr-ui/ui/app.slint:1836`](../../ui/dr-ui/ui/app.slint#L1836), [`ui/dr-ui/ui/histogram.slint:182`](../../ui/dr-ui/ui/histogram.slint#L182), [`ui/dr-ui/ui/histogram.slint:250`](../../ui/dr-ui/ui/histogram.slint#L250) | | FR-DSP-5 | [`core/dr-gpu/tests/frame_budget.rs:101`](../../core/dr-gpu/tests/frame_budget.rs#L101), [`core/dr-gpu/tests/zoom_resolution.rs:135`](../../core/dr-gpu/tests/zoom_resolution.rs#L135), [`core/dr-gpu/tests/zoom_resolution.rs:166`](../../core/dr-gpu/tests/zoom_resolution.rs#L166), [`core/dr-gpu/tests/zoom_resolution.rs:1`](../../core/dr-gpu/tests/zoom_resolution.rs#L1), [`core/dr-gpu/tests/zoom_resolution.rs:216`](../../core/dr-gpu/tests/zoom_resolution.rs#L216) |