diff --git a/.gitignore b/.gitignore index ba2478f..69321fb 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,10 @@ Cargo.lock.bak # tools/film-profiles/convert.py --fetch. Not source: the converted # profiles in core/dr-film/profiles are. tools/film-profiles/upstream/ + +# flatpak-builder's cache and its output tree. `packaging/flatpak/` holds the +# manifest, which is source; everything a build derives from it is not — and +# `.flatpak-builder/` in particular caches an unpacked copy of the whole +# checkout, so it is larger than the repository it sits in. +/.flatpak-builder/ +/build/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6e1e06..7c554e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -151,6 +151,7 @@ One commit per change. If you fixed two things, that is two commits. | [`docs/architecture.md`](docs/architecture.md) | Anything touching the render path, catalog or sync | | [`docs/code-health.md`](docs/code-health.md) | Deciding what to work on; grades each seam by what it costs | | [`docs/technical-debt.md`](docs/technical-debt.md) | Something looks wrong — check it was not chosen | +| [`docs/distribution.md`](docs/distribution.md) | Packaging a build, or adding a permission to one | | [`docs/requirements.md`](docs/requirements.md) | Reference, not reading | `technical-debt.md` is the one to check before "fixing" anything surprising. diff --git a/README.md b/README.md index 0125092..8656899 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,25 @@ A cross-platform, non-destructive RAW photo editor for Linux and Android. -**Status:** early. v0.1 is a remote library viewer — see -[docs/milestone-v0.1.md](docs/milestone-v0.1.md). +**Status:** 0.9.0, and no longer a spike. A library opens, culls, develops and +exports on both platforms, across eight tagged releases. What is *not* +built is written down rather than merely absent — see +[docs/outstanding.md](docs/outstanding.md) for the requirements that have no +implementation and why, and [docs/technical-debt.md](docs/technical-debt.md) +for the compromises that were chosen. ## Documentation | Document | Contents | |---|---| -| [requirements.md](docs/requirements.md) | What the software must do — 122 numbered requirements | +| [CONTRIBUTING.md](CONTRIBUTING.md) | How to land a first change without reading the rest | +| [requirements.md](docs/requirements.md) | What the software must do — 179 numbered requirements | | [architecture.md](docs/architecture.md) | How it is built — crates, GPU pipeline, data model, sync | -| [milestone-v0.1.md](docs/milestone-v0.1.md) | The first buildable milestone | -| [faces.md](docs/faces.md) | Face detection and identity — the models, the licence problem, and what S14 measures | +| [technical-debt.md](docs/technical-debt.md) | Compromises taken deliberately, each with the condition that retires it | +| [outstanding.md](docs/outstanding.md) | What is not built, and whether that is a decision or a gap | +| [code-health.md](docs/code-health.md) | What a contribution costs, per seam, measured | +| [traceability.md](docs/traceability.md) | Generated: which requirement is claimed by which file | +| [faces.md](docs/faces.md) | Face detection and identity — the models, the licence problem, and what S14 measured | ## Building @@ -28,21 +36,48 @@ Android (containerised toolchain, see [docker/android](docker/android/README.md) ./docker/android/build.sh cargo ndk -t arm64-v8a build --release ``` +Git LFS is required for the model weights, and the toolchain pins itself. +[CONTRIBUTING.md](CONTRIBUTING.md) has the details and the four commands CI +will run against what you send. + ## Current state -Working: workspace, GPU context and compute pass, adaptive Slint shell, Android -cross-compilation of the core crates. +**Working.** A catalog over a local folder, a Nextcloud account, or a folder a +sync client keeps in virtual-files mode — where a placeholder is treated as the +photograph rather than as a one-byte file. A virtualised library grid with a +capture-time timeline, ratings, labels, keywords, collections and a trash that +survives a crash mid-operation. Card ingest. Face detection and identity, with +the index syncing between devices. A develop pipeline of fifteen declared +operations fused into a single compute dispatch, plus the neighbourhood +operations that cannot be — clarity, texture, capture sharpening, noise +reduction, lens correction, spectral film simulation. Crop, straighten, spot +removal, gradient and subject-segmentation masks, named presets, and a +generated panel that no operation in `ui/` is allowed to name. Export to JPEG, +PNG and 8- or 16-bit TIFF with resize and output sharpening. -**Not yet working:** the zero-copy display path. The build currently uploads -frames through the CPU, which is exactly what -[ARCH §6.1](docs/architecture.md) forbids — measured at 96% of frame time at -4K. Replacing it is spike S1, the project's highest priority. +**The zero-copy display path works on desktop.** The compute pass writes a +texture that Slint composites directly, which is what +[ARCH §6.1](docs/architecture.md) requires; the readback it forbids costs 96% +of frame time at 4K, and -``` +```bash cargo run -p dr-gpu --example bench --features readback ``` -reproduces that measurement. +still reproduces that measurement. **The one exception is the Android develop +view**, which reads the frame back through the CPU because zero-copy there +needs wgpu's Vulkan swapchain, and that tears a portrait window on a tablet +whose panel is mounted landscape. It is debt, not a revision of the rule: the +reasoning, the on-device measurements that forced it, and the three separate +things any one of which would remove it are in +[technical-debt.md TD-1](docs/technical-debt.md). + +**Not built.** Plugins, compare and survey culling, focus peaking, burst +grouping, AI denoise, tiled and progressive rendering, and most of the Android +platform integration beyond running. The performance targets in §4.1 are +unverified rather than unmet — the per-commit benchmark suite §8 requires does +not exist, so nothing fails a build on a regression. +[docs/outstanding.md](docs/outstanding.md) is the list, with the reasoning. ## Licence diff --git a/core/dr-export/src/lib.rs b/core/dr-export/src/lib.rs index cc9699d..a406101 100644 --- a/core/dr-export/src/lib.rs +++ b/core/dr-export/src/lib.rs @@ -1,4 +1,4 @@ -//! TRACES: FR-EXP-1 | FR-EXP-2 | FR-EXP-3 | FR-EXP-4 | FR-EXP-6 | FR-EXP-9 +//! TRACES: FR-EXP-1 | FR-EXP-2 | FR-EXP-3 | FR-EXP-4 | FR-EXP-6 | FR-EXP-9 | R3 //! Turning a rendered frame into a file's worth of bytes. //! //! # What this crate is, and is not diff --git a/core/dr-gpu/src/focus.rs b/core/dr-gpu/src/focus.rs new file mode 100644 index 0000000..11818af --- /dev/null +++ b/core/dr-gpu/src/focus.rs @@ -0,0 +1,1007 @@ +//! TRACES: FR-CULL-3 | NFR-P14 +//! Focus peaking — saying what is sharp, so nobody has to zoom in to find out. +//! +//! # What this is for +//! +//! FR-CULL-3 lists three raw-truth overlays and gives the reason for this one +//! plainly: peaking "removes the largest single source of culling latency from +//! the critical path". Checking focus by zooming to 1:1 costs a render, a pan +//! to the subject's eye, and a zoom back, per frame, on a folder of three +//! thousand. D11 keeps 1:1 available for certainty; this is what makes reaching +//! for it the exception. +//! +//! It is *raw*-truth for the same reason the histogram beside it is: what this +//! measures is the frame the develop pipeline rendered from sensor data +//! through the demosaic, not the camera's embedded JPEG. A JPEG has already +//! been sharpened by the body, at a strength and radius nobody outside the +//! manufacturer knows, and peaking on one measures that sharpening at least as +//! much as it measures the lens. +//! +//! # Why a layer, and not a tint in the picture +//! +//! The first shape tried was the obvious one — read the display frame, write +//! the same frame with marked pixels replaced, hand *that* to the compositor. +//! It is wrong, and `app.slint` had already written down why, on the region +//! overlay it composites over the canvas: a diagnostic "must not reach the +//! histogram, an export, or the texture the develop pass hands the +//! compositor". +//! +//! All three would have happened. `HistogramPass` counts whatever texture the +//! session last rendered, so a red mark on every in-focus edge would have +//! arrived in the histogram as a red spike and in the clipping figure as blown +//! highlights. So this pass writes its **own** texture, transparent everywhere +//! except where something is in focus, and the interface lays it over the +//! canvas. The photograph is untouched by construction rather than by care. +//! +//! # Why it stays on the GPU +//! +//! Per-pixel work over the whole frame, every settled frame, is exactly what +//! ARCH §6.1 exists about: the measurement on this project puts a 4K readback +//! at 7.4 ms against a 0.28 ms compute pass, 96% of it transfer. The marks are +//! produced where the pixels already are and handed to the compositor as a +//! texture, and nothing in this file can read a pixel back on the display path +//! — see [`FocusPeakPass::read_overlay`] for the one transfer that exists and +//! who is allowed to call it. +//! +//! # Two textures, alternating +//! +//! For the reason [`crate::AdjustPass`] keeps two: Slint decides whether to +//! repaint by comparing the image property against its previous value, and two +//! images wrapping the same `wgpu::Texture` compare equal. A pass that always +//! wrote one texture would compute a new overlay every frame and never once be +//! asked to show it. +//! +//! # What it costs +//! +//! One dispatch, nine texture loads per pixel, no readback and no +//! reallocation at a steady viewport size. NFR-P14 allows 100 ms after the +//! preview on desktop and 150 ms on Android; this is two orders of magnitude +//! inside that, and the assertion in `overlay_is_ready_well_inside_the_budget` +//! is what keeps the claim honest rather than remembered. + +use wgpu::util::DeviceExt as _; + +use crate::readback::await_mapping; +use crate::{GpuContext, GpuError}; + +/// How much local contrast counts as focus. +/// +/// Three steps rather than a slider, because the number underneath is not one +/// a photographer can reason about and the choice being made is coarse: *this +/// frame is noisy, mark less* or *this subject is low-contrast, mark more*. +/// Every camera that offers peaking offers it this way. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +pub enum PeakSensitivity { + /// The high-ISO setting. Marks only edges nothing but focus explains. + Low, + #[default] + Medium, + /// For a low-contrast subject — fur, fabric, distant foliage — at the + /// price of marking noise as well. + High, +} + +impl PeakSensitivity { + /// The luma difference, 0..1, at which a pixel is called in focus. + /// + /// **Where these three numbers come from.** A hard one-pixel step of + /// height *D* produces a response of `0.375 D` (see the shader), so a + /// threshold *t* marks any sharp edge whose contrast exceeds `t / 0.375`. + /// At `Medium` that is 0.107 in luma — about 27 code values — which is a + /// perfectly ordinary edge and well above what a photograph's own texture + /// produces by accident. + /// + /// **And what bounds them from below is noise, not taste.** Sensor noise + /// surviving into an 8-bit render is a few code values; call it a standard + /// deviation of 0.008. The response subtracts a mean of eight independent + /// neighbours from one sample, so its standard deviation is + /// `0.008 * sqrt(1 + 1/8)` = 0.0085. `Medium` sits 4.7 of those out, which + /// a normal tail puts at roughly one pixel in a million; `High` sits 2.4 + /// out, which is about one pixel in a hundred — visible as a dusting on a + /// noisy frame, which is the trade the setting is named for. `Low` is 9.4 + /// out and will not mark noise at all. + /// + /// Noise reduction, if the edit has any, has already run by the time this + /// pass sees the frame, so those figures are the pessimistic end. + pub fn threshold(self) -> f32 { + match self { + PeakSensitivity::Low => 0.080, + PeakSensitivity::Medium => 0.040, + PeakSensitivity::High => 0.020, + } + } +} + +/// What colour the marks are drawn in. +/// +/// A choice rather than a constant, and the reason is NFR-A11Y-3's: status +/// must not rest on hue alone. An overlay's information *is* positional — it +/// says where, not what — so the requirement is not violated by there being a +/// colour; it would be violated by there being only one, because a red mark on +/// a red jersey conveys nothing, and a photographer with a red-green +/// deficiency looking at foliage is in the same position permanently. +/// +/// Four fully saturated choices, no mixtures. A desaturated mark has to +/// compete with the photograph for the same colours, which is the one thing a +/// mark must not do. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +pub enum PeakColour { + /// The convention, and what most cameras do. + #[default] + Red, + /// For a red or warm subject, and the most visible of the four on a dark + /// frame. + Yellow, + /// For a warm photograph as a whole, and the choice that survives a + /// red-green deficiency. + Cyan, + /// For a cool or green photograph — the hue photographs contain least. + Magenta, +} + +impl PeakColour { + /// The mark, as the encoded triple the overlay is written in. + pub fn rgb(self) -> [f32; 3] { + match self { + PeakColour::Red => [1.0, 0.0, 0.0], + PeakColour::Yellow => [1.0, 1.0, 0.0], + PeakColour::Cyan => [0.0, 1.0, 1.0], + PeakColour::Magenta => [1.0, 0.0, 1.0], + } + } +} + +/// TRACES: FR-CULL-3 +/// Everything the photographer chose about the overlay. +/// +/// One value rather than two arguments, because the two travel together +/// everywhere — into the session, into the sidecar-free view state, back out +/// to the panel — and a pair that is always passed together is a type. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct FocusPeaking { + pub sensitivity: PeakSensitivity, + pub colour: PeakColour, +} + +/// The dispatch's view of the frame. Padded to std140's 16 bytes before the +/// marker, exactly as the shader's `Params` declares it. +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct Params { + width: u32, + height: u32, + threshold: f32, + pad_0: u32, + marker: [f32; 4], +} + +/// One overlay texture. +struct Layer { + texture: wgpu::Texture, + view: wgpu::TextureView, + width: u32, + height: u32, +} + +/// TRACES: FR-CULL-3 | NFR-P14 +/// Produces the focus-peaking overlay for a rendered frame. +pub struct FocusPeakPass { + ctx: GpuContext, + pipeline: wgpu::ComputePipeline, + bind_group_layout: wgpu::BindGroupLayout, + params: wgpu::Buffer, + /// Alternating overlay textures — see the module documentation for why + /// there are two rather than one. + layers: [Option; 2], + current: usize, + /// Textures allocated since this pass was created. Exists to be asserted + /// on: an overlay reallocated per frame instead of per resize costs a + /// great deal of bandwidth and looks identical in the picture, which is + /// the shape of regression only a counter can see. + allocations: usize, + dispatches: usize, +} + +impl FocusPeakPass { + /// The overlay's format. + /// + /// The same `Rgba8Unorm` [`crate::AdjustPass`] writes, and for the same + /// non-negotiable reason: it is one of the two formats Slint's texture + /// import accepts. The alpha channel is what carries the overlay, so the + /// eight bits it has are seven more than this needs. + pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; + + pub fn new(ctx: &GpuContext) -> Result { + // A validation error here is a bug in the shader beside this file + // rather than anything a user did, so it is caught in an error scope + // and returned — wgpu's default handler panics. + let scope = ctx.device.push_error_scope(wgpu::ErrorFilter::Validation); + + let module = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("focus-peak"), + source: wgpu::ShaderSource::Wgsl(include_str!("shaders/focus_peak.wgsl").into()), + }); + + let bind_group_layout = + ctx.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("focus-peak-bgl"), + entries: &[ + // The rendered frame, read with `textureLoad` — the + // same texture the compositor is showing, so what is + // measured is what is on screen. + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::StorageTexture { + access: wgpu::StorageTextureAccess::WriteOnly, + format: Self::FORMAT, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + ], + }); + + let layout = ctx + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("focus-peak-layout"), + bind_group_layouts: &[Some(&bind_group_layout)], + immediate_size: 0, + }); + + let pipeline = ctx + .device + .create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: Some("focus-peak-pipeline"), + layout: Some(&layout), + module: &module, + entry_point: Some("main"), + compilation_options: Default::default(), + cache: None, + }); + + if let Some(err) = pollster::block_on(scope.pop()) { + return Err(GpuError::ShaderCompilation(err.to_string())); + } + + let params = ctx + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("focus-peak-params"), + contents: bytemuck::bytes_of(&Params { + width: 0, + height: 0, + threshold: PeakSensitivity::default().threshold(), + pad_0: 0, + marker: [0.0; 4], + }), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + Ok(Self { + ctx: ctx.clone(), + pipeline, + bind_group_layout, + params, + layers: [None, None], + current: 0, + allocations: 0, + dispatches: 0, + }) + } + + /// TRACES: FR-CULL-3 | NFR-P14 + /// Mark the in-focus regions of `frame`, returning the overlay to lay + /// over it. + /// + /// `frame` must carry `TEXTURE_BINDING`, which [`crate::AdjustPass`]'s + /// output does because the compositor samples it. + /// + /// **Give this a settled frame, not a draft one.** The measure is the + /// energy in the top octave of what it is handed, so it is a statement + /// about a particular sampling grid: at half resolution — which is what a + /// draft frame is rendered at — a defocused edge spanning four pixels + /// spans two, which is the signature of a sharp one. Peaking a draft frame + /// would mark the out-of-focus background of every photograph, briefly, + /// during every drag. The interface runs this where it runs the histogram, + /// on the settled frame, and for the same class of reason. + pub fn render( + &mut self, + frame: &wgpu::Texture, + settings: FocusPeaking, + ) -> Result<&wgpu::Texture, GpuError> { + // No zero-size guard: wgpu will not create a texture with a zero + // extent, so a frame that exists has at least one pixel in it. + let (width, height) = (frame.width(), frame.height()); + self.ensure_layer(width, height); + + let rgb = settings.colour.rgb(); + self.ctx.queue.write_buffer( + &self.params, + 0, + bytemuck::bytes_of(&Params { + width, + height, + threshold: settings.sensitivity.threshold(), + pad_0: 0, + marker: [rgb[0], rgb[1], rgb[2], 1.0], + }), + ); + + let layer = self.layers[self.current] + .as_ref() + .expect("ensure_layer just built it"); + let source = frame.create_view(&Default::default()); + let bind_group = self + .ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("focus-peak-bg"), + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&source), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: self.params.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&layer.view), + }, + ], + }); + + let mut enc = self + .ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("focus-peak-encoder"), + }); + { + let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("focus-peak-pass"), + timestamp_writes: None, + }); + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &bind_group, &[]); + pass.dispatch_workgroups(width.div_ceil(8), height.div_ceil(8), 1); + } + // Submitted on its own queue entry after the render that produced + // `frame`. Submission order is the whole of the synchronisation, as it + // is between the fused pass and the detail chain: one queue, and this + // reads what that wrote. + self.ctx.queue.submit(Some(enc.finish())); + self.dispatches += 1; + + Ok(&self.layers[self.current] + .as_ref() + .expect("just written") + .texture) + } + + /// The overlay the last [`Self::render`] wrote, if there has been one. + pub fn overlay(&self) -> Option<&wgpu::Texture> { + self.layers[self.current].as_ref().map(|l| &l.texture) + } + + /// Forget the overlay, so nothing stale is composited. + /// + /// Called when the photograph changes and when peaking is switched off. + /// The alternative — leaving the last overlay resident and merely not + /// drawing it — is one interface bug away from laying one photograph's + /// focus marks over another's, which is the single worst thing an + /// instrument like this can do. + pub fn clear(&mut self) { + self.layers = [None, None]; + self.current = 0; + } + + /// Overlay textures allocated since this pass was created. + /// + /// For tests. A steady viewport must not move this number, and the only + /// evidence of that is a counter — a per-frame reallocation renders + /// identically to a cached one. + pub fn allocations(&self) -> usize { + self.allocations + } + + /// Dispatches encoded since this pass was created. + /// + /// The other half of [`Self::allocations`]: together they say that eight + /// frames cost eight dispatches and two textures, which is the shape a + /// steady viewport is supposed to have. + pub fn dispatches(&self) -> usize { + self.dispatches + } + + /// Make sure the current slot holds a texture of this size. + /// + /// Rotates first, so consecutive frames land in different textures — see + /// the module documentation. A size change drops both, because neither + /// fits any more and a stale one of the wrong size would be composited + /// stretched over the new frame. + fn ensure_layer(&mut self, width: u32, height: u32) { + self.current ^= 1; + let fits = self.layers[self.current] + .as_ref() + .is_some_and(|l| l.width == width && l.height == height); + if fits { + return; + } + let texture = self.ctx.device.create_texture(&wgpu::TextureDescriptor { + label: Some("focus-peak-overlay"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: Self::FORMAT, + // STORAGE_BINDING to be written by the dispatch and + // TEXTURE_BINDING to be sampled by the compositor. + // RENDER_ATTACHMENT is not used by anything here and is required + // anyway: Slint rejects an imported texture without it. COPY_SRC + // is for `read_overlay` and its two callers. + usage: wgpu::TextureUsages::STORAGE_BINDING + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + let view = texture.create_view(&Default::default()); + self.layers[self.current] = Some(Layer { + texture, + view, + width, + height, + }); + self.allocations += 1; + } + + /// TRACES: AC-8 + /// Copy the overlay to the CPU, as RGBA8 rows with no padding. + /// + /// **Two callers, and neither is the desktop display path.** The tests + /// below are one: an overlay is a claim about which pixels are sharp, and + /// there is no way to check that claim without looking at the pixels. The + /// other is the Android develop view, which reads the *frame* back for the + /// reasons `technical-debt.md` TD-1 records — wgpu's Android swapchain + /// tears a portrait window, so Slint is not drawing with wgpu there and no + /// texture can be handed over. An overlay that stayed on the device on a + /// platform where the picture underneath it does not would simply never be + /// seen. + /// + /// On desktop nothing calls this, and ARCH §6.1 holds on the path that + /// matters: the overlay reaches the compositor as a texture. + pub fn read_overlay(&self) -> Result<(Vec, u32, u32), GpuError> { + let Some(layer) = self.layers[self.current].as_ref() else { + return Err(GpuError::Readback("no overlay has been rendered".into())); + }; + let (w, h) = (layer.width, layer.height); + + let unpadded = w * 4; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let padded = unpadded.div_ceil(align) * align; + + let buf = self.ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("focus-peak-readback"), + size: (padded * h) as u64, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + let mut enc = self.ctx.device.create_command_encoder(&Default::default()); + enc.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture: &layer.texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyBufferInfo { + buffer: &buf, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(padded), + rows_per_image: Some(h), + }, + }, + wgpu::Extent3d { + width: w, + height: h, + depth_or_array_layers: 1, + }, + ); + self.ctx.queue.submit(Some(enc.finish())); + + let slice = buf.slice(..); + let (tx, rx) = std::sync::mpsc::channel(); + slice.map_async(wgpu::MapMode::Read, move |r| { + let _ = tx.send(r); + }); + await_mapping(&self.ctx, &rx)?; + + let data = slice.get_mapped_range(); + let mut out = Vec::with_capacity((unpadded * h) as usize); + for row in 0..h { + let start = (row * padded) as usize; + out.extend_from_slice(&data[start..start + unpadded as usize]); + } + drop(data); + buf.unmap(); + Ok((out, w, h)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ctx() -> Option { + match pollster::block_on(GpuContext::new_headless()) { + Ok(c) => Some(c), + Err(e) => { + eprintln!("skipping: no GPU adapter ({e})"); + None + } + } + } + + /// Upload a greyscale frame the pass can read, the way `AdjustPass` hands + /// its output over. + fn frame(ctx: &GpuContext, grey: &[f32], width: u32, height: u32) -> wgpu::Texture { + let rgba: Vec = grey + .iter() + .flat_map(|v| { + let c = (v.clamp(0.0, 1.0) * 255.0).round() as u8; + [c, c, c, 255] + }) + .collect(); + let tex = ctx.device.create_texture(&wgpu::TextureDescriptor { + label: Some("focus-peak-test-frame"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + ctx.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &tex, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &rgba, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(width * 4), + rows_per_image: Some(height), + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + ctx.queue.submit(std::iter::empty()); + tex + } + + /// A vertical step from `dark` to `bright`, blurred with a Gaussian of + /// this `sigma` in pixels. `sigma == 0.0` is the hard edge. + /// + /// The profile is evaluated analytically rather than by convolving a + /// sampled image, so "the same edge, out of focus" is exactly that and not + /// a second thing the test would also have to trust. + fn step(width: u32, height: u32, dark: f32, bright: f32, sigma: f32) -> Vec { + let edge = width as f32 / 2.0 - 0.5; + (0..height) + .flat_map(|_| { + (0..width).map(move |x| { + let d = x as f32 - edge; + let t = if sigma <= 0.0 { + if d < 0.0 { + 0.0 + } else { + 1.0 + } + } else { + // The error function, which is what a Gaussian blur of + // a step is, via a tanh approximation good to ~1e-4 — + // ample, since the assertions below are about orders + // of magnitude rather than about the fourth digit. + 0.5 * (1.0 + (1.202_7 * (d / sigma)).tanh()) + }; + dark + t * (bright - dark) + }) + }) + .collect() + } + + /// How many pixels of an overlay carry a mark, and what colour they are. + fn marks(pixels: &[u8]) -> (usize, Vec<[u8; 3]>) { + let mut count = 0; + let mut colours: Vec<[u8; 3]> = Vec::new(); + for px in pixels.chunks_exact(4) { + if px[3] != 0 { + count += 1; + let c = [px[0], px[1], px[2]]; + if !colours.contains(&c) { + colours.push(c); + } + } + } + (count, colours) + } + + fn peak( + ctx: &GpuContext, + pass: &mut FocusPeakPass, + grey: &[f32], + w: u32, + h: u32, + settings: FocusPeaking, + ) -> Vec { + let tex = frame(ctx, grey, w, h); + pass.render(&tex, settings).expect("peak"); + pass.read_overlay().expect("readback").0 + } + + #[test] + fn the_three_sensitivities_are_ordered_and_none_of_them_is_zero() { + // Arithmetic, so no device. A threshold of zero marks every pixel of + // every photograph — including a flat sky, where the response is + // whatever the last bit of the encoder rounded to — and an overlay + // that covers the frame says nothing at all. + let (low, med, high) = ( + PeakSensitivity::Low.threshold(), + PeakSensitivity::Medium.threshold(), + PeakSensitivity::High.threshold(), + ); + assert!(high > 0.0, "a zero threshold marks everything"); + assert!( + low > med && med > high, + "more sensitive must mean a lower bar: {low} {med} {high}" + ); + assert_eq!(PeakSensitivity::default(), PeakSensitivity::Medium); + } + + #[test] + fn every_colour_is_fully_saturated_and_distinct() { + // The overlay competes with the photograph for attention, and a + // desaturated mark loses. Each choice must also be a different colour + // from the others — two entries that render the same would be a menu + // that lies. + let mut seen: Vec<[f32; 3]> = Vec::new(); + for c in [ + PeakColour::Red, + PeakColour::Yellow, + PeakColour::Cyan, + PeakColour::Magenta, + ] { + let rgb = c.rgb(); + assert!( + rgb.iter().all(|v| *v == 0.0 || *v == 1.0), + "{c:?} is not a saturated primary: {rgb:?}" + ); + assert!(rgb.iter().any(|v| *v > 0.0), "{c:?} is black"); + assert!(!seen.contains(&rgb), "{c:?} duplicates another choice"); + seen.push(rgb); + } + } + + #[test] + fn a_flat_frame_is_marked_nowhere() { + // The floor. An overlay that marks an empty sky is one a photographer + // stops believing within a frame, and there is nothing subtle about + // the failure — it would be the whole picture. + let Some(ctx) = ctx() else { return }; + let mut pass = FocusPeakPass::new(&ctx).expect("pass"); + + let flat = vec![0.45f32; 64 * 64]; + let px = peak(&ctx, &mut pass, &flat, 64, 64, FocusPeaking::default()); + assert_eq!(marks(&px).0, 0, "flat grey produced marks"); + } + + #[test] + fn a_sharp_edge_is_marked_and_the_same_edge_defocused_is_not() { + // **The claim the whole overlay rests on**, and the one a gradient + // detector would fail: these two frames have identical contrast and + // differ only in how many pixels the transition is spread over. If + // both are marked, the overlay is an edge detector wearing focus + // peaking's name and it will light up every out-of-focus background + // ever photographed. + let Some(ctx) = ctx() else { return }; + let mut pass = FocusPeakPass::new(&ctx).expect("pass"); + let (w, h) = (64u32, 16u32); + + let sharp = step(w, h, 0.25, 0.75, 0.0); + let soft = step(w, h, 0.25, 0.75, 2.0); + + let marked_sharp = marks(&peak( + &ctx, + &mut pass, + &sharp, + w, + h, + FocusPeaking::default(), + )) + .0; + let marked_soft = marks(&peak(&ctx, &mut pass, &soft, w, h, FocusPeaking::default())).0; + + // Two columns of the sharp edge respond — the pixel each side of the + // transition — so a full-height edge marks 2 per row. + assert_eq!( + marked_sharp, + (2 * h) as usize, + "a hard edge should mark the column each side of it, on every row" + ); + assert_eq!( + marked_soft, 0, + "the same edge at sigma 2 is out of focus and must not be marked" + ); + } + + #[test] + fn sensitivity_decides_how_faint_an_edge_still_counts() { + // The setting doing what its name says, from both sides. A hard edge + // of 0.08 contrast responds at `0.375 * 0.08` = 0.031 — above `High`'s + // bar of 0.020 and below `Medium`'s of 0.040 — so the same photograph + // must be marked at one setting and clean at the other. Without this, + // three menu entries that all behaved identically would pass every + // other test in this file. + let Some(ctx) = ctx() else { return }; + let mut pass = FocusPeakPass::new(&ctx).expect("pass"); + let (w, h) = (64u32, 8u32); + let faint = step(w, h, 0.46, 0.54, 0.0); + + let at = |pass: &mut FocusPeakPass, sensitivity| { + marks(&peak( + &ctx, + pass, + &faint, + w, + h, + FocusPeaking { + sensitivity, + ..Default::default() + }, + )) + .0 + }; + + assert!( + at(&mut pass, PeakSensitivity::High) > 0, + "a faint but sharp edge is exactly what High is for" + ); + assert_eq!( + at(&mut pass, PeakSensitivity::Medium), + 0, + "Medium should hold its bar above an 0.08 edge" + ); + assert_eq!(at(&mut pass, PeakSensitivity::Low), 0); + } + + #[test] + fn the_mark_is_the_colour_that_was_asked_for_and_the_rest_is_transparent() { + // Both halves matter. The colour, because a menu that quietly draws + // red whatever is chosen is worse than not offering the choice; and + // the transparency, because the overlay is composited over the + // photograph and an alpha of even 1/255 across the frame is a veil + // over every judgement made through it. + let Some(ctx) = ctx() else { return }; + let mut pass = FocusPeakPass::new(&ctx).expect("pass"); + let (w, h) = (32u32, 8u32); + let sharp = step(w, h, 0.2, 0.8, 0.0); + + for colour in [ + PeakColour::Red, + PeakColour::Yellow, + PeakColour::Cyan, + PeakColour::Magenta, + ] { + let px = peak( + &ctx, + &mut pass, + &sharp, + w, + h, + FocusPeaking { + colour, + ..Default::default() + }, + ); + let (count, colours) = marks(&px); + assert!(count > 0, "{colour:?} marked nothing"); + let expected: [u8; 3] = colour.rgb().map(|v| (v * 255.0).round() as u8); + assert_eq!(colours, vec![expected], "{colour:?} drew the wrong ink"); + + // Everything unmarked is fully transparent *and* black, which is + // what makes the layer correct whether the compositor treats it as + // premultiplied or not. + for texel in px.chunks_exact(4) { + if texel[3] == 0 { + assert!( + texel[..3].iter().all(|c| *c == 0), + "an unmarked texel carried colour under a zero alpha" + ); + } else { + assert_eq!(texel[3], 255, "a mark was drawn part-way transparent"); + } + } + } + } + + #[test] + fn the_edge_of_the_frame_is_not_marked_by_its_own_edge() { + // The clamp in `neighbour`. Wrapping instead would fold the right-hand + // column of the picture into the left-hand one's neighbourhood, and a + // photograph whose two sides differ — which is most of them — would be + // marked down both borders regardless of focus. + let Some(ctx) = ctx() else { return }; + let mut pass = FocusPeakPass::new(&ctx).expect("pass"); + let (w, h) = (33u32, 17u32); + + // Dark on the left, bright on the right, with the transition a long + // way from either border. + let split = step(w, h, 0.1, 0.9, 0.0); + let px = peak(&ctx, &mut pass, &split, w, h, FocusPeaking::default()); + + for y in 0..h { + for x in [0u32, w - 1] { + let a = px[(((y * w + x) * 4) + 3) as usize]; + assert_eq!(a, 0, "the frame border at ({x}, {y}) was marked"); + } + } + // A size that is not a multiple of the 8x8 workgroup, so the edge + // groups run off the image: every texel must still have been written. + assert_eq!(px.len(), (w * h * 4) as usize); + } + + #[test] + fn consecutive_overlays_are_different_textures() { + // Slint compares the image property by value to decide whether to + // repaint, so two frames wrapping one texture compare equal and the + // second is never drawn. `AdjustPass` keeps two targets for this + // reason; the overlay beside it has to do the same or it freezes on + // whatever it first showed. + let Some(ctx) = ctx() else { return }; + let mut pass = FocusPeakPass::new(&ctx).expect("pass"); + let grey = vec![0.5f32; 16 * 16]; + + let tex = frame(&ctx, &grey, 16, 16); + let first = + std::ptr::from_ref(pass.render(&tex, FocusPeaking::default()).expect("first")) as usize; + let second = std::ptr::from_ref(pass.render(&tex, FocusPeaking::default()).expect("second")) + as usize; + assert_ne!( + first, second, + "two consecutive overlays landed in the same texture" + ); + } + + #[test] + fn a_steady_viewport_allocates_nothing_after_the_first_two_frames() { + // Reallocating a viewport-sized texture per frame costs a great deal + // of bandwidth and renders identically, which is why this is a counter + // and not an assertion about the picture. Two allocations, not one: + // the pair alternates, so the second frame builds the other slot. + let Some(ctx) = ctx() else { return }; + let mut pass = FocusPeakPass::new(&ctx).expect("pass"); + let grey = vec![0.5f32; 64 * 64]; + let tex = frame(&ctx, &grey, 64, 64); + + for _ in 0..8 { + pass.render(&tex, FocusPeaking::default()).expect("render"); + } + assert_eq!(pass.dispatches(), 8); + assert_eq!( + pass.allocations(), + 2, + "the overlay was reallocated per frame" + ); + + // A resize is the one thing that must reallocate: a stale overlay of + // the wrong size would be composited stretched over the new frame. + let small = vec![0.5f32; 32 * 32]; + let smaller = frame(&ctx, &small, 32, 32); + pass.render(&smaller, FocusPeaking::default()) + .expect("render"); + assert_eq!(pass.allocations(), 3); + } + + #[test] + fn a_cleared_pass_has_no_overlay_to_composite() { + // Switching peaking off, or opening a different photograph, must leave + // nothing behind. One interface bug away from laying one frame's focus + // marks over another's, which is the worst thing an instrument can do: + // be confidently about the wrong picture. + let Some(ctx) = ctx() else { return }; + let mut pass = FocusPeakPass::new(&ctx).expect("pass"); + let grey = vec![0.5f32; 16 * 16]; + let tex = frame(&ctx, &grey, 16, 16); + pass.render(&tex, FocusPeaking::default()).expect("render"); + assert!(pass.overlay().is_some()); + + pass.clear(); + assert!(pass.overlay().is_none()); + assert!( + pass.read_overlay().is_err(), + "a cleared pass must not hand out a stale overlay" + ); + } + + #[test] + fn the_overlay_is_ready_well_inside_its_budget() { + // NFR-P14: ready within 100 ms of the preview on desktop, 150 ms on + // Android. The bound asserted here is 50 ms at 4K, which is half the + // desktop budget and a third of Android's, and is still around two + // orders of magnitude above what the dispatch actually costs — chosen + // so that an unrelated machine under load does not fail the suite, + // while a change that made this a multi-pass or readback-bound + // operation could not possibly stay under it. + // + // The readback is deliberately outside the timed region. It is a + // property of this test rather than of the overlay, and at 4K it is + // the 7 ms transfer ARCH §6.1 exists to keep off the frame path. + let Some(ctx) = ctx() else { return }; + let mut pass = FocusPeakPass::new(&ctx).expect("pass"); + let (w, h) = (3840u32, 2160u32); + let tex = frame(&ctx, &step(w, h, 0.2, 0.8, 0.0), w, h); + + // One frame to allocate the layer and warm the pipeline; a resize is + // not what the budget is about. + pass.render(&tex, FocusPeaking::default()).expect("warm"); + ctx.device + .poll(wgpu::PollType::wait_indefinitely()) + .expect("idle"); + + let start = std::time::Instant::now(); + pass.render(&tex, FocusPeaking::default()).expect("render"); + ctx.device + .poll(wgpu::PollType::wait_indefinitely()) + .expect("idle"); + let elapsed = start.elapsed(); + + assert!( + elapsed.as_millis() < 50, + "the overlay took {elapsed:?} at {w}x{h}; NFR-P14 allows 100 ms" + ); + } +} diff --git a/core/dr-gpu/src/lib.rs b/core/dr-gpu/src/lib.rs index 705ceb1..fa46ff9 100644 --- a/core/dr-gpu/src/lib.rs +++ b/core/dr-gpu/src/lib.rs @@ -1,3 +1,4 @@ +//! TRACES: NFR-PORT-2 //! GPU device and compute for DarkRoom. //! //! In v0.1 this exists to prove one thing: a compute shader can write a @@ -21,6 +22,7 @@ mod adjust; mod demosaic; mod detail; mod error; +mod focus; mod histogram; mod mask; mod readback; @@ -33,6 +35,7 @@ pub use adjust::AdjustPass; pub use demosaic::{DemosaicedImage, Demosaicer}; pub use detail::INTERMEDIATE_FORMAT as DETAIL_INTERMEDIATE_FORMAT; pub use error::GpuError; +pub use focus::{FocusPeakPass, FocusPeaking, PeakColour, PeakSensitivity}; // Renamed on the way out: `BINS` says enough inside `histogram`, and nothing // at all at a crate root shared with demosaic and segmentation. pub use histogram::{Histogram, HistogramPass, BINS as HISTOGRAM_BINS}; diff --git a/core/dr-gpu/src/shaders/focus_peak.wgsl b/core/dr-gpu/src/shaders/focus_peak.wgsl new file mode 100644 index 0000000..098cba2 --- /dev/null +++ b/core/dr-gpu/src/shaders/focus_peak.wgsl @@ -0,0 +1,141 @@ +// TRACES: FR-CULL-3 | NFR-P14 +// Marking what is sharp, in a layer laid over the frame rather than into it. +// +// # Why the top octave, and not a gradient +// +// The obvious detector is a gradient magnitude — Sobel, or a central +// difference — and it is the wrong one, for a reason that decides whether the +// overlay is useful at all. A gradient answers "is there an edge here", and a +// defocused edge is still an edge: blur a 100-code step with a two-pixel +// Gaussian and the peak gradient is still around 20 codes per pixel, larger +// than a genuinely sharp edge across a low-contrast texture. Peaking built on +// gradients lights up the out-of-focus background of every portrait ever +// taken, which is the frame it exists to reject. +// +// What separates sharp from soft is *scale*, not amplitude. Defocus is a +// low-pass: it removes the top octave and leaves everything below it intact. +// So the detector is a high-pass — this pixel against the mean of its eight +// neighbours, a discrete Laplacian — which by construction responds only to +// the frequencies defocus destroys. +// +// The arithmetic, on a one-dimensional step of height D: +// +// | profile | abs(centre - mean of 8) | +// |--------------------------|-------------------------| +// | hard step, 1 px | 0.375 D | +// | Gaussian blur, sigma 1 | ~0.10 D | +// | Gaussian blur, sigma 2 | ~0.03 D | +// | linear ramp, any slope | 0 | +// +// The ramp row is the property being bought: the smooth luminance falloff +// across an out-of-focus highlight scores zero however bright it is. +// +// # Why luma, and why the histogram's luma +// +// One channel rather than three, because a colour edge carrying no luminance +// difference is both rare and, at the acuity an overlay is read at, invisible. +// The weights are `histogram.wgsl`'s 54/183/19 over 256 — the same Rec.709 +// weighting on the same encoded values — so the two instruments in this +// application agree about what "luma" means. Two definitions of brightness in +// one panel is the kind of disagreement nobody finds until it has already +// misled someone. +// +// # Why the frame is read where it is encoded, and not in linear light +// +// This runs on the output of the display transform, on encoded values, and +// that is deliberate: a fixed difference in sRGB code values is roughly +// equally visible wherever it sits in the range, which is what a transfer +// curve is for. Measured in linear light the same detector would need a +// threshold that varied with exposure, and a shadow texture the photographer +// can plainly see would score a hundredth of the identical texture in the +// highlights. The encoding has already done the normalisation, so the +// threshold is one number. +// +// # Why this writes a layer and not the picture +// +// The frame the compositor is handed is also what the histogram counts and +// what an export renders (`app.slint`, on the region overlay: a diagnostic +// "must not reach the histogram, an export, or the texture the develop pass +// hands the compositor"). So the marks go in their own texture — transparent +// everywhere except where something is in focus — and the compositor blends +// them. Nothing about the photograph changes, and the peaking overlay cannot +// leak into a measurement or a file. +// +// Alpha is written as exactly 0 or exactly 1, never between. The importing +// compositor's convention for whether colour arrives premultiplied is not +// something this shader can see, and at those two values the two conventions +// agree — which is a cheaper guarantee than being right about which one it is. + +struct Params { + width: u32, + height: u32, + // Luma difference at which a pixel is called in focus. See + // `PeakSensitivity::threshold` for where the three values come from. + threshold: f32, + // std140 rounds the scalar block up to 16 bytes before the vec4; named so + // the Rust struct's padding is visibly the same shape. + pad_0: u32, + // The mark's colour, fully saturated. Its alpha is ignored — see above. + marker: vec4, +} + +@group(0) @binding(0) var frame: texture_2d; +@group(0) @binding(1) var params: Params; +@group(0) @binding(2) var marks: texture_storage_2d; + +/// Rec.709 luma of an encoded triple, weighted exactly as `histogram.wgsl` +/// weights it. 54 + 183 + 19 is 256, so the weights sum to unity. +fn luma(c: vec3) -> f32 { + return dot(c, vec3(54.0, 183.0, 19.0) / 256.0); +} + +/// A neighbour, with the frame edge held rather than wrapped. +/// +/// Clamping duplicates the edge pixel into the missing half of the +/// neighbourhood, which pulls the mean towards the centre and so biases the +/// response *down* on the outermost row and column. That is the right +/// direction to be wrong in: the failure is a missing mark at the frame edge, +/// where nobody is judging focus, rather than a false mark produced by +/// folding the opposite side of the picture into the kernel. +fn neighbour(x: i32, y: i32) -> f32 { + let cx = clamp(x, 0i, i32(params.width) - 1i); + let cy = clamp(y, 0i, i32(params.height) - 1i); + return luma(textureLoad(frame, vec2(cx, cy), 0).rgb); +} + +// 8x8, matching the detail stage's dispatch. Each texel is loaded by nine +// invocations and no workgroup-memory tile is built to avoid that: at viewport +// resolution the reads are perfectly coherent and the texture cache serves +// eight of the nine. The budget is NFR-P14's 100 ms against a dispatch +// measured in tenths of a millisecond, so there is nothing here worth the +// complexity of a tiled load. +@compute @workgroup_size(8, 8, 1) +fn main(@builtin(global_invocation_id) gid: vec3) { + if (gid.x >= params.width || gid.y >= params.height) { + return; + } + let x = i32(gid.x); + let y = i32(gid.y); + + // The eight neighbours, centre excluded. Excluded rather than folded in + // because it makes the response readable: `abs(c - mean8)` is the height + // of this pixel above its surroundings in the same units as the step it + // sits on, so the threshold can be quoted as a luma difference rather than + // as eight-ninths of one. + var sum = 0.0; + for (var dy = -1; dy <= 1; dy = dy + 1) { + for (var dx = -1; dx <= 1; dx = dx + 1) { + if (dx != 0 || dy != 0) { + sum = sum + neighbour(x + dx, y + dy); + } + } + } + let centre = luma(textureLoad(frame, vec2(x, y), 0).rgb); + let response = abs(centre - sum / 8.0); + + if (response >= params.threshold) { + textureStore(marks, vec2(x, y), vec4(params.marker.rgb, 1.0)); + } else { + textureStore(marks, vec2(x, y), vec4(0.0, 0.0, 0.0, 0.0)); + } +} diff --git a/core/dr-pipeline/src/graph.rs b/core/dr-pipeline/src/graph.rs index 2d52762..3a9a2e6 100644 --- a/core/dr-pipeline/src/graph.rs +++ b/core/dr-pipeline/src/graph.rs @@ -1,3 +1,4 @@ +//! TRACES: FR-DEV-1 //! The edit graph — an ordered set of operations (ARCH §3.4). //! //! CPU-side state, deliberately. The GPU device can be lost and rebuilt at any diff --git a/core/dr-pipeline/src/lib.rs b/core/dr-pipeline/src/lib.rs index 6222417..e61f9cf 100644 --- a/core/dr-pipeline/src/lib.rs +++ b/core/dr-pipeline/src/lib.rs @@ -1,3 +1,4 @@ +//! TRACES: R3 //! The develop pipeline — operations, descriptors, and shader composition. //! //! # What this crate is diff --git a/core/dr-pipeline/src/sidecar.rs b/core/dr-pipeline/src/sidecar.rs index 93e2372..f70d91c 100644 --- a/core/dr-pipeline/src/sidecar.rs +++ b/core/dr-pipeline/src/sidecar.rs @@ -1,3 +1,4 @@ +//! TRACES: FR-DEV-1 //! Sidecar serialisation — the edit graph as durable, mergeable data. //! //! # Generic, for the same reason the UI is generic diff --git a/core/dr-sync-folder/src/borrow.rs b/core/dr-sync-folder/src/borrow.rs index f48526a..490a938 100644 --- a/core/dr-sync-folder/src/borrow.rs +++ b/core/dr-sync-folder/src/borrow.rs @@ -1,4 +1,4 @@ -// TRACES: FR-NC-6c | FR-NC-6a +// TRACES: FR-NC-6c | FR-NC-6a | FR-NC-6d //! Hydrating a file for as long as it is needed, and no longer. //! //! A pass over a library — thumbnails, face indexing — needs each photograph's diff --git a/core/dr-sync-folder/src/lib.rs b/core/dr-sync-folder/src/lib.rs index 69f0d22..24b84cd 100644 --- a/core/dr-sync-folder/src/lib.rs +++ b/core/dr-sync-folder/src/lib.rs @@ -1,4 +1,4 @@ -// TRACES: FR-NC-13 | FR-NC-12 +// TRACES: FR-NC-13 | FR-NC-12 | FR-NC-6d //! A library that is just a directory. //! //! The second [`RemoteBackend`], and the one that exists to prove the first diff --git a/core/dr-sync-folder/src/vfs.rs b/core/dr-sync-folder/src/vfs.rs index 3e05e4d..a49e0d9 100644 --- a/core/dr-sync-folder/src/vfs.rs +++ b/core/dr-sync-folder/src/vfs.rs @@ -1,4 +1,4 @@ -// TRACES: FR-NC-6c +// TRACES: FR-NC-6c | FR-NC-6d //! Virtual-filesystem conventions layered over a directory. //! //! A sync client in virtual-files mode leaves a *placeholder* where a file is diff --git a/core/dr-sync-nextcloud/src/lib.rs b/core/dr-sync-nextcloud/src/lib.rs index bfc30f7..ad93db0 100644 --- a/core/dr-sync-nextcloud/src/lib.rs +++ b/core/dr-sync-nextcloud/src/lib.rs @@ -1,3 +1,4 @@ +//! TRACES: R6 | NFR-SEC-3 //! Nextcloud connector. //! //! One of two [`RemoteBackend`] implementations, registered through diff --git a/core/dr-sync-nextcloud/src/provider.rs b/core/dr-sync-nextcloud/src/provider.rs index c182935..7b1a503 100644 --- a/core/dr-sync-nextcloud/src/provider.rs +++ b/core/dr-sync-nextcloud/src/provider.rs @@ -1,4 +1,4 @@ -// TRACES: FR-NC-12 | FR-NC-1 +// TRACES: FR-NC-12 | FR-NC-1 | NFR-SEC-3 //! Registering Nextcloud as a storage backend. //! //! The account model this connector used to own now lives in diff --git a/core/dr-types/src/settings.rs b/core/dr-types/src/settings.rs index bec5286..b027de3 100644 --- a/core/dr-types/src/settings.rs +++ b/core/dr-types/src/settings.rs @@ -1,4 +1,4 @@ -//! TRACES: FR-NC-6a | FR-EXP-1 | FR-EXP-2 | FR-EXP-3 | FR-EXP-6 | FR-PLAT-LIN-1 +//! TRACES: FR-NC-6a | FR-EXP-1 | FR-EXP-2 | FR-EXP-3 | FR-EXP-6 | FR-PLAT-LIN-1 | NFR-OPS-3 //! Device preferences: how much disk to spend, and what an export defaults to. //! //! # Why these live beside the session and not in the catalog diff --git a/docs/distribution.md b/docs/distribution.md new file mode 100644 index 0000000..90e3075 --- /dev/null +++ b/docs/distribution.md @@ -0,0 +1,251 @@ +# DarkRoom — Distribution + +**Satisfies:** NFR-COMPAT-2 (v1 channels) · FR-PLAT-LIN-3 (sandboxed distribution) +**Companion to:** [requirements.md](requirements.md) §3.8, §4.8 · [storage.md](storage.md) + +NFR-COMPAT-2 asks for the v1 channels to be *stated*, and says why in its own +second sentence: the channel decision and the storage design are coupled. A +channel is not a build target. It is a set of constraints that reach back into +the code — what the application is allowed to see, what it may ask for, and +what it must be able to do without asking. This document records which channels +v1 targets and what each one costs, and it is where to look before adding a +permission to a package rather than after. + +--- + +## 1. The channels + +| Platform | Channel | State | What it constrains | +|---|---|---|---| +| Linux | Arch source package — [`packaging/PKGBUILD`](../packaging/PKGBUILD) | Built, in tree | Nothing. Full filesystem access, system Vulkan, system secret daemon | +| Linux | Flatpak — [`packaging/flatpak/`](../packaging/flatpak/) | Manifest in tree, **library selection does not work** (§4) | Portals only. No `--filesystem=`, no host mount table, no typed paths | +| Linux | AppImage | v1 channel, **recipe not yet written** (§5) | Oldest supported glibc, and no sandbox at all | +| Android | F-Droid | v1 channel, not yet submitted | GPLv3-clean build, reproducible, no proprietary blobs | +| Android | Play Store | **Not v1** (§6) | Would make ARCH §6.9 binding as policy rather than as engineering | + +Three of these five exist as recipes and two do not. That is stated rather than +smoothed over, because the value of writing the channels down is knowing which +constraints are already being met and which are promises. + +### What every channel has to get right + +Independent of packaging format, and each of these has bitten a package +somewhere: + +- **One identifier, four places.** `paris.tourolle.darkroom` is the AppStream + component id, the `.desktop` basename, the Flatpak application id, and the + string `dr_ui::run` sets as the Wayland `app_id` and X11 `WM_CLASS`. A rename + that misses one of them costs the icon in the shell or the association in the + software centre, and neither failure announces itself. +- **The metainfo, not just the desktop entry.** + [`packaging/paris.tourolle.darkroom.metainfo.xml`](../packaging/paris.tourolle.darkroom.metainfo.xml) + is the single description of the application, installed by every channel that + has somewhere to put it. Its `metadata_license` is CC0-1.0 and its + `project_license` is GPL-3.0-or-later; those differ on purpose — see the + comment in the file. +- **Vulkan is a requirement, not a preference.** The develop pipeline is + compute shaders through wgpu, and NFR-R8 — how far a CPU fallback goes — is + still open, so today there is nothing behind it. A package that installs onto + a machine with no working ICD produces an application that starts and cannot + develop. +- **A Secret Service implementation, or an honest degraded mode.** FR-NC-2 is + explicit that the absence of a secrets daemon is a stated degraded mode and + never a silent fall back to plaintext. Packages express this as an optional + dependency (the PKGBUILD) or a talk hole (the Flatpak manifest), never as a + hard dependency — a headless or minimal-WM install is a supported way to run. +- **The face models are Git LFS objects.** A checkout without `git lfs pull` + has ~130-byte pointers where 11 MB models should be. Both the PKGBUILD and + the Flatpak manifest check the file size and refuse, because the alternative + is a package whose face indexing fails inside the graph loader on a user's + machine rather than on the packager's. + +--- + +## 2. Why Flatpak is the channel that matters most + +Not because it is expected to be the most used. Because it is the only one that +tests anything. + +The Arch package and an AppImage both hand the application the same +unrestricted process the developer runs it in, so neither can discover that a +design assumed unrestricted access. Flatpak takes that assumption away, and +FR-PLAT-LIN-3 exists to make the discovery happen deliberately rather than in a +bug report. §4 is what it discovered. + +The same argument runs the other way on Android, where SAF has been the only +option since before the first line was written (ARCH §6.9) and `SourceRef` +exists because of it. Linux got the abstraction — `LocalStorage::grant` is the +one place a `Path` enters — and never got the constraint that would have proved +it worked. + +--- + +## 3. What already works inside the sandbox, unchanged + +Worth listing, because it is the part FR-PLAT-LIN-1 quietly paid for in +advance: + +- **XDG directories.** Flatpak redirects `XDG_CONFIG_HOME`, `XDG_DATA_HOME` and + `XDG_CACHE_HOME` into `~/.var/app/paris.tourolle.darkroom/`. Settings + (`settings_store.rs`), accounts (`dr_sync::account`), the catalog and the + thumbnail store all read those variables, so every one of them lands in the + application's own directory with no code change and no permission. +- **The face models.** `system_face_models_dirs()` reads `$XDG_DATA_DIRS` + rather than hard-coding `/usr/share`, which is exactly why `/app/share` + inside a Flatpak is found by the same lookup that finds the Arch package's + copy. +- **Opening a photograph from a file manager.** The `.desktop` entry declares + the RAW MIME types and `Exec=darkroom-desktop %F`; under Flatpak the file is + exported through the document portal and arrives in `argv` as a path under + `/run/user/$UID/doc/`, which is mounted in every sandbox. `main.rs` takes + paths from `argv` and `collect()` handles a file or a directory. This is + genuine portal-mediated access and it needs nothing new. +- **The Nextcloud sign-in browser.** `open_in_browser` spawns `xdg-open`; the + freedesktop runtime's `xdg-open` forwards to the OpenURI portal, and portal + calls need no `--talk-name` because Flatpak always permits them. FR-NC-1's + "system browser, never an embedded webview" therefore holds inside the + sandbox for the same reason it holds outside it. +- **Credentials.** The keyring crate speaks the Secret Service D-Bus interface, + reached through the session-bus proxy with one talk hole. The app password + stays visible to `secret-tool` and Seahorse, which is what keeps it + individually revocable by the user. + +--- + +## 4. What does not work: choosing a library + +**FR-PLAT-LIN-3 is not satisfied today, and the manifest does not pretend +otherwise.** + +A folder library is chosen by typing an absolute path. `dr-sync-folder`'s +provider declares `SignIn::EndpointOnly` with the placeholder +`/home/you/Pictures`, and `normalise_endpoint` expands `~`, requires the path +to be absolute, and checks it with `std::fs`. Nothing in the tree calls the +FileChooser portal — there is no `ashpd`, no `rfd`, and no toolkit file dialog +anywhere in `ui/`, `platform/` or `core/`. + +Inside a sandbox with no `--filesystem=`, `$HOME` still resolves to the real +home *path* but that directory holds only the application's own +`.var/app/…` tree. So a typed `~/Pictures` fails the `exists()` check and the +launch screen says `No folder at /home/you/Pictures.` — a truthful message +about a situation the user cannot fix from inside the application. + +Import is blocked one step earlier. `dr_plat::volumes()` finds a camera card by +reading `/proc/self/mountinfo` and the `removable` flag under `/sys`. A +sandboxed process is in its own mount namespace, so the table it reads +describes the sandbox; a card mounted at `/run/media/…` on the host is not in +it. `volumes()` correctly returns an empty list, which the interface presents +as "no card found" — right for the code, wrong for the user, who is looking at +a card. + +### The permission that would hide this, and why it is not in the manifest + +`--filesystem=host` makes both work immediately and is the thing FR-PLAT-LIN-3 +names as the alternative to portals. Granting it would mean the sandboxed build +never exercises the sandbox, which removes the entire reason for shipping one +(§2). `--filesystem=xdg-pictures` is narrower and would be tempting, but it is +still a static grant that lets a typed path resolve — it makes the same design +work by not testing it, only in a smaller directory. + +So the manifest grants no filesystem access at all. The consequence is stated +plainly: **a Flatpak built from this manifest can open photographs handed to it +and cannot yet be pointed at a library.** + +### What closes it + +Two changes, in this order: + +1. **A portal file chooser behind a platform seam.** `ashpd`'s + `OpenFileRequest` with `directory(true)` returns a URI the document portal + has exported, which the sandbox can read and which stays valid across + restarts. It resolves to a real path under `/run/user/$UID/doc/`, so + `normalise_endpoint` accepts it as it stands — `canonicalize()` on a fuse + path returns the path itself. The seam matters more than the crate: this + belongs beside `LocalStorage::grant` in `dr-plat`, which is already the one + place a `Path` enters the application, and must not become a second way for + `ui/` to learn about paths. +2. **Removable volumes through the same door.** There is no portal for "list + the mounted cards". The honest answer is that under a sandbox + `imports_supported()` should report the same `false` it reports on Android, + for the same reason it gives there — the operation cannot be performed + however hard the user tries — and the import flow should offer the folder + chooser instead of a volume list. + +**Done when:** a Flatpak built from +[`packaging/flatpak/paris.tourolle.darkroom.yml`](../packaging/flatpak/paris.tourolle.darkroom.yml), +with its `finish-args` unchanged and no `flatpak override` applied, can select a +library root, scan it, and write a sidecar back into it. + +### Running a Flatpak build before then + +For testing the rest of the application inside the sandbox, grant the access +per-installation rather than in the manifest, so the file that describes the +application keeps telling the truth: + +```bash +flatpak override --user --filesystem=~/Pictures paris.tourolle.darkroom +``` + +--- + +## 5. AppImage + +A v1 channel, and the recipe is outstanding work rather than a decision to be +made. What it will have to account for, none of which is a surprise: + +- **glibc.** An AppImage links against the oldest glibc it must run on, so it + is built in a container with an old base rather than on a rolling-release + developer machine. A release binary built on a current rolling-release host carries + `GLIBC_2.44` references and would run on almost nothing else. +- **What to bundle and what not to.** The binary links fontconfig, freetype, + expat, libpng, zlib, brotli and bzip2 — bundle those. It does *not* link + Vulkan, libxkbcommon or either display-server library: wgpu `dlopen`s + `libvulkan.so.1`, and `x11rb` and `wayland-client` speak the wire protocols + in Rust. The Vulkan loader and the ICD must come from the host, and bundling + a loader is the classic way to break an AppImage on a driver it did not + expect. +- **The models.** ~15 MB of ONNX weights inside the image, or a first-run + download. In-tree is consistent with how the Lensfun database ships and with + NFR-SEC-5's local-first posture; the licence question (D13) is the same one + it is everywhere else and is not made easier or harder by this channel. +- **No sandbox.** An AppImage tests nothing about FR-PLAT-LIN-3. It is a + convenience channel for distributions the PKGBUILD does not serve, and should + never be the channel a portal problem is discovered on. + +--- + +## 6. Android: F-Droid in v1, Play deferred + +NFR-COMPAT-2 says Play distribution is what makes ARCH §6.9's constraints +binding, and that is worth reading precisely, because the constraint is already +met and would be met whatever the channel. + +§6.9 is *verified*, not assumed: `MANAGE_EXTERNAL_STORAGE` is not grantable +under Play policy, and `READ_MEDIA_IMAGES` would not help because proprietary +RAW is not typed `image/*` by the platform scanner and does not appear in +`MediaStore.Images`. SAF is the only route that works, so FR-PLAT-AND-1 asks +for it unconditionally and `SourceRef` (ARCH §3.1) exists to make it possible. +A sideloaded or F-Droid build *could* ask for broader permissions; it would +gain nothing by doing so. + +So the coupling runs the opposite way from how it is usually described. Play is +deferred for a reason that has nothing to do with storage: GPLv3 distribution +through Play is generally workable but has not been confirmed for this project +(ARCH §14), and F-Droid has no such question. Confirming it is a licence-reading +exercise; nothing in the storage design waits on the answer. + +--- + +## 7. Where the recipes live + +``` +packaging/ + PKGBUILD Arch source package + paris.tourolle.darkroom.desktop the desktop entry, installed by every channel + paris.tourolle.darkroom.metainfo.xml AppStream, installed by every channel + flatpak/ + paris.tourolle.darkroom.yml the manifest, and where the permissions are argued +``` + +`packaging/` also accumulates built `.pkg.tar.zst` artefacts from local +`makepkg` runs. Those are not part of any channel and should not be committed. diff --git a/docs/outstanding.md b/docs/outstanding.md new file mode 100644 index 0000000..3e6011e --- /dev/null +++ b/docs/outstanding.md @@ -0,0 +1,358 @@ +# DarkRoom — Outstanding work + +**Status:** Living document · first written 2026-08-29 +**Companion to:** [requirements.md §7](requirements.md), [technical-debt.md](technical-debt.md), +[traceability.md](traceability.md) + +What is specified and not built, and for each cluster whether that is a decision, a dependency, or a +gap nobody has looked at. + +This document exists because [traceability.md](traceability.md) cannot tell those apart. It reports +one number — the share of requirements carrying a `TRACES` tag — and a missing tag means either +"nobody has built this" or "somebody built it and did not say so". Both read the same way in the +summary table, which makes that figure pessimistic *and* uninformative at once: it understates what +works while hiding which of the remainder matters. Eight requirements gained a tag on this branch +because the code already satisfied them and nobody had said so. Everything below is the other kind. + +It is also not a plan. [requirements.md §7](requirements.md) records what was deferred deliberately +and needs no argument; this records what is still nominally in scope, so that the distance between +the register and the binary is visible rather than something a reader has to reconstruct from a +percentage. Where the honest answer is "this requirement should be amended rather than met", it says +so — an unbuilt requirement that nobody intends to build is worse than a deferred one, because it +keeps costing attention. + +**Four of these are being built right now**, in parallel worktrees, and are marked **⟳ in +progress** where they appear: focus peaking (part of FR-CULL-3), burst grouping (FR-CULL-5), +Flatpak packaging (FR-PLAT-LIN-3), and Android platform integration (FR-PLAT-AND-2/4/5/6). Strike +those lines as they land rather than rewriting around them. + +--- + +## 1. Plugins — 21 requirements, and a contradiction to resolve before any of them + +**Untagged:** FR-PLG-1, -1a, -2a, -2b, -2c, -3, -3a, -4, -4a, -5, -5a, -5b, -5c, -6, -6a, -7, -8, +-9, -10, -11, -12. + +No plugin host exists. No crate loads anything at runtime: there is no manifest reader, no WASM or +Lua engine, no registry, no signature check, no install path, no capability grant, no per-plugin +failure ledger. `declared/mod.rs` says as much in its own documentation — the operation format is +"not a plugin directory read at startup". + +**Two of §3.10's requirements are met, and they are the interesting two.** FR-PLG-2 and FR-PLG-2d — +the declarative node format — are built and tagged: `core/dr-pipeline/ops/*.yaml` compiled by +`build.rs`, with the restricted expression grammar in `declared/expr.rs` and a parity test asserting +a declared operation and a hand-written one produce identical output. +[code-health.md §3](code-health.md) calls it "a working plugin system that happens to resolve at +build time", and that is exactly right. What is missing is not the format; it is everything that +would let somebody who is not in this repository use it. + +**The contradiction.** [requirements.md §7](requirements.md) lists `| Plugin API | — |` among the +things deferred for v1 — a bare row, where most deferrals carry a justifying note. §3.10 then spends +roughly 280 lines and 23 requirement IDs specifying that same Plugin API in detail. Both statements +are in the register of record, and the traceability denominator counts the second one: 21 IDs, 12% +of all 179 defined requirements, worth about twelve points of coverage on their own — and nearly a +third of everything the matrix reports as uncovered. A reader looking at the coverage figure has no +way to know that, or that the subsystem behind it is one the same document says is not in this +version. + +**And D16 is open.** [Decision D16](requirements.md) — plugin licensing — records that GPLv3 +answers the derivative-work question differently for each of §3.10's three plugin forms, and that +this "must be answered *before* an ecosystem exists, not after", because a term introduced later +cannot be applied to plugins already written. D16 explicitly does not block FR-PLG-2; it blocks +publishing a third-party format as stable. + +**What would resolve this:** an edit to `requirements.md`, not code. Either §7 drops the row, or +§3.10 is marked deferred with the two built requirements carved out. Until one of those happens the +coverage figure is measuring a decision that has already been taken, and taking it again every time +somebody reads the matrix. + +--- + +## 2. Culling — the stated differentiator, half built + +[D11](requirements.md) names culling "the core differentiator". FR-CULL-1, -2, -4 and -8 through -12 +are built. Four are not. + +**FR-CULL-3 — Raw-truth overlays. All three bullets, unbuilt.** Focus peaking does not exist +anywhere; the string appears zero times in the tree. + +The other two are easy to mistake for present, and are not. A histogram and clipping indicators do +exist — `dr-gpu/src/histogram.rs`, `ui/dr-ui/src/histogram.rs`, the panel in `histogram.slint` — +but they are tagged FR-DSP-7 and they answer the opposite question. They read `AdjustPass`'s 8-bit +output and count clipping as `r == 255`, which is to say they describe **the frame the display is +about to show**, after the whole develop chain has run. FR-CULL-3 asks for the histogram of the +*sensor data*, on the explicit grounds that a rendered image "systematically lies about what is +recoverable in the raw". A readout that measures the render cannot answer that however it is +presented, so this is not a matter of moving an existing widget into the culling view. + +The requirement exists because a culling decision made against a rendered preview is a decision made +against the wrong image, and the whole of it is still to build. **⟳ in progress** (focus peaking). + +**FR-CULL-5 — Burst and near-duplicate grouping.** Absent. Worth knowing before it is built: +`core/dr-face/src/calibrate.rs` already *assumes* it exists — "since FR-CULL-5 already groups +bursts, positives are bootstrapped from bursts" — and in fact bootstraps from confirmed labels +instead. That comment is a forward reference to this requirement and will need correcting either +way. `core/dr-catalog/src/dedup.rs` is not this: it is re-import detection under FR-CAT-11, matching +a file against one already catalogued, not two photographs against each other. **⟳ in progress**. + +**FR-CULL-6 — Compare and survey.** Absent. No side-by-side view, no synchronised zoom or pan. +This is the one of the four with no adjacent machinery at all, and it is also the one that most +directly distinguishes culling from browsing. + +**FR-CULL-7 — Culling on tablet.** Absent, and blocked by the three above rather than independent +of them: there is no separate tablet culling surface to build until there is something to put on it. + +--- + +## 3. FR-DEV-3g — AI denoise + +Promoted into v1 by [D11](requirements.md), and named there as the precondition for deferring AI +masking — the argument being that one learned stage earns the runtime that a second could then +reuse. Only classical noise reduction exists: `ops/noise_reduction.rs`, a bilateral filter in two +arrangements, exact for luminance and separable for chroma. It is good, and it is not this. + +`models/` holds two face models and nothing else; `core/dr-segment/models/` holds a YOLO +segmentation model for subject masks. There is no denoise model, no learned demosaic, and no +inference path that is not face or segmentation. + +The obstacle is not the pipeline. It is that [D13](requirements.md) — model licensing — is still +open for the models that already ship, and adding a third learned stage adds a third licence to +answer for. Building the runtime before that is settled means owning the same problem in one more +place. + +--- + +## 4. The render path — FR-DSP-2, FR-DSP-4, NFR-RES-2 + +**FR-DSP-2 — Tiled computation. Unbuilt, and under challenge.** [architecture.md §6.2](architecture.md) +calls for tiling "from day one" on the grounds that retrofitting it is a rewrite. It was not built, +and the evidence has since moved. `core/dr-gpu/tests/frame_budget.rs` carries the argument in its +own header: one fused dispatch over a viewport-sized target is comfortably inside the frame budget, +and "if that stops being true, the recommendation to strike tiled computation from the interactive +path stops being supported, and this test is what says so." +[technical-debt.md TD-4](technical-debt.md) reaches the same place from the other direction — a +tiled convolution at clarity's radius reads nearly twice the taps that an untiled one does, so the +stage that looks most like it wants a tile cache is the stage that would be hurt most by one. + +What exists is the declaration and not the mechanism: `DetailPass::radius` is documented as the halo +a tile would have to be grown by, with a test that pins it, and there is no scheduler to read it. +That is deliberate plumbing, not an oversight. + +**So the open question here is not "when is tiling built" but "is FR-DSP-2 still a requirement".** +Two measurements say it costs more than it saves on the interactive path. Neither says anything +about the export path or about a device under memory pressure, which is where the case for it +actually lives — and that is spike S6, which has not run. + +**FR-DSP-4 — Progressive refinement.** Unbuilt. FR-DSP-1's proxy rendering and TD-4's +quarter-resolution base are adjacent and are not it: both are fixed choices about what resolution to +compute at, where FR-DSP-4 asks for a first frame that is deliberately cheap and a second that +replaces it. Nothing tracks a "this frame is provisional" state. + +**NFR-RES-2 — Images larger than GPU memory.** No answer, and §4.3 knows it: the requirement text +itself asks the reader to "decide explicitly" how ARCH §6.4 and NFR-RES-2 are reconciled. There is +no headroom budget, no allocation-failure fallback, and no spill. Spike S6 — a tiled pipeline on a +mid-range Android device with an image larger than available GPU memory — is the one that would +settle both this and FR-DSP-2, and there is no evidence it has run. + +--- + +## 5. Android beyond running, and Flatpak + +The Android app is not a stub — it builds an APK, runs the whole application, unpacks bundled face +models, and has been measured on a tablet ([faces.md §12.1](faces.md), +[technical-debt.md TD-1](technical-debt.md)). What is missing is the platform contract around it. + +**FR-PLAT-AND-1 is tagged and should not be relied on.** The requirement demands that library access +be obtained *exclusively* through the Storage Access Framework. There is no SAF code: no +`ACTION_OPEN_DOCUMENT_TREE`, no `takePersistableUriPermission`, no `DocumentsContract`. The two tags +rest on a `SourceRef::Document` variant that nothing constructs and a volumes helper, which is the +"plumbing a future feature would use" case [CONTRIBUTING.md](../CONTRIBUTING.md) and +[code-health.md CH-4](code-health.md) both warn about. Android reaches a library through a Nextcloud +account or a folder, over paths, like the desktop. + +That has a consequence for the rest of the cluster: **FR-PLAT-AND-2** — detecting the loss of a +granted tree permission and marking images offline rather than deleting rows — cannot be built until +there is a permission to lose. It is listed here as unbuilt, but it is blocked, not skipped. + +**FR-PLAT-AND-4** (managed background execution, foreground service for exports, stated Doze +behaviour): the manifest declares one activity, no service, and neither `FOREGROUND_SERVICE` nor +`POST_NOTIFICATIONS`. **FR-PLAT-AND-5** (`onTrimMemory` with a stated eviction order): no callback +is registered, though the eviction order it is supposed to drive is specified in FR-NC-6's text. +**FR-PLAT-AND-6** (view and share intents, `FileProvider`): the only intent filter is +`MAIN`/`LAUNCHER`. **⟳ in progress** for this group. + +**FR-PLAT-LIN-3 — Flatpak.** `packaging/` holds an Arch `PKGBUILD` and a `.desktop` entry. There is +no Flatpak manifest, nothing goes through a portal, and `platform/dr-plat/src/secrets.rs` talks to +the Secret Service directly rather than through the portal the requirement names. **⟳ in progress**. + +**NFR-COMPAT-2 — distribution channels.** Unstated, and this is the requirement that makes the +others binding: §4.8 observes that the decision to publish on Play is what turns SAF from a +preference into a constraint. Spike S11, the Play permissions dry-run that would settle it, has not +run. Related, NFR-COMPAT-1's baseline is real but scattered — API 28/36 live in the Android +Dockerfile and are checked in CI against the built ELF, which is good — while the items the +requirement singles out are missing: whether `shaderFloat16` and 16-bit storage are required (the +one it flags as jeopardising R1), minimum RAM, minimum desktop Mesa, and a named reference device +from a second GPU vendor. + +**NFR-OPS-2 and NFR-OPS-4.** Crash reporting is a `log::error!` panic hook on Android and nothing at +all on desktop: no local crash record, no backtrace capture, no upload path and therefore no opt-in +gate to guard it. Update and first run are undefined; the concrete reason NFR-OPS-4 gives — that D2 +pins rawler at a non-SemVer alpha whose camera-support fixes users will need — is unaddressed, and +there is no update mechanism of any kind. + +--- + +## 6. Accessibility and internationalisation — the hard half is done and the easy half is not + +**NFR-A11Y-1 — Localisation.** `@tr(` appears **zero** times across 14,482 lines of Slint. That +number overstates the problem, because the part that is genuinely architectural was got right: +`LocalizedKey` keeps display strings out of `core/` entirely, every operation publishes a key rather +than a label, and `labels::resolve` is the single point where a key becomes text. What that single +point does, however, is a hardcoded English `match` in Rust source — so changing a translation +requires a recompile, which is the one thing the requirement explicitly forbids. There is no message +catalogue in any format, no locale-resolution rule, and no decision recorded about RTL. + +The work left is therefore smaller than it looks and entirely mechanical: a catalogue format, a load +path behind `resolve`, and `@tr(` around the Slint literals. The design it needs already exists. + +**NFR-A11Y-2 — Accessibility.** `accessible-*` appears five times in the whole interface, all five +on one control — the parameter slider in `adjust.slint` — and nothing is set from the Rust side at +all. Everything else in eighteen Slint files is unnamed to AT-SPI and TalkBack. The requirement's own +caveat, that Slint's Android accessibility needs verifying, is spike S13, which has not run. + +**NFR-A11Y-3 — Colour-independent status.** No compliance work found. This is cheap to satisfy while +a control is being written and expensive to retrofit across forty of them, which is an argument for +doing it as part of the NFR-A11Y-2 pass rather than after it. + +--- + +## 7. Catalog and sync + +**FR-CAT-14 — Migration import.** Reading ratings, labels, keywords and collections out of a +Lightroom `.lrcat` or a darktable `library.db`. Unbuilt. The destination is not: keywords, +collections, ratings and the cross-device merge rules are all built and tested, and +`keywords.rs` already anticipates the arrival ("an import from Lightroom can bring in…"). What is +missing is only the two source adapters — which is a comparatively contained piece of work for a +requirement that decides whether somebody can try this software on a library they already have. + +**FR-NC-11 — Initial catalog build.** Using WebDAV `SEARCH` (RFC 5323) against `/remote.php/dav/`, +filtered by mimetype and paginated, in preference to walking folders with PROPFIND. Unbuilt: no +`SEARCH` request is issued anywhere. The PROPFIND walk this exists to replace is fully built and +well optimised — ETag pruning under FR-NC-4 turns an unchanged 50k library into one request — so the +gap is narrower than it reads. It is the *first* build against a large remote library that pays, and +that is the moment a new user meets. + +**FR-CAT-13 — XMP interoperability, tagged and not met.** Read and write standard XMP sidecars. The +single tag sits on `keywords.rs`, which stores keywords; no XMP is parsed or written anywhere in the +tree, and `dr-export`'s metadata module says so about its own half ("neither is read by `dr-decode` +today"). Listed here rather than silently, because a tag makes a gap invisible and this one is +load-bearing for interoperating with the editors FR-CAT-14 imports from. + +--- + +## 8. The performance targets are unverified, not unmet + +Eleven of the fifteen §4.1 targets carry no tag: NFR-P2, -P3, -P4, -P6, -P7, -P8, -P10, -P11, -P12, +-P14, -P15. That is the uninteresting part of this section. + +The interesting part is that §8 and §4.1 both require the same thing, in the same words, and it does +not exist: an automated benchmark suite against a synthetic 50k catalog, run per commit, where **"a +regression beyond a stated tolerance is a build failure, not a notification."** There is no +`benches/` directory in the workspace, no criterion dependency, and no synthetic catalog. The three +CI workflows run `cargo fmt --check`, clippy, `cargo test --workspace`, a release build, an Android +cross-build and a layering check. None of them measures anything, so there is no baseline to +regress against and no tolerance to exceed. + +What does exist is narrower and genuinely good: `dr-gpu/examples/frame_budget` is a real instrument, +its results are committed in [frame-budget.md](frame-budget.md) with the machine and profile named, +and TD-4's before-and-after was measured with it. But it is run by hand — frame-budget.md's own +instruction is "rerun and diff this file" — and the guard version that does live in CI skips itself +where there is no GPU adapter, which the workflow notes is the normal case on a runner, while +asserting its CPU half only when `debug_assertions` is off, which a dev-profile `cargo test` is not. +In CI it therefore asserts approximately nothing. + +**The claim to take from this is precise.** Nothing here says the performance targets are missed. +Several are plausibly met. It says that if one were broken tomorrow, nobody would find out — which +is the failure mode §8 was written to prevent, and the reason it belongs in this document rather +than in a backlog. + +--- + +## 9. Two core requirements that cannot be closed as written + +**R1 — Cross-platform output within a bounded tolerance.** §2 states that the threshold "must be +fixed before spike S9", because S9 both validates R1 and calibrates what tolerance is achievable. +The threshold was never fixed and S9 has not run, so R1 currently has no acceptance criterion at +all — there is nothing a test could assert. + +Worse, the matrix reports R1 as *covered*. Both of its tags are string literals inside the +traceability tool's own unit tests (`tools/traceability/src/lib.rs`), which the tool scans along with +everything else, because a fixture demonstrating tag extraction is indistinguishable from a tag. +NFR-OPS-1 is covered the same way, from a tag on `compute_coverage` — and no rotating, size-capped +on-disk log exists; logging goes to stderr and logcat. These are two of the cases +[CONTRIBUTING.md](../CONTRIBUTING.md) already warns about, now named. + +**R2 — Efficient display of huge RAW libraries.** Its acceptance criterion contains "*(figure +TBD)*" — the scroll velocity below which no cell may render as a placeholder — and asks for a stated +prefetch margin and cache-hit rate. No figure is stated anywhere in the tree, neither quantity is +measured, and [TD-2](technical-debt.md) and [TD-3](technical-debt.md) both describe the thumbnail +path falling short of it in ways that were measured. R2 was deliberately left untagged on this branch +for that reason: the machinery is substantial and the criterion is unmet and partly undefined. + +Both belong with §8 above. A requirement whose threshold was never chosen and a target nothing +measures fail in the same way — not by being wrong, but by being unfalsifiable. + +--- + +## 10. Spikes + +§9 defines fourteen validation spikes and says of three of them: "S1, S2 and S10 are the three that +can invalidate the architecture." + +Only **S1** (Slint + wgpu zero-copy on Linux) and **S14** (the face pipeline on a real library) have +recorded results. S14's are the best evidence of any spike — a dedicated document, a measured pass +over an 18,143-face library, a named device and a reproducible command — though D13's licensing half +remains open. + +**S6, S9, S10, S11 and S13 show no evidence of having run at all.** Each is referenced only from the +requirement text that asks for it: + +| Spike | Would settle | Blocked on | +|---|---|---| +| S6 | FR-DSP-2, NFR-RES-2 — tiling and images larger than GPU memory | Nothing; needs a device and a large image | +| S9 | R1's tolerance threshold, and therefore R1 | Nothing; the threshold is defined *by* running it | +| S10 | Whether SAF at 10k files meets NFR-P1/P3 | §5 — there is no SAF code to measure | +| S11 | NFR-COMPAT-2, and whether Play makes SAF binding | Nothing | +| S13 | NFR-A11Y-2 on Android | §6 — there is almost nothing to test with | + +S2, S3, S4, S5, S7, S8 and S12 are also unrun, several with acknowledgements in the code that say +so (`dr-sync/src/upload.rs` on S8, `dr-sync-nextcloud/src/lib.rs` on S3). S2 is one of the three +architecture-invalidating spikes and needs Adreno and Mali hardware, which the manifest notes no +emulator represents. + +The pattern is worth stating rather than leaving to be inferred: the spikes that ran are the ones +whose subject was being built anyway. The ones that did not are the ones that would have said +whether something *should* be built — which is the opposite of the order §9 asks for. + +--- + +## 11. D12, which governs all of the above + +[Decision D12 — scope versus pace](requirements.md) is still **OPEN**, and says: + +> The calibration selected an ambitious feature set — full tablet editing, full ingest, culling as a +> differentiator, complete GPU masking, AI denoise, Fuji-first colour, deep sync, sidecar durability +> — against a stated pace of evenings and weekends, indefinitely. +> +> **Those are not compatible as stated.** + +Sections 1 through 10 are what that incompatibility looks like eleven versions later, and they land +almost exactly where D12 predicted: tablet editing carries SAF at unproven scale, background +execution limits and two GPU vendors to validate (§5), and every one of those is unbuilt or unrun. +The parts that *were* built — the develop pipeline, sync, faces, the catalog — are the parts that +did not need a decision first. + +D12 is not resolved by choosing to work faster. It is resolved by moving requirements across the +line into §7, which costs nothing but the admission, and which this document is intended to make +easy: every cluster above is a candidate, and each says what it would take to build and what it +would cost to drop. Resolving D12 sets D3 and [architecture.md §10](architecture.md)'s Phase 2. diff --git a/docs/technical-debt.md b/docs/technical-debt.md index 057157c..fb972b7 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -55,6 +55,27 @@ readback is at viewport resolution, not sensor resolution. The 7.43 ms at 4K in not the bill. **It has not been measured on the device**, which is the first thing to do if the develop view feels heavy on the tablet; do not assume this is the cause without a number. +### And a second transfer, while focus peaking is on + +Added 2026-08-29 with FR-CULL-3. The focus-peaking overlay is a compute pass writing its own +`Rgba8Unorm` texture, which on desktop reaches the compositor with no copy — but on Android there is +no more a path for *that* texture than for the frame it belongs to, and an overlay that stayed on +the device while the picture underneath it did not would simply never be seen. So +`FocusPeakPass::read_overlay` follows the frame back through memory, and the Android frame path +carries **two** full-resolution `copy_texture_to_buffer` transfers instead of one. + +This is recorded under TD-1 rather than as its own entry because it is not an independent choice. +It exists only because TD-1 exists, it is bounded by the same thing — `render` fits the pass to the +canvas, so both transfers are at viewport resolution — and TD-1's "Done when" already covers it: +whichever of the three fixes above lands removes the readback for the frame and the overlay +together, because both are the same missing capability. + +Two things worth saying plainly. The doubling is **reasoned, not measured on the device** — the same +gap TD-1 admits about its own cost, and the reason neither number should be quoted as a measurement. +And it is paid only while the photographer has the overlay switched on: `DevelopSession::focus_overlay` +returns on its first line when peaking is off, so with it off there is no dispatch and no transfer, +and the Android frame path is exactly what it was before this feature existed. + ### Paying it off Any one of these removes it: diff --git a/docs/traceability.md b/docs/traceability.md index bb09ff3..9b4a75e 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,19 +9,19 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 294 | -| TRACES tags found | 908 | +| Source files scanned | 298 | +| TRACES tags found | 940 | | Requirements defined | 179 | -| Requirements covered | 107 | -| **Coverage** | **59.8%** (107/179) | +| Requirements covered | 117 | +| **Coverage** | **65.4%** (117/179) | ### By type | Type | Covered | Defined | |---|---|---| -| FR | 85 | 124 | -| NFR | 20 | 49 | -| R | 2 | 6 | +| FR | 89 | 124 | +| NFR | 24 | 49 | +| R | 4 | 6 | ## Orphan tags @@ -34,130 +34,137 @@ _None._ | ID | Tagged in | |---|---| | FR-CAT-1 | [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:109`](../core/dr-catalog/src/walk.rs#L109), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`core/dr-types/src/lib.rs:270`](../core/dr-types/src/lib.rs#L270), [`core/dr-types/src/lib.rs:303`](../core/dr-types/src/lib.rs#L303), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479), [`tools/traceability/src/lib.rs:511`](../tools/traceability/src/lib.rs#L511), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1) | -| FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:586`](../platform/dr-plat/src/storage.rs#L586), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1176`](../ui/dr-ui/src/lib.rs#L1176), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1031`](../ui/dr-ui/ui/library.slint#L1031), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:866`](../ui/dr-ui/ui/library.slint#L866) | -| FR-CAT-11 | [`core/dr-catalog/src/dedup.rs:1`](../core/dr-catalog/src/dedup.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:392`](../core/dr-ingest/src/lib.rs#L392), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1176`](../ui/dr-ui/src/lib.rs#L1176), [`ui/dr-ui/src/library.rs:162`](../ui/dr-ui/src/library.rs#L162), [`ui/dr-ui/src/library.rs:2375`](../ui/dr-ui/src/library.rs#L2375), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | -| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118) | +| FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:586`](../platform/dr-plat/src/storage.rs#L586), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1183`](../ui/dr-ui/src/lib.rs#L1183), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1031`](../ui/dr-ui/ui/library.slint#L1031), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:866`](../ui/dr-ui/ui/library.slint#L866) | +| FR-CAT-11 | [`core/dr-catalog/src/dedup.rs:1`](../core/dr-catalog/src/dedup.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:392`](../core/dr-ingest/src/lib.rs#L392), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1183`](../ui/dr-ui/src/lib.rs#L1183), [`ui/dr-ui/src/library.rs:162`](../ui/dr-ui/src/library.rs#L162), [`ui/dr-ui/src/library.rs:2375`](../ui/dr-ui/src/library.rs#L2375), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | +| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:119`](../core/dr-pipeline/src/sidecar.rs#L119) | | FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) | -| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:641`](../core/dr-catalog/src/schema.rs#L641), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:456`](../core/dr-sync-nextcloud/src/lib.rs#L456), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:429`](../core/dr-sync/src/scan.rs#L429), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1219`](../ui/dr-ui/src/collections_ui.rs#L1219), [`ui/dr-ui/src/collections_ui.rs:1975`](../ui/dr-ui/src/collections_ui.rs#L1975), [`ui/dr-ui/src/library.rs:162`](../ui/dr-ui/src/library.rs#L162), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library.rs:208`](../ui/dr-ui/src/library.rs#L208), [`ui/dr-ui/src/library.rs:3869`](../ui/dr-ui/src/library.rs#L3869), [`ui/dr-ui/src/library.rs:3903`](../ui/dr-ui/src/library.rs#L3903), [`ui/dr-ui/src/library_ui.rs:190`](../ui/dr-ui/src/library_ui.rs#L190), [`ui/dr-ui/src/library_ui.rs:756`](../ui/dr-ui/src/library_ui.rs#L756), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) | +| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:641`](../core/dr-catalog/src/schema.rs#L641), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:457`](../core/dr-sync-nextcloud/src/lib.rs#L457), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:429`](../core/dr-sync/src/scan.rs#L429), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1219`](../ui/dr-ui/src/collections_ui.rs#L1219), [`ui/dr-ui/src/collections_ui.rs:1975`](../ui/dr-ui/src/collections_ui.rs#L1975), [`ui/dr-ui/src/library.rs:162`](../ui/dr-ui/src/library.rs#L162), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library.rs:208`](../ui/dr-ui/src/library.rs#L208), [`ui/dr-ui/src/library.rs:3869`](../ui/dr-ui/src/library.rs#L3869), [`ui/dr-ui/src/library.rs:3903`](../ui/dr-ui/src/library.rs#L3903), [`ui/dr-ui/src/library_ui.rs:190`](../ui/dr-ui/src/library_ui.rs#L190), [`ui/dr-ui/src/library_ui.rs:756`](../ui/dr-ui/src/library_ui.rs#L756), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) | | FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:54`](../core/dr-types/src/lib.rs#L54), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:46`](../platform/dr-plat/src/storage.rs#L46) | | FR-CAT-2 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) | -| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:69`](../core/dr-sync/src/scan.rs#L69), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488), [`ui/dr-ui/src/library.rs:2848`](../ui/dr-ui/src/library.rs#L2848), [`ui/dr-ui/src/library.rs:3356`](../ui/dr-ui/src/library.rs#L3356), [`ui/dr-ui/src/library_ui.rs:172`](../ui/dr-ui/src/library_ui.rs#L172), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/src/library_ui.rs:4592`](../ui/dr-ui/src/library_ui.rs#L4592), [`ui/dr-ui/ui/app.slint:348`](../ui/dr-ui/ui/app.slint#L348), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | +| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:69`](../core/dr-sync/src/scan.rs#L69), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488), [`ui/dr-ui/src/library.rs:2848`](../ui/dr-ui/src/library.rs#L2848), [`ui/dr-ui/src/library.rs:3356`](../ui/dr-ui/src/library.rs#L3356), [`ui/dr-ui/src/library_ui.rs:172`](../ui/dr-ui/src/library_ui.rs#L172), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/src/library_ui.rs:4592`](../ui/dr-ui/src/library_ui.rs#L4592), [`ui/dr-ui/ui/app.slint:365`](../ui/dr-ui/ui/app.slint#L365), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | | FR-CAT-4 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/schema.rs:318`](../core/dr-catalog/src/schema.rs#L318), [`ui/dr-ui/src/library.rs:189`](../ui/dr-ui/src/library.rs#L189), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1) | -| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1059`](../core/dr-catalog/src/schema.rs#L1059), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/collections_ui.rs:1558`](../ui/dr-ui/src/collections_ui.rs#L1558), [`ui/dr-ui/src/collections_ui.rs:1577`](../ui/dr-ui/src/collections_ui.rs#L1577), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3917`](../ui/dr-ui/src/library.rs#L3917), [`ui/dr-ui/src/library_ui.rs:631`](../ui/dr-ui/src/library_ui.rs#L631), [`ui/dr-ui/src/library_ui.rs:6389`](../ui/dr-ui/src/library_ui.rs#L6389), [`ui/dr-ui/src/library_ui.rs:6400`](../ui/dr-ui/src/library_ui.rs#L6400), [`ui/dr-ui/src/library_ui.rs:6413`](../ui/dr-ui/src/library_ui.rs#L6413), [`ui/dr-ui/src/library_ui.rs:6428`](../ui/dr-ui/src/library_ui.rs#L6428), [`ui/dr-ui/src/library_ui.rs:6437`](../ui/dr-ui/src/library_ui.rs#L6437), [`ui/dr-ui/ui/app.slint:293`](../ui/dr-ui/ui/app.slint#L293), [`ui/dr-ui/ui/app.slint:474`](../ui/dr-ui/ui/app.slint#L474), [`ui/dr-ui/ui/library.slint:1242`](../ui/dr-ui/ui/library.slint#L1242), [`ui/dr-ui/ui/library.slint:1245`](../ui/dr-ui/ui/library.slint#L1245), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:859`](../ui/dr-ui/ui/library.slint#L859), [`ui/dr-ui/ui/library.slint:911`](../ui/dr-ui/ui/library.slint#L911) | -| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:213`](../ui/dr-ui/src/library.rs#L213), [`ui/dr-ui/src/library.rs:4163`](../ui/dr-ui/src/library.rs#L4163), [`ui/dr-ui/src/library_ui.rs:321`](../ui/dr-ui/src/library_ui.rs#L321), [`ui/dr-ui/src/library_ui.rs:393`](../ui/dr-ui/src/library_ui.rs#L393), [`ui/dr-ui/src/library_ui.rs:5203`](../ui/dr-ui/src/library_ui.rs#L5203), [`ui/dr-ui/src/library_ui.rs:5256`](../ui/dr-ui/src/library_ui.rs#L5256), [`ui/dr-ui/src/library_ui.rs:6529`](../ui/dr-ui/src/library_ui.rs#L6529), [`ui/dr-ui/ui/app.slint:296`](../ui/dr-ui/ui/app.slint#L296), [`ui/dr-ui/ui/app.slint:474`](../ui/dr-ui/ui/app.slint#L474), [`ui/dr-ui/ui/app.slint:762`](../ui/dr-ui/ui/app.slint#L762), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1318`](../ui/dr-ui/ui/library.slint#L1318), [`ui/dr-ui/ui/library.slint:911`](../ui/dr-ui/ui/library.slint#L911), [`ui/dr-ui/ui/settings.slint:150`](../ui/dr-ui/ui/settings.slint#L150) | -| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3489`](../ui/dr-ui/src/library_ui.rs#L3489), [`ui/dr-ui/src/library_ui.rs:4182`](../ui/dr-ui/src/library_ui.rs#L4182), [`ui/dr-ui/ui/app.slint:469`](../ui/dr-ui/ui/app.slint#L469), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2585`](../ui/dr-ui/ui/library.slint#L2585), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) | -| FR-CAT-8 | [`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/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`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), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3570`](../ui/dr-ui/src/develop.rs#L3570), [`ui/dr-ui/src/develop.rs:3599`](../ui/dr-ui/src/develop.rs#L3599), [`ui/dr-ui/src/export.rs:750`](../ui/dr-ui/src/export.rs#L750), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:1830`](../ui/dr-ui/src/lib.rs#L1830), [`ui/dr-ui/src/lib.rs:1965`](../ui/dr-ui/src/lib.rs#L1965), [`ui/dr-ui/src/lib.rs:554`](../ui/dr-ui/src/lib.rs#L554), [`ui/dr-ui/src/lib.rs:979`](../ui/dr-ui/src/lib.rs#L979), [`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:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:790`](../ui/dr-ui/src/library.rs#L790), [`ui/dr-ui/src/library_ui.rs:4884`](../ui/dr-ui/src/library_ui.rs#L4884), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | -| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:120`](../core/dr-types/src/lib.rs#L120), [`ui/dr-ui/src/develop.rs:2997`](../ui/dr-ui/src/develop.rs#L2997), [`ui/dr-ui/src/library.rs:148`](../ui/dr-ui/src/library.rs#L148), [`ui/dr-ui/src/library.rs:1747`](../ui/dr-ui/src/library.rs#L1747), [`ui/dr-ui/src/library.rs:1823`](../ui/dr-ui/src/library.rs#L1823), [`ui/dr-ui/src/library.rs:233`](../ui/dr-ui/src/library.rs#L233), [`ui/dr-ui/src/library.rs:4270`](../ui/dr-ui/src/library.rs#L4270), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:774`](../ui/dr-ui/src/library.rs#L774), [`ui/dr-ui/src/library.rs:790`](../ui/dr-ui/src/library.rs#L790), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library_ui.rs:1580`](../ui/dr-ui/src/library_ui.rs#L1580), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1622`](../ui/dr-ui/src/library_ui.rs#L1622), [`ui/dr-ui/src/library_ui.rs:1716`](../ui/dr-ui/src/library_ui.rs#L1716), [`ui/dr-ui/src/library_ui.rs:232`](../ui/dr-ui/src/library_ui.rs#L232), [`ui/dr-ui/src/library_ui.rs:2332`](../ui/dr-ui/src/library_ui.rs#L2332), [`ui/dr-ui/src/library_ui.rs:265`](../ui/dr-ui/src/library_ui.rs#L265), [`ui/dr-ui/src/library_ui.rs:2770`](../ui/dr-ui/src/library_ui.rs#L2770), [`ui/dr-ui/src/library_ui.rs:2992`](../ui/dr-ui/src/library_ui.rs#L2992), [`ui/dr-ui/src/library_ui.rs:3270`](../ui/dr-ui/src/library_ui.rs#L3270), [`ui/dr-ui/src/library_ui.rs:3358`](../ui/dr-ui/src/library_ui.rs#L3358), [`ui/dr-ui/src/library_ui.rs:3540`](../ui/dr-ui/src/library_ui.rs#L3540), [`ui/dr-ui/src/library_ui.rs:3654`](../ui/dr-ui/src/library_ui.rs#L3654), [`ui/dr-ui/src/library_ui.rs:446`](../ui/dr-ui/src/library_ui.rs#L446), [`ui/dr-ui/src/library_ui.rs:504`](../ui/dr-ui/src/library_ui.rs#L504), [`ui/dr-ui/src/library_ui.rs:5301`](../ui/dr-ui/src/library_ui.rs#L5301), [`ui/dr-ui/src/library_ui.rs:5416`](../ui/dr-ui/src/library_ui.rs#L5416), [`ui/dr-ui/src/presets.rs:446`](../ui/dr-ui/src/presets.rs#L446), [`ui/dr-ui/src/presets.rs:458`](../ui/dr-ui/src/presets.rs#L458), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1059`](../core/dr-catalog/src/schema.rs#L1059), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:136`](../core/dr-pipeline/src/sidecar.rs#L136), [`ui/dr-ui/src/collections_ui.rs:1558`](../ui/dr-ui/src/collections_ui.rs#L1558), [`ui/dr-ui/src/collections_ui.rs:1577`](../ui/dr-ui/src/collections_ui.rs#L1577), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3917`](../ui/dr-ui/src/library.rs#L3917), [`ui/dr-ui/src/library_ui.rs:631`](../ui/dr-ui/src/library_ui.rs#L631), [`ui/dr-ui/src/library_ui.rs:6389`](../ui/dr-ui/src/library_ui.rs#L6389), [`ui/dr-ui/src/library_ui.rs:6400`](../ui/dr-ui/src/library_ui.rs#L6400), [`ui/dr-ui/src/library_ui.rs:6413`](../ui/dr-ui/src/library_ui.rs#L6413), [`ui/dr-ui/src/library_ui.rs:6428`](../ui/dr-ui/src/library_ui.rs#L6428), [`ui/dr-ui/src/library_ui.rs:6437`](../ui/dr-ui/src/library_ui.rs#L6437), [`ui/dr-ui/ui/app.slint:310`](../ui/dr-ui/ui/app.slint#L310), [`ui/dr-ui/ui/app.slint:491`](../ui/dr-ui/ui/app.slint#L491), [`ui/dr-ui/ui/library.slint:1242`](../ui/dr-ui/ui/library.slint#L1242), [`ui/dr-ui/ui/library.slint:1245`](../ui/dr-ui/ui/library.slint#L1245), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:859`](../ui/dr-ui/ui/library.slint#L859), [`ui/dr-ui/ui/library.slint:911`](../ui/dr-ui/ui/library.slint#L911) | +| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:213`](../ui/dr-ui/src/library.rs#L213), [`ui/dr-ui/src/library.rs:4163`](../ui/dr-ui/src/library.rs#L4163), [`ui/dr-ui/src/library_ui.rs:321`](../ui/dr-ui/src/library_ui.rs#L321), [`ui/dr-ui/src/library_ui.rs:393`](../ui/dr-ui/src/library_ui.rs#L393), [`ui/dr-ui/src/library_ui.rs:5203`](../ui/dr-ui/src/library_ui.rs#L5203), [`ui/dr-ui/src/library_ui.rs:5256`](../ui/dr-ui/src/library_ui.rs#L5256), [`ui/dr-ui/src/library_ui.rs:6529`](../ui/dr-ui/src/library_ui.rs#L6529), [`ui/dr-ui/ui/app.slint:313`](../ui/dr-ui/ui/app.slint#L313), [`ui/dr-ui/ui/app.slint:491`](../ui/dr-ui/ui/app.slint#L491), [`ui/dr-ui/ui/app.slint:779`](../ui/dr-ui/ui/app.slint#L779), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1318`](../ui/dr-ui/ui/library.slint#L1318), [`ui/dr-ui/ui/library.slint:911`](../ui/dr-ui/ui/library.slint#L911), [`ui/dr-ui/ui/settings.slint:150`](../ui/dr-ui/ui/settings.slint#L150) | +| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3489`](../ui/dr-ui/src/library_ui.rs#L3489), [`ui/dr-ui/src/library_ui.rs:4182`](../ui/dr-ui/src/library_ui.rs#L4182), [`ui/dr-ui/ui/app.slint:486`](../ui/dr-ui/ui/app.slint#L486), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2585`](../ui/dr-ui/ui/library.slint#L2585), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) | +| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:346`](../core/dr-pipeline/src/graph.rs#L346), [`core/dr-pipeline/src/graph.rs:385`](../core/dr-pipeline/src/graph.rs#L385), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1637`](../core/dr-pipeline/src/sidecar.rs#L1637), [`core/dr-pipeline/src/sidecar.rs:93`](../core/dr-pipeline/src/sidecar.rs#L93), [`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), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3693`](../ui/dr-ui/src/develop.rs#L3693), [`ui/dr-ui/src/develop.rs:3722`](../ui/dr-ui/src/develop.rs#L3722), [`ui/dr-ui/src/export.rs:750`](../ui/dr-ui/src/export.rs#L750), [`ui/dr-ui/src/lib.rs:1436`](../ui/dr-ui/src/lib.rs#L1436), [`ui/dr-ui/src/lib.rs:1893`](../ui/dr-ui/src/lib.rs#L1893), [`ui/dr-ui/src/lib.rs:2028`](../ui/dr-ui/src/lib.rs#L2028), [`ui/dr-ui/src/lib.rs:561`](../ui/dr-ui/src/lib.rs#L561), [`ui/dr-ui/src/lib.rs:986`](../ui/dr-ui/src/lib.rs#L986), [`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:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:790`](../ui/dr-ui/src/library.rs#L790), [`ui/dr-ui/src/library_ui.rs:4884`](../ui/dr-ui/src/library_ui.rs#L4884), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:120`](../core/dr-types/src/lib.rs#L120), [`ui/dr-ui/src/develop.rs:3120`](../ui/dr-ui/src/develop.rs#L3120), [`ui/dr-ui/src/library.rs:148`](../ui/dr-ui/src/library.rs#L148), [`ui/dr-ui/src/library.rs:1747`](../ui/dr-ui/src/library.rs#L1747), [`ui/dr-ui/src/library.rs:1823`](../ui/dr-ui/src/library.rs#L1823), [`ui/dr-ui/src/library.rs:233`](../ui/dr-ui/src/library.rs#L233), [`ui/dr-ui/src/library.rs:4270`](../ui/dr-ui/src/library.rs#L4270), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:774`](../ui/dr-ui/src/library.rs#L774), [`ui/dr-ui/src/library.rs:790`](../ui/dr-ui/src/library.rs#L790), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library_ui.rs:1580`](../ui/dr-ui/src/library_ui.rs#L1580), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1622`](../ui/dr-ui/src/library_ui.rs#L1622), [`ui/dr-ui/src/library_ui.rs:1716`](../ui/dr-ui/src/library_ui.rs#L1716), [`ui/dr-ui/src/library_ui.rs:232`](../ui/dr-ui/src/library_ui.rs#L232), [`ui/dr-ui/src/library_ui.rs:2332`](../ui/dr-ui/src/library_ui.rs#L2332), [`ui/dr-ui/src/library_ui.rs:265`](../ui/dr-ui/src/library_ui.rs#L265), [`ui/dr-ui/src/library_ui.rs:2770`](../ui/dr-ui/src/library_ui.rs#L2770), [`ui/dr-ui/src/library_ui.rs:2992`](../ui/dr-ui/src/library_ui.rs#L2992), [`ui/dr-ui/src/library_ui.rs:3270`](../ui/dr-ui/src/library_ui.rs#L3270), [`ui/dr-ui/src/library_ui.rs:3358`](../ui/dr-ui/src/library_ui.rs#L3358), [`ui/dr-ui/src/library_ui.rs:3540`](../ui/dr-ui/src/library_ui.rs#L3540), [`ui/dr-ui/src/library_ui.rs:3654`](../ui/dr-ui/src/library_ui.rs#L3654), [`ui/dr-ui/src/library_ui.rs:446`](../ui/dr-ui/src/library_ui.rs#L446), [`ui/dr-ui/src/library_ui.rs:504`](../ui/dr-ui/src/library_ui.rs#L504), [`ui/dr-ui/src/library_ui.rs:5301`](../ui/dr-ui/src/library_ui.rs#L5301), [`ui/dr-ui/src/library_ui.rs:5416`](../ui/dr-ui/src/library_ui.rs#L5416), [`ui/dr-ui/src/presets.rs:446`](../ui/dr-ui/src/presets.rs#L446), [`ui/dr-ui/src/presets.rs:458`](../ui/dr-ui/src/presets.rs#L458), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) | -| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:357`](../core/dr-catalog/src/schema.rs#L357), [`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/develop.rs:119`](../ui/dr-ui/src/develop.rs#L119), [`ui/dr-ui/src/develop.rs:128`](../ui/dr-ui/src/develop.rs#L128), [`ui/dr-ui/src/develop.rs:1920`](../ui/dr-ui/src/develop.rs#L1920), [`ui/dr-ui/src/develop.rs:194`](../ui/dr-ui/src/develop.rs#L194), [`ui/dr-ui/src/develop.rs:687`](../ui/dr-ui/src/develop.rs#L687), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1937`](../ui/dr-ui/src/lib.rs#L1937), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | +| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:357`](../core/dr-catalog/src/schema.rs#L357), [`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/develop.rs:120`](../ui/dr-ui/src/develop.rs#L120), [`ui/dr-ui/src/develop.rs:129`](../ui/dr-ui/src/develop.rs#L129), [`ui/dr-ui/src/develop.rs:1944`](../ui/dr-ui/src/develop.rs#L1944), [`ui/dr-ui/src/develop.rs:195`](../ui/dr-ui/src/develop.rs#L195), [`ui/dr-ui/src/develop.rs:688`](../ui/dr-ui/src/develop.rs#L688), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2000`](../ui/dr-ui/src/lib.rs#L2000), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-11 | [`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), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/library.rs:254`](../ui/dr-ui/src/library.rs#L254), [`ui/dr-ui/src/library.rs:284`](../ui/dr-ui/src/library.rs#L284), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:357`](../core/dr-catalog/src/schema.rs#L357), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463) | -| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/library.rs:213`](../ui/dr-ui/src/library.rs#L213), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459) | +| FR-CULL-3 | [`core/dr-gpu/src/focus.rs:154`](../core/dr-gpu/src/focus.rs#L154), [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`ui/dr-ui/src/develop.rs:2930`](../ui/dr-ui/src/develop.rs#L2930), [`ui/dr-ui/src/develop.rs:2941`](../ui/dr-ui/src/develop.rs#L2941), [`ui/dr-ui/src/develop.rs:2947`](../ui/dr-ui/src/develop.rs#L2947), [`ui/dr-ui/src/develop.rs:2964`](../ui/dr-ui/src/develop.rs#L2964), [`ui/dr-ui/src/develop.rs:726`](../ui/dr-ui/src/develop.rs#L726), [`ui/dr-ui/src/develop.rs:732`](../ui/dr-ui/src/develop.rs#L732), [`ui/dr-ui/src/lib.rs:1475`](../ui/dr-ui/src/lib.rs#L1475), [`ui/dr-ui/src/lib.rs:1553`](../ui/dr-ui/src/lib.rs#L1553), [`ui/dr-ui/src/lib.rs:1626`](../ui/dr-ui/src/lib.rs#L1626), [`ui/dr-ui/src/lib.rs:1660`](../ui/dr-ui/src/lib.rs#L1660), [`ui/dr-ui/src/lib.rs:2910`](../ui/dr-ui/src/lib.rs#L2910), [`ui/dr-ui/src/lib.rs:321`](../ui/dr-ui/src/lib.rs#L321), [`ui/dr-ui/src/peaking.rs:1`](../ui/dr-ui/src/peaking.rs#L1), [`ui/dr-ui/ui/app.slint:1689`](../ui/dr-ui/ui/app.slint#L1689), [`ui/dr-ui/ui/app.slint:2234`](../ui/dr-ui/ui/app.slint#L2234), [`ui/dr-ui/ui/app.slint:75`](../ui/dr-ui/ui/app.slint#L75), [`ui/dr-ui/ui/peaking.slint:1`](../ui/dr-ui/ui/peaking.slint#L1), [`ui/dr-ui/ui/peaking.slint:25`](../ui/dr-ui/ui/peaking.slint#L25), [`ui/dr-ui/ui/peaking.slint:56`](../ui/dr-ui/ui/peaking.slint#L56) | +| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:136`](../core/dr-pipeline/src/sidecar.rs#L136), [`ui/dr-ui/src/library.rs:213`](../ui/dr-ui/src/library.rs#L213), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459) | | 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:419`](../ui/dr-ui/ui/settings.slint#L419), [`ui/dr-ui/ui/settings.slint:82`](../ui/dr-ui/ui/settings.slint#L82) | | 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-1 | [`core/dr-pipeline/src/graph.rs:1`](../core/dr-pipeline/src/graph.rs#L1), [`core/dr-pipeline/src/sidecar.rs:1`](../core/dr-pipeline/src/sidecar.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: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:177`](../core/dr-pipeline/src/framing.rs#L177), [`core/dr-pipeline/src/framing.rs:234`](../core/dr-pipeline/src/framing.rs#L234), [`core/dr-pipeline/src/framing.rs:314`](../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/framing.rs:488`](../core/dr-pipeline/src/framing.rs#L488), [`core/dr-pipeline/src/framing.rs:743`](../core/dr-pipeline/src/framing.rs#L743), [`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:1424`](../ui/dr-ui/src/develop.rs#L1424), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1902`](../ui/dr-ui/src/develop.rs#L1902), [`ui/dr-ui/src/develop.rs:1920`](../ui/dr-ui/src/develop.rs#L1920), [`ui/dr-ui/src/develop.rs:1934`](../ui/dr-ui/src/develop.rs#L1934), [`ui/dr-ui/src/develop.rs:1956`](../ui/dr-ui/src/develop.rs#L1956), [`ui/dr-ui/src/develop.rs:2102`](../ui/dr-ui/src/develop.rs#L2102), [`ui/dr-ui/src/develop.rs:2200`](../ui/dr-ui/src/develop.rs#L2200), [`ui/dr-ui/src/develop.rs:3268`](../ui/dr-ui/src/develop.rs#L3268), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3298`](../ui/dr-ui/src/develop.rs#L3298), [`ui/dr-ui/src/develop.rs:3364`](../ui/dr-ui/src/develop.rs#L3364), [`ui/dr-ui/src/develop.rs:3378`](../ui/dr-ui/src/develop.rs#L3378), [`ui/dr-ui/src/develop.rs:3570`](../ui/dr-ui/src/develop.rs#L3570), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:4230`](../ui/dr-ui/src/develop.rs#L4230), [`ui/dr-ui/src/develop.rs:4284`](../ui/dr-ui/src/develop.rs#L4284), [`ui/dr-ui/src/develop.rs:4328`](../ui/dr-ui/src/develop.rs#L4328), [`ui/dr-ui/src/develop.rs:4378`](../ui/dr-ui/src/develop.rs#L4378), [`ui/dr-ui/src/develop.rs:586`](../ui/dr-ui/src/develop.rs#L586), [`ui/dr-ui/src/develop.rs:647`](../ui/dr-ui/src/develop.rs#L647), [`ui/dr-ui/src/develop.rs:726`](../ui/dr-ui/src/develop.rs#L726), [`ui/dr-ui/src/develop.rs:773`](../ui/dr-ui/src/develop.rs#L773), [`ui/dr-ui/src/develop.rs:802`](../ui/dr-ui/src/develop.rs#L802), [`ui/dr-ui/src/lib.rs:1514`](../ui/dr-ui/src/lib.rs#L1514), [`ui/dr-ui/src/lib.rs:2240`](../ui/dr-ui/src/lib.rs#L2240), [`ui/dr-ui/src/lib.rs:2436`](../ui/dr-ui/src/lib.rs#L2436), [`ui/dr-ui/src/lib.rs:2608`](../ui/dr-ui/src/lib.rs#L2608), [`ui/dr-ui/src/lib.rs:2672`](../ui/dr-ui/src/lib.rs#L2672), [`ui/dr-ui/src/lib.rs:2726`](../ui/dr-ui/src/lib.rs#L2726), [`ui/dr-ui/src/lib.rs:320`](../ui/dr-ui/src/lib.rs#L320), [`ui/dr-ui/src/lib.rs:363`](../ui/dr-ui/src/lib.rs#L363), [`ui/dr-ui/src/lib.rs:386`](../ui/dr-ui/src/lib.rs#L386), [`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/adjust.slint:517`](../ui/dr-ui/ui/adjust.slint#L517), [`ui/dr-ui/ui/adjust.slint:635`](../ui/dr-ui/ui/adjust.slint#L635), [`ui/dr-ui/ui/app.slint:119`](../ui/dr-ui/ui/app.slint#L119), [`ui/dr-ui/ui/app.slint:165`](../ui/dr-ui/ui/app.slint#L165), [`ui/dr-ui/ui/app.slint:1862`](../ui/dr-ui/ui/app.slint#L1862), [`ui/dr-ui/ui/app.slint:872`](../ui/dr-ui/ui/app.slint#L872), [`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:385`](../core/dr-pipeline/src/framing.rs#L385), [`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:1321`](../ui/dr-ui/src/develop.rs#L1321), [`ui/dr-ui/src/lib.rs:669`](../ui/dr-ui/src/lib.rs#L669), [`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:385`](../core/dr-pipeline/src/framing.rs#L385), [`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:4957`](../ui/dr-ui/src/develop.rs#L4957) | -| 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:314`](../core/dr-pipeline/src/framing.rs#L314), [`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-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:177`](../core/dr-pipeline/src/framing.rs#L177), [`core/dr-pipeline/src/framing.rs:234`](../core/dr-pipeline/src/framing.rs#L234), [`core/dr-pipeline/src/framing.rs:314`](../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/framing.rs:488`](../core/dr-pipeline/src/framing.rs#L488), [`core/dr-pipeline/src/framing.rs:743`](../core/dr-pipeline/src/framing.rs#L743), [`core/dr-pipeline/src/graph.rs:170`](../core/dr-pipeline/src/graph.rs#L170), [`core/dr-pipeline/src/graph.rs:578`](../core/dr-pipeline/src/graph.rs#L578), [`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:157`](../core/dr-pipeline/src/sidecar.rs#L157), [`core/dr-pipeline/src/sidecar.rs:1637`](../core/dr-pipeline/src/sidecar.rs#L1637), [`core/dr-pipeline/src/sidecar.rs:1697`](../core/dr-pipeline/src/sidecar.rs#L1697), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:102`](../ui/dr-ui/src/develop.rs#L102), [`ui/dr-ui/src/develop.rs:1448`](../ui/dr-ui/src/develop.rs#L1448), [`ui/dr-ui/src/develop.rs:164`](../ui/dr-ui/src/develop.rs#L164), [`ui/dr-ui/src/develop.rs:1926`](../ui/dr-ui/src/develop.rs#L1926), [`ui/dr-ui/src/develop.rs:1944`](../ui/dr-ui/src/develop.rs#L1944), [`ui/dr-ui/src/develop.rs:1958`](../ui/dr-ui/src/develop.rs#L1958), [`ui/dr-ui/src/develop.rs:1980`](../ui/dr-ui/src/develop.rs#L1980), [`ui/dr-ui/src/develop.rs:2126`](../ui/dr-ui/src/develop.rs#L2126), [`ui/dr-ui/src/develop.rs:2224`](../ui/dr-ui/src/develop.rs#L2224), [`ui/dr-ui/src/develop.rs:327`](../ui/dr-ui/src/develop.rs#L327), [`ui/dr-ui/src/develop.rs:3391`](../ui/dr-ui/src/develop.rs#L3391), [`ui/dr-ui/src/develop.rs:3421`](../ui/dr-ui/src/develop.rs#L3421), [`ui/dr-ui/src/develop.rs:3487`](../ui/dr-ui/src/develop.rs#L3487), [`ui/dr-ui/src/develop.rs:3501`](../ui/dr-ui/src/develop.rs#L3501), [`ui/dr-ui/src/develop.rs:364`](../ui/dr-ui/src/develop.rs#L364), [`ui/dr-ui/src/develop.rs:3693`](../ui/dr-ui/src/develop.rs#L3693), [`ui/dr-ui/src/develop.rs:4353`](../ui/dr-ui/src/develop.rs#L4353), [`ui/dr-ui/src/develop.rs:4407`](../ui/dr-ui/src/develop.rs#L4407), [`ui/dr-ui/src/develop.rs:4451`](../ui/dr-ui/src/develop.rs#L4451), [`ui/dr-ui/src/develop.rs:4501`](../ui/dr-ui/src/develop.rs#L4501), [`ui/dr-ui/src/develop.rs:587`](../ui/dr-ui/src/develop.rs#L587), [`ui/dr-ui/src/develop.rs:648`](../ui/dr-ui/src/develop.rs#L648), [`ui/dr-ui/src/develop.rs:746`](../ui/dr-ui/src/develop.rs#L746), [`ui/dr-ui/src/develop.rs:793`](../ui/dr-ui/src/develop.rs#L793), [`ui/dr-ui/src/develop.rs:822`](../ui/dr-ui/src/develop.rs#L822), [`ui/dr-ui/src/lib.rs:1534`](../ui/dr-ui/src/lib.rs#L1534), [`ui/dr-ui/src/lib.rs:2303`](../ui/dr-ui/src/lib.rs#L2303), [`ui/dr-ui/src/lib.rs:2499`](../ui/dr-ui/src/lib.rs#L2499), [`ui/dr-ui/src/lib.rs:2671`](../ui/dr-ui/src/lib.rs#L2671), [`ui/dr-ui/src/lib.rs:2735`](../ui/dr-ui/src/lib.rs#L2735), [`ui/dr-ui/src/lib.rs:2789`](../ui/dr-ui/src/lib.rs#L2789), [`ui/dr-ui/src/lib.rs:327`](../ui/dr-ui/src/lib.rs#L327), [`ui/dr-ui/src/lib.rs:370`](../ui/dr-ui/src/lib.rs#L370), [`ui/dr-ui/src/lib.rs:393`](../ui/dr-ui/src/lib.rs#L393), [`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/adjust.slint:517`](../ui/dr-ui/ui/adjust.slint#L517), [`ui/dr-ui/ui/adjust.slint:635`](../ui/dr-ui/ui/adjust.slint#L635), [`ui/dr-ui/ui/app.slint:136`](../ui/dr-ui/ui/app.slint#L136), [`ui/dr-ui/ui/app.slint:182`](../ui/dr-ui/ui/app.slint#L182), [`ui/dr-ui/ui/app.slint:1891`](../ui/dr-ui/ui/app.slint#L1891), [`ui/dr-ui/ui/app.slint:889`](../ui/dr-ui/ui/app.slint#L889), [`ui/dr-ui/ui/masks.slint:524`](../ui/dr-ui/ui/masks.slint#L524) | +| 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:385`](../core/dr-pipeline/src/framing.rs#L385), [`core/dr-pipeline/src/graph.rs:24`](../core/dr-pipeline/src/graph.rs#L24), [`core/dr-pipeline/src/graph.rs:251`](../core/dr-pipeline/src/graph.rs#L251), [`core/dr-pipeline/src/graph.rs:46`](../core/dr-pipeline/src/graph.rs#L46), [`core/dr-pipeline/src/graph.rs:59`](../core/dr-pipeline/src/graph.rs#L59), [`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:1345`](../ui/dr-ui/src/develop.rs#L1345), [`ui/dr-ui/src/lib.rs:676`](../ui/dr-ui/src/lib.rs#L676), [`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:385`](../core/dr-pipeline/src/framing.rs#L385), [`core/dr-pipeline/src/graph.rs:59`](../core/dr-pipeline/src/graph.rs#L59), [`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:251`](../core/dr-pipeline/src/graph.rs#L251), [`core/dr-pipeline/src/graph.rs:46`](../core/dr-pipeline/src/graph.rs#L46), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`ui/dr-ui/src/develop.rs:5080`](../ui/dr-ui/src/develop.rs#L5080) | +| 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:314`](../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/graph.rs:617`](../core/dr-pipeline/src/graph.rs#L617), [`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:3012`](../ui/dr-ui/src/develop.rs#L3012), [`ui/dr-ui/src/develop.rs:3029`](../ui/dr-ui/src/develop.rs#L3029), [`ui/dr-ui/src/develop.rs:3041`](../ui/dr-ui/src/develop.rs#L3041), [`ui/dr-ui/src/develop.rs:3079`](../ui/dr-ui/src/develop.rs#L3079), [`ui/dr-ui/src/develop.rs:3088`](../ui/dr-ui/src/develop.rs#L3088), [`ui/dr-ui/src/develop.rs:3191`](../ui/dr-ui/src/develop.rs#L3191), [`ui/dr-ui/src/develop.rs:3607`](../ui/dr-ui/src/develop.rs#L3607), [`ui/dr-ui/src/develop.rs:3622`](../ui/dr-ui/src/develop.rs#L3622), [`ui/dr-ui/src/lib.rs:2214`](../ui/dr-ui/src/lib.rs#L2214), [`ui/dr-ui/src/lib.rs:602`](../ui/dr-ui/src/lib.rs#L602), [`ui/dr-ui/src/lib.rs:660`](../ui/dr-ui/src/lib.rs#L660), [`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:395`](../ui/dr-ui/src/presets.rs#L395), [`ui/dr-ui/ui/adjust.slint:1034`](../ui/dr-ui/ui/adjust.slint#L1034), [`ui/dr-ui/ui/adjust.slint:1116`](../ui/dr-ui/ui/adjust.slint#L1116), [`ui/dr-ui/ui/app.slint:2377`](../ui/dr-ui/ui/app.slint#L2377), [`ui/dr-ui/ui/app.slint:600`](../ui/dr-ui/ui/app.slint#L600) | -| 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:1050`](../core/dr-pipeline/src/framing.rs#L1050), [`core/dr-pipeline/src/framing.rs:328`](../core/dr-pipeline/src/framing.rs#L328), [`core/dr-pipeline/src/framing.rs:488`](../core/dr-pipeline/src/framing.rs#L488), [`core/dr-types/src/lib.rs:337`](../core/dr-types/src/lib.rs#L337), [`core/dr-types/src/lib.rs:445`](../core/dr-types/src/lib.rs#L445), [`core/dr-types/src/lib.rs:457`](../core/dr-types/src/lib.rs#L457), [`core/dr-types/src/lib.rs:473`](../core/dr-types/src/lib.rs#L473), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:2119`](../ui/dr-ui/src/develop.rs#L2119), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) | -| FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:335`](../core/dr-gpu/src/lib.rs#L335), [`ui/dr-ui/ui/crop.slint:1`](../ui/dr-ui/ui/crop.slint#L1) | -| 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:3041`](../ui/dr-ui/src/develop.rs#L3041), [`ui/dr-ui/src/develop.rs:3622`](../ui/dr-ui/src/develop.rs#L3622), [`ui/dr-ui/src/develop.rs:3652`](../ui/dr-ui/src/develop.rs#L3652), [`ui/dr-ui/src/develop.rs:3665`](../ui/dr-ui/src/develop.rs#L3665), [`ui/dr-ui/src/develop.rs:3677`](../ui/dr-ui/src/develop.rs#L3677), [`ui/dr-ui/src/develop.rs:3693`](../ui/dr-ui/src/develop.rs#L3693), [`ui/dr-ui/src/develop.rs:3725`](../ui/dr-ui/src/develop.rs#L3725), [`ui/dr-ui/src/develop.rs:3729`](../ui/dr-ui/src/develop.rs#L3729), [`ui/dr-ui/src/develop.rs:3748`](../ui/dr-ui/src/develop.rs#L3748), [`ui/dr-ui/src/develop.rs:3764`](../ui/dr-ui/src/develop.rs#L3764), [`ui/dr-ui/src/develop.rs:708`](../ui/dr-ui/src/develop.rs#L708), [`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:1462`](../ui/dr-ui/src/lib.rs#L1462), [`ui/dr-ui/src/lib.rs:1492`](../ui/dr-ui/src/lib.rs#L1492), [`ui/dr-ui/src/lib.rs:1500`](../ui/dr-ui/src/lib.rs#L1500), [`ui/dr-ui/src/lib.rs:2410`](../ui/dr-ui/src/lib.rs#L2410), [`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-pipeline/src/preset.rs:381`](../core/dr-pipeline/src/preset.rs#L381), [`core/dr-pipeline/src/preset.rs:414`](../core/dr-pipeline/src/preset.rs#L414), [`core/dr-pipeline/src/starter.rs:1`](../core/dr-pipeline/src/starter.rs#L1), [`core/dr-preset-xmp/src/lib.rs:1`](../core/dr-preset-xmp/src/lib.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3564`](../ui/dr-ui/src/develop.rs#L3564), [`ui/dr-ui/src/develop.rs:3585`](../ui/dr-ui/src/develop.rs#L3585), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:2025`](../ui/dr-ui/src/lib.rs#L2025), [`ui/dr-ui/src/lib.rs:2054`](../ui/dr-ui/src/lib.rs#L2054), [`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/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:657`](../ui/dr-ui/src/presets.rs#L657), [`ui/dr-ui/src/presets.rs:691`](../ui/dr-ui/src/presets.rs#L691), [`ui/dr-ui/ui/adjust.slint:709`](../ui/dr-ui/ui/adjust.slint#L709), [`ui/dr-ui/ui/adjust.slint:736`](../ui/dr-ui/ui/adjust.slint#L736), [`ui/dr-ui/ui/adjust.slint:779`](../ui/dr-ui/ui/adjust.slint#L779), [`ui/dr-ui/ui/app.slint:1456`](../ui/dr-ui/ui/app.slint#L1456), [`ui/dr-ui/ui/app.slint:2466`](../ui/dr-ui/ui/app.slint#L2466), [`ui/dr-ui/ui/app.slint:672`](../ui/dr-ui/ui/app.slint#L672), [`ui/dr-ui/ui/app.slint:683`](../ui/dr-ui/ui/app.slint#L683), [`ui/dr-ui/ui/library.slint:1345`](../ui/dr-ui/ui/library.slint#L1345), [`ui/dr-ui/ui/library.slint:1352`](../ui/dr-ui/ui/library.slint#L1352), [`ui/dr-ui/ui/library.slint:837`](../ui/dr-ui/ui/library.slint#L837), [`ui/dr-ui/ui/library.slint:853`](../ui/dr-ui/ui/library.slint#L853), [`ui/dr-ui/ui/library.slint:922`](../ui/dr-ui/ui/library.slint#L922), [`ui/dr-ui/ui/library.slint:937`](../ui/dr-ui/ui/library.slint#L937), [`ui/dr-ui/ui/presets.slint:105`](../ui/dr-ui/ui/presets.slint#L105), [`ui/dr-ui/ui/presets.slint:113`](../ui/dr-ui/ui/presets.slint#L113), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/presets.slint:276`](../ui/dr-ui/ui/presets.slint#L276), [`ui/dr-ui/ui/presets.slint:297`](../ui/dr-ui/ui/presets.slint#L297), [`ui/dr-ui/ui/presets.slint:5`](../ui/dr-ui/ui/presets.slint#L5), [`ui/dr-ui/ui/presets.slint:60`](../ui/dr-ui/ui/presets.slint#L60), [`ui/dr-ui/ui/settings.slint:101`](../ui/dr-ui/ui/settings.slint#L101), [`ui/dr-ui/ui/settings.slint:557`](../ui/dr-ui/ui/settings.slint#L557) | -| 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:3693`](../ui/dr-ui/src/develop.rs#L3693), [`ui/dr-ui/src/develop.rs:3725`](../ui/dr-ui/src/develop.rs#L3725), [`ui/dr-ui/src/lib.rs:1500`](../ui/dr-ui/src/lib.rs#L1500), [`ui/dr-ui/src/lib.rs:2410`](../ui/dr-ui/src/lib.rs#L2410), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | -| 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:2271`](../ui/dr-ui/src/develop.rs#L2271), [`ui/dr-ui/src/develop.rs:2309`](../ui/dr-ui/src/develop.rs#L2309), [`ui/dr-ui/src/develop.rs:2374`](../ui/dr-ui/src/develop.rs#L2374), [`ui/dr-ui/src/develop.rs:2457`](../ui/dr-ui/src/develop.rs#L2457), [`ui/dr-ui/src/develop.rs:2471`](../ui/dr-ui/src/develop.rs#L2471), [`ui/dr-ui/src/develop.rs:756`](../ui/dr-ui/src/develop.rs#L756), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1521`](../ui/dr-ui/src/lib.rs#L1521), [`ui/dr-ui/src/lib.rs:2528`](../ui/dr-ui/src/lib.rs#L2528), [`ui/dr-ui/src/lib.rs:325`](../ui/dr-ui/src/lib.rs#L325), [`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:810`](../ui/dr-ui/ui/adjust.slint#L810), [`ui/dr-ui/ui/app.slint:109`](../ui/dr-ui/ui/app.slint#L109), [`ui/dr-ui/ui/app.slint:1765`](../ui/dr-ui/ui/app.slint#L1765), [`ui/dr-ui/ui/app.slint:1940`](../ui/dr-ui/ui/app.slint#L1940), [`ui/dr-ui/ui/app.slint:2283`](../ui/dr-ui/ui/app.slint#L2283), [`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:156`](../core/dr-gpu/src/lib.rs#L156), [`core/dr-gpu/src/lib.rs:176`](../core/dr-gpu/src/lib.rs#L176), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:77`](../core/dr-gpu/src/lib.rs#L77), [`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:2795`](../ui/dr-ui/src/develop.rs#L2795), [`ui/dr-ui/src/develop.rs:4019`](../ui/dr-ui/src/develop.rs#L4019), [`ui/dr-ui/src/develop.rs:4502`](../ui/dr-ui/src/develop.rs#L4502), [`ui/dr-ui/src/develop.rs:4536`](../ui/dr-ui/src/develop.rs#L4536), [`ui/dr-ui/src/lib.rs:73`](../ui/dr-ui/src/lib.rs#L73), [`ui/dr-ui/src/lib.rs:810`](../ui/dr-ui/src/lib.rs#L810), [`ui/dr-ui/src/lib.rs:869`](../ui/dr-ui/src/lib.rs#L869) | +| 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:102`](../core/dr-pipeline/src/graph.rs#L102), [`core/dr-pipeline/src/graph.rs:125`](../core/dr-pipeline/src/graph.rs#L125), [`core/dr-pipeline/src/graph.rs:325`](../core/dr-pipeline/src/graph.rs#L325), [`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:112`](../core/dr-pipeline/src/sidecar.rs#L112), [`core/dr-pipeline/src/sidecar.rs:168`](../core/dr-pipeline/src/sidecar.rs#L168), [`core/dr-pipeline/src/sidecar.rs:1958`](../core/dr-pipeline/src/sidecar.rs#L1958), [`core/dr-pipeline/src/sidecar.rs:2034`](../core/dr-pipeline/src/sidecar.rs#L2034), [`core/dr-pipeline/src/sidecar.rs:534`](../core/dr-pipeline/src/sidecar.rs#L534), [`core/dr-pipeline/src/sidecar.rs:661`](../core/dr-pipeline/src/sidecar.rs#L661), [`core/dr-pipeline/src/sidecar.rs:793`](../core/dr-pipeline/src/sidecar.rs#L793), [`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:3135`](../ui/dr-ui/src/develop.rs#L3135), [`ui/dr-ui/src/develop.rs:3152`](../ui/dr-ui/src/develop.rs#L3152), [`ui/dr-ui/src/develop.rs:3164`](../ui/dr-ui/src/develop.rs#L3164), [`ui/dr-ui/src/develop.rs:3202`](../ui/dr-ui/src/develop.rs#L3202), [`ui/dr-ui/src/develop.rs:3211`](../ui/dr-ui/src/develop.rs#L3211), [`ui/dr-ui/src/develop.rs:3314`](../ui/dr-ui/src/develop.rs#L3314), [`ui/dr-ui/src/develop.rs:3730`](../ui/dr-ui/src/develop.rs#L3730), [`ui/dr-ui/src/develop.rs:3745`](../ui/dr-ui/src/develop.rs#L3745), [`ui/dr-ui/src/lib.rs:2277`](../ui/dr-ui/src/lib.rs#L2277), [`ui/dr-ui/src/lib.rs:609`](../ui/dr-ui/src/lib.rs#L609), [`ui/dr-ui/src/lib.rs:667`](../ui/dr-ui/src/lib.rs#L667), [`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:395`](../ui/dr-ui/src/presets.rs#L395), [`ui/dr-ui/ui/adjust.slint:1034`](../ui/dr-ui/ui/adjust.slint#L1034), [`ui/dr-ui/ui/adjust.slint:1116`](../ui/dr-ui/ui/adjust.slint#L1116), [`ui/dr-ui/ui/app.slint:2426`](../ui/dr-ui/ui/app.slint#L2426), [`ui/dr-ui/ui/app.slint:617`](../ui/dr-ui/ui/app.slint#L617) | +| 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:1050`](../core/dr-pipeline/src/framing.rs#L1050), [`core/dr-pipeline/src/framing.rs:328`](../core/dr-pipeline/src/framing.rs#L328), [`core/dr-pipeline/src/framing.rs:488`](../core/dr-pipeline/src/framing.rs#L488), [`core/dr-types/src/lib.rs:337`](../core/dr-types/src/lib.rs#L337), [`core/dr-types/src/lib.rs:445`](../core/dr-types/src/lib.rs#L445), [`core/dr-types/src/lib.rs:457`](../core/dr-types/src/lib.rs#L457), [`core/dr-types/src/lib.rs:473`](../core/dr-types/src/lib.rs#L473), [`ui/dr-ui/src/develop.rs:139`](../ui/dr-ui/src/develop.rs#L139), [`ui/dr-ui/src/develop.rs:2143`](../ui/dr-ui/src/develop.rs#L2143), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) | +| FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:338`](../core/dr-gpu/src/lib.rs#L338), [`ui/dr-ui/ui/crop.slint:1`](../ui/dr-ui/ui/crop.slint#L1) | +| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:346`](../core/dr-pipeline/src/graph.rs#L346), [`core/dr-pipeline/src/graph.rs:385`](../core/dr-pipeline/src/graph.rs#L385), [`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:3164`](../ui/dr-ui/src/develop.rs#L3164), [`ui/dr-ui/src/develop.rs:3745`](../ui/dr-ui/src/develop.rs#L3745), [`ui/dr-ui/src/develop.rs:3775`](../ui/dr-ui/src/develop.rs#L3775), [`ui/dr-ui/src/develop.rs:3788`](../ui/dr-ui/src/develop.rs#L3788), [`ui/dr-ui/src/develop.rs:3800`](../ui/dr-ui/src/develop.rs#L3800), [`ui/dr-ui/src/develop.rs:3816`](../ui/dr-ui/src/develop.rs#L3816), [`ui/dr-ui/src/develop.rs:3848`](../ui/dr-ui/src/develop.rs#L3848), [`ui/dr-ui/src/develop.rs:3852`](../ui/dr-ui/src/develop.rs#L3852), [`ui/dr-ui/src/develop.rs:3871`](../ui/dr-ui/src/develop.rs#L3871), [`ui/dr-ui/src/develop.rs:3887`](../ui/dr-ui/src/develop.rs#L3887), [`ui/dr-ui/src/develop.rs:709`](../ui/dr-ui/src/develop.rs#L709), [`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:1469`](../ui/dr-ui/src/lib.rs#L1469), [`ui/dr-ui/src/lib.rs:1512`](../ui/dr-ui/src/lib.rs#L1512), [`ui/dr-ui/src/lib.rs:1520`](../ui/dr-ui/src/lib.rs#L1520), [`ui/dr-ui/src/lib.rs:2473`](../ui/dr-ui/src/lib.rs#L2473), [`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-pipeline/src/preset.rs:381`](../core/dr-pipeline/src/preset.rs#L381), [`core/dr-pipeline/src/preset.rs:414`](../core/dr-pipeline/src/preset.rs#L414), [`core/dr-pipeline/src/starter.rs:1`](../core/dr-pipeline/src/starter.rs#L1), [`core/dr-preset-xmp/src/lib.rs:1`](../core/dr-preset-xmp/src/lib.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3687`](../ui/dr-ui/src/develop.rs#L3687), [`ui/dr-ui/src/develop.rs:3708`](../ui/dr-ui/src/develop.rs#L3708), [`ui/dr-ui/src/lib.rs:1436`](../ui/dr-ui/src/lib.rs#L1436), [`ui/dr-ui/src/lib.rs:2088`](../ui/dr-ui/src/lib.rs#L2088), [`ui/dr-ui/src/lib.rs:2117`](../ui/dr-ui/src/lib.rs#L2117), [`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/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:657`](../ui/dr-ui/src/presets.rs#L657), [`ui/dr-ui/src/presets.rs:691`](../ui/dr-ui/src/presets.rs#L691), [`ui/dr-ui/ui/adjust.slint:709`](../ui/dr-ui/ui/adjust.slint#L709), [`ui/dr-ui/ui/adjust.slint:736`](../ui/dr-ui/ui/adjust.slint#L736), [`ui/dr-ui/ui/adjust.slint:779`](../ui/dr-ui/ui/adjust.slint#L779), [`ui/dr-ui/ui/app.slint:1473`](../ui/dr-ui/ui/app.slint#L1473), [`ui/dr-ui/ui/app.slint:2515`](../ui/dr-ui/ui/app.slint#L2515), [`ui/dr-ui/ui/app.slint:689`](../ui/dr-ui/ui/app.slint#L689), [`ui/dr-ui/ui/app.slint:700`](../ui/dr-ui/ui/app.slint#L700), [`ui/dr-ui/ui/library.slint:1345`](../ui/dr-ui/ui/library.slint#L1345), [`ui/dr-ui/ui/library.slint:1352`](../ui/dr-ui/ui/library.slint#L1352), [`ui/dr-ui/ui/library.slint:837`](../ui/dr-ui/ui/library.slint#L837), [`ui/dr-ui/ui/library.slint:853`](../ui/dr-ui/ui/library.slint#L853), [`ui/dr-ui/ui/library.slint:922`](../ui/dr-ui/ui/library.slint#L922), [`ui/dr-ui/ui/library.slint:937`](../ui/dr-ui/ui/library.slint#L937), [`ui/dr-ui/ui/presets.slint:105`](../ui/dr-ui/ui/presets.slint#L105), [`ui/dr-ui/ui/presets.slint:113`](../ui/dr-ui/ui/presets.slint#L113), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/presets.slint:276`](../ui/dr-ui/ui/presets.slint#L276), [`ui/dr-ui/ui/presets.slint:297`](../ui/dr-ui/ui/presets.slint#L297), [`ui/dr-ui/ui/presets.slint:5`](../ui/dr-ui/ui/presets.slint#L5), [`ui/dr-ui/ui/presets.slint:60`](../ui/dr-ui/ui/presets.slint#L60), [`ui/dr-ui/ui/settings.slint:101`](../ui/dr-ui/ui/settings.slint#L101), [`ui/dr-ui/ui/settings.slint:557`](../ui/dr-ui/ui/settings.slint#L557) | +| 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:3816`](../ui/dr-ui/src/develop.rs#L3816), [`ui/dr-ui/src/develop.rs:3848`](../ui/dr-ui/src/develop.rs#L3848), [`ui/dr-ui/src/lib.rs:1520`](../ui/dr-ui/src/lib.rs#L1520), [`ui/dr-ui/src/lib.rs:2473`](../ui/dr-ui/src/lib.rs#L2473), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | +| 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:114`](../core/dr-pipeline/src/graph.rs#L114), [`core/dr-pipeline/src/graph.rs:192`](../core/dr-pipeline/src/graph.rs#L192), [`core/dr-pipeline/src/graph.rs:686`](../core/dr-pipeline/src/graph.rs#L686), [`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:184`](../core/dr-pipeline/src/sidecar.rs#L184), [`core/dr-pipeline/src/sidecar.rs:353`](../core/dr-pipeline/src/sidecar.rs#L353), [`core/dr-pipeline/src/sidecar.rs:673`](../core/dr-pipeline/src/sidecar.rs#L673), [`core/dr-pipeline/src/sidecar.rs:809`](../core/dr-pipeline/src/sidecar.rs#L809), [`core/dr-pipeline/src/sidecar.rs:863`](../core/dr-pipeline/src/sidecar.rs#L863), [`core/dr-pipeline/src/sidecar.rs:893`](../core/dr-pipeline/src/sidecar.rs#L893), [`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:2295`](../ui/dr-ui/src/develop.rs#L2295), [`ui/dr-ui/src/develop.rs:2333`](../ui/dr-ui/src/develop.rs#L2333), [`ui/dr-ui/src/develop.rs:2398`](../ui/dr-ui/src/develop.rs#L2398), [`ui/dr-ui/src/develop.rs:2481`](../ui/dr-ui/src/develop.rs#L2481), [`ui/dr-ui/src/develop.rs:2495`](../ui/dr-ui/src/develop.rs#L2495), [`ui/dr-ui/src/develop.rs:776`](../ui/dr-ui/src/develop.rs#L776), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1541`](../ui/dr-ui/src/lib.rs#L1541), [`ui/dr-ui/src/lib.rs:2591`](../ui/dr-ui/src/lib.rs#L2591), [`ui/dr-ui/src/lib.rs:332`](../ui/dr-ui/src/lib.rs#L332), [`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:810`](../ui/dr-ui/ui/adjust.slint#L810), [`ui/dr-ui/ui/app.slint:126`](../ui/dr-ui/ui/app.slint#L126), [`ui/dr-ui/ui/app.slint:1794`](../ui/dr-ui/ui/app.slint#L1794), [`ui/dr-ui/ui/app.slint:1969`](../ui/dr-ui/ui/app.slint#L1969), [`ui/dr-ui/ui/app.slint:2332`](../ui/dr-ui/ui/app.slint#L2332), [`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:159`](../core/dr-gpu/src/lib.rs#L159), [`core/dr-gpu/src/lib.rs:179`](../core/dr-gpu/src/lib.rs#L179), [`core/dr-gpu/src/lib.rs:57`](../core/dr-gpu/src/lib.rs#L57), [`core/dr-gpu/src/lib.rs:80`](../core/dr-gpu/src/lib.rs#L80), [`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:548`](../core/dr-pipeline/src/graph.rs#L548), [`core/dr-pipeline/src/graph.rs:578`](../core/dr-pipeline/src/graph.rs#L578), [`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:2819`](../ui/dr-ui/src/develop.rs#L2819), [`ui/dr-ui/src/develop.rs:4142`](../ui/dr-ui/src/develop.rs#L4142), [`ui/dr-ui/src/develop.rs:4625`](../ui/dr-ui/src/develop.rs#L4625), [`ui/dr-ui/src/develop.rs:4659`](../ui/dr-ui/src/develop.rs#L4659), [`ui/dr-ui/src/lib.rs:74`](../ui/dr-ui/src/lib.rs#L74), [`ui/dr-ui/src/lib.rs:817`](../ui/dr-ui/src/lib.rs#L817), [`ui/dr-ui/src/lib.rs:876`](../ui/dr-ui/src/lib.rs#L876) | | 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:2825`](../ui/dr-ui/src/develop.rs#L2825), [`ui/dr-ui/src/develop.rs:5794`](../ui/dr-ui/src/develop.rs#L5794), [`ui/dr-ui/src/lib.rs:1477`](../ui/dr-ui/src/lib.rs#L1477), [`ui/dr-ui/src/lib.rs:2847`](../ui/dr-ui/src/lib.rs#L2847) | -| 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:2874`](../ui/dr-ui/src/develop.rs#L2874), [`ui/dr-ui/src/develop.rs:5604`](../ui/dr-ui/src/develop.rs#L5604), [`ui/dr-ui/src/develop.rs:5636`](../ui/dr-ui/src/develop.rs#L5636), [`ui/dr-ui/src/develop.rs:719`](../ui/dr-ui/src/develop.rs#L719), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1577`](../ui/dr-ui/src/lib.rs#L1577), [`ui/dr-ui/src/lib.rs:315`](../ui/dr-ui/src/lib.rs#L315), [`ui/dr-ui/ui/app.slint:69`](../ui/dr-ui/ui/app.slint#L69), [`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-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:2771`](../ui/dr-ui/src/develop.rs#L2771), [`ui/dr-ui/src/develop.rs:2825`](../ui/dr-ui/src/develop.rs#L2825), [`ui/dr-ui/src/develop.rs:5794`](../ui/dr-ui/src/develop.rs#L5794), [`ui/dr-ui/src/develop.rs:5840`](../ui/dr-ui/src/develop.rs#L5840), [`ui/dr-ui/src/develop.rs:5859`](../ui/dr-ui/src/develop.rs#L5859), [`ui/dr-ui/src/develop.rs:787`](../ui/dr-ui/src/develop.rs#L787), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1451`](../ui/dr-ui/src/lib.rs#L1451), [`ui/dr-ui/src/lib.rs:1477`](../ui/dr-ui/src/lib.rs#L1477), [`ui/dr-ui/src/lib.rs:2824`](../ui/dr-ui/src/lib.rs#L2824), [`ui/dr-ui/src/lib.rs:2847`](../ui/dr-ui/src/lib.rs#L2847), [`ui/dr-ui/ui/app.slint:1625`](../ui/dr-ui/ui/app.slint#L1625), [`ui/dr-ui/ui/app.slint:48`](../ui/dr-ui/ui/app.slint#L48), [`ui/dr-ui/ui/settings.slint:124`](../ui/dr-ui/ui/settings.slint#L124), [`ui/dr-ui/ui/settings.slint:794`](../ui/dr-ui/ui/settings.slint#L794) | +| 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:2849`](../ui/dr-ui/src/develop.rs#L2849), [`ui/dr-ui/src/develop.rs:5917`](../ui/dr-ui/src/develop.rs#L5917), [`ui/dr-ui/src/lib.rs:1497`](../ui/dr-ui/src/lib.rs#L1497), [`ui/dr-ui/src/lib.rs:2974`](../ui/dr-ui/src/lib.rs#L2974) | +| 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:2898`](../ui/dr-ui/src/develop.rs#L2898), [`ui/dr-ui/src/develop.rs:5727`](../ui/dr-ui/src/develop.rs#L5727), [`ui/dr-ui/src/develop.rs:5759`](../ui/dr-ui/src/develop.rs#L5759), [`ui/dr-ui/src/develop.rs:720`](../ui/dr-ui/src/develop.rs#L720), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1607`](../ui/dr-ui/src/lib.rs#L1607), [`ui/dr-ui/src/lib.rs:316`](../ui/dr-ui/src/lib.rs#L316), [`ui/dr-ui/ui/app.slint:70`](../ui/dr-ui/ui/app.slint#L70), [`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-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:2795`](../ui/dr-ui/src/develop.rs#L2795), [`ui/dr-ui/src/develop.rs:2849`](../ui/dr-ui/src/develop.rs#L2849), [`ui/dr-ui/src/develop.rs:5917`](../ui/dr-ui/src/develop.rs#L5917), [`ui/dr-ui/src/develop.rs:5963`](../ui/dr-ui/src/develop.rs#L5963), [`ui/dr-ui/src/develop.rs:5982`](../ui/dr-ui/src/develop.rs#L5982), [`ui/dr-ui/src/develop.rs:807`](../ui/dr-ui/src/develop.rs#L807), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1458`](../ui/dr-ui/src/lib.rs#L1458), [`ui/dr-ui/src/lib.rs:1497`](../ui/dr-ui/src/lib.rs#L1497), [`ui/dr-ui/src/lib.rs:2887`](../ui/dr-ui/src/lib.rs#L2887), [`ui/dr-ui/src/lib.rs:2974`](../ui/dr-ui/src/lib.rs#L2974), [`ui/dr-ui/ui/app.slint:1642`](../ui/dr-ui/ui/app.slint#L1642), [`ui/dr-ui/ui/app.slint:49`](../ui/dr-ui/ui/app.slint#L49), [`ui/dr-ui/ui/settings.slint:124`](../ui/dr-ui/ui/settings.slint#L124), [`ui/dr-ui/ui/settings.slint:794`](../ui/dr-ui/ui/settings.slint#L794) | | 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) | -| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2398`](../core/dr-gpu/src/adjust.rs#L2398), [`core/dr-pipeline/src/graph.rs:537`](../core/dr-pipeline/src/graph.rs#L537), [`core/dr-pipeline/src/graph.rs:591`](../core/dr-pipeline/src/graph.rs#L591), [`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), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:623`](../core/dr-types/src/settings.rs#L623), [`ui/dr-ui/src/develop.rs:5859`](../ui/dr-ui/src/develop.rs#L5859), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | +| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2398`](../core/dr-gpu/src/adjust.rs#L2398), [`core/dr-pipeline/src/graph.rs:538`](../core/dr-pipeline/src/graph.rs#L538), [`core/dr-pipeline/src/graph.rs:592`](../core/dr-pipeline/src/graph.rs#L592), [`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), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:623`](../core/dr-types/src/settings.rs#L623), [`ui/dr-ui/src/develop.rs:5982`](../ui/dr-ui/src/develop.rs#L5982), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-3 | [`core/dr-export/src/lib.rs:182`](../core/dr-export/src/lib.rs#L182), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:136`](../core/dr-export/src/size.rs#L136), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-export/src/size.rs:84`](../core/dr-export/src/size.rs#L84), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:668`](../core/dr-types/src/settings.rs#L668), [`core/dr-types/src/settings.rs:676`](../core/dr-types/src/settings.rs#L676), [`core/dr-types/src/settings.rs:692`](../core/dr-types/src/settings.rs#L692), [`core/dr-types/src/settings.rs:761`](../core/dr-types/src/settings.rs#L761), [`core/dr-types/src/settings.rs:789`](../core/dr-types/src/settings.rs#L789), [`core/dr-types/src/settings.rs:800`](../core/dr-types/src/settings.rs#L800), [`core/dr-types/src/settings.rs:806`](../core/dr-types/src/settings.rs#L806), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:203`](../ui/dr-ui/src/settings_ui.rs#L203), [`ui/dr-ui/src/settings_ui.rs:216`](../ui/dr-ui/src/settings_ui.rs#L216), [`ui/dr-ui/src/settings_ui.rs:231`](../ui/dr-ui/src/settings_ui.rs#L231), [`ui/dr-ui/src/settings_ui.rs:561`](../ui/dr-ui/src/settings_ui.rs#L561), [`ui/dr-ui/ui/settings.slint:697`](../ui/dr-ui/ui/settings.slint#L697), [`ui/dr-ui/ui/settings.slint:707`](../ui/dr-ui/ui/settings.slint#L707) | | FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | -| FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:415`](../ui/dr-ui/src/lib.rs#L415), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:49`](../ui/dr-ui/src/settings_ui.rs#L49), [`ui/dr-ui/src/settings_ui.rs:675`](../ui/dr-ui/src/settings_ui.rs#L675) | -| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/lib.rs:205`](../ui/dr-ui/src/lib.rs#L205), [`ui/dr-ui/src/lib.rs:2156`](../ui/dr-ui/src/lib.rs#L2156), [`ui/dr-ui/src/lib.rs:415`](../ui/dr-ui/src/lib.rs#L415), [`ui/dr-ui/src/lib.rs:450`](../ui/dr-ui/src/lib.rs#L450), [`ui/dr-ui/src/lib.rs:477`](../ui/dr-ui/src/lib.rs#L477), [`ui/dr-ui/src/library_ui.rs:3375`](../ui/dr-ui/src/library_ui.rs#L3375), [`ui/dr-ui/src/library_ui.rs:563`](../ui/dr-ui/src/library_ui.rs#L563), [`ui/dr-ui/src/library_ui.rs:6371`](../ui/dr-ui/src/library_ui.rs#L6371), [`ui/dr-ui/src/library_ui.rs:6448`](../ui/dr-ui/src/library_ui.rs#L6448), [`ui/dr-ui/src/library_ui.rs:6460`](../ui/dr-ui/src/library_ui.rs#L6460), [`ui/dr-ui/src/library_ui.rs:663`](../ui/dr-ui/src/library_ui.rs#L663), [`ui/dr-ui/src/library_ui.rs:720`](../ui/dr-ui/src/library_ui.rs#L720), [`ui/dr-ui/ui/app.slint:1008`](../ui/dr-ui/ui/app.slint#L1008), [`ui/dr-ui/ui/app.slint:1466`](../ui/dr-ui/ui/app.slint#L1466), [`ui/dr-ui/ui/library.slint:1355`](../ui/dr-ui/ui/library.slint#L1355), [`ui/dr-ui/ui/library.slint:841`](../ui/dr-ui/ui/library.slint#L841), [`ui/dr-ui/ui/library.slint:951`](../ui/dr-ui/ui/library.slint#L951) | +| FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:422`](../ui/dr-ui/src/lib.rs#L422), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:49`](../ui/dr-ui/src/settings_ui.rs#L49), [`ui/dr-ui/src/settings_ui.rs:675`](../ui/dr-ui/src/settings_ui.rs#L675) | +| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/lib.rs:206`](../ui/dr-ui/src/lib.rs#L206), [`ui/dr-ui/src/lib.rs:2219`](../ui/dr-ui/src/lib.rs#L2219), [`ui/dr-ui/src/lib.rs:422`](../ui/dr-ui/src/lib.rs#L422), [`ui/dr-ui/src/lib.rs:457`](../ui/dr-ui/src/lib.rs#L457), [`ui/dr-ui/src/lib.rs:484`](../ui/dr-ui/src/lib.rs#L484), [`ui/dr-ui/src/library_ui.rs:3375`](../ui/dr-ui/src/library_ui.rs#L3375), [`ui/dr-ui/src/library_ui.rs:563`](../ui/dr-ui/src/library_ui.rs#L563), [`ui/dr-ui/src/library_ui.rs:6371`](../ui/dr-ui/src/library_ui.rs#L6371), [`ui/dr-ui/src/library_ui.rs:6448`](../ui/dr-ui/src/library_ui.rs#L6448), [`ui/dr-ui/src/library_ui.rs:6460`](../ui/dr-ui/src/library_ui.rs#L6460), [`ui/dr-ui/src/library_ui.rs:663`](../ui/dr-ui/src/library_ui.rs#L663), [`ui/dr-ui/src/library_ui.rs:720`](../ui/dr-ui/src/library_ui.rs#L720), [`ui/dr-ui/ui/app.slint:1025`](../ui/dr-ui/ui/app.slint#L1025), [`ui/dr-ui/ui/app.slint:1483`](../ui/dr-ui/ui/app.slint#L1483), [`ui/dr-ui/ui/library.slint:1355`](../ui/dr-ui/ui/library.slint#L1355), [`ui/dr-ui/ui/library.slint:841`](../ui/dr-ui/ui/library.slint#L841), [`ui/dr-ui/ui/library.slint:951`](../ui/dr-ui/ui/library.slint#L951) | | FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:653`](../core/dr-types/src/lib.rs#L653), [`core/dr-types/src/settings.rs:336`](../core/dr-types/src/settings.rs#L336), [`ui/dr-ui/src/export.rs:619`](../ui/dr-ui/src/export.rs#L619), [`ui/dr-ui/src/export.rs:647`](../ui/dr-ui/src/export.rs#L647), [`ui/dr-ui/src/export.rs:777`](../ui/dr-ui/src/export.rs#L777), [`ui/dr-ui/src/export.rs:794`](../ui/dr-ui/src/export.rs#L794), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | -| FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2956`](../ui/dr-ui/src/develop.rs#L2956), [`ui/dr-ui/src/lib.rs:415`](../ui/dr-ui/src/lib.rs#L415) | +| FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:3079`](../ui/dr-ui/src/develop.rs#L3079), [`ui/dr-ui/src/lib.rs:422`](../ui/dr-ui/src/lib.rs#L422) | | FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:355`](../core/dr-sync/src/account.rs#L355), [`ui/dr-ui/src/launch.rs:277`](../ui/dr-ui/src/launch.rs#L277), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:417`](../ui/dr-ui/src/launch_ui.rs#L417) | -| FR-NC-10 | [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:477`](../ui/dr-ui/src/lib.rs#L477), [`ui/dr-ui/src/library.rs:1068`](../ui/dr-ui/src/library.rs#L1068), [`ui/dr-ui/src/library.rs:1823`](../ui/dr-ui/src/library.rs#L1823), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library_ui.rs:1622`](../ui/dr-ui/src/library_ui.rs#L1622), [`ui/dr-ui/src/library_ui.rs:3375`](../ui/dr-ui/src/library_ui.rs#L3375), [`ui/dr-ui/src/library_ui.rs:504`](../ui/dr-ui/src/library_ui.rs#L504), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | -| FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:39`](../core/dr-sync-nextcloud/src/lib.rs#L39), [`core/dr-sync-nextcloud/src/lib.rs:913`](../core/dr-sync-nextcloud/src/lib.rs#L913), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:81`](../core/dr-sync/src/account.rs#L81), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`core/dr-sync/src/provider.rs:1`](../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | +| FR-NC-10 | [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:484`](../ui/dr-ui/src/lib.rs#L484), [`ui/dr-ui/src/library.rs:1068`](../ui/dr-ui/src/library.rs#L1068), [`ui/dr-ui/src/library.rs:1823`](../ui/dr-ui/src/library.rs#L1823), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library_ui.rs:1622`](../ui/dr-ui/src/library_ui.rs#L1622), [`ui/dr-ui/src/library_ui.rs:3375`](../ui/dr-ui/src/library_ui.rs#L3375), [`ui/dr-ui/src/library_ui.rs:504`](../ui/dr-ui/src/library_ui.rs#L504), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:40`](../core/dr-sync-nextcloud/src/lib.rs#L40), [`core/dr-sync-nextcloud/src/lib.rs:914`](../core/dr-sync-nextcloud/src/lib.rs#L914), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:81`](../core/dr-sync/src/account.rs#L81), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`core/dr-sync/src/provider.rs:1`](../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | | FR-NC-13 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`ui/dr-ui/src/launch_ui.rs:316`](../ui/dr-ui/src/launch_ui.rs#L316), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | | FR-NC-2 | [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:298`](../core/dr-sync/src/account.rs#L298), [`core/dr-sync/src/account.rs:355`](../core/dr-sync/src/account.rs#L355), [`core/dr-sync/src/account.rs:59`](../core/dr-sync/src/account.rs#L59), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) | -| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:85`](../core/dr-sync/src/capability.rs#L85), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2848`](../ui/dr-ui/src/library.rs#L2848), [`ui/dr-ui/src/library.rs:3356`](../ui/dr-ui/src/library.rs#L3356), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/src/library_ui.rs:4592`](../ui/dr-ui/src/library_ui.rs#L4592), [`ui/dr-ui/ui/app.slint:348`](../ui/dr-ui/ui/app.slint#L348), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | +| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:85`](../core/dr-sync/src/capability.rs#L85), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2848`](../ui/dr-ui/src/library.rs#L2848), [`ui/dr-ui/src/library.rs:3356`](../ui/dr-ui/src/library.rs#L3356), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/src/library_ui.rs:4592`](../ui/dr-ui/src/library_ui.rs#L4592), [`ui/dr-ui/ui/app.slint:365`](../ui/dr-ui/ui/app.slint#L365), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | | FR-NC-4 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-nextcloud/src/propfind.rs:103`](../core/dr-sync-nextcloud/src/propfind.rs#L103), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61) | | FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488) | | FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) | -| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1014`](../core/dr-catalog/src/schema.rs#L1014), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3174`](../ui/dr-ui/src/collections_ui.rs#L3174), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1860`](../ui/dr-ui/src/lib.rs#L1860), [`ui/dr-ui/src/lib.rs:2910`](../ui/dr-ui/src/lib.rs#L2910), [`ui/dr-ui/src/library.rs:1449`](../ui/dr-ui/src/library.rs#L1449), [`ui/dr-ui/src/library.rs:1472`](../ui/dr-ui/src/library.rs#L1472), [`ui/dr-ui/src/library.rs:1747`](../ui/dr-ui/src/library.rs#L1747), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1429`](../ui/dr-ui/src/library_ui.rs#L1429), [`ui/dr-ui/src/library_ui.rs:1547`](../ui/dr-ui/src/library_ui.rs#L1547), [`ui/dr-ui/src/library_ui.rs:2074`](../ui/dr-ui/src/library_ui.rs#L2074), [`ui/dr-ui/src/library_ui.rs:273`](../ui/dr-ui/src/library_ui.rs#L273), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:292`](../ui/dr-ui/src/library_ui.rs#L292), [`ui/dr-ui/src/library_ui.rs:304`](../ui/dr-ui/src/library_ui.rs#L304), [`ui/dr-ui/src/library_ui.rs:313`](../ui/dr-ui/src/library_ui.rs#L313), [`ui/dr-ui/src/library_ui.rs:405`](../ui/dr-ui/src/library_ui.rs#L405), [`ui/dr-ui/src/library_ui.rs:415`](../ui/dr-ui/src/library_ui.rs#L415), [`ui/dr-ui/src/library_ui.rs:457`](../ui/dr-ui/src/library_ui.rs#L457), [`ui/dr-ui/src/library_ui.rs:520`](../ui/dr-ui/src/library_ui.rs#L520), [`ui/dr-ui/src/library_ui.rs:5318`](../ui/dr-ui/src/library_ui.rs#L5318), [`ui/dr-ui/src/library_ui.rs:5336`](../ui/dr-ui/src/library_ui.rs#L5336), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/src/library_ui.rs:551`](../ui/dr-ui/src/library_ui.rs#L551), [`ui/dr-ui/src/library_ui.rs:563`](../ui/dr-ui/src/library_ui.rs#L563), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2456`](../ui/dr-ui/ui/app.slint#L2456), [`ui/dr-ui/ui/app.slint:463`](../ui/dr-ui/ui/app.slint#L463), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:997`](../ui/dr-ui/ui/library.slint#L997) | +| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1014`](../core/dr-catalog/src/schema.rs#L1014), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3174`](../ui/dr-ui/src/collections_ui.rs#L3174), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1923`](../ui/dr-ui/src/lib.rs#L1923), [`ui/dr-ui/src/lib.rs:3037`](../ui/dr-ui/src/lib.rs#L3037), [`ui/dr-ui/src/library.rs:1449`](../ui/dr-ui/src/library.rs#L1449), [`ui/dr-ui/src/library.rs:1472`](../ui/dr-ui/src/library.rs#L1472), [`ui/dr-ui/src/library.rs:1747`](../ui/dr-ui/src/library.rs#L1747), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1429`](../ui/dr-ui/src/library_ui.rs#L1429), [`ui/dr-ui/src/library_ui.rs:1547`](../ui/dr-ui/src/library_ui.rs#L1547), [`ui/dr-ui/src/library_ui.rs:2074`](../ui/dr-ui/src/library_ui.rs#L2074), [`ui/dr-ui/src/library_ui.rs:273`](../ui/dr-ui/src/library_ui.rs#L273), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:292`](../ui/dr-ui/src/library_ui.rs#L292), [`ui/dr-ui/src/library_ui.rs:304`](../ui/dr-ui/src/library_ui.rs#L304), [`ui/dr-ui/src/library_ui.rs:313`](../ui/dr-ui/src/library_ui.rs#L313), [`ui/dr-ui/src/library_ui.rs:405`](../ui/dr-ui/src/library_ui.rs#L405), [`ui/dr-ui/src/library_ui.rs:415`](../ui/dr-ui/src/library_ui.rs#L415), [`ui/dr-ui/src/library_ui.rs:457`](../ui/dr-ui/src/library_ui.rs#L457), [`ui/dr-ui/src/library_ui.rs:520`](../ui/dr-ui/src/library_ui.rs#L520), [`ui/dr-ui/src/library_ui.rs:5318`](../ui/dr-ui/src/library_ui.rs#L5318), [`ui/dr-ui/src/library_ui.rs:5336`](../ui/dr-ui/src/library_ui.rs#L5336), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/src/library_ui.rs:551`](../ui/dr-ui/src/library_ui.rs#L551), [`ui/dr-ui/src/library_ui.rs:563`](../ui/dr-ui/src/library_ui.rs#L563), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2505`](../ui/dr-ui/ui/app.slint#L2505), [`ui/dr-ui/ui/app.slint:480`](../ui/dr-ui/ui/app.slint#L480), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:997`](../ui/dr-ui/ui/library.slint#L997) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294) | | FR-NC-6c | [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync-folder/src/lib.rs:803`](../core/dr-sync-folder/src/lib.rs#L803), [`core/dr-sync-folder/src/lib.rs:842`](../core/dr-sync-folder/src/lib.rs#L842), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:167`](../core/dr-sync-nextcloud/src/desktop_client.rs#L167), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-sync/src/error.rs:39`](../core/dr-sync/src/error.rs#L39), [`core/dr-sync/src/lib.rs:158`](../core/dr-sync/src/lib.rs#L158), [`core/dr-sync/src/types.rs:101`](../core/dr-sync/src/types.rs#L101), [`core/dr-types/src/lib.rs:120`](../core/dr-types/src/lib.rs#L120), [`core/dr-types/src/lib.rs:202`](../core/dr-types/src/lib.rs#L202), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3174`](../ui/dr-ui/src/collections_ui.rs#L3174), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/derived_sync.rs:555`](../ui/dr-ui/src/derived_sync.rs#L555), [`ui/dr-ui/src/derived_sync.rs:627`](../ui/dr-ui/src/derived_sync.rs#L627), [`ui/dr-ui/src/library.rs:1569`](../ui/dr-ui/src/library.rs#L1569), [`ui/dr-ui/src/library.rs:1659`](../ui/dr-ui/src/library.rs#L1659), [`ui/dr-ui/src/library.rs:3138`](../ui/dr-ui/src/library.rs#L3138), [`ui/dr-ui/src/library.rs:3476`](../ui/dr-ui/src/library.rs#L3476), [`ui/dr-ui/src/library.rs:948`](../ui/dr-ui/src/library.rs#L948), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1337`](../ui/dr-ui/src/library_ui.rs#L1337), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) | -| FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:104`](../core/dr-sync-nextcloud/src/lib.rs#L104), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3356`](../ui/dr-ui/src/library.rs#L3356), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387) | -| FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1176`](../ui/dr-ui/src/lib.rs#L1176), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | -| FR-NC-7b | [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`ui/dr-ui/src/import.rs:122`](../ui/dr-ui/src/import.rs#L122), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import.rs:584`](../ui/dr-ui/src/import.rs#L584), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1176`](../ui/dr-ui/src/lib.rs#L1176) | -| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1830`](../ui/dr-ui/src/lib.rs#L1830), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library_ui.rs:472`](../ui/dr-ui/src/library_ui.rs#L472) | -| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/derived_sync.rs:555`](../ui/dr-ui/src/derived_sync.rs#L555), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library.rs:998`](../ui/dr-ui/src/library.rs#L998) | +| FR-NC-6d | [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1) | +| FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:105`](../core/dr-sync-nextcloud/src/lib.rs#L105), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3356`](../ui/dr-ui/src/library.rs#L3356), [`ui/dr-ui/src/library_ui.rs:3627`](../ui/dr-ui/src/library_ui.rs#L3627), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387) | +| FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1183`](../ui/dr-ui/src/lib.rs#L1183), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | +| FR-NC-7b | [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`ui/dr-ui/src/import.rs:122`](../ui/dr-ui/src/import.rs#L122), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import.rs:584`](../ui/dr-ui/src/import.rs#L584), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1183`](../ui/dr-ui/src/lib.rs#L1183) | +| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:119`](../core/dr-pipeline/src/sidecar.rs#L119), [`core/dr-pipeline/src/sidecar.rs:93`](../core/dr-pipeline/src/sidecar.rs#L93), [`ui/dr-ui/src/lib.rs:1893`](../ui/dr-ui/src/lib.rs#L1893), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library_ui.rs:472`](../ui/dr-ui/src/library_ui.rs#L472) | +| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:157`](../core/dr-pipeline/src/sidecar.rs#L157), [`core/dr-pipeline/src/sidecar.rs:184`](../core/dr-pipeline/src/sidecar.rs#L184), [`core/dr-pipeline/src/sidecar.rs:2034`](../core/dr-pipeline/src/sidecar.rs#L2034), [`core/dr-pipeline/src/sidecar.rs:353`](../core/dr-pipeline/src/sidecar.rs#L353), [`core/dr-pipeline/src/sidecar.rs:451`](../core/dr-pipeline/src/sidecar.rs#L451), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/derived_sync.rs:555`](../ui/dr-ui/src/derived_sync.rs#L555), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library.rs:998`](../ui/dr-ui/src/library.rs#L998) | | FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:54`](../core/dr-types/src/lib.rs#L54), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) | -| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:919`](../ui/dr-ui/src/lib.rs#L919), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | +| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:926`](../ui/dr-ui/src/lib.rs#L926), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-PLAT-LIN-2 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1) | | FR-PLG-2 | [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/expr.rs:152`](../core/dr-pipeline/src/declared/expr.rs#L152), [`core/dr-pipeline/src/declared/expr.rs:1`](../core/dr-pipeline/src/declared/expr.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:82`](../core/dr-pipeline/src/declared/mod.rs#L82), [`core/dr-pipeline/src/descriptor.rs:15`](../core/dr-pipeline/src/descriptor.rs#L15), [`core/dr-pipeline/src/descriptor.rs:652`](../core/dr-pipeline/src/descriptor.rs#L652), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/tests/declared_parity.rs:1`](../core/dr-pipeline/tests/declared_parity.rs#L1), [`core/dr-pipeline/tests/declared_parity.rs:240`](../core/dr-pipeline/tests/declared_parity.rs#L240), [`core/dr-pipeline/tests/declared_parity.rs:305`](../core/dr-pipeline/tests/declared_parity.rs#L305), [`core/dr-pipeline/tests/declared_parity.rs:358`](../core/dr-pipeline/tests/declared_parity.rs#L358), [`core/dr-pipeline/tests/declared_parity.rs:416`](../core/dr-pipeline/tests/declared_parity.rs#L416) | | FR-PLG-2d | [`core/dr-pipeline/src/declared/decl.rs:112`](../core/dr-pipeline/src/declared/decl.rs#L112), [`core/dr-pipeline/src/declared/decl.rs:152`](../core/dr-pipeline/src/declared/decl.rs#L152), [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/decl.rs:420`](../core/dr-pipeline/src/declared/decl.rs#L420), [`core/dr-pipeline/src/declared/decl.rs:67`](../core/dr-pipeline/src/declared/decl.rs#L67), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:384`](../core/dr-pipeline/src/declared/mod.rs#L384), [`core/dr-pipeline/src/declared/mod.rs:403`](../core/dr-pipeline/src/declared/mod.rs#L403) | | FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:130`](../core/dr-types/src/lib.rs#L130), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201) | | FR-RAW-3 | [`core/dr-decode/src/lib.rs:139`](../core/dr-decode/src/lib.rs#L139), [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-decode/src/locate.rs:1366`](../core/dr-decode/src/locate.rs#L1366) | -| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:205`](../ui/dr-ui/src/lib.rs#L205) | +| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:206`](../ui/dr-ui/src/lib.rs#L206) | | FR-RAW-5 | [`core/dr-decode/src/lib.rs:167`](../core/dr-decode/src/lib.rs#L167), [`core/dr-gpu/src/demosaic.rs:34`](../core/dr-gpu/src/demosaic.rs#L34), [`core/dr-gpu/src/demosaic.rs:602`](../core/dr-gpu/src/demosaic.rs#L602), [`core/dr-gpu/src/demosaic.rs:681`](../core/dr-gpu/src/demosaic.rs#L681), [`core/dr-gpu/src/demosaic.rs:805`](../core/dr-gpu/src/demosaic.rs#L805) | -| FR-UI-1 | [`ui/dr-ui/src/lib.rs:2992`](../ui/dr-ui/src/lib.rs#L2992), [`ui/dr-ui/src/lib.rs:81`](../ui/dr-ui/src/lib.rs#L81), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/identity.slint:164`](../ui/dr-ui/ui/identity.slint#L164), [`ui/dr-ui/ui/library.slint:1063`](../ui/dr-ui/ui/library.slint#L1063) | -| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1488`](../ui/dr-ui/src/collections_ui.rs#L1488), [`ui/dr-ui/src/collections_ui.rs:1502`](../ui/dr-ui/src/collections_ui.rs#L1502), [`ui/dr-ui/src/collections_ui.rs:1548`](../ui/dr-ui/src/collections_ui.rs#L1548), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1646`](../ui/dr-ui/src/collections_ui.rs#L1646), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:81`](../ui/dr-ui/src/lib.rs#L81), [`ui/dr-ui/src/lib.rs:88`](../ui/dr-ui/src/lib.rs#L88), [`ui/dr-ui/src/library_ui.rs:292`](../ui/dr-ui/src/library_ui.rs#L292), [`ui/dr-ui/src/library_ui.rs:5203`](../ui/dr-ui/src/library_ui.rs#L5203), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/src/library_ui.rs:6479`](../ui/dr-ui/src/library_ui.rs#L6479), [`ui/dr-ui/ui/adjust.slint:1072`](../ui/dr-ui/ui/adjust.slint#L1072), [`ui/dr-ui/ui/adjust.slint:544`](../ui/dr-ui/ui/adjust.slint#L544), [`ui/dr-ui/ui/adjust.slint:741`](../ui/dr-ui/ui/adjust.slint#L741), [`ui/dr-ui/ui/app.slint:2046`](../ui/dr-ui/ui/app.slint#L2046), [`ui/dr-ui/ui/app.slint:493`](../ui/dr-ui/ui/app.slint#L493), [`ui/dr-ui/ui/app.slint:500`](../ui/dr-ui/ui/app.slint#L500), [`ui/dr-ui/ui/app.slint:55`](../ui/dr-ui/ui/app.slint#L55), [`ui/dr-ui/ui/app.slint:843`](../ui/dr-ui/ui/app.slint#L843), [`ui/dr-ui/ui/controls.slint:474`](../ui/dr-ui/ui/controls.slint#L474), [`ui/dr-ui/ui/develop.slint:223`](../ui/dr-ui/ui/develop.slint#L223), [`ui/dr-ui/ui/histogram.slint:127`](../ui/dr-ui/ui/histogram.slint#L127), [`ui/dr-ui/ui/history.slint:118`](../ui/dr-ui/ui/history.slint#L118), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1226`](../ui/dr-ui/ui/library.slint#L1226), [`ui/dr-ui/ui/library.slint:1232`](../ui/dr-ui/ui/library.slint#L1232), [`ui/dr-ui/ui/library.slint:2335`](../ui/dr-ui/ui/library.slint#L2335), [`ui/dr-ui/ui/library.slint:829`](../ui/dr-ui/ui/library.slint#L829), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/masks.slint:304`](../ui/dr-ui/ui/masks.slint#L304), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:110`](../ui/dr-ui/ui/settings.slint#L110), [`ui/dr-ui/ui/spots.slint:88`](../ui/dr-ui/ui/spots.slint#L88) | -| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2200`](../ui/dr-ui/src/develop.rs#L2200), [`ui/dr-ui/src/develop.rs:2309`](../ui/dr-ui/src/develop.rs#L2309), [`ui/dr-ui/src/library_ui.rs:4387`](../ui/dr-ui/src/library_ui.rs#L4387), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:1862`](../ui/dr-ui/ui/app.slint#L1862), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) | -| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1488`](../ui/dr-ui/src/collections_ui.rs#L1488), [`ui/dr-ui/src/collections_ui.rs:1502`](../ui/dr-ui/src/collections_ui.rs#L1502), [`ui/dr-ui/src/collections_ui.rs:1548`](../ui/dr-ui/src/collections_ui.rs#L1548), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1646`](../ui/dr-ui/src/collections_ui.rs#L1646), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4387`](../ui/dr-ui/src/library_ui.rs#L4387), [`ui/dr-ui/src/library_ui.rs:4432`](../ui/dr-ui/src/library_ui.rs#L4432), [`ui/dr-ui/src/library_ui.rs:4535`](../ui/dr-ui/src/library_ui.rs#L4535), [`ui/dr-ui/src/library_ui.rs:4563`](../ui/dr-ui/src/library_ui.rs#L4563), [`ui/dr-ui/src/library_ui.rs:5336`](../ui/dr-ui/src/library_ui.rs#L5336), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/ui/app.slint:1702`](../ui/dr-ui/ui/app.slint#L1702), [`ui/dr-ui/ui/app.slint:469`](../ui/dr-ui/ui/app.slint#L469), [`ui/dr-ui/ui/app.slint:500`](../ui/dr-ui/ui/app.slint#L500), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1226`](../ui/dr-ui/ui/library.slint#L1226), [`ui/dr-ui/ui/library.slint:1232`](../ui/dr-ui/ui/library.slint#L1232), [`ui/dr-ui/ui/library.slint:2335`](../ui/dr-ui/ui/library.slint#L2335), [`ui/dr-ui/ui/library.slint:829`](../ui/dr-ui/ui/library.slint#L829), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) | -| FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2493`](../ui/dr-ui/src/lib.rs#L2493), [`ui/dr-ui/src/lib.rs:3031`](../ui/dr-ui/src/lib.rs#L3031), [`ui/dr-ui/src/lib.rs:3216`](../ui/dr-ui/src/lib.rs#L3216), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:90`](../ui/dr-ui/ui/app.slint#L90), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | +| FR-UI-1 | [`ui/dr-ui/src/lib.rs:3119`](../ui/dr-ui/src/lib.rs#L3119), [`ui/dr-ui/src/lib.rs:82`](../ui/dr-ui/src/lib.rs#L82), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/identity.slint:164`](../ui/dr-ui/ui/identity.slint#L164), [`ui/dr-ui/ui/library.slint:1063`](../ui/dr-ui/ui/library.slint#L1063) | +| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1488`](../ui/dr-ui/src/collections_ui.rs#L1488), [`ui/dr-ui/src/collections_ui.rs:1502`](../ui/dr-ui/src/collections_ui.rs#L1502), [`ui/dr-ui/src/collections_ui.rs:1548`](../ui/dr-ui/src/collections_ui.rs#L1548), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1646`](../ui/dr-ui/src/collections_ui.rs#L1646), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:82`](../ui/dr-ui/src/lib.rs#L82), [`ui/dr-ui/src/lib.rs:89`](../ui/dr-ui/src/lib.rs#L89), [`ui/dr-ui/src/library_ui.rs:292`](../ui/dr-ui/src/library_ui.rs#L292), [`ui/dr-ui/src/library_ui.rs:5203`](../ui/dr-ui/src/library_ui.rs#L5203), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/src/library_ui.rs:6479`](../ui/dr-ui/src/library_ui.rs#L6479), [`ui/dr-ui/ui/adjust.slint:1072`](../ui/dr-ui/ui/adjust.slint#L1072), [`ui/dr-ui/ui/adjust.slint:544`](../ui/dr-ui/ui/adjust.slint#L544), [`ui/dr-ui/ui/adjust.slint:741`](../ui/dr-ui/ui/adjust.slint#L741), [`ui/dr-ui/ui/app.slint:2075`](../ui/dr-ui/ui/app.slint#L2075), [`ui/dr-ui/ui/app.slint:510`](../ui/dr-ui/ui/app.slint#L510), [`ui/dr-ui/ui/app.slint:517`](../ui/dr-ui/ui/app.slint#L517), [`ui/dr-ui/ui/app.slint:56`](../ui/dr-ui/ui/app.slint#L56), [`ui/dr-ui/ui/app.slint:860`](../ui/dr-ui/ui/app.slint#L860), [`ui/dr-ui/ui/controls.slint:474`](../ui/dr-ui/ui/controls.slint#L474), [`ui/dr-ui/ui/develop.slint:223`](../ui/dr-ui/ui/develop.slint#L223), [`ui/dr-ui/ui/histogram.slint:127`](../ui/dr-ui/ui/histogram.slint#L127), [`ui/dr-ui/ui/history.slint:118`](../ui/dr-ui/ui/history.slint#L118), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1226`](../ui/dr-ui/ui/library.slint#L1226), [`ui/dr-ui/ui/library.slint:1232`](../ui/dr-ui/ui/library.slint#L1232), [`ui/dr-ui/ui/library.slint:2335`](../ui/dr-ui/ui/library.slint#L2335), [`ui/dr-ui/ui/library.slint:829`](../ui/dr-ui/ui/library.slint#L829), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/masks.slint:314`](../ui/dr-ui/ui/masks.slint#L314), [`ui/dr-ui/ui/peaking.slint:82`](../ui/dr-ui/ui/peaking.slint#L82), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:110`](../ui/dr-ui/ui/settings.slint#L110), [`ui/dr-ui/ui/spots.slint:88`](../ui/dr-ui/ui/spots.slint#L88) | +| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2224`](../ui/dr-ui/src/develop.rs#L2224), [`ui/dr-ui/src/develop.rs:2333`](../ui/dr-ui/src/develop.rs#L2333), [`ui/dr-ui/src/library_ui.rs:4387`](../ui/dr-ui/src/library_ui.rs#L4387), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:1891`](../ui/dr-ui/ui/app.slint#L1891), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/masks.slint:524`](../ui/dr-ui/ui/masks.slint#L524) | +| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1488`](../ui/dr-ui/src/collections_ui.rs#L1488), [`ui/dr-ui/src/collections_ui.rs:1502`](../ui/dr-ui/src/collections_ui.rs#L1502), [`ui/dr-ui/src/collections_ui.rs:1548`](../ui/dr-ui/src/collections_ui.rs#L1548), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1646`](../ui/dr-ui/src/collections_ui.rs#L1646), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4387`](../ui/dr-ui/src/library_ui.rs#L4387), [`ui/dr-ui/src/library_ui.rs:4432`](../ui/dr-ui/src/library_ui.rs#L4432), [`ui/dr-ui/src/library_ui.rs:4535`](../ui/dr-ui/src/library_ui.rs#L4535), [`ui/dr-ui/src/library_ui.rs:4563`](../ui/dr-ui/src/library_ui.rs#L4563), [`ui/dr-ui/src/library_ui.rs:5336`](../ui/dr-ui/src/library_ui.rs#L5336), [`ui/dr-ui/src/library_ui.rs:5348`](../ui/dr-ui/src/library_ui.rs#L5348), [`ui/dr-ui/ui/app.slint:1731`](../ui/dr-ui/ui/app.slint#L1731), [`ui/dr-ui/ui/app.slint:486`](../ui/dr-ui/ui/app.slint#L486), [`ui/dr-ui/ui/app.slint:517`](../ui/dr-ui/ui/app.slint#L517), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1226`](../ui/dr-ui/ui/library.slint#L1226), [`ui/dr-ui/ui/library.slint:1232`](../ui/dr-ui/ui/library.slint#L1232), [`ui/dr-ui/ui/library.slint:2335`](../ui/dr-ui/ui/library.slint#L2335), [`ui/dr-ui/ui/library.slint:829`](../ui/dr-ui/ui/library.slint#L829), [`ui/dr-ui/ui/library.slint:882`](../ui/dr-ui/ui/library.slint#L882), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) | +| FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2556`](../ui/dr-ui/src/lib.rs#L2556), [`ui/dr-ui/src/lib.rs:3158`](../ui/dr-ui/src/lib.rs#L3158), [`ui/dr-ui/src/lib.rs:3343`](../ui/dr-ui/src/lib.rs#L3343), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:107`](../ui/dr-ui/ui/app.slint#L107), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | +| FR-UI-6 | [`ui/dr-ui/ui/widgets.slint:1`](../ui/dr-ui/ui/widgets.slint#L1) | | FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:385`](../core/dr-pipeline/src/framing.rs#L385) | | NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:2953`](../ui/dr-ui/src/library.rs#L2953) | -| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1553`](../ui/dr-ui/src/export.rs#L1553), [`ui/dr-ui/src/export.rs:1579`](../ui/dr-ui/src/export.rs#L1579), [`ui/dr-ui/src/export.rs:409`](../ui/dr-ui/src/export.rs#L409), [`ui/dr-ui/src/export.rs:435`](../ui/dr-ui/src/export.rs#L435), [`ui/dr-ui/src/lib.rs:2190`](../ui/dr-ui/src/lib.rs#L2190), [`ui/dr-ui/ui/app.slint:1019`](../ui/dr-ui/ui/app.slint#L1019), [`ui/dr-ui/ui/library.slint:951`](../ui/dr-ui/ui/library.slint#L951) | +| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1553`](../ui/dr-ui/src/export.rs#L1553), [`ui/dr-ui/src/export.rs:1579`](../ui/dr-ui/src/export.rs#L1579), [`ui/dr-ui/src/export.rs:409`](../ui/dr-ui/src/export.rs#L409), [`ui/dr-ui/src/export.rs:435`](../ui/dr-ui/src/export.rs#L435), [`ui/dr-ui/src/lib.rs:2253`](../ui/dr-ui/src/lib.rs#L2253), [`ui/dr-ui/ui/app.slint:1036`](../ui/dr-ui/ui/app.slint#L1036), [`ui/dr-ui/ui/library.slint:951`](../ui/dr-ui/ui/library.slint#L951) | | NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`platform/dr-plat/src/storage.rs:148`](../platform/dr-plat/src/storage.rs#L148), [`ui/dr-ui/src/export.rs:499`](../ui/dr-ui/src/export.rs#L499) | | NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) | +| NFR-OPS-3 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) | | NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) | +| NFR-P14 | [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`ui/dr-ui/src/develop.rs:2964`](../ui/dr-ui/src/develop.rs#L2964), [`ui/dr-ui/src/lib.rs:1626`](../ui/dr-ui/src/lib.rs#L1626) | | NFR-P5 | [`core/dr-catalog/src/schema.rs:318`](../core/dr-catalog/src/schema.rs#L318), [`ui/dr-ui/src/library.rs:4003`](../ui/dr-ui/src/library.rs#L4003), [`ui/dr-ui/src/library.rs:4795`](../ui/dr-ui/src/library.rs#L4795), [`ui/dr-ui/src/library_ui.rs:4072`](../ui/dr-ui/src/library_ui.rs#L4072), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) | | NFR-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) | | NFR-PORT-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:270`](../core/dr-types/src/lib.rs#L270), [`core/dr-types/src/lib.rs:303`](../core/dr-types/src/lib.rs#L303), [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) | +| NFR-PORT-2 | [`core/dr-gpu/src/lib.rs:1`](../core/dr-gpu/src/lib.rs#L1) | | NFR-PORT-3 | [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1) | -| NFR-R1 | [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-gpu/src/lib.rs:176`](../core/dr-gpu/src/lib.rs#L176), [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/library.rs:1068`](../ui/dr-ui/src/library.rs#L1068) | +| NFR-R1 | [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-gpu/src/lib.rs:179`](../core/dr-gpu/src/lib.rs#L179), [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/library.rs:1068`](../ui/dr-ui/src/library.rs#L1068) | | NFR-R2 | [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1) | | NFR-R5 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1) | | NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) | | NFR-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) | -| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/lib.rs:73`](../ui/dr-ui/src/lib.rs#L73) | -| NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-gpu/src/lib.rs:77`](../core/dr-gpu/src/lib.rs#L77), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/library.rs:2848`](../ui/dr-ui/src/library.rs#L2848) | +| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/lib.rs:74`](../ui/dr-ui/src/lib.rs#L74) | +| NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-gpu/src/lib.rs:80`](../core/dr-gpu/src/lib.rs#L80), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/library.rs:2848`](../ui/dr-ui/src/library.rs#L2848) | | NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) | | NFR-SEC-2 | [`core/dr-sync/src/account.rs:298`](../core/dr-sync/src/account.rs#L298), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) | +| NFR-SEC-3 | [`core/dr-sync-nextcloud/src/lib.rs:1`](../core/dr-sync-nextcloud/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1) | | NFR-SEC-5 | [`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), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | R1 | [`tools/traceability/src/lib.rs:495`](../tools/traceability/src/lib.rs#L495), [`tools/traceability/src/lib.rs:499`](../tools/traceability/src/lib.rs#L499) | -| R4 | [`core/dr-gpu/src/lib.rs:335`](../core/dr-gpu/src/lib.rs#L335) | +| R3 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-pipeline/src/lib.rs:1`](../core/dr-pipeline/src/lib.rs#L1) | +| R4 | [`core/dr-gpu/src/lib.rs:338`](../core/dr-gpu/src/lib.rs#L338) | +| R6 | [`core/dr-sync-nextcloud/src/lib.rs:1`](../core/dr-sync-nextcloud/src/lib.rs#L1) | ## Not yet tagged -72 of 179 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built. +62 of 179 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
Show untagged requirements - FR-CAT-14 -- FR-CULL-3 - FR-CULL-5 - FR-CULL-6 - FR-CULL-7 -- FR-DEV-1 - FR-DEV-3g - FR-DSP-2 - FR-DSP-4 - FR-NC-11 -- FR-NC-6d - FR-PLAT-AND-2 - FR-PLAT-AND-4 - FR-PLAT-AND-5 @@ -185,7 +192,6 @@ _None._ - FR-PLG-8 - FR-PLG-9 - FR-RAW-2 -- FR-UI-6 - NFR-A11Y-1 - NFR-A11Y-2 - NFR-A11Y-3 @@ -193,12 +199,10 @@ _None._ - NFR-COMPAT-1 - NFR-COMPAT-2 - NFR-OPS-2 -- NFR-OPS-3 - NFR-OPS-4 - NFR-P10 - NFR-P11 - NFR-P12 -- NFR-P14 - NFR-P15 - NFR-P2 - NFR-P3 @@ -206,18 +210,14 @@ _None._ - NFR-P6 - NFR-P7 - NFR-P8 -- NFR-PORT-2 - NFR-R3 - NFR-R4 - NFR-R6 - NFR-RES-2 - NFR-RES-3 -- NFR-SEC-3 - NFR-SEC-4 - NFR-SEC-6 - R2 -- R3 - R5 -- R6
diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD index b75f03d..f5fde21 100644 --- a/packaging/PKGBUILD +++ b/packaging/PKGBUILD @@ -37,6 +37,14 @@ package() { install -Dm644 "packaging/paris.tourolle.darkroom.desktop" \ "${pkgdir}/usr/share/applications/paris.tourolle.darkroom.desktop" + # The same AppStream file the Flatpak installs, so a software centre + # describes the two packages identically instead of falling back to the + # desktop entry's one-line Comment for this one. Installed here rather than + # written twice: the description, the licence fields and the OARS rating + # are facts about the application, not about how it was packaged. + install -Dm644 "packaging/paris.tourolle.darkroom.metainfo.xml" \ + "${pkgdir}/usr/share/metainfo/paris.tourolle.darkroom.metainfo.xml" + # The icon's *name* is the contract, not its path: the desktop entry says # `Icon=paris.tourolle.darkroom` and the compositor resolves that through # the hicolor theme. Installed under 256x256 because that is the source's diff --git a/packaging/flatpak/paris.tourolle.darkroom.yml b/packaging/flatpak/paris.tourolle.darkroom.yml new file mode 100644 index 0000000..c6b9c27 --- /dev/null +++ b/packaging/flatpak/paris.tourolle.darkroom.yml @@ -0,0 +1,186 @@ +# Flatpak manifest — FR-PLAT-LIN-3 (sandboxed distribution), NFR-COMPAT-2. +# +# YAML rather than JSON because this file has more to explain than to declare, +# and JSON cannot hold a comment. flatpak-builder reads both. +# +# Build it, from the repository root: +# +# flatpak-builder --user --install --force-clean \ +# build/flatpak packaging/flatpak/paris.tourolle.darkroom.yml +# +# Read docs/distribution.md before changing any permission below. Every line in +# `finish-args` is a hole in the sandbox, and the one that is conspicuously +# absent — `--filesystem=` — is absent on purpose and is explained there. +id: paris.tourolle.darkroom + +# 25.08 is the current freedesktop runtime, and the choice is made by what the +# binary needs rather than by what is newest. `ldd` on a release build names +# fontconfig, freetype, expat, libpng, zlib, brotli and bzip2 — all in the +# Platform — and nothing else: Vulkan, libxkbcommon and the two display-server +# protocols are reached without a link-time dependency (wgpu dlopens +# libvulkan.so.1, and x11rb and wayland-client speak the wire protocols in Rust +# rather than binding libxcb or libwayland). So the runtime has to supply a +# Vulkan loader and an ICD at *runtime*, which is the GL extension's job, and +# not much else. +runtime: org.freedesktop.Platform +runtime-version: '25.08' +sdk: org.freedesktop.Sdk + +# The Rust toolchain is an SDK extension rather than something this manifest +# installs, so the build is offline-capable in the part that matters and the +# compiler is the one freedesktop tested against its own glibc. +# +# Note what this quietly overrides: `rust-toolchain.toml` pins 1.92.0, and that +# pin is honoured by *rustup*, which is not what the extension provides. The +# extension's cargo therefore ignores the file and builds with its own stable +# (1.98.0 on 25.08). That is fine here and deliberately different from +# CONTRIBUTING.md's "do not override the toolchain": the pin exists so `cargo +# fmt --check` and `clippy -D warnings` agree between a laptop and CI, and +# neither runs in this build. A *release binary* only needs a compiler at or +# above the workspace's `rust-version`. +sdk-extensions: + - org.freedesktop.Sdk.Extension.rust-stable + +command: darkroom-desktop + +finish-args: + # FR-PLAT-LIN-2 asks for both display servers. `fallback-x11` rather than + # `x11`: it grants the X socket only when Wayland is unavailable, so a + # Wayland session does not leave an X11 hole open beside the socket actually + # in use. `--share=ipc` goes with it — without it X11 cannot use shared + # memory and every frame is pushed through the socket instead. + - --socket=wayland + - --socket=fallback-x11 + - --share=ipc + + # The GPU. The develop pipeline is compute shaders through wgpu and there is + # no CPU renderer behind it, so this is not an optimisation: without + # /dev/dri the application starts and cannot develop anything. + # + # `dri` rather than `all`: it covers the render nodes and the NVIDIA device + # nodes, which is the whole of what a Vulkan ICD opens. `all` would add every + # other device on the machine for no gain. + - --device=dri + + # Nextcloud (FR-NC-*). Nothing else here reaches the network — face grouping, + # segmentation and lens correction are all local and stay local (NFR-SEC-5). + - --share=network + + # Credential storage (FR-NC-2). The keyring crate speaks the Secret Service + # D-Bus interface directly, which GNOME Keyring and KWallet's `ksecretd` both + # implement, so what it needs is a talk hole to that well-known name. + # + # Worth being precise, because FR-PLAT-LIN-3 says "Secret Service portal" and + # these are two different things: xdg-desktop-portal's `org.freedesktop. + # portal.Secret` hands an application a master key for a store it keeps + # itself, whereas this talks to the session's secret daemon. Only the latter + # puts the app password where `secret-tool` and Seahorse can see it, which is + # what makes a credential individually revocable by the user rather than + # opaque inside our own data directory. If no daemon answers, FR-NC-2's + # degraded mode is what the user gets — the same behaviour as outside a + # sandbox, which is the point. + - --talk-name=org.freedesktop.secrets + + # No `--filesystem=` line of any kind, and this is the substance of + # FR-PLAT-LIN-3 rather than an omission. + # + # What that leaves working: /run/user/$UID/doc is mounted in every sandbox, so + # a photograph opened from a file manager — the .desktop entry declares the + # RAW MIME types and `Exec=darkroom-desktop %F` — arrives as a document-portal + # path in argv and opens. That path is genuinely portal-mediated and needs no + # code change. + # + # What that leaves broken: choosing a *library root*. The folder connector + # takes a typed absolute path (`SignIn::EndpointOnly`, placeholder + # `/home/you/Pictures`) and checks it with `std::fs`, and nothing in the tree + # calls the FileChooser portal — there is no ashpd, no rfd, no toolkit dialog. + # A path typed into that field does not exist in this sandbox, so the launch + # screen refuses it with "that folder does not exist", which is at least an + # honest error. + # + # `--filesystem=host` would make that work today and is exactly what the + # requirement forbids, so it is not here. docs/distribution.md §4 records what + # closes the gap and how to run a Flatpak build in the meantime. + +modules: + - name: darkroom + buildsystem: simple + + build-options: + append-path: /usr/lib/sdk/rust-stable/bin + env: + # Inside the build sandbox rather than in $HOME, so a rebuild starts + # from the state flatpak-builder is managing and not from whatever the + # host's cargo cache happens to hold. + CARGO_HOME: /run/build/darkroom/cargo + # Cargo fetches 826 crates, and Flathub's builders forbid this — a + # submission there needs `cargo-sources.json` generated by + # flatpak-builder-tools' `flatpak-cargo-generator.py` from Cargo.lock, + # listing every crate as its own source, plus a vendored-registry + # `.cargo/config.toml`. That file is ~30k lines, has to be regenerated on + # every dependency change, and buys nothing for a build from a local + # checkout, which is what this manifest is for and what packaging/PKGBUILD + # is for as well. Add it when there is a Flathub submission, not before. + build-args: + - --share=network + + build-commands: + # `--locked` for the reason CI uses it: a lockfile that resolves + # differently in the packaging build than in the tree is a release whose + # dependency versions nobody chose. + - cargo build --release --locked -p darkroom-desktop + + - install -Dm755 target/release/darkroom-desktop /app/bin/darkroom-desktop + + - install -Dm644 packaging/paris.tourolle.darkroom.desktop + /app/share/applications/paris.tourolle.darkroom.desktop + + - install -Dm644 packaging/paris.tourolle.darkroom.metainfo.xml + /app/share/metainfo/paris.tourolle.darkroom.metainfo.xml + + # The icon's name is the contract, not its path — the desktop entry says + # `Icon=paris.tourolle.darkroom` and the shell resolves that through the + # hicolor theme. 256x256 because that is the source's actual size; + # installing it under a size it is not makes scaled icons look wrong. + - install -Dm644 ui/dr-ui/ui/app-icon.png + /app/share/icons/hicolor/256x256/apps/paris.tourolle.darkroom.png + + # The face models, where `system_face_models_dirs()` looks: it reads + # $XDG_DATA_DIRS, which includes /app/share inside a Flatpak, so this is + # the same lookup that finds /usr/share/darkroom/models from the Arch + # package. Last in the search order, so a pair the user dropped in their + # own data directory still outranks these. + # + # These live in Git LFS. A checkout made without `git lfs pull` has + # ~130-byte pointers here, and `type: dir` below would copy the pointers + # in without complaint — producing a Flatpak whose face indexing fails + # inside the graph loader on the user's machine. Refuse instead, with the + # command that fixes it. + - | + for m in scrfd_500m_640.onnx arcface_mbf_b1.onnx; do + if [ "$(stat -c%s "models/face/$m")" -lt 100000 ]; then + echo "error: $m is an LFS pointer, not a model — run: git lfs pull" >&2 + exit 1 + fi + install -Dm644 "models/face/$m" "/app/share/darkroom/models/$m" + done + + - install -Dm644 README.md /app/share/doc/darkroom/README.md + + sources: + # The local checkout, for the same reason packaging/PKGBUILD builds from + # one: this makes a Flatpak of what you are actually working on. Swap it + # for an `archive` or `git` source with a tag when there is a release to + # point at. + # + # `skip` is not tidiness. `target/` is tens of gigabytes and `.git` with + # LFS objects is not small; flatpak-builder copies a `dir` source + # wholesale, so without these two lines the copy is the slowest part of + # the build by a wide margin. + - type: dir + path: ../.. + skip: + - target + - target-android + - .git + - build diff --git a/packaging/paris.tourolle.darkroom.metainfo.xml b/packaging/paris.tourolle.darkroom.metainfo.xml new file mode 100644 index 0000000..864c662 --- /dev/null +++ b/packaging/paris.tourolle.darkroom.metainfo.xml @@ -0,0 +1,97 @@ + + + + + paris.tourolle.darkroom + + + CC0-1.0 + GPL-3.0-or-later + + DarkRoom + Non-destructive RAW photo library and editor + + +

+ DarkRoom catalogues, culls and develops RAW photographs. Edits are stored + as a graph of operations beside the original rather than baked into it, + so every change stays reversible and the file the camera wrote is never + rewritten. +

+

+ The library can live in a plain directory — a local disk, an external + drive, an NFS or SMB mount — or on a Nextcloud server, browsed and edited + without downloading whole RAW files first. +

+

Where it differs from the tools it sits beside:

+
    +
  • Culling shows the camera's embedded preview immediately and replaces it with a full render when one is ready, so moving to the next frame does not wait on a demosaic
  • +
  • Sidecars are the record of an edit; the catalog is a cache that can be deleted and rebuilt
  • +
  • The develop pipeline runs on the GPU through Vulkan, including drawn masks — a working Vulkan driver is required, not merely preferred, because there is no CPU renderer behind it
  • +
  • Faces are detected and grouped locally — nothing is uploaded to identify anybody
  • +
+
+ + paris.tourolle.darkroom.desktop + + darkroom-desktop + + + https://gitea.tourolle.paris/dtourolle/DarkRoom + https://gitea.tourolle.paris/dtourolle/DarkRoom/issues + https://gitea.tourolle.paris/dtourolle/DarkRoom + + + Duncan Tourolle + + + + Graphics + Photography + + + + RAW + photography + develop + darkroom + catalog + + + + + 768 + + + pointing + keyboard + touch + + + + + + + +
diff --git a/ui/dr-ui/src/develop.rs b/ui/dr-ui/src/develop.rs index 199dac9..d900dba 100644 --- a/ui/dr-ui/src/develop.rs +++ b/ui/dr-ui/src/develop.rs @@ -14,7 +14,8 @@ use std::sync::Arc; use dr_decode::RawImage; use dr_gpu::{ - AdjustPass, DemosaicedImage, Demosaicer, GpuContext, Histogram, HistogramPass, MaskPass, + AdjustPass, DemosaicedImage, Demosaicer, FocusPeakPass, FocusPeaking, GpuContext, Histogram, + HistogramPass, MaskPass, }; use dr_pipeline::mask::{MaskLayer, MaskSource}; @@ -722,6 +723,25 @@ pub struct DevelopSession { /// old driver, a device without the storage-buffer atomics it needs — the /// photographer loses the histogram and keeps the photograph. histogram: Option, + /// TRACES: FR-CULL-3 + /// The focus-peaking overlay, on the same terms as the histogram above: + /// optional, because a device that cannot compile the pass is still a + /// device that can develop the photograph. What is lost is an instrument, + /// not the picture. + peak: Option, + /// TRACES: FR-CULL-3 + /// What the photographer asked the overlay to look like, or `None` for + /// off. + /// + /// **Interface state, not part of the edit** — the same category as + /// `show_overlay` beside it. It changes no pixel of the photograph, it is + /// not in the sidecar, and it is not on the undo stack: pressing undo + /// after switching peaking on should take back the last *edit*, not the + /// last thing looked at. + /// + /// An `Option` rather than a bool plus a settings field, so that "off" and + /// "on, in some configuration" cannot disagree with each other. + peaking: Option, /// TRACES: FR-DEV-3 /// The region map local masks select from, once it has been computed. @@ -889,6 +909,10 @@ impl DevelopSession { histogram: HistogramPass::new(ctx) .inspect_err(|e| log::warn!("no histogram on this device: {e}")) .ok(), + peak: FocusPeakPass::new(ctx) + .inspect_err(|e| log::warn!("no focus peaking on this device: {e}")) + .ok(), + peaking: None, segmentation: None, masks: None, subjects: None, @@ -2903,6 +2927,105 @@ impl DevelopSession { .ok() } + /// TRACES: FR-CULL-3 + /// Whether this device could build the focus-peaking overlay. + /// + /// Asked by the interface so that it can say the overlay is unavailable + /// rather than offer a switch that does nothing. The same courtesy the + /// histogram is not paid, and should be: a control that silently does + /// nothing is worse than one that is visibly absent. + pub fn peaking_available(&self) -> bool { + self.peak.is_some() + } + + /// TRACES: FR-CULL-3 + /// What the overlay is set to, or `None` when it is off. + pub fn peaking(&self) -> Option { + self.peaking + } + + /// TRACES: FR-CULL-3 + /// Switch the overlay on with these settings, or off. + /// + /// Asking for peaking on a device that could not build the pass leaves it + /// off, so that [`Self::peaking`] never claims something is being drawn + /// that is not. Switching off drops the overlay textures rather than + /// merely stopping drawing them: a resident overlay from the last frame is + /// one interface bug away from being laid over the next photograph. + pub fn set_peaking(&mut self, settings: Option) { + self.peaking = settings.filter(|_| self.peak.is_some()); + if self.peaking.is_none() { + if let Some(pass) = self.peak.as_mut() { + pass.clear(); + } + } + } + + /// TRACES: FR-CULL-3 | NFR-P14 + /// Mark the in-focus regions of the frame that is currently on the canvas. + /// + /// **Reads the frame [`Self::render`] last produced**, exactly as + /// [`Self::histogram`] does and for the same reason: the overlay has to + /// describe what the photographer is looking at, and rendering a second + /// time to measure it would cost a pass and admit the possibility of the + /// two disagreeing about the picture. + /// + /// That the frame is the displayed one is what makes the marks land where + /// the eye is. It is at viewport resolution, cropped and zoomed as the + /// view is, and — the point of FR-CULL-3 — descended from sensor data + /// through the demosaic rather than from the camera's embedded JPEG, whose + /// in-body sharpening this would otherwise be measuring at least as much + /// as the lens. + /// + /// **Call this only after a settled render.** See + /// [`dr_gpu::FocusPeakPass::render`] for why a half-resolution draft frame + /// cannot be measured for sharpness. + /// + /// `None` where nothing has been rendered, where peaking is off, or where + /// the device could not build the pass. + pub fn focus_overlay(&mut self) -> Option { + let settings = self.peaking?; + // Cloned rather than borrowed: a `wgpu::Texture` handle is an `Arc`, + // and holding a shared borrow of `self.adjust` across the mutable + // borrow of `self.peak` would cost a `Self { .. }` destructure to say + // something the clone says in one word. + let frame = self.adjust.output()?.clone(); + let pass = self.peak.as_mut()?; + let overlay = pass + .render(&frame, settings) + .inspect_err(|e| log::warn!("focus peaking failed: {e}")) + .ok()? + .clone(); + + #[cfg(not(target_os = "android"))] + { + // A layer over the canvas rather than a tint in it, so nothing + // here reaches the histogram or an export — see `FocusPeakPass` + // for the whole of that argument. + slint::Image::try_from(overlay) + .inspect_err(|e| log::warn!("the focus overlay is not importable: {e}")) + .ok() + } + + // Android draws with Skia over OpenGL and cannot sample a + // `wgpu::Texture`, so the overlay follows the frame it belongs to back + // through memory (technical-debt.md TD-1). The measurement still + // happens on the GPU; only this last hop does not. + #[cfg(target_os = "android")] + { + let _ = overlay; + let (rgba, w, h) = pass + .read_overlay() + .inspect_err(|e| log::warn!("reading the focus overlay back: {e}")) + .ok()?; + let mut buf = slint::SharedPixelBuffer::::new(w, h); + let wanted = (w as usize) * (h as usize) * 4; + let src = &rgba[..wanted.min(rgba.len())]; + buf.make_mut_bytes()[..src.len()].copy_from_slice(src); + Some(slint::Image::from_rgba8(buf)) + } + } + /// Render the *whole* frame for the crop overlay to be drawn over. /// /// Crop mode cannot use [`Self::render`]: that applies the crop, so the diff --git a/ui/dr-ui/src/lib.rs b/ui/dr-ui/src/lib.rs index 6b6b07e..cdb13f1 100644 --- a/ui/dr-ui/src/lib.rs +++ b/ui/dr-ui/src/lib.rs @@ -39,6 +39,7 @@ mod library_ui; mod live_style; mod masks_ui; mod net_runtime; +mod peaking; mod preset_store; mod presets; mod remote; @@ -317,6 +318,12 @@ fn reset_view_state(window: &AppWindow) { // beside the next one's filename is a confident, precise lie, and the gap // before the new frame settles is exactly long enough to read it. window.set_histogram(histogram::empty()); + // TRACES: FR-CULL-3 + // The marks go down with it, and for the same reason. What is *not* reset + // is whether peaking is switched on: that is a way of looking at a folder + // rather than a property of one photograph, so it survives to the next + // frame — see `chosen_peaking` for the whole of that argument. + window.set_focus_overlay_ready(false); // TRACES: FR-DEV-3 // The region map belongs to one photograph. Carrying the stack, the // overlay or the crosshair to the next one would offer a selection of @@ -1465,11 +1472,24 @@ pub fn run(paths: Vec) -> Result<()> { // is redrawn, and those are very different rates. let drawn_history: Rc>> = Rc::new(Cell::new(None)); + // TRACES: FR-CULL-3 + // How the photographer wants focus peaking drawn, or `None` for off. + // + // **Held here rather than on the session, which is the opposite of where + // every edit lives.** A session is one photograph; peaking is a way of + // *looking* at a folder of them. Someone culling three thousand frames + // switches it on once, and a flag that reset with the session would ask + // them to switch it on three thousand times — which is why + // `reset_view_state` deliberately leaves it alone while emptying the + // histogram beside it. + let chosen_peaking: Rc>> = Rc::new(Cell::new(None)); + let render_now: Render = { let session = session.clone(); let viewport = viewport.clone(); let drawn_history = drawn_history.clone(); let display = display.clone(); + let chosen_peaking = chosen_peaking.clone(); Rc::new(move |window: &AppWindow, draft: bool| { let mut slot = session.borrow_mut(); let Some(s) = slot.as_mut() else { return }; @@ -1530,6 +1550,16 @@ pub fn run(paths: Vec) -> Result<()> { // arrival takes. spots_ui::sync_panel(window, s); + // TRACES: FR-CULL-3 + // The session owns the pass and the interface owns the choice, so + // they are joined here — on the one path every frame takes, which + // is also what makes a photograph opened with peaking already on + // arrive with its marks rather than without them. + if s.peaking() != chosen_peaking.get() { + s.set_peaking(chosen_peaking.get()); + } + window.set_peaking_available(s.peaking_available()); + let (mut w, mut h) = *viewport.borrow(); // **Half resolution while the gesture is still moving.** @@ -1592,6 +1622,34 @@ pub fn run(paths: Vec) -> Result<()> { .map_or_else(histogram::empty, histogram::view), ); } + + // TRACES: FR-CULL-3 | NFR-P14 + // **Marked on the settled frame and no other**, and unlike + // the histogram beside it the marks are taken *down* in + // between rather than left standing. + // + // The reason is not budget — the dispatch is a fraction of + // a millisecond and would fit inside a draft frame + // comfortably. It is that peaking measures the top octave + // of the frame it is given, and a draft frame is rendered + // at half resolution: a defocused edge that spans four + // pixels there spans two, which is the signature of a + // sharp one. Measuring it would mark the out-of-focus + // background of every photograph, briefly, during every + // drag. A stale overlay is no better, because a pan moves + // the picture out from under it. + // + // So the marks pause while a control is moving and return + // when it stops, which the panel says out loud rather than + // leaving to be discovered. + let overlay = (!draft).then(|| s.focus_overlay()).flatten(); + match overlay { + Some(image) => { + window.set_focus_overlay(image); + window.set_focus_overlay_ready(true); + } + None => window.set_focus_overlay_ready(false), + } } Err(e) => { log::warn!("render failed: {e}"); @@ -1599,6 +1657,11 @@ pub fn run(paths: Vec) -> Result<()> { // No frame, so nothing to describe. The stale plot would // otherwise sit beside the error message looking current. window.set_histogram(histogram::empty()); + // TRACES: FR-CULL-3 + // And nothing to mark. Focus marks over the last frame + // that rendered, beside a message saying this one did not, + // is the same confident lie in a second instrument. + window.set_focus_overlay_ready(false); } } }) @@ -2844,6 +2907,70 @@ pub fn run(paths: Vec) -> Result<()> { }); } + // TRACES: FR-CULL-3 + // The peaking switch and its two choices. + // + // All three write `chosen_peaking` and then redraw, because the marks are + // produced by a compute pass over the rendered frame: there is nothing the + // interface can change about the overlay that does not require the frame + // to be measured again. Turning peaking *off* redraws for the same reason + // — that render is what drops the overlay textures and clears the flag. + { + let weak = window.as_weak(); + let chosen = chosen_peaking.clone(); + let redraw = redraw.clone(); + window.on_peaking_toggled(move |on| { + let Some(w) = weak.upgrade() else { return }; + // Built from the chips as they currently stand rather than from a + // remembered value: they are what the photographer can see, and an + // overlay that came back in a configuration the panel is not + // showing would be the panel lying about itself. + let next = on.then(|| dr_gpu::FocusPeaking { + sensitivity: peaking::sensitivity(w.get_peaking_sensitivity()), + colour: peaking::colour(w.get_peaking_colour()), + }); + chosen.set(next); + w.set_peaking_on(next.is_some()); + redraw(&w); + }); + } + { + let weak = window.as_weak(); + let chosen = chosen_peaking.clone(); + let redraw = redraw.clone(); + window.on_peaking_sensitivity_picked(move |index| { + let Some(w) = weak.upgrade() else { return }; + w.set_peaking_sensitivity(index); + // Only reachable while peaking is on — the chips are not drawn + // otherwise — but written as a conditional rather than an + // `expect`, because a panel is free to change its mind about that + // and nothing here should fall over when it does. + if let Some(mut current) = chosen.get() { + current.sensitivity = peaking::sensitivity(index); + chosen.set(Some(current)); + redraw(&w); + } + }); + } + { + let weak = window.as_weak(); + let chosen = chosen_peaking.clone(); + let redraw = redraw.clone(); + window.on_peaking_colour_picked(move |index| { + let Some(w) = weak.upgrade() else { return }; + w.set_peaking_colour(index); + if let Some(mut current) = chosen.get() { + current.colour = peaking::colour(index); + chosen.set(Some(current)); + redraw(&w); + } + }); + } + // The chips open on whatever the vocabulary calls its default, so the + // panel and the pass agree before anything has been pressed. + window.set_peaking_sensitivity(peaking::sensitivity_index(Default::default())); + window.set_peaking_colour(peaking::colour_index(Default::default())); + // TRACES: FR-DSP-8 | FR-DSP-6 // And which display that canvas is on, from now until the window closes. display_ui::attach(&window, &display, &viewport, redraw.clone()); diff --git a/ui/dr-ui/src/peaking.rs b/ui/dr-ui/src/peaking.rs new file mode 100644 index 0000000..70666d6 --- /dev/null +++ b/ui/dr-ui/src/peaking.rs @@ -0,0 +1,160 @@ +//! TRACES: FR-CULL-3 +//! The focus-peaking vocabulary, as the indices a chip row can carry. +//! +//! `dr_gpu` decides what peaking *is* — the measure, the thresholds, the +//! marks. This decides how a menu of three sensitivities and four colours +//! crosses the boundary into Slint, which has no notion of a Rust enum and +//! carries the choice as an `int` into an array of labels. +//! +//! That translation is small and it is the kind of small that goes wrong +//! silently. An index the interface sends that Rust reads as a different +//! variant produces a control that changes something other than what it says, +//! which nobody notices as a bug — they notice it as peaking behaving oddly. +//! So the order lives in one place here, both directions are asserted to round +//! trip, and a test checks that the labels in `ui/peaking.slint` still number +//! the same as the vocabularies they claim to name. +//! +//! Free-standing functions over plain integers, deliberately, for the reason +//! `crate::histogram` gives: none of this needs a GPU, a window or a +//! photograph to be checked, and all of it is invisible when wrong. + +use dr_gpu::{PeakColour, PeakSensitivity}; + +/// The sensitivities, in the order the chip row shows them. +/// +/// Least sensitive first, so the row reads left to right as "mark less" to +/// "mark more" — the axis the photographer is actually moving along. +pub(crate) const SENSITIVITIES: [PeakSensitivity; 3] = [ + PeakSensitivity::Low, + PeakSensitivity::Medium, + PeakSensitivity::High, +]; + +/// The mark colours, in the order the chip row shows them. +pub(crate) const COLOURS: [PeakColour; 4] = [ + PeakColour::Red, + PeakColour::Yellow, + PeakColour::Cyan, + PeakColour::Magenta, +]; + +/// The sensitivity an index names. +/// +/// Out of range falls back to the default rather than panicking. The index +/// arrives from the interface, and the interface is the half of this that can +/// be recompiled without recompiling the other — a chip row that grew an entry +/// should degrade to a sane setting, not take the application down mid-cull. +pub(crate) fn sensitivity(index: i32) -> PeakSensitivity { + usize::try_from(index) + .ok() + .and_then(|i| SENSITIVITIES.get(i).copied()) + .unwrap_or_default() +} + +/// The colour an index names, on the same terms. +pub(crate) fn colour(index: i32) -> PeakColour { + usize::try_from(index) + .ok() + .and_then(|i| COLOURS.get(i).copied()) + .unwrap_or_default() +} + +/// Which chip is lit for this sensitivity. +pub(crate) fn sensitivity_index(value: PeakSensitivity) -> i32 { + SENSITIVITIES.iter().position(|s| *s == value).unwrap_or(0) as i32 +} + +/// Which chip is lit for this colour. +pub(crate) fn colour_index(value: PeakColour) -> i32 { + COLOURS.iter().position(|c| *c == value).unwrap_or(0) as i32 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_variant_appears_exactly_once_in_its_row() { + // A variant missing from the row is a setting the photographer cannot + // reach; one listed twice is two chips that do the same thing, of + // which only the first can ever look selected. Both are invisible in + // the running application until somebody presses the wrong chip. + for s in SENSITIVITIES { + assert_eq!( + SENSITIVITIES.iter().filter(|x| **x == s).count(), + 1, + "{s:?} is listed more than once" + ); + } + for c in COLOURS { + assert_eq!(COLOURS.iter().filter(|x| **x == c).count(), 1); + } + // Named rather than counted, so adding a variant to `dr_gpu` without + // adding it here fails to compile instead of passing quietly. + assert!(SENSITIVITIES.contains(&PeakSensitivity::Low)); + assert!(SENSITIVITIES.contains(&PeakSensitivity::Medium)); + assert!(SENSITIVITIES.contains(&PeakSensitivity::High)); + assert!(COLOURS.contains(&PeakColour::Red)); + assert!(COLOURS.contains(&PeakColour::Yellow)); + assert!(COLOURS.contains(&PeakColour::Cyan)); + assert!(COLOURS.contains(&PeakColour::Magenta)); + } + + #[test] + fn an_index_and_its_variant_agree_in_both_directions() { + // The failure this catches is a chip that lights up under the pointer + // while a different setting takes effect — the two directions drifting + // apart is exactly what one shared array is here to prevent, and the + // only way to see it is to go round. + for (i, s) in SENSITIVITIES.iter().enumerate() { + assert_eq!(sensitivity(i as i32), *s); + assert_eq!(sensitivity_index(*s), i as i32); + } + for (i, c) in COLOURS.iter().enumerate() { + assert_eq!(colour(i as i32), *c); + assert_eq!(colour_index(*c), i as i32); + } + } + + #[test] + fn an_index_from_nowhere_lands_on_the_default_rather_than_panicking() { + // Slint has no bound on the `int` it sends and Rust has no way to + // refuse one. A panic here would be an application that closes because + // a chip row was edited. + assert_eq!(sensitivity(-1), PeakSensitivity::default()); + assert_eq!(sensitivity(99), PeakSensitivity::default()); + assert_eq!(colour(-1), PeakColour::default()); + assert_eq!(colour(99), PeakColour::default()); + } + + #[test] + fn the_panel_offers_exactly_the_choices_this_module_knows_about() { + // **The one seam neither compiler checks.** The labels live in + // `ui/peaking.slint` and the meanings live here, joined only by an + // integer; a fifth colour added to the chip row would send index 4 to + // `colour`, which would quietly answer Red. Reading the file is + // clumsier than a derive, and it is what there is. + let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/ui/peaking.slint")) + .expect("the panel this module serves"); + + let listed = |line_start: &str| -> usize { + let line = src + .lines() + .map(str::trim) + .find(|l| l.starts_with(line_start)) + .unwrap_or_else(|| panic!("no `{line_start}` row in peaking.slint")); + line.matches('"').count() / 2 + }; + + assert_eq!( + listed("options: [\"Low\""), + SENSITIVITIES.len(), + "the sensitivity chips and `SENSITIVITIES` disagree" + ); + assert_eq!( + listed("options: [\"Red\""), + COLOURS.len(), + "the colour chips and `COLOURS` disagree" + ); + } +} diff --git a/ui/dr-ui/src/settings_store.rs b/ui/dr-ui/src/settings_store.rs index 755772d..d8b9c54 100644 --- a/ui/dr-ui/src/settings_store.rs +++ b/ui/dr-ui/src/settings_store.rs @@ -1,4 +1,4 @@ -//! TRACES: FR-PLAT-LIN-1 | FR-NC-6a | FR-EXP-5 +//! TRACES: FR-PLAT-LIN-1 | FR-NC-6a | FR-EXP-5 | NFR-OPS-3 //! Reads and writes `settings.json` beside the session config. //! //! Deliberately a near-twin of [`SessionStore`](dr_sync_nextcloud::SessionStore) diff --git a/ui/dr-ui/ui/app.slint b/ui/dr-ui/ui/app.slint index 6c955e8..af492c3 100644 --- a/ui/dr-ui/ui/app.slint +++ b/ui/dr-ui/ui/app.slint @@ -11,6 +11,7 @@ import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, Progres import { CollectionsPanel, CollectionRow, OfflinePrompt } from "collections.slint"; import { HistogramPanel, HistogramView } from "histogram.slint"; import { PresetSheet, ScopeChips, ScopeKind } from "presets.slint"; +import { FocusMarks, FocusPanel } from "peaking.slint"; import { SettingsPage } from "settings.slint"; import { ImportPage } from "import.slint"; import { StatusBar, InfoPanel } from "develop.slint"; @@ -71,6 +72,22 @@ export component AppWindow inherits Window { /// of a draft frame is a histogram of an image nobody is reading. in property histogram; + /// TRACES: FR-CULL-3 + /// Focus peaking: the marks, whether they describe *this* frame, and the + /// three things the photographer chose. All of them are Rust's, because + /// the marks come from a compute pass — see `peaking.slint` for why + /// `focus-overlay-ready` is a separate question from `peaking-on`. + in property focus-overlay; + in property focus-overlay-ready: false; + in property peaking-on: false; + in property peaking-available: true; + in property peaking-sensitivity: 1; + in property peaking-colour: 0; + + callback peaking-toggled(bool); + callback peaking-sensitivity-picked(int); + callback peaking-colour-picked(int); + // --- zoom, pan and crop (FR-DEV-4) --- // // Zoom is a *viewing* state, not an edit: it changes the resolution the @@ -1669,6 +1686,18 @@ in property panel-visible: true; image-rendering: ImageRendering.pixelated; } + // TRACES: FR-CULL-3 + // The focus marks, over the same fitted rect. See + // `peaking.slint` for why they are a layer over the canvas + // rather than a tint in it. + if root.focus-overlay-ready && root.total > 0: FocusMarks { + x: parent.shown-x; + y: parent.shown-y; + width: parent.shown-w; + height: parent.shown-h; + marks: root.focus-overlay; + } + // Where the photograph actually sits inside this box. // // `image-fit: contain` letterboxes, and Slint does not report @@ -2202,6 +2231,26 @@ in property panel-visible: true; background: Theme.rule; } + // TRACES: FR-CULL-3 + // Under the histogram, because the two are the same + // kind of thing: instruments that report on the + // photograph rather than change it. Kept in every + // mode for the same reason the histogram is. + FocusPanel { + available: root.peaking-available; + showing: root.peaking-on; + sensitivity: root.peaking-sensitivity; + colour: root.peaking-colour; + toggled(v) => { root.peaking-toggled(v); } + sensitivity-picked(i) => { root.peaking-sensitivity-picked(i); } + colour-picked(i) => { root.peaking-colour-picked(i); } + } + + Rectangle { + height: 1px; + background: Theme.rule; + } + // Framing above the colour work, matching how the edit is // made rather than how it is applied: the frame is decided // by eye first and the pipeline runs it last (see diff --git a/ui/dr-ui/ui/masks.slint b/ui/dr-ui/ui/masks.slint index 1851779..aacccea 100644 --- a/ui/dr-ui/ui/masks.slint +++ b/ui/dr-ui/ui/masks.slint @@ -137,10 +137,18 @@ component MaskEntry inherits Rectangle { alignment: center; spacing: 0px; + // Both lines below are bounded for the reason the subject + // row is: a mask's label and kind come from the model, and an + // unbounded `Text` asks for its whole string at layout time + // even when `elide` means it will never draw it. A mask *is* a + // segmentation result, so without this the column moved when a + // subject was clicked as well as when one was found. Label { text: root.data.label; emphasised: root.data.selected || touch.has-hover; overflow: elide; + min-width: 0px; + max-width: 160px; } Caption { @@ -148,6 +156,8 @@ component MaskEntry inherits Rectangle { // targets in a row, and wrapping would give the rows of a // stack different heights for no gain. overflow: elide; + min-width: 0px; + max-width: 160px; // Three states worth distinguishing, and each has a // different remedy: stale needs the segmentation re-run, // unadjusted needs a slider moved, and the ordinary case @@ -418,6 +428,30 @@ export component MaskPanel inherits Rectangle { emphasised: subject-row.has-hover; horizontal-stretch: 1; overflow: elide; + // **`elide` is a paint-time behaviour, and this is a + // layout-time problem.** A `Text` asks for the width of + // its whole string whether or not it will draw all of + // it, so without a stated maximum this row asked for + // whatever the model happened to return, that became + // `layout.preferred-width`, the panel publishes that as + // its `min-width`, and the develop column takes the + // widest minimum any panel declares. The column + // therefore moved the instant segmentation finished — + // a photograph the user was looking at, jumping + // sideways because a label said "traffic light". + // + // Stated as a maximum for the reason `ChipGrid` + // declares its width from its column count rather than + // from its options: what a panel asks for must follow + // from its structure, never from its data. Past this + // the row elides, which is what `elide` was for. + // + // 160px is the same judgement as `ChipGrid`'s 88px + // chip — comfortable for the class names this model + // returns, and narrow enough that a subject list + // cannot be what sets the column. + min-width: 0px; + max-width: 160px; } Value { text: round(subject.score * 100) + "%"; } } diff --git a/ui/dr-ui/ui/peaking.slint b/ui/dr-ui/ui/peaking.slint new file mode 100644 index 0000000..3821e55 --- /dev/null +++ b/ui/dr-ui/ui/peaking.slint @@ -0,0 +1,150 @@ +// TRACES: FR-CULL-3 +// The focus-peaking switch, and the two choices it exposes. +// +// **An instrument, not an operation**, exactly as the histogram above it is: +// it has no parameters in the edit graph, changes nothing about the +// photograph, and answers a question rather than asking one. So it is written +// by hand rather than generated from a descriptor, and FR-DEV-3a is untroubled +// by it — nothing here names an operation or reads a parameter out of one. +// +// **Both choices are words, not swatches.** The colour picker is the obvious +// place to draw four coloured squares, and NFR-A11Y-3 is the reason not to: +// a control for choosing between hues, presented only as hues, is unusable by +// the person most likely to need to change it. The chips say "Red" and "Cyan". +// +// **Why the two chip rows only exist while peaking is on.** They are settings +// for something that is not happening, and the develop column is the +// photographer's instrument panel — every row it holds is a slider pushed +// below the fold. The panel's own height is bound to its content, so the +// column reflows rather than leaving a gap. + +import { Theme } from "theme.slint"; +import { Button, PanelHeading, Caption } from "widgets.slint"; +import { Segmented } from "controls.slint"; + +// TRACES: FR-CULL-3 +// The marks themselves, composited over the canvas. +// +// **A layer over the photograph and not a tint in it**, which is the same rule +// `app.slint` states on the region map: a diagnostic "must not reach the +// histogram, an export, or the texture the develop pass hands the compositor". +// `HistogramPass` counts whatever the develop pass last rendered, so marks +// painted into that frame would arrive in the histogram as a spike and in the +// clipping figure as blown highlights. The layer is transparent everywhere +// except where something is in focus. +// +// **No `source-clip` and no rotation**, unlike the region map. That is a +// source-space picture being windowed down to the visible part; this was +// measured on the rendered frame itself, so it is already cropped, zoomed and +// turned exactly as the canvas is. One fewer thing that can drift out of +// registration. +// +// The caller places it on `canvas-area`'s fitted rect, which is the shared +// contract for anything that lands on the picture. +export component FocusMarks inherits Image { + /// The overlay `DevelopSession::focus_overlay` produced for this frame. + in property marks; + + source: root.marks; + image-fit: fill; + // Nearest-neighbour: a mark is one pixel wide, and smoothing spreads it + // into a grey haze that reads as softness — the opposite of what it is + // reporting. + image-rendering: ImageRendering.pixelated; +} + +// TRACES: FR-CULL-3 +export component FocusPanel inherits Rectangle { + /// Whether this device could build the overlay at all. + /// + /// A compute pass can fail to compile on a driver nobody here has, and the + /// honest response is to say so rather than to offer a switch that does + /// nothing when pressed. The develop view keeps working without it; only + /// this panel changes. + in property available: true; + /// Whether the overlay is currently being drawn. + /// + /// `showing` rather than the obvious `on`: Slint has no reserved word + /// there today, and a one-word property that might become one is not worth + /// the bet on a panel this small. + in property showing: false; + /// Index into `PeakSensitivity`, in the order Rust declares it. + in property sensitivity: 1; + /// Index into `PeakColour`, likewise. + in property colour: 0; + + callback toggled(bool); + callback sensitivity-picked(int); + callback colour-picked(int); + + background: Theme.surface; + + /// TRACES: FR-UI-2 + /// How wide this panel has to be before it clips itself. The develop + /// column is the largest of these and nothing else; see `SpotPanel` and + /// `HistogramPanel` for the whole protocol. + /// + /// Both chip rows wrap at three, which is what keeps this number at the + /// narrowest column the application supports rather than at four chips + /// abreast — a single row of four would set the width of the entire + /// sidebar for every other panel in it. + out property content-width: layout.preferred-width; + min-width: root.content-width; + + // Flat rather than nested, for the reason `SpotPanel` and `MaskPanel` both + // give: a nested conditional layout under-reports its height here and the + // rows below it get drawn on top of one another. Every row carries its own + // `if`. + layout := VerticalLayout { + padding: Theme.gap; + spacing: Theme.gap-sm; + alignment: start; + + PanelHeading { text: "FOCUS"; } + + if !root.available: Caption { + text: "This device could not build the overlay."; + wrap: word-wrap; + } + + if root.available: Button { + // The label states the action rather than the state, as the mask + // overlay's does: a photographer reads a button for what pressing + // it will do. + text: root.showing ? "Hide focus peaking" : "Show focus peaking"; + active: root.showing; + clicked => { root.toggled(!root.showing); } + } + + if root.available && root.showing: Segmented { + label: "Sensitivity"; + hint: "lower on a noisy frame"; + options: ["Low", "Medium", "High"]; + selected: root.sensitivity; + columns: 3; + picked(i) => { root.sensitivity-picked(i); } + } + + if root.available && root.showing: Segmented { + label: "Marks"; + hint: "pick what the subject is not"; + options: ["Red", "Yellow", "Cyan", "Magenta"]; + selected: root.colour; + columns: 3; + picked(i) => { root.colour-picked(i); } + } + + if root.available && root.showing: Caption { + // Said once, here, rather than left to be discovered: the marks go + // away while a control is moving because a half-resolution draft + // frame cannot be measured for sharpness (see `FocusPeakPass`). + // + // Kept to one short sentence on purpose. A wrapping Text reports + // its *unwrapped* width as its preferred one, and this panel's + // `content-width` is what the develop column sizes itself from — + // a paragraph here would hold the whole sidebar open. + text: "Marks pause while a control is dragged."; + wrap: word-wrap; + } + } +} diff --git a/ui/dr-ui/ui/widgets.slint b/ui/dr-ui/ui/widgets.slint index 5a9db2f..793439e 100644 --- a/ui/dr-ui/ui/widgets.slint +++ b/ui/dr-ui/ui/widgets.slint @@ -1,3 +1,4 @@ +// TRACES: FR-UI-6 // Shared chrome primitives and the style layer. // // Before this file every button was a Rectangle + TouchArea written out where