Let clarity's base be computed where it is still fully determined

Clarity's Gaussian sigma is 1.2% of the frame's shorter edge, so its radius
is a property of the viewport: 52 render pixels at 4K, two separable passes
of 105 taps each over 8.3 M pixels. That measured 33.9 ms — seven times the
entire fused point chain, for one slider — and is docs/technical-debt.md TD-4.

A detail pass may now declare `output_scale`, and clarity's base is computed
on a grid a quarter the size on each axis.

The pass that combines needs the blur *and* the full-resolution colour, and a
colour that has been through a quarter-scale target is no longer full
resolution. So a scaled pass cannot simply join the ping-pong: there are two
chains now. The full-resolution one carries the colour and no scaled pass
touches it; the reduced one carries the base and reaches the combining pass
through a second binding as `reduced_at()`.

The reduce is a dispatch of its own rather than something the first blur half
does on the way past, and that is the whole difference between this and the
strided kernel the module documentation rules out. A stride samples an image
that is not band-limited and aliases high-frequency content down into the
base, which is then subtracted, and arrives in the output as mottling across
smooth gradients. This band-limits first and samples after. What is discarded
is content the base could not represent at any resolution, because a Gaussian
at sigma = 26 px holds nothing above one cycle per 26 px and the quarter-scale
grid carries one per 8 — so the reduced base is not an approximation of the
full-resolution one, it is the same function sampled where it is still
determined.

Which is also why the scale belongs to the band rather than to the stage.
Texture's sigma is a decade finer, so the reduce pass's own box would be wider
than the Gaussian it was prefiltering; texture never reduces. And clarity
steps 4 -> 2 -> 1 as sigma falls, because a quarter of a small sigma is not a
Gaussian either — the case that gives up is the one that was already cheap.

`radius` stays in each pass's own pixels and `ComposedDetail::radius` multiplies
it back up, so 13 reduced pixels at scale 4 still report the 52 render pixels a
tile would have to be grown by. The halo a scheduler sees does not move.

