diff --git a/core/dr-pipeline/src/coverage.rs b/core/dr-pipeline/src/coverage.rs new file mode 100644 index 0000000..fd80962 --- /dev/null +++ b/core/dr-pipeline/src/coverage.rs @@ -0,0 +1,634 @@ +//! TRACES: FR-DEV-3 | FR-CAT-8 +//! A model's mask, in a form a sidecar can carry. +//! +//! [`MaskSource::Subject`](crate::mask::MaskSource::Subject) and +//! [`MaskSource::Category`](crate::mask::MaskSource::Category) name what they +//! cover — an index, a class, a category — and naming is enough only while +//! the run that produced the numbers is still in memory. Reopen the +//! photograph, or export it from the grid, and there is no run: the layer +//! resolves to nothing and the local adjustment silently is not applied. That +//! is what this module exists to stop. It stores the *pixels* the layer +//! covered, beside the identity rather than instead of it, so a second model +//! pass is an optimisation rather than a precondition. +//! +//! # Two levels, and why that is not a compromise +//! +//! The model hands out a byte per pixel, but nothing downstream reads more +//! than one bit of it. A subject or category layer becomes a mask by way of +//! an exact Euclidean distance field, and that field is measured from +//! `coverage >= threshold` — the soft shoulder the model produced is +//! discarded on the first line of the transform. Everything soft about the +//! rendered edge comes afterwards, from [`MaskLayer::feather`] and +//! [`MaskLayer::falloff`], which are read off the *distance*. +//! +//! [`MaskLayer::feather`]: crate::mask::MaskLayer::feather +//! [`MaskLayer::falloff`]: crate::mask::MaskLayer::falloff +//! +//! So [`RENDERED_LEVELS`] is two, and the result is not an approximation of +//! what the model said: it is exactly the part of what the model said that +//! reaches a pixel. Storing all 256 levels would be storing 1.7 MB of +//! interpolation to reconstruct a predicate — and it would not even compress, +//! because a model mask is a bilinear upsample of a coarse grid and therefore +//! has almost no two adjacent bytes alike. Measured on a simulated sky and a +//! simulated figure at 1600x1067, against 1.71 MB raw: **4.0 kB and 6.5 kB at +//! two levels**, 46 kB and 76 kB at sixteen, and 835 kB and 1.43 MB at all +//! 256 — the last two being over [`MAX_PAYLOAD`] and therefore not storable +//! at all. +//! +//! [`Coverage::encode`] still takes the level count, and it is written into +//! the line, so a later build that finds a use for the shoulder can write +//! sixteen levels and this one will read them back correctly rather than +//! misreading a stream of lengths as pairs. +//! +//! # One line, because a node is a line +//! +//! FR-NC-9 merges the edit graph per node and [`Version::merge`] does that by +//! comparing lines, so a stored mask is one `coverage = ...` line inside the +//! layer's block — the same shape the `regions = ...` line already had, for +//! the same reason. Splitting it over many lines would put a single opaque +//! blob into the merge as several independently-winnable keys, which is a +//! merge that can produce a mask neither device ever had. +//! +//! [`Version::merge`]: crate::sidecar::Version::merge +//! +//! # Hand-rolled, and it has to be +//! +//! `dr-pipeline` links nothing (ARCH §6.5a), which is what lets the descriptor +//! and codegen logic be tested without a device. That rules out `flate2`, +//! `serde` and `base64`, so the run-length coder and the digits below are +//! written out. It is forty lines, and the alternative was a dependency in the +//! one crate that has none. + +use std::fmt::Write as _; + +/// The number of coverage levels the renderer can actually tell apart. +/// +/// Two. See the module header: the distance field is built from a threshold, +/// so a second level is the whole of the information that survives into a +/// rendered frame. Named rather than written as `2` at the call site because +/// the number is a *claim about the render path*, and a claim wants somewhere +/// to be explained. +pub const RENDERED_LEVELS: u32 = 2; + +/// The most encoded payload a stored coverage may take, in bytes. +/// +/// Sidecars sync over WebDAV and are read whole by every device that opens the +/// photograph, so a mask that will not compress must not be allowed to make +/// the file enormous — it is a *cache* of something a model can produce again, +/// and no cache is worth a megabyte of sync traffic per layer. +/// +/// A realistic mask lands between 4 and 7 kB, so this is roughly ten times the +/// worst case anyone has measured: enough for genuinely awkward subjects — +/// foliage, chain-link, hair against a busy background — and far short of a +/// file a human cannot open. Past it [`Coverage::encode`] returns `None`, the +/// layer stores nothing, and the behaviour falls back to what it was before +/// this module existed: the mask needs the model run. Refusing rather than +/// truncating, because half a mask renders as a *wrong* mask, which is the +/// failure that announces itself to nobody. +pub const MAX_PAYLOAD: usize = 64 * 1024; + +/// The most pixels a coverage read from a file may claim. +/// +/// A file is not trusted. The proxy a mask is built at is bounded by the long +/// edge the segmentation runs on — under three megapixels — so this is ample +/// headroom, and it is here so that `width * height` from a corrupt line +/// cannot ask for an allocation measured in gigabytes. +pub const MAX_PIXELS: usize = 16 << 20; + +/// Digits of the payload's base-64 varint. Ordered so the alphabet is stable +/// and contains nothing a line-oriented format would have to escape — no +/// whitespace, no `=`, no `#`. +const DIGITS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +/// Bit set in a digit that means "another digit follows". +const CONTINUE: u32 = 32; + +/// Value bits carried by one digit. +const CHUNK: u32 = 5; + +const fn reverse_digits() -> [u8; 256] { + let mut table = [255u8; 256]; + let mut i = 0; + while i < 64 { + table[DIGITS[i] as usize] = i as u8; + i += 1; + } + table +} + +/// Digit value by byte, `255` for anything that is not a digit. +const REVERSE: [u8; 256] = reverse_digits(); + +/// TRACES: FR-DEV-3 +/// One layer's pixel coverage, held in the form it is stored in. +/// +/// **Encoded, not expanded.** The struct owns the payload text rather than the +/// 1.7 MB raster it decodes to, because that raster is wanted exactly once — +/// when a distance field is built — and is held by nothing afterwards. Keeping +/// it expanded would put a megabyte and a half per layer into every undo +/// snapshot the history stack holds, to save a decode that costs far less than +/// the exact Euclidean transform immediately following it. +/// +/// It also makes the round trip byte-identical for free: a coverage read from +/// a file and written back is the same characters, which is the property that +/// lets a caller skip an upload by comparing content +/// ([`Sidecar`](crate::Sidecar)). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Coverage { + width: usize, + height: usize, + levels: u32, + payload: String, +} + +impl Coverage { + /// Encode one byte-per-pixel coverage, or `None` where it will not fit. + /// + /// `levels` is what the bytes are quantised to on the way in; see + /// [`RENDERED_LEVELS`] for why two is the honest answer for a mask that is + /// going to be thresholded. + /// + /// `None` for a mismatched length, a nonsensical level count, or a payload + /// over [`MAX_PAYLOAD`] — all three meaning "do not store this", which the + /// caller can act on identically because the fallback is the same in every + /// case. + pub fn encode(values: &[u8], width: usize, height: usize, levels: u32) -> Option { + if width == 0 || height == 0 || values.len() != width.checked_mul(height)? { + return None; + } + if !(2..=256).contains(&levels) { + return None; + } + + let top = levels - 1; + let mut payload = String::new(); + let mut index = 0; + while index < values.len() { + let level = quantise(values[index], top); + let mut run = 1; + while index + run < values.len() && quantise(values[index + run], top) == level { + run += 1; + } + push_varint(&mut payload, level as u64); + push_varint(&mut payload, run as u64); + index += run; + + // Checked inside the loop rather than after it: the pathological + // input is one that runs to a payload larger than the raster, and + // building the whole of that before deciding to throw it away is + // the allocation this bound exists to prevent. + if payload.len() > MAX_PAYLOAD { + return None; + } + } + + Some(Self { + width, + height, + levels, + payload, + }) + } + + /// Read the value of a sidecar `coverage` line. + /// + /// `None` for anything that does not describe a complete raster. A + /// coverage is a cache, so refusing it costs a model run; accepting a + /// partial one costs a photograph rendered with a mask that is wrong in a + /// way nothing reports. + pub fn parse(value: &str) -> Option { + let mut tokens = value.split_whitespace(); + let width: usize = tokens.next()?.parse().ok()?; + let height: usize = tokens.next()?.parse().ok()?; + let levels: u32 = tokens.next()?.parse().ok()?; + let payload = tokens.next()?; + + let pixels = width.checked_mul(height)?; + if pixels == 0 || pixels > MAX_PIXELS || !(2..=256).contains(&levels) { + return None; + } + if payload.len() > MAX_PAYLOAD { + return None; + } + + // Measured rather than expanded. The payload has to be checked here — + // failing at the point of use would put the error in the renderer, + // where there is no longer a file to name in the message — but a + // library scan parses thousands of sidecars, and materialising a + // megabyte and a half per layer to establish that the arithmetic adds + // up would make opening the grid pay for masks nobody is rendering. + if measure(payload, levels)? != pixels { + return None; + } + + Some(Self { + width, + height, + levels, + payload: payload.to_string(), + }) + } + + /// The value to write after `coverage = `. + pub fn to_text(&self) -> String { + format!( + "{} {} {} {}", + self.width, self.height, self.levels, self.payload + ) + } + + pub fn width(&self) -> usize { + self.width + } + + pub fn height(&self) -> usize { + self.height + } + + pub fn levels(&self) -> u32 { + self.levels + } + + /// The encoded payload's length in bytes — what this costs a sidecar. + pub fn encoded_len(&self) -> usize { + self.payload.len() + } + + /// Expand back to one byte per pixel, at the size it was stored at. + pub fn decode(&self) -> Vec { + decode(&self.payload, self.width * self.height, self.levels) + .expect("a Coverage only exists once its payload has been decoded once") + } + + /// Expand to `width` x `height`, resampling if that is not the size it was + /// stored at. + /// + /// Nearest neighbour, and deliberately: the values are a threshold's two + /// sides, so interpolating between them would invent coverage levels that + /// mean nothing and move the boundary by a rounding rule rather than by a + /// measurement. The resample only runs at all when a build reads a mask + /// stored against a different proxy edge — in the ordinary case the sizes + /// match and this is the decode. + pub fn decode_at(&self, width: usize, height: usize) -> Vec { + let source = self.decode(); + if (width, height) == (self.width, self.height) { + return source; + } + if width == 0 || height == 0 { + return Vec::new(); + } + + let mut out = vec![0u8; width * height]; + for y in 0..height { + let sy = ((y * self.height) / height).min(self.height - 1); + let row = sy * self.width; + for x in 0..width { + let sx = ((x * self.width) / width).min(self.width - 1); + out[y * width + x] = source[row + sx]; + } + } + out + } +} + +/// One byte to its level, rounding to nearest. +fn quantise(value: u8, top: u32) -> u32 { + ((value as u32 * top) + 127) / 255 +} + +/// One level back to a byte, so that the top level is exactly 255. +fn dequantise(level: u32, top: u32) -> u8 { + (((level * 255) + top / 2) / top).min(255) as u8 +} + +/// Little-endian base-64 varint: five value bits per digit, the sixth saying +/// whether another follows. +fn push_varint(out: &mut String, mut value: u64) { + loop { + let chunk = (value & (CONTINUE - 1) as u64) as u32; + value >>= CHUNK; + let more = if value != 0 { CONTINUE } else { 0 }; + let _ = out.write_char(DIGITS[(chunk | more) as usize] as char); + if value == 0 { + return; + } + } +} + +/// Read one varint, returning it and how many digits it took. +fn read_varint(bytes: &[u8]) -> Option<(u64, usize)> { + let mut value: u64 = 0; + let mut shift = 0; + for (taken, &byte) in bytes.iter().enumerate() { + let digit = REVERSE[byte as usize]; + if digit == 255 { + return None; + } + // A run cannot exceed MAX_PIXELS and a level cannot exceed 255, so a + // varint past this width is a corrupt line rather than a large number. + if shift >= 64 { + return None; + } + value |= ((digit as u64) & (CONTINUE - 1) as u64) << shift; + if digit as u32 & CONTINUE == 0 { + return Some((value, taken + 1)); + } + shift += CHUNK; + } + None +} + +/// Walk a payload's runs, handing each `(level, length)` to `take`. +/// +/// Returns the total length, or `None` for a payload that is not well formed: +/// a digit that is not one, a truncated varint, a zero-length run, or a level +/// the declared count does not contain. +fn walk(payload: &str, levels: u32, mut take: impl FnMut(u32, usize)) -> Option { + let top = levels - 1; + let bytes = payload.as_bytes(); + let mut total: usize = 0; + let mut at = 0; + + while at < bytes.len() { + let (level, used) = read_varint(&bytes[at..])?; + at += used; + let (run, used) = read_varint(&bytes[at..])?; + at += used; + + let level = u32::try_from(level).ok()?; + if level > top { + return None; + } + let run = usize::try_from(run).ok()?; + // A zero-length run is not a shorter way of saying anything, so it is + // a corrupt line rather than a run to skip — and left in, two of them + // would encode the same raster two ways and break the byte-identical + // round trip the sidecar relies on. + if run == 0 { + return None; + } + total = total.checked_add(run)?; + if total > MAX_PIXELS { + return None; + } + take(level, run); + } + + Some(total) +} + +/// How many pixels a payload covers, without building any of them. +fn measure(payload: &str, levels: u32) -> Option { + walk(payload, levels, |_, _| {}) +} + +/// Expand a payload to `pixels` bytes, or `None` if it does not describe +/// exactly that many. +fn decode(payload: &str, pixels: usize, levels: u32) -> Option> { + let top = levels - 1; + let mut out = Vec::with_capacity(pixels); + let total = walk(payload, levels, |level, run| { + out.resize(out.len() + run, dequantise(level, top)); + })?; + (total == pixels).then_some(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Round trip at the level count the renderer actually uses. + fn round_trip(values: &[u8], width: usize, height: usize) -> Vec { + let coverage = Coverage::encode(values, width, height, RENDERED_LEVELS) + .expect("this mask should encode"); + let text = coverage.to_text(); + let read = Coverage::parse(&text).expect("what was written should parse"); + assert_eq!(read, coverage, "the round trip changed the encoding"); + assert_eq!(read.to_text(), text, "re-writing must be byte-identical"); + read.decode() + } + + /// Two levels is exactly the predicate the distance transform applies, so + /// the round trip must agree with it on every pixel. + fn thresholded(values: &[u8]) -> Vec { + values.iter().map(|&v| v >= 128).collect() + } + + #[test] + fn an_empty_mask_round_trips() { + let values = vec![0u8; 64 * 32]; + let back = round_trip(&values, 64, 32); + assert_eq!(back, values); + } + + #[test] + fn a_full_mask_round_trips() { + let values = vec![255u8; 64 * 32]; + let back = round_trip(&values, 64, 32); + assert_eq!(back, values); + } + + #[test] + fn a_single_pixel_mask_round_trips() { + let mut values = vec![0u8; 64 * 32]; + values[17 * 64 + 33] = 255; + let back = round_trip(&values, 64, 32); + assert_eq!(back, values); + } + + #[test] + fn a_one_pixel_raster_round_trips() { + assert_eq!(round_trip(&[255], 1, 1), vec![255]); + assert_eq!(round_trip(&[0], 1, 1), vec![0]); + } + + /// The whole of the fidelity claim: two levels loses nothing the renderer + /// could have used, because the renderer thresholds. + #[test] + fn two_levels_preserve_the_threshold_exactly() { + let values: Vec = (0..=255u8).collect(); + let back = round_trip(&values, 16, 16); + assert_eq!(thresholded(&back), thresholded(&values)); + // And the shoulder really is gone, which is the cost being paid. + assert!(back.iter().all(|&v| v == 0 || v == 255)); + } + + /// A soft edge quantised to sixteen levels stays within one step of what + /// went in, so a later build that wants the shoulder can have it. + #[test] + fn sixteen_levels_are_within_one_step() { + let values: Vec = (0..256).map(|i| i as u8).collect(); + let coverage = Coverage::encode(&values, 16, 16, 16).expect("should encode"); + let back = Coverage::parse(&coverage.to_text()) + .expect("should parse") + .decode(); + for (a, b) in values.iter().zip(&back) { + assert!( + (*a as i32 - *b as i32).abs() <= 255 / 15 / 2 + 1, + "{a} came back as {b}" + ); + } + } + + /// The case run-length coding is worst at. It must refuse rather than + /// write a payload larger than the raster it came from. + #[test] + fn alternating_detail_is_refused_rather_than_expanded() { + let (w, h) = (512, 512); + let values: Vec = (0..w * h) + .map(|i| if i % 2 == 0 { 0 } else { 255 }) + .collect(); + assert!( + Coverage::encode(&values, w, h, RENDERED_LEVELS).is_none(), + "a checkerboard must not be stored" + ); + } + + /// Small enough to fit, and still exact — the bound is on size, not on + /// shape, so awkward detail that *does* fit must survive intact. + #[test] + fn alternating_detail_that_fits_is_exact() { + let (w, h) = (64, 64); + let values: Vec = (0..w * h) + .map(|i| if i % 2 == 0 { 0 } else { 255 }) + .collect(); + let back = round_trip(&values, w, h); + assert_eq!(back, values); + } + + #[test] + fn a_mask_of_the_wrong_length_is_refused() { + assert!(Coverage::encode(&[0u8; 10], 4, 4, RENDERED_LEVELS).is_none()); + assert!(Coverage::encode(&[], 0, 0, RENDERED_LEVELS).is_none()); + } + + #[test] + fn a_payload_that_does_not_cover_the_raster_is_refused() { + let values = vec![0u8; 32]; + let coverage = Coverage::encode(&values, 8, 4, RENDERED_LEVELS).expect("should encode"); + let payload = coverage.to_text(); + let payload = payload.rsplit_once(' ').expect("a payload").1; + // The same payload, against a raster twice the size it covers. + assert!(Coverage::parse(&format!("8 4 2 {payload}")).is_some()); + assert!(Coverage::parse(&format!("8 8 2 {payload}")).is_none()); + } + + #[test] + fn nonsense_is_refused_rather_than_guessed_at() { + assert!(Coverage::parse("").is_none()); + assert!(Coverage::parse("8 4 2").is_none(), "no payload"); + assert!( + Coverage::parse("8 4 1 AA").is_none(), + "one level is not a mask" + ); + assert!(Coverage::parse("8 4 2 ****").is_none(), "not digits"); + assert!( + Coverage::parse(&format!("{} {} 2 AA", usize::MAX, usize::MAX)).is_none(), + "a size that overflows must not be believed" + ); + assert!( + Coverage::parse("100000 100000 2 A_____").is_none(), + "a raster past the cap must not be allocated" + ); + } + + /// A level a payload is not allowed to name, in a file that names it. + #[test] + fn a_level_outside_the_range_is_refused() { + // "BB" is level 1, run 1 — the shortest legal payload there is. + assert_eq!(decode("BB", 1, 2), Some(vec![255])); + // "DB" is level 3, run 1, and a two-level coverage has no level 3. + assert!(decode("DB", 1, 2).is_none()); + // A run of zero says nothing and is refused rather than skipped. + assert!(decode("BA", 1, 2).is_none()); + } + + #[test] + fn resampling_lands_on_the_same_shape() { + let (w, h) = (32, 32); + let mut values = vec![0u8; w * h]; + for y in 8..24 { + for x in 8..24 { + values[y * w + x] = 255; + } + } + let coverage = Coverage::encode(&values, w, h, RENDERED_LEVELS).expect("should encode"); + + let same = coverage.decode_at(w, h); + assert_eq!(same, values, "the matching size must not resample at all"); + + let half = coverage.decode_at(16, 16); + assert_eq!(half.len(), 256); + assert_eq!(half.iter().filter(|&&v| v == 255).count(), 64); + + let double = coverage.decode_at(64, 64); + assert_eq!(double.len(), 4096); + assert_eq!(double.iter().filter(|&&v| v == 255).count(), 1024); + } + + /// Long runs cross rows, which is what makes a flat mask cost almost + /// nothing: a 1600x1067 empty frame is two numbers. + #[test] + fn a_flat_mask_costs_almost_nothing() { + let values = vec![0u8; 1600 * 1067]; + let coverage = + Coverage::encode(&values, 1600, 1067, RENDERED_LEVELS).expect("should encode"); + assert!( + coverage.encoded_len() < 16, + "an empty mask took {} bytes", + coverage.encoded_len() + ); + } + + /// What a real one costs. The shape is a bilinear upsample of a coarse + /// grid, which is what both models produce, so the run structure is the + /// one a photograph actually gives. + #[test] + fn a_realistic_mask_fits_in_a_sidecar() { + let (w, h) = (1600usize, 1067usize); + let (gw, gh) = (160usize, 107usize); + let mut grid = vec![0f32; gw * gh]; + for y in 0..gh { + for x in 0..gw { + let dx = (x as f32 - 80.0) / 26.0; + let dy = (y as f32 - 60.0) / 42.0; + let r = (dx * dx + dy * dy).sqrt() + + 0.06 * ((y as f32 * 0.9).sin() * (x as f32 * 0.7).cos()); + grid[y * gw + x] = 1.0 / (1.0 + ((r - 1.0) * 9.0).exp()); + } + } + let mut values = vec![0u8; w * h]; + for y in 0..h { + let fy = ((y as f32 + 0.5) / h as f32 * gh as f32 - 0.5).max(0.0); + let (y0, ty) = (fy.floor() as usize, fy.fract()); + let y1 = (y0 + 1).min(gh - 1); + for x in 0..w { + let fx = ((x as f32 + 0.5) / w as f32 * gw as f32 - 0.5).max(0.0); + let (x0, tx) = (fx.floor() as usize, fx.fract()); + let x1 = (x0 + 1).min(gw - 1); + let a = grid[y0 * gw + x0] * (1.0 - tx) + grid[y0 * gw + x1] * tx; + let b = grid[y1 * gw + x0] * (1.0 - tx) + grid[y1 * gw + x1] * tx; + values[y * w + x] = ((a * (1.0 - ty) + b * ty) * 255.0) as u8; + } + } + + let coverage = Coverage::encode(&values, w, h, RENDERED_LEVELS).expect("should encode"); + let expected: Vec = values + .iter() + .map(|&v| if v >= 128 { 255 } else { 0 }) + .collect(); + assert_eq!( + coverage.decode(), + expected, + "the stored mask must threshold identically to the model's" + ); + // Measured at 6,464 bytes; the bound is loose enough not to fail over + // a change of rounding and tight enough to catch a coder that has + // stopped coding. Against 1,707,200 bytes raw. + assert!( + coverage.encoded_len() < 8 * 1024, + "a realistic subject took {} bytes", + coverage.encoded_len() + ); + } +} diff --git a/core/dr-pipeline/src/lib.rs b/core/dr-pipeline/src/lib.rs index e61f9cf..68e2379 100644 --- a/core/dr-pipeline/src/lib.rs +++ b/core/dr-pipeline/src/lib.rs @@ -32,6 +32,7 @@ //! single multiply and white balance a per-channel scale; on gamma-encoded //! data neither would be physically meaningful (ARCH §5.2). +pub mod coverage; pub mod declared; pub mod descriptor; pub mod detail; @@ -48,6 +49,7 @@ pub mod spot; pub mod starter; pub mod state; +pub use coverage::Coverage; pub use declared::{Declaration, DeclaredOp}; pub use descriptor::{ Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind, diff --git a/core/dr-pipeline/src/mask.rs b/core/dr-pipeline/src/mask.rs index b9ce4a3..5d5cc53 100644 --- a/core/dr-pipeline/src/mask.rs +++ b/core/dr-pipeline/src/mask.rs @@ -34,10 +34,27 @@ //! The cost is that the ids only mean anything alongside the segmentation that //! produced them, so [`MaskSource::Regions::signature`] records which one — //! see there for what happens when it does not match. +//! +//! # And the raster that had to come back anyway +//! +//! The same reasoning was applied to [`MaskSource::Subject`] and +//! [`MaskSource::Category`], and there it went one step too far. A model's +//! coverage is reproducible in principle, but only by running the model — and +//! nothing runs one except a photographer pressing a button. So a stored +//! subject layer resolved to no pixels on every path that did not have a run +//! already in memory: reopening the photograph, and exporting it from the +//! grid, which never runs one at all. +//! +//! [`MaskLayer::coverage`] is the answer, and note what it is *not*: the +//! source still stores identity, still diffs as a handful of numbers, and +//! still merges per field. The raster sits beside it as a cache, takes no part +//! in equality, and is thrown away rather than trusted when it does not fit. +//! See [`crate::coverage`]. use std::fmt::Write as _; use std::sync::Arc; +use crate::coverage::Coverage; use crate::descriptor::{OpDescriptor, ParamId}; use crate::operation::Operation; use crate::ops; @@ -527,10 +544,12 @@ pub enum MaskSource { /// right and soft, and dilation, erosion and a chosen falloff are how it /// is made to fit. /// - /// Stored as *identity*, not as pixels. The mask itself is several - /// megabytes and is reproducible by running the same model over the same - /// image, so the sidecar carries what is needed to find it again and the - /// session carries the pixels. + /// Stored as *identity*, not as pixels: the mask is several megabytes and + /// the fields below are what is needed to find it again. The pixels do go + /// in the sidecar as well, run-length coded beside the layer rather than + /// inside this variant, because "reproducible by running the model again" + /// turned out to mean "absent everywhere a model has not been run" — see + /// [`MaskLayer::coverage`]. Subject { /// Which segmentation run produced it, so a layer can tell whether /// the index below still means what it meant. @@ -559,9 +578,8 @@ pub enum MaskSource { /// before the mask ever existed, and [`Self::Subject`] is the source for /// that question. /// - /// Stored as identity like a subject, and for the same reason: the - /// coverage is megabytes and is reproducible from the same model over the - /// same image. + /// Stored as identity like a subject, and the coverage travels beside it + /// for the same reason — see [`MaskLayer::coverage`]. Category { /// Which segmentation run produced it, so a layer can tell whether /// the name below still refers to something that was computed. @@ -715,6 +733,41 @@ pub struct MaskLayer { /// the same reason: two layers may sit on the same category and want /// different amounts of it, and the model ran once for both. pub refine: f32, + + /// The pixels this layer covered, when a model produced them and they + /// were worth storing. + /// + /// # Beside the source, not inside it + /// + /// [`MaskSource::Subject`] and [`MaskSource::Category`] are *identity* — + /// which run, which instance, which category — and that is what makes them + /// diffable, small, and mergeable per field under FR-NC-9. Putting a + /// raster inside either variant would make two devices that selected the + /// same dog hold different values for the same selection, and the merge + /// would then have a binary blob to arbitrate rather than an index. + /// + /// So this sits alongside as what it actually is: a **materialisation** of + /// the source, produced by a run of a model this crate has never heard of + /// and knows nothing about. `MaskSource` still says what the layer means; + /// this says what that meant last time anybody worked it out. The + /// distinction is why it takes no part in [`PartialEq`] — a layer with the + /// pixels cached and one without are the same edit, and a merge that + /// called them different would raise a conflict over a cache. + /// + /// # Why it exists at all + /// + /// Without it a stored subject or category layer renders as nothing until + /// somebody presses "find subjects" — so reopening a photograph dropped + /// its local adjustments, and a batch export, which never runs a model, + /// could not have them at any point. See [`crate::coverage`]. + /// + /// Shared rather than owned because the undo stack holds a snapshot per + /// step and a layer is cloned by value; an `Arc` makes recording a slider + /// drag cost a refcount rather than a copy of every mask in the stack. + /// Only ever set for the two model sources — nothing else has a model + /// behind it to cache. + pub coverage: Option>, + /// This layer's adjustments. /// /// A full chain, the same one [`crate::EditGraph`] holds. That is the @@ -772,6 +825,8 @@ impl Clone for MaskLayer { morphology: self.morphology, morph_radius: self.morph_radius, refine: self.refine, + // A refcount, not a raster. See the field. + coverage: self.coverage.clone(), ops, } } @@ -789,12 +844,22 @@ impl std::fmt::Debug for MaskLayer { .field("feather", &self.feather) .field("falloff", &self.falloff) .field("morphology", &self.morphology) + .field("coverage", &self.coverage.as_ref().map(|c| c.encoded_len())) .field("active_ops", &self.active_ops().count()) .finish() } } impl PartialEq for MaskLayer { + /// Every field that is the *edit*, and deliberately not + /// [`Self::coverage`]. + /// + /// This comparison is what [`crate::sidecar::Version::merge`] uses to + /// decide whether a device changed a layer (FR-NC-9). A cached raster is + /// not something a photographer changed: one device that has run the model + /// and one that has not hold the same edit, and counting the difference + /// would raise a conflict over a cache — and, with `remote_wins`, could + /// answer it by discarding the only copy of the pixels. fn eq(&self, other: &Self) -> bool { self.id == other.id && self.name == other.name @@ -833,6 +898,9 @@ impl MaskLayer { // `dr_segment`'s number to state and this crate does not depend on // it — `Session::add_category_mask` sets it on the way in. refine: 0.0, + // Nothing has run yet. Filled in the first time a model's coverage + // is turned into a distance field — see [`Self::coverage`]. + coverage: None, ops: layer_chain(), } } diff --git a/core/dr-pipeline/src/sidecar.rs b/core/dr-pipeline/src/sidecar.rs index b13cdff..31e1161 100644 --- a/core/dr-pipeline/src/sidecar.rs +++ b/core/dr-pipeline/src/sidecar.rs @@ -67,6 +67,7 @@ use std::collections::BTreeMap; use std::fmt; use std::fmt::Write as _; +use crate::coverage::Coverage; use crate::graph::EditGraph; use crate::mask::{Falloff, MaskLayer, MaskSource, MaskStack, Morphology, Stroke, DEFAULT_FEATHER}; use crate::preset::Preset; @@ -1043,6 +1044,25 @@ fn write_mask(out: &mut String, version: &str, layer: &MaskLayer) { for (op, param, value) in layer.params() { let _ = writeln!(out, "{op}.{param} = {}", format_value(value)); } + + // TRACES: FR-DEV-3 | FR-CAT-8 + // The pixels a model found, so that opening the photograph again — or + // exporting it from the grid, where no model is ever run — renders the + // layer instead of silently dropping it. See [`crate::coverage`] for the + // encoding and for why it is one line. + // + // **Last in the block, and that is on purpose.** It is thousands of + // characters against a dozen elsewhere, and a sidecar is read by hand when + // an edit has gone wrong (ARCH §6.12); everything a human is looking for + // should be above it rather than after it. + // + // Omitted, not truncated, when it will not encode: a layer with no stored + // coverage behaves exactly as every layer did before this existed, which + // is a mask that needs the model run — where a *partial* one would be a + // mask that is confidently wrong. + if let Some(coverage) = layer.coverage.as_ref() { + let _ = writeln!(out, "coverage = {}", coverage.to_text()); + } } /// Write a brush layer's strokes, one line each. @@ -1150,6 +1170,7 @@ struct PartialMask { morphology: Morphology, morph_radius: f32, refine: f32, + coverage: Option, strokes: Vec, params: Vec<(String, String, f32)>, } @@ -1181,6 +1202,7 @@ impl PartialMask { morphology: Morphology::default(), morph_radius: 0.0, refine: 0.0, + coverage: None, strokes: Vec::new(), params: Vec::new(), } @@ -1217,6 +1239,19 @@ impl PartialMask { "angle" => self.angle = value.parse().unwrap_or(0.0), "width" => self.width = value.parse().unwrap_or(0.0), "feather" => self.feather = value.parse().unwrap_or(0.0), + // A cache, so an unreadable one is dropped rather than refused: + // the layer still says what it selects, and the worst a `None` + // here costs is a model run. Refusing the layer over it would + // throw away an edit to protect a copy of something reproducible. + "coverage" => { + self.coverage = Coverage::parse(value); + if self.coverage.is_none() { + log::warn!( + "sidecar: mask {} has unreadable coverage; it will need the model run", + self.id + ); + } + } // Appended rather than assigned: a brush layer is a list of these, // and the file's line order is the order they were painted in. "stroke" => self.strokes.extend(parse_stroke(value)), @@ -1321,6 +1356,16 @@ impl PartialMask { layer.morphology = self.morphology; layer.morph_radius = self.morph_radius; layer.refine = self.refine; + // Only where there is a model behind the layer to have produced it. A + // gradient or a brush that arrived carrying one is a file that has + // been hand-edited or written by a build that means something else by + // the key, and honouring it would upload a raster nothing samples. + layer.coverage = match layer.source { + MaskSource::Subject { .. } | MaskSource::Category { .. } => { + self.coverage.map(std::sync::Arc::new) + } + _ => None, + }; for (op, param, value) in &self.params { // `ParamId` holds a `&'static str` and this one came off disk, so // it is matched against the descriptors and the *static* id is diff --git a/core/dr-pipeline/tests/mask_sidecar.rs b/core/dr-pipeline/tests/mask_sidecar.rs index 07d08bc..d0dbec4 100644 --- a/core/dr-pipeline/tests/mask_sidecar.rs +++ b/core/dr-pipeline/tests/mask_sidecar.rs @@ -778,3 +778,349 @@ fn a_category_from_another_run_is_stale() { assert!(layer.is_stale(2), "a different run must invalidate it"); assert!(!layer.is_stale(1), "the run it was built against must not"); } + +// --------------------------------------------------------------------------- +// Stored coverage (dr_pipeline::coverage) +// --------------------------------------------------------------------------- +// +// A subject or a category is stored as *identity* — which run, which instance, +// which category — and identity alone is only enough while the run is still in +// memory. These pin down the raster that goes beside it, which is what lets a +// reopened photograph and a batch export render the layer without a model. + +use dr_pipeline::coverage::{Coverage, RENDERED_LEVELS}; +use std::sync::Arc; + +const PROXY: (usize, usize) = (96, 64); + +fn subject(signature: u64, index: u32) -> MaskSource { + MaskSource::Subject { + signature, + index, + class: "dog".into(), + score: 0.94, + } +} + +/// A soft-edged disc, which is the shape a model actually hands out: a coarse +/// sigmoid with a shoulder several pixels wide. +fn a_disc() -> Vec { + let (w, h) = PROXY; + let mut values = vec![0u8; w * h]; + for y in 0..h { + for x in 0..w { + let dx = (x as f32 - 40.0) / 20.0; + let dy = (y as f32 - 30.0) / 20.0; + let r = (dx * dx + dy * dy).sqrt(); + values[y * w + x] = ((1.0 / (1.0 + ((r - 1.0) * 4.0).exp())) * 255.0) as u8; + } + } + values +} + +/// What the renderer would make of a coverage: the distance transform reads +/// `>= 128` and nothing else, so this is the whole of the information a stored +/// mask has to preserve. +fn inside(values: &[u8]) -> Vec { + values.iter().map(|&v| v >= 128).collect() +} + +fn with_coverage(id: &str, source: MaskSource, values: &[u8]) -> MaskLayer { + let mut layer = MaskLayer::new(id, source); + layer.set_param("exposure", ParamId("exposure"), 0.75); + layer.coverage = Some(Arc::new( + Coverage::encode(values, PROXY.0, PROXY.1, RENDERED_LEVELS).expect("a disc should encode"), + )); + layer +} + +/// The bug this whole thing exists for: without the raster, reopening the +/// photograph gave the layer nothing to be, and the local adjustment was +/// silently absent until somebody pressed "find subjects". +#[test] +fn a_subjects_coverage_survives_a_round_trip() { + let values = a_disc(); + let mut graph = EditGraph::default_chain(); + graph + .masks_mut() + .push(with_coverage("m1", subject(0x1234, 2), &values)); + + let restored = round_trip(&graph); + let layer = &restored.masks().layers()[0]; + + assert_eq!( + layer.source, + subject(0x1234, 2), + "the identity is still there" + ); + let coverage = layer.coverage.as_ref().expect("the pixels came back too"); + assert_eq!((coverage.width(), coverage.height()), PROXY); + assert_eq!( + inside(&coverage.decode()), + inside(&values), + "every pixel must fall on the same side of the threshold" + ); +} + +#[test] +fn a_categorys_coverage_survives_a_round_trip() { + let values = a_disc(); + let source = MaskSource::Category { + signature: 0x5678, + name: "sky".into(), + }; + let mut graph = EditGraph::default_chain(); + graph + .masks_mut() + .push(with_coverage("m1", source.clone(), &values)); + + let restored = round_trip(&graph); + let layer = &restored.masks().layers()[0]; + assert_eq!(layer.source, source); + assert_eq!( + inside(&layer.coverage.as_ref().expect("coverage").decode()), + inside(&values) + ); +} + +/// The property that lets a caller skip an upload by comparing content — now +/// with several kilobytes of run-length payload in the file. An encoder that +/// re-coded the same raster differently on the way out would put a spurious +/// upload on every save. +#[test] +fn a_stored_coverage_writes_the_same_bytes_every_time() { + let mut graph = EditGraph::default_chain(); + graph + .masks_mut() + .push(with_coverage("m1", subject(1, 0), &a_disc())); + + let mut sidecar = Sidecar::new(); + sidecar.put(Version::from_graph("default", "Default", &graph)); + let once = sidecar.to_text(); + let twice = Sidecar::parse(&once).expect("reparse").to_text(); + assert_eq!(once, twice); +} + +/// The line goes last in its block, and that is a claim about reading the file +/// by hand: it is thousands of characters against a dozen everywhere else, and +/// a sidecar is what somebody opens when an edit has gone wrong (ARCH §6.12). +#[test] +fn the_coverage_line_comes_after_everything_a_human_is_looking_for() { + let mut graph = EditGraph::default_chain(); + graph + .masks_mut() + .push(with_coverage("m1", subject(1, 0), &a_disc())); + let mut sidecar = Sidecar::new(); + sidecar.put(Version::from_graph("default", "Default", &graph)); + let text = sidecar.to_text(); + + let block = text.split("[mask ").nth(1).expect("a mask block"); + let keys: Vec<&str> = block + .lines() + .filter_map(|l| l.split_once(" = ")) + .map(|(k, _)| k) + .collect(); + assert_eq!( + keys.last(), + Some(&"coverage"), + "coverage should be the last key in the block: {keys:?}" + ); + assert!( + keys.contains(&"class") && keys.contains(&"exposure.exposure"), + "and everything else should still be above it: {keys:?}" + ); + assert_eq!( + block.lines().filter(|l| l.starts_with("coverage")).count(), + 1, + "one line, because a node is a line and the merge is key-wise" + ); +} + +/// Version skew, backwards: a file written before any of this existed. +/// +/// The layer must load and behave exactly as it did then — which is to say it +/// needs the model run — rather than reading as a mask with no pixels. +#[test] +fn a_sidecar_written_before_coverage_existed_still_loads() { + let text = "\ +drsc 1 + +[version default] +name = Default +default = 1 +revision = 3 + +[mask default m1] +name = Dog +source = subject +signature = 4660 +index = 2 +class = dog +score = 0.94 +exposure.exposure = 0.75 +"; + let sidecar = Sidecar::parse(text).expect("an old file must still parse"); + let mut graph = EditGraph::default_chain(); + sidecar + .versions + .get("default") + .expect("version") + .apply(&mut graph) + .expect_no_film(); + + let layer = &graph.masks().layers()[0]; + assert_eq!(layer.source, subject(4660, 2)); + assert_eq!(layer.name, "Dog"); + assert!( + layer.coverage.is_none(), + "absent means absent, not an empty mask" + ); +} + +/// Version skew, forwards: a coverage this build cannot make sense of. +/// +/// The stand-in for a payload written by a build that means something else by +/// the key. It costs the pixels — a model run — and must not cost the layer, +/// because the layer is the edit and the raster is a cache of it. +#[test] +fn an_unreadable_coverage_costs_the_pixels_and_not_the_layer() { + let text = "\ +drsc 1 + +[version default] +name = Default +default = 1 + +[mask default m1] +name = Dog +source = subject +signature = 4660 +index = 2 +class = dog +score = 0.94 +exposure.exposure = 0.75 +coverage = 96 64 999 not-a-payload +"; + let sidecar = Sidecar::parse(text).expect("parse"); + let mut graph = EditGraph::default_chain(); + sidecar + .versions + .get("default") + .expect("version") + .apply(&mut graph) + .expect_no_film(); + + let layer = &graph.masks().layers()[0]; + assert_eq!(layer.source, subject(4660, 2)); + assert_eq!(layer.name, "Dog"); + assert!(layer.coverage.is_none()); + assert_eq!( + layer + .ops + .iter() + .find(|o| o.descriptor().id.0 == "exposure") + .map(|o| o.param(ParamId("exposure"))), + Some(0.75), + "the adjustment is the thing that must not be lost" + ); +} + +/// A raster only means anything against a source a model produced. A gradient +/// carrying one is a hand-edited or mis-written file, and honouring it would +/// upload a buffer nothing samples. +#[test] +fn coverage_is_not_loaded_onto_a_source_with_no_model_behind_it() { + let payload = Coverage::encode(&a_disc(), PROXY.0, PROXY.1, RENDERED_LEVELS) + .expect("encode") + .to_text(); + let text = format!( + "drsc 1\n\n[version default]\nname = Default\ndefault = 1\n\n\ + [mask default m1]\nsource = radial\ncentre = 0.5 0.5\nradii = 0.25 0.25\n\ + coverage = {payload}\n" + ); + let sidecar = Sidecar::parse(&text).expect("parse"); + let mut graph = EditGraph::default_chain(); + sidecar + .versions + .get("default") + .expect("version") + .apply(&mut graph) + .expect_no_film(); + + let layer = &graph.masks().layers()[0]; + assert!(matches!(layer.source, MaskSource::Radial { .. })); + assert!(layer.coverage.is_none()); +} + +/// TRACES: FR-NC-9 +/// One device has run the model and the other has not. That is the same edit. +/// +/// The merge decides "did this device change the layer" by comparing layers, +/// so a cached raster taking part would make a photograph opened on the phone +/// conflict with itself on the desktop — and, with `remote_wins`, resolve the +/// conflict by discarding the only copy of the pixels. +#[test] +fn a_coverage_one_device_has_and_the_other_lacks_is_not_a_conflict() { + let layer = |with: bool| { + let mut l = lit("m1", &[1], 1.0); + l.source = subject(7, 0); + if with { + l.coverage = Some(Arc::new( + Coverage::encode(&a_disc(), PROXY.0, PROXY.1, RENDERED_LEVELS).expect("encode"), + )); + } + l + }; + + let base = version_with("default", 1, |g| { + g.masks_mut().push(layer(false)); + }); + let mut ours = version_with("default", 2, |g| { + g.masks_mut().push(layer(true)); + }); + let theirs = version_with("default", 9, |g| { + g.masks_mut().push(layer(false)); + }); + + let conflicts = ours.merge(&theirs, Some(&base)); + assert!( + conflicts.is_empty(), + "running the model is not an edit: {conflicts:?}" + ); + assert!( + ours.masks.get("m1").expect("layer").coverage.is_some(), + "and the side that has the pixels keeps them" + ); +} + +/// The other half of the same claim: a real edit is still a conflict when both +/// sides also happen to hold coverage. +#[test] +fn a_real_edit_is_still_a_conflict_with_coverage_present() { + let stored = || { + Some(Arc::new( + Coverage::encode(&a_disc(), PROXY.0, PROXY.1, RENDERED_LEVELS).expect("encode"), + )) + }; + let layer = |ev: f32| { + let mut l = lit("m1", &[1], ev); + l.source = subject(7, 0); + l.coverage = stored(); + l + }; + + let base = version_with("default", 1, |g| { + g.masks_mut().push(layer(0.5)); + }); + let mut ours = version_with("default", 2, |g| { + g.masks_mut().push(layer(1.0)); + }); + let theirs = version_with("default", 9, |g| { + g.masks_mut().push(layer(-1.0)); + }); + + assert_eq!( + ours.merge(&theirs, Some(&base)), + vec![("mask".to_string(), "m1".to_string())] + ); +} diff --git a/docs/traceability.md b/docs/traceability.md index 5028e9f..e1d9b9d 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 331 | -| TRACES tags found | 1048 | +| Source files scanned | 332 | +| TRACES tags found | 1058 | | Requirements defined | 179 | | Requirements covered | 126 | | **Coverage** | **70.4%** (126/179) | @@ -36,57 +36,57 @@ _None._ | 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), [`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:206`](../core/dr-types/src/settings.rs#L206), [`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:1303`](../ui/dr-ui/src/lib.rs#L1303), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1169`](../ui/dr-ui/ui/library.slint#L1169), [`ui/dr-ui/ui/library.slint:899`](../ui/dr-ui/ui/library.slint#L899), [`ui/dr-ui/ui/library.slint:953`](../ui/dr-ui/ui/library.slint#L953) | | 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:1303`](../ui/dr-ui/src/lib.rs#L1303), [`ui/dr-ui/src/library.rs:175`](../ui/dr-ui/src/library.rs#L175), [`ui/dr-ui/src/library.rs:2491`](../ui/dr-ui/src/library.rs#L2491), [`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-15 | [`core/dr-catalog/src/schema.rs:753`](../core/dr-catalog/src/schema.rs#L753), [`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:563`](../core/dr-sync/src/scan.rs#L563), [`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), [`core/dr-types/src/place.rs:91`](../core/dr-types/src/place.rs#L91), [`ui/dr-ui/src/collections_ui.rs:1283`](../ui/dr-ui/src/collections_ui.rs#L1283), [`ui/dr-ui/src/collections_ui.rs:2189`](../ui/dr-ui/src/collections_ui.rs#L2189), [`ui/dr-ui/src/library.rs:175`](../ui/dr-ui/src/library.rs#L175), [`ui/dr-ui/src/library.rs:192`](../ui/dr-ui/src/library.rs#L192), [`ui/dr-ui/src/library.rs:241`](../ui/dr-ui/src/library.rs#L241), [`ui/dr-ui/src/library.rs:4137`](../ui/dr-ui/src/library.rs#L4137), [`ui/dr-ui/src/library.rs:4171`](../ui/dr-ui/src/library.rs#L4171), [`ui/dr-ui/src/library_ui.rs:238`](../ui/dr-ui/src/library_ui.rs#L238), [`ui/dr-ui/src/library_ui.rs:838`](../ui/dr-ui/src/library_ui.rs#L838), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:621`](../ui/dr-ui/ui/collections.slint#L621) | -| FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:56`](../core/dr-types/src/lib.rs#L56), [`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-12 | [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120) | +| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:753`](../core/dr-catalog/src/schema.rs#L753), [`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:563`](../core/dr-sync/src/scan.rs#L563), [`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:1283`](../ui/dr-ui/src/collections_ui.rs#L1283), [`ui/dr-ui/src/collections_ui.rs:2189`](../ui/dr-ui/src/collections_ui.rs#L2189), [`ui/dr-ui/src/library.rs:175`](../ui/dr-ui/src/library.rs#L175), [`ui/dr-ui/src/library.rs:192`](../ui/dr-ui/src/library.rs#L192), [`ui/dr-ui/src/library.rs:241`](../ui/dr-ui/src/library.rs#L241), [`ui/dr-ui/src/library.rs:4117`](../ui/dr-ui/src/library.rs#L4117), [`ui/dr-ui/src/library.rs:4151`](../ui/dr-ui/src/library.rs#L4151), [`ui/dr-ui/src/library_ui.rs:195`](../ui/dr-ui/src/library_ui.rs#L195), [`ui/dr-ui/src/library_ui.rs:784`](../ui/dr-ui/src/library_ui.rs#L784), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:621`](../ui/dr-ui/ui/collections.slint#L621) | +| 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) | | 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:2984`](../ui/dr-ui/src/library.rs#L2984), [`ui/dr-ui/src/library.rs:3580`](../ui/dr-ui/src/library.rs#L3580), [`ui/dr-ui/src/library_ui.rs:177`](../ui/dr-ui/src/library_ui.rs#L177), [`ui/dr-ui/src/library_ui.rs:4221`](../ui/dr-ui/src/library_ui.rs#L4221), [`ui/dr-ui/src/library_ui.rs:5720`](../ui/dr-ui/src/library_ui.rs#L5720), [`ui/dr-ui/ui/app.slint:428`](../ui/dr-ui/ui/app.slint#L428), [`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:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:222`](../ui/dr-ui/src/library.rs#L222), [`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:1171`](../core/dr-catalog/src/schema.rs#L1171), [`core/dr-catalog/src/schema.rs:668`](../core/dr-catalog/src/schema.rs#L668), [`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:1579`](../ui/dr-ui/src/collections_ui.rs#L1579), [`ui/dr-ui/src/collections_ui.rs:1685`](../ui/dr-ui/src/collections_ui.rs#L1685), [`ui/dr-ui/src/collections_ui.rs:1734`](../ui/dr-ui/src/collections_ui.rs#L1734), [`ui/dr-ui/src/collections_ui.rs:238`](../ui/dr-ui/src/collections_ui.rs#L238), [`ui/dr-ui/src/collections_ui.rs:335`](../ui/dr-ui/src/collections_ui.rs#L335), [`ui/dr-ui/src/collections_ui.rs:87`](../ui/dr-ui/src/collections_ui.rs#L87), [`ui/dr-ui/src/library.rs:4165`](../ui/dr-ui/src/library.rs#L4165), [`ui/dr-ui/src/library_ui.rs:659`](../ui/dr-ui/src/library_ui.rs#L659), [`ui/dr-ui/src/library_ui.rs:6953`](../ui/dr-ui/src/library_ui.rs#L6953), [`ui/dr-ui/src/library_ui.rs:6964`](../ui/dr-ui/src/library_ui.rs#L6964), [`ui/dr-ui/src/library_ui.rs:6977`](../ui/dr-ui/src/library_ui.rs#L6977), [`ui/dr-ui/src/library_ui.rs:6992`](../ui/dr-ui/src/library_ui.rs#L6992), [`ui/dr-ui/src/library_ui.rs:7001`](../ui/dr-ui/src/library_ui.rs#L7001), [`ui/dr-ui/ui/app.slint:333`](../ui/dr-ui/ui/app.slint#L333), [`ui/dr-ui/ui/app.slint:547`](../ui/dr-ui/ui/app.slint#L547), [`ui/dr-ui/ui/library.slint:1275`](../ui/dr-ui/ui/library.slint#L1275), [`ui/dr-ui/ui/library.slint:1299`](../ui/dr-ui/ui/library.slint#L1299), [`ui/dr-ui/ui/library.slint:1352`](../ui/dr-ui/ui/library.slint#L1352), [`ui/dr-ui/ui/library.slint:19`](../ui/dr-ui/ui/library.slint#L19), [`ui/dr-ui/ui/library.slint:3992`](../ui/dr-ui/ui/library.slint#L3992) | +| 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:1171`](../core/dr-catalog/src/schema.rs#L1171), [`core/dr-catalog/src/schema.rs:668`](../core/dr-catalog/src/schema.rs#L668), [`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:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/collections_ui.rs:1579`](../ui/dr-ui/src/collections_ui.rs#L1579), [`ui/dr-ui/src/collections_ui.rs:1685`](../ui/dr-ui/src/collections_ui.rs#L1685), [`ui/dr-ui/src/collections_ui.rs:1734`](../ui/dr-ui/src/collections_ui.rs#L1734), [`ui/dr-ui/src/collections_ui.rs:238`](../ui/dr-ui/src/collections_ui.rs#L238), [`ui/dr-ui/src/collections_ui.rs:335`](../ui/dr-ui/src/collections_ui.rs#L335), [`ui/dr-ui/src/collections_ui.rs:87`](../ui/dr-ui/src/collections_ui.rs#L87), [`ui/dr-ui/src/library.rs:4165`](../ui/dr-ui/src/library.rs#L4165), [`ui/dr-ui/src/library_ui.rs:659`](../ui/dr-ui/src/library_ui.rs#L659), [`ui/dr-ui/src/library_ui.rs:6953`](../ui/dr-ui/src/library_ui.rs#L6953), [`ui/dr-ui/src/library_ui.rs:6964`](../ui/dr-ui/src/library_ui.rs#L6964), [`ui/dr-ui/src/library_ui.rs:6977`](../ui/dr-ui/src/library_ui.rs#L6977), [`ui/dr-ui/src/library_ui.rs:6992`](../ui/dr-ui/src/library_ui.rs#L6992), [`ui/dr-ui/src/library_ui.rs:7001`](../ui/dr-ui/src/library_ui.rs#L7001), [`ui/dr-ui/ui/app.slint:333`](../ui/dr-ui/ui/app.slint#L333), [`ui/dr-ui/ui/app.slint:547`](../ui/dr-ui/ui/app.slint#L547), [`ui/dr-ui/ui/library.slint:1275`](../ui/dr-ui/ui/library.slint#L1275), [`ui/dr-ui/ui/library.slint:1299`](../ui/dr-ui/ui/library.slint#L1299), [`ui/dr-ui/ui/library.slint:1352`](../ui/dr-ui/ui/library.slint#L1352), [`ui/dr-ui/ui/library.slint:19`](../ui/dr-ui/ui/library.slint#L19), [`ui/dr-ui/ui/library.slint:3992`](../ui/dr-ui/ui/library.slint#L3992) | | 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:668`](../core/dr-catalog/src/schema.rs#L668), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:64`](../core/dr-types/src/settings.rs#L64), [`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:246`](../ui/dr-ui/src/library.rs#L246), [`ui/dr-ui/src/library.rs:4558`](../ui/dr-ui/src/library.rs#L4558), [`ui/dr-ui/src/library_ui.rs:342`](../ui/dr-ui/src/library_ui.rs#L342), [`ui/dr-ui/src/library_ui.rs:416`](../ui/dr-ui/src/library_ui.rs#L416), [`ui/dr-ui/src/library_ui.rs:5688`](../ui/dr-ui/src/library_ui.rs#L5688), [`ui/dr-ui/src/library_ui.rs:5741`](../ui/dr-ui/src/library_ui.rs#L5741), [`ui/dr-ui/src/library_ui.rs:7093`](../ui/dr-ui/src/library_ui.rs#L7093), [`ui/dr-ui/ui/app.slint:345`](../ui/dr-ui/ui/app.slint#L345), [`ui/dr-ui/ui/app.slint:547`](../ui/dr-ui/ui/app.slint#L547), [`ui/dr-ui/ui/app.slint:850`](../ui/dr-ui/ui/app.slint#L850), [`ui/dr-ui/ui/library.slint:110`](../ui/dr-ui/ui/library.slint#L110), [`ui/dr-ui/ui/library.slint:1462`](../ui/dr-ui/ui/library.slint#L1462), [`ui/dr-ui/ui/library.slint:3992`](../ui/dr-ui/ui/library.slint#L3992), [`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:1073`](../ui/dr-ui/src/collections_ui.rs#L1073), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1598`](../ui/dr-ui/src/collections_ui.rs#L1598), [`ui/dr-ui/src/collections_ui.rs:1848`](../ui/dr-ui/src/collections_ui.rs#L1848), [`ui/dr-ui/src/collections_ui.rs:1913`](../ui/dr-ui/src/collections_ui.rs#L1913), [`ui/dr-ui/src/collections_ui.rs:1973`](../ui/dr-ui/src/collections_ui.rs#L1973), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/collections_ui.rs:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:365`](../ui/dr-ui/src/collections_ui.rs#L365), [`ui/dr-ui/src/collections_ui.rs:569`](../ui/dr-ui/src/collections_ui.rs#L569), [`ui/dr-ui/src/collections_ui.rs:965`](../ui/dr-ui/src/collections_ui.rs#L965), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:4363`](../ui/dr-ui/src/library.rs#L4363), [`ui/dr-ui/src/library.rs:4389`](../ui/dr-ui/src/library.rs#L4389), [`ui/dr-ui/src/library.rs:4444`](../ui/dr-ui/src/library.rs#L4444), [`ui/dr-ui/src/library_ui.rs:2467`](../ui/dr-ui/src/library_ui.rs#L2467), [`ui/dr-ui/src/library_ui.rs:3863`](../ui/dr-ui/src/library_ui.rs#L3863), [`ui/dr-ui/src/library_ui.rs:4590`](../ui/dr-ui/src/library_ui.rs#L4590), [`ui/dr-ui/ui/app.slint:336`](../ui/dr-ui/ui/app.slint#L336), [`ui/dr-ui/ui/app.slint:542`](../ui/dr-ui/ui/app.slint#L542), [`ui/dr-ui/ui/app.slint:571`](../ui/dr-ui/ui/app.slint#L571), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:1242`](../ui/dr-ui/ui/library.slint#L1242), [`ui/dr-ui/ui/library.slint:2777`](../ui/dr-ui/ui/library.slint#L2777), [`ui/dr-ui/ui/library.slint:3970`](../ui/dr-ui/ui/library.slint#L3970), [`ui/dr-ui/ui/library.slint:918`](../ui/dr-ui/ui/library.slint#L918) | -| 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:1669`](../core/dr-pipeline/src/sidecar.rs#L1669), [`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:4046`](../ui/dr-ui/src/develop.rs#L4046), [`ui/dr-ui/src/develop.rs:4075`](../ui/dr-ui/src/develop.rs#L4075), [`ui/dr-ui/src/export.rs:799`](../ui/dr-ui/src/export.rs#L799), [`ui/dr-ui/src/lib.rs:1081`](../ui/dr-ui/src/lib.rs#L1081), [`ui/dr-ui/src/lib.rs:1597`](../ui/dr-ui/src/lib.rs#L1597), [`ui/dr-ui/src/lib.rs:2087`](../ui/dr-ui/src/lib.rs#L2087), [`ui/dr-ui/src/lib.rs:2222`](../ui/dr-ui/src/lib.rs#L2222), [`ui/dr-ui/src/lib.rs:633`](../ui/dr-ui/src/lib.rs#L633), [`ui/dr-ui/src/library.rs:1903`](../ui/dr-ui/src/library.rs#L1903), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:539`](../ui/dr-ui/src/library.rs#L539), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library_ui.rs:5313`](../ui/dr-ui/src/library_ui.rs#L5313), [`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:725`](../core/dr-catalog/src/schema.rs#L725), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`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:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:175`](../core/dr-sync/src/scan.rs#L175), [`core/dr-sync/src/scan.rs:464`](../core/dr-sync/src/scan.rs#L464), [`core/dr-types/src/lib.rs:120`](../core/dr-types/src/lib.rs#L120), [`ui/dr-ui/src/develop.rs:3433`](../ui/dr-ui/src/develop.rs#L3433), [`ui/dr-ui/src/develop.rs:3476`](../ui/dr-ui/src/develop.rs#L3476), [`ui/dr-ui/src/library.rs:1222`](../ui/dr-ui/src/library.rs#L1222), [`ui/dr-ui/src/library.rs:1275`](../ui/dr-ui/src/library.rs#L1275), [`ui/dr-ui/src/library.rs:1306`](../ui/dr-ui/src/library.rs#L1306), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library.rs:161`](../ui/dr-ui/src/library.rs#L161), [`ui/dr-ui/src/library.rs:1863`](../ui/dr-ui/src/library.rs#L1863), [`ui/dr-ui/src/library.rs:1939`](../ui/dr-ui/src/library.rs#L1939), [`ui/dr-ui/src/library.rs:266`](../ui/dr-ui/src/library.rs#L266), [`ui/dr-ui/src/library.rs:4666`](../ui/dr-ui/src/library.rs#L4666), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:807`](../ui/dr-ui/src/library.rs#L807), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1241`](../ui/dr-ui/src/library_ui.rs#L1241), [`ui/dr-ui/src/library_ui.rs:1832`](../ui/dr-ui/src/library_ui.rs#L1832), [`ui/dr-ui/src/library_ui.rs:1876`](../ui/dr-ui/src/library_ui.rs#L1876), [`ui/dr-ui/src/library_ui.rs:1892`](../ui/dr-ui/src/library_ui.rs#L1892), [`ui/dr-ui/src/library_ui.rs:1986`](../ui/dr-ui/src/library_ui.rs#L1986), [`ui/dr-ui/src/library_ui.rs:237`](../ui/dr-ui/src/library_ui.rs#L237), [`ui/dr-ui/src/library_ui.rs:2706`](../ui/dr-ui/src/library_ui.rs#L2706), [`ui/dr-ui/src/library_ui.rs:270`](../ui/dr-ui/src/library_ui.rs#L270), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:3144`](../ui/dr-ui/src/library_ui.rs#L3144), [`ui/dr-ui/src/library_ui.rs:3366`](../ui/dr-ui/src/library_ui.rs#L3366), [`ui/dr-ui/src/library_ui.rs:3644`](../ui/dr-ui/src/library_ui.rs#L3644), [`ui/dr-ui/src/library_ui.rs:3732`](../ui/dr-ui/src/library_ui.rs#L3732), [`ui/dr-ui/src/library_ui.rs:3914`](../ui/dr-ui/src/library_ui.rs#L3914), [`ui/dr-ui/src/library_ui.rs:4028`](../ui/dr-ui/src/library_ui.rs#L4028), [`ui/dr-ui/src/library_ui.rs:469`](../ui/dr-ui/src/library_ui.rs#L469), [`ui/dr-ui/src/library_ui.rs:532`](../ui/dr-ui/src/library_ui.rs#L532), [`ui/dr-ui/src/library_ui.rs:5786`](../ui/dr-ui/src/library_ui.rs#L5786), [`ui/dr-ui/src/library_ui.rs:5901`](../ui/dr-ui/src/library_ui.rs#L5901), [`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), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | +| FR-CAT-8 | [`core/dr-pipeline/src/coverage.rs:1`](../core/dr-pipeline/src/coverage.rs#L1), [`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:1048`](../core/dr-pipeline/src/sidecar.rs#L1048), [`core/dr-pipeline/src/sidecar.rs:1714`](../core/dr-pipeline/src/sidecar.rs#L1714), [`core/dr-pipeline/src/sidecar.rs:94`](../core/dr-pipeline/src/sidecar.rs#L94), [`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:1922`](../ui/dr-ui/src/develop.rs#L1922), [`ui/dr-ui/src/develop.rs:4157`](../ui/dr-ui/src/develop.rs#L4157), [`ui/dr-ui/src/develop.rs:4186`](../ui/dr-ui/src/develop.rs#L4186), [`ui/dr-ui/src/develop.rs:4938`](../ui/dr-ui/src/develop.rs#L4938), [`ui/dr-ui/src/develop.rs:4992`](../ui/dr-ui/src/develop.rs#L4992), [`ui/dr-ui/src/develop.rs:5094`](../ui/dr-ui/src/develop.rs#L5094), [`ui/dr-ui/src/export.rs:799`](../ui/dr-ui/src/export.rs#L799), [`ui/dr-ui/src/lib.rs:1081`](../ui/dr-ui/src/lib.rs#L1081), [`ui/dr-ui/src/lib.rs:1597`](../ui/dr-ui/src/lib.rs#L1597), [`ui/dr-ui/src/lib.rs:2087`](../ui/dr-ui/src/lib.rs#L2087), [`ui/dr-ui/src/lib.rs:2222`](../ui/dr-ui/src/lib.rs#L2222), [`ui/dr-ui/src/lib.rs:633`](../ui/dr-ui/src/lib.rs#L633), [`ui/dr-ui/src/library.rs:1903`](../ui/dr-ui/src/library.rs#L1903), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:539`](../ui/dr-ui/src/library.rs#L539), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library_ui.rs:5313`](../ui/dr-ui/src/library_ui.rs#L5313), [`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:725`](../core/dr-catalog/src/schema.rs#L725), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`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:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:175`](../core/dr-sync/src/scan.rs#L175), [`core/dr-sync/src/scan.rs:464`](../core/dr-sync/src/scan.rs#L464), [`core/dr-types/src/lib.rs:120`](../core/dr-types/src/lib.rs#L120), [`ui/dr-ui/src/develop.rs:3544`](../ui/dr-ui/src/develop.rs#L3544), [`ui/dr-ui/src/develop.rs:3587`](../ui/dr-ui/src/develop.rs#L3587), [`ui/dr-ui/src/library.rs:1222`](../ui/dr-ui/src/library.rs#L1222), [`ui/dr-ui/src/library.rs:1275`](../ui/dr-ui/src/library.rs#L1275), [`ui/dr-ui/src/library.rs:1306`](../ui/dr-ui/src/library.rs#L1306), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library.rs:161`](../ui/dr-ui/src/library.rs#L161), [`ui/dr-ui/src/library.rs:1863`](../ui/dr-ui/src/library.rs#L1863), [`ui/dr-ui/src/library.rs:1939`](../ui/dr-ui/src/library.rs#L1939), [`ui/dr-ui/src/library.rs:266`](../ui/dr-ui/src/library.rs#L266), [`ui/dr-ui/src/library.rs:4666`](../ui/dr-ui/src/library.rs#L4666), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:807`](../ui/dr-ui/src/library.rs#L807), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1241`](../ui/dr-ui/src/library_ui.rs#L1241), [`ui/dr-ui/src/library_ui.rs:1832`](../ui/dr-ui/src/library_ui.rs#L1832), [`ui/dr-ui/src/library_ui.rs:1876`](../ui/dr-ui/src/library_ui.rs#L1876), [`ui/dr-ui/src/library_ui.rs:1892`](../ui/dr-ui/src/library_ui.rs#L1892), [`ui/dr-ui/src/library_ui.rs:1986`](../ui/dr-ui/src/library_ui.rs#L1986), [`ui/dr-ui/src/library_ui.rs:237`](../ui/dr-ui/src/library_ui.rs#L237), [`ui/dr-ui/src/library_ui.rs:2706`](../ui/dr-ui/src/library_ui.rs#L2706), [`ui/dr-ui/src/library_ui.rs:270`](../ui/dr-ui/src/library_ui.rs#L270), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:3144`](../ui/dr-ui/src/library_ui.rs#L3144), [`ui/dr-ui/src/library_ui.rs:3366`](../ui/dr-ui/src/library_ui.rs#L3366), [`ui/dr-ui/src/library_ui.rs:3644`](../ui/dr-ui/src/library_ui.rs#L3644), [`ui/dr-ui/src/library_ui.rs:3732`](../ui/dr-ui/src/library_ui.rs#L3732), [`ui/dr-ui/src/library_ui.rs:3914`](../ui/dr-ui/src/library_ui.rs#L3914), [`ui/dr-ui/src/library_ui.rs:4028`](../ui/dr-ui/src/library_ui.rs#L4028), [`ui/dr-ui/src/library_ui.rs:469`](../ui/dr-ui/src/library_ui.rs#L469), [`ui/dr-ui/src/library_ui.rs:532`](../ui/dr-ui/src/library_ui.rs#L532), [`ui/dr-ui/src/library_ui.rs:5786`](../ui/dr-ui/src/library_ui.rs#L5786), [`ui/dr-ui/src/library_ui.rs:5901`](../ui/dr-ui/src/library_ui.rs#L5901), [`ui/dr-ui/src/presets.rs:453`](../ui/dr-ui/src/presets.rs#L453), [`ui/dr-ui/src/presets.rs:465`](../ui/dr-ui/src/presets.rs#L465), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.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:376`](../core/dr-catalog/src/schema.rs#L376), [`core/dr-catalog/src/schema.rs:554`](../core/dr-catalog/src/schema.rs#L554), [`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), [`core/dr-types/src/settings.rs:117`](../core/dr-types/src/settings.rs#L117), [`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:195`](../ui/dr-ui/src/develop.rs#L195), [`ui/dr-ui/src/develop.rs:2044`](../ui/dr-ui/src/develop.rs#L2044), [`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:2194`](../ui/dr-ui/src/lib.rs#L2194), [`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:376`](../core/dr-catalog/src/schema.rs#L376), [`core/dr-catalog/src/schema.rs:554`](../core/dr-catalog/src/schema.rs#L554), [`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), [`core/dr-types/src/settings.rs:117`](../core/dr-types/src/settings.rs#L117), [`ui/dr-ui/src/develop.rs:121`](../ui/dr-ui/src/develop.rs#L121), [`ui/dr-ui/src/develop.rs:130`](../ui/dr-ui/src/develop.rs#L130), [`ui/dr-ui/src/develop.rs:196`](../ui/dr-ui/src/develop.rs#L196), [`ui/dr-ui/src/develop.rs:2154`](../ui/dr-ui/src/develop.rs#L2154), [`ui/dr-ui/src/develop.rs:689`](../ui/dr-ui/src/develop.rs#L689), [`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:2194`](../ui/dr-ui/src/lib.rs#L2194), [`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:554`](../core/dr-catalog/src/schema.rs#L554), [`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:287`](../ui/dr-ui/src/library.rs#L287), [`ui/dr-ui/src/library.rs:317`](../ui/dr-ui/src/library.rs#L317), [`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:376`](../core/dr-catalog/src/schema.rs#L376), [`core/dr-catalog/src/schema.rs:554`](../core/dr-catalog/src/schema.rs#L554), [`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-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/raw_histogram.rs:129`](../core/dr-gpu/src/raw_histogram.rs#L129), [`core/dr-gpu/src/raw_histogram.rs:1`](../core/dr-gpu/src/raw_histogram.rs#L1), [`core/dr-gpu/src/raw_histogram.rs:272`](../core/dr-gpu/src/raw_histogram.rs#L272), [`core/dr-gpu/src/raw_histogram.rs:407`](../core/dr-gpu/src/raw_histogram.rs#L407), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`core/dr-gpu/src/shaders/raw_histogram.wgsl:1`](../core/dr-gpu/src/shaders/raw_histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:3139`](../ui/dr-ui/src/develop.rs#L3139), [`ui/dr-ui/src/develop.rs:3152`](../ui/dr-ui/src/develop.rs#L3152), [`ui/dr-ui/src/develop.rs:3197`](../ui/dr-ui/src/develop.rs#L3197), [`ui/dr-ui/src/develop.rs:3208`](../ui/dr-ui/src/develop.rs#L3208), [`ui/dr-ui/src/develop.rs:3214`](../ui/dr-ui/src/develop.rs#L3214), [`ui/dr-ui/src/develop.rs:3231`](../ui/dr-ui/src/develop.rs#L3231), [`ui/dr-ui/src/develop.rs:6173`](../ui/dr-ui/src/develop.rs#L6173), [`ui/dr-ui/src/develop.rs:6237`](../ui/dr-ui/src/develop.rs#L6237), [`ui/dr-ui/src/develop.rs:6260`](../ui/dr-ui/src/develop.rs#L6260), [`ui/dr-ui/src/develop.rs:754`](../ui/dr-ui/src/develop.rs#L754), [`ui/dr-ui/src/develop.rs:774`](../ui/dr-ui/src/develop.rs#L774), [`ui/dr-ui/src/develop.rs:780`](../ui/dr-ui/src/develop.rs#L780), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/histogram.rs:208`](../ui/dr-ui/src/histogram.rs#L208), [`ui/dr-ui/src/histogram.rs:228`](../ui/dr-ui/src/histogram.rs#L228), [`ui/dr-ui/src/histogram.rs:272`](../ui/dr-ui/src/histogram.rs#L272), [`ui/dr-ui/src/histogram.rs:544`](../ui/dr-ui/src/histogram.rs#L544), [`ui/dr-ui/src/histogram.rs:565`](../ui/dr-ui/src/histogram.rs#L565), [`ui/dr-ui/src/histogram.rs:593`](../ui/dr-ui/src/histogram.rs#L593), [`ui/dr-ui/src/histogram.rs:621`](../ui/dr-ui/src/histogram.rs#L621), [`ui/dr-ui/src/histogram.rs:658`](../ui/dr-ui/src/histogram.rs#L658), [`ui/dr-ui/src/lib.rs:1636`](../ui/dr-ui/src/lib.rs#L1636), [`ui/dr-ui/src/lib.rs:1714`](../ui/dr-ui/src/lib.rs#L1714), [`ui/dr-ui/src/lib.rs:1813`](../ui/dr-ui/src/lib.rs#L1813), [`ui/dr-ui/src/lib.rs:1854`](../ui/dr-ui/src/lib.rs#L1854), [`ui/dr-ui/src/lib.rs:3104`](../ui/dr-ui/src/lib.rs#L3104), [`ui/dr-ui/src/lib.rs:382`](../ui/dr-ui/src/lib.rs#L382), [`ui/dr-ui/src/peaking.rs:1`](../ui/dr-ui/src/peaking.rs#L1), [`ui/dr-ui/ui/app.slint:1820`](../ui/dr-ui/ui/app.slint#L1820), [`ui/dr-ui/ui/app.slint:2425`](../ui/dr-ui/ui/app.slint#L2425), [`ui/dr-ui/ui/app.slint:89`](../ui/dr-ui/ui/app.slint#L89), [`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:246`](../ui/dr-ui/src/library.rs#L246), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492) | +| 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/raw_histogram.rs:129`](../core/dr-gpu/src/raw_histogram.rs#L129), [`core/dr-gpu/src/raw_histogram.rs:1`](../core/dr-gpu/src/raw_histogram.rs#L1), [`core/dr-gpu/src/raw_histogram.rs:272`](../core/dr-gpu/src/raw_histogram.rs#L272), [`core/dr-gpu/src/raw_histogram.rs:407`](../core/dr-gpu/src/raw_histogram.rs#L407), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`core/dr-gpu/src/shaders/raw_histogram.wgsl:1`](../core/dr-gpu/src/shaders/raw_histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:3250`](../ui/dr-ui/src/develop.rs#L3250), [`ui/dr-ui/src/develop.rs:3263`](../ui/dr-ui/src/develop.rs#L3263), [`ui/dr-ui/src/develop.rs:3308`](../ui/dr-ui/src/develop.rs#L3308), [`ui/dr-ui/src/develop.rs:3319`](../ui/dr-ui/src/develop.rs#L3319), [`ui/dr-ui/src/develop.rs:3325`](../ui/dr-ui/src/develop.rs#L3325), [`ui/dr-ui/src/develop.rs:3342`](../ui/dr-ui/src/develop.rs#L3342), [`ui/dr-ui/src/develop.rs:6643`](../ui/dr-ui/src/develop.rs#L6643), [`ui/dr-ui/src/develop.rs:6707`](../ui/dr-ui/src/develop.rs#L6707), [`ui/dr-ui/src/develop.rs:6730`](../ui/dr-ui/src/develop.rs#L6730), [`ui/dr-ui/src/develop.rs:755`](../ui/dr-ui/src/develop.rs#L755), [`ui/dr-ui/src/develop.rs:775`](../ui/dr-ui/src/develop.rs#L775), [`ui/dr-ui/src/develop.rs:781`](../ui/dr-ui/src/develop.rs#L781), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/histogram.rs:208`](../ui/dr-ui/src/histogram.rs#L208), [`ui/dr-ui/src/histogram.rs:228`](../ui/dr-ui/src/histogram.rs#L228), [`ui/dr-ui/src/histogram.rs:272`](../ui/dr-ui/src/histogram.rs#L272), [`ui/dr-ui/src/histogram.rs:544`](../ui/dr-ui/src/histogram.rs#L544), [`ui/dr-ui/src/histogram.rs:565`](../ui/dr-ui/src/histogram.rs#L565), [`ui/dr-ui/src/histogram.rs:593`](../ui/dr-ui/src/histogram.rs#L593), [`ui/dr-ui/src/histogram.rs:621`](../ui/dr-ui/src/histogram.rs#L621), [`ui/dr-ui/src/histogram.rs:658`](../ui/dr-ui/src/histogram.rs#L658), [`ui/dr-ui/src/lib.rs:1636`](../ui/dr-ui/src/lib.rs#L1636), [`ui/dr-ui/src/lib.rs:1714`](../ui/dr-ui/src/lib.rs#L1714), [`ui/dr-ui/src/lib.rs:1813`](../ui/dr-ui/src/lib.rs#L1813), [`ui/dr-ui/src/lib.rs:1854`](../ui/dr-ui/src/lib.rs#L1854), [`ui/dr-ui/src/lib.rs:3104`](../ui/dr-ui/src/lib.rs#L3104), [`ui/dr-ui/src/lib.rs:382`](../ui/dr-ui/src/lib.rs#L382), [`ui/dr-ui/src/peaking.rs:1`](../ui/dr-ui/src/peaking.rs#L1), [`ui/dr-ui/ui/app.slint:1820`](../ui/dr-ui/ui/app.slint#L1820), [`ui/dr-ui/ui/app.slint:2425`](../ui/dr-ui/ui/app.slint#L2425), [`ui/dr-ui/ui/app.slint:89`](../ui/dr-ui/ui/app.slint#L89), [`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:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/library.rs:246`](../ui/dr-ui/src/library.rs#L246), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492) | | FR-CULL-5 | [`core/dr-catalog/src/bursts.rs:1`](../core/dr-catalog/src/bursts.rs#L1), [`core/dr-catalog/src/schema.rs:419`](../core/dr-catalog/src/schema.rs#L419), [`ui/dr-ui/src/bursts.rs:1`](../ui/dr-ui/src/bursts.rs#L1), [`ui/dr-ui/src/library.rs:202`](../ui/dr-ui/src/library.rs#L202), [`ui/dr-ui/src/library.rs:5267`](../ui/dr-ui/src/library.rs#L5267) | | 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:487`](../core/dr-catalog/src/schema.rs#L487), [`core/dr-catalog/src/schema.rs:513`](../core/dr-catalog/src/schema.rs#L513), [`core/dr-catalog/src/schema.rs:554`](../core/dr-catalog/src/schema.rs#L554), [`core/dr-face/src/align.rs:291`](../core/dr-face/src/align.rs#L291), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/faces.rs:330`](../ui/dr-ui/src/faces.rs#L330), [`ui/dr-ui/src/faces.rs:346`](../ui/dr-ui/src/faces.rs#L346), [`ui/dr-ui/src/identity_ui.rs:540`](../ui/dr-ui/src/identity_ui.rs#L540), [`ui/dr-ui/src/lib.rs:1255`](../ui/dr-ui/src/lib.rs#L1255), [`ui/dr-ui/src/library.rs:2965`](../ui/dr-ui/src/library.rs#L2965), [`ui/dr-ui/src/library.rs:3058`](../ui/dr-ui/src/library.rs#L3058), [`ui/dr-ui/src/library.rs:3279`](../ui/dr-ui/src/library.rs#L3279), [`ui/dr-ui/src/library.rs:3423`](../ui/dr-ui/src/library.rs#L3423), [`ui/dr-ui/src/library.rs:3462`](../ui/dr-ui/src/library.rs#L3462), [`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:554`](../core/dr-catalog/src/schema.rs#L554), [`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), [`core/dr-types/src/settings.rs:117`](../core/dr-types/src/settings.rs#L117), [`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), [`ui/dr-ui/ui/identity.slint:283`](../ui/dr-ui/ui/identity.slint#L283) | | 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:2203`](../core/dr-gpu/src/adjust.rs#L2203), [`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:498`](../core/dr-pipeline/src/framing.rs#L498), [`core/dr-pipeline/src/framing.rs:753`](../core/dr-pipeline/src/framing.rs#L753), [`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:134`](../core/dr-pipeline/src/mask.rs#L134), [`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:1669`](../core/dr-pipeline/src/sidecar.rs#L1669), [`core/dr-pipeline/src/sidecar.rs:1729`](../core/dr-pipeline/src/sidecar.rs#L1729), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`core/dr-segment/src/refine.rs:442`](../core/dr-segment/src/refine.rs#L442), [`ui/dr-ui/src/develop.rs:102`](../ui/dr-ui/src/develop.rs#L102), [`ui/dr-ui/src/develop.rs:1505`](../ui/dr-ui/src/develop.rs#L1505), [`ui/dr-ui/src/develop.rs:164`](../ui/dr-ui/src/develop.rs#L164), [`ui/dr-ui/src/develop.rs:2026`](../ui/dr-ui/src/develop.rs#L2026), [`ui/dr-ui/src/develop.rs:2044`](../ui/dr-ui/src/develop.rs#L2044), [`ui/dr-ui/src/develop.rs:2058`](../ui/dr-ui/src/develop.rs#L2058), [`ui/dr-ui/src/develop.rs:2080`](../ui/dr-ui/src/develop.rs#L2080), [`ui/dr-ui/src/develop.rs:2226`](../ui/dr-ui/src/develop.rs#L2226), [`ui/dr-ui/src/develop.rs:2324`](../ui/dr-ui/src/develop.rs#L2324), [`ui/dr-ui/src/develop.rs:327`](../ui/dr-ui/src/develop.rs#L327), [`ui/dr-ui/src/develop.rs:364`](../ui/dr-ui/src/develop.rs#L364), [`ui/dr-ui/src/develop.rs:3744`](../ui/dr-ui/src/develop.rs#L3744), [`ui/dr-ui/src/develop.rs:3774`](../ui/dr-ui/src/develop.rs#L3774), [`ui/dr-ui/src/develop.rs:3840`](../ui/dr-ui/src/develop.rs#L3840), [`ui/dr-ui/src/develop.rs:3854`](../ui/dr-ui/src/develop.rs#L3854), [`ui/dr-ui/src/develop.rs:4046`](../ui/dr-ui/src/develop.rs#L4046), [`ui/dr-ui/src/develop.rs:4706`](../ui/dr-ui/src/develop.rs#L4706), [`ui/dr-ui/src/develop.rs:4760`](../ui/dr-ui/src/develop.rs#L4760), [`ui/dr-ui/src/develop.rs:4804`](../ui/dr-ui/src/develop.rs#L4804), [`ui/dr-ui/src/develop.rs:4854`](../ui/dr-ui/src/develop.rs#L4854), [`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:794`](../ui/dr-ui/src/develop.rs#L794), [`ui/dr-ui/src/develop.rs:841`](../ui/dr-ui/src/develop.rs#L841), [`ui/dr-ui/src/develop.rs:870`](../ui/dr-ui/src/develop.rs#L870), [`ui/dr-ui/src/lib.rs:1695`](../ui/dr-ui/src/lib.rs#L1695), [`ui/dr-ui/src/lib.rs:2497`](../ui/dr-ui/src/lib.rs#L2497), [`ui/dr-ui/src/lib.rs:2693`](../ui/dr-ui/src/lib.rs#L2693), [`ui/dr-ui/src/lib.rs:2865`](../ui/dr-ui/src/lib.rs#L2865), [`ui/dr-ui/src/lib.rs:2929`](../ui/dr-ui/src/lib.rs#L2929), [`ui/dr-ui/src/lib.rs:2983`](../ui/dr-ui/src/lib.rs#L2983), [`ui/dr-ui/src/lib.rs:388`](../ui/dr-ui/src/lib.rs#L388), [`ui/dr-ui/src/lib.rs:431`](../ui/dr-ui/src/lib.rs#L431), [`ui/dr-ui/src/lib.rs:454`](../ui/dr-ui/src/lib.rs#L454), [`ui/dr-ui/src/library.rs:539`](../ui/dr-ui/src/library.rs#L539), [`ui/dr-ui/src/masks_ui.rs:249`](../ui/dr-ui/src/masks_ui.rs#L249), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:884`](../ui/dr-ui/src/masks_ui.rs#L884), [`ui/dr-ui/src/masks_ui.rs:998`](../ui/dr-ui/src/masks_ui.rs#L998), [`ui/dr-ui/src/segmentation.rs:305`](../ui/dr-ui/src/segmentation.rs#L305), [`ui/dr-ui/src/segmentation.rs:321`](../ui/dr-ui/src/segmentation.rs#L321), [`ui/dr-ui/src/segmentation.rs:451`](../ui/dr-ui/src/segmentation.rs#L451), [`ui/dr-ui/src/segmentation.rs:479`](../ui/dr-ui/src/segmentation.rs#L479), [`ui/dr-ui/ui/adjust.slint:547`](../ui/dr-ui/ui/adjust.slint#L547), [`ui/dr-ui/ui/adjust.slint:666`](../ui/dr-ui/ui/adjust.slint#L666), [`ui/dr-ui/ui/app.slint:150`](../ui/dr-ui/ui/app.slint#L150), [`ui/dr-ui/ui/app.slint:196`](../ui/dr-ui/ui/app.slint#L196), [`ui/dr-ui/ui/app.slint:2022`](../ui/dr-ui/ui/app.slint#L2022), [`ui/dr-ui/ui/app.slint:960`](../ui/dr-ui/ui/app.slint#L960), [`ui/dr-ui/ui/masks.slint:611`](../ui/dr-ui/ui/masks.slint#L611) | -| FR-DEV-3a | [`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:395`](../core/dr-pipeline/src/framing.rs#L395), [`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:1024`](../core/dr-pipeline/src/mask.rs#L1024), [`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:1402`](../ui/dr-ui/src/develop.rs#L1402), [`ui/dr-ui/src/lib.rs:748`](../ui/dr-ui/src/lib.rs#L748), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../ui/dr-ui/tests/ui_names_no_operation.rs#L1) | +| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2203`](../core/dr-gpu/src/adjust.rs#L2203), [`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/coverage.rs:122`](../core/dr-pipeline/src/coverage.rs#L122), [`core/dr-pipeline/src/coverage.rs:1`](../core/dr-pipeline/src/coverage.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:498`](../core/dr-pipeline/src/framing.rs#L498), [`core/dr-pipeline/src/framing.rs:753`](../core/dr-pipeline/src/framing.rs#L753), [`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:151`](../core/dr-pipeline/src/mask.rs#L151), [`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:1048`](../core/dr-pipeline/src/sidecar.rs#L1048), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/src/sidecar.rs:1714`](../core/dr-pipeline/src/sidecar.rs#L1714), [`core/dr-pipeline/src/sidecar.rs:1774`](../core/dr-pipeline/src/sidecar.rs#L1774), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`core/dr-segment/src/refine.rs:442`](../core/dr-segment/src/refine.rs#L442), [`ui/dr-ui/src/develop.rs:103`](../ui/dr-ui/src/develop.rs#L103), [`ui/dr-ui/src/develop.rs:1506`](../ui/dr-ui/src/develop.rs#L1506), [`ui/dr-ui/src/develop.rs:165`](../ui/dr-ui/src/develop.rs#L165), [`ui/dr-ui/src/develop.rs:1922`](../ui/dr-ui/src/develop.rs#L1922), [`ui/dr-ui/src/develop.rs:2136`](../ui/dr-ui/src/develop.rs#L2136), [`ui/dr-ui/src/develop.rs:2154`](../ui/dr-ui/src/develop.rs#L2154), [`ui/dr-ui/src/develop.rs:2168`](../ui/dr-ui/src/develop.rs#L2168), [`ui/dr-ui/src/develop.rs:2190`](../ui/dr-ui/src/develop.rs#L2190), [`ui/dr-ui/src/develop.rs:2336`](../ui/dr-ui/src/develop.rs#L2336), [`ui/dr-ui/src/develop.rs:2434`](../ui/dr-ui/src/develop.rs#L2434), [`ui/dr-ui/src/develop.rs:328`](../ui/dr-ui/src/develop.rs#L328), [`ui/dr-ui/src/develop.rs:365`](../ui/dr-ui/src/develop.rs#L365), [`ui/dr-ui/src/develop.rs:3855`](../ui/dr-ui/src/develop.rs#L3855), [`ui/dr-ui/src/develop.rs:3885`](../ui/dr-ui/src/develop.rs#L3885), [`ui/dr-ui/src/develop.rs:3951`](../ui/dr-ui/src/develop.rs#L3951), [`ui/dr-ui/src/develop.rs:3965`](../ui/dr-ui/src/develop.rs#L3965), [`ui/dr-ui/src/develop.rs:4157`](../ui/dr-ui/src/develop.rs#L4157), [`ui/dr-ui/src/develop.rs:4817`](../ui/dr-ui/src/develop.rs#L4817), [`ui/dr-ui/src/develop.rs:4938`](../ui/dr-ui/src/develop.rs#L4938), [`ui/dr-ui/src/develop.rs:5094`](../ui/dr-ui/src/develop.rs#L5094), [`ui/dr-ui/src/develop.rs:5230`](../ui/dr-ui/src/develop.rs#L5230), [`ui/dr-ui/src/develop.rs:5274`](../ui/dr-ui/src/develop.rs#L5274), [`ui/dr-ui/src/develop.rs:5324`](../ui/dr-ui/src/develop.rs#L5324), [`ui/dr-ui/src/develop.rs:588`](../ui/dr-ui/src/develop.rs#L588), [`ui/dr-ui/src/develop.rs:649`](../ui/dr-ui/src/develop.rs#L649), [`ui/dr-ui/src/develop.rs:795`](../ui/dr-ui/src/develop.rs#L795), [`ui/dr-ui/src/develop.rs:842`](../ui/dr-ui/src/develop.rs#L842), [`ui/dr-ui/src/develop.rs:871`](../ui/dr-ui/src/develop.rs#L871), [`ui/dr-ui/src/lib.rs:1695`](../ui/dr-ui/src/lib.rs#L1695), [`ui/dr-ui/src/lib.rs:2497`](../ui/dr-ui/src/lib.rs#L2497), [`ui/dr-ui/src/lib.rs:2693`](../ui/dr-ui/src/lib.rs#L2693), [`ui/dr-ui/src/lib.rs:2865`](../ui/dr-ui/src/lib.rs#L2865), [`ui/dr-ui/src/lib.rs:2929`](../ui/dr-ui/src/lib.rs#L2929), [`ui/dr-ui/src/lib.rs:2983`](../ui/dr-ui/src/lib.rs#L2983), [`ui/dr-ui/src/lib.rs:388`](../ui/dr-ui/src/lib.rs#L388), [`ui/dr-ui/src/lib.rs:431`](../ui/dr-ui/src/lib.rs#L431), [`ui/dr-ui/src/lib.rs:454`](../ui/dr-ui/src/lib.rs#L454), [`ui/dr-ui/src/library.rs:539`](../ui/dr-ui/src/library.rs#L539), [`ui/dr-ui/src/masks_ui.rs:249`](../ui/dr-ui/src/masks_ui.rs#L249), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:884`](../ui/dr-ui/src/masks_ui.rs#L884), [`ui/dr-ui/src/masks_ui.rs:998`](../ui/dr-ui/src/masks_ui.rs#L998), [`ui/dr-ui/src/presets.rs:394`](../ui/dr-ui/src/presets.rs#L394), [`ui/dr-ui/src/segmentation.rs:116`](../ui/dr-ui/src/segmentation.rs#L116), [`ui/dr-ui/src/segmentation.rs:330`](../ui/dr-ui/src/segmentation.rs#L330), [`ui/dr-ui/src/segmentation.rs:346`](../ui/dr-ui/src/segmentation.rs#L346), [`ui/dr-ui/src/segmentation.rs:476`](../ui/dr-ui/src/segmentation.rs#L476), [`ui/dr-ui/src/segmentation.rs:504`](../ui/dr-ui/src/segmentation.rs#L504), [`ui/dr-ui/ui/adjust.slint:547`](../ui/dr-ui/ui/adjust.slint#L547), [`ui/dr-ui/ui/adjust.slint:666`](../ui/dr-ui/ui/adjust.slint#L666), [`ui/dr-ui/ui/app.slint:150`](../ui/dr-ui/ui/app.slint#L150), [`ui/dr-ui/ui/app.slint:196`](../ui/dr-ui/ui/app.slint#L196), [`ui/dr-ui/ui/app.slint:2022`](../ui/dr-ui/ui/app.slint#L2022), [`ui/dr-ui/ui/app.slint:960`](../ui/dr-ui/ui/app.slint#L960), [`ui/dr-ui/ui/masks.slint:611`](../ui/dr-ui/ui/masks.slint#L611) | +| FR-DEV-3a | [`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:395`](../core/dr-pipeline/src/framing.rs#L395), [`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:1092`](../core/dr-pipeline/src/mask.rs#L1092), [`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:1403`](../ui/dr-ui/src/develop.rs#L1403), [`ui/dr-ui/src/lib.rs:748`](../ui/dr-ui/src/lib.rs#L748), [`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:395`](../core/dr-pipeline/src/framing.rs#L395), [`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/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:1024`](../core/dr-pipeline/src/mask.rs#L1024), [`ui/dr-ui/src/develop.rs:5433`](../ui/dr-ui/src/develop.rs#L5433) | +| FR-DEV-3c | [`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:1092`](../core/dr-pipeline/src/mask.rs#L1092), [`ui/dr-ui/src/develop.rs:5903`](../ui/dr-ui/src/develop.rs#L5903) | | FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:1079`](../core/dr-gpu/src/adjust.rs#L1079), [`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: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:2015`](../core/dr-pipeline/src/sidecar.rs#L2015), [`core/dr-pipeline/src/sidecar.rs:2091`](../core/dr-pipeline/src/sidecar.rs#L2091), [`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:3491`](../ui/dr-ui/src/develop.rs#L3491), [`ui/dr-ui/src/develop.rs:3508`](../ui/dr-ui/src/develop.rs#L3508), [`ui/dr-ui/src/develop.rs:3520`](../ui/dr-ui/src/develop.rs#L3520), [`ui/dr-ui/src/develop.rs:3558`](../ui/dr-ui/src/develop.rs#L3558), [`ui/dr-ui/src/develop.rs:3567`](../ui/dr-ui/src/develop.rs#L3567), [`ui/dr-ui/src/develop.rs:3670`](../ui/dr-ui/src/develop.rs#L3670), [`ui/dr-ui/src/develop.rs:4083`](../ui/dr-ui/src/develop.rs#L4083), [`ui/dr-ui/src/develop.rs:4098`](../ui/dr-ui/src/develop.rs#L4098), [`ui/dr-ui/src/lib.rs:2471`](../ui/dr-ui/src/lib.rs#L2471), [`ui/dr-ui/src/lib.rs:681`](../ui/dr-ui/src/lib.rs#L681), [`ui/dr-ui/src/lib.rs:739`](../ui/dr-ui/src/lib.rs#L739), [`ui/dr-ui/src/library.rs:530`](../ui/dr-ui/src/library.rs#L530), [`ui/dr-ui/src/library.rs:778`](../ui/dr-ui/src/library.rs#L778), [`ui/dr-ui/src/presets.rs:395`](../ui/dr-ui/src/presets.rs#L395), [`ui/dr-ui/ui/adjust.slint:1072`](../ui/dr-ui/ui/adjust.slint#L1072), [`ui/dr-ui/ui/adjust.slint:996`](../ui/dr-ui/ui/adjust.slint#L996), [`ui/dr-ui/ui/app.slint:2622`](../ui/dr-ui/ui/app.slint#L2622), [`ui/dr-ui/ui/app.slint:688`](../ui/dr-ui/ui/app.slint#L688) | -| 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:1060`](../core/dr-pipeline/src/framing.rs#L1060), [`core/dr-pipeline/src/framing.rs:328`](../core/dr-pipeline/src/framing.rs#L328), [`core/dr-pipeline/src/framing.rs:498`](../core/dr-pipeline/src/framing.rs#L498), [`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:2243`](../ui/dr-ui/src/develop.rs#L2243), [`ui/dr-ui/src/segmentation.rs:451`](../ui/dr-ui/src/segmentation.rs#L451) | +| 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:113`](../core/dr-pipeline/src/sidecar.rs#L113), [`core/dr-pipeline/src/sidecar.rs:169`](../core/dr-pipeline/src/sidecar.rs#L169), [`core/dr-pipeline/src/sidecar.rs:2060`](../core/dr-pipeline/src/sidecar.rs#L2060), [`core/dr-pipeline/src/sidecar.rs:2136`](../core/dr-pipeline/src/sidecar.rs#L2136), [`core/dr-pipeline/src/sidecar.rs:535`](../core/dr-pipeline/src/sidecar.rs#L535), [`core/dr-pipeline/src/sidecar.rs:662`](../core/dr-pipeline/src/sidecar.rs#L662), [`core/dr-pipeline/src/sidecar.rs:794`](../core/dr-pipeline/src/sidecar.rs#L794), [`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:3602`](../ui/dr-ui/src/develop.rs#L3602), [`ui/dr-ui/src/develop.rs:3619`](../ui/dr-ui/src/develop.rs#L3619), [`ui/dr-ui/src/develop.rs:3631`](../ui/dr-ui/src/develop.rs#L3631), [`ui/dr-ui/src/develop.rs:3669`](../ui/dr-ui/src/develop.rs#L3669), [`ui/dr-ui/src/develop.rs:3678`](../ui/dr-ui/src/develop.rs#L3678), [`ui/dr-ui/src/develop.rs:3781`](../ui/dr-ui/src/develop.rs#L3781), [`ui/dr-ui/src/develop.rs:4194`](../ui/dr-ui/src/develop.rs#L4194), [`ui/dr-ui/src/develop.rs:4209`](../ui/dr-ui/src/develop.rs#L4209), [`ui/dr-ui/src/lib.rs:2471`](../ui/dr-ui/src/lib.rs#L2471), [`ui/dr-ui/src/lib.rs:681`](../ui/dr-ui/src/lib.rs#L681), [`ui/dr-ui/src/lib.rs:739`](../ui/dr-ui/src/lib.rs#L739), [`ui/dr-ui/src/library.rs:530`](../ui/dr-ui/src/library.rs#L530), [`ui/dr-ui/src/library.rs:778`](../ui/dr-ui/src/library.rs#L778), [`ui/dr-ui/src/presets.rs:402`](../ui/dr-ui/src/presets.rs#L402), [`ui/dr-ui/ui/adjust.slint:1072`](../ui/dr-ui/ui/adjust.slint#L1072), [`ui/dr-ui/ui/adjust.slint:996`](../ui/dr-ui/ui/adjust.slint#L996), [`ui/dr-ui/ui/app.slint:2622`](../ui/dr-ui/ui/app.slint#L2622), [`ui/dr-ui/ui/app.slint:688`](../ui/dr-ui/ui/app.slint#L688) | +| 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:1060`](../core/dr-pipeline/src/framing.rs#L1060), [`core/dr-pipeline/src/framing.rs:328`](../core/dr-pipeline/src/framing.rs#L328), [`core/dr-pipeline/src/framing.rs:498`](../core/dr-pipeline/src/framing.rs#L498), [`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:140`](../ui/dr-ui/src/develop.rs#L140), [`ui/dr-ui/src/develop.rs:2353`](../ui/dr-ui/src/develop.rs#L2353), [`ui/dr-ui/src/segmentation.rs:476`](../ui/dr-ui/src/segmentation.rs#L476) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:351`](../core/dr-gpu/src/lib.rs#L351), [`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:3520`](../ui/dr-ui/src/develop.rs#L3520), [`ui/dr-ui/src/develop.rs:4098`](../ui/dr-ui/src/develop.rs#L4098), [`ui/dr-ui/src/develop.rs:4128`](../ui/dr-ui/src/develop.rs#L4128), [`ui/dr-ui/src/develop.rs:4141`](../ui/dr-ui/src/develop.rs#L4141), [`ui/dr-ui/src/develop.rs:4153`](../ui/dr-ui/src/develop.rs#L4153), [`ui/dr-ui/src/develop.rs:4169`](../ui/dr-ui/src/develop.rs#L4169), [`ui/dr-ui/src/develop.rs:4201`](../ui/dr-ui/src/develop.rs#L4201), [`ui/dr-ui/src/develop.rs:4205`](../ui/dr-ui/src/develop.rs#L4205), [`ui/dr-ui/src/develop.rs:4224`](../ui/dr-ui/src/develop.rs#L4224), [`ui/dr-ui/src/develop.rs:4240`](../ui/dr-ui/src/develop.rs#L4240), [`ui/dr-ui/src/develop.rs:737`](../ui/dr-ui/src/develop.rs#L737), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:217`](../ui/dr-ui/src/labels.rs#L217), [`ui/dr-ui/src/lib.rs:1630`](../ui/dr-ui/src/lib.rs#L1630), [`ui/dr-ui/src/lib.rs:1673`](../ui/dr-ui/src/lib.rs#L1673), [`ui/dr-ui/src/lib.rs:1681`](../ui/dr-ui/src/lib.rs#L1681), [`ui/dr-ui/src/lib.rs:2667`](../ui/dr-ui/src/lib.rs#L2667), [`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:272`](../core/dr-types/src/settings.rs#L272), [`ui/dr-ui/src/develop.rs:4040`](../ui/dr-ui/src/develop.rs#L4040), [`ui/dr-ui/src/develop.rs:4061`](../ui/dr-ui/src/develop.rs#L4061), [`ui/dr-ui/src/lib.rs:1597`](../ui/dr-ui/src/lib.rs#L1597), [`ui/dr-ui/src/lib.rs:2282`](../ui/dr-ui/src/lib.rs#L2282), [`ui/dr-ui/src/lib.rs:2311`](../ui/dr-ui/src/lib.rs#L2311), [`ui/dr-ui/src/library.rs:1903`](../ui/dr-ui/src/library.rs#L1903), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:520`](../ui/dr-ui/src/library.rs#L520), [`ui/dr-ui/src/library_ui.rs:2966`](../ui/dr-ui/src/library_ui.rs#L2966), [`ui/dr-ui/src/library_ui.rs:3366`](../ui/dr-ui/src/library_ui.rs#L3366), [`ui/dr-ui/src/library_ui.rs:500`](../ui/dr-ui/src/library_ui.rs#L500), [`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:740`](../ui/dr-ui/ui/adjust.slint#L740), [`ui/dr-ui/ui/adjust.slint:767`](../ui/dr-ui/ui/adjust.slint#L767), [`ui/dr-ui/ui/adjust.slint:801`](../ui/dr-ui/ui/adjust.slint#L801), [`ui/dr-ui/ui/app.slint:1575`](../ui/dr-ui/ui/app.slint#L1575), [`ui/dr-ui/ui/app.slint:2711`](../ui/dr-ui/ui/app.slint#L2711), [`ui/dr-ui/ui/app.slint:760`](../ui/dr-ui/ui/app.slint#L760), [`ui/dr-ui/ui/app.slint:771`](../ui/dr-ui/ui/app.slint#L771), [`ui/dr-ui/ui/library.slint:1489`](../ui/dr-ui/ui/library.slint#L1489), [`ui/dr-ui/ui/library.slint:1495`](../ui/dr-ui/ui/library.slint#L1495), [`ui/dr-ui/ui/library.slint:4008`](../ui/dr-ui/ui/library.slint#L4008), [`ui/dr-ui/ui/library.slint:4023`](../ui/dr-ui/ui/library.slint#L4023), [`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:4169`](../ui/dr-ui/src/develop.rs#L4169), [`ui/dr-ui/src/develop.rs:4201`](../ui/dr-ui/src/develop.rs#L4201), [`ui/dr-ui/src/lib.rs:1681`](../ui/dr-ui/src/lib.rs#L1681), [`ui/dr-ui/src/lib.rs:2667`](../ui/dr-ui/src/lib.rs#L2667), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | -| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:405`](../core/dr-gpu/src/detail.rs#L405), [`core/dr-gpu/src/detail.rs:623`](../core/dr-gpu/src/detail.rs#L623), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:410`](../core/dr-pipeline/src/detail.rs#L410), [`core/dr-pipeline/src/detail.rs: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:2395`](../ui/dr-ui/src/develop.rs#L2395), [`ui/dr-ui/src/develop.rs:2433`](../ui/dr-ui/src/develop.rs#L2433), [`ui/dr-ui/src/develop.rs:2498`](../ui/dr-ui/src/develop.rs#L2498), [`ui/dr-ui/src/develop.rs:2581`](../ui/dr-ui/src/develop.rs#L2581), [`ui/dr-ui/src/develop.rs:2595`](../ui/dr-ui/src/develop.rs#L2595), [`ui/dr-ui/src/develop.rs:824`](../ui/dr-ui/src/develop.rs#L824), [`ui/dr-ui/src/labels.rs:53`](../ui/dr-ui/src/labels.rs#L53), [`ui/dr-ui/src/lib.rs:1702`](../ui/dr-ui/src/lib.rs#L1702), [`ui/dr-ui/src/lib.rs:2785`](../ui/dr-ui/src/lib.rs#L2785), [`ui/dr-ui/src/lib.rs:393`](../ui/dr-ui/src/lib.rs#L393), [`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:832`](../ui/dr-ui/ui/adjust.slint#L832), [`ui/dr-ui/ui/app.slint:140`](../ui/dr-ui/ui/app.slint#L140), [`ui/dr-ui/ui/app.slint:1925`](../ui/dr-ui/ui/app.slint#L1925), [`ui/dr-ui/ui/app.slint:2100`](../ui/dr-ui/ui/app.slint#L2100), [`ui/dr-ui/ui/app.slint:2523`](../ui/dr-ui/ui/app.slint#L2523), [`ui/dr-ui/ui/icons.slint:338`](../ui/dr-ui/ui/icons.slint#L338), [`ui/dr-ui/ui/spots.slint:172`](../ui/dr-ui/ui/spots.slint#L172), [`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:2126`](../core/dr-gpu/src/adjust.rs#L2126), [`core/dr-gpu/src/adjust.rs:2203`](../core/dr-gpu/src/adjust.rs#L2203), [`core/dr-gpu/src/adjust.rs:2288`](../core/dr-gpu/src/adjust.rs#L2288), [`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:172`](../core/dr-gpu/src/lib.rs#L172), [`core/dr-gpu/src/lib.rs:192`](../core/dr-gpu/src/lib.rs#L192), [`core/dr-gpu/src/lib.rs:70`](../core/dr-gpu/src/lib.rs#L70), [`core/dr-gpu/src/lib.rs:93`](../core/dr-gpu/src/lib.rs#L93), [`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:3028`](../ui/dr-ui/src/develop.rs#L3028), [`ui/dr-ui/src/develop.rs:4495`](../ui/dr-ui/src/develop.rs#L4495), [`ui/dr-ui/src/develop.rs:4978`](../ui/dr-ui/src/develop.rs#L4978), [`ui/dr-ui/src/develop.rs:5012`](../ui/dr-ui/src/develop.rs#L5012), [`ui/dr-ui/src/lib.rs:889`](../ui/dr-ui/src/lib.rs#L889), [`ui/dr-ui/src/lib.rs:92`](../ui/dr-ui/src/lib.rs#L92), [`ui/dr-ui/src/lib.rs:948`](../ui/dr-ui/src/lib.rs#L948) | +| 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:3631`](../ui/dr-ui/src/develop.rs#L3631), [`ui/dr-ui/src/develop.rs:4209`](../ui/dr-ui/src/develop.rs#L4209), [`ui/dr-ui/src/develop.rs:4239`](../ui/dr-ui/src/develop.rs#L4239), [`ui/dr-ui/src/develop.rs:4252`](../ui/dr-ui/src/develop.rs#L4252), [`ui/dr-ui/src/develop.rs:4264`](../ui/dr-ui/src/develop.rs#L4264), [`ui/dr-ui/src/develop.rs:4280`](../ui/dr-ui/src/develop.rs#L4280), [`ui/dr-ui/src/develop.rs:4312`](../ui/dr-ui/src/develop.rs#L4312), [`ui/dr-ui/src/develop.rs:4316`](../ui/dr-ui/src/develop.rs#L4316), [`ui/dr-ui/src/develop.rs:4335`](../ui/dr-ui/src/develop.rs#L4335), [`ui/dr-ui/src/develop.rs:4351`](../ui/dr-ui/src/develop.rs#L4351), [`ui/dr-ui/src/develop.rs:738`](../ui/dr-ui/src/develop.rs#L738), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:217`](../ui/dr-ui/src/labels.rs#L217), [`ui/dr-ui/src/lib.rs:1630`](../ui/dr-ui/src/lib.rs#L1630), [`ui/dr-ui/src/lib.rs:1673`](../ui/dr-ui/src/lib.rs#L1673), [`ui/dr-ui/src/lib.rs:1681`](../ui/dr-ui/src/lib.rs#L1681), [`ui/dr-ui/src/lib.rs:2667`](../ui/dr-ui/src/lib.rs#L2667), [`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:272`](../core/dr-types/src/settings.rs#L272), [`ui/dr-ui/src/develop.rs:4151`](../ui/dr-ui/src/develop.rs#L4151), [`ui/dr-ui/src/develop.rs:4172`](../ui/dr-ui/src/develop.rs#L4172), [`ui/dr-ui/src/lib.rs:1597`](../ui/dr-ui/src/lib.rs#L1597), [`ui/dr-ui/src/lib.rs:2282`](../ui/dr-ui/src/lib.rs#L2282), [`ui/dr-ui/src/lib.rs:2311`](../ui/dr-ui/src/lib.rs#L2311), [`ui/dr-ui/src/library.rs:1903`](../ui/dr-ui/src/library.rs#L1903), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:520`](../ui/dr-ui/src/library.rs#L520), [`ui/dr-ui/src/library_ui.rs:2966`](../ui/dr-ui/src/library_ui.rs#L2966), [`ui/dr-ui/src/library_ui.rs:3366`](../ui/dr-ui/src/library_ui.rs#L3366), [`ui/dr-ui/src/library_ui.rs:500`](../ui/dr-ui/src/library_ui.rs#L500), [`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:664`](../ui/dr-ui/src/presets.rs#L664), [`ui/dr-ui/src/presets.rs:698`](../ui/dr-ui/src/presets.rs#L698), [`ui/dr-ui/ui/adjust.slint:740`](../ui/dr-ui/ui/adjust.slint#L740), [`ui/dr-ui/ui/adjust.slint:767`](../ui/dr-ui/ui/adjust.slint#L767), [`ui/dr-ui/ui/adjust.slint:801`](../ui/dr-ui/ui/adjust.slint#L801), [`ui/dr-ui/ui/app.slint:1575`](../ui/dr-ui/ui/app.slint#L1575), [`ui/dr-ui/ui/app.slint:2711`](../ui/dr-ui/ui/app.slint#L2711), [`ui/dr-ui/ui/app.slint:760`](../ui/dr-ui/ui/app.slint#L760), [`ui/dr-ui/ui/app.slint:771`](../ui/dr-ui/ui/app.slint#L771), [`ui/dr-ui/ui/library.slint:1489`](../ui/dr-ui/ui/library.slint#L1489), [`ui/dr-ui/ui/library.slint:1495`](../ui/dr-ui/ui/library.slint#L1495), [`ui/dr-ui/ui/library.slint:4008`](../ui/dr-ui/ui/library.slint#L4008), [`ui/dr-ui/ui/library.slint:4023`](../ui/dr-ui/ui/library.slint#L4023), [`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:4280`](../ui/dr-ui/src/develop.rs#L4280), [`ui/dr-ui/src/develop.rs:4312`](../ui/dr-ui/src/develop.rs#L4312), [`ui/dr-ui/src/lib.rs:1681`](../ui/dr-ui/src/lib.rs#L1681), [`ui/dr-ui/src/lib.rs:2667`](../ui/dr-ui/src/lib.rs#L2667), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | +| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:405`](../core/dr-gpu/src/detail.rs#L405), [`core/dr-gpu/src/detail.rs:623`](../core/dr-gpu/src/detail.rs#L623), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:410`](../core/dr-pipeline/src/detail.rs#L410), [`core/dr-pipeline/src/detail.rs: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:185`](../core/dr-pipeline/src/sidecar.rs#L185), [`core/dr-pipeline/src/sidecar.rs:354`](../core/dr-pipeline/src/sidecar.rs#L354), [`core/dr-pipeline/src/sidecar.rs:674`](../core/dr-pipeline/src/sidecar.rs#L674), [`core/dr-pipeline/src/sidecar.rs:810`](../core/dr-pipeline/src/sidecar.rs#L810), [`core/dr-pipeline/src/sidecar.rs:864`](../core/dr-pipeline/src/sidecar.rs#L864), [`core/dr-pipeline/src/sidecar.rs:894`](../core/dr-pipeline/src/sidecar.rs#L894), [`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:2505`](../ui/dr-ui/src/develop.rs#L2505), [`ui/dr-ui/src/develop.rs:2543`](../ui/dr-ui/src/develop.rs#L2543), [`ui/dr-ui/src/develop.rs:2608`](../ui/dr-ui/src/develop.rs#L2608), [`ui/dr-ui/src/develop.rs:2691`](../ui/dr-ui/src/develop.rs#L2691), [`ui/dr-ui/src/develop.rs:2705`](../ui/dr-ui/src/develop.rs#L2705), [`ui/dr-ui/src/develop.rs:825`](../ui/dr-ui/src/develop.rs#L825), [`ui/dr-ui/src/labels.rs:53`](../ui/dr-ui/src/labels.rs#L53), [`ui/dr-ui/src/lib.rs:1702`](../ui/dr-ui/src/lib.rs#L1702), [`ui/dr-ui/src/lib.rs:2785`](../ui/dr-ui/src/lib.rs#L2785), [`ui/dr-ui/src/lib.rs:393`](../ui/dr-ui/src/lib.rs#L393), [`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:832`](../ui/dr-ui/ui/adjust.slint#L832), [`ui/dr-ui/ui/app.slint:140`](../ui/dr-ui/ui/app.slint#L140), [`ui/dr-ui/ui/app.slint:1925`](../ui/dr-ui/ui/app.slint#L1925), [`ui/dr-ui/ui/app.slint:2100`](../ui/dr-ui/ui/app.slint#L2100), [`ui/dr-ui/ui/app.slint:2523`](../ui/dr-ui/ui/app.slint#L2523), [`ui/dr-ui/ui/icons.slint:338`](../ui/dr-ui/ui/icons.slint#L338), [`ui/dr-ui/ui/spots.slint:172`](../ui/dr-ui/ui/spots.slint#L172), [`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:2126`](../core/dr-gpu/src/adjust.rs#L2126), [`core/dr-gpu/src/adjust.rs:2203`](../core/dr-gpu/src/adjust.rs#L2203), [`core/dr-gpu/src/adjust.rs:2288`](../core/dr-gpu/src/adjust.rs#L2288), [`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:172`](../core/dr-gpu/src/lib.rs#L172), [`core/dr-gpu/src/lib.rs:192`](../core/dr-gpu/src/lib.rs#L192), [`core/dr-gpu/src/lib.rs:70`](../core/dr-gpu/src/lib.rs#L70), [`core/dr-gpu/src/lib.rs:93`](../core/dr-gpu/src/lib.rs#L93), [`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:3139`](../ui/dr-ui/src/develop.rs#L3139), [`ui/dr-ui/src/develop.rs:4606`](../ui/dr-ui/src/develop.rs#L4606), [`ui/dr-ui/src/develop.rs:5448`](../ui/dr-ui/src/develop.rs#L5448), [`ui/dr-ui/src/develop.rs:5482`](../ui/dr-ui/src/develop.rs#L5482), [`ui/dr-ui/src/lib.rs:889`](../ui/dr-ui/src/lib.rs#L889), [`ui/dr-ui/src/lib.rs:92`](../ui/dr-ui/src/lib.rs#L92), [`ui/dr-ui/src/lib.rs:948`](../ui/dr-ui/src/lib.rs#L948) | | 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:3058`](../ui/dr-ui/src/develop.rs#L3058), [`ui/dr-ui/src/develop.rs:6406`](../ui/dr-ui/src/develop.rs#L6406), [`ui/dr-ui/src/lib.rs:1658`](../ui/dr-ui/src/lib.rs#L1658), [`ui/dr-ui/src/lib.rs:3168`](../ui/dr-ui/src/lib.rs#L3168) | -| 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:3107`](../ui/dr-ui/src/develop.rs#L3107), [`ui/dr-ui/src/develop.rs:6080`](../ui/dr-ui/src/develop.rs#L6080), [`ui/dr-ui/src/develop.rs:6112`](../ui/dr-ui/src/develop.rs#L6112), [`ui/dr-ui/src/develop.rs:748`](../ui/dr-ui/src/develop.rs#L748), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1768`](../ui/dr-ui/src/lib.rs#L1768), [`ui/dr-ui/src/lib.rs:372`](../ui/dr-ui/src/lib.rs#L372), [`ui/dr-ui/ui/app.slint:74`](../ui/dr-ui/ui/app.slint#L74), [`ui/dr-ui/ui/histogram.slint:168`](../ui/dr-ui/ui/histogram.slint#L168), [`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:3004`](../ui/dr-ui/src/develop.rs#L3004), [`ui/dr-ui/src/develop.rs:3058`](../ui/dr-ui/src/develop.rs#L3058), [`ui/dr-ui/src/develop.rs:6406`](../ui/dr-ui/src/develop.rs#L6406), [`ui/dr-ui/src/develop.rs:6452`](../ui/dr-ui/src/develop.rs#L6452), [`ui/dr-ui/src/develop.rs:6471`](../ui/dr-ui/src/develop.rs#L6471), [`ui/dr-ui/src/develop.rs:855`](../ui/dr-ui/src/develop.rs#L855), [`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:1619`](../ui/dr-ui/src/lib.rs#L1619), [`ui/dr-ui/src/lib.rs:1658`](../ui/dr-ui/src/lib.rs#L1658), [`ui/dr-ui/src/lib.rs:3081`](../ui/dr-ui/src/lib.rs#L3081), [`ui/dr-ui/src/lib.rs:3168`](../ui/dr-ui/src/lib.rs#L3168), [`ui/dr-ui/ui/app.slint:1773`](../ui/dr-ui/ui/app.slint#L1773), [`ui/dr-ui/ui/app.slint:53`](../ui/dr-ui/ui/app.slint#L53), [`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:3169`](../ui/dr-ui/src/develop.rs#L3169), [`ui/dr-ui/src/develop.rs:6876`](../ui/dr-ui/src/develop.rs#L6876), [`ui/dr-ui/src/lib.rs:1658`](../ui/dr-ui/src/lib.rs#L1658), [`ui/dr-ui/src/lib.rs:3168`](../ui/dr-ui/src/lib.rs#L3168) | +| 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:3218`](../ui/dr-ui/src/develop.rs#L3218), [`ui/dr-ui/src/develop.rs:6550`](../ui/dr-ui/src/develop.rs#L6550), [`ui/dr-ui/src/develop.rs:6582`](../ui/dr-ui/src/develop.rs#L6582), [`ui/dr-ui/src/develop.rs:749`](../ui/dr-ui/src/develop.rs#L749), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1768`](../ui/dr-ui/src/lib.rs#L1768), [`ui/dr-ui/src/lib.rs:372`](../ui/dr-ui/src/lib.rs#L372), [`ui/dr-ui/ui/app.slint:74`](../ui/dr-ui/ui/app.slint#L74), [`ui/dr-ui/ui/histogram.slint:168`](../ui/dr-ui/ui/histogram.slint#L168), [`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:3115`](../ui/dr-ui/src/develop.rs#L3115), [`ui/dr-ui/src/develop.rs:3169`](../ui/dr-ui/src/develop.rs#L3169), [`ui/dr-ui/src/develop.rs:6876`](../ui/dr-ui/src/develop.rs#L6876), [`ui/dr-ui/src/develop.rs:6922`](../ui/dr-ui/src/develop.rs#L6922), [`ui/dr-ui/src/develop.rs:6941`](../ui/dr-ui/src/develop.rs#L6941), [`ui/dr-ui/src/develop.rs:856`](../ui/dr-ui/src/develop.rs#L856), [`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:1619`](../ui/dr-ui/src/lib.rs#L1619), [`ui/dr-ui/src/lib.rs:1658`](../ui/dr-ui/src/lib.rs#L1658), [`ui/dr-ui/src/lib.rs:3081`](../ui/dr-ui/src/lib.rs#L3081), [`ui/dr-ui/src/lib.rs:3168`](../ui/dr-ui/src/lib.rs#L3168), [`ui/dr-ui/ui/app.slint:1773`](../ui/dr-ui/ui/app.slint#L1773), [`ui/dr-ui/ui/app.slint:53`](../ui/dr-ui/ui/app.slint#L53), [`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:2436`](../core/dr-gpu/src/adjust.rs#L2436), [`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:713`](../core/dr-types/src/settings.rs#L713), [`ui/dr-ui/src/develop.rs:6471`](../ui/dr-ui/src/develop.rs#L6471), [`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:2436`](../core/dr-gpu/src/adjust.rs#L2436), [`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:713`](../core/dr-types/src/settings.rs#L713), [`ui/dr-ui/src/develop.rs:6941`](../ui/dr-ui/src/develop.rs#L6941), [`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:758`](../core/dr-types/src/settings.rs#L758), [`core/dr-types/src/settings.rs:766`](../core/dr-types/src/settings.rs#L766), [`core/dr-types/src/settings.rs:782`](../core/dr-types/src/settings.rs#L782), [`core/dr-types/src/settings.rs:851`](../core/dr-types/src/settings.rs#L851), [`core/dr-types/src/settings.rs:879`](../core/dr-types/src/settings.rs#L879), [`core/dr-types/src/settings.rs:890`](../core/dr-types/src/settings.rs#L890), [`core/dr-types/src/settings.rs:896`](../core/dr-types/src/settings.rs#L896), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:207`](../ui/dr-ui/src/settings_ui.rs#L207), [`ui/dr-ui/src/settings_ui.rs:220`](../ui/dr-ui/src/settings_ui.rs#L220), [`ui/dr-ui/src/settings_ui.rs:235`](../ui/dr-ui/src/settings_ui.rs#L235), [`ui/dr-ui/src/settings_ui.rs:565`](../ui/dr-ui/src/settings_ui.rs#L565), [`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:483`](../ui/dr-ui/src/lib.rs#L483), [`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:679`](../ui/dr-ui/src/settings_ui.rs#L679) | | 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:992`](../ui/dr-ui/src/export.rs#L992), [`ui/dr-ui/src/lib.rs:231`](../ui/dr-ui/src/lib.rs#L231), [`ui/dr-ui/src/lib.rs:2413`](../ui/dr-ui/src/lib.rs#L2413), [`ui/dr-ui/src/lib.rs:483`](../ui/dr-ui/src/lib.rs#L483), [`ui/dr-ui/src/lib.rs:529`](../ui/dr-ui/src/lib.rs#L529), [`ui/dr-ui/src/lib.rs:556`](../ui/dr-ui/src/lib.rs#L556), [`ui/dr-ui/src/library_ui.rs:3749`](../ui/dr-ui/src/library_ui.rs#L3749), [`ui/dr-ui/src/library_ui.rs:591`](../ui/dr-ui/src/library_ui.rs#L591), [`ui/dr-ui/src/library_ui.rs:691`](../ui/dr-ui/src/library_ui.rs#L691), [`ui/dr-ui/src/library_ui.rs:6935`](../ui/dr-ui/src/library_ui.rs#L6935), [`ui/dr-ui/src/library_ui.rs:7012`](../ui/dr-ui/src/library_ui.rs#L7012), [`ui/dr-ui/src/library_ui.rs:7024`](../ui/dr-ui/src/library_ui.rs#L7024), [`ui/dr-ui/src/library_ui.rs:748`](../ui/dr-ui/src/library_ui.rs#L748), [`ui/dr-ui/ui/app.slint:1099`](../ui/dr-ui/ui/app.slint#L1099), [`ui/dr-ui/ui/app.slint:1585`](../ui/dr-ui/ui/app.slint#L1585), [`ui/dr-ui/ui/library.slint:1498`](../ui/dr-ui/ui/library.slint#L1498), [`ui/dr-ui/ui/library.slint:4048`](../ui/dr-ui/ui/library.slint#L4048) | -| 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:426`](../core/dr-types/src/settings.rs#L426), [`ui/dr-ui/src/develop.rs:3363`](../ui/dr-ui/src/develop.rs#L3363), [`ui/dr-ui/src/develop.rs:3374`](../ui/dr-ui/src/develop.rs#L3374), [`ui/dr-ui/src/develop.rs:702`](../ui/dr-ui/src/develop.rs#L702), [`ui/dr-ui/src/export.rs:467`](../ui/dr-ui/src/export.rs#L467), [`ui/dr-ui/src/export.rs:634`](../ui/dr-ui/src/export.rs#L634), [`ui/dr-ui/src/export.rs:680`](../ui/dr-ui/src/export.rs#L680), [`ui/dr-ui/src/export.rs:702`](../ui/dr-ui/src/export.rs#L702), [`ui/dr-ui/src/export.rs:826`](../ui/dr-ui/src/export.rs#L826), [`ui/dr-ui/src/export.rs:844`](../ui/dr-ui/src/export.rs#L844), [`ui/dr-ui/src/lib.rs:231`](../ui/dr-ui/src/lib.rs#L231), [`ui/dr-ui/src/lib.rs:254`](../ui/dr-ui/src/lib.rs#L254), [`ui/dr-ui/src/lib.rs:515`](../ui/dr-ui/src/lib.rs#L515), [`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:1106`](../core/dr-gpu/src/adjust.rs#L1106), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/develop.rs:3395`](../ui/dr-ui/src/develop.rs#L3395), [`ui/dr-ui/src/develop.rs:3433`](../ui/dr-ui/src/develop.rs#L3433), [`ui/dr-ui/src/lib.rs:483`](../ui/dr-ui/src/lib.rs#L483), [`ui/dr-ui/src/library.rs:3058`](../ui/dr-ui/src/library.rs#L3058), [`ui/dr-ui/src/library.rs:3423`](../ui/dr-ui/src/library.rs#L3423), [`ui/dr-ui/src/library.rs:3462`](../ui/dr-ui/src/library.rs#L3462), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | +| 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:426`](../core/dr-types/src/settings.rs#L426), [`ui/dr-ui/src/develop.rs:3474`](../ui/dr-ui/src/develop.rs#L3474), [`ui/dr-ui/src/develop.rs:3485`](../ui/dr-ui/src/develop.rs#L3485), [`ui/dr-ui/src/develop.rs:703`](../ui/dr-ui/src/develop.rs#L703), [`ui/dr-ui/src/export.rs:467`](../ui/dr-ui/src/export.rs#L467), [`ui/dr-ui/src/export.rs:634`](../ui/dr-ui/src/export.rs#L634), [`ui/dr-ui/src/export.rs:680`](../ui/dr-ui/src/export.rs#L680), [`ui/dr-ui/src/export.rs:702`](../ui/dr-ui/src/export.rs#L702), [`ui/dr-ui/src/export.rs:826`](../ui/dr-ui/src/export.rs#L826), [`ui/dr-ui/src/export.rs:844`](../ui/dr-ui/src/export.rs#L844), [`ui/dr-ui/src/lib.rs:231`](../ui/dr-ui/src/lib.rs#L231), [`ui/dr-ui/src/lib.rs:254`](../ui/dr-ui/src/lib.rs#L254), [`ui/dr-ui/src/lib.rs:515`](../ui/dr-ui/src/lib.rs#L515), [`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:1106`](../core/dr-gpu/src/adjust.rs#L1106), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/develop.rs:3506`](../ui/dr-ui/src/develop.rs#L3506), [`ui/dr-ui/src/develop.rs:3544`](../ui/dr-ui/src/develop.rs#L3544), [`ui/dr-ui/src/develop.rs:4992`](../ui/dr-ui/src/develop.rs#L4992), [`ui/dr-ui/src/lib.rs:483`](../ui/dr-ui/src/lib.rs#L483), [`ui/dr-ui/src/library.rs:3058`](../ui/dr-ui/src/library.rs#L3058), [`ui/dr-ui/src/library.rs:3423`](../ui/dr-ui/src/library.rs#L3423), [`ui/dr-ui/src/library.rs:3462`](../ui/dr-ui/src/library.rs#L3462), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | | 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:556`](../ui/dr-ui/src/lib.rs#L556), [`ui/dr-ui/src/library.rs:1101`](../ui/dr-ui/src/library.rs#L1101), [`ui/dr-ui/src/library.rs:1939`](../ui/dr-ui/src/library.rs#L1939), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1892`](../ui/dr-ui/src/library_ui.rs#L1892), [`ui/dr-ui/src/library_ui.rs:3749`](../ui/dr-ui/src/library_ui.rs#L3749), [`ui/dr-ui/src/library_ui.rs:532`](../ui/dr-ui/src/library_ui.rs#L532), [`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) | @@ -103,25 +103,25 @@ _None._ | 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:3560`](../ui/dr-ui/src/library.rs#L3560), [`ui/dr-ui/src/library_ui.rs:4001`](../ui/dr-ui/src/library_ui.rs#L4001), [`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:206`](../core/dr-types/src/settings.rs#L206), [`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:1303`](../ui/dr-ui/src/lib.rs#L1303), [`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:1303`](../ui/dr-ui/src/lib.rs#L1303) | -| 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:2087`](../ui/dr-ui/src/lib.rs#L2087), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library_ui.rs:500`](../ui/dr-ui/src/library_ui.rs#L500) | -| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:668`](../core/dr-catalog/src/schema.rs#L668), [`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:2091`](../core/dr-pipeline/src/sidecar.rs#L2091), [`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:1031`](../ui/dr-ui/src/library.rs#L1031), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877) | +| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120), [`core/dr-pipeline/src/sidecar.rs:94`](../core/dr-pipeline/src/sidecar.rs#L94), [`ui/dr-ui/src/lib.rs:2087`](../ui/dr-ui/src/lib.rs#L2087), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library_ui.rs:500`](../ui/dr-ui/src/library_ui.rs#L500) | +| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:668`](../core/dr-catalog/src/schema.rs#L668), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/src/sidecar.rs:185`](../core/dr-pipeline/src/sidecar.rs#L185), [`core/dr-pipeline/src/sidecar.rs:2136`](../core/dr-pipeline/src/sidecar.rs#L2136), [`core/dr-pipeline/src/sidecar.rs:354`](../core/dr-pipeline/src/sidecar.rs#L354), [`core/dr-pipeline/src/sidecar.rs:452`](../core/dr-pipeline/src/sidecar.rs#L452), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/mask_sidecar.rs:1055`](../core/dr-pipeline/tests/mask_sidecar.rs#L1055), [`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:1031`](../ui/dr-ui/src/library.rs#L1031), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877) | | FR-PLAT-AND-2 | [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-sync-folder/src/lib.rs:242`](../core/dr-sync-folder/src/lib.rs#L242), [`core/dr-sync-folder/src/tests.rs:51`](../core/dr-sync-folder/src/tests.rs#L51), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/error.rs:195`](../core/dr-sync/src/error.rs#L195), [`core/dr-sync/src/scan.rs:175`](../core/dr-sync/src/scan.rs#L175), [`core/dr-sync/src/scan.rs:464`](../core/dr-sync/src/scan.rs#L464), [`core/dr-sync/src/scan.rs:490`](../core/dr-sync/src/scan.rs#L490), [`core/dr-sync/src/scan.rs:519`](../core/dr-sync/src/scan.rs#L519), [`ui/dr-ui/src/library.rs:1222`](../ui/dr-ui/src/library.rs#L1222), [`ui/dr-ui/src/library.rs:1275`](../ui/dr-ui/src/library.rs#L1275), [`ui/dr-ui/src/library.rs:1306`](../ui/dr-ui/src/library.rs#L1306), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library_ui.rs:1162`](../ui/dr-ui/src/library_ui.rs#L1162), [`ui/dr-ui/src/library_ui.rs:1241`](../ui/dr-ui/src/library_ui.rs#L1241), [`ui/dr-ui/src/library_ui.rs:1840`](../ui/dr-ui/src/library_ui.rs#L1840), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:469`](../ui/dr-ui/src/library_ui.rs#L469) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | | FR-PLAT-AND-4 | [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | -| FR-PLAT-AND-5 | [`apps/darkroom-android/src/lib.rs:166`](../apps/darkroom-android/src/lib.rs#L166), [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-gpu/src/detail.rs:161`](../core/dr-gpu/src/detail.rs#L161), [`core/dr-gpu/src/detail.rs:547`](../core/dr-gpu/src/detail.rs#L547), [`core/dr-gpu/tests/detail_stage.rs:417`](../core/dr-gpu/tests/detail_stage.rs#L417), [`ui/dr-ui/src/develop.rs:3336`](../ui/dr-ui/src/develop.rs#L3336), [`ui/dr-ui/src/identity_ui.rs:121`](../ui/dr-ui/src/identity_ui.rs#L121), [`ui/dr-ui/src/lib.rs:1122`](../ui/dr-ui/src/lib.rs#L1122), [`ui/dr-ui/src/lib.rs:1571`](../ui/dr-ui/src/lib.rs#L1571), [`ui/dr-ui/src/memory.rs:163`](../ui/dr-ui/src/memory.rs#L163), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | +| FR-PLAT-AND-5 | [`apps/darkroom-android/src/lib.rs:166`](../apps/darkroom-android/src/lib.rs#L166), [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-gpu/src/detail.rs:161`](../core/dr-gpu/src/detail.rs#L161), [`core/dr-gpu/src/detail.rs:547`](../core/dr-gpu/src/detail.rs#L547), [`core/dr-gpu/tests/detail_stage.rs:417`](../core/dr-gpu/tests/detail_stage.rs#L417), [`ui/dr-ui/src/develop.rs:3447`](../ui/dr-ui/src/develop.rs#L3447), [`ui/dr-ui/src/identity_ui.rs:121`](../ui/dr-ui/src/identity_ui.rs#L121), [`ui/dr-ui/src/lib.rs:1122`](../ui/dr-ui/src/lib.rs#L1122), [`ui/dr-ui/src/lib.rs:1571`](../ui/dr-ui/src/lib.rs#L1571), [`ui/dr-ui/src/memory.rs:163`](../ui/dr-ui/src/memory.rs#L163), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | | FR-PLAT-AND-6 | [`apps/darkroom-android/src/lib.rs:388`](../apps/darkroom-android/src/lib.rs#L388) | | 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:998`](../ui/dr-ui/src/lib.rs#L998), [`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-PLG-8 | [`core/dr-pipeline/src/sidecar.rs:1762`](../core/dr-pipeline/src/sidecar.rs#L1762), [`core/dr-pipeline/src/sidecar.rs:1795`](../core/dr-pipeline/src/sidecar.rs#L1795) | -| FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:132`](../core/dr-types/src/lib.rs#L132), [`core/dr-types/src/lib.rs:203`](../core/dr-types/src/lib.rs#L203) | +| FR-PLG-8 | [`core/dr-pipeline/src/sidecar.rs:1807`](../core/dr-pipeline/src/sidecar.rs#L1807), [`core/dr-pipeline/src/sidecar.rs:1840`](../core/dr-pipeline/src/sidecar.rs#L1840) | +| 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:231`](../ui/dr-ui/src/lib.rs#L231) | | 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:100`](../ui/dr-ui/src/lib.rs#L100), [`ui/dr-ui/src/lib.rs:107`](../ui/dr-ui/src/lib.rs#L107), [`ui/dr-ui/src/lib.rs:3322`](../ui/dr-ui/src/lib.rs#L3322), [`ui/dr-ui/src/masks_ui.rs:884`](../ui/dr-ui/src/masks_ui.rs#L884), [`ui/dr-ui/ui/app.slint:2240`](../ui/dr-ui/ui/app.slint#L2240), [`ui/dr-ui/ui/identity.slint:188`](../ui/dr-ui/ui/identity.slint#L188), [`ui/dr-ui/ui/library.slint:996`](../ui/dr-ui/ui/library.slint#L996), [`ui/dr-ui/ui/toolrail.slint:88`](../ui/dr-ui/ui/toolrail.slint#L88) | | FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1045`](../ui/dr-ui/src/collections_ui.rs#L1045), [`ui/dr-ui/src/collections_ui.rs:1055`](../ui/dr-ui/src/collections_ui.rs#L1055), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1569`](../ui/dr-ui/src/collections_ui.rs#L1569), [`ui/dr-ui/src/collections_ui.rs:1821`](../ui/dr-ui/src/collections_ui.rs#L1821), [`ui/dr-ui/src/collections_ui.rs:2926`](../ui/dr-ui/src/collections_ui.rs#L2926), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:100`](../ui/dr-ui/src/lib.rs#L100), [`ui/dr-ui/src/library_ui.rs:313`](../ui/dr-ui/src/library_ui.rs#L313), [`ui/dr-ui/src/library_ui.rs:5688`](../ui/dr-ui/src/library_ui.rs#L5688), [`ui/dr-ui/src/library_ui.rs:5833`](../ui/dr-ui/src/library_ui.rs#L5833), [`ui/dr-ui/src/library_ui.rs:7043`](../ui/dr-ui/src/library_ui.rs#L7043), [`ui/dr-ui/ui/app.slint:566`](../ui/dr-ui/ui/app.slint#L566), [`ui/dr-ui/ui/app.slint:580`](../ui/dr-ui/ui/app.slint#L580), [`ui/dr-ui/ui/app.slint:60`](../ui/dr-ui/ui/app.slint#L60), [`ui/dr-ui/ui/app.slint:931`](../ui/dr-ui/ui/app.slint#L931), [`ui/dr-ui/ui/controls.slint:593`](../ui/dr-ui/ui/controls.slint#L593), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1202`](../ui/dr-ui/ui/library.slint#L1202), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:1306`](../ui/dr-ui/ui/library.slint#L1306), [`ui/dr-ui/ui/library.slint:2490`](../ui/dr-ui/ui/library.slint#L2490), [`ui/dr-ui/ui/library.slint:4086`](../ui/dr-ui/ui/library.slint#L4086), [`ui/dr-ui/ui/library.slint:885`](../ui/dr-ui/ui/library.slint#L885), [`ui/dr-ui/ui/library.slint:918`](../ui/dr-ui/ui/library.slint#L918), [`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) | -| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2324`](../ui/dr-ui/src/develop.rs#L2324), [`ui/dr-ui/src/develop.rs:2433`](../ui/dr-ui/src/develop.rs#L2433), [`ui/dr-ui/src/library_ui.rs:4795`](../ui/dr-ui/src/library_ui.rs#L4795), [`ui/dr-ui/src/masks_ui.rs:249`](../ui/dr-ui/src/masks_ui.rs#L249), [`ui/dr-ui/src/masks_ui.rs:976`](../ui/dr-ui/src/masks_ui.rs#L976), [`ui/dr-ui/src/masks_ui.rs:998`](../ui/dr-ui/src/masks_ui.rs#L998), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2022`](../ui/dr-ui/ui/app.slint#L2022), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/masks.slint:611`](../ui/dr-ui/ui/masks.slint#L611) | +| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2434`](../ui/dr-ui/src/develop.rs#L2434), [`ui/dr-ui/src/develop.rs:2543`](../ui/dr-ui/src/develop.rs#L2543), [`ui/dr-ui/src/library_ui.rs:4795`](../ui/dr-ui/src/library_ui.rs#L4795), [`ui/dr-ui/src/masks_ui.rs:249`](../ui/dr-ui/src/masks_ui.rs#L249), [`ui/dr-ui/src/masks_ui.rs:976`](../ui/dr-ui/src/masks_ui.rs#L976), [`ui/dr-ui/src/masks_ui.rs:998`](../ui/dr-ui/src/masks_ui.rs#L998), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2022`](../ui/dr-ui/ui/app.slint#L2022), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/masks.slint:611`](../ui/dr-ui/ui/masks.slint#L611) | | FR-UI-4 | [`tools/traceability/src/gestures.rs:1`](../tools/traceability/src/gestures.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1045`](../ui/dr-ui/src/collections_ui.rs#L1045), [`ui/dr-ui/src/collections_ui.rs:1055`](../ui/dr-ui/src/collections_ui.rs#L1055), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:126`](../ui/dr-ui/src/collections_ui.rs#L126), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1569`](../ui/dr-ui/src/collections_ui.rs#L1569), [`ui/dr-ui/src/collections_ui.rs:1685`](../ui/dr-ui/src/collections_ui.rs#L1685), [`ui/dr-ui/src/collections_ui.rs:1821`](../ui/dr-ui/src/collections_ui.rs#L1821), [`ui/dr-ui/src/collections_ui.rs:1848`](../ui/dr-ui/src/collections_ui.rs#L1848), [`ui/dr-ui/src/collections_ui.rs:1964`](../ui/dr-ui/src/collections_ui.rs#L1964), [`ui/dr-ui/src/collections_ui.rs:2926`](../ui/dr-ui/src/collections_ui.rs#L2926), [`ui/dr-ui/src/collections_ui.rs:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:365`](../ui/dr-ui/src/collections_ui.rs#L365), [`ui/dr-ui/src/collections_ui.rs:569`](../ui/dr-ui/src/collections_ui.rs#L569), [`ui/dr-ui/src/collections_ui.rs:589`](../ui/dr-ui/src/collections_ui.rs#L589), [`ui/dr-ui/src/gesture_book.rs:3`](../ui/dr-ui/src/gesture_book.rs#L3), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/library_ui.rs:4795`](../ui/dr-ui/src/library_ui.rs#L4795), [`ui/dr-ui/src/library_ui.rs:4807`](../ui/dr-ui/src/library_ui.rs#L4807), [`ui/dr-ui/src/library_ui.rs:4861`](../ui/dr-ui/src/library_ui.rs#L4861), [`ui/dr-ui/src/library_ui.rs:4964`](../ui/dr-ui/src/library_ui.rs#L4964), [`ui/dr-ui/src/library_ui.rs:4992`](../ui/dr-ui/src/library_ui.rs#L4992), [`ui/dr-ui/src/library_ui.rs:5821`](../ui/dr-ui/src/library_ui.rs#L5821), [`ui/dr-ui/src/library_ui.rs:5833`](../ui/dr-ui/src/library_ui.rs#L5833), [`ui/dr-ui/ui/app.slint:1862`](../ui/dr-ui/ui/app.slint#L1862), [`ui/dr-ui/ui/app.slint:542`](../ui/dr-ui/ui/app.slint#L542), [`ui/dr-ui/ui/app.slint:571`](../ui/dr-ui/ui/app.slint#L571), [`ui/dr-ui/ui/app.slint:580`](../ui/dr-ui/ui/app.slint#L580), [`ui/dr-ui/ui/app.slint:665`](../ui/dr-ui/ui/app.slint#L665), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1177`](../ui/dr-ui/ui/library.slint#L1177), [`ui/dr-ui/ui/library.slint:1202`](../ui/dr-ui/ui/library.slint#L1202), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:1242`](../ui/dr-ui/ui/library.slint#L1242), [`ui/dr-ui/ui/library.slint:1299`](../ui/dr-ui/ui/library.slint#L1299), [`ui/dr-ui/ui/library.slint:1306`](../ui/dr-ui/ui/library.slint#L1306), [`ui/dr-ui/ui/library.slint:2490`](../ui/dr-ui/ui/library.slint#L2490), [`ui/dr-ui/ui/library.slint:3970`](../ui/dr-ui/ui/library.slint#L3970), [`ui/dr-ui/ui/library.slint:4086`](../ui/dr-ui/ui/library.slint#L4086), [`ui/dr-ui/ui/library.slint:885`](../ui/dr-ui/ui/library.slint#L885), [`ui/dr-ui/ui/library.slint:912`](../ui/dr-ui/ui/library.slint#L912), [`ui/dr-ui/ui/library.slint:918`](../ui/dr-ui/ui/library.slint#L918), [`ui/dr-ui/ui/library.slint:973`](../ui/dr-ui/ui/library.slint#L973) | | 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:2750`](../ui/dr-ui/src/lib.rs#L2750), [`ui/dr-ui/src/lib.rs:3361`](../ui/dr-ui/src/lib.rs#L3361), [`ui/dr-ui/src/lib.rs:3546`](../ui/dr-ui/src/lib.rs#L3546), [`ui/dr-ui/src/masks_ui.rs:931`](../ui/dr-ui/src/masks_ui.rs#L931), [`ui/dr-ui/ui/app.slint:121`](../ui/dr-ui/ui/app.slint#L121), [`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) | @@ -137,7 +137,7 @@ _None._ | 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/bench/src/catalog_open.rs:1`](../tools/bench/src/catalog_open.rs#L1) | | 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:3231`](../ui/dr-ui/src/develop.rs#L3231), [`ui/dr-ui/src/lib.rs:1813`](../ui/dr-ui/src/lib.rs#L1813) | +| 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:3342`](../ui/dr-ui/src/develop.rs#L3342), [`ui/dr-ui/src/lib.rs:1813`](../ui/dr-ui/src/lib.rs#L1813) | | NFR-P3 | [`tools/bench/src/thumbnails.rs:1`](../tools/bench/src/thumbnails.rs#L1) | | NFR-P5 | [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/library.rs:4257`](../ui/dr-ui/src/library.rs#L4257), [`ui/dr-ui/src/library.rs:5191`](../ui/dr-ui/src/library.rs#L5191), [`ui/dr-ui/src/library_ui.rs:4468`](../ui/dr-ui/src/library_ui.rs#L4468), [`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:992`](../ui/dr-ui/src/export.rs#L992), [`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) | @@ -150,7 +150,7 @@ _None._ | NFR-R6 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/lib.rs:255`](../core/dr-catalog/src/lib.rs#L255), [`core/dr-catalog/src/recovery.rs:1`](../core/dr-catalog/src/recovery.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-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/develop.rs:3336`](../ui/dr-ui/src/develop.rs#L3336), [`ui/dr-ui/src/identity_ui.rs:121`](../ui/dr-ui/src/identity_ui.rs#L121), [`ui/dr-ui/src/lib.rs:92`](../ui/dr-ui/src/lib.rs#L92), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | +| NFR-RES-1 | [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/develop.rs:3447`](../ui/dr-ui/src/develop.rs#L3447), [`ui/dr-ui/src/identity_ui.rs:121`](../ui/dr-ui/src/identity_ui.rs#L121), [`ui/dr-ui/src/lib.rs:92`](../ui/dr-ui/src/lib.rs#L92), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | | NFR-RES-2 | [`ui/dr-ui/src/faces.rs:346`](../ui/dr-ui/src/faces.rs#L346), [`ui/dr-ui/src/library.rs:3058`](../ui/dr-ui/src/library.rs#L3058), [`ui/dr-ui/src/library.rs:3249`](../ui/dr-ui/src/library.rs#L3249) | | 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:725`](../core/dr-catalog/src/schema.rs#L725), [`core/dr-gpu/src/lib.rs:93`](../core/dr-gpu/src/lib.rs#L93), [`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:2964`](../ui/dr-ui/src/library.rs#L2964) | | NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) | diff --git a/ui/dr-ui/src/develop.rs b/ui/dr-ui/src/develop.rs index 28198c2..c3652c6 100644 --- a/ui/dr-ui/src/develop.rs +++ b/ui/dr-ui/src/develop.rs @@ -9,6 +9,7 @@ //! declared [`ParamKind`], not from which parameter it is (ARCH §4.3), so a //! new operation appears in the panel with no change here (FR-DEV-3c). +use std::borrow::Cow; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; @@ -1868,63 +1869,172 @@ impl DevelopSession { h } - /// Rebuild the distance fields if anything they depend on moved. - fn ensure_subject_fields(&mut self, ctx: &GpuContext) { + /// One layer's coverage: what the model says now, or what the sidecar + /// remembered it saying. + /// + /// **The model first, always.** It is the live answer, it is the only one + /// that can respond to the refine control, and a run in this sitting is by + /// definition newer than anything a file was holding. + /// + /// The fallback is the point of the stored raster. A photograph reopened, + /// and a batch export from the grid — which opens a session, applies a + /// version and never runs a model at all — have no segmentation to ask, so + /// before this they resolved every subject and category layer to nothing + /// and wrote out a file missing the local adjustments, with a line in the + /// log as the only sign. See [`dr_pipeline::coverage`]. + /// + /// `None` for every other source: a gradient and a brush are rasterised + /// from their own geometry and have no coverage to fetch, and the caller + /// gives them a placeholder field so the slot indices still line up. + fn layer_coverage<'a>( + &'a self, + layer: &'a dr_pipeline::mask::MaskLayer, + width: usize, + height: usize, + ) -> Option> { use dr_pipeline::mask::MaskSource; + let live = self + .segmentation + .as_ref() + .and_then(|seg| match &layer.source { + MaskSource::Category { name, .. } => seg.category_mask_at(name, layer.refine), + MaskSource::Subject { index, .. } => { + seg.instance_mask(*index as usize).map(Cow::Borrowed) + } + _ => None, + }); + if live.is_some() { + return live; + } + + match layer.source { + // Resampled where it was written against a different proxy edge; + // in the ordinary case the sizes match and this is the decode. + MaskSource::Subject { .. } | MaskSource::Category { .. } => layer + .coverage + .as_ref() + .map(|stored| Cow::Owned(stored.decode_at(width, height))), + _ => None, + } + } + + /// TRACES: FR-CAT-8 | FR-DEV-3 + /// The mask stack as it should be written to a sidecar. + /// + /// The stack the graph holds, with each model layer's coverage brought up + /// to what the segmentation now says it is. That raster is what lets the + /// *next* opening of this photograph render the layer without a model run + /// — the whole of [`dr_pipeline::coverage`]'s reason to exist. + /// + /// # Why here, and not where the field is built + /// + /// `ensure_subject_fields` is the tempting place: it is the one funnel + /// every coverage passes through, so recording it there would catch every + /// route automatically. But it runs on a *drag* — dilating a mask with a + /// compound morphology rebuilds the field every frame — and encoding a + /// megapixel raster per frame is exactly the kind of work NFR-P5 is about. + /// + /// Saving happens when the photograph stops being the open one, once, and + /// already costs a network round trip. So the encoding is done here, where + /// nothing is waiting on it, and the session's own rendering goes on + /// reading the model directly. + /// + /// Returns the stack by value rather than mutating: the caller is + /// serialising, not editing, and a graph that quietly gained a field on + /// the way past would be a mutation nobody asked for and undo would not + /// know about. + pub fn masks_for_storage(&self) -> dr_pipeline::mask::MaskStack { + use dr_pipeline::coverage::{Coverage, RENDERED_LEVELS}; + use dr_pipeline::mask::MaskSource; + + let mut stack = self.graph.masks().clone(); + let Some(seg) = self.segmentation.as_ref() else { + // No model has run this sitting, so whatever the layers arrived + // holding is still the best answer anyone has. Handing the stack + // back untouched is what stops a photograph that was opened, + // glanced at and closed from losing the coverage its own sidecar + // gave it. + return stack; + }; + let (pw, ph) = seg.proxy_size(); + + for layer in stack.layers_mut() { + let values = match &layer.source { + MaskSource::Category { name, .. } => seg.category_mask_at(name, layer.refine), + MaskSource::Subject { index, .. } => { + seg.instance_mask(*index as usize).map(Cow::Borrowed) + } + // Nothing else has a model behind it. Left alone rather than + // cleared, so a hand-written file's key survives a round trip + // even though nothing samples it. + _ => continue, + }; + // The layer names something this run does not contain — a stale + // index, a category the scene model no longer offers. Keeping what + // was stored is right: it is a mask that was once correct, and the + // panel is already telling the user the layer is stale. + let Some(values) = values else { continue }; + + // `None` from the encoder means "will not fit in a sidecar", and + // the stored raster is then cleared rather than left standing. It + // would describe the layer at some earlier refine, and a mask of + // the wrong shape presented as authoritative is worse than the + // honest state, which is that this one needs the model run. + layer.coverage = Coverage::encode(&values, pw, ph, RENDERED_LEVELS).map(Arc::new); + } + + stack + } + + /// Rebuild the distance fields if anything they depend on moved. + fn ensure_subject_fields(&mut self, ctx: &GpuContext) { let key = self.subject_signature(); if key == self.subject_key && self.subjects.is_some() { return; } - let Some(seg) = self.segmentation.as_ref() else { - return; + // The proxy the fields are measured in: the segmentation's own where + // one has been run, and otherwise the size the mask array is + // rasterised at. `mask_raster_size` derives that from the photograph + // rather than from a segmentation for exactly this case, and the two + // are the same number by construction — see there. + let (pw, ph) = match self.segmentation.as_ref() { + Some(seg) => seg.proxy_size(), + None => { + let (w, h) = self.mask_raster_size(); + (w as usize, h as usize) + } }; - let (pw, ph) = seg.proxy_size(); // In `active()` order, because that is the order the rasteriser walks // and the order it indexes these by. let mut fields: Vec> = Vec::new(); for layer in self.graph.masks().active() { - let field = match &layer.source { - MaskSource::Category { name, .. } => seg - .category_mask_at(name, layer.refine) - .map(|coverage| { - dr_segment::Shaped::build( - &coverage, - pw, - ph, - 128, - morphology_for(layer.morphology), - layer.morph_radius * pw.min(ph) as f32, - ) - .distance - }) - // Full size, never `unwrap_or_default`: an empty vec is a - // wrong-sized field, `SubjectMasks::upload` rejects the - // whole batch on one, and every other layer in the stack - // then loses its mask too. One stale name should cost one - // layer, not all of them. - .unwrap_or_else(|| vec![-1.0; pw * ph]), - MaskSource::Subject { index, .. } => seg - .instance_mask(*index as usize) - .map(|coverage| { - dr_segment::Shaped::build( - coverage, - pw, - ph, - 128, - morphology_for(layer.morphology), - // Radii are fractions of the shorter edge; the - // field is in proxy pixels. - layer.morph_radius * pw.min(ph) as f32, - ) - .distance - }) - .unwrap_or_else(|| vec![-1.0; pw * ph]), - // A placeholder of the right size, so the slot indices line up - // with `active()` whatever mix of sources the stack holds. - _ => vec![-1.0; pw * ph], + let field = match self.layer_coverage(layer, pw, ph) { + Some(coverage) => { + dr_segment::Shaped::build( + &coverage, + pw, + ph, + 128, + morphology_for(layer.morphology), + // Radii are fractions of the shorter edge; the field + // is in proxy pixels. + layer.morph_radius * pw.min(ph) as f32, + ) + .distance + } + // Full size, never `unwrap_or_default`: an empty vec is a + // wrong-sized field, `SubjectMasks::upload` rejects the whole + // batch on one, and every other layer in the stack then loses + // its mask too. One stale name should cost one layer, not all + // of them. + // + // It is also the placeholder a gradient or a brush gets, so + // the slot indices line up with `active()` whatever mix of + // sources the stack holds. + None => vec![-1.0; pw * ph], }; fields.push(field); } @@ -2978,10 +3088,11 @@ impl DevelopSession { }; match self.segmentation.as_ref() { Some(seg) => layer.is_stale(seg.signature()), - // Nothing loaded to compare against. Not stale, just unrenderable - // — the distinction matters because "stale" invites the user to - // recompute the selection and this only needs the segmentation - // running. + // Nothing loaded to compare against, and nothing to report: the + // layer renders from the raster its sidecar stored (see + // `layer_coverage`). It was already not *stale* before that — the + // word invites the user to recompute a selection that is fine — + // and now it is not unrenderable either. None => false, } } @@ -4757,6 +4868,365 @@ mod tests { ); } + // ---------------------------------------------------------------------- + // Stored coverage (dr_pipeline::coverage) + // ---------------------------------------------------------------------- + // + // A subject or category layer is stored in the sidecar as identity — which + // run, which instance, which category — and identity resolves to pixels + // only while that run is in memory. So reopening an edited photograph + // dropped every model-backed local adjustment, and a batch export, which + // never runs a model at all, could not have them at any point. Both + // failures were silent: the shader still emitted the layer's block, the + // placeholder multiplied it by zero, and the result was a well-formed + // frame with the adjustment simply absent. + + /// A stored edit holding one subject layer over the left half of the + /// frame, as a session that *had* run the model would have written it. + /// + /// `stored` is the whole variable: with the raster, this is a sidecar + /// written by a build that persists coverage; without it, one written + /// before that existed. Everything else about the two is identical, which + /// is what makes the pair of tests below a measurement rather than an + /// assertion about two different edits. + fn version_with_a_subject(proxy: (u32, u32), stored: bool) -> dr_pipeline::Version { + use dr_pipeline::coverage::{Coverage, RENDERED_LEVELS}; + + let (pw, ph) = (proxy.0 as usize, proxy.1 as usize); + let mut values = vec![0u8; pw * ph]; + for y in 0..ph { + for x in 0..pw / 2 { + values[y * pw + x] = 255; + } + } + + let mut layer = MaskLayer::new( + "m1", + MaskSource::Subject { + signature: 0xfeed, + index: 0, + class: "dog".into(), + score: 0.9, + }, + ); + layer.set_param("exposure", ParamId("exposure"), 2.0); + if stored { + layer.coverage = Some(Arc::new( + Coverage::encode(&values, pw, ph, RENDERED_LEVELS).expect("a half frame encodes"), + )); + } + + let mut graph = EditGraph::default_chain(); + graph.masks_mut().push(layer); + dr_pipeline::Version::from_graph("default", "Default", &graph) + } + + /// A flat grey session with nothing segmented, and its render. + fn grey_session(ctx: &GpuContext) -> (DevelopSession, Vec) { + let rgba: Vec = (0..64 * 64).flat_map(|_| [128u8, 128, 128, 255]).collect(); + let mut session = + DevelopSession::open_rgb(ctx, &rgba, 64, 64, dr_types::Orientation::NORMAL) + .expect("session"); + assert!( + !session.has_segmentation(), + "the premise: no model has been run" + ); + let before = read_back(ctx, &session.render(64, 64).expect("render")); + (session, before) + } + + /// TRACES: FR-DEV-3 | FR-CAT-8 + /// Reopening an edited photograph renders its subject mask, with no model. + /// + /// This is the failure the stored raster exists for. `apply_version` is + /// exactly what opening a photograph from the library does with the + /// sidecar it fetched, and until the coverage went into the file the layer + /// resolved to nothing every time. + #[test] + fn a_stored_subject_mask_renders_with_no_model_run() { + let Some(ctx) = headless() else { return }; + let (mut session, before) = grey_session(&ctx); + + let proxy = session.mask_raster_size(); + session.apply_version(&version_with_a_subject(proxy, true)); + let after = read_back(&ctx, &session.render(64, 64).expect("render")); + + let at = |px: &[u8], x: usize, y: usize| px[(y * 64 + x) * 4]; + assert!( + at(&after, 16, 32) > at(&before, 16, 32) + 20, + "the covered half must brighten: {} against {}", + at(&after, 16, 32), + at(&before, 16, 32) + ); + // And only that half. A mask that failed the other way — covering + // everything — would pass the assertion above and is the louder bug. + assert_eq!( + at(&after, 56, 32), + at(&before, 56, 32), + "the uncovered half must not move" + ); + } + + /// The control for the test above: the same edit with the raster left out + /// is the behaviour every build had before this, which is no mask at all. + /// + /// Worth pinning down in both directions. It is what a sidecar written by + /// an older build looks like, and reading one must go on being harmless — + /// the layer needs the model run, exactly as it always did, rather than + /// rendering as an empty mask or as the whole frame. + #[test] + fn a_subject_mask_with_no_stored_coverage_still_needs_the_model() { + let Some(ctx) = headless() else { return }; + let (mut session, before) = grey_session(&ctx); + + let proxy = session.mask_raster_size(); + session.apply_version(&version_with_a_subject(proxy, false)); + let after = read_back(&ctx, &session.render(64, 64).expect("render")); + + assert_eq!( + after, before, + "with no coverage the layer contributes nothing" + ); + } + + /// TRACES: FR-EXP-9 | FR-CAT-8 + /// A batch export renders the stored mask too. + /// + /// `export::render_from_library` fetches the RAW and the sidecar, opens a + /// session, applies the version and calls `render_for_export` — and it + /// runs no model on the way, so before the coverage was stored a batch + /// wrote out files with the photographer's local adjustments missing, over + /// a log warning nobody was reading. These two lines are that path with + /// the fetch taken out. + #[test] + fn an_export_renders_a_stored_subject_mask() { + let Some(ctx) = headless() else { return }; + let rgba: Vec = (0..64 * 64).flat_map(|_| [128u8, 128, 128, 255]).collect(); + let mut session = + DevelopSession::open_rgb(&ctx, &rgba, 64, 64, dr_types::Orientation::NORMAL) + .expect("session"); + + let plain = session + .render_for_export(dr_types::ColourSpace::Srgb) + .expect("export"); + + let proxy = session.mask_raster_size(); + session.apply_version(&version_with_a_subject(proxy, true)); + let masked = session + .render_for_export(dr_types::ColourSpace::Srgb) + .expect("export"); + + let at = |f: &dr_export::Frame, x: usize, y: usize| f.rgba[(y * 64 + x) * 4]; + assert!( + at(&masked, 16, 32) > at(&plain, 16, 32) + 20, + "the exported file must carry the local adjustment: {} against {}", + at(&masked, 16, 32), + at(&plain, 16, 32) + ); + assert_eq!( + at(&masked, 56, 32), + at(&plain, 56, 32), + "and only where the mask covers" + ); + } + + /// What a session hands the sidecar writer when no model has run. + /// + /// Untouched, and that is the whole of it: a photograph opened, looked at + /// and closed must not have the coverage its own sidecar gave it stripped + /// out on the way past. Saving is automatic, so a stack that lost the + /// raster here would lose it on disk within seconds of being opened. + #[test] + fn saving_without_a_model_keeps_the_coverage_that_was_loaded() { + let Some(ctx) = headless() else { return }; + let (mut session, _) = grey_session(&ctx); + + let proxy = session.mask_raster_size(); + session.apply_version(&version_with_a_subject(proxy, true)); + let stored = session.masks_for_storage(); + + assert_eq!(stored.len(), 1); + assert!( + stored.layers()[0].coverage.is_some(), + "the raster the sidecar gave us must go back to the sidecar" + ); + } + + /// A session holding a segmentation with one instance over the left half. + /// + /// Built rather than detected. What these tests need is coverage of a + /// *known* shape, so that "the stored raster renders what the model's did" + /// is a comparison rather than a hope — and asking a real run what it + /// happened to find in a synthetic frame would make the assertion depend + /// on the weights. `segmented_session` above is the one that runs a model, + /// and it goes on doing so. + fn session_with_a_left_half_subject(ctx: &GpuContext) -> DevelopSession { + let rgba: Vec = (0..64 * 64).flat_map(|_| [128u8, 128, 128, 255]).collect(); + let mut session = + DevelopSession::open_rgb(ctx, &rgba, 64, 64, dr_types::Orientation::NORMAL) + .expect("session"); + + let (pw, ph) = session.mask_raster_size(); + let (pw, ph) = (pw as usize, ph as usize); + let mut mask = vec![0u8; pw * ph]; + for y in 0..ph { + for x in 0..pw / 2 { + mask[y * pw + x] = 255; + } + } + + session.segmentation = Some(segmentation::Segmentation::for_test( + vec![segmentation::InstanceSummary { + class_name: "dog".into(), + score: 0.9, + mask, + bbox: (0.0, 0.0, (pw / 2) as f32, ph as f32), + }], + Vec::new(), + 0xfeed, + (pw, ph), + )); + session.subjects = None; + session.subject_key = 0; + session + } + + /// TRACES: FR-DEV-3 | FR-CAT-8 + /// The whole claim, end to end: what is stored renders what was rendered. + /// + /// A session with a model's coverage in hand draws the mask; the stack it + /// hands the sidecar writer goes through the file and into a session with + /// no model at all; and the two frames must be the same. Anything weaker + /// — that the coverage is present, that it round-trips as bytes — would + /// still pass if the raster came back at the wrong scale, upside down, or + /// a threshold out. + #[test] + fn a_stored_mask_renders_exactly_what_the_model_rendered() { + let Some(ctx) = headless() else { return }; + + let mut live = session_with_a_left_half_subject(&ctx); + let id = live.add_subject_mask(0).expect("a subject layer"); + live.graph + .masks_mut() + .get_mut(&id) + .expect("the layer") + .set_param("exposure", ParamId("exposure"), 2.0); + let with_model = read_back(&ctx, &live.render(64, 64).expect("render")); + + // Through the sidecar, the way `presets::save_open_edit` does it: the + // graph's own parameters, with the stack that carries the coverage + // substituted for the one the graph is holding. + let mut version = dr_pipeline::Version::from_graph("default", "Default", &live.graph); + version.masks = live.masks_for_storage(); + let mut sidecar = dr_pipeline::Sidecar::new(); + sidecar.put(version); + let text = sidecar.to_text(); + let parsed = dr_pipeline::Sidecar::parse(&text).expect("reparse"); + + let rgba: Vec = (0..64 * 64).flat_map(|_| [128u8, 128, 128, 255]).collect(); + let mut reopened = + DevelopSession::open_rgb(&ctx, &rgba, 64, 64, dr_types::Orientation::NORMAL) + .expect("session"); + assert!( + !reopened.has_segmentation(), + "the point: nothing has run a model in this session" + ); + reopened.apply_version(parsed.default_version().expect("a version")); + let from_the_file = read_back(&ctx, &reopened.render(64, 64).expect("render")); + + assert_eq!( + from_the_file, with_model, + "the reopened frame must be the frame the model produced" + ); + // And that both are actually a mask rather than both being nothing. + let at = |px: &[u8], x: usize| px[(32 * 64 + x) * 4]; + assert!( + at(&with_model, 16) > at(&with_model, 56) + 20, + "the premise: the live render really is masked ({} against {})", + at(&with_model, 16), + at(&with_model, 56) + ); + } + + /// And what a session hands over when a model *has* run: the coverage + /// folded in, at the proxy the distance field is measured in. + /// + /// The encoding happens here rather than where the field is built because + /// that runs on a drag — see `masks_for_storage`. + #[test] + fn saving_after_a_model_run_folds_the_coverage_in() { + let Some(ctx) = headless() else { return }; + let mut session = session_with_a_left_half_subject(&ctx); + let id = session.add_subject_mask(0).expect("a subject layer"); + + let stored = session.masks_for_storage(); + let coverage = stored + .get(&id) + .expect("the layer is in the stack") + .coverage + .as_ref() + .expect("a subject the model found has coverage to store"); + + let (pw, ph) = session.mask_raster_size(); + assert_eq!( + (coverage.width(), coverage.height()), + (pw as usize, ph as usize) + ); + let decoded = coverage.decode(); + assert_eq!(decoded[32 * pw as usize + 8], 255, "inside the subject"); + assert_eq!(decoded[32 * pw as usize + 56], 0, "outside it"); + assert!( + coverage.encoded_len() < 512, + "a half-frame mask is a handful of runs, not {} bytes", + coverage.encoded_len() + ); + } + + /// A layer whose coverage came from the file must still take the model's + /// once a model runs — the live answer is the only one that responds to + /// the refine control, and a run in this sitting is newer than a file. + #[test] + fn a_model_run_takes_precedence_over_what_was_stored() { + let Some(ctx) = headless() else { return }; + let mut session = session_with_a_left_half_subject(&ctx); + + // Stored coverage saying the *right* half, against a segmentation + // saying the left. + let (pw, ph) = session.mask_raster_size(); + let (pw, ph) = (pw as usize, ph as usize); + let mut mirrored = vec![0u8; pw * ph]; + for y in 0..ph { + for x in pw / 2..pw { + mirrored[y * pw + x] = 255; + } + } + + let id = session.add_subject_mask(0).expect("a subject layer"); + { + let layer = session.graph.masks_mut().get_mut(&id).expect("the layer"); + layer.set_param("exposure", ParamId("exposure"), 2.0); + layer.coverage = Some(Arc::new( + dr_pipeline::Coverage::encode( + &mirrored, + pw, + ph, + dr_pipeline::coverage::RENDERED_LEVELS, + ) + .expect("encode"), + )); + } + + let frame = read_back(&ctx, &session.render(64, 64).expect("render")); + let at = |x: usize| frame[(32 * 64 + x) * 4]; + assert!( + at(16) > at(56) + 20, + "the model's left half must win over the file's right half: \ + {} against {}", + at(16), + at(56) + ); + } + /// TRACES: FR-DEV-3 /// Multi-select: one slider, applied to every selected layer. /// diff --git a/ui/dr-ui/src/presets.rs b/ui/dr-ui/src/presets.rs index def724f..05d3c80 100644 --- a/ui/dr-ui/src/presets.rs +++ b/ui/dr-ui/src/presets.rs @@ -391,7 +391,14 @@ pub fn save_open_edit( let Some((preset, masks, film)) = session.borrow().as_ref().map(|s| { ( s.copy_settings(), - s.masks().clone(), + // TRACES: FR-DEV-3 + // Not `masks().clone()`: this is the stack *with the model's + // coverage folded in*, so a subject or category layer survives + // being closed and reopened. Without it the sidecar stored the + // layer's identity and nothing else, and the next session — or a + // batch export, which never runs a model — rendered the + // photograph with the local adjustment silently missing. + s.masks_for_storage(), // TRACES: FR-DEV-3f // Taken in the same borrow as the other two, for the reason the // note above gives: they are one edit, and a stock read from a diff --git a/ui/dr-ui/src/segmentation.rs b/ui/dr-ui/src/segmentation.rs index c8dc26f..616c726 100644 --- a/ui/dr-ui/src/segmentation.rs +++ b/ui/dr-ui/src/segmentation.rs @@ -113,6 +113,31 @@ pub struct Segmentation { } impl Segmentation { + /// TRACES: FR-DEV-3 + /// A segmentation assembled by hand, for tests that need one without a + /// model. + /// + /// Test-only, and it exists because the alternative is worse: the tests + /// that need coverage to be *a particular shape* — that a stored raster + /// renders the same pixels as the model's own did — cannot ask a real run + /// for a shape, and running the model to find out what it happened to + /// detect in a synthetic frame makes the assertion depend on the weights. + /// Every test that is genuinely about the model still runs it. + #[cfg(test)] + pub(crate) fn for_test( + instances: Vec, + categories: Vec, + signature: u64, + proxy: (usize, usize), + ) -> Self { + Self { + instances, + categories, + signature, + proxy, + } + } + pub fn signature(&self) -> u64 { self.signature }