From 8f9e59b9fa30ae96361cd14439f679293eb6ffa9 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 4 Oct 2026 02:38:48 -0400 Subject: [PATCH] Find hot photosites without repairing them, and measure a sensor's aging The hot-pixel pass could only repair: it returned how many photosites it changed and threw away which. find_hot_pixels runs the same pass and returns them as sensor coordinates, leaving the frame alone, so a sensor's defects can be tracked across frames. sensor_scan prints each frame's candidates, and with --probe reads a list of coordinates back out of every frame. Run over 53 6D raws from 2015 to 2026, it found 32 persistent defects, 2 in 2015 and 32 by 2026, and showed what a defect map has to account for: a frame that does not flag a photosite proves nothing unless its neighbourhood is dark, and the 6D hides some of its defects itself above ISO 5000. docs/dev/sensor-health.md records the findings and the design they argue for. --- core/dr-gpu/examples/sensor_scan.rs | 117 ++++++++++++++++++++++++++++ core/dr-gpu/src/demosaic.rs | 70 ++++++++++++++--- core/dr-gpu/src/lib.rs | 2 +- core/dr-gpu/tests/hot_pixels.rs | 42 +++++++++- docs/dev/sensor-health.md | 82 +++++++++++++++++++ docs/dev/traceability.md | 6 +- 6 files changed, 305 insertions(+), 14 deletions(-) create mode 100644 core/dr-gpu/examples/sensor_scan.rs create mode 100644 docs/dev/sensor-health.md diff --git a/core/dr-gpu/examples/sensor_scan.rs b/core/dr-gpu/examples/sensor_scan.rs new file mode 100644 index 0000000..b88dd63 --- /dev/null +++ b/core/dr-gpu/examples/sensor_scan.rs @@ -0,0 +1,117 @@ +//! List each RAW file's hot and dead photosite candidates (docs/dev/sensor-health.md). +//! +//! Reads paths on stdin and prints one JSON line per file: its capture +//! conditions and every photosite [`Demosaicer::find_hot_pixels`] flags, as +//! `[x, y, value, hot]` in sensor coordinates. Which candidates are defects +//! is a question across frames, so this answers nothing on its own. +//! +//! ```sh +//! find ~/Pictures -name '*.CR2' | cargo run --release -p dr-gpu --example sensor_scan +//! ``` + +use std::io::{BufRead, Write}; + +use dr_gpu::{Demosaicer, GpuContext}; + +fn main() { + if let Some(list) = std::env::args().skip_while(|a| a != "--probe").nth(1) { + return probe(&list); + } + let ctx = pollster::block_on(GpuContext::new_headless()).expect("a GPU for the hot-pixel pass"); + let demosaicer = Demosaicer::new(&ctx).expect("demosaicer"); + for line in std::io::stdin().lock().lines() { + let path = line.expect("stdin"); + match scan(&path, &demosaicer) { + Ok(json) => println!("{json}"), + Err(e) => eprintln!("fail\t{path}\t{e}"), + } + std::io::stdout().flush().ok(); + } +} + +fn scan(path: &str, demosaicer: &Demosaicer) -> Result { + let bytes = std::fs::read(path).map_err(|e| e.to_string())?; + let raw = dr_decode::decode(&bytes).map_err(|e| e.to_string())?; + let meta = dr_decode::metadata(&bytes).map_err(|e| e.to_string())?; + let sites = demosaicer + .find_hot_pixels(&raw) + .map_err(|e| e.to_string())?; + let opt = |v: Option| v.map_or("null".to_string(), |v| v.to_string()); + let list: Vec = sites + .iter() + .map(|s| { + let v = raw.data[(s.y * raw.width + s.x) as usize]; + format!("[{},{},{},{}]", s.x, s.y, v, u8::from(s.hot)) + }) + .collect(); + Ok(format!( + "{{\"path\":{:?},\"model\":{:?},\"captured\":{},\"iso\":{},\"shutter\":{},\"white\":{},\"black\":{:?},\"crop\":[{},{},{},{}],\"sites\":[{}]}}", + path, + format!("{} {}", raw.make, raw.model), + meta.captured_at.map_or("null".to_string(), |t| t.to_string()), + opt(meta.iso.map(f64::from)), + opt(meta.shutter.map(f64::from)), + raw.white_level, + raw.black_level, + raw.crop.x, + raw.crop.y, + raw.crop.width, + raw.crop.height, + list.join(","), + )) +} + +/// `--probe COORDS`: for each path on stdin, each `x y` line of COORDS as +/// `[value, same-colour neighbour max, median]` over black, on the CPU. A +/// probe asks whether a photosite stood out in a frame where it would have +/// been visible, which the scan's verdict cannot say: a frame that does not +/// flag a defect may only have been too bright around it. +fn probe(list: &str) { + let coords: Vec<(u32, u32)> = std::fs::read_to_string(list) + .expect("coords") + .lines() + .filter_map(|l| { + let mut it = l.split_whitespace().map(|v| v.parse().ok()); + Some((it.next()??, it.next()??)) + }) + .collect(); + for line in std::io::stdin().lock().lines() { + let path = line.expect("stdin"); + let Ok(bytes) = std::fs::read(&path) else { + continue; + }; + let (Ok(raw), Ok(meta)) = (dr_decode::decode(&bytes), dr_decode::metadata(&bytes)) else { + continue; + }; + let w = raw.width as i64; + let at = |x: i64, y: i64| { + let cell = (((y - raw.crop.y as i64) & 1) * 2 + ((x - raw.crop.x as i64) & 1)) as usize; + raw.data[(y * w + x) as usize].saturating_sub(raw.black_level[cell]) + }; + let rows: Vec = coords + .iter() + .map(|&(x, y)| { + let (x, y) = (x as i64, y as i64); + let mut n: Vec = Vec::new(); + for dy in [-2i64, 0, 2] { + for dx in [-2i64, 0, 2] { + if (dx, dy) != (0, 0) { + n.push(at(x + dx, y + dy)); + } + } + } + n.sort_unstable(); + format!("[{},{},{}]", at(x, y), n[n.len() - 1], n[n.len() / 2]) + }) + .collect(); + println!( + "{{\"path\":{:?},\"captured\":{},\"iso\":{},\"shutter\":{},\"range\":{},\"p\":[{}]}}", + path, + meta.captured_at.unwrap_or(0), + meta.iso.unwrap_or(0), + meta.shutter.unwrap_or(0.0), + raw.white_level - raw.black_level[0], + rows.join(","), + ); + } +} diff --git a/core/dr-gpu/src/demosaic.rs b/core/dr-gpu/src/demosaic.rs index c9c9f5c..6760f1e 100644 --- a/core/dr-gpu/src/demosaic.rs +++ b/core/dr-gpu/src/demosaic.rs @@ -1071,6 +1071,46 @@ impl Demosaicer { if raw.samples_per_pixel != 1 { return Ok(0); } + let words = self.hot_pixel_words(raw)?; + let mut changed = 0; + for (i, v) in raw.data.iter_mut().enumerate() { + let new = unpack_sample(&words, i); + changed += usize::from(new != *v); + *v = new; + } + Ok(changed) + } + + /// The photosites [`Self::repair_hot_pixels`] would replace, in sensor + /// coordinates, without replacing them. + /// + /// For the sensor health record (docs/dev/sensor-health.md): one frame's + /// verdict is a candidate list, not a defect map — a single photosite of a + /// star that passes both tests reads the same as a hot one. Which of them + /// is the sensor is decided across frames, by who keeps coming back. + pub fn find_hot_pixels(&self, raw: &RawImage) -> Result, GpuError> { + if raw.samples_per_pixel != 1 { + return Ok(Vec::new()); + } + let words = self.hot_pixel_words(raw)?; + let stride = raw.width.max(1); + Ok(raw + .data + .iter() + .enumerate() + .filter_map(|(i, &v)| { + let new = unpack_sample(&words, i); + (new != v).then(|| Photosite { + x: i as u32 % stride, + y: i as u32 / stride, + hot: new < v, + }) + }) + .collect()) + } + + /// The hot-pixel pass over `raw`, read back as packed words. + fn hot_pixel_words(&self, raw: &RawImage) -> Result, GpuError> { let (width, height) = (raw.crop.width.max(1), raw.crop.height.max(1)); let xtrans_tile = raw .cfa_pattern @@ -1122,18 +1162,30 @@ impl Demosaicer { .map_err(|e| GpuError::Readback(e.to_string()))?; let words: Vec = bytemuck::cast_slice(&slice.get_mapped_range()).to_vec(); readback.unmap(); - - let mut changed = 0; - for (i, v) in raw.data.iter_mut().enumerate() { - let w = words[i / 2]; - let new = if i % 2 == 0 { w & 0xFFFF } else { w >> 16 } as u16; - changed += usize::from(new != *v); - *v = new; - } - Ok(changed) + Ok(words) } } +/// One photosite the hot-pixel pass judged defective. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct Photosite { + /// Sensor coordinates: the full readout, masked border included. + pub x: u32, + pub y: u32, + /// Read far above its neighbourhood; otherwise far below (dead). + pub hot: bool, +} + +/// Sample `i` of a readout packed by [`pack_samples`]. +fn unpack_sample(words: &[u32], i: usize) -> u16 { + let w = words[i / 2]; + (if i.is_multiple_of(2) { + w & 0xFFFF + } else { + w >> 16 + }) as u16 +} + const IDENTITY_3X3: [f32; 9] = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]; /// Pack u16 samples two per u32, little-endian within the word. diff --git a/core/dr-gpu/src/lib.rs b/core/dr-gpu/src/lib.rs index 8d3dc13..aeeb8aa 100644 --- a/core/dr-gpu/src/lib.rs +++ b/core/dr-gpu/src/lib.rs @@ -38,7 +38,7 @@ pub use adjust::AdjustPass; // rather than an implementation detail: a detail pass is guaranteed linear, // unclipped, full internal precision (FR-DEV-2), and anyone reasoning about // VRAM at 24 MP needs to know what an intermediate costs. -pub use demosaic::{DemosaicedImage, Demosaicer}; +pub use demosaic::{DemosaicedImage, Demosaicer, Photosite}; pub use detail::INTERMEDIATE_FORMAT as DETAIL_INTERMEDIATE_FORMAT; pub use error::GpuError; pub use focus::{FocusPeakPass, FocusPeaking, PeakColour, PeakSensitivity}; diff --git a/core/dr-gpu/tests/hot_pixels.rs b/core/dr-gpu/tests/hot_pixels.rs index bb36874..0c2c975 100644 --- a/core/dr-gpu/tests/hot_pixels.rs +++ b/core/dr-gpu/tests/hot_pixels.rs @@ -7,7 +7,7 @@ //! see of a defect it missed is the coloured cross the demosaic makes of it. use dr_decode::{CfaPattern, CropRect, RawImage}; -use dr_gpu::{AdjustPass, Demosaicer, GpuContext}; +use dr_gpu::{AdjustPass, Demosaicer, GpuContext, Photosite}; use dr_pipeline::EditGraph; const SIZE: u32 = 36; @@ -171,3 +171,43 @@ fn the_repaired_mosaic_reads_back_with_only_the_defect_changed() { let others = (0..raw.data.len()).filter(|&i| i != at); assert!(others.into_iter().all(|i| raw.data[i] == before.data[i])); } + +/// Finding without repairing (docs/dev/sensor-health.md): the same verdict as +/// the repair, as sensor coordinates, with the frame left as it was. The +/// sensor health record builds on this, so it must name exactly the +/// photosites the repair would change — the hot one and the dead one, and +/// not the star. +#[test] +fn finding_names_what_the_repair_would_change_and_changes_nothing() { + let Some(ctx) = ctx() else { + eprintln!("no GPU adapter; skipping"); + return; + }; + let d = Demosaicer::new(&ctx).expect("demosaicer"); + let mut set = vec![(MIDDLE, MIDDLE, WHITE), (9, 25, 0)]; + for dy in 0..3 { + for dx in 0..3 { + set.push((4 + dx, 4 + dy, WHITE)); + } + } + let raw = frame(CfaPattern::Rggb, 1600, &set); + let mut found = d.find_hot_pixels(&raw).expect("find"); + found.sort_by_key(|p| (p.y, p.x)); + assert_eq!( + found, + vec![ + Photosite { + x: MIDDLE, + y: MIDDLE, + hot: true + }, + Photosite { + x: 9, + y: 25, + hot: false + }, + ] + ); + let mut repaired = raw.clone(); + assert_eq!(d.repair_hot_pixels(&mut repaired).expect("repair"), 2); +} diff --git a/docs/dev/sensor-health.md b/docs/dev/sensor-health.md new file mode 100644 index 0000000..1a28f3a --- /dev/null +++ b/docs/dev/sensor-health.md @@ -0,0 +1,82 @@ +# DarkRoom — Sensor health: a dated defect map per body + +**Status:** Spike · 2026-10-04 · not built +**Companion to:** [requirements.md](requirements.md) FR-RAW-3, [catalog.md](catalog.md) + +A sensor gains defective photosites as it ages, and a photosite that has gone bad does not recover. +This records, per camera body, which photosites are defective and since when, so that the library +can show how a sensor has aged and the hot-pixel repair can fix the defects a body is known to have +rather than only those that stand out in the frame at hand. + +What exists is the measuring tool: `Demosaicer::find_hot_pixels` (the photosites the repair pass +would replace, without replacing them) and `core/dr-gpu/examples/sensor_scan.rs`, which prints them +per frame and, with `--probe`, reads a list of coordinates back out of each frame. The rest of this +document is what a spike with them on the 6D found, and the design it argues for. + +--- + +## 1. What is wanted + +- **Settings → Bodies**, one entry per body, with a graph of the defective share over time in two + series: photosites (the raw mosaic) and 2×2 cells holding at least one defective photosite (what + reaches a pixel of the output). +- **A dated defect map** per body, synced with the library like any other catalog data, and + cumulative: an entry is never removed. +- **The repair reads the map** whose date is nearest the frame's, and fixes every defect the body had + by then, whether or not it stands out in that frame. +- One body per model for now: the catalog stores `make model`, not a body serial. + +## 2. What the spike found (6D, 2026-10-03) + +53 CR2s, up to four per quarter at the highest ISO of a day, 2015 to 2026. The library holds almost +no 6D raws from 2016–2022, so onsets in that span are dated to the span, not the year. `sensor_scan` +ran at 0.8 s per frame, decode included. + +**Persistence separates the sensor from the scene.** 4,179 photosites were flagged at least once; +3,554 on one day only (stars, glints, noise). A defect is a photosite that keeps coming back. + +**A frame that does not flag a photosite is not evidence it was clean.** The repair's test is +relative to the neighbourhood, so a defect in a lit area does not stand out. Confirmed defects were +flagged in a median 20 % of the frames after their first sighting. Only a frame whose neighbourhood +at that photosite is dark counts, either way. + +**The 6D hides some defects itself at high ISO.** (2517, 3172) reads 8,000–13,000 over neighbours +near 200 at ISO 2000–5000, and does not stand out at all at ISO 6400–12800 (82 over 149 on +2025-03-15). The camera appears to map out photosites it knows at those gains. So evidence for this +body comes from ISO ≤ 5000; the cut-off must be learned per body, not fixed. + +**Long exposures light everything.** A 9.8 s frame saturated every candidate; it confirms, it does +not date. + +**The curve.** Counting a photosite as defective from the first frame where it stands out, provided +it stands out in at least 60 % of the observable frames after that (32 defects; 31 with a clean +observable frame before onset to bracket it): + +| Year | Defects | Share of photosites | +|---|---|---| +| 2015 | 2 | 0.1 ppm | +| 2016–2021 | 2 | 0.1 ppm | +| 2022 | 7 | 0.3 ppm | +| 2023 | 26 | 1.3 ppm | +| 2026 | 32 | 1.6 ppm | + +(2517, 3172) is the shape every entry should have: clean at ISO 800–1000 in 2015 and at ISO 100–200 +in 2016, then 338 over 72 at ISO 100 on 2022-08-13 and in every comparable frame since. + +Weak defects exist too, about twice their neighbours ((1814, 3039)); the blind repair misses them in +most frames. A known map would catch them. + +## 3. Design it argues for + +- **Evidence per frame, per known photosite**: observable (dark neighbourhood, ISO inside the + body's band) and, if so, lit or clean. Not just the frame's flagged list. +- **A map entry is a bracket**: last clean observation, first lit observation, strength, kind. Onset + lies between the two; the graph plots it at the first, and can show the bracket. +- **The repair**: every entry whose first lit date is on or before the frame's capture date; for an + entry whose bracket contains the date, probe the photosite in the frame itself. +- **Storage and sync**: catalog tables created on first use (as `albums` does), so no schema bump + breaks an older peer. They travel in the snapshot by default. Merge is a set union of photosites + per body, the earlier first-lit and the later last-clean winning, which makes it commutative and + keeps the map cumulative. +- **Work**: a sample, not the library. Frames are picked for what they can reveal (dark, mid ISO, + long exposures), a few per body per month, and after the first pass only new imports are read. diff --git a/docs/dev/traceability.md b/docs/dev/traceability.md index 7bcf311..802a80b 100644 --- a/docs/dev/traceability.md +++ b/docs/dev/traceability.md @@ -9,7 +9,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 500 | +| Source files scanned | 501 | | TRACES tags found | 2105 | | Requirements defined | 193 | | Requirements deferred (post-v1) | 24 | @@ -149,9 +149,9 @@ _None._ | FR-PLAT-WIN-3 | [`apps/darkroom-desktop/src/main.rs:35`](../../apps/darkroom-desktop/src/main.rs#L35) | | FR-RAW-1 | [`core/dr-decode/src/lib.rs:266`](../../core/dr-decode/src/lib.rs#L266), [`core/dr-types/src/lib.rs:135`](../../core/dr-types/src/lib.rs#L135), [`core/dr-types/src/lib.rs:206`](../../core/dr-types/src/lib.rs#L206) | | FR-RAW-2 | [`core/dr-decode/src/decoder.rs:1`](../../core/dr-decode/src/decoder.rs#L1), [`core/dr-decode/src/decoder.rs:26`](../../core/dr-decode/src/decoder.rs#L26), [`core/dr-decode/src/decoder.rs:59`](../../core/dr-decode/src/decoder.rs#L59), [`core/dr-decode/src/decoder.rs:95`](../../core/dr-decode/src/decoder.rs#L95), [`ui/dr-ui/src/decoder_seam.rs:185`](../../ui/dr-ui/src/decoder_seam.rs#L185), [`ui/dr-ui/src/decoder_seam.rs:1`](../../ui/dr-ui/src/decoder_seam.rs#L1), [`ui/dr-ui/src/decoder_seam.rs:216`](../../ui/dr-ui/src/decoder_seam.rs#L216), [`ui/dr-ui/src/decoder_seam.rs:257`](../../ui/dr-ui/src/decoder_seam.rs#L257), [`ui/dr-ui/src/export.rs:1238`](../../ui/dr-ui/src/export.rs#L1238), [`ui/dr-ui/src/export.rs:973`](../../ui/dr-ui/src/export.rs#L973), [`ui/dr-ui/src/import.rs:96`](../../ui/dr-ui/src/import.rs#L96), [`ui/dr-ui/src/library/sweep.rs:322`](../../ui/dr-ui/src/library/sweep.rs#L322), [`ui/dr-ui/src/library/sweep.rs:654`](../../ui/dr-ui/src/library/sweep.rs#L654), [`ui/dr-ui/src/library/thumbnails_gen.rs:109`](../../ui/dr-ui/src/library/thumbnails_gen.rs#L109), [`ui/dr-ui/src/merge.rs:119`](../../ui/dr-ui/src/merge.rs#L119), [`ui/dr-ui/src/repairs.rs:243`](../../ui/dr-ui/src/repairs.rs#L243) | -| FR-RAW-3 | [`core/dr-decode/src/lib.rs:162`](../../core/dr-decode/src/lib.rs#L162), [`core/dr-decode/src/lib.rs:564`](../../core/dr-decode/src/lib.rs#L564), [`core/dr-decode/src/locate.rs:1435`](../../core/dr-decode/src/locate.rs#L1435), [`core/dr-gpu/src/demosaic.rs:1063`](../../core/dr-gpu/src/demosaic.rs#L1063), [`core/dr-gpu/src/demosaic.rs:1409`](../../core/dr-gpu/src/demosaic.rs#L1409), [`core/dr-gpu/src/demosaic.rs:868`](../../core/dr-gpu/src/demosaic.rs#L868), [`core/dr-gpu/tests/hot_pixels.rs:1`](../../core/dr-gpu/tests/hot_pixels.rs#L1) | +| FR-RAW-3 | [`core/dr-decode/src/lib.rs:162`](../../core/dr-decode/src/lib.rs#L162), [`core/dr-decode/src/lib.rs:564`](../../core/dr-decode/src/lib.rs#L564), [`core/dr-decode/src/locate.rs:1435`](../../core/dr-decode/src/locate.rs#L1435), [`core/dr-gpu/src/demosaic.rs:1063`](../../core/dr-gpu/src/demosaic.rs#L1063), [`core/dr-gpu/src/demosaic.rs:1461`](../../core/dr-gpu/src/demosaic.rs#L1461), [`core/dr-gpu/src/demosaic.rs:868`](../../core/dr-gpu/src/demosaic.rs#L868), [`core/dr-gpu/tests/hot_pixels.rs:1`](../../core/dr-gpu/tests/hot_pixels.rs#L1) | | FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../../core/dr-decode/src/error.rs#L1), [`core/dr-decode/src/error.rs:30`](../../core/dr-decode/src/error.rs#L30), [`ui/dr-ui/src/lib.rs:295`](../../ui/dr-ui/src/lib.rs#L295) | -| FR-RAW-5 | [`core/dr-decode/src/lib.rs:190`](../../core/dr-decode/src/lib.rs#L190), [`core/dr-gpu/src/demosaic.rs:1204`](../../core/dr-gpu/src/demosaic.rs#L1204), [`core/dr-gpu/src/demosaic.rs:1283`](../../core/dr-gpu/src/demosaic.rs#L1283), [`core/dr-gpu/src/demosaic.rs:1407`](../../core/dr-gpu/src/demosaic.rs#L1407), [`core/dr-gpu/src/demosaic.rs:34`](../../core/dr-gpu/src/demosaic.rs#L34) | +| FR-RAW-5 | [`core/dr-decode/src/lib.rs:190`](../../core/dr-decode/src/lib.rs#L190), [`core/dr-gpu/src/demosaic.rs:1256`](../../core/dr-gpu/src/demosaic.rs#L1256), [`core/dr-gpu/src/demosaic.rs:1335`](../../core/dr-gpu/src/demosaic.rs#L1335), [`core/dr-gpu/src/demosaic.rs:1459`](../../core/dr-gpu/src/demosaic.rs#L1459), [`core/dr-gpu/src/demosaic.rs:34`](../../core/dr-gpu/src/demosaic.rs#L34) | | FR-UI-1 | [`core/dr-types/src/settings.rs:1390`](../../core/dr-types/src/settings.rs#L1390), [`core/dr-types/src/settings.rs:492`](../../core/dr-types/src/settings.rs#L492), [`core/dr-types/src/settings.rs:512`](../../core/dr-types/src/settings.rs#L512), [`ui/dr-ui/src/develop_ui.rs:284`](../../ui/dr-ui/src/develop_ui.rs#L284), [`ui/dr-ui/src/lib.rs:126`](../../ui/dr-ui/src/lib.rs#L126), [`ui/dr-ui/src/lib.rs:141`](../../ui/dr-ui/src/lib.rs#L141), [`ui/dr-ui/src/lib.rs:163`](../../ui/dr-ui/src/lib.rs#L163), [`ui/dr-ui/src/lib.rs:3387`](../../ui/dr-ui/src/lib.rs#L3387), [`ui/dr-ui/src/lib.rs:3406`](../../ui/dr-ui/src/lib.rs#L3406), [`ui/dr-ui/src/lib.rs:3433`](../../ui/dr-ui/src/lib.rs#L3433), [`ui/dr-ui/src/lib.rs:3484`](../../ui/dr-ui/src/lib.rs#L3484), [`ui/dr-ui/src/lib.rs:3517`](../../ui/dr-ui/src/lib.rs#L3517), [`ui/dr-ui/src/library_ui/grid.rs:242`](../../ui/dr-ui/src/library_ui/grid.rs#L242), [`ui/dr-ui/src/masks_ui.rs:1608`](../../ui/dr-ui/src/masks_ui.rs#L1608), [`ui/dr-ui/src/settings_ui.rs:143`](../../ui/dr-ui/src/settings_ui.rs#L143), [`ui/dr-ui/ui/adjust.slint:1000`](../../ui/dr-ui/ui/adjust.slint#L1000), [`ui/dr-ui/ui/app.slint:3147`](../../ui/dr-ui/ui/app.slint#L3147), [`ui/dr-ui/ui/app.slint:670`](../../ui/dr-ui/ui/app.slint#L670), [`ui/dr-ui/ui/app.slint:815`](../../ui/dr-ui/ui/app.slint#L815), [`ui/dr-ui/ui/identity.slint:249`](../../ui/dr-ui/ui/identity.slint#L249), [`ui/dr-ui/ui/library.slint:1469`](../../ui/dr-ui/ui/library.slint#L1469), [`ui/dr-ui/ui/settings.slint:129`](../../ui/dr-ui/ui/settings.slint#L129), [`ui/dr-ui/ui/settings.slint:660`](../../ui/dr-ui/ui/settings.slint#L660), [`ui/dr-ui/ui/toolrail.slint:180`](../../ui/dr-ui/ui/toolrail.slint#L180), [`ui/dr-ui/ui/toolrail.slint:92`](../../ui/dr-ui/ui/toolrail.slint#L92), [`ui/dr-ui/ui/widgets.slint:1117`](../../ui/dr-ui/ui/widgets.slint#L1117) | | FR-UI-2 | [`ui/dr-ui/src/collections_ui/controller.rs:45`](../../ui/dr-ui/src/collections_ui/controller.rs#L45), [`ui/dr-ui/src/collections_ui/controller.rs:83`](../../ui/dr-ui/src/collections_ui/controller.rs#L83), [`ui/dr-ui/src/collections_ui/drag.rs:246`](../../ui/dr-ui/src/collections_ui/drag.rs#L246), [`ui/dr-ui/src/collections_ui/drag.rs:256`](../../ui/dr-ui/src/collections_ui/drag.rs#L256), [`ui/dr-ui/src/collections_ui/press.rs:453`](../../ui/dr-ui/src/collections_ui/press.rs#L453), [`ui/dr-ui/src/collections_ui/wiring_grid.rs:121`](../../ui/dr-ui/src/collections_ui/wiring_grid.rs#L121), [`ui/dr-ui/src/collections_ui/wiring_grid.rs:138`](../../ui/dr-ui/src/collections_ui/wiring_grid.rs#L138), [`ui/dr-ui/src/collections_ui/wiring_grid.rs:415`](../../ui/dr-ui/src/collections_ui/wiring_grid.rs#L415), [`ui/dr-ui/src/collections_ui/wiring_tree.rs:287`](../../ui/dr-ui/src/collections_ui/wiring_tree.rs#L287), [`ui/dr-ui/src/gestures.rs:1`](../../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:126`](../../ui/dr-ui/src/lib.rs#L126), [`ui/dr-ui/src/library_ui/filter_bar.rs:351`](../../ui/dr-ui/src/library_ui/filter_bar.rs#L351), [`ui/dr-ui/src/library_ui/grid.rs:242`](../../ui/dr-ui/src/library_ui/grid.rs#L242), [`ui/dr-ui/src/library_ui/window.rs:1164`](../../ui/dr-ui/src/library_ui/window.rs#L1164), [`ui/dr-ui/tests/scroll_cue.rs:1`](../../ui/dr-ui/tests/scroll_cue.rs#L1), [`ui/dr-ui/ui/app.slint:137`](../../ui/dr-ui/ui/app.slint#L137), [`ui/dr-ui/ui/app.slint:3545`](../../ui/dr-ui/ui/app.slint#L3545), [`ui/dr-ui/ui/app.slint:799`](../../ui/dr-ui/ui/app.slint#L799), [`ui/dr-ui/ui/collections.slint:1354`](../../ui/dr-ui/ui/collections.slint#L1354), [`ui/dr-ui/ui/controls.slint:626`](../../ui/dr-ui/ui/controls.slint#L626), [`ui/dr-ui/ui/gestures.slint:1`](../../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1356`](../../ui/dr-ui/ui/library.slint#L1356), [`ui/dr-ui/ui/library.slint:1389`](../../ui/dr-ui/ui/library.slint#L1389), [`ui/dr-ui/ui/library.slint:1717`](../../ui/dr-ui/ui/library.slint#L1717), [`ui/dr-ui/ui/library.slint:1724`](../../ui/dr-ui/ui/library.slint#L1724), [`ui/dr-ui/ui/library.slint:1825`](../../ui/dr-ui/ui/library.slint#L1825), [`ui/dr-ui/ui/library.slint:3168`](../../ui/dr-ui/ui/library.slint#L3168), [`ui/dr-ui/ui/library.slint:586`](../../ui/dr-ui/ui/library.slint#L586), [`ui/dr-ui/ui/presets.slint:114`](../../ui/dr-ui/ui/presets.slint#L114), [`ui/dr-ui/ui/settings.slint:143`](../../ui/dr-ui/ui/settings.slint#L143), [`ui/dr-ui/ui/widgets.slint:1117`](../../ui/dr-ui/ui/widgets.slint#L1117) | | FR-UI-3 | [`ui/dr-ui/src/collections_ui/controller.rs:143`](../../ui/dr-ui/src/collections_ui/controller.rs#L143), [`ui/dr-ui/src/collections_ui/controller.rs:348`](../../ui/dr-ui/src/collections_ui/controller.rs#L348), [`ui/dr-ui/src/collections_ui/controller.rs:361`](../../ui/dr-ui/src/collections_ui/controller.rs#L361), [`ui/dr-ui/src/collections_ui/wiring_tree.rs:287`](../../ui/dr-ui/src/collections_ui/wiring_tree.rs#L287), [`ui/dr-ui/src/develop/masks.rs:361`](../../ui/dr-ui/src/develop/masks.rs#L361), [`ui/dr-ui/src/develop/repairs.rs:49`](../../ui/dr-ui/src/develop/repairs.rs#L49), [`ui/dr-ui/src/library_ui/grid.rs:228`](../../ui/dr-ui/src/library_ui/grid.rs#L228), [`ui/dr-ui/src/masks_ui.rs:1700`](../../ui/dr-ui/src/masks_ui.rs#L1700), [`ui/dr-ui/src/masks_ui.rs:1722`](../../ui/dr-ui/src/masks_ui.rs#L1722), [`ui/dr-ui/src/masks_ui.rs:339`](../../ui/dr-ui/src/masks_ui.rs#L339), [`ui/dr-ui/src/spots_ui.rs:19`](../../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2356`](../../ui/dr-ui/ui/app.slint#L2356), [`ui/dr-ui/ui/collections.slint:1228`](../../ui/dr-ui/ui/collections.slint#L1228), [`ui/dr-ui/ui/collections.slint:4`](../../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:78`](../../ui/dr-ui/ui/collections.slint#L78), [`ui/dr-ui/ui/collections.slint:978`](../../ui/dr-ui/ui/collections.slint#L978), [`ui/dr-ui/ui/masks.slint:1246`](../../ui/dr-ui/ui/masks.slint#L1246), [`ui/dr-ui/ui/widgets.slint:72`](../../ui/dr-ui/ui/widgets.slint#L72) |