The halo tests pass unchanged, which was TD-4's stated bar; they render at
1024 px and so exercise the reduced path rather than stepping around it. Added
`crossing_the_reduction_threshold_does_not_change_the_picture`, because
nothing yet compared the reduced form against a *less* reduced one — every
other test measures one form against itself. It renders the same edit either
side of the 4 -> 2 step-down and holds the peak excursion to 0.03 stops and
the reach to 2% of the frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 13:12:29 +02:00
co-authored by Claude Opus 5
parent 0407fb8d2d
commit bff95e25ad
10 changed files with 833 additions and 82 deletions
+184 -11
View File
@@ -49,6 +49,32 @@
//! resolve pass to pay for. That leaves the allocation at `1 + min(N-1, 2)`
//! textures: one for a single-pass operation, two for a separable blur, three
//! however long the chain gets after that.
//!
//! # The reduced chain, and why a second one was needed
//!
//! A pass may declare [`dr_pipeline::detail::DetailPass::output_scale`] and
//! write a target a fraction of the render size — clarity's base does, which
//! is what TD-4 bought back. Such a pass cannot be part of the ping-pong
//! above, and the reason is the shape of an unsharp mask rather than anything
//! about textures: the pass that *combines* needs the blur **and** the
//! full-resolution colour, and a colour that has been through a quarter-scale
//! target is no longer full resolution. If the scaled passes wrote into the
//! main chain they would destroy the very thing the last pass is going to
//! subtract from.
//!
//! So there are two chains. The full-resolution one carries the colour and is
//! untouched by a scaled pass; the reduced one carries the base. A scaled pass
//! reads the reduced chain if anything has been written to it and the
//! full-resolution chain otherwise — which is exactly "read what the pass
//! before you wrote", the same rule as before. A full-resolution pass always
//! reads the full-resolution chain, and sees the reduced one through binding 4
//! as `reduced_at()`.
//!
//! One reduced buffer, not one per operation. Two operations both wanting a
//! reduced base in the same frame would need more, and nothing does: clarity
//! is the only caller and texture's band is a decade finer, so it must stay at
//! full resolution. A `debug_assert` in [`DetailRunner::encode`] holds that
//! claim rather than leaving it as a comment.
use std::collections::HashMap;
@@ -149,8 +175,14 @@ pub(crate) struct DetailRunner {
/// Compiled pipelines by pass structure hash.
cache: HashMap<u64, wgpu::ComputePipeline>,
pool: Intermediates,
/// The reduced chain — see the module documentation. Its own pool rather
/// than more slots in `pool`, because its textures are a different size
/// and [`Intermediates::ensure`] drops the lot when the size changes.
reduced: Intermediates,
/// See [`placeholder_instances`].
no_instances: wgpu::Buffer,
/// See [`placeholder_reduced`].
no_reduced: wgpu::TextureView,
}
struct Layout {
@@ -174,6 +206,32 @@ fn placeholder_instances(ctx: &GpuContext) -> wgpu::Buffer {
})
}
/// What binding 4 holds for a pass that never calls `reduced_at`.
///
/// The same trick as [`placeholder_instances`], for the same reason: one bind
/// group layout has to serve a pass that reads the reduced chain and a pass
/// that has never heard of it, and a binding cannot be left unbound. 1x1 and
/// allocated once, so the cost of the arrangement is four bytes for the life
/// of the runner.
fn placeholder_reduced(ctx: &GpuContext) -> wgpu::TextureView {
ctx.device
.create_texture(&wgpu::TextureDescriptor {
label: Some("detail-reduced-placeholder"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: INTERMEDIATE_FORMAT,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
.create_view(&Default::default())
}
impl DetailRunner {
pub(crate) fn new(ctx: &GpuContext) -> Self {
Self {
@@ -182,7 +240,9 @@ impl DetailRunner {
to_output: Layout::new(ctx, crate::AdjustPass::FORMAT, "detail-output"),
cache: HashMap::new(),
pool: Intermediates::new(),
reduced: Intermediates::new(),
no_instances: placeholder_instances(ctx),
no_reduced: placeholder_reduced(ctx),
}
}
@@ -221,18 +281,93 @@ impl DetailRunner {
self.compile(pass)?;
}
for (index, pass) in chain.passes.iter().enumerate() {
// Read what the previous pass wrote; write the next slot, or the
// display texture if this is the last one. `index % 2` alternates
// between slots 1 and 2, so a pass never reads the texture it is
// writing — which on a compute pass is not an error the driver
// reports, merely a picture that depends on scheduling.
let source_slot = if index == 0 { 0 } else { 2 - (index % 2) };
let source = &self.pool.slots[source_slot].view;
// The reduced chain's size, allocated once for the whole chain.
//
// One scale per chain, so the first scaled pass names the only scale
// there is — see the module documentation for why one buffer is
// enough, and the assertion for what would have to change.
if let Some(scale) = chain
.passes
.iter()
.map(|p| p.output_scale)
.find(|&scale| scale > 1)
{
debug_assert!(
chain
.passes
.iter()
.all(|p| p.output_scale == 1 || p.output_scale == scale),
"two reduced scales in one chain, and the runner holds one \
reduced buffer"
);
// Two, for the ping-pong the reduce and the two blur halves need.
// A separable blur cannot read the texture it is writing.
self.reduced.ensure(
&self.ctx,
2,
width.div_ceil(scale).max(1),
height.div_ceil(scale).max(1),
);
}
// Where each chain last wrote. `full` starts at slot 0 — what the
// fused colour pass left there — and `carried` starts empty, which is
// what makes the first scaled pass read the colour rather than an
// uninitialised base.
let mut full = 0usize;
let mut full_writes = 0usize;
let mut carried: Option<usize> = None;
let mut reduced_writes = 0usize;
for pass in chain.passes.iter() {
let scaled = pass.output_scale > 1;
// The last pass carries the output transform into the display
// texture, which is the render size by definition. A scaled pass
// there would bind a shader dispatching over a quarter-size grid
// to a full-size target and write a quarter of the picture — a
// wrong image rather than a validation failure, so it is caught
// here and named.
if scaled && pass.writes_output {
return Err(GpuError::ShaderCompilation(format!(
"detail pass {} declares output_scale {} and is last in \
the chain; the output transform is written at the render \
size",
pass.label, pass.output_scale
)));
}
let (dispatch_w, dispatch_h) = if scaled {
(
width.div_ceil(pass.output_scale).max(1),
height.div_ceil(pass.output_scale).max(1),
)
} else {
(width, height)
};
// Read what the previous pass in *this pass's own chain* wrote;
// write the next slot of it, or the display texture if this is the
// last pass. Alternating slots is what stops a pass reading the
// texture it is writing — on a compute pass that is not an error
// the driver reports, merely a picture that depends on scheduling.
let source = match (scaled, carried) {
(true, Some(slot)) => &self.reduced.slots[slot].view,
_ => &self.pool.slots[full].view,
};
let destination = if pass.writes_output {
output
} else if scaled {
&self.reduced.slots[reduced_writes % 2].view
} else {
&self.pool.slots[1 + (index % 2)].view
&self.pool.slots[1 + (full_writes % 2)].view
};
// Binding 4. Present for every pass, because one bind group layout
// serves both kinds; a pass that never calls `reduced_at` gets the
// 1x1 placeholder and never reads it.
let reduced_source = match carried {
Some(slot) => &self.reduced.slots[slot].view,
None => &self.no_reduced,
};
let layout = if pass.writes_output {
&self.to_output
@@ -290,6 +425,10 @@ impl DetailRunner {
binding: 3,
resource: instances.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(reduced_source),
},
],
});
@@ -304,7 +443,23 @@ impl DetailRunner {
});
compute.set_pipeline(pipeline);
compute.set_bind_group(0, &bind_group, &[]);
compute.dispatch_workgroups(width.div_ceil(8), height.div_ceil(8), 1);
compute.dispatch_workgroups(dispatch_w.div_ceil(8), dispatch_h.div_ceil(8), 1);
drop(compute);
if pass.writes_output {
// Nothing downstream to hand anything to.
} else if scaled {
carried = Some(reduced_writes % 2);
reduced_writes += 1;
} else {
full = 1 + (full_writes % 2);
full_writes += 1;
// A full-resolution pass consumes the reduced chain. It is the
// combine — the base has been subtracted and now lives in the
// colour — so a later operation must not be handed a base
// belonging to this one.
carried = None;
}
}
Ok(chain.passes.len())
@@ -375,7 +530,10 @@ impl DetailRunner {
/// created. For tests — see [`crate::MaskPass::allocations`] for the
/// regression this shape of counter exists to catch.
pub(crate) fn allocations(&self) -> usize {
self.pool.allocations
// Both pools. A reduced buffer reallocated every frame is exactly the
// regression this counter exists to catch, and counting only the
// full-resolution one would hide it.
self.pool.allocations + self.reduced.allocations
}
}
@@ -439,6 +597,21 @@ impl Layout {
// that never reads the buffer costs nothing for it being
// bound.
storage_entry(3),
// The reduced chain, for a pass that calls `reduced_at`.
// Bound on every layout for the same reason binding 3 is:
// a pass that never reads it costs nothing for it being
// there, and two more layouts would cost a great deal more
// than that.
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
],
});
+1
View File
@@ -67,6 +67,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
.to_string();
ComposedDetailPass {
output_scale: 1,
label: "test/instances".to_string(),
source,
uniforms: vec![SIZE as f32, SIZE as f32, 1.0, 0.0],
+101 -6
View File
@@ -27,6 +27,7 @@
use dr_gpu::{AdjustPass, DemosaicedImage, GpuContext};
use dr_pipeline::descriptor::{OpId, ParamId};
use dr_pipeline::detail::RenderScale;
use dr_pipeline::ops::local_contrast::Clarity;
use dr_pipeline::{Affects, EditGraph, OutputMode};
use dr_types::ColourSpace;
@@ -323,6 +324,87 @@ fn a_proxy_and_an_export_agree_about_the_effect() {
);
}
#[test]
fn crossing_the_reduction_threshold_does_not_change_the_picture() {
// TRACES: FR-DSP-3 — `docs/technical-debt.md` TD-4, held in pixels.
//
// Clarity's base is computed on a reduced grid, and how reduced depends on
// the viewport: `LocalContrast::reduction` steps 4 -> 2 -> 1 as sigma
// falls, because a quarter of a small sigma is not a Gaussian any more.
// The whole claim of the optimisation is that this is invisible — that a
// base sampled at a quarter is not an approximation of the full-resolution
// one but the same band-limited function, sampled where it is still fully
// determined.
//
// Every other test in this file measures one form against itself. This is
// the only one that measures the forms against *each other*, and it is the
// one that would fail if the reduction were quietly softening the control,
// shifting it half a reduced pixel, or blocking the base into 4x4 squares.
//
// The two sizes are chosen to sit either side of a step-down: sigma is
// 1.2% of the shorter edge, so 512 gives 6.1 px and reduces by four, while
// 288 gives 3.5 px — under `MIN_REDUCED_SIGMA` once quartered — and
// reduces by two. Asserted rather than assumed, because the whole test is
// vacuous if both sides land on the same reduction.
let Some(ctx) = ctx() else { return };
assert_eq!(
Clarity::with_amount(100.0).reduction(RenderScale::full((512, 512))),
4
);
assert_eq!(
Clarity::with_amount(100.0).reduction(RenderScale::full((288, 288))),
2
);
let source = step_edge(&ctx, SIZE, [1.0, 1.0, 1.0]);
// Peak excursion in stops and the fraction of the frame it covers — the
// same two numbers `a_proxy_and_an_export_agree_about_the_effect` uses,
// and for the same reason: both are scale-free, so they are comparable
// between two renders of different sizes.
let measure = |out: u32| -> (f32, f32) {
let mut plain_pass = AdjustPass::new(&ctx);
let plain = row(
&render(&mut plain_pass, &EditGraph::default_chain(), &source, out),
out,
out / 2,
);
let mut pass = AdjustPass::new(&ctx);
let edited = row(
&render(&mut pass, &graph_with(CLARITY, 100.0), &source, out),
out,
out / 2,
);
let moved: Vec<f32> = (0..out as usize)
.map(|x| stops(edited[x], plain[x]).abs())
.collect();
let peak = moved.iter().cloned().fold(0.0f32, f32::max);
let touched = moved.iter().filter(|m| **m > peak * 0.1).count();
(peak, touched as f32 / out as f32)
};
let (quartered_peak, quartered_reach) = measure(512);
let (halved_peak, halved_reach) = measure(288);
// The same tolerances the proxy/export test uses. They are not loose: the
// bound this operation guarantees is 0.35 stops, so 0.03 is under a tenth
// of the full excursion.
assert!(
(quartered_peak - halved_peak).abs() < 0.03,
"a quarter-scale base gives {quartered_peak:.3} stops and a half-scale \
one {halved_peak:.3}; the reduction is supposed to be invisible"
);
assert!(
(quartered_reach - halved_reach).abs() < 0.02,
"the effect covers {quartered_reach:.3} of the frame reduced by four \
and {halved_reach:.3} reduced by two; the base has changed width"
);
assert!(
quartered_peak > 0.1,
"{quartered_peak:.3} stops — two flat images would also agree"
);
}
#[test]
fn texture_acts_at_a_finer_scale_than_clarity() {
// The whole reason there are two nodes. If the two controls ever reach the
@@ -485,10 +567,15 @@ fn dragging_the_slider_re_runs_the_detail_stage_and_nothing_else() {
let mut graph = graph_with(CLARITY, 40.0);
render(&mut pass, &graph, &source, 256);
assert_eq!(pass.colour_dispatches(), 1);
// Four at this size: reduce, the two blur halves on the reduced grid, and
// the combine. σ is 1.2% of 256 px, so `LocalContrast::reduction` lands on
// a half here rather than the quarter a desktop viewport gets — the number
// is the viewport's, and what this test is about is that it does not
// change while the slider moves.
assert_eq!(
pass.detail_dispatches(),
2,
"a separable mask is two passes"
4,
"a reduced separable mask is four passes"
);
let pipelines = pass.cached_detail_pipelines();
@@ -501,17 +588,21 @@ fn dragging_the_slider_re_runs_the_detail_stage_and_nothing_else() {
1,
"the fused colour pass re-ran for a change it does not depend on"
);
assert_eq!(pass.detail_dispatches(), 8);
// Four renders of a four-pass chain. The counter accumulates, so this is
// the claim that every one of those renders ran the detail stage and only
// the detail stage.
assert_eq!(pass.detail_dispatches(), 16);
assert_eq!(
pass.cached_detail_pipelines(),
pipelines,
"an amount is a uniform, not a shader"
);
// Turning on the other control adds its own pair, and only its own pair.
// Turning on the other control adds its own pair, and only its own pair:
// 16, plus clarity's four again, plus texture's two.
graph.set_param(TEXTURE, AMOUNT, 40.0);
render(&mut pass, &graph, &source, 256);
assert_eq!(pass.detail_dispatches(), 12);
assert_eq!(pass.detail_dispatches(), 22);
assert_eq!(pass.colour_dispatches(), 1);
}
@@ -536,7 +627,11 @@ fn the_two_controls_stack_without_overwriting_each_other() {
graph.set_param(TEXTURE, AMOUNT, 80.0);
let mut pass = AdjustPass::new(&ctx);
let both = row(&render(&mut pass, &graph, &source, SIZE), SIZE, SIZE / 2);
assert_eq!(pass.detail_dispatches(), 4);
// Clarity's four plus texture's two. Texture is never reduced — its band
// is a decade finer than clarity's, so a coarser grid could not hold its
// base — and the two operations keeping different pass counts here is that
// asymmetry showing through.
assert_eq!(pass.detail_dispatches(), 6);
let edge = (SIZE / 2) as usize;
// Both sides of the edge move the way local contrast moves them...
+121 -3
View File
@@ -301,6 +301,40 @@ pub struct DetailPass {
/// the kind of artefact that looks like a driver bug. State it honestly.
pub radius: u32,
/// How much smaller than the render this pass writes.
///
/// `1` is the ordinary case and means "the render size", which is what
/// every pass did before this field existed. A larger value writes a
/// target that many times smaller on each axis, into a **second** chain
/// held alongside the full-resolution one — see [`Self::wgsl`] for how the
/// two are addressed, and the module documentation for why there are two.
///
/// # Why a pass may want this
///
/// A blur wide enough to be a *base* — clarity's is 1.2% of the frame,
/// 52 render pixels at 4K — holds no spatial frequency a quarter-scale
/// grid cannot represent. Computing it at the render size therefore buys
/// nothing and costs everything: 105 taps over 8.3 M pixels, twice, which
/// measured at 34 ms and is where `docs/technical-debt.md` TD-4 came from.
/// At a quarter it is a sixteenth of the pixels at a quarter of the
/// radius, and the result is not an approximation of the full-resolution
/// base — it is the same band-limited function, sampled where it is still
/// Nyquist-safe.
///
/// [`Self::radius`] stays in this pass's **own** pixels, so a pass at
/// scale 4 with a radius of 13 declares 13, not 52. The halo it implies
/// for a tile scheduler is `radius * output_scale`, and
/// [`ComposedDetail::radius`] is what performs that multiplication —
/// stating the radius in the grid the loop actually runs in is what keeps
/// the shader and the declaration the same number.
///
/// **Never the last pass.** The final pass carries the output transform
/// and writes the display texture, which is full resolution by
/// definition; a scaled pass in that position is a codegen bug and
/// `dr-gpu` refuses it rather than binding a shader to a target of the
/// wrong size.
pub output_scale: u32,
/// The WGSL body.
///
/// Reads and writes `c`, a `vec3<f32>` of **linear sRGB**, pre-loaded with
@@ -319,6 +353,19 @@ pub struct DetailPass {
/// pixel that survives to the next pass**, pre-loaded with what the
/// previous pass left there and written back out unless the body
/// assigns it.
/// - `reduced_at(coord) -> f32` — the **reduced chain's** scalar at this
/// pixel,
/// bilinearly upsampled. Zero unless a scaled pass ran earlier in this
/// operation; see [`Self::output_scale`].
///
/// `coord` is always in *this pass's own* output grid, and `tap` maps it
/// into the source's grid for you. A pass at [`Self::output_scale`] 4
/// therefore addresses its own quarter-size target with `coord`, while
/// `tap(coord, offset)` offsets in **source** pixels — which is what lets
/// a reduce pass average the 4 x 4 block a single output pixel covers by
/// looping `offset` over it. Where source and target are the same size the
/// mapping is the identity, so every pass written before scaling existed
/// behaves exactly as it did.
///
/// # Why `aux` exists
///
@@ -424,6 +471,8 @@ pub struct ComposedDetailPass {
pub storage: Vec<[f32; 4]>,
/// See [`DetailPass::radius`].
pub radius: u32,
/// See [`DetailPass::output_scale`].
pub output_scale: u32,
/// Whether this pass writes the display/export texture rather than another
/// linear intermediate.
///
@@ -457,8 +506,18 @@ impl ComposedDetail {
}
/// The widest halo any pass needs, in render pixels (ARCH §5.3).
///
/// A pass declares its radius in its own grid, so a scaled pass's has to
/// be multiplied back up before the maxima are comparable: 13 reduced
/// pixels at scale 4 reach exactly as far across the photograph as 52
/// render pixels do, and a scheduler comparing the two unscaled would size
/// a halo at a quarter of what the pass actually reads.
pub fn radius(&self) -> u32 {
self.passes.iter().map(|p| p.radius).max().unwrap_or(0)
self.passes
.iter()
.map(|p| p.radius.saturating_mul(p.output_scale))
.max()
.unwrap_or(0)
}
}
@@ -572,6 +631,7 @@ pub fn compose_detail_with(
RESOLVE_ID,
&[],
&DetailPass {
output_scale: 1,
label: "resolve",
radius: 0,
wgsl: String::new(),
@@ -723,6 +783,25 @@ struct Params {{
// 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>>;
// The reduced chain — what a scaled pass most recently wrote, at whatever
// fraction of the render size it declared. Bound to a 1x1 placeholder for
// every pass that never calls `base`, so that one bind group layout serves a
// pass which uses it and a pass which has never heard of it.
@group(0) @binding(4) var reduced: texture_2d<f32>;
// Where in `source` this output pixel begins.
//
// The ratio is 1 whenever a pass writes what it reads, which is every pass
// that does not set `output_scale` — the multiply and the divide cancel
// exactly, so the ordinary case is unchanged and pays two integer operations
// for the privilege. A scaled pass gets the top-left of the block it covers,
// which is what makes `tap`'s offsets mean *source* pixels and lets a reduce
// pass walk its own footprint.
fn source_origin(coord: vec2<i32>) -> vec2<i32> {{
let src = vec2<i32>(textureDimensions(source));
let dst = vec2<i32>(textureDimensions(output));
return coord * src / max(dst, vec2<i32>(1));
}}
// A neighbour, clamped to the edge of the image.
//
@@ -732,13 +811,48 @@ struct Params {{
// classic way a first convolution goes wrong.
fn tap(coord: vec2<i32>, offset: vec2<i32>) -> vec3<f32> {{
let last = vec2<i32>(textureDimensions(source)) - vec2<i32>(1);
return textureLoad(source, clamp(coord + offset, vec2<i32>(0), last), 0).rgb;
let at = source_origin(coord) + offset;
return textureLoad(source, clamp(at, vec2<i32>(0), last), 0).rgb;
}}
// The same neighbour's scratch lane — see `aux` in the body below.
fn tap_aux(coord: vec2<i32>, offset: vec2<i32>) -> f32 {{
let last = vec2<i32>(textureDimensions(source)) - vec2<i32>(1);
return textureLoad(source, clamp(coord + offset, vec2<i32>(0), last), 0).a;
let at = source_origin(coord) + offset;
return textureLoad(source, clamp(at, vec2<i32>(0), last), 0).a;
}}
// The reduced chain, read at this pass's own resolution.
//
// Named `reduced_at` rather than `base` because `base` is a natural local in a
// body that has just computed one — spot removal already has such a local, and
// a function shadowed by a variable is a compile error a long way from its
// cause.
//
// Bilinear, and on pixel *centres* rather than corners: the reduce pass took
// its sample at the centre of the block it averaged, so an upsample that
// treated the grids as corner-aligned would shift the base by half a reduced
// pixel — two full pixels at scale 4, which on a wide unsharp mask is a base
// offset from the image it is subtracted from, and reads as a directional
// smear along every edge.
//
// Nearest would be cheaper and is not enough: the base is subtracted from the
// full-resolution image, so any blockiness in it appears in the *difference*
// at full contrast. That is a visible 4-pixel grid over the whole frame.
fn reduced_at(coord: vec2<i32>) -> f32 {{
let rd = vec2<f32>(textureDimensions(reduced));
let dst = vec2<f32>(max(textureDimensions(output), vec2<u32>(1u)));
let p = (vec2<f32>(coord) + vec2<f32>(0.5)) * rd / dst - vec2<f32>(0.5);
let last = vec2<i32>(rd) - vec2<i32>(1);
let base_px = vec2<i32>(floor(p));
let f = fract(p);
let s00 = textureLoad(reduced, clamp(base_px, vec2<i32>(0), last), 0).a;
let s10 = textureLoad(reduced, clamp(base_px + vec2<i32>(1, 0), vec2<i32>(0), last), 0).a;
let s01 = textureLoad(reduced, clamp(base_px + vec2<i32>(0, 1), vec2<i32>(0), last), 0).a;
let s11 = textureLoad(reduced, clamp(base_px + vec2<i32>(1, 1), vec2<i32>(0), last), 0).a;
return mix(mix(s00, s10, f.x), mix(s01, s11, f.x), f.y);
}}
{helper_src}{encode_fn}
@@ -786,6 +900,10 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
uniforms: uniform_values,
storage: pass.storage.clone(),
radius: pass.radius,
// Clamped rather than trusted: a zero would divide by nothing in the
// dispatch size and a declaration is data, which since FR-PLG-2 can
// come from a file this build did not write.
output_scale: pass.output_scale.max(1),
writes_output,
structure_hash,
}
+1
View File
@@ -142,6 +142,7 @@ impl DetailStage for BoxBlur {
.iter()
.enumerate()
.map(|(axis, _)| DetailPass {
output_scale: 1,
label: if axis == 0 { "horizontal" } else { "vertical" },
radius: r,
// A convolution, not a list: nothing to bind at binding 3.
@@ -359,6 +359,7 @@ impl DetailStage for CaptureSharpen {
["horizontal", "vertical"]
.into_iter()
.map(|label| DetailPass {
output_scale: 1,
label,
radius: extent,
// A convolution, not a list: nothing to bind at binding 3.
@@ -412,6 +413,7 @@ impl DetailStage for CaptureSharpen {
/// texture, which is a rounding error against the dispatches around it.
fn nothing_to_sharpen() -> DetailPass {
DetailPass {
output_scale: 1,
label: "unresolved",
// Reads only the pixel it writes, so a tile needs no halo at all.
radius: 0,
+414 -56
View File
@@ -141,16 +141,35 @@
//! # What this costs
//!
//! Clarity's kernel is large — of the order of a hundred taps per pass at
//! preview resolution — and the two passes are the honest, exact separable
//! Gaussian rather than a sparse approximation of one. A strided kernel would
//! be several times cheaper and is deliberately not taken: undersampling an
//! image that is not band-limited aliases high-frequency content down into the
//! base, the base is then subtracted, and the aliasing arrives in the output as
//! preview resolution — and each half is the honest, exact separable Gaussian
//! rather than a sparse approximation of one. A strided kernel would be
//! several times cheaper and is deliberately not taken: undersampling an image
//! that is not band-limited aliases high-frequency content down into the base,
//! the base is then subtracted, and the aliasing arrives in the output as
//! low-frequency mottling across smooth gradients. Mottled skies are precisely
//! the artefact this control must not have. The right optimisation is a base
//! computed at reduced resolution, which needs a detail stage that can write a
//! smaller target than it reads; that is a change to [`crate::detail`], not to
//! this file.
//! the artefact this control must not have.
//!
//! Run at the render size, that measured **34 ms at 4K** — seven times the
//! entire fused point chain, for one slider — which is `docs/technical-debt.md`
//! TD-4 and is what [`Recipe::base_scale`] now answers. The base is computed on
//! a grid a quarter the size on each axis: a sixteenth of the pixels at a
//! quarter of the radius.
//!
//! **This is not the strided kernel wearing a hat**, and the difference is
//! exactly the paragraph above. A stride samples an image that is not band-
//! limited and aliases; the reduction *band-limits first* — that is what the
//! `reduce` pass is for and why it is a separate dispatch — and only then
//! samples. What is thrown away is content the base could not represent at any
//! resolution, because a Gaussian at σ = 26 px has nothing above one cycle per
//! 26 px in it and the quarter-scale grid carries one cycle per 8 px. So the
//! reduced base is not an approximation of the full-resolution base; it is the
//! same band-limited function, sampled where it is still fully determined.
//!
//! Which is also why [`LocalContrast::reduction`] steps down and why texture
//! never reduces at all. The argument holds only while the reduced grid can
//! still carry the Gaussian, and the moment it cannot, the honest answer is
//! the full-resolution one — which is the cheap case anyway, because the
//! viewport that produced it is small.
use std::marker::PhantomData;
use std::sync::{Arc, LazyLock};
@@ -176,6 +195,14 @@ pub const AMOUNT: ParamId = ParamId("amount");
/// pipeline.
const TRUNCATION: f32 = 2.0;
/// The smallest σ, in reduced pixels, worth running a Gaussian over.
///
/// One pixel, which with [`TRUNCATION`] is a five-tap kernel — the narrowest
/// that still has a shape. Below it the weights collapse towards a single tap
/// and the blur that survives is the reduce pass's box, which is a different
/// filter with a different edge response. See [`LocalContrast::reduction`].
const MIN_REDUCED_SIGMA: f32 = 1.0;
/// Everything that makes one of these two controls the control it is.
///
/// A struct rather than four associated constants so that the differences
@@ -200,6 +227,15 @@ pub struct Recipe {
/// (4) — true for clarity, false for texture, and that asymmetry is
/// deliberate.
midtone_taper: bool,
/// The most this band's base may be shrunk before it is blurred.
///
/// A ceiling, not the answer — [`LocalContrast::reduction`] steps it down
/// on a viewport too small to carry it. `1` refuses the optimisation
/// outright, which is the only correct value for a band whose σ is already
/// a few pixels.
///
/// Must be a power of two: the step-down halves.
base_scale: u32,
}
/// The band of spatial frequencies a control acts on.
@@ -235,6 +271,22 @@ impl Band for Coarse {
threshold: 0.35,
gain: 1.0,
midtone_taper: true,
// A quarter, which is what `docs/technical-debt.md` TD-4 bought back.
//
// σ is 1.2% of the shorter edge — 26 px at 4K — so the base holds no
// spatial frequency anywhere near the quarter-scale Nyquist of one
// cycle per 8 px. Computing it there is not an approximation of the
// full-resolution base; it is the same band-limited function sampled
// where it is still fully determined. What it costs is a sixteenth of
// the pixels at a quarter of the radius, about a sixty-fourth of the
// work, against the 34 ms this control measured at 4K.
//
// Not an eighth. σ/8 is 3.2 px at 4K and under two on a 1080p
// viewport, which is where the reduce pass's own box filter starts
// doing more of the blurring than the Gaussian does — and the halo
// behaviour this operation is careful about is a property of the
// Gaussian.
base_scale: 4,
};
}
@@ -256,6 +308,13 @@ impl Band for Fine {
// equal numbers on the two sliders should land at comparable strength.
gain: 1.25,
midtone_taper: false,
// Never reduced, and this is the reason the scale belongs to the band
// rather than to the stage. Texture's σ is a decade finer — 2.6 px at
// 4K — so a quarter-scale grid would not hold its base at all: the
// reduce pass's 4x4 box is already wider than the Gaussian it would be
// prefiltering, and what came back would be a blur of the wrong width
// rather than a cheaper blur of the right one.
base_scale: 1,
};
}
@@ -385,6 +444,32 @@ impl<B: Band> LocalContrast<B> {
(self.sigma(scale) * TRUNCATION).round().max(0.0) as u32
}
/// TRACES: FR-DSP-3
/// The factor this render's base is computed at — 1 meaning "the render
/// size", as everything did before TD-4.
///
/// [`Recipe::base_scale`] is a ceiling rather than the answer, because a
/// reduced grid still has to hold a Gaussian. At a quarter of a small
/// viewport clarity's σ falls under a pixel, and a kernel of one or two
/// taps is not a Gaussian — it is the reduce pass's own box filter with a
/// rounding error on top, which would make the control change character on
/// a window resize rather than merely get cheaper.
///
/// So the reduction steps down by halves until the reduced σ is worth
/// convolving: a quarter on a desktop viewport, a half on a small one,
/// none on a thumbnail. Stepping down rather than switching off keeps most
/// of the saving in the middle of the range, and the case it gives up on
/// is the one that was already cheap — the cost is `radius x pixels` and a
/// small viewport is small in both.
pub fn reduction(&self, scale: RenderScale) -> u32 {
let sigma = self.sigma(scale);
let mut reduction = B::RECIPE.base_scale.max(1);
while reduction > 1 && sigma / (reduction as f32) < MIN_REDUCED_SIGMA {
reduction /= 2;
}
reduction
}
/// Stops of local contrast at this slider position.
fn gain(&self) -> f32 {
self.amount / 100.0 * B::RECIPE.gain
@@ -481,27 +566,134 @@ impl<B: Band> DetailStage for LocalContrast<B> {
value: self.gain(),
});
let reduction = self.reduction(scale);
if reduction == 1 {
// The full-resolution form, unchanged: blur x into the scratch
// lane, then finish along y and apply the mask in one pass.
return vec![
DetailPass {
output_scale: 1,
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 {
output_scale: 1,
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, Base::Convolved),
},
];
}
// The reduced form. Four passes rather than two, and cheaper than the
// two by a factor of about `reduction²`: three of them run on a grid
// that many times smaller on each axis, and the one that does not is a
// single bilinear read.
//
// The σ and the radius are the band's own, divided — not recomputed
// from `RenderScale`, which knows nothing about this grid. Deriving
// them from the numbers the full-resolution path uses is what keeps
// the two forms the same filter, so that crossing the threshold in
// `reduction` does not change the picture.
let reduced_sigma = sigma / reduction as f32;
// At least one tap either side. `reduction` has already guaranteed
// σ >= MIN_REDUCED_SIGMA, so this floor is a belt on top of a brace.
let reduced_radius = ((reduced_sigma * TRUNCATION).round() as u32).max(1);
let reduced_shape = vec![
Uniform {
name: "radius",
value: reduced_radius as f32,
},
Uniform {
name: "inv_variance",
value: 1.0 / (reduced_sigma * reduced_sigma),
},
];
// The combining pass no longer convolves anything, so it needs neither
// the radius nor the variance — only the two numbers the unsharp mask
// itself is made of.
let mask = vec![
Uniform {
name: "threshold",
value: B::RECIPE.threshold,
},
Uniform {
name: "gain",
value: self.gain(),
},
];
vec![
DetailPass {
label: "base",
radius,
// A convolution, not a list: nothing to bind at binding 3.
output_scale: reduction,
label: "reduce",
// Reads only the block it writes, so it reaches no further
// than the pixel it is producing and a tile needs no halo for
// it. The halo the *chain* needs comes from the blurs below.
radius: 0,
storage: Vec::new(),
uniforms: shape,
wgsl: BASE_X.to_string(),
uniforms: vec![Uniform {
name: "reduction",
value: reduction as f32,
}],
wgsl: REDUCE.to_string(),
},
DetailPass {
label: "combine",
radius,
// A convolution, not a list: nothing to bind at binding 3.
output_scale: reduction,
label: "base-x",
radius: reduced_radius,
storage: Vec::new(),
uniforms: combine,
wgsl: combine_body(B::RECIPE.midtone_taper),
uniforms: reduced_shape.clone(),
wgsl: reduced_blur(Axis::X),
},
DetailPass {
output_scale: reduction,
label: "base-y",
radius: reduced_radius,
storage: Vec::new(),
uniforms: reduced_shape,
wgsl: reduced_blur(Axis::Y),
},
DetailPass {
output_scale: 1,
label: "combine",
// One bilinear read of the reduced chain, which reaches one
// reduced pixel — `reduction` render pixels — around itself.
// Stated rather than left at zero because an understated
// radius is a tile seam, and a seam is worth more than the
// three lines it costs to be accurate here.
radius: reduction,
storage: Vec::new(),
uniforms: mask,
wgsl: combine_body(B::RECIPE.midtone_taper, Base::Reduced),
},
]
}
}
/// Which way a separable half runs.
enum Axis {
X,
Y,
}
/// Where the combining pass finds the base it subtracts.
enum Base {
/// Convolved along y by the combining pass itself, out of the scratch
/// lane the previous pass wrote. The full-resolution form.
Convolved,
/// Already finished, on the reduced chain, and read back up.
Reduced,
}
/// Half of the base, along x.
///
/// Deliberately does not touch `c`: the pass after this one needs the
@@ -533,13 +725,114 @@ for (var i = -r; i <= r; i = i + 1) {
// exist.
aux = sum / weight;";
/// Band-limit the image onto the reduced grid, in log luminance.
///
/// A pass of its own rather than something the first blur half does on the
/// way past, because it is a different filter doing a different job: this one
/// exists so that the Gaussian's *input* is representable on the coarse grid.
/// Sampling every fourth pixel instead would alias — a shimmer that changes
/// when the viewport is resized, which is the classic way a mip-based blur
/// goes wrong and is very hard to attribute to a clarity slider.
///
/// A box over exactly the block the output pixel covers. Not a wider or
/// prettier prefilter: the Gaussian that follows is 8σ wide on this grid, so
/// what a better prefilter would buy is a correction of a fraction of a
/// reduced pixel to a curve four pixels across, and it would cost taps on the
/// only pass here that reads the full-resolution image.
///
/// **In log luminance, not linear.** The base is a mean of logarithms — that
/// is what makes `detail` a ratio and the whole operation exposure-invariant
/// (see the module documentation, halo control 1). Averaging linear values
/// here and taking the logarithm later is a different number, and the
/// difference is precisely the local contrast this operation exists to
/// measure: it would be quietly subtracted out of every block.
const REDUCE: &str = "\
// The colour rides through untouched, as it does in every pass of this
// operation — though here it is untouched and also unused: the reduced chain
// carries a scalar, and the colour the combining pass subtracts from is the
// full-resolution one it reads from the other chain.
let s = i32(reduction);
var sum = 0.0;
for (var y = 0; y < s; y = y + 1) {
for (var x = 0; x < s; x = x + 1) {
sum = sum + log_luma(tap(coord, vec2<i32>(x, y)));
}
}
aux = sum / f32(s * s);";
/// One half of the reduced separable Gaussian.
///
/// Reads the scratch lane rather than the colour, which is the one line that
/// differs from [`BASE_X`]: by this point the log-luminance conversion has
/// already been done, once, by the reduce pass. Doing it again per tap would
/// be a logarithm inside the inner loop for a value that cannot have changed.
fn reduced_blur(axis: Axis) -> String {
let offset = match axis {
Axis::X => "vec2<i32>(i, 0)",
Axis::Y => "vec2<i32>(0, i)",
};
format!(
"\
// Half of a separable Gaussian over the reduced base, in log luminance.
//
// Weights are evaluated rather than tabulated, as in `BASE_X` and for the same
// reason — and here the loop is a quarter as long, which is the whole point.
let r = i32(radius);
var sum = 0.0;
var weight = 0.0;
for (var i = -r; i <= r; i = i + 1) {{
let f = f32(i);
let w = exp(-0.5 * f * f * inv_variance);
sum = sum + w * tap_aux(coord, {offset});
weight = weight + w;
}}
// Normalised by the weights actually summed, so a kernel clamped at the
// border averages the pixels that exist rather than fading towards zero.
aux = sum / weight;"
)
}
/// The second pass: finish the base along y, then apply the mask.
///
/// Generated rather than constant because the midtone taper is present for
/// clarity and absent for texture. Emitting the line only where it applies
/// keeps texture's shader honest about not having one, and saves it a uniform
/// and two helper functions it would never call.
fn combine_body(midtone_taper: bool) -> String {
fn combine_body(midtone_taper: bool, base: Base) -> String {
// Where the base comes from — the one thing that differs between the two
// forms. Everything below this line is the unsharp mask itself, written
// once, so the reduced form cannot drift into being a different operation
// from the full-resolution one it replaces.
let base = match base {
Base::Convolved => "\
// The other half of the base, then the unsharp mask itself.
//
// `tap_aux` reads the previous pass's log-luminance blur, while `c` is still
// the colour the colour pass produced — which is the arrangement that makes an
// unsharp mask expressible in a chain that hands on one texture per pass.
let r = i32(radius);
var sum = 0.0;
var weight = 0.0;
for (var i = -r; i <= r; i = i + 1) {
let f = f32(i);
let w = exp(-0.5 * f * f * inv_variance);
sum = sum + w * tap_aux(coord, vec2<i32>(0, i));
weight = weight + w;
}
let base = sum / weight;"
.to_string(),
Base::Reduced => "\
// The base, finished on the reduced chain and read back up bilinearly. `c` is
// the full-resolution colour, straight off the other chain — which is why the
// reduced passes had to leave that chain alone, and why this pass reads two
// textures rather than one.
//
// One read where the full-resolution form runs a 105-tap convolution. That
// difference *is* TD-4.
let base = reduced_at(coord);"
.to_string(),
};
let weight = if midtone_taper {
"\n\
// Clarity only: tapered to nothing at both ends of the range. See\n\
@@ -556,21 +849,7 @@ fn combine_body(midtone_taper: bool) -> String {
format!(
"\
// The other half of the base, then the unsharp mask itself.
//
// `tap_aux` reads the previous pass's log-luminance blur, while `c` is still
// the colour the colour pass produced — which is the arrangement that makes an
// unsharp mask expressible in a chain that hands on one texture per pass.
let r = i32(radius);
var sum = 0.0;
var weight = 0.0;
for (var i = -r; i <= r; i = i + 1) {{
let f = f32(i);
let w = exp(-0.5 * f * f * inv_variance);
sum = sum + w * tap_aux(coord, vec2<i32>(0, i));
weight = weight + w;
}}
let base = sum / weight;
{base}
// Local contrast, in **stops**. Both terms are logarithms, so this is a ratio:
// `detail` says how much brighter this pixel is than its surroundings, and
@@ -632,14 +911,34 @@ mod tests {
// forty-pixel blur for a control sitting at zero.
let scale = RenderScale::full((2000, 1500));
let only_clarity = composed(50.0, 0.0, scale);
assert_eq!(only_clarity.len(), 2, "one operation, two passes");
// Four at this viewport, because clarity's base is computed reduced
// here — reduce, two blur halves, combine. The number is the band's
// and the viewport's, not a constant; what this test is about is that
// *all* of them belong to clarity.
assert_eq!(only_clarity.len(), 4, "one operation, its own passes");
assert!(only_clarity
.passes
.iter()
.all(|p| p.label.starts_with("clarity/")));
// Both on: clarity's four plus texture's two. Texture stays at the
// render size whatever the viewport — its band is a decade finer, so
// a reduced grid could not hold its base — and that asymmetry is the
// reason the scale belongs to the band rather than to the stage.
let both = composed(50.0, 50.0, scale);
assert_eq!(both.len(), 4);
assert_eq!(both.len(), 6);
assert_eq!(
both.passes
.iter()
.filter(|p| p.label.starts_with("texture/"))
.count(),
2
);
assert!(both
.passes
.iter()
.filter(|p| p.label.starts_with("texture/"))
.all(|p| p.output_scale == 1));
}
#[test]
@@ -759,21 +1058,46 @@ mod tests {
// passes on one texture per pass. If the blur pass ever writes `c`,
// the combining pass has nothing to subtract the base *from* and the
// operation silently becomes a blur.
let passes = Clarity::with_amount(50.0).passes(RenderScale::full((2000, 1500)));
let base = &passes[0];
let combine = &passes[1];
// Both forms, because they are two chains and the property has to
// hold in each. A thumbnail is too small to carry a reduced base and
// takes the two-pass path; a desktop viewport takes the four-pass one.
for scale in [
RenderScale::full((160, 120)),
RenderScale::full((2000, 1500)),
] {
let passes = Clarity::with_amount(50.0).passes(scale);
let (combine, blurs) = passes.split_last().expect("clarity is active");
assert!(base.wgsl.contains("aux = sum / weight;"));
for blur in blurs {
assert!(
!blur.wgsl.contains("c = "),
"{} must leave the colour alone: {}",
blur.label,
blur.wgsl
);
assert!(
blur.wgsl.contains("aux ="),
"{} must put its result in the scratch lane",
blur.label
);
}
// However the base was arrived at, the mask subtracts it from the
// colour the *colour* pass produced. That is the whole property:
// an unsharp mask needs the blur and the original together, and
// neither chain may have overwritten the original on the way.
assert!(combine.wgsl.contains("log_luma(c) - base"));
}
// And the two forms differ in exactly one place — where `base` came
// from. A reduced combine does no convolution at all.
let reduced = Clarity::with_amount(50.0).passes(RenderScale::full((2000, 1500)));
let combine = reduced.last().expect("clarity is active");
assert!(combine.wgsl.contains("let base = reduced_at(coord);"));
assert!(
!base.wgsl.contains("c = "),
"the blur pass must leave the colour alone: {}",
base.wgsl
!combine.wgsl.contains("tap_aux("),
"a reduced combine has nothing left to convolve"
);
assert!(
combine.wgsl.contains("tap_aux("),
"the combining pass must read the blur out of the scratch lane"
);
assert!(combine.wgsl.contains("log_luma(c) - base"));
}
#[test]
@@ -795,8 +1119,29 @@ mod tests {
let clarity = Clarity::with_amount(50.0).kernel(scale);
assert_eq!(composed.radius(), clarity, "the widest pass sets the halo");
for pass in &composed.passes {
// The reduce pass is the one honest exception: it reads exactly
// the block it writes and no further, so a tile computing it needs
// no halo at all. Every other pass reaches somewhere and must say
// so.
if pass.label.ends_with("/reduce") {
assert_eq!(pass.radius, 0, "the reduce pass reads only its own block");
continue;
}
assert!(pass.radius > 0, "{} declared no reach", pass.label);
}
// The equality above is the claim worth restating: a reduced base
// reaches exactly as far across the photograph as the full-resolution
// one it replaces. `radius x output_scale`, 9 x 4 against 36, which is
// what makes the reduction invisible to a tile scheduler.
let reduced = Clarity::with_amount(50.0);
assert_eq!(reduced.reduction(scale), 4);
let base_x = composed
.passes
.iter()
.find(|p| p.label.ends_with("/base-x"))
.expect("a reduced chain has an x half");
assert_eq!(base_x.radius * base_x.output_scale, clarity);
}
#[test]
@@ -805,7 +1150,8 @@ mod tests {
// saturates at the threshold, so the largest overshoot a full-travel
// slider can produce is `gain * threshold` stops however violent the
// edge — a bound that holds by construction rather than by tuning.
let combine = &Clarity::with_amount(100.0).passes(RenderScale::full((2000, 1500)))[1];
let passes = Clarity::with_amount(100.0).passes(RenderScale::full((2000, 1500)));
let combine = passes.last().expect("clarity is active");
assert!(combine
.wgsl
.contains("threshold * tanh(detail / threshold)"));
@@ -824,7 +1170,8 @@ mod tests {
// Both at full travel, which is what makes them a bound and not a
// measurement: no picture, and no edge in any picture, can produce more.
assert!((bound(&combine.uniforms) - 0.35).abs() < 1e-6);
let texture = &Texture::with_amount(100.0).passes(RenderScale::full((2000, 1500)))[1];
let texture_passes = Texture::with_amount(100.0).passes(RenderScale::full((2000, 1500)));
let texture = texture_passes.last().expect("texture is active");
assert!((bound(&texture.uniforms) - 1.25).abs() < 1e-6);
}
@@ -834,8 +1181,10 @@ mod tests {
// highlight and fabric in a shadow, which is exactly where clarity
// must not.
let scale = RenderScale::full((2000, 1500));
let clarity = &Clarity::with_amount(50.0).passes(scale)[1];
let texture = &Texture::with_amount(50.0).passes(scale)[1];
let clarity_passes = Clarity::with_amount(50.0).passes(scale);
let texture_passes = Texture::with_amount(50.0).passes(scale);
let clarity = clarity_passes.last().expect("clarity is active");
let texture = texture_passes.last().expect("texture is active");
assert!(clarity.wgsl.contains("midtone_weight(luminance(c))"));
assert!(!texture.wgsl.contains("midtone_weight"));
@@ -865,15 +1214,24 @@ mod tests {
let up = Clarity::with_amount(50.0).passes(scale);
let down = Clarity::with_amount(-50.0).passes(scale);
let gain = |p: &[DetailPass]| {
p[1].uniforms
p.last()
.expect("clarity is active")
.uniforms
.iter()
.find(|u| u.name == "gain")
.unwrap()
.value
};
assert!((gain(&up) + gain(&down)).abs() < 1e-6);
// Same kernel either way — the direction is a sign, not a scale.
assert_eq!(up[0].radius, down[0].radius);
// Same kernel either way — the direction is a sign, not a scale. Read
// off the blur halves rather than the first pass, which since the
// reduced base exists is a downscale carrying no radius at all.
let radii = |p: &[DetailPass]| {
p.iter()
.map(|d| (d.radius, d.output_scale))
.collect::<Vec<_>>()
};
assert_eq!(radii(&up), radii(&down));
}
#[test]
@@ -437,6 +437,7 @@ impl DetailStage for NoiseReduction {
let luma = self.luminance_kernel(scale);
if luma > 0 {
passes.push(DetailPass {
output_scale: 1,
label: "luminance",
radius: luma,
// A convolution, not a list: nothing to bind at binding 3.
@@ -473,6 +474,7 @@ impl DetailStage for NoiseReduction {
// from the other and the result acquires a diagonal bias.
for (index, (sx, sy)) in [(1.0, 0.0), (0.0, 1.0)].into_iter().enumerate() {
passes.push(DetailPass {
output_scale: 1,
label: if index == 0 {
"chroma-horizontal"
} else {
+1
View File
@@ -764,6 +764,7 @@ impl SpotSet {
}
passes.push(DetailPass {
output_scale: 1,
label: round_label(round),
radius: reach.ceil() as u32,
wgsl: SPOT_BODY.to_string(),
+6 -6
View File
@@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
| Metric | Value |
|---|---|
| Source files scanned | 290 |
| TRACES tags found | 843 |
| TRACES tags found | 845 |
| Requirements defined | 179 |
| Requirements covered | 107 |
| **Coverage** | **59.8%** (107/179) |
@@ -57,11 +57,11 @@ _None._
| FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:401`](../core/dr-catalog/src/schema.rs#L401), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:2849`](../ui/dr-ui/src/library.rs#L2849), [`ui/dr-ui/src/library.rs:2953`](../ui/dr-ui/src/library.rs#L2953), [`ui/dr-ui/ui/settings.slint:404`](../ui/dr-ui/ui/settings.slint#L404), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) |
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) |
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389) |
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:620`](../core/dr-pipeline/src/framing.rs#L620), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`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:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:1696`](../core/dr-pipeline/src/sidecar.rs#L1696), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1297`](../ui/dr-ui/src/develop.rs#L1297), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1775`](../ui/dr-ui/src/develop.rs#L1775), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:1807`](../ui/dr-ui/src/develop.rs#L1807), [`ui/dr-ui/src/develop.rs:1829`](../ui/dr-ui/src/develop.rs#L1829), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3963`](../ui/dr-ui/src/develop.rs#L3963), [`ui/dr-ui/src/develop.rs:4007`](../ui/dr-ui/src/develop.rs#L4007), [`ui/dr-ui/src/develop.rs:4057`](../ui/dr-ui/src/develop.rs#L4057), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1461`](../ui/dr-ui/src/lib.rs#L1461), [`ui/dr-ui/src/lib.rs:2162`](../ui/dr-ui/src/lib.rs#L2162), [`ui/dr-ui/src/lib.rs:319`](../ui/dr-ui/src/lib.rs#L319), [`ui/dr-ui/src/library.rs:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:1766`](../ui/dr-ui/ui/app.slint#L1766), [`ui/dr-ui/ui/app.slint:793`](../ui/dr-ui/ui/app.slint#L793), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) |
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/framing.rs:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:620`](../core/dr-pipeline/src/framing.rs#L620), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`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:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:1696`](../core/dr-pipeline/src/sidecar.rs#L1696), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1297`](../ui/dr-ui/src/develop.rs#L1297), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1775`](../ui/dr-ui/src/develop.rs#L1775), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:1807`](../ui/dr-ui/src/develop.rs#L1807), [`ui/dr-ui/src/develop.rs:1829`](../ui/dr-ui/src/develop.rs#L1829), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3963`](../ui/dr-ui/src/develop.rs#L3963), [`ui/dr-ui/src/develop.rs:4007`](../ui/dr-ui/src/develop.rs#L4007), [`ui/dr-ui/src/develop.rs:4057`](../ui/dr-ui/src/develop.rs#L4057), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1461`](../ui/dr-ui/src/lib.rs#L1461), [`ui/dr-ui/src/lib.rs:2162`](../ui/dr-ui/src/lib.rs#L2162), [`ui/dr-ui/src/lib.rs:319`](../ui/dr-ui/src/lib.rs#L319), [`ui/dr-ui/src/library.rs:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:1766`](../ui/dr-ui/ui/app.slint#L1766), [`ui/dr-ui/ui/app.slint:793`](../ui/dr-ui/ui/app.slint#L793), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) |
| FR-DEV-3a | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:194`](../core/dr-pipeline/src/descriptor.rs#L194), [`core/dr-pipeline/src/descriptor.rs:234`](../core/dr-pipeline/src/descriptor.rs#L234), [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/descriptor.rs:313`](../core/dr-pipeline/src/descriptor.rs#L313), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365), [`core/dr-pipeline/src/ops/curve.rs:319`](../core/dr-pipeline/src/ops/curve.rs#L319), [`ui/dr-ui/src/develop.rs:1194`](../ui/dr-ui/src/develop.rs#L1194), [`ui/dr-ui/src/lib.rs:616`](../ui/dr-ui/src/lib.rs#L616), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../ui/dr-ui/tests/ui_names_no_operation.rs#L1) |
| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365) |
| FR-DEV-3c | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`ui/dr-ui/src/develop.rs:4636`](../ui/dr-ui/src/develop.rs#L4636) |
| FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`core/dr-gpu/tests/capture_sharpen.rs:434`](../core/dr-gpu/tests/capture_sharpen.rs#L434), [`core/dr-gpu/tests/detail_stage.rs:242`](../core/dr-gpu/tests/detail_stage.rs#L242), [`core/dr-gpu/tests/local_contrast.rs:476`](../core/dr-gpu/tests/local_contrast.rs#L476), [`core/dr-gpu/tests/noise_reduction.rs:556`](../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/graph.rs:616`](../core/dr-pipeline/src/graph.rs#L616), [`core/dr-pipeline/src/operation.rs:32`](../core/dr-pipeline/src/operation.rs#L32), [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389), [`core/dr-pipeline/src/operation.rs:53`](../core/dr-pipeline/src/operation.rs#L53), [`core/dr-pipeline/src/operation.rs:71`](../core/dr-pipeline/src/operation.rs#L71) |
| FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`core/dr-gpu/tests/capture_sharpen.rs:434`](../core/dr-gpu/tests/capture_sharpen.rs#L434), [`core/dr-gpu/tests/detail_stage.rs:242`](../core/dr-gpu/tests/detail_stage.rs#L242), [`core/dr-gpu/tests/local_contrast.rs:558`](../core/dr-gpu/tests/local_contrast.rs#L558), [`core/dr-gpu/tests/noise_reduction.rs:556`](../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/graph.rs:616`](../core/dr-pipeline/src/graph.rs#L616), [`core/dr-pipeline/src/operation.rs:32`](../core/dr-pipeline/src/operation.rs#L32), [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389), [`core/dr-pipeline/src/operation.rs:53`](../core/dr-pipeline/src/operation.rs#L53), [`core/dr-pipeline/src/operation.rs:71`](../core/dr-pipeline/src/operation.rs#L71) |
| FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`core/dr-decode/src/profile.rs:102`](../core/dr-decode/src/profile.rs#L102), [`core/dr-decode/src/profile.rs:151`](../core/dr-decode/src/profile.rs#L151), [`core/dr-decode/src/profile.rs:1`](../core/dr-decode/src/profile.rs#L1), [`core/dr-decode/src/profile.rs:235`](../core/dr-decode/src/profile.rs#L235), [`core/dr-decode/src/profile.rs:286`](../core/dr-decode/src/profile.rs#L286), [`core/dr-decode/src/profile.rs:343`](../core/dr-decode/src/profile.rs#L343), [`core/dr-decode/src/profile.rs:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:967`](../core/dr-gpu/src/adjust.rs#L967), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`core/dr-gpu/src/demosaic.rs:86`](../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:1576`](../core/dr-pipeline/src/operation.rs#L1576), [`core/dr-pipeline/src/operation.rs:1601`](../core/dr-pipeline/src/operation.rs#L1601), [`core/dr-pipeline/src/operation.rs:1616`](../core/dr-pipeline/src/operation.rs#L1616), [`core/dr-pipeline/src/operation.rs:1640`](../core/dr-pipeline/src/operation.rs#L1640), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/operation.rs:440`](../core/dr-pipeline/src/operation.rs#L440), [`core/dr-pipeline/src/operation.rs:450`](../core/dr-pipeline/src/operation.rs#L450), [`core/dr-pipeline/src/operation.rs:600`](../core/dr-pipeline/src/operation.rs#L600) |
| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/boolean_grain.rs:1`](../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../core/dr-film/src/lib.rs#L160), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:101`](../core/dr-pipeline/src/graph.rs#L101), [`core/dr-pipeline/src/graph.rs:124`](../core/dr-pipeline/src/graph.rs#L124), [`core/dr-pipeline/src/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`core/dr-pipeline/src/operation.rs:1065`](../core/dr-pipeline/src/operation.rs#L1065), [`core/dr-pipeline/src/operation.rs:1094`](../core/dr-pipeline/src/operation.rs#L1094), [`core/dr-pipeline/src/operation.rs:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:296`](../core/dr-pipeline/src/operation.rs#L296), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/ops/film_sim.rs:129`](../core/dr-pipeline/src/ops/film_sim.rs#L129), [`core/dr-pipeline/src/ops/film_sim.rs:153`](../core/dr-pipeline/src/ops/film_sim.rs#L153), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:331`](../core/dr-pipeline/src/ops/film_sim.rs#L331), [`core/dr-pipeline/src/ops/film_sim.rs:43`](../core/dr-pipeline/src/ops/film_sim.rs#L43), [`core/dr-pipeline/src/ops/film_sim.rs:87`](../core/dr-pipeline/src/ops/film_sim.rs#L87), [`core/dr-pipeline/src/ops/film_sim.rs:92`](../core/dr-pipeline/src/ops/film_sim.rs#L92), [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:167`](../core/dr-pipeline/src/sidecar.rs#L167), [`core/dr-pipeline/src/sidecar.rs:1957`](../core/dr-pipeline/src/sidecar.rs#L1957), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:533`](../core/dr-pipeline/src/sidecar.rs#L533), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:792`](../core/dr-pipeline/src/sidecar.rs#L792), [`core/dr-pipeline/src/state.rs:100`](../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop.rs:2885`](../ui/dr-ui/src/develop.rs#L2885), [`ui/dr-ui/src/develop.rs:2902`](../ui/dr-ui/src/develop.rs#L2902), [`ui/dr-ui/src/develop.rs:2914`](../ui/dr-ui/src/develop.rs#L2914), [`ui/dr-ui/src/develop.rs:2952`](../ui/dr-ui/src/develop.rs#L2952), [`ui/dr-ui/src/develop.rs:2961`](../ui/dr-ui/src/develop.rs#L2961), [`ui/dr-ui/src/develop.rs:3064`](../ui/dr-ui/src/develop.rs#L3064), [`ui/dr-ui/src/develop.rs:3382`](../ui/dr-ui/src/develop.rs#L3382), [`ui/dr-ui/src/develop.rs:3397`](../ui/dr-ui/src/develop.rs#L3397), [`ui/dr-ui/src/lib.rs:2136`](../ui/dr-ui/src/lib.rs#L2136), [`ui/dr-ui/src/lib.rs:549`](../ui/dr-ui/src/lib.rs#L549), [`ui/dr-ui/src/lib.rs:607`](../ui/dr-ui/src/lib.rs#L607), [`ui/dr-ui/src/library.rs:497`](../ui/dr-ui/src/library.rs#L497), [`ui/dr-ui/src/library.rs:745`](../ui/dr-ui/src/library.rs#L745), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:1006`](../ui/dr-ui/ui/adjust.slint#L1006), [`ui/dr-ui/ui/adjust.slint:924`](../ui/dr-ui/ui/adjust.slint#L924), [`ui/dr-ui/ui/app.slint:2256`](../ui/dr-ui/ui/app.slint#L2256), [`ui/dr-ui/ui/app.slint:571`](../ui/dr-ui/ui/app.slint#L571) |
| FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:205`](../core/dr-pipeline/src/framing.rs#L205), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:927`](../core/dr-pipeline/src/framing.rs#L927), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/lib.rs:456`](../core/dr-types/src/lib.rs#L456), [`core/dr-types/src/lib.rs:472`](../core/dr-types/src/lib.rs#L472), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:1992`](../ui/dr-ui/src/develop.rs#L1992), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) |
@@ -69,9 +69,9 @@ _None._
| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`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), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:2914`](../ui/dr-ui/src/develop.rs#L2914), [`ui/dr-ui/src/develop.rs:3397`](../ui/dr-ui/src/develop.rs#L3397), [`ui/dr-ui/src/develop.rs:3427`](../ui/dr-ui/src/develop.rs#L3427), [`ui/dr-ui/src/develop.rs:3440`](../ui/dr-ui/src/develop.rs#L3440), [`ui/dr-ui/src/develop.rs:3452`](../ui/dr-ui/src/develop.rs#L3452), [`ui/dr-ui/src/develop.rs:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/develop.rs:3504`](../ui/dr-ui/src/develop.rs#L3504), [`ui/dr-ui/src/develop.rs:3523`](../ui/dr-ui/src/develop.rs#L3523), [`ui/dr-ui/src/develop.rs:3539`](../ui/dr-ui/src/develop.rs#L3539), [`ui/dr-ui/src/develop.rs:610`](../ui/dr-ui/src/develop.rs#L610), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1409`](../ui/dr-ui/src/lib.rs#L1409), [`ui/dr-ui/src/lib.rs:1439`](../ui/dr-ui/src/lib.rs#L1439), [`ui/dr-ui/src/lib.rs:1447`](../ui/dr-ui/src/lib.rs#L1447), [`ui/dr-ui/src/lib.rs:2332`](../ui/dr-ui/src/lib.rs#L2332), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/develop.rs:3360`](../ui/dr-ui/src/develop.rs#L3360), [`ui/dr-ui/src/lib.rs:1376`](../ui/dr-ui/src/lib.rs#L1376), [`ui/dr-ui/src/library.rs:1787`](../ui/dr-ui/src/library.rs#L1787), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library.rs:487`](../ui/dr-ui/src/library.rs#L487), [`ui/dr-ui/src/library_ui.rs:2592`](../ui/dr-ui/src/library_ui.rs#L2592), [`ui/dr-ui/src/library_ui.rs:2992`](../ui/dr-ui/src/library_ui.rs#L2992), [`ui/dr-ui/src/library_ui.rs:472`](../ui/dr-ui/src/library_ui.rs#L472), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:549`](../ui/dr-ui/src/settings_ui.rs#L549), [`ui/dr-ui/ui/adjust.slint:613`](../ui/dr-ui/ui/adjust.slint#L613), [`ui/dr-ui/ui/library.slint:1328`](../ui/dr-ui/ui/library.slint#L1328), [`ui/dr-ui/ui/library.slint:837`](../ui/dr-ui/ui/library.slint#L837), [`ui/dr-ui/ui/library.slint:919`](../ui/dr-ui/ui/library.slint#L919), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100) |
| 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.rs:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:1447`](../ui/dr-ui/src/lib.rs#L1447), [`ui/dr-ui/src/lib.rs:2332`](../ui/dr-ui/src/lib.rs#L2332), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`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:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:685`](../core/dr-pipeline/src/graph.rs#L685), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`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.rs:2144`](../ui/dr-ui/src/develop.rs#L2144), [`ui/dr-ui/src/develop.rs:2182`](../ui/dr-ui/src/develop.rs#L2182), [`ui/dr-ui/src/develop.rs:2247`](../ui/dr-ui/src/develop.rs#L2247), [`ui/dr-ui/src/develop.rs:2330`](../ui/dr-ui/src/develop.rs#L2330), [`ui/dr-ui/src/develop.rs:2344`](../ui/dr-ui/src/develop.rs#L2344), [`ui/dr-ui/src/develop.rs:658`](../ui/dr-ui/src/develop.rs#L658), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1468`](../ui/dr-ui/src/lib.rs#L1468), [`ui/dr-ui/src/lib.rs:2429`](../ui/dr-ui/src/lib.rs#L2429), [`ui/dr-ui/src/lib.rs:324`](../ui/dr-ui/src/lib.rs#L324), [`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/adjust.slint:700`](../ui/dr-ui/ui/adjust.slint#L700), [`ui/dr-ui/ui/app.slint:108`](../ui/dr-ui/ui/app.slint#L108), [`ui/dr-ui/ui/app.slint:1671`](../ui/dr-ui/ui/app.slint#L1671), [`ui/dr-ui/ui/app.slint:1844`](../ui/dr-ui/ui/app.slint#L1844), [`ui/dr-ui/ui/app.slint:2162`](../ui/dr-ui/ui/app.slint#L2162), [`ui/dr-ui/ui/spots.slint:180`](../ui/dr-ui/ui/spots.slint#L180), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) |
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`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:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`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:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:547`](../core/dr-pipeline/src/graph.rs#L547), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`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:657`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L657), [`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:672`](../core/dr-pipeline/src/ops/local_contrast.rs#L672), [`core/dr-pipeline/src/ops/noise_reduction.rs:698`](../core/dr-pipeline/src/ops/noise_reduction.rs#L698), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2668`](../ui/dr-ui/src/develop.rs#L2668), [`ui/dr-ui/src/develop.rs:3698`](../ui/dr-ui/src/develop.rs#L3698), [`ui/dr-ui/src/develop.rs:4181`](../ui/dr-ui/src/develop.rs#L4181), [`ui/dr-ui/src/develop.rs:4215`](../ui/dr-ui/src/develop.rs#L4215), [`ui/dr-ui/src/lib.rs:72`](../ui/dr-ui/src/lib.rs#L72), [`ui/dr-ui/src/lib.rs:757`](../ui/dr-ui/src/lib.rs#L757), [`ui/dr-ui/src/lib.rs:816`](../ui/dr-ui/src/lib.rs#L816) |
| FR-DSP-3 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101) |
| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:387`](../core/dr-gpu/src/detail.rs#L387), [`core/dr-gpu/src/detail.rs:592`](../core/dr-gpu/src/detail.rs#L592), [`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:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:469`](../core/dr-pipeline/src/detail.rs#L469), [`core/dr-pipeline/src/detail.rs:555`](../core/dr-pipeline/src/detail.rs#L555), [`core/dr-pipeline/src/graph.rs:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:685`](../core/dr-pipeline/src/graph.rs#L685), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`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.rs:2144`](../ui/dr-ui/src/develop.rs#L2144), [`ui/dr-ui/src/develop.rs:2182`](../ui/dr-ui/src/develop.rs#L2182), [`ui/dr-ui/src/develop.rs:2247`](../ui/dr-ui/src/develop.rs#L2247), [`ui/dr-ui/src/develop.rs:2330`](../ui/dr-ui/src/develop.rs#L2330), [`ui/dr-ui/src/develop.rs:2344`](../ui/dr-ui/src/develop.rs#L2344), [`ui/dr-ui/src/develop.rs:658`](../ui/dr-ui/src/develop.rs#L658), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1468`](../ui/dr-ui/src/lib.rs#L1468), [`ui/dr-ui/src/lib.rs:2429`](../ui/dr-ui/src/lib.rs#L2429), [`ui/dr-ui/src/lib.rs:324`](../ui/dr-ui/src/lib.rs#L324), [`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/adjust.slint:700`](../ui/dr-ui/ui/adjust.slint#L700), [`ui/dr-ui/ui/app.slint:108`](../ui/dr-ui/ui/app.slint#L108), [`ui/dr-ui/ui/app.slint:1671`](../ui/dr-ui/ui/app.slint#L1671), [`ui/dr-ui/ui/app.slint:1844`](../ui/dr-ui/ui/app.slint#L1844), [`ui/dr-ui/ui/app.slint:2162`](../ui/dr-ui/ui/app.slint#L2162), [`ui/dr-ui/ui/spots.slint:180`](../ui/dr-ui/ui/spots.slint#L180), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) |
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`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:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/graph.rs:547`](../core/dr-pipeline/src/graph.rs#L547), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`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/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.rs:2668`](../ui/dr-ui/src/develop.rs#L2668), [`ui/dr-ui/src/develop.rs:3698`](../ui/dr-ui/src/develop.rs#L3698), [`ui/dr-ui/src/develop.rs:4181`](../ui/dr-ui/src/develop.rs#L4181), [`ui/dr-ui/src/develop.rs:4215`](../ui/dr-ui/src/develop.rs#L4215), [`ui/dr-ui/src/lib.rs:72`](../ui/dr-ui/src/lib.rs#L72), [`ui/dr-ui/src/lib.rs:757`](../ui/dr-ui/src/lib.rs#L757), [`ui/dr-ui/src/lib.rs:816`](../ui/dr-ui/src/lib.rs#L816) |
| 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-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) |
| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:483`](../core/dr-pipeline/src/operation.rs#L483), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`ui/dr-ui/src/develop.rs:2698`](../ui/dr-ui/src/develop.rs#L2698), [`ui/dr-ui/src/develop.rs:5473`](../ui/dr-ui/src/develop.rs#L5473), [`ui/dr-ui/src/lib.rs:1424`](../ui/dr-ui/src/lib.rs#L1424), [`ui/dr-ui/src/lib.rs:2635`](../ui/dr-ui/src/lib.rs#L2635) |
| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2747`](../ui/dr-ui/src/develop.rs#L2747), [`ui/dr-ui/src/develop.rs:5283`](../ui/dr-ui/src/develop.rs#L5283), [`ui/dr-ui/src/develop.rs:5315`](../ui/dr-ui/src/develop.rs#L5315), [`ui/dr-ui/src/develop.rs:621`](../ui/dr-ui/src/develop.rs#L621), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1524`](../ui/dr-ui/src/lib.rs#L1524), [`ui/dr-ui/src/lib.rs:314`](../ui/dr-ui/src/lib.rs#L314), [`ui/dr-ui/ui/app.slint:68`](../ui/dr-ui/ui/app.slint#L68), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) |