Let a detail pass carry a list, not only a kernel

Every neighbourhood pass so far has been a convolution, whose whole
description fits in the uniform block because its structure fixes how many
numbers it needs. Spot removal is not that shape: sixty-four repairs and
one repair are the same shader with a different buffer behind it.

So a pass may declare `storage`, which arrives at binding 3 as
`array<vec4<f32>>` with `arrayLength` in scope. The alternative — packing
the list into uniforms — needs a fixed maximum paid for on every frame, a
composer that can emit vec4 fields because a uniform array's stride is 16
whatever it holds, and it gives the next operation that wants a table
nothing to build on.

The property worth having is what stays out of the generated source: the
count is in the buffer, so placing the tenth spot uploads 512 bytes and
reuses the compiled pipeline, exactly as moving a slider does for the
fused pass. `changing_the_list_does_not_recompile` is that, asserted.

One bind group entry rather than two more layouts, and one placeholder
buffer allocated in `new` rather than sixteen bytes per pass per frame —
a zero-length storage buffer cannot be bound, and per-frame allocation is
what this module's documentation exists to refuse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 20:00:24 +02:00
co-authored by Claude Opus 5
parent 44444f7768
commit 6997c0f7ac
8 changed files with 287 additions and 5 deletions
+63
View File
@@ -149,6 +149,8 @@ pub(crate) struct DetailRunner {
/// Compiled pipelines by pass structure hash.
cache: HashMap<u64, wgpu::ComputePipeline>,
pool: Intermediates,
/// See [`placeholder_instances`].
no_instances: wgpu::Buffer,
}
struct Layout {
@@ -156,6 +158,22 @@ struct Layout {
pipeline: wgpu::PipelineLayout,
}
/// What binding 3 holds for a pass that declared no instance list.
///
/// One zeroed element, allocated once. Zero-length storage buffers cannot be
/// bound, and the passes that read this binding are exactly the ones that
/// uploaded something of their own, so nothing ever reads the placeholder's
/// contents — it exists to keep one bind group layout serving both kinds of
/// pass.
fn placeholder_instances(ctx: &GpuContext) -> wgpu::Buffer {
ctx.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("detail-instances-placeholder"),
contents: bytemuck::cast_slice(&[[0.0f32; 4]]),
usage: wgpu::BufferUsages::STORAGE,
})
}
impl DetailRunner {
pub(crate) fn new(ctx: &GpuContext) -> Self {
Self {
@@ -164,6 +182,7 @@ impl DetailRunner {
to_output: Layout::new(ctx, crate::AdjustPass::FORMAT, "detail-output"),
cache: HashMap::new(),
pool: Intermediates::new(),
no_instances: placeholder_instances(ctx),
}
}
@@ -230,6 +249,24 @@ impl DetailRunner {
usage: wgpu::BufferUsages::UNIFORM,
});
// TRACES: FR-DEV-8
// The instance list, uploaded only by the passes that have one. A
// kernel pass — which is every pass that is a convolution — is
// handed the placeholder allocated once in `new`, because a storage
// buffer of length zero is not bindable and allocating a fresh
// sixteen bytes per pass per frame is the per-frame allocation this
// module's documentation exists to refuse.
let instances = (!pass.storage.is_empty()).then(|| {
self.ctx
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("detail-instances"),
contents: bytemuck::cast_slice(pass.storage.as_slice()),
usage: wgpu::BufferUsages::STORAGE,
})
});
let instances = instances.as_ref().unwrap_or(&self.no_instances);
let bind_group = self
.ctx
.device
@@ -249,6 +286,10 @@ impl DetailRunner {
binding: 2,
resource: wgpu::BindingResource::TextureView(destination),
},
wgpu::BindGroupEntry {
binding: 3,
resource: instances.as_entire_binding(),
},
],
});
@@ -338,6 +379,20 @@ impl DetailRunner {
}
}
/// A read-only storage buffer entry, as `mask.rs` declares its strokes.
fn storage_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}
impl Layout {
fn new(ctx: &GpuContext, format: wgpu::TextureFormat, label: &str) -> Self {
let bind_group = ctx
@@ -376,6 +431,14 @@ impl Layout {
},
count: None,
},
// TRACES: FR-DEV-8
// The instance list, for a pass whose work is a list rather
// than a kernel (`DetailPass::storage`). Every other pass
// gets `Intermediates`' placeholder here — one entry on both
// layouts rather than two more layouts, since a convolution
// that never reads the buffer costs nothing for it being
// bound.
storage_entry(3),
],
});
+169
View File
@@ -0,0 +1,169 @@
//! TRACES: FR-DEV-8
//! The instance binding: a detail pass whose work is a list, not a kernel.
//!
//! Spot removal needs a detail pass to read a variable number of records —
//! sixty-four repairs and one repair are the same shader with a different
//! buffer behind it. That is binding 3, and this file proves the three things
//! about it that a picture would not tell you clearly:
//!
//! - the data uploaded is the data the shader reads, in order;
//! - a pass that declares no list still runs, bound to the placeholder;
//! - the same shader with a *different* list does not recompile, which is what
//! keeps placing a spot as cheap as moving a slider.
//!
//! The passes here are synthetic on purpose. `spot_removal.rs` asserts the
//! repair; this asserts the plumbing, so a failure in one does not have to be
//! read to work out which of the two broke.
use dr_gpu::{AdjustPass, DemosaicedImage, GpuContext};
use dr_pipeline::detail::{ComposedDetail, ComposedDetailPass};
use dr_pipeline::{Affects, EditGraph};
use dr_types::ColourSpace;
const SIZE: u32 = 8;
fn ctx() -> Option<GpuContext> {
match pollster::block_on(GpuContext::new_headless()) {
Ok(c) => Some(c),
Err(e) => {
eprintln!("skipping: no GPU adapter ({e})");
None
}
}
}
/// A flat mid-grey frame, so anything the pass adds is the whole answer.
fn grey(ctx: &GpuContext) -> DemosaicedImage {
let data: Vec<u8> = (0..SIZE * SIZE).flat_map(|_| [0u8, 0, 0, 255]).collect();
DemosaicedImage::from_rgba8(ctx, &data, SIZE, SIZE).expect("upload")
}
/// A pass that sums the instance list into the red channel and writes the
/// output. Deliberately trivial: the value on screen is then a direct readout
/// of what arrived in the buffer.
fn summing_pass(storage: Vec<[f32; 4]>, structure: u64) -> ComposedDetailPass {
let source = "
@group(0) @binding(0) var source: texture_2d<f32>;
struct Params { detail_base: vec4<f32> }
@group(0) @binding(1) var<uniform> u: Params;
@group(0) @binding(2) var output: texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(3) var<storage, read> instances: array<vec4<f32>>;
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = textureDimensions(output);
if (gid.x >= dims.x || gid.y >= dims.y) { return; }
// Weighted by index, so a buffer read back to front fails this rather
// than passing by symmetry.
var total = 0.0;
let n = arrayLength(&instances);
for (var i = 0u; i < n; i = i + 1u) {
total = total + instances[i].x * f32(i + 1u);
}
textureStore(output, vec2<i32>(gid.xy), vec4<f32>(total, f32(n) / 255.0, 0.0, 1.0));
}
"
.to_string();
ComposedDetailPass {
label: "test/instances".to_string(),
source,
uniforms: vec![SIZE as f32, SIZE as f32, 1.0, 0.0],
storage,
radius: 0,
writes_output: true,
// Any distinct number: the hash is a cache key, and these tests are
// what decide whether two chains share a pipeline.
structure_hash: structure,
}
}
fn render(pass: &mut AdjustPass, source: &DemosaicedImage, chain: &ComposedDetail) -> Vec<u8> {
// The fused half has to be composed knowing a detail stage follows it, or
// it encodes its own output and the chain would quantise twice — a mismatch
// `render_detailed` refuses outright. The probe is the graph that says so;
// its own passes are not used, since the chain here is hand-built.
let mut graph = EditGraph::with_detail_probe();
graph.set_param(
dr_pipeline::descriptor::OpId("detail_probe"),
dr_pipeline::descriptor::ParamId("radius"),
0.05,
);
let shader = graph.compose_for(ColourSpace::Srgb);
let key = graph.invalidation().through(Affects::Colour);
pass.render_detailed(source, &shader, SIZE, SIZE, None, chain, key)
.expect("render");
pass.export_pixels().expect("readback").0
}
/// The list arrives whole, in order, and the shader can tell how long it is.
#[test]
fn a_pass_reads_the_list_it_was_given() {
let Some(ctx) = ctx() else { return };
let source = grey(&ctx);
let mut pass = AdjustPass::new(&ctx);
// 0.1·1 + 0.2·2 + 0.3·3 = 1.4, which clips to 1.0 — so instead: values
// chosen to land at a quarter, unambiguously distinguishable from both the
// "read nothing" answer of 0 and the "read them unweighted" answer of 0.15.
let chain = ComposedDetail {
passes: vec![summing_pass(
vec![[0.05, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0]],
1,
)],
};
let pixels = render(&mut pass, &source, &chain);
let (red, green) = (pixels[0], pixels[1]);
// 0.05·1 + 0.1·2 = 0.25, written straight to an rgba8 target.
assert!(
red.abs_diff((0.25 * 255.0) as u8) <= 1,
"the shader summed {red}, not the list it was handed"
);
assert_eq!(green, 2, "arrayLength saw both entries");
}
/// A convolution declares no list and must still run: it is bound to the
/// placeholder rather than to nothing, because a zero-length storage buffer
/// cannot be bound at all and a second bind group layout for the difference
/// would be two layouts to keep in step.
#[test]
fn a_pass_with_no_list_still_runs() {
let Some(ctx) = ctx() else { return };
let source = grey(&ctx);
let mut pass = AdjustPass::new(&ctx);
let chain = ComposedDetail {
passes: vec![summing_pass(Vec::new(), 2)],
};
let pixels = render(&mut pass, &source, &chain);
assert_eq!(pixels[0], 0, "the placeholder is zeroed");
assert_eq!(pixels[1], 1, "and is exactly one element long");
}
/// The property that makes placing the tenth spot as cheap as moving a slider:
/// the list is in the buffer, not in the source, so the pipeline is compiled
/// once however many entries arrive.
#[test]
fn changing_the_list_does_not_recompile() {
let Some(ctx) = ctx() else { return };
let source = grey(&ctx);
let mut pass = AdjustPass::new(&ctx);
for count in 1..=6 {
let list = (0..count).map(|_| [0.01, 0.0, 0.0, 0.0]).collect();
let chain = ComposedDetail {
passes: vec![summing_pass(list, 3)],
};
render(&mut pass, &source, &chain);
}
assert_eq!(
pass.cached_detail_pipelines(),
1,
"six different lists, one compiled pipeline"
);
}
+36
View File
@@ -359,6 +359,29 @@ pub struct DetailPass {
/// Uniform values this pass's body reads.
pub uniforms: Vec<Uniform>,
/// TRACES: FR-DEV-8
/// Per-instance data, for a pass whose work is a *list* rather than a
/// kernel.
///
/// Reaches the body as `instances: array<vec4<f32>>`, with
/// `instance_count` in scope as a `u32`. Empty for every pass that is a
/// convolution, which is every pass that existed before spot removal.
///
/// # Why not the uniform block
///
/// Because the uniform block is fixed by the pass's *structure*, and this
/// is not: sixty-four repairs and one repair are the same shader with a
/// different buffer behind it. Packing the list into uniforms would need a
/// fixed maximum, paid for on every frame whether the photograph carries
/// one spot or none, and it would need the composer to emit `vec4` fields —
/// a WGSL uniform array has a stride of 16 whatever it holds.
///
/// The property that matters more: with the list in storage the generated
/// source does not mention how many there are, so placing the tenth spot
/// uploads 512 bytes and reuses the compiled pipeline, exactly as moving a
/// slider does for the fused pass.
pub storage: Vec<[f32; 4]>,
}
/// TRACES: FR-DEV-3 | FR-DEV-8
@@ -396,6 +419,9 @@ pub struct ComposedDetailPass {
pub source: String,
/// Uniform values in the order the generated struct declares them.
pub uniforms: Vec<f32>,
/// TRACES: FR-DEV-8
/// The instance list, if this pass declared one. See [`DetailPass::storage`].
pub storage: Vec<[f32; 4]>,
/// See [`DetailPass::radius`].
pub radius: u32,
/// Whether this pass writes the display/export texture rather than another
@@ -509,6 +535,7 @@ pub fn compose_detail(
radius: 0,
wgsl: String::new(),
uniforms: Vec::new(),
storage: Vec::new(),
},
0,
scale,
@@ -651,6 +678,10 @@ struct Params {{
@group(0) @binding(0) var source: texture_2d<f32>;
@group(0) @binding(1) var<uniform> u: Params;
@group(0) @binding(2) var output: texture_storage_2d<{store_format}, write>;
// A pass whose work is a list rather than a kernel reads it here; every other
// pass leaves this bound to a single empty element and never looks at it. See
// `DetailPass::storage` for why the list is not in the uniform block.
@group(0) @binding(3) var<storage, read> instances: array<vec4<f32>>;
// A neighbour, clamped to the edge of the image.
//
@@ -681,6 +712,10 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
// What this render is, relative to the export it has to match.
let render_dims = u.detail_base.xy;
let render_scale = u.detail_base.z;
// How many entries `instances` actually holds, read from the buffer itself
// rather than from a uniform so the two cannot disagree. A pass that
// declared no list is bound to a one-element placeholder and never asks.
let instance_count = arrayLength(&instances);
var c = tap(coord, vec2<i32>(0));
// One scalar per pixel that survives the hand-off from one pass to the
@@ -708,6 +743,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
label,
source,
uniforms: uniform_values,
storage: pass.storage.clone(),
radius: pass.radius,
writes_output,
structure_hash,
+2
View File
@@ -141,6 +141,8 @@ impl DetailStage for BoxBlur {
.map(|(axis, _)| DetailPass {
label: if axis == 0 { "horizontal" } else { "vertical" },
radius: r,
// A convolution, not a list: nothing to bind at binding 3.
storage: Vec::new(),
uniforms: vec![
Uniform {
name: "radius",
@@ -358,6 +358,8 @@ impl DetailStage for CaptureSharpen {
.map(|label| DetailPass {
label,
radius: extent,
// A convolution, not a list: nothing to bind at binding 3.
storage: Vec::new(),
uniforms: vec![
Uniform {
// −100…100 as a gain around zero. A hundred percent is
@@ -410,6 +412,8 @@ fn nothing_to_sharpen() -> DetailPass {
label: "unresolved",
// Reads only the pixel it writes, so a tile needs no halo at all.
radius: 0,
// A convolution, not a list: nothing to bind at binding 3.
storage: Vec::new(),
uniforms: Vec::new(),
wgsl: "// The chosen radius is finer than one pixel of this render, so the detail
// it would act on is not in this texture — it was lost to the downscale
@@ -475,12 +475,16 @@ impl<B: Band> DetailStage for LocalContrast<B> {
DetailPass {
label: "base",
radius,
// A convolution, not a list: nothing to bind at binding 3.
storage: Vec::new(),
uniforms: shape,
wgsl: BASE_X.to_string(),
},
DetailPass {
label: "combine",
radius,
// A convolution, not a list: nothing to bind at binding 3.
storage: Vec::new(),
uniforms: combine,
wgsl: combine_body(B::RECIPE.midtone_taper),
},
@@ -436,6 +436,8 @@ impl DetailStage for NoiseReduction {
passes.push(DetailPass {
label: "luminance",
radius: luma,
// A convolution, not a list: nothing to bind at binding 3.
storage: Vec::new(),
uniforms: vec![
Uniform {
name: "radius",
@@ -474,6 +476,8 @@ impl DetailStage for NoiseReduction {
"chroma-vertical"
},
radius: chroma,
// A convolution, not a list: nothing to bind at binding 3.
storage: Vec::new(),
uniforms: vec![
Uniform {
name: "radius",
+5 -5
View File
@@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
| Metric | Value |
|---|---|
| Source files scanned | 230 |
| TRACES tags found | 638 |
| Source files scanned | 231 |
| TRACES tags found | 643 |
| Requirements defined | 177 |
| Requirements covered | 91 |
| **Coverage** | **51.4%** (91/177) |
@@ -52,7 +52,7 @@ _None._
| FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464) |
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:138`](../core/dr-pipeline/src/sidecar.rs#L138), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364) |
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) |
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`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:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:165`](../core/dr-pipeline/src/graph.rs#L165), [`core/dr-pipeline/src/graph.rs:479`](../core/dr-pipeline/src/graph.rs#L479), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:473`](../core/dr-pipeline/src/operation.rs#L473), [`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:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`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:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:1536`](../core/dr-pipeline/src/sidecar.rs#L1536), [`core/dr-pipeline/src/sidecar.rs:1592`](../core/dr-pipeline/src/sidecar.rs#L1592), [`core/dr-pipeline/src/sidecar.rs:159`](../core/dr-pipeline/src/sidecar.rs#L159), [`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:1111`](../ui/dr-ui/src/develop.rs#L1111), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1584`](../ui/dr-ui/src/develop.rs#L1584), [`ui/dr-ui/src/develop.rs:1599`](../ui/dr-ui/src/develop.rs#L1599), [`ui/dr-ui/src/develop.rs:1621`](../ui/dr-ui/src/develop.rs#L1621), [`ui/dr-ui/src/develop.rs:1753`](../ui/dr-ui/src/develop.rs#L1753), [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/develop.rs:238`](../ui/dr-ui/src/develop.rs#L238), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:3194`](../ui/dr-ui/src/develop.rs#L3194), [`ui/dr-ui/src/develop.rs:3248`](../ui/dr-ui/src/develop.rs#L3248), [`ui/dr-ui/src/develop.rs:3292`](../ui/dr-ui/src/develop.rs#L3292), [`ui/dr-ui/src/develop.rs:3342`](../ui/dr-ui/src/develop.rs#L3342), [`ui/dr-ui/src/develop.rs:506`](../ui/dr-ui/src/develop.rs#L506), [`ui/dr-ui/src/develop.rs:544`](../ui/dr-ui/src/develop.rs#L544), [`ui/dr-ui/src/lib.rs:1275`](../ui/dr-ui/src/lib.rs#L1275), [`ui/dr-ui/src/lib.rs:1934`](../ui/dr-ui/src/lib.rs#L1934), [`ui/dr-ui/src/lib.rs:290`](../ui/dr-ui/src/lib.rs#L290), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`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:218`](../ui/dr-ui/src/segmentation.rs#L218), [`ui/dr-ui/ui/app.slint:1915`](../ui/dr-ui/ui/app.slint#L1915), [`ui/dr-ui/ui/app.slint:913`](../ui/dr-ui/ui/app.slint#L913) |
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`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:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:165`](../core/dr-pipeline/src/graph.rs#L165), [`core/dr-pipeline/src/graph.rs:479`](../core/dr-pipeline/src/graph.rs#L479), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:473`](../core/dr-pipeline/src/operation.rs#L473), [`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:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`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:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:1536`](../core/dr-pipeline/src/sidecar.rs#L1536), [`core/dr-pipeline/src/sidecar.rs:1592`](../core/dr-pipeline/src/sidecar.rs#L1592), [`core/dr-pipeline/src/sidecar.rs:159`](../core/dr-pipeline/src/sidecar.rs#L159), [`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:1111`](../ui/dr-ui/src/develop.rs#L1111), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1584`](../ui/dr-ui/src/develop.rs#L1584), [`ui/dr-ui/src/develop.rs:1599`](../ui/dr-ui/src/develop.rs#L1599), [`ui/dr-ui/src/develop.rs:1621`](../ui/dr-ui/src/develop.rs#L1621), [`ui/dr-ui/src/develop.rs:1753`](../ui/dr-ui/src/develop.rs#L1753), [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/develop.rs:238`](../ui/dr-ui/src/develop.rs#L238), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:3194`](../ui/dr-ui/src/develop.rs#L3194), [`ui/dr-ui/src/develop.rs:3248`](../ui/dr-ui/src/develop.rs#L3248), [`ui/dr-ui/src/develop.rs:3292`](../ui/dr-ui/src/develop.rs#L3292), [`ui/dr-ui/src/develop.rs:3342`](../ui/dr-ui/src/develop.rs#L3342), [`ui/dr-ui/src/develop.rs:506`](../ui/dr-ui/src/develop.rs#L506), [`ui/dr-ui/src/develop.rs:544`](../ui/dr-ui/src/develop.rs#L544), [`ui/dr-ui/src/lib.rs:1275`](../ui/dr-ui/src/lib.rs#L1275), [`ui/dr-ui/src/lib.rs:1934`](../ui/dr-ui/src/lib.rs#L1934), [`ui/dr-ui/src/lib.rs:290`](../ui/dr-ui/src/lib.rs#L290), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`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:218`](../ui/dr-ui/src/segmentation.rs#L218), [`ui/dr-ui/ui/app.slint:1915`](../ui/dr-ui/ui/app.slint#L1915), [`ui/dr-ui/ui/app.slint:913`](../ui/dr-ui/ui/app.slint#L913) |
| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:19`](../core/dr-pipeline/src/graph.rs#L19), [`core/dr-pipeline/src/graph.rs:239`](../core/dr-pipeline/src/graph.rs#L239), [`core/dr-pipeline/src/graph.rs:41`](../core/dr-pipeline/src/graph.rs#L41), [`core/dr-pipeline/src/graph.rs:54`](../core/dr-pipeline/src/graph.rs#L54), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1008`](../ui/dr-ui/src/develop.rs#L1008), [`ui/dr-ui/src/lib.rs:579`](../ui/dr-ui/src/lib.rs#L579) |
| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:54`](../core/dr-pipeline/src/graph.rs#L54), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) |
| FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:239`](../core/dr-pipeline/src/graph.rs#L239), [`core/dr-pipeline/src/graph.rs:41`](../core/dr-pipeline/src/graph.rs#L41), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:3921`](../ui/dr-ui/src/develop.rs#L3921) |
@@ -63,8 +63,8 @@ _None._
| FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2817`](../ui/dr-ui/src/develop.rs#L2817), [`ui/dr-ui/src/develop.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1267`](../ui/dr-ui/src/lib.rs#L1267) |
| 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:2756`](../ui/dr-ui/src/develop.rs#L2756), [`ui/dr-ui/src/develop.rs:2777`](../ui/dr-ui/src/develop.rs#L2777), [`ui/dr-ui/src/lib.rs:1239`](../ui/dr-ui/src/lib.rs#L1239), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:535`](../ui/dr-ui/src/settings_ui.rs#L535), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1305`](../ui/dr-ui/ui/library.slint#L1305), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:912`](../ui/dr-ui/ui/library.slint#L912), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) |
| FR-DEV-8 | [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/graph.rs:109`](../core/dr-pipeline/src/graph.rs#L109), [`core/dr-pipeline/src/graph.rs:187`](../core/dr-pipeline/src/graph.rs#L187), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/sidecar.rs:186`](../core/dr-pipeline/src/sidecar.rs#L186), [`core/dr-pipeline/src/sidecar.rs:330`](../core/dr-pipeline/src/sidecar.rs#L330), [`core/dr-pipeline/src/sidecar.rs:664`](../core/dr-pipeline/src/sidecar.rs#L664), [`core/dr-pipeline/src/sidecar.rs:800`](../core/dr-pipeline/src/sidecar.rs#L800), [`core/dr-pipeline/src/sidecar.rs:854`](../core/dr-pipeline/src/sidecar.rs#L854), [`core/dr-pipeline/src/sidecar.rs:884`](../core/dr-pipeline/src/sidecar.rs#L884), [`core/dr-pipeline/src/spot.rs:107`](../core/dr-pipeline/src/spot.rs#L107), [`core/dr-pipeline/src/spot.rs:143`](../core/dr-pipeline/src/spot.rs#L143), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:341`](../core/dr-pipeline/src/spot.rs#L341), [`core/dr-pipeline/src/spot.rs:426`](../core/dr-pipeline/src/spot.rs#L426), [`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) |
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2090`](../core/dr-gpu/src/adjust.rs#L2090), [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:2252`](../core/dr-gpu/src/adjust.rs#L2252), [`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:199`](../core/dr-gpu/tests/capture_sharpen.rs#L199), [`core/dr-gpu/tests/detail_stage.rs:327`](../core/dr-gpu/tests/detail_stage.rs#L327), [`core/dr-gpu/tests/local_contrast.rs:262`](../core/dr-gpu/tests/local_contrast.rs#L262), [`core/dr-gpu/tests/noise_reduction.rs:377`](../core/dr-gpu/tests/noise_reduction.rs#L377), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/graph.rs:449`](../core/dr-pipeline/src/graph.rs#L449), [`core/dr-pipeline/src/graph.rs:479`](../core/dr-pipeline/src/graph.rs#L479), [`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:647`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L647), [`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:658`](../core/dr-pipeline/src/ops/local_contrast.rs#L658), [`core/dr-pipeline/src/ops/noise_reduction.rs:691`](../core/dr-pipeline/src/ops/noise_reduction.rs#L691), [`ui/dr-ui/src/develop.rs:2161`](../ui/dr-ui/src/develop.rs#L2161), [`ui/dr-ui/src/develop.rs:2983`](../ui/dr-ui/src/develop.rs#L2983), [`ui/dr-ui/src/develop.rs:3466`](../ui/dr-ui/src/develop.rs#L3466), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59), [`ui/dr-ui/src/lib.rs:720`](../ui/dr-ui/src/lib.rs#L720) |
| 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-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/graph.rs:109`](../core/dr-pipeline/src/graph.rs#L109), [`core/dr-pipeline/src/graph.rs:187`](../core/dr-pipeline/src/graph.rs#L187), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/sidecar.rs:186`](../core/dr-pipeline/src/sidecar.rs#L186), [`core/dr-pipeline/src/sidecar.rs:330`](../core/dr-pipeline/src/sidecar.rs#L330), [`core/dr-pipeline/src/sidecar.rs:664`](../core/dr-pipeline/src/sidecar.rs#L664), [`core/dr-pipeline/src/sidecar.rs:800`](../core/dr-pipeline/src/sidecar.rs#L800), [`core/dr-pipeline/src/sidecar.rs:854`](../core/dr-pipeline/src/sidecar.rs#L854), [`core/dr-pipeline/src/sidecar.rs:884`](../core/dr-pipeline/src/sidecar.rs#L884), [`core/dr-pipeline/src/spot.rs:107`](../core/dr-pipeline/src/spot.rs#L107), [`core/dr-pipeline/src/spot.rs:143`](../core/dr-pipeline/src/spot.rs#L143), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:341`](../core/dr-pipeline/src/spot.rs#L341), [`core/dr-pipeline/src/spot.rs:426`](../core/dr-pipeline/src/spot.rs#L426), [`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) |
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2090`](../core/dr-gpu/src/adjust.rs#L2090), [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:2252`](../core/dr-gpu/src/adjust.rs#L2252), [`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:199`](../core/dr-gpu/tests/capture_sharpen.rs#L199), [`core/dr-gpu/tests/detail_stage.rs:327`](../core/dr-gpu/tests/detail_stage.rs#L327), [`core/dr-gpu/tests/local_contrast.rs:262`](../core/dr-gpu/tests/local_contrast.rs#L262), [`core/dr-gpu/tests/noise_reduction.rs:377`](../core/dr-gpu/tests/noise_reduction.rs#L377), [`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:449`](../core/dr-pipeline/src/graph.rs#L449), [`core/dr-pipeline/src/graph.rs:479`](../core/dr-pipeline/src/graph.rs#L479), [`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:651`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L651), [`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:662`](../core/dr-pipeline/src/ops/local_contrast.rs#L662), [`core/dr-pipeline/src/ops/noise_reduction.rs:695`](../core/dr-pipeline/src/ops/noise_reduction.rs#L695), [`ui/dr-ui/src/develop.rs:2161`](../ui/dr-ui/src/develop.rs#L2161), [`ui/dr-ui/src/develop.rs:2983`](../ui/dr-ui/src/develop.rs#L2983), [`ui/dr-ui/src/develop.rs:3466`](../ui/dr-ui/src/develop.rs#L3466), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59), [`ui/dr-ui/src/lib.rs:720`](../ui/dr-ui/src/lib.rs#L720) |
| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1) |
| 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:2206`](../ui/dr-ui/src/develop.rs#L2206), [`ui/dr-ui/src/develop.rs:4568`](../ui/dr-ui/src/develop.rs#L4568), [`ui/dr-ui/src/develop.rs:4600`](../ui/dr-ui/src/develop.rs#L4600), [`ui/dr-ui/src/develop.rs:499`](../ui/dr-ui/src/develop.rs#L499), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1326`](../ui/dr-ui/src/lib.rs#L1326), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`ui/dr-ui/ui/app.slint:276`](../ui/dr-ui/ui/app.slint#L276), [`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) |
| FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |