diff --git a/core/dr-gpu/src/detail.rs b/core/dr-gpu/src/detail.rs index a9d7cab..1c417ff 100644 --- a/core/dr-gpu/src/detail.rs +++ b/core/dr-gpu/src/detail.rs @@ -149,6 +149,8 @@ pub(crate) struct DetailRunner { /// Compiled pipelines by pass structure hash. cache: HashMap, 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), ], }); diff --git a/core/dr-gpu/tests/detail_instances.rs b/core/dr-gpu/tests/detail_instances.rs new file mode 100644 index 0000000..ec4d6a8 --- /dev/null +++ b/core/dr-gpu/tests/detail_instances.rs @@ -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 { + 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 = (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; +struct Params { detail_base: vec4 } +@group(0) @binding(1) var u: Params; +@group(0) @binding(2) var output: texture_storage_2d; +@group(0) @binding(3) var instances: array>; + +@compute @workgroup_size(8, 8, 1) +fn main(@builtin(global_invocation_id) gid: vec3) { + 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(gid.xy), vec4(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 { + // 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" + ); +} diff --git a/core/dr-pipeline/src/detail.rs b/core/dr-pipeline/src/detail.rs index 24c3be2..efb6da3 100644 --- a/core/dr-pipeline/src/detail.rs +++ b/core/dr-pipeline/src/detail.rs @@ -359,6 +359,29 @@ pub struct DetailPass { /// Uniform values this pass's body reads. pub uniforms: Vec, + + /// 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>`, 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, + /// 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; @group(0) @binding(1) var 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 instances: array>; // A neighbour, clamped to the edge of the image. // @@ -681,6 +712,10 @@ fn main(@builtin(global_invocation_id) gid: vec3) {{ // 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(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) {{ label, source, uniforms: uniform_values, + storage: pass.storage.clone(), radius: pass.radius, writes_output, structure_hash, diff --git a/core/dr-pipeline/src/detail/probe.rs b/core/dr-pipeline/src/detail/probe.rs index e9a6039..361fb4b 100644 --- a/core/dr-pipeline/src/detail/probe.rs +++ b/core/dr-pipeline/src/detail/probe.rs @@ -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", diff --git a/core/dr-pipeline/src/ops/capture_sharpen.rs b/core/dr-pipeline/src/ops/capture_sharpen.rs index 4f01908..08b21c6 100644 --- a/core/dr-pipeline/src/ops/capture_sharpen.rs +++ b/core/dr-pipeline/src/ops/capture_sharpen.rs @@ -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 diff --git a/core/dr-pipeline/src/ops/local_contrast.rs b/core/dr-pipeline/src/ops/local_contrast.rs index 63cbd31..fe25ab5 100644 --- a/core/dr-pipeline/src/ops/local_contrast.rs +++ b/core/dr-pipeline/src/ops/local_contrast.rs @@ -475,12 +475,16 @@ impl DetailStage for LocalContrast { 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), }, diff --git a/core/dr-pipeline/src/ops/noise_reduction.rs b/core/dr-pipeline/src/ops/noise_reduction.rs index 3006587..b57a828 100644 --- a/core/dr-pipeline/src/ops/noise_reduction.rs +++ b/core/dr-pipeline/src/ops/noise_reduction.rs @@ -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", diff --git a/docs/traceability.md b/docs/traceability.md index be74c2a..3a1a437 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -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) |