diff --git a/core/dr-pipeline/build.rs b/core/dr-pipeline/build.rs index e0e1a51..5ca58ab 100644 --- a/core/dr-pipeline/build.rs +++ b/core/dr-pipeline/build.rs @@ -18,7 +18,7 @@ //! //! It does not change the runtime model. The generated code implements the //! same [`Operation`](../src/operation.rs) trait, publishes the same -//! `&'static OpDescriptor`, and composes through the same fused-shader path. +//! `Arc`, and composes through the same fused-shader path. //! Nothing downstream — not `dr-gpu`, not the develop panel — can tell a //! declared node from a hand-written one, which is what allows the two to sit //! side by side in one chain. @@ -788,7 +788,7 @@ fn param_ctor( .collect::, _>>()?; ( format!( - "ParamDescriptor::choice({:?}, {:?}, &[{}])", + "ParamDescriptor::choice({:?}, {:?}, vec![{}])", id, label, keys.join(", ") @@ -1495,7 +1495,8 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> { \x20 Scale, Unit, WidgetDemand, WidgetKind,\n\ \x20 };\n\ \x20 #[allow(unused_imports)]\n\ - \x20 use crate::operation::{Helper, Operation, Uniform};\n\n", + \x20 use crate::operation::{Helper, Operation, Uniform};\n\ + \x20 use std::sync::{Arc, LazyLock};\n\n", ); // Ids as consts, so a caller names a parameter through the type system @@ -1511,20 +1512,26 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> { } // Descriptor. - let _ = writeln!( - out, - "\n static DESCRIPTOR: OpDescriptor = OpDescriptor {{\n\ - \x20 id: ID,\n\ - \x20 label: LocalizedKey({label:?}),\n\ - \x20 params: &[" + // + // Built once, behind a `LazyLock`, and handed out as an `Arc` clone. It + // was a plain `static` until descriptors became owned (FR-PLG-2): an + // `OpDescriptor` now holds `Vec`s, and an `Arc` is not const-constructible + // in any case. The cost is one lock check the first time an operation is + // asked what it is, and an atomic increment thereafter. + out.push_str( + "\n static DESCRIPTOR: LazyLock> = LazyLock::new(|| {\n\ + \x20 Arc::new(OpDescriptor {\n", ); + let _ = writeln!(out, " id: ID,"); + let _ = writeln!(out, " label: LocalizedKey({label:?}),"); + out.push_str(" params: vec![\n"); for p in params { if let Some(doc) = &p.doc { - out.push_str(&comment(doc, "//", 12)); + out.push_str(&comment(doc, "//", 16)); } - let _ = writeln!(out, " {},", p.ctor); + let _ = writeln!(out, " {},", p.ctor); } - out.push_str(" ],\n"); + out.push_str(" ],\n"); // What the operation is about. The panel groups by these and names no // operation, which is what keeps FR-DEV-3a true as the set grows. @@ -1539,8 +1546,8 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> { format!("Attribute::{head}{}", c.as_str()) }) .collect(); - let _ = writeln!(out, " attributes: &[{}],", attrs.join(", ")); - out.push_str(" };\n"); + let _ = writeln!(out, " attributes: vec![{}],", attrs.join(", ")); + out.push_str(" })\n });\n"); // Helpers: node-local definitions first, then the assembled list. for h in local_helpers { @@ -1583,7 +1590,7 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> { // The trait. let _ = writeln!(out, "\n impl Operation for {ty} {{"); out.push_str( - " fn descriptor(&self) -> &'static OpDescriptor {\n &DESCRIPTOR\n }\n\n", + " fn descriptor(&self) -> Arc {\n DESCRIPTOR.clone()\n }\n\n", ); out.push_str( @@ -1650,7 +1657,7 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> { if !helper_refs.is_empty() { out.push_str( - "\n fn helpers(&self) -> &'static [Helper] {\n HELPERS\n }\n", + "\n fn helpers(&self) -> &[Helper] {\n HELPERS\n }\n", ); } @@ -1661,12 +1668,12 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> { out, "\n fn presentation(&self) -> Option {{\n\ \x20 Some(Presentation {{\n\ - \x20 widgets: &[{}],\n\ + \x20 widgets: vec![{}],\n\ \x20 demand: WidgetDemand {{\n\ \x20 two_dimensional: {},\n\ \x20 precise_pointing: {},\n\ \x20 }},\n\ - \x20 params: &[{}],\n\ + \x20 params: vec![{}],\n\ \x20 }})\n\ \x20 }}", p.widgets.join(", "), diff --git a/core/dr-pipeline/src/descriptor.rs b/core/dr-pipeline/src/descriptor.rs index 6e18352..5f8a080 100644 --- a/core/dr-pipeline/src/descriptor.rs +++ b/core/dr-pipeline/src/descriptor.rs @@ -8,12 +8,75 @@ //! Labels are keys, not strings: resolving them needs a localiser, and //! `core/` must not depend on one (NFR-A11Y-1). +use std::collections::HashSet; use std::fmt; +use std::sync::{LazyLock, Mutex}; + +/// TRACES: FR-PLG-2 +/// Give a string read at run time the `'static` lifetime the identifier types +/// carry. +/// +/// # Why the identifiers stayed `&'static str` when the descriptors did not +/// +/// [`OpDescriptor`] became owned so a declaration read at *load* time can +/// produce one (FR-PLG-2). The three identifier newtypes below deliberately +/// did not follow it. +/// +/// An id is not content; it is a key. [`ParamId`] is `Copy`, is compared in +/// `match` arms against the constants `build.rs` generates, is a map key in +/// the sidecar and in history, and is threaded through `dr-ui` into Slint +/// model rows. An `Arc` there would put a refcount on every one of those +/// and would take `match id { EXPOSURE => .. }` away from the generated code — +/// which is precisely the inspectability of the built-in chain that keeping +/// the generated path was for. +/// +/// So ids are interned instead, and interning is honest about its lifetime +/// rather than pretending to one. The set of interned ids is: +/// +/// - **Bounded.** One entry per *distinct* string, deduplicated on the way in. +/// Parsing the same declaration a thousand times adds nothing after the +/// first. +/// - **Process-lifetime by construction.** A loaded declaration's vocabulary +/// is never withdrawn. Nothing unloads a plugin, and nothing could: the +/// sidecar on disk stores parameters by `(op_id, param_id)`, so an id has to +/// stay resolvable for as long as any edit naming it can be opened. +/// +/// A leak whose bound is "the distinct ids this process has ever seen" is a +/// different thing from one that grows with use, and this is the first. +pub fn intern(s: &str) -> &'static str { + static POOL: LazyLock>> = + LazyLock::new(|| Mutex::new(HashSet::new())); + + // A poisoned pool is still a correct pool: every entry in it is a + // `&'static str` that was interned successfully, and a panic elsewhere + // while the lock was held cannot have made one invalid. Refusing to + // intern here would turn an unrelated panic into an application that can + // no longer read a declaration. + let mut pool = POOL.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(found) = pool.get(s) { + return found; + } + let leaked: &'static str = Box::leak(s.to_owned().into_boxed_str()); + pool.insert(leaked); + leaked +} /// Identifies a parameter within an operation. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ParamId(pub &'static str); +impl ParamId { + /// The same id, from a name read out of a declaration at load time. + /// + /// Equal to `ParamId("exposure")` when the name is `"exposure"`: the + /// derived `PartialEq` compares the `str` contents, not the pointer, which + /// is what lets an interned id match a generated `match` arm. See + /// [`intern`]. + pub fn interned(name: &str) -> Self { + Self(intern(name)) + } +} + impl fmt::Display for ParamId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.0) @@ -24,6 +87,13 @@ impl fmt::Display for ParamId { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct OpId(pub &'static str); +impl OpId { + /// The same id, from a declaration read at load time. See [`intern`]. + pub fn interned(name: &str) -> Self { + Self(intern(name)) + } +} + impl fmt::Display for OpId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.0) @@ -34,6 +104,13 @@ impl fmt::Display for OpId { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct LocalizedKey(pub &'static str); +impl LocalizedKey { + /// The same key, from a declaration read at load time. See [`intern`]. + pub fn interned(key: &str) -> Self { + Self(intern(key)) + } +} + /// What a slider's travel means. /// /// Photographic controls are rarely linear in their underlying quantity: @@ -170,7 +247,11 @@ pub enum ParamKind { Enum { /// In index order. The label is a localisation key, resolved by the /// frontend — `core/` must not depend on a localiser (NFR-A11Y-1). - variants: &'static [LocalizedKey], + /// + /// Owned rather than `&'static`, for the reason [`OpDescriptor`] + /// gives: a declaration parsed at load time has nowhere to put a + /// `'static` slice. + variants: Vec, }, } @@ -204,7 +285,7 @@ pub struct Presentation { /// pair of sliders, and an operation that can say so gets a good control /// on a workstation and a usable one on a phone without the core knowing /// which it is talking to. - pub widgets: &'static [WidgetKind], + pub widgets: Vec, /// What the preferred widget needs in order to be worth drawing. /// /// Applies to the list as a whole rather than per entry: a frontend that @@ -215,7 +296,7 @@ pub struct Presentation { /// /// Parameters absent from this list are presented normally, so an /// operation can pair a curve with an ordinary strength slider. - pub params: &'static [ParamId], + pub params: Vec, } impl Presentation { @@ -328,11 +409,13 @@ impl ParamDescriptor { /// holds the way it does for every other kind: index 0 is the neutral /// choice, and an operation whose default is not its first variant has /// listed them in the wrong order. - pub const fn choice( - id: &'static str, - label: &'static str, - variants: &'static [LocalizedKey], - ) -> Self { + // + // Not `const`, unlike its four siblings, and the reason is the `Vec` in + // [`ParamKind::Enum`]: a heap allocation cannot happen in a const context. + // Nothing is lost — every descriptor now lives inside a `LazyLock` + // initialiser rather than a `static`, because `descriptor()` hands out an + // `Arc` and an `Arc` is not const-constructible either. + pub fn choice(id: &'static str, label: &'static str, variants: Vec) -> Self { Self { id: ParamId(id), label: LocalizedKey(label), @@ -366,13 +449,15 @@ impl ParamDescriptor { /// A general scalar with an explicit range and default. // - // Eight arguments, and a builder would be the usual answer — but this has - // to be `const` so descriptors can be `static`, which rules out the - // `&mut self` builder the pattern usually takes. (A `self`-by-value step - // *is* const-callable — `faceted` below is one — but eight of them would - // be eight methods to say what one call already says.) The two common - // shapes have their own constructors above; this is the escape hatch for - // the rest. + // Eight arguments, and a builder would be the usual answer — but eight + // `&mut self` steps would be eight methods to say what one call already + // says, and each one would be a place for a caller to forget a field. The + // two common shapes have their own constructors above; this is the escape + // hatch for the rest. + // + // Still `const` although no descriptor is a `static` any more: it costs + // nothing, and it keeps the five constructors uniform where only `choice` + // genuinely cannot be. #[allow(clippy::too_many_arguments)] pub const fn scalar( id: &'static str, @@ -404,10 +489,13 @@ impl ParamDescriptor { /// A method rather than a sixth constructor, because a facet is orthogonal /// to the shape of the value: a faceted parameter is still an amount, or /// still a scalar in stops, and pairing every constructor with a faceted - /// twin would double the list above to say one thing. Taking `self` by - /// value is what keeps it usable in the `static` descriptors — a `&mut - /// self` builder is what cannot be `const`. - pub const fn faceted(mut self, facet: Facet) -> Self { + /// twin would double the list above to say one thing. + // + // No longer `const`: a `ParamDescriptor` can now carry a `Vec` (an enum's + // variants), which gives the type drop glue, and assigning over a field of + // such a type is not something a const function may do. Nothing is lost — + // every descriptor is built inside a `LazyLock` initialiser now. + pub fn faceted(mut self, facet: Facet) -> Self { self.facet = Some(facet); self } @@ -418,10 +506,10 @@ impl ParamDescriptor { /// bounds, or a sidecar written by a newer version with a wider range, /// must not produce out-of-range uniforms. pub fn clamp(&self, value: f32) -> f32 { - match self.kind { + match &self.kind { ParamKind::Scalar { min, max, .. } => { if value.is_finite() { - value.clamp(min, max) + value.clamp(*min, *max) } else { // A NaN from a corrupt sidecar would otherwise poison the // uniform block and blank the image. @@ -544,20 +632,45 @@ impl Attribute { } } -/// The static description of an operation. +/// TRACES: FR-PLG-2 +/// The description of an operation. +/// +/// # Owned, not `&'static` +/// +/// This used to be a `static` with `&'static [ParamDescriptor]` inside it, and +/// [`crate::Operation::descriptor`] used to hand out a reference to it. That +/// shape made a build-time node free and a run-time node **impossible**: a +/// declaration parsed at startup has nothing to borrow from, so no amount of +/// interpreting `ops/*.yaml` at load time could ever produce a descriptor the +/// rest of the application would accept. FR-PLG-2 says a bundled operation and +/// a third-party plugin are the same kind of thing, differing only in where +/// the file was found — and a lifetime that only a compile-time literal can +/// satisfy is exactly a second, weaker format for outsiders. +/// +/// So the descriptor owns its contents and is handed out as an +/// `Arc`. The `Arc` rather than a `&self`-borrowed reference +/// because the callers want to *keep* it: the develop panel collects +/// descriptors and then mutates the graph, and a borrow would tie the +/// descriptor's lifetime to a borrow of the operation it came from — which is +/// the one thing `&'static` was doing right. +/// +/// The cost is a refcount per read, on a path that reads descriptors when a +/// panel is built rather than per pixel. See `Operation::descriptor` for the +/// one place that is read per composition and why it does not matter. #[derive(Debug, Clone, PartialEq)] pub struct OpDescriptor { pub id: OpId, pub label: LocalizedKey, - pub params: &'static [ParamDescriptor], + pub params: Vec, /// What this operation is about (ARCH §4.3a). /// - /// **Never empty**, and `build.rs` refuses to generate an operation that - /// declares none. An operation with no attribute would be invisible to a - /// frontend that filters by them, and a control that silently does not - /// exist is a worse failure than a build that stops — particularly when - /// the cause would be a missing line in a YAML file nobody looked at. - pub attributes: &'static [Attribute], + /// **Never empty**, and both the build-time and the load-time reader + /// refuse an operation that declares none. An operation with no attribute + /// would be invisible to a frontend that filters by them, and a control + /// that silently does not exist is a worse failure than a build that stops + /// — particularly when the cause would be a missing line in a YAML file + /// nobody looked at. + pub attributes: Vec, } impl OpDescriptor { diff --git a/core/dr-pipeline/src/detail.rs b/core/dr-pipeline/src/detail.rs index c07dfda..8026d52 100644 --- a/core/dr-pipeline/src/detail.rs +++ b/core/dr-pipeline/src/detail.rs @@ -519,7 +519,13 @@ pub fn compose_detail_with( ) -> ComposedDetail { // Every pass of every active detail operation, flattened, carrying the // operation it came from for the uniform prefix and the helper set. - let mut planned: Vec<(&'static str, &'static [Helper], DetailPass, usize)> = Vec::new(); + // + // The helper slice borrows from the operation rather than being `'static`: + // `Operation::helpers` hands out a slice owned by the operation now, so + // that a node built from a declaration at load time can own its list + // (FR-PLG-2). The borrow lasts as long as `ops`, which outlives this + // function's body. + let mut planned: Vec<(&str, &[Helper], DetailPass, usize)> = Vec::new(); for (index, pass) in spots.iter().enumerate() { planned.push(( crate::spot::SPOT_ID, diff --git a/core/dr-pipeline/src/detail/probe.rs b/core/dr-pipeline/src/detail/probe.rs index 361fb4b..f5c6c73 100644 --- a/core/dr-pipeline/src/detail/probe.rs +++ b/core/dr-pipeline/src/detail/probe.rs @@ -29,6 +29,7 @@ //! was built for: a horizontal pass then a vertical one is mathematically a 2D //! box average, so if the ping-pong is wired backwards or a pass reads its own //! output the result is visibly not a box blur rather than subtly wrong. +use std::sync::{Arc, LazyLock}; use crate::descriptor::{ Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind, Scale, Unit, @@ -36,28 +37,30 @@ use crate::descriptor::{ use crate::detail::{DetailPass, DetailStage, RenderScale}; use crate::operation::{Affects, Operation, Uniform}; -static DESCRIPTOR: OpDescriptor = OpDescriptor { - id: OpId("detail_probe"), - label: LocalizedKey("op.detail_probe"), - params: &[ParamDescriptor { - id: ParamId("radius"), - label: LocalizedKey("param.detail_probe.radius"), - // A fraction of the frame's shorter edge, which is the unit - // `RenderScale::frame_fraction` converts and the unit a mask feather - // is already stored in. Stating it in pixels is the mistake this - // whole stage is arranged to make impossible. - kind: ParamKind::Scalar { - min: 0.0, - max: 0.25, - scale: Scale::Linear, - unit: Unit::None, - precision: 4, - }, - default: 0.0, - facet: None, - }], - attributes: &[Attribute::Detail], -}; +static DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + id: OpId("detail_probe"), + label: LocalizedKey("op.detail_probe"), + params: vec![ParamDescriptor { + id: ParamId("radius"), + label: LocalizedKey("param.detail_probe.radius"), + // A fraction of the frame's shorter edge, which is the unit + // `RenderScale::frame_fraction` converts and the unit a mask feather + // is already stored in. Stating it in pixels is the mistake this + // whole stage is arranged to make impossible. + kind: ParamKind::Scalar { + min: 0.0, + max: 0.25, + scale: Scale::Linear, + unit: Unit::None, + precision: 4, + }, + default: 0.0, + facet: None, + }], + attributes: vec![Attribute::Detail], + }) +}); /// A separable box blur whose radius is a fraction of the frame's shorter edge. #[derive(Debug, Clone, Copy, Default)] @@ -85,8 +88,8 @@ impl BoxBlur { } impl Operation for BoxBlur { - fn descriptor(&self) -> &'static OpDescriptor { - &DESCRIPTOR + fn descriptor(&self) -> Arc { + DESCRIPTOR.clone() } fn set_param(&mut self, _id: ParamId, value: f32) { diff --git a/core/dr-pipeline/src/framing.rs b/core/dr-pipeline/src/framing.rs index a92cf89..20a58a3 100644 --- a/core/dr-pipeline/src/framing.rs +++ b/core/dr-pipeline/src/framing.rs @@ -40,6 +40,7 @@ use std::f32::consts::PI; use std::fmt::Write as _; +use std::sync::{Arc, LazyLock}; use crate::descriptor::{ Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation, Scale, @@ -73,50 +74,52 @@ static FRAMING_PARAMS: [ParamId; 8] = [ CROP_X, CROP_Y, CROP_W, CROP_H, ANGLE, ROTATION, FLIP_H, FLIP_V, ]; -static DESCRIPTOR: OpDescriptor = OpDescriptor { - // The shape of the frame, and the only operation that changes the - // output's dimensions. - attributes: &[Attribute::Geometry], - id: ID, - label: LocalizedKey("op.framing"), - params: &[ - // Straightening. Degrees rather than a normalised amount because a - // photographer reading "-1.4°" off a horizon knows what it means. - ParamDescriptor::scalar( - "angle", - "param.angle", - -MAX_STRAIGHTEN, - MAX_STRAIGHTEN, - 0.0, - Unit::None, - Scale::Linear, - 2, - ), - // Quarter turns, 0..3. Separate from `angle` because these are exact - // and lossless, and because reorienting a frame is a different - // gesture from nudging a horizon. - ParamDescriptor::scalar( - "rotation", - "param.rotation", - 0.0, - 3.0, - 0.0, - Unit::None, - Scale::Linear, - 0, - ), - ParamDescriptor::switch("flip_h", "param.flip_h"), - ParamDescriptor::switch("flip_v", "param.flip_v"), - // The crop rect, in fractions of the source. Normalised rather than - // in pixels so a crop survives being applied to a proxy, a full - // resolution render, or an export at another size — the same reason - // the viewport renders at display resolution (FR-DSP-1). - ParamDescriptor::fraction("crop_x", "param.crop_x", 0.0), - ParamDescriptor::fraction("crop_y", "param.crop_y", 0.0), - ParamDescriptor::fraction("crop_w", "param.crop_w", 1.0), - ParamDescriptor::fraction("crop_h", "param.crop_h", 1.0), - ], -}; +static DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + // The shape of the frame, and the only operation that changes the + // output's dimensions. + attributes: vec![Attribute::Geometry], + id: ID, + label: LocalizedKey("op.framing"), + params: vec![ + // Straightening. Degrees rather than a normalised amount because a + // photographer reading "-1.4°" off a horizon knows what it means. + ParamDescriptor::scalar( + "angle", + "param.angle", + -MAX_STRAIGHTEN, + MAX_STRAIGHTEN, + 0.0, + Unit::None, + Scale::Linear, + 2, + ), + // Quarter turns, 0..3. Separate from `angle` because these are exact + // and lossless, and because reorienting a frame is a different + // gesture from nudging a horizon. + ParamDescriptor::scalar( + "rotation", + "param.rotation", + 0.0, + 3.0, + 0.0, + Unit::None, + Scale::Linear, + 0, + ), + ParamDescriptor::switch("flip_h", "param.flip_h"), + ParamDescriptor::switch("flip_v", "param.flip_v"), + // The crop rect, in fractions of the source. Normalised rather than + // in pixels so a crop survives being applied to a proxy, a full + // resolution render, or an export at another size — the same reason + // the viewport renders at display resolution (FR-DSP-1). + ParamDescriptor::fraction("crop_x", "param.crop_x", 0.0), + ParamDescriptor::fraction("crop_y", "param.crop_y", 0.0), + ParamDescriptor::fraction("crop_w", "param.crop_w", 1.0), + ParamDescriptor::fraction("crop_h", "param.crop_h", 1.0), + ], + }) +}); /// A normalised crop rectangle, in fractions of the source image. #[derive(Debug, Clone, Copy, PartialEq)] @@ -252,8 +255,8 @@ impl Framing { Self::default() } - pub fn descriptor(&self) -> &'static OpDescriptor { - &DESCRIPTOR + pub fn descriptor(&self) -> Arc { + DESCRIPTOR.clone() } /// TRACES: FR-DEV-3a | FR-DEV-3b | FR-UI-7 @@ -280,7 +283,7 @@ impl Framing { /// into the generated panel underneath a crop control that already exists. pub fn presentation(&self) -> Option { Some(Presentation { - widgets: &[WidgetKind::CropOverlay], + widgets: vec![WidgetKind::CropOverlay], demand: WidgetDemand { // A crop rect is dragged by its corners; nothing about that // reduces to one axis at a time. @@ -290,7 +293,7 @@ impl Framing { // modality, so a thumb is as workable as a mouse. precise_pointing: false, }, - params: &FRAMING_PARAMS, + params: FRAMING_PARAMS.to_vec(), }) } @@ -1426,7 +1429,7 @@ mod tests { fn every_parameter_at_its_extremes_is_survivable() { // The whole descriptor driven to both ends, which is what a codegen // test does and what a corrupt sidecar can do. - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { for value in [-1e9, -1.0, 0.0, 1.0, 1e9, f32::NAN] { let mut f = Framing::new(); f.set_param(p.id, p.clamp(value)); @@ -1610,7 +1613,7 @@ mod tests { // The same contract the operations honour, checked against the // descriptor rather than a literal. let mut f = Framing::new(); - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { f.set_param(p.id, p.default); } assert!(!f.is_active(), "descriptor defaults must be neutral"); @@ -1618,7 +1621,7 @@ mod tests { #[test] fn every_default_is_within_its_declared_range() { - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { assert_eq!(p.clamp(p.default), p.default, "{} is out of range", p.id); } } diff --git a/core/dr-pipeline/src/graph.rs b/core/dr-pipeline/src/graph.rs index 900f25f..209bbd3 100644 --- a/core/dr-pipeline/src/graph.rs +++ b/core/dr-pipeline/src/graph.rs @@ -51,8 +51,8 @@ pub struct OpCapability { /// other, so a new operation joins the right group by declaring what it /// is — which is the only thing its author is well placed to say. /// - /// Never empty; `build.rs` refuses an operation that declares none. - pub attributes: &'static [Attribute], + /// Never empty; both readers refuse an operation that declares none. + pub attributes: Vec, } /// TRACES: FR-DEV-3a | FR-DEV-3b @@ -243,7 +243,7 @@ impl EditGraph { /// because this is what the codegen tests count `---- ` shader blocks /// against, and framing generates a prologue rather than a colour block. /// A UI wanting everything should read [`Self::capabilities`] (FR-DEV-3a). - pub fn descriptors(&self) -> Vec<&'static OpDescriptor> { + pub fn descriptors(&self) -> Vec> { self.ops.iter().map(|o| o.descriptor()).collect() } @@ -281,7 +281,7 @@ impl EditGraph { }) .collect(), presentation: op.presentation(), - attributes: desc.attributes, + attributes: desc.attributes.clone(), } }); @@ -310,7 +310,7 @@ impl EditGraph { // sliders for framing *without naming framing* — see // `Framing::presentation`. presentation: self.framing.presentation(), - attributes: desc.attributes, + attributes: desc.attributes.clone(), }; ops.chain(std::iter::once(framing)).collect() @@ -427,7 +427,11 @@ impl EditGraph { pub fn set_param(&mut self, op: OpId, param: ParamId, value: f32) { if op == crate::framing::ID { - let Some(desc) = self.framing.descriptor().param(param) else { + // Bound rather than chained: `descriptor()` hands back an owned + // `Arc` now, so a `param()` borrowed straight out of the call + // would outlive the temporary it came from. + let descriptor = self.framing.descriptor(); + let Some(desc) = descriptor.param(param) else { log::warn!("unknown parameter {param} on {op}; ignoring"); return; }; @@ -469,7 +473,7 @@ impl EditGraph { /// Reset every parameter of every operation, and the framing, to default. pub fn reset(&mut self) { for op in &mut self.ops { - for p in op.descriptor().params { + for p in &op.descriptor().params { op.set_param(p.id, p.default); } } @@ -625,7 +629,7 @@ impl EditGraph { // structure key deliberately omits because they do not recompile a // shader. Both matter to a cached *result*, so both are here. let mut geometry = mix(FNV_OFFSET, self.framing.structure_key()); - for p in self.framing.descriptor().params { + for p in &self.framing.descriptor().params { geometry = hash_bytes(geometry, p.id.0.as_bytes()); geometry = mix( geometry, @@ -944,7 +948,7 @@ mod tests { .capabilities() .iter() .flat_map(|c| { - c.params.iter().map(move |p| match p.kind { + c.params.iter().map(move |p| match &p.kind { ParamKind::Scalar { min, max, diff --git a/core/dr-pipeline/src/lens.rs b/core/dr-pipeline/src/lens.rs index f6c0b99..a1753f7 100644 --- a/core/dr-pipeline/src/lens.rs +++ b/core/dr-pipeline/src/lens.rs @@ -53,6 +53,7 @@ //! wearing the same lens, which defeats the point of a lens profile. use std::fmt::Write as _; +use std::sync::Arc; use crate::descriptor::{OpDescriptor, ParamId}; use crate::operation::{Helper, Uniform}; @@ -62,8 +63,10 @@ use crate::operation::{Helper, Uniform}; /// Object-safe for the same reason [`crate::operation::Operation`] is: the /// graph holds `Box` in order, so the geometry chain is data. pub trait Warp: Send + Sync { - /// Static description, driving UI generation exactly as for an operation. - fn descriptor(&self) -> &'static OpDescriptor; + /// This warp's description, driving UI generation exactly as for an + /// operation — including being owned rather than `&'static`, for the + /// reason [`crate::descriptor::OpDescriptor`] gives. + fn descriptor(&self) -> Arc; /// Set a parameter. Values arrive already clamped to the descriptor. fn set_param(&mut self, id: ParamId, value: f32); @@ -204,32 +207,38 @@ fn sanitise(id: &str) -> String { #[cfg(test)] mod tests { + use std::sync::LazyLock; + use super::*; use crate::descriptor::Attribute; use crate::descriptor::{LocalizedKey, OpDescriptor, OpId, ParamDescriptor}; - static DESC_A: OpDescriptor = OpDescriptor { - id: OpId("warp_a"), - label: LocalizedKey("a"), - params: &[ParamDescriptor::amount("amount", "a.amount")], - attributes: &[Attribute::Tone], - }; - static DESC_B: OpDescriptor = OpDescriptor { - id: OpId("warp_b"), - label: LocalizedKey("b"), - params: &[ParamDescriptor::amount("amount", "b.amount")], - attributes: &[Attribute::Tone], - }; + static DESC_A: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + id: OpId("warp_a"), + label: LocalizedKey("a"), + params: vec![ParamDescriptor::amount("amount", "a.amount")], + attributes: vec![Attribute::Tone], + }) + }); + static DESC_B: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + id: OpId("warp_b"), + label: LocalizedKey("b"), + params: vec![ParamDescriptor::amount("amount", "b.amount")], + attributes: vec![Attribute::Tone], + }) + }); struct Fake { - desc: &'static OpDescriptor, + desc: Arc, amount: f32, splits: bool, } impl Warp for Fake { - fn descriptor(&self) -> &'static OpDescriptor { - self.desc + fn descriptor(&self) -> Arc { + self.desc.clone() } fn set_param(&mut self, _id: ParamId, value: f32) { self.amount = value; @@ -254,7 +263,7 @@ mod tests { } } - fn fake(desc: &'static OpDescriptor, amount: f32, splits: bool) -> Box { + fn fake(desc: Arc, amount: f32, splits: bool) -> Box { Box::new(Fake { desc, amount, @@ -266,7 +275,7 @@ mod tests { fn no_active_warp_composes_to_nothing() { // The property that keeps the common case free: an image with no lens // correction must not pay for a bilinear sample. - let composed = compose_warps(&[fake(&DESC_A, 0.0, false)]); + let composed = compose_warps(&[fake(DESC_A.clone(), 0.0, false)]); assert!(!composed.is_active()); assert!(composed.uniforms.is_empty()); assert!(!composed.splits_channels); @@ -274,7 +283,7 @@ mod tests { #[test] fn an_active_warp_appears_once() { - let composed = compose_warps(&[fake(&DESC_A, 2.0, false)]); + let composed = compose_warps(&[fake(DESC_A.clone(), 2.0, false)]); assert!(composed.is_active()); assert!(composed.body.contains("---- warp: warp_a ----")); assert!(composed.body.contains("u.warp_a_amount")); @@ -284,7 +293,10 @@ mod tests { fn uniforms_are_prefixed_so_warps_cannot_collide() { // Both fakes declare `amount`; without prefixing the generated struct // would carry a duplicate field and fail to compile. - let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 2.0, false)]); + let composed = compose_warps(&[ + fake(DESC_A.clone(), 1.0, false), + fake(DESC_B.clone(), 2.0, false), + ]); assert!(composed.uniform_fields.contains("warp_a_amount: f32")); assert!(composed.uniform_fields.contains("warp_b_amount: f32")); assert_eq!(composed.uniforms, vec![1.0, 2.0]); @@ -294,7 +306,10 @@ mod tests { fn channel_splitting_is_requested_by_any_active_warp() { // One CA warp among several must switch the whole stage to the // three-sample path. - let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, true)]); + let composed = compose_warps(&[ + fake(DESC_A.clone(), 1.0, false), + fake(DESC_B.clone(), 1.0, true), + ]); assert!(composed.splits_channels); } @@ -302,14 +317,20 @@ mod tests { fn an_inactive_splitting_warp_does_not_force_three_samples() { // CA present but at neutral must cost nothing — otherwise every image // with the panel visible pays triple bandwidth. - let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 0.0, true)]); + let composed = compose_warps(&[ + fake(DESC_A.clone(), 1.0, false), + fake(DESC_B.clone(), 0.0, true), + ]); assert!(composed.is_active()); assert!(!composed.splits_channels); } #[test] fn warps_compose_in_order() { - let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, false)]); + let composed = compose_warps(&[ + fake(DESC_A.clone(), 1.0, false), + fake(DESC_B.clone(), 1.0, false), + ]); let a = composed.body.find("warp_a").expect("a present"); let b = composed.body.find("warp_b").expect("b present"); assert!(a < b, "warps must chain in graph order"); diff --git a/core/dr-pipeline/src/lib.rs b/core/dr-pipeline/src/lib.rs index 4b64e36..e59af4f 100644 --- a/core/dr-pipeline/src/lib.rs +++ b/core/dr-pipeline/src/lib.rs @@ -81,13 +81,13 @@ mod tests { let mut g = EditGraph::default_chain(); for desc in g.descriptors() { for (i, p) in desc.params.iter().enumerate() { - let v = match p.kind { + let v = match &p.kind { ParamKind::Scalar { min, max, .. } => { // A fraction that differs per parameter, so no two // move in lockstep. let fraction = 0.15 + 0.05 * (i % 4) as f32; let step = (max - min) * fraction; - if p.default + step <= max { + if p.default + step <= *max { p.default + step } else { p.default - step @@ -217,7 +217,7 @@ mod tests { // A default outside its own range would mean a fresh image opens // with a value the UI cannot represent. for desc in EditGraph::default_chain().descriptors() { - for p in desc.params { + for p in &desc.params { assert_eq!( p.clamp(p.default), p.default, @@ -236,7 +236,7 @@ mod tests { // operation must read its own default as neutral. let g = EditGraph::default_chain(); for desc in g.descriptors() { - for p in desc.params { + for p in &desc.params { assert_eq!( g.param(desc.id, p.id), Some(p.default), diff --git a/core/dr-pipeline/src/mask.rs b/core/dr-pipeline/src/mask.rs index 47e0067..5ed23da 100644 --- a/core/dr-pipeline/src/mask.rs +++ b/core/dr-pipeline/src/mask.rs @@ -36,6 +36,7 @@ //! see there for what happens when it does not match. use std::fmt::Write as _; +use std::sync::Arc; use crate::descriptor::{OpDescriptor, ParamId}; use crate::operation::Operation; @@ -692,7 +693,7 @@ impl Clone for MaskLayer { fn clone(&self) -> Self { let mut ops = layer_chain(); for (dst, src) in ops.iter_mut().zip(&self.ops) { - for p in src.descriptor().params { + for p in &src.descriptor().params { dst.set_param(p.id, src.param(p.id)); } } @@ -947,7 +948,7 @@ impl MaskLayer { } } - pub fn descriptors(&self) -> Vec<&'static OpDescriptor> { + pub fn descriptors(&self) -> Vec> { self.ops.iter().map(|o| o.descriptor()).collect() } @@ -995,7 +996,7 @@ impl MaskLayer { }) .collect(), presentation: op.presentation(), - attributes: desc.attributes, + attributes: desc.attributes.clone(), } }) .collect() @@ -1007,7 +1008,7 @@ impl MaskLayer { /// arrive at — so "start this layer's edit again" must not throw it away. pub fn reset_adjustments(&mut self) { for op in &mut self.ops { - for p in op.descriptor().params { + for p in &op.descriptor().params { op.set_param(p.id, p.default); } } @@ -1026,10 +1027,18 @@ impl MaskLayer { /// Every non-default parameter, for the sidecar. pub fn params(&self) -> impl Iterator + '_ { self.ops.iter().flat_map(|o| { - let id = o.descriptor().id.0; - o.descriptor().params.iter().filter_map(move |p| { - let v = o.param(p.id); - (v != p.default).then_some((id, p.id.0, v)) + let desc = o.descriptor(); + let id = desc.id.0; + // Collected rather than borrowed from `desc`: a descriptor is an + // `Arc` handed over by value now (FR-PLG-2), so it would be + // dropped at the end of this closure and the lazy iterator would + // outlive it. The ids and defaults are all this needs, and there + // are a handful of them. + let params: Vec<(ParamId, f32)> = + desc.params.iter().map(|p| (p.id, p.default)).collect(); + params.into_iter().filter_map(move |(param, default)| { + let v = o.param(param); + (v != default).then_some((id, param.0, v)) }) }) } diff --git a/core/dr-pipeline/src/operation.rs b/core/dr-pipeline/src/operation.rs index 35978bc..cebcf2f 100644 --- a/core/dr-pipeline/src/operation.rs +++ b/core/dr-pipeline/src/operation.rs @@ -21,6 +21,7 @@ //! generator emits readable, commented output — see [`compose`]. use std::fmt::Write as _; +use std::sync::Arc; use dr_types::{ColourSpace, Transfer}; @@ -171,7 +172,7 @@ impl Invalidation { pub(crate) fn hash_op(h: u64, op: &dyn Operation) -> u64 { let desc = op.descriptor(); let mut h = hash_bytes(h, desc.id.0.as_bytes()); - for p in desc.params { + for p in &desc.params { h = hash_bytes(h, p.id.0.as_bytes()); h = mix(h, u64::from(canonical_bits(op.param(p.id)))); } @@ -213,6 +214,12 @@ pub(crate) const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; pub struct Uniform { /// Field name as it appears in WGSL. Prefixed with the op id by the /// composer, so two operations may both declare `amount`. + /// + /// `&'static str` for the reason [`crate::descriptor::intern`] gives about + /// ids: a uniform name is a small, deduplicated, process-lifetime piece of + /// vocabulary, and a declared operation interns its names once when it is + /// parsed rather than allocating them on every `uniforms()` call — which + /// happens per composition, and composition happens per frame. pub name: &'static str, pub value: f32, } @@ -222,8 +229,32 @@ pub struct Uniform { /// Object-safe: the pipeline holds `Box` in graph order, so /// order is data rather than code (ARCH §3.4). pub trait Operation: Send + Sync { - /// Static description, driving UI generation (FR-DEV-3a). - fn descriptor(&self) -> &'static OpDescriptor; + /// TRACES: FR-DEV-3a | FR-PLG-2 + /// This operation's description, driving UI generation (FR-DEV-3a). + /// + /// **Shared and owned rather than `&'static`.** See [`OpDescriptor`] for + /// why — in short, a `&'static` descriptor is one a compile-time literal + /// can produce and a load-time declaration cannot, which would make a + /// plugin a second-class kind of operation for a reason that is purely an + /// artefact of how the built-ins happen to be written. + /// + /// # What this costs, and where + /// + /// One `Arc` clone and drop per call. Descriptors are read when a panel is + /// built (`EditGraph::capabilities`), when a sidecar is written or read, + /// and when the history names what changed — none of which is a per-frame + /// path. + /// + /// There is **one** exception, and it is worth stating plainly rather than + /// letting somebody discover it with a profiler: [`compose_full`] reads + /// `descriptor().id` once per *active* operation to prefix its uniforms, + /// and `dr-ui` composes on every frame it draws. That is a handful of + /// atomic increments — a dozen or so, against a composition that is + /// already building several kilobytes of WGSL text from scratch on the + /// same call. If composition ever stops being a per-frame operation, this + /// stops being a question at all; while it is one, the refcount is not + /// what makes it expensive. + fn descriptor(&self) -> Arc; /// Set a parameter. Values arrive already clamped to the descriptor. fn set_param(&mut self, id: ParamId, value: f32); @@ -321,7 +352,13 @@ pub trait Operation: Send + Sync { /// Emitted once per *distinct* function name even if several operations /// request it, so shared helpers (luminance, soft clipping) are declared /// exactly once. - fn helpers(&self) -> &'static [Helper] { + /// + /// Borrowed from `self` rather than `'static`, for the reason + /// [`Self::descriptor`] is owned: a generated operation returns a + /// `&'static [Helper]` and coerces, while an operation built from a + /// declaration at load time owns its list. The [`Helper`] *strings* + /// themselves stay `&'static` — they are interned, like the ids. + fn helpers(&self) -> &[Helper] { &[] } @@ -1184,31 +1221,37 @@ pub(crate) fn sanitise(id: &str) -> String { #[cfg(test)] mod tests { use super::*; + use std::sync::LazyLock; + use crate::descriptor::Attribute; use crate::descriptor::{LocalizedKey, OpId, ParamDescriptor}; - static DESC_A: OpDescriptor = OpDescriptor { - id: OpId("op_a"), - label: LocalizedKey("a"), - params: &[ParamDescriptor::amount("amount", "a.amount")], - attributes: &[Attribute::Tone], - }; - static DESC_B: OpDescriptor = OpDescriptor { - id: OpId("op_b"), - label: LocalizedKey("b"), - params: &[ParamDescriptor::amount("amount", "b.amount")], - attributes: &[Attribute::Tone], - }; + static DESC_A: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + id: OpId("op_a"), + label: LocalizedKey("a"), + params: vec![ParamDescriptor::amount("amount", "a.amount")], + attributes: vec![Attribute::Tone], + }) + }); + static DESC_B: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + id: OpId("op_b"), + label: LocalizedKey("b"), + params: vec![ParamDescriptor::amount("amount", "b.amount")], + attributes: vec![Attribute::Tone], + }) + }); struct Fake { - desc: &'static OpDescriptor, + desc: Arc, amount: f32, helper: Option, } impl Operation for Fake { - fn descriptor(&self) -> &'static OpDescriptor { - self.desc + fn descriptor(&self) -> Arc { + self.desc.clone() } fn set_param(&mut self, _id: ParamId, value: f32) { self.amount = value; @@ -1241,7 +1284,7 @@ mod tests { source: "fn luma(c: vec3) -> f32 { return c.g; }", }]; - fn fake(desc: &'static OpDescriptor, amount: f32, helper: bool) -> Box { + fn fake(desc: Arc, amount: f32, helper: bool) -> Box { Box::new(Fake { desc, amount, @@ -1253,7 +1296,7 @@ mod tests { fn an_inactive_operation_contributes_nothing() { // The point of composing rather than branching: an op at neutral // must not appear in the source at all. - let ops = vec![fake(&DESC_A, 0.0, false)]; + let ops = vec![fake(DESC_A.clone(), 0.0, false)]; let shader = compose(&ops); assert!( !shader.source.contains("op_a"), @@ -1274,7 +1317,7 @@ mod tests { #[test] fn an_active_operation_appears_once() { - let ops = vec![fake(&DESC_A, 2.0, false)]; + let ops = vec![fake(DESC_A.clone(), 2.0, false)]; let shader = compose(&ops); assert!(shader.source.contains("---- op_a ----")); assert!(shader.source.contains("u.op_a_amount")); @@ -1285,7 +1328,10 @@ mod tests { // Both fakes declare a uniform called `amount`. Without prefixing, // the generated struct would have a duplicate field and fail to // compile — the failure mode that makes naive concatenation fragile. - let ops = vec![fake(&DESC_A, 1.0, false), fake(&DESC_B, 2.0, false)]; + let ops = vec![ + fake(DESC_A.clone(), 1.0, false), + fake(DESC_B.clone(), 2.0, false), + ]; let shader = compose(&ops); assert!(shader.source.contains("op_a_amount: f32")); assert!(shader.source.contains("op_b_amount: f32")); @@ -1295,7 +1341,10 @@ mod tests { #[test] fn uniform_values_follow_declaration_order() { - let ops = vec![fake(&DESC_A, 1.5, false), fake(&DESC_B, 2.5, false)]; + let ops = vec![ + fake(DESC_A.clone(), 1.5, false), + fake(DESC_B.clone(), 2.5, false), + ]; let shader = compose(&ops); assert_eq!(shader.uniforms[PREAMBLE_FIELDS], 1.5); assert_eq!(shader.uniforms[PREAMBLE_FIELDS + 1], 2.5); @@ -1305,7 +1354,10 @@ mod tests { fn a_shared_helper_is_emitted_once() { // Two operations wanting the same helper must not produce a // duplicate function definition. - let ops = vec![fake(&DESC_A, 1.0, true), fake(&DESC_B, 1.0, true)]; + let ops = vec![ + fake(DESC_A.clone(), 1.0, true), + fake(DESC_B.clone(), 1.0, true), + ]; let shader = compose(&ops); assert_eq!( shader.source.matches("fn luma(").count(), @@ -1319,7 +1371,17 @@ mod tests { // WGSL rejects a uniform struct whose size is not a multiple of 16. for n in 0..6 { let ops: Vec> = (0..n) - .map(|i| fake(if i % 2 == 0 { &DESC_A } else { &DESC_B }, 1.0, false)) + .map(|i| { + fake( + if i % 2 == 0 { + DESC_A.clone() + } else { + DESC_B.clone() + }, + 1.0, + false, + ) + }) .collect(); let shader = compose(&ops); assert_eq!( @@ -1335,11 +1397,14 @@ mod tests { fn structure_hash_ignores_values_but_tracks_the_op_set() { // The property the shader cache depends on: moving a slider must not // trigger a recompile, but enabling an operation must. - let a1 = compose(&[fake(&DESC_A, 1.0, false)]).structure_hash; - let a2 = compose(&[fake(&DESC_A, 9.0, false)]).structure_hash; + let a1 = compose(&[fake(DESC_A.clone(), 1.0, false)]).structure_hash; + let a2 = compose(&[fake(DESC_A.clone(), 9.0, false)]).structure_hash; assert_eq!(a1, a2, "a value change must reuse the compiled pipeline"); - let both = compose(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, false)]); + let both = compose(&[ + fake(DESC_A.clone(), 1.0, false), + fake(DESC_B.clone(), 1.0, false), + ]); assert_ne!(a1, both.structure_hash, "a different op-set must recompile"); } @@ -1347,8 +1412,14 @@ mod tests { fn structure_hash_is_order_sensitive() { // Operation order is data (ARCH §3.4); two orders are different // shaders and must not share a cache entry. - let ab = compose(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, false)]); - let ba = compose(&[fake(&DESC_B, 1.0, false), fake(&DESC_A, 1.0, false)]); + let ab = compose(&[ + fake(DESC_A.clone(), 1.0, false), + fake(DESC_B.clone(), 1.0, false), + ]); + let ba = compose(&[ + fake(DESC_B.clone(), 1.0, false), + fake(DESC_A.clone(), 1.0, false), + ]); assert_ne!(ab.structure_hash, ba.structure_hash); } @@ -1412,7 +1483,7 @@ mod tests { // Exposure and the tonal controls act on white-balanced values; if // the multiply came afterwards, every operation would be reasoning // about a green-cast image. - let ops = vec![fake(&DESC_A, 2.0, false)]; + let ops = vec![fake(DESC_A.clone(), 2.0, false)]; let source = compose(&ops).source; let wb = source.find("u.as_shot_wb").expect("wb applied"); let op = source.find("---- op_a ----").expect("op present"); @@ -1472,7 +1543,7 @@ mod tests { // The other half, and the one that would fail silently: a bug that // suppressed the tail unconditionally renders every ordinary edit // flat and uncorrected, which reads as a broken camera profile. - let source = compose(&[fake(&DESC_A, 2.0, false)]).source; + let source = compose(&[fake(DESC_A.clone(), 2.0, false)]).source; assert!(source.contains("base_curve_last.z > 0.5")); assert!(source.contains("Camera space -> linear sRGB")); } @@ -1484,7 +1555,7 @@ mod tests { // stock loaded is the default state of every photograph in the // catalogue, and it must not disturb the camera's own rendering. let film: Box = Box::new(crate::ops::FilmSim::new()); - let source = compose(&[film, fake(&DESC_A, 2.0, false)]).source; + let source = compose(&[film, fake(DESC_A.clone(), 2.0, false)]).source; assert!(source.contains("base_curve_last.z > 0.5")); assert!(source.contains("Camera space -> linear sRGB")); } @@ -1493,7 +1564,7 @@ mod tests { fn the_camera_matrix_is_applied_after_the_operations() { // Adjustments are meaningful in sensor-native space, where highlight // headroom still exists; converting first would clip it away. - let ops = vec![fake(&DESC_A, 2.0, false)]; + let ops = vec![fake(DESC_A.clone(), 2.0, false)]; let source = compose(&ops).source; let op = source.find("---- op_a ----").expect("op present"); let matrix = source.find("u.cam_to_srgb_0").expect("matrix applied"); @@ -1514,7 +1585,7 @@ mod tests { // Before the matrix: the curve was tuned against this body's own // primaries. Applied after the conversion it would be a Canon // rendering acting on sRGB values, which is a different curve. - let ops = vec![fake(&DESC_A, 2.0, false)]; + let ops = vec![fake(DESC_A.clone(), 2.0, false)]; let source = compose(&ops).source; let op = source.find("---- op_a ----").expect("op present"); let curve = source @@ -1592,7 +1663,7 @@ mod tests { // it must not pick up an identity matrix multiply for the sake of // generality. Asserted against the source rather than against timing, // which would not fail reliably. - let ops = vec![fake(&DESC_A, 1.0, false)]; + let ops = vec![fake(DESC_A.clone(), 1.0, false)]; let srgb = compose_to(&ops, ColourSpace::Srgb).source; assert!( !srgb.contains("Linear sRGB -> linear sRGB"), @@ -1607,7 +1678,7 @@ mod tests { // in linear sRGB, the primaries conversion carries it into the wider // space, and only then is it clipped — clipping first would discard // exactly the colours the wider space was chosen to keep. - let source = compose_to(&[fake(&DESC_A, 1.0, false)], ColourSpace::DisplayP3).source; + let source = compose_to(&[fake(DESC_A.clone(), 1.0, false)], ColourSpace::DisplayP3).source; let camera = source.find("u.cam_to_srgb_0").expect("camera matrix"); let convert = source .find("Linear sRGB -> linear Display P3") @@ -1659,7 +1730,7 @@ mod tests { // an export that came out sRGB and claimed to be Display P3. let mut seen: Vec = Vec::new(); for space in ColourSpace::ALL { - let h = compose_to(&[fake(&DESC_A, 1.0, false)], space).structure_hash; + let h = compose_to(&[fake(DESC_A.clone(), 1.0, false)], space).structure_hash; assert!(!seen.contains(&h), "{space:?} collides with another space"); seen.push(h); } @@ -1669,7 +1740,7 @@ mod tests { fn generated_source_carries_a_do_not_edit_banner() { // Someone will eventually find this in a debugger and try to fix it // in place. - let shader = compose(&[fake(&DESC_A, 1.0, false)]); + let shader = compose(&[fake(DESC_A.clone(), 1.0, false)]); assert!(shader.source.starts_with("// GENERATED")); } diff --git a/core/dr-pipeline/src/ops/aberration.rs b/core/dr-pipeline/src/ops/aberration.rs index 1cf7bdb..0d6117e 100644 --- a/core/dr-pipeline/src/ops/aberration.rs +++ b/core/dr-pipeline/src/ops/aberration.rs @@ -31,6 +31,7 @@ //! untouched means a mis-set correction shifts the channels that contribute //! least to perceived sharpness. Scaling all three about a virtual reference //! would soften the image even when the correction is right. +use std::sync::{Arc, LazyLock}; use crate::descriptor::{ Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit, @@ -49,36 +50,38 @@ pub const BLUE: ParamId = ParamId("blue"); /// resolution left to tune by eye at 100%. const MAX_SCALE: f32 = 0.005; -static DESCRIPTOR: OpDescriptor = OpDescriptor { - attributes: &[Attribute::Optics], - id: ID, - label: LocalizedKey("op.aberration"), - params: &[ - // Two independent controls rather than one: the red and blue - // displacements are caused by different ends of the spectrum and are - // not symmetric, so a single "fringing" slider could not remove both. - ParamDescriptor::scalar( - "red", - "param.aberration.red", - -100.0, - 100.0, - 0.0, - Unit::None, - Scale::Linear, - 0, - ), - ParamDescriptor::scalar( - "blue", - "param.aberration.blue", - -100.0, - 100.0, - 0.0, - Unit::None, - Scale::Linear, - 0, - ), - ], -}; +static DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + attributes: vec![Attribute::Optics], + id: ID, + label: LocalizedKey("op.aberration"), + params: vec![ + // Two independent controls rather than one: the red and blue + // displacements are caused by different ends of the spectrum and are + // not symmetric, so a single "fringing" slider could not remove both. + ParamDescriptor::scalar( + "red", + "param.aberration.red", + -100.0, + 100.0, + 0.0, + Unit::None, + Scale::Linear, + 0, + ), + ParamDescriptor::scalar( + "blue", + "param.aberration.blue", + -100.0, + 100.0, + 0.0, + Unit::None, + Scale::Linear, + 0, + ), + ], + }) +}); #[derive(Debug, Default, Clone)] pub struct Aberration { @@ -113,8 +116,8 @@ impl Aberration { } impl Warp for Aberration { - fn descriptor(&self) -> &'static OpDescriptor { - &DESCRIPTOR + fn descriptor(&self) -> Arc { + DESCRIPTOR.clone() } fn set_param(&mut self, id: ParamId, value: f32) { @@ -300,7 +303,7 @@ mod tests { #[test] fn every_default_is_neutral() { let mut a = Aberration::new(); - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { a.set_param(p.id, p.default); } assert!(!a.is_active(), "descriptor defaults must be neutral"); diff --git a/core/dr-pipeline/src/ops/capture_sharpen.rs b/core/dr-pipeline/src/ops/capture_sharpen.rs index 20e4909..6bda4d2 100644 --- a/core/dr-pipeline/src/ops/capture_sharpen.rs +++ b/core/dr-pipeline/src/ops/capture_sharpen.rs @@ -124,6 +124,7 @@ //! would be a guess dressed as a number; `resolves` is the line the stage //! already draws, and drawing it in two places differently is worse than a //! visible step. +use std::sync::{Arc, LazyLock}; use crate::descriptor::{ Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit, @@ -163,46 +164,48 @@ const MAX_KERNEL: f32 = 48.0; /// every editor's capture sharpening starts. const DEFAULT_RADIUS: f32 = 1.0; -static DESCRIPTOR: OpDescriptor = OpDescriptor { - id: ID, - label: LocalizedKey("op.capture_sharpen"), - params: &[ - // Amount carries the neutral, which is why it is first: the operation - // is off when this is zero regardless of the other two, so a reset is - // one control and the panel's ordering matches the way it is used. - ParamDescriptor::amount("amount", "param.amount"), - // In **source pixels** — see the module documentation. Half a photosite - // is the smallest radius that means anything on a Bayer sensor, and - // three is already past the point where an unsharp mask is sharpening - // rather than adding local contrast; a photographer wanting the latter - // wants clarity, which is a different operation with a different unit. - ParamDescriptor::scalar( - "radius", - "param.radius", - 0.5, - 3.0, - DEFAULT_RADIUS, - Unit::None, - Scale::Linear, - 2, - ), - // A fraction, but declared as a scalar rather than through - // `ParamDescriptor::fraction` for its precision alone: four decimal - // places on a control whose whole useful travel is a dozen steps - // reads as noise, and invites fiddling with digits that do nothing. - ParamDescriptor::scalar( - "threshold", - "param.threshold", - 0.0, - 1.0, - 0.0, - Unit::None, - Scale::Linear, - 2, - ), - ], - attributes: &[Attribute::Detail], -}; +static DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + id: ID, + label: LocalizedKey("op.capture_sharpen"), + params: vec![ + // Amount carries the neutral, which is why it is first: the operation + // is off when this is zero regardless of the other two, so a reset is + // one control and the panel's ordering matches the way it is used. + ParamDescriptor::amount("amount", "param.amount"), + // In **source pixels** — see the module documentation. Half a photosite + // is the smallest radius that means anything on a Bayer sensor, and + // three is already past the point where an unsharp mask is sharpening + // rather than adding local contrast; a photographer wanting the latter + // wants clarity, which is a different operation with a different unit. + ParamDescriptor::scalar( + "radius", + "param.radius", + 0.5, + 3.0, + DEFAULT_RADIUS, + Unit::None, + Scale::Linear, + 2, + ), + // A fraction, but declared as a scalar rather than through + // `ParamDescriptor::fraction` for its precision alone: four decimal + // places on a control whose whole useful travel is a dozen steps + // reads as noise, and invites fiddling with digits that do nothing. + ParamDescriptor::scalar( + "threshold", + "param.threshold", + 0.0, + 1.0, + 0.0, + Unit::None, + Scale::Linear, + 2, + ), + ], + attributes: vec![Attribute::Detail], + }) +}); /// TRACES: FR-DEV-3 /// Capture sharpening: a separable unsharp mask with a contrast threshold. @@ -275,8 +278,8 @@ impl CaptureSharpen { } impl Operation for CaptureSharpen { - fn descriptor(&self) -> &'static OpDescriptor { - &DESCRIPTOR + fn descriptor(&self) -> Arc { + DESCRIPTOR.clone() } fn set_param(&mut self, id: ParamId, value: f32) { diff --git a/core/dr-pipeline/src/ops/colour_mixer.rs b/core/dr-pipeline/src/ops/colour_mixer.rs index 60abe72..c2205b2 100644 --- a/core/dr-pipeline/src/ops/colour_mixer.rs +++ b/core/dr-pipeline/src/ops/colour_mixer.rs @@ -29,6 +29,7 @@ //! the band acts at full strength right up to a hard edge; and with two bands //! adjusted, each one's share depends on what the other is set to, so turning //! up one colour's saturation quietly weakened its neighbour's hue shift. +use std::sync::{Arc, LazyLock}; use crate::descriptor::{ Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, @@ -123,8 +124,9 @@ impl Channel { } // Parameter descriptors, one per band per channel. Written out rather than -// generated because `ParamDescriptor` must be `const` to live in a `static`, -// and a const loop cannot build a slice. The macro keeps it honest. +// looped because `concat!` needs literals: every id is built from its band's +// key, and a runtime loop has no way to spell `orange_sat`. The macro keeps it +// honest. // // **Every one of them is faceted**, and that is what makes the operation // legible in a panel. Thirty-six parameters presented as a flat list are @@ -137,7 +139,7 @@ impl Channel { // §4.3a); this only says the parameter acts on the band centred there. macro_rules! band_params { ($(($key:literal, $hue:literal)),* $(,)?) => { - &[ + vec![ $( ParamDescriptor::amount( concat!($key, "_hue"), @@ -175,25 +177,27 @@ macro_rules! band_params { // the same reason as those: `concat!` needs literals, so the keys and hues // cannot be read out of `BANDS` here. `facets_match_their_bands` below is // what keeps them from drifting. -static DESCRIPTOR: OpDescriptor = OpDescriptor { - attributes: &[Attribute::Colour], - id: ID, - label: LocalizedKey("op.colour_mixer"), - params: band_params![ - ("red", 0.0), - ("orange", 30.0), - ("yellow", 60.0), - ("chartreuse", 90.0), - ("green", 120.0), - ("spring", 150.0), - ("cyan", 180.0), - ("azure", 210.0), - ("blue", 240.0), - ("violet", 270.0), - ("magenta", 300.0), - ("rose", 330.0), - ], -}; +static DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + attributes: vec![Attribute::Colour], + id: ID, + label: LocalizedKey("op.colour_mixer"), + params: band_params![ + ("red", 0.0), + ("orange", 30.0), + ("yellow", 60.0), + ("chartreuse", 90.0), + ("green", 120.0), + ("spring", 150.0), + ("cyan", 180.0), + ("azure", 210.0), + ("blue", 240.0), + ("violet", 270.0), + ("magenta", 300.0), + ("rose", 330.0), + ], + }) +}); static MIXER_HELPERS: &[Helper] = &[ helpers::LUMINANCE, @@ -306,8 +310,8 @@ impl ColourMixer { } impl Operation for ColourMixer { - fn descriptor(&self) -> &'static OpDescriptor { - &DESCRIPTOR + fn descriptor(&self) -> Arc { + DESCRIPTOR.clone() } fn set_param(&mut self, id: ParamId, value: f32) { @@ -505,7 +509,7 @@ mod tests { fn every_descriptor_id_resolves_to_a_band_and_channel() { // The link between the descriptor list and the value array. A // mismatch would make a slider silently adjust nothing. - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { assert!( ColourMixer::index_of(p.id).is_some(), "{} does not map to a band", @@ -547,7 +551,7 @@ mod tests { // and a hue mistyped there would put a row's swatch on a colour the // band does not act on — a control that lies about what it edits, // which is worse than one with no swatch at all. - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { let facet = p.facet.expect("every mixer parameter is faceted"); let (band_key, _) = p.id.0.rsplit_once('_').expect("id is band_channel"); let band = BANDS @@ -577,7 +581,7 @@ mod tests { // the aspect keyed per band, grouping by it would produce thirty-six // groups of one and nothing would have been gained. let mut per_aspect = std::collections::BTreeMap::new(); - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { let facet = p.facet.expect("faceted"); *per_aspect.entry(facet.aspect.0).or_insert(0) += 1; } diff --git a/core/dr-pipeline/src/ops/curve.rs b/core/dr-pipeline/src/ops/curve.rs index ce845bd..03c201e 100644 --- a/core/dr-pipeline/src/ops/curve.rs +++ b/core/dr-pipeline/src/ops/curve.rs @@ -79,6 +79,7 @@ //! whole composition scheme rests on (ARCH §5.6). use std::fmt::Write as _; +use std::sync::{Arc, LazyLock}; use crate::descriptor::{ Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation, @@ -341,11 +342,12 @@ const fn facet_of(aspect: &'static str, channel: Channel) -> Facet { /// One channel's ten descriptors, defaulted onto the identity diagonal. /// -/// Written out per point rather than looped because a `ParamDescriptor` has to -/// be `const` to live in a `static`, and a const loop cannot build a slice. +/// Written out per point rather than looped because `concat!` needs literals: +/// the parameter ids are built from the channel's prefix, and a runtime loop +/// has no way to spell `r_p0_x`. macro_rules! channel_params { ($(($prefix:literal, $channel:expr)),* $(,)?) => { - &[$( + vec![$( coord(concat!($prefix, "p0_x"), "param.curve.p0_x", 0.0) .faceted(facet_of("param.curve.p0_x", $channel)), coord(concat!($prefix, "p0_y"), "param.curve.p0_y", 0.0) @@ -370,26 +372,28 @@ macro_rules! channel_params { }; } -static DESCRIPTOR: OpDescriptor = OpDescriptor { - // Both, and this is the case the plural exists for: the master curve is - // tonal and the per-channel curves are chromatic. Filing it under one - // would hide it from half the people looking for it. - attributes: &[Attribute::Tone, Attribute::Colour], - id: ID, - label: LocalizedKey("op.tone_curve"), - // Defaults lie on y = x, so a fresh curve is the identity and the - // operation reports itself inactive — on every channel. - // - // The master's ten come first, and stay first: a frontend addresses a - // point by its offset from the first parameter of the run it is drawing, - // and this is also the order one falling back to sliders reads them in. - params: channel_params![ - ("", Channel::Master), - ("r_", Channel::Red), - ("g_", Channel::Green), - ("b_", Channel::Blue), - ], -}; +static DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + // Both, and this is the case the plural exists for: the master curve is + // tonal and the per-channel curves are chromatic. Filing it under one + // would hide it from half the people looking for it. + attributes: vec![Attribute::Tone, Attribute::Colour], + id: ID, + label: LocalizedKey("op.tone_curve"), + // Defaults lie on y = x, so a fresh curve is the identity and the + // operation reports itself inactive — on every channel. + // + // The master's ten come first, and stay first: a frontend addresses a + // point by its offset from the first parameter of the run it is drawing, + // and this is also the order one falling back to sliders reads them in. + params: channel_params![ + ("", Channel::Master), + ("r_", Channel::Red), + ("g_", Channel::Green), + ("b_", Channel::Blue), + ], + }) +}); /// One span of a monotone cubic Hermite spline. Shared by all four curves. const CURVE_SPAN: Helper = Helper { @@ -699,8 +703,8 @@ impl ToneCurve { } impl Operation for ToneCurve { - fn descriptor(&self) -> &'static OpDescriptor { - &DESCRIPTOR + fn descriptor(&self) -> Arc { + DESCRIPTOR.clone() } fn set_param(&mut self, id: ParamId, value: f32) { @@ -727,7 +731,7 @@ impl Operation for ToneCurve { Some(Presentation { // One entry: there is no second way to draw a tone curve that is // better than the sliders the frontend falls back to anyway. - widgets: &[WidgetKind::ToneCurve], + widgets: vec![WidgetKind::ToneCurve], demand: WidgetDemand { // A point is dragged in x and y together — that is what a // curve *is*, and a frontend that can only move one axis at a @@ -739,7 +743,7 @@ impl Operation for ToneCurve { // leave the other thirty stranded as sliders beneath the plot; // which of the four it draws at a time is its own affair, and the // facets are what let it decide without naming a channel. - params: &CURVE_PARAMS, + params: CURVE_PARAMS.to_vec(), }) } @@ -910,7 +914,7 @@ mod tests { // Opening an unedited image must show the image. let c = ToneCurve::new(); assert!(!c.is_active()); - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { assert_eq!(c.param(p.id), p.default); } } @@ -927,7 +931,7 @@ mod tests { #[test] fn every_parameter_id_maps_to_a_point() { - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { assert!( ToneCurve::index_of(p.id).is_some(), "{} does not map to a point", @@ -1196,7 +1200,7 @@ mod tests { ); assert_eq!(presentation.choose(|_| false), None); assert_eq!(presentation.params.len(), DESCRIPTOR.params.len()); - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { assert!( presentation.params.contains(&p.id), "{} is not owned by the widget", diff --git a/core/dr-pipeline/src/ops/distortion.rs b/core/dr-pipeline/src/ops/distortion.rs index d5fe6b7..c327e1c 100644 --- a/core/dr-pipeline/src/ops/distortion.rs +++ b/core/dr-pipeline/src/ops/distortion.rs @@ -25,6 +25,7 @@ //! set three correlated coefficients, and hand-correcting a lens with no //! profile is a "make the horizon straight" task, which one term does well. //! The full triple is reachable by loading a profile. +use std::sync::{Arc, LazyLock}; use crate::descriptor::{ Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit, @@ -35,25 +36,27 @@ use crate::operation::{Helper, Uniform}; pub const ID: OpId = OpId("distortion"); pub const AMOUNT: ParamId = ParamId("amount"); -static DESCRIPTOR: OpDescriptor = OpDescriptor { - attributes: &[Attribute::Optics], - id: ID, - label: LocalizedKey("op.distortion"), - // ±100 maps to a ±0.25 cubic coefficient. That covers an uncorrected - // fisheye at one end and strong pincushion at the other; beyond it the - // inverse mapping stops being single-valued near the corners and the - // correction folds the image over itself. - params: &[ParamDescriptor::scalar( - "amount", - "param.distortion.amount", - -100.0, - 100.0, - 0.0, - Unit::None, - Scale::Linear, - 0, - )], -}; +static DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + attributes: vec![Attribute::Optics], + id: ID, + label: LocalizedKey("op.distortion"), + // ±100 maps to a ±0.25 cubic coefficient. That covers an uncorrected + // fisheye at one end and strong pincushion at the other; beyond it the + // inverse mapping stops being single-valued near the corners and the + // correction folds the image over itself. + params: vec![ParamDescriptor::scalar( + "amount", + "param.distortion.amount", + -100.0, + 100.0, + 0.0, + Unit::None, + Scale::Linear, + 0, + )], + }) +}); /// The cubic coefficient at full slider travel. const MAX_COEFF: f32 = 0.25; @@ -108,8 +111,8 @@ impl Distortion { } impl Warp for Distortion { - fn descriptor(&self) -> &'static OpDescriptor { - &DESCRIPTOR + fn descriptor(&self) -> Arc { + DESCRIPTOR.clone() } fn set_param(&mut self, id: ParamId, value: f32) { diff --git a/core/dr-pipeline/src/ops/film_sim.rs b/core/dr-pipeline/src/ops/film_sim.rs index 49b8a16..19a3ac0 100644 --- a/core/dr-pipeline/src/ops/film_sim.rs +++ b/core/dr-pipeline/src/ops/film_sim.rs @@ -29,6 +29,7 @@ //! Declared as a plain struct here rather than imported, so that dr-pipeline //! keeps its no-dependency property (ARCH §6.5a) exactly as `vignetting` does //! with `Pa`. +use std::sync::{Arc, LazyLock}; use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId}; use crate::operation::{Operation, Uniform}; @@ -73,29 +74,31 @@ static MATRIX_FIELDS: [[&str; 3]; 3] = [ ["m20", "m21", "m22"], ]; -static DESCRIPTOR: OpDescriptor = OpDescriptor { - // Tone and colour both, and not `Effect`: a stock is not something applied - // on top of a photograph, it is what the photograph was made on. - attributes: &[Attribute::Tone, Attribute::Colour], - id: ID, - label: LocalizedKey("op.film_sim"), - params: &[ - ParamDescriptor::stops("exposure", "param.film_sim.exposure", -3.0, 3.0), - ParamDescriptor::stops("print_exposure", "param.film_sim.print_exposure", -3.0, 3.0), - // TRACES: FR-DEV-3f - // Development, in stops of push. Bounded by what the manufacturers - // actually published: Double-X's measured axis spans about -1 to +2, - // and beyond a range like that a curve would have to be invented. - ParamDescriptor::stops("push", "param.film_sim.push", -1.0, 3.0), - // TRACES: FR-DEV-3f - // Which frame this was taken on — the half of the enlargement a - // photograph cannot supply. A crystal is a fixed size in micrometres, - // so how grainy a picture looks is film size against output size, and - // the same emulsion on 4x5 renders about three times smoother than on - // 35mm at the same print. - ParamDescriptor::choice("format", "param.film_sim.format", &FORMATS), - ], -}; +static DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + // Tone and colour both, and not `Effect`: a stock is not something applied + // on top of a photograph, it is what the photograph was made on. + attributes: vec![Attribute::Tone, Attribute::Colour], + id: ID, + label: LocalizedKey("op.film_sim"), + params: vec![ + ParamDescriptor::stops("exposure", "param.film_sim.exposure", -3.0, 3.0), + ParamDescriptor::stops("print_exposure", "param.film_sim.print_exposure", -3.0, 3.0), + // TRACES: FR-DEV-3f + // Development, in stops of push. Bounded by what the manufacturers + // actually published: Double-X's measured axis spans about -1 to +2, + // and beyond a range like that a curve would have to be invented. + ParamDescriptor::stops("push", "param.film_sim.push", -1.0, 3.0), + // TRACES: FR-DEV-3f + // Which frame this was taken on — the half of the enlargement a + // photograph cannot supply. A crystal is a fixed size in micrometres, + // so how grainy a picture looks is film size against output size, and + // the same emulsion on 4x5 renders about three times smoother than on + // 35mm at the same print. + ParamDescriptor::choice("format", "param.film_sim.format", FORMATS.to_vec()), + ], + }) +}); /// A stock reduced to what a shader runs, as `dr-film` bakes it. /// @@ -190,8 +193,8 @@ impl FilmSim { } impl Operation for FilmSim { - fn descriptor(&self) -> &'static OpDescriptor { - &DESCRIPTOR + fn descriptor(&self) -> Arc { + DESCRIPTOR.clone() } fn set_param(&mut self, id: ParamId, value: f32) { diff --git a/core/dr-pipeline/src/ops/local_contrast.rs b/core/dr-pipeline/src/ops/local_contrast.rs index fe25ab5..b030a9d 100644 --- a/core/dr-pipeline/src/ops/local_contrast.rs +++ b/core/dr-pipeline/src/ops/local_contrast.rs @@ -153,6 +153,7 @@ //! this file. use std::marker::PhantomData; +use std::sync::{Arc, LazyLock}; use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId}; use crate::detail::{DetailPass, DetailStage, RenderScale}; @@ -181,7 +182,12 @@ const TRUNCATION: f32 = 2.0; /// between clarity and texture can be read side by side, which is the one /// thing a reader comes to this file to do. pub struct Recipe { - descriptor: &'static OpDescriptor, + /// The static this operation's descriptor is built in. A `LazyLock` + /// rather than a reference to a descriptor, because a descriptor is an + /// owned value handed out as an `Arc` now (FR-PLG-2), and a `const` + /// recipe cannot hold an `Arc` — only a reference to the static that + /// makes one. + descriptor: &'static LazyLock>, helpers: &'static [Helper], /// The Gaussian's σ, as a fraction of the frame's shorter edge. sigma: f32, @@ -253,19 +259,23 @@ impl Band for Fine { }; } -static CLARITY_DESCRIPTOR: OpDescriptor = OpDescriptor { - id: CLARITY, - label: LocalizedKey("op.clarity"), - params: &[ParamDescriptor::amount("amount", "param.clarity.amount")], - attributes: &[Attribute::Detail], -}; +static CLARITY_DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + id: CLARITY, + label: LocalizedKey("op.clarity"), + params: vec![ParamDescriptor::amount("amount", "param.clarity.amount")], + attributes: vec![Attribute::Detail], + }) +}); -static TEXTURE_DESCRIPTOR: OpDescriptor = OpDescriptor { - id: TEXTURE, - label: LocalizedKey("op.texture"), - params: &[ParamDescriptor::amount("amount", "param.texture.amount")], - attributes: &[Attribute::Detail], -}; +static TEXTURE_DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + id: TEXTURE, + label: LocalizedKey("op.texture"), + params: vec![ParamDescriptor::amount("amount", "param.texture.amount")], + attributes: vec![Attribute::Detail], + }) +}); /// Luminance as a position on a logarithmic scale, floored. /// @@ -394,8 +404,8 @@ impl LocalContrast { } impl Operation for LocalContrast { - fn descriptor(&self) -> &'static OpDescriptor { - B::RECIPE.descriptor + fn descriptor(&self) -> Arc { + Arc::clone(B::RECIPE.descriptor) } fn set_param(&mut self, _id: ParamId, value: f32) { diff --git a/core/dr-pipeline/src/ops/mod.rs b/core/dr-pipeline/src/ops/mod.rs index ef9283c..15b169a 100644 --- a/core/dr-pipeline/src/ops/mod.rs +++ b/core/dr-pipeline/src/ops/mod.rs @@ -179,7 +179,7 @@ mod tests { // perfectly and silently breaks the sidecar. for mut op in chain() { let descriptor = op.descriptor(); - for p in descriptor.params { + for p in &descriptor.params { let crate::descriptor::ParamKind::Scalar { min, max, .. } = p.kind else { continue; }; diff --git a/core/dr-pipeline/src/ops/noise_reduction.rs b/core/dr-pipeline/src/ops/noise_reduction.rs index b57a828..e35afcb 100644 --- a/core/dr-pipeline/src/ops/noise_reduction.rs +++ b/core/dr-pipeline/src/ops/noise_reduction.rs @@ -147,6 +147,7 @@ //! lie. The chroma radius, ten times larger, still resolves — which is also //! true of the fault it treats, since a blotch twenty pixels across survives //! being halved. +use std::sync::{Arc, LazyLock}; use crate::descriptor::{ Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit, @@ -158,38 +159,40 @@ pub const ID: OpId = OpId("noise_reduction"); pub const LUMINANCE: ParamId = ParamId("luminance"); pub const CHROMA: ParamId = ParamId("chroma"); -static DESCRIPTOR: OpDescriptor = OpDescriptor { - id: ID, - label: LocalizedKey("op.noise_reduction"), - attributes: &[Attribute::Detail], - // Zero to a hundred rather than the symmetric `amount` shape the tonal - // controls use. There is no meaningful negative: "minus fifty noise - // reduction" would be adding grain, which is a look rather than a repair - // and belongs to a different operation carrying `Attribute::Effect`. A - // control whose left half does nothing is worse than one that stops. - params: &[ - ParamDescriptor::scalar( - "luminance", - "param.noise_reduction.luminance", - 0.0, - 100.0, - 0.0, - Unit::None, - Scale::Linear, - 0, - ), - ParamDescriptor::scalar( - "chroma", - "param.noise_reduction.chroma", - 0.0, - 100.0, - 0.0, - Unit::None, - Scale::Linear, - 0, - ), - ], -}; +static DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + id: ID, + label: LocalizedKey("op.noise_reduction"), + attributes: vec![Attribute::Detail], + // Zero to a hundred rather than the symmetric `amount` shape the tonal + // controls use. There is no meaningful negative: "minus fifty noise + // reduction" would be adding grain, which is a look rather than a repair + // and belongs to a different operation carrying `Attribute::Effect`. A + // control whose left half does nothing is worse than one that stops. + params: vec![ + ParamDescriptor::scalar( + "luminance", + "param.noise_reduction.luminance", + 0.0, + 100.0, + 0.0, + Unit::None, + Scale::Linear, + 0, + ), + ParamDescriptor::scalar( + "chroma", + "param.noise_reduction.chroma", + 0.0, + 100.0, + 0.0, + Unit::None, + Scale::Linear, + 0, + ), + ], + }) +}); /// The luminance radius at the lowest and the highest amount, in **source** /// pixels. @@ -365,8 +368,8 @@ fn inv_spatial(kernel: u32) -> f32 { } impl Operation for NoiseReduction { - fn descriptor(&self) -> &'static OpDescriptor { - &DESCRIPTOR + fn descriptor(&self) -> Arc { + DESCRIPTOR.clone() } fn set_param(&mut self, id: ParamId, value: f32) { diff --git a/core/dr-pipeline/src/ops/vignetting.rs b/core/dr-pipeline/src/ops/vignetting.rs index 3bf7724..7c6ffd7 100644 --- a/core/dr-pipeline/src/ops/vignetting.rs +++ b/core/dr-pipeline/src/ops/vignetting.rs @@ -32,6 +32,7 @@ //! division is the whole reason this operation must run before the tonal //! stages: a corner recovered by two stops has to be recovered while the //! highlight headroom to hold it still exists (ARCH §5.2). +use std::sync::{Arc, LazyLock}; use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId}; use crate::operation::{Helper, Operation, Uniform}; @@ -45,19 +46,21 @@ pub const AMOUNT: ParamId = ParamId("amount"); /// fast prime wide open — the case that actually needs correcting. const MAX_K1: f32 = -0.5; -static DESCRIPTOR: OpDescriptor = OpDescriptor { - // Optics rather than effect: this carries lens-profile coefficients - // and corrects what the lens did. A *creative* vignette is a different - // operation that does not exist yet, and would be `Effect`. - attributes: &[Attribute::Optics], - id: ID, - label: LocalizedKey("op.vignetting"), - // Bidirectional deliberately. Negative values *add* falloff, which is a - // legitimate creative choice as well as a correction, and a control that - // only removed vignetting would need a second one beside it to put any - // back. - params: &[ParamDescriptor::amount("amount", "param.vignetting.amount")], -}; +static DESCRIPTOR: LazyLock> = LazyLock::new(|| { + Arc::new(OpDescriptor { + // Optics rather than effect: this carries lens-profile coefficients + // and corrects what the lens did. A *creative* vignette is a different + // operation that does not exist yet, and would be `Effect`. + attributes: vec![Attribute::Optics], + id: ID, + label: LocalizedKey("op.vignetting"), + // Bidirectional deliberately. Negative values *add* falloff, which is a + // legitimate creative choice as well as a correction, and a control that + // only removed vignetting would need a second one beside it to put any + // back. + params: vec![ParamDescriptor::amount("amount", "param.vignetting.amount")], + }) +}); /// The `pa` polynomial coefficients, as Lensfun stores them. #[derive(Debug, Clone, Copy, PartialEq)] @@ -107,8 +110,8 @@ impl Vignetting { } impl Operation for Vignetting { - fn descriptor(&self) -> &'static OpDescriptor { - &DESCRIPTOR + fn descriptor(&self) -> Arc { + DESCRIPTOR.clone() } fn set_param(&mut self, id: ParamId, value: f32) { @@ -371,7 +374,7 @@ mod tests { #[test] fn every_default_is_neutral() { let mut v = Vignetting::new(); - for p in DESCRIPTOR.params { + for p in &DESCRIPTOR.params { v.set_param(p.id, p.default); } assert!(!v.is_active()); diff --git a/core/dr-pipeline/src/sidecar.rs b/core/dr-pipeline/src/sidecar.rs index c36ad2e..93e2372 100644 --- a/core/dr-pipeline/src/sidecar.rs +++ b/core/dr-pipeline/src/sidecar.rs @@ -1297,8 +1297,17 @@ impl PartialMask { .ops .iter() .find(|o| o.descriptor().id.0 == op) - .and_then(|o| o.descriptor().params.iter().find(|p| p.id.0 == param)) - .map(|p| p.id) + // The descriptor is bound inside the closure rather than + // chained through: it is an owned `Arc` now, so a + // `ParamDescriptor` borrowed out of it would not outlive the + // expression. The `ParamId` is `Copy`, so it does. + .and_then(|o| { + o.descriptor() + .params + .iter() + .find(|p| p.id.0 == param) + .map(|p| p.id) + }) else { log::warn!("sidecar: unknown mask parameter {op}.{param}; ignoring"); continue; diff --git a/docs/traceability.md b/docs/traceability.md index ddc7ab2..64cf961 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,17 +9,17 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 254 | -| TRACES tags found | 732 | +| Source files scanned | 257 | +| TRACES tags found | 746 | | Requirements defined | 177 | -| Requirements covered | 98 | -| **Coverage** | **55.4%** (98/177) | +| Requirements covered | 100 | +| **Coverage** | **56.5%** (100/177) | ### By type | Type | Covered | Defined | |---|---|---| -| FR | 78 | 122 | +| FR | 80 | 122 | | NFR | 18 | 49 | | R | 2 | 6 | @@ -46,7 +46,7 @@ _None._ | 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:1001`](../core/dr-catalog/src/schema.rs#L1001), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3110`](../ui/dr-ui/src/library.rs#L3110), [`ui/dr-ui/src/library_ui.rs:6271`](../ui/dr-ui/src/library_ui.rs#L6271), [`ui/dr-ui/src/library_ui.rs:6282`](../ui/dr-ui/src/library_ui.rs#L6282), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6295`](../ui/dr-ui/src/library_ui.rs#L6295), [`ui/dr-ui/src/library_ui.rs:6310`](../ui/dr-ui/src/library_ui.rs#L6310), [`ui/dr-ui/src/library_ui.rs:6319`](../ui/dr-ui/src/library_ui.rs#L6319), [`ui/dr-ui/ui/app.slint:485`](../ui/dr-ui/ui/app.slint#L485), [`ui/dr-ui/ui/app.slint:655`](../ui/dr-ui/ui/app.slint#L655), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1222`](../ui/dr-ui/ui/library.slint#L1222), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902) | | 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:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:3356`](../ui/dr-ui/src/library.rs#L3356), [`ui/dr-ui/src/library_ui.rs:315`](../ui/dr-ui/src/library_ui.rs#L315), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:5140`](../ui/dr-ui/src/library_ui.rs#L5140), [`ui/dr-ui/src/library_ui.rs:5193`](../ui/dr-ui/src/library_ui.rs#L5193), [`ui/dr-ui/src/library_ui.rs:6411`](../ui/dr-ui/src/library_ui.rs#L6411), [`ui/dr-ui/ui/app.slint:488`](../ui/dr-ui/ui/app.slint#L488), [`ui/dr-ui/ui/app.slint:655`](../ui/dr-ui/ui/app.slint#L655), [`ui/dr-ui/ui/app.slint:894`](../ui/dr-ui/ui/app.slint#L894), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1288`](../ui/dr-ui/ui/library.slint#L1288), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902), [`ui/dr-ui/ui/settings.slint:128`](../ui/dr-ui/ui/settings.slint#L128) | | 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:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3482`](../ui/dr-ui/src/library_ui.rs#L3482), [`ui/dr-ui/src/library_ui.rs:4180`](../ui/dr-ui/src/library_ui.rs#L4180), [`ui/dr-ui/ui/app.slint:650`](../ui/dr-ui/ui/app.slint#L650), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2528`](../ui/dr-ui/ui/library.slint#L2528), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) | -| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/ops/curve.rs:136`](../core/dr-pipeline/src/ops/curve.rs#L136), [`core/dr-pipeline/src/ops/curve.rs:652`](../core/dr-pipeline/src/ops/curve.rs#L652), [`core/dr-pipeline/src/sidecar.rs:1627`](../core/dr-pipeline/src/sidecar.rs#L1627), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:3316`](../ui/dr-ui/src/develop.rs#L3316), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1365`](../ui/dr-ui/src/lib.rs#L1365), [`ui/dr-ui/src/lib.rs:1738`](../ui/dr-ui/src/lib.rs#L1738), [`ui/dr-ui/src/lib.rs:1874`](../ui/dr-ui/src/lib.rs#L1874), [`ui/dr-ui/src/lib.rs:480`](../ui/dr-ui/src/lib.rs#L480), [`ui/dr-ui/src/lib.rs:905`](../ui/dr-ui/src/lib.rs#L905), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4882`](../ui/dr-ui/src/library_ui.rs#L4882), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:3316`](../ui/dr-ui/src/develop.rs#L3316), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1365`](../ui/dr-ui/src/lib.rs#L1365), [`ui/dr-ui/src/lib.rs:1738`](../ui/dr-ui/src/lib.rs#L1738), [`ui/dr-ui/src/lib.rs:1874`](../ui/dr-ui/src/lib.rs#L1874), [`ui/dr-ui/src/lib.rs:480`](../ui/dr-ui/src/lib.rs#L480), [`ui/dr-ui/src/lib.rs:905`](../ui/dr-ui/src/lib.rs#L905), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4882`](../ui/dr-ui/src/library_ui.rs#L4882), [`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:555`](../core/dr-catalog/src/schema.rs#L555), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2812`](../ui/dr-ui/src/develop.rs#L2812), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1511`](../ui/dr-ui/src/library.rs#L1511), [`ui/dr-ui/src/library.rs:1588`](../ui/dr-ui/src/library.rs#L1588), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3463`](../ui/dr-ui/src/library.rs#L3463), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library_ui.rs:1564`](../ui/dr-ui/src/library_ui.rs#L1564), [`ui/dr-ui/src/library_ui.rs:1590`](../ui/dr-ui/src/library_ui.rs#L1590), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1700`](../ui/dr-ui/src/library_ui.rs#L1700), [`ui/dr-ui/src/library_ui.rs:226`](../ui/dr-ui/src/library_ui.rs#L226), [`ui/dr-ui/src/library_ui.rs:2316`](../ui/dr-ui/src/library_ui.rs#L2316), [`ui/dr-ui/src/library_ui.rs:259`](../ui/dr-ui/src/library_ui.rs#L259), [`ui/dr-ui/src/library_ui.rs:2754`](../ui/dr-ui/src/library_ui.rs#L2754), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:3257`](../ui/dr-ui/src/library_ui.rs#L3257), [`ui/dr-ui/src/library_ui.rs:3345`](../ui/dr-ui/src/library_ui.rs#L3345), [`ui/dr-ui/src/library_ui.rs:3533`](../ui/dr-ui/src/library_ui.rs#L3533), [`ui/dr-ui/src/library_ui.rs:3651`](../ui/dr-ui/src/library_ui.rs#L3651), [`ui/dr-ui/src/library_ui.rs:440`](../ui/dr-ui/src/library_ui.rs#L440), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:5238`](../ui/dr-ui/src/library_ui.rs#L5238), [`ui/dr-ui/src/library_ui.rs:5353`](../ui/dr-ui/src/library_ui.rs#L5353), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) | | FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/develop.rs:119`](../ui/dr-ui/src/develop.rs#L119), [`ui/dr-ui/src/develop.rs:128`](../ui/dr-ui/src/develop.rs#L128), [`ui/dr-ui/src/develop.rs:1776`](../ui/dr-ui/src/develop.rs#L1776), [`ui/dr-ui/src/develop.rs:194`](../ui/dr-ui/src/develop.rs#L194), [`ui/dr-ui/src/develop.rs:589`](../ui/dr-ui/src/develop.rs#L589), [`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:1846`](../ui/dr-ui/src/lib.rs#L1846), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | @@ -56,25 +56,25 @@ _None._ | FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364) | | 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:343`](../core/dr-catalog/src/schema.rs#L343), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/ui/settings.slint:385`](../ui/dr-ui/ui/settings.slint#L385), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) | | FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) | -| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) | -| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:362`](../core/dr-pipeline/src/framing.rs#L362), [`core/dr-pipeline/src/framing.rs:617`](../core/dr-pipeline/src/framing.rs#L617), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:573`](../core/dr-pipeline/src/graph.rs#L573), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:479`](../core/dr-pipeline/src/operation.rs#L479), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1627`](../core/dr-pipeline/src/sidecar.rs#L1627), [`core/dr-pipeline/src/sidecar.rs:1687`](../core/dr-pipeline/src/sidecar.rs#L1687), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1280`](../ui/dr-ui/src/develop.rs#L1280), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1758`](../ui/dr-ui/src/develop.rs#L1758), [`ui/dr-ui/src/develop.rs:1776`](../ui/dr-ui/src/develop.rs#L1776), [`ui/dr-ui/src/develop.rs:1790`](../ui/dr-ui/src/develop.rs#L1790), [`ui/dr-ui/src/develop.rs:1812`](../ui/dr-ui/src/develop.rs#L1812), [`ui/dr-ui/src/develop.rs:1958`](../ui/dr-ui/src/develop.rs#L1958), [`ui/dr-ui/src/develop.rs:2056`](../ui/dr-ui/src/develop.rs#L2056), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3851`](../ui/dr-ui/src/develop.rs#L3851), [`ui/dr-ui/src/develop.rs:3905`](../ui/dr-ui/src/develop.rs#L3905), [`ui/dr-ui/src/develop.rs:3949`](../ui/dr-ui/src/develop.rs#L3949), [`ui/dr-ui/src/develop.rs:3999`](../ui/dr-ui/src/develop.rs#L3999), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1422`](../ui/dr-ui/src/lib.rs#L1422), [`ui/dr-ui/src/lib.rs:2124`](../ui/dr-ui/src/lib.rs#L2124), [`ui/dr-ui/src/lib.rs:301`](../ui/dr-ui/src/lib.rs#L301), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:2100`](../ui/dr-ui/ui/app.slint#L2100), [`ui/dr-ui/ui/app.slint:981`](../ui/dr-ui/ui/app.slint#L981) | -| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1177`](../ui/dr-ui/src/develop.rs#L1177), [`ui/dr-ui/src/lib.rs:595`](../ui/dr-ui/src/lib.rs#L595) | -| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) | -| FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:4578`](../ui/dr-ui/src/develop.rs#L4578) | -| FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`core/dr-gpu/tests/capture_sharpen.rs:434`](../core/dr-gpu/tests/capture_sharpen.rs#L434), [`core/dr-gpu/tests/detail_stage.rs:242`](../core/dr-gpu/tests/detail_stage.rs#L242), [`core/dr-gpu/tests/local_contrast.rs:476`](../core/dr-gpu/tests/local_contrast.rs#L476), [`core/dr-gpu/tests/noise_reduction.rs:556`](../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/graph.rs:612`](../core/dr-pipeline/src/graph.rs#L612), [`core/dr-pipeline/src/operation.rs:31`](../core/dr-pipeline/src/operation.rs#L31), [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352), [`core/dr-pipeline/src/operation.rs:52`](../core/dr-pipeline/src/operation.rs#L52), [`core/dr-pipeline/src/operation.rs:70`](../core/dr-pipeline/src/operation.rs#L70) | -| 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:1424`](../core/dr-pipeline/src/operation.rs#L1424), [`core/dr-pipeline/src/operation.rs:1505`](../core/dr-pipeline/src/operation.rs#L1505), [`core/dr-pipeline/src/operation.rs:1530`](../core/dr-pipeline/src/operation.rs#L1530), [`core/dr-pipeline/src/operation.rs:1545`](../core/dr-pipeline/src/operation.rs#L1545), [`core/dr-pipeline/src/operation.rs:1569`](../core/dr-pipeline/src/operation.rs#L1569), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/operation.rs:403`](../core/dr-pipeline/src/operation.rs#L403), [`core/dr-pipeline/src/operation.rs:413`](../core/dr-pipeline/src/operation.rs#L413), [`core/dr-pipeline/src/operation.rs:563`](../core/dr-pipeline/src/operation.rs#L563) | -| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/boolean_grain.rs:1`](../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../core/dr-film/src/lib.rs#L160), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:101`](../core/dr-pipeline/src/graph.rs#L101), [`core/dr-pipeline/src/graph.rs:124`](../core/dr-pipeline/src/graph.rs#L124), [`core/dr-pipeline/src/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`core/dr-pipeline/src/operation.rs:1028`](../core/dr-pipeline/src/operation.rs#L1028), [`core/dr-pipeline/src/operation.rs:1057`](../core/dr-pipeline/src/operation.rs#L1057), [`core/dr-pipeline/src/operation.rs:1424`](../core/dr-pipeline/src/operation.rs#L1424), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/ops/film_sim.rs:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`core/dr-pipeline/src/ops/film_sim.rs:150`](../core/dr-pipeline/src/ops/film_sim.rs#L150), [`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:328`](../core/dr-pipeline/src/ops/film_sim.rs#L328), [`core/dr-pipeline/src/ops/film_sim.rs:42`](../core/dr-pipeline/src/ops/film_sim.rs#L42), [`core/dr-pipeline/src/ops/film_sim.rs:85`](../core/dr-pipeline/src/ops/film_sim.rs#L85), [`core/dr-pipeline/src/ops/film_sim.rs:90`](../core/dr-pipeline/src/ops/film_sim.rs#L90), [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:167`](../core/dr-pipeline/src/sidecar.rs#L167), [`core/dr-pipeline/src/sidecar.rs:1948`](../core/dr-pipeline/src/sidecar.rs#L1948), [`core/dr-pipeline/src/sidecar.rs:2024`](../core/dr-pipeline/src/sidecar.rs#L2024), [`core/dr-pipeline/src/sidecar.rs:533`](../core/dr-pipeline/src/sidecar.rs#L533), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:792`](../core/dr-pipeline/src/sidecar.rs#L792), [`core/dr-pipeline/src/state.rs:100`](../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:2844`](../ui/dr-ui/src/develop.rs#L2844), [`ui/dr-ui/src/develop.rs:2856`](../ui/dr-ui/src/develop.rs#L2856), [`ui/dr-ui/src/develop.rs:2894`](../ui/dr-ui/src/develop.rs#L2894), [`ui/dr-ui/src/develop.rs:2903`](../ui/dr-ui/src/develop.rs#L2903), [`ui/dr-ui/src/develop.rs:3006`](../ui/dr-ui/src/develop.rs#L3006), [`ui/dr-ui/src/develop.rs:3324`](../ui/dr-ui/src/develop.rs#L3324), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/lib.rs:2098`](../ui/dr-ui/src/lib.rs#L2098), [`ui/dr-ui/src/lib.rs:528`](../ui/dr-ui/src/lib.rs#L528), [`ui/dr-ui/src/lib.rs:586`](../ui/dr-ui/src/lib.rs#L586), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:889`](../ui/dr-ui/ui/adjust.slint#L889), [`ui/dr-ui/ui/adjust.slint:959`](../ui/dr-ui/ui/adjust.slint#L959), [`ui/dr-ui/ui/app.slint:2712`](../ui/dr-ui/ui/app.slint#L2712), [`ui/dr-ui/ui/app.slint:775`](../ui/dr-ui/ui/app.slint#L775) | -| 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:202`](../core/dr-pipeline/src/framing.rs#L202), [`core/dr-pipeline/src/framing.rs:362`](../core/dr-pipeline/src/framing.rs#L362), [`core/dr-pipeline/src/framing.rs:924`](../core/dr-pipeline/src/framing.rs#L924), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/lib.rs:456`](../core/dr-types/src/lib.rs#L456), [`core/dr-types/src/lib.rs:472`](../core/dr-types/src/lib.rs#L472), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) | +| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389) | +| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:620`](../core/dr-pipeline/src/framing.rs#L620), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:1696`](../core/dr-pipeline/src/sidecar.rs#L1696), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1280`](../ui/dr-ui/src/develop.rs#L1280), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1758`](../ui/dr-ui/src/develop.rs#L1758), [`ui/dr-ui/src/develop.rs:1776`](../ui/dr-ui/src/develop.rs#L1776), [`ui/dr-ui/src/develop.rs:1790`](../ui/dr-ui/src/develop.rs#L1790), [`ui/dr-ui/src/develop.rs:1812`](../ui/dr-ui/src/develop.rs#L1812), [`ui/dr-ui/src/develop.rs:1958`](../ui/dr-ui/src/develop.rs#L1958), [`ui/dr-ui/src/develop.rs:2056`](../ui/dr-ui/src/develop.rs#L2056), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3851`](../ui/dr-ui/src/develop.rs#L3851), [`ui/dr-ui/src/develop.rs:3905`](../ui/dr-ui/src/develop.rs#L3905), [`ui/dr-ui/src/develop.rs:3949`](../ui/dr-ui/src/develop.rs#L3949), [`ui/dr-ui/src/develop.rs:3999`](../ui/dr-ui/src/develop.rs#L3999), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1422`](../ui/dr-ui/src/lib.rs#L1422), [`ui/dr-ui/src/lib.rs:2124`](../ui/dr-ui/src/lib.rs#L2124), [`ui/dr-ui/src/lib.rs:301`](../ui/dr-ui/src/lib.rs#L301), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:2100`](../ui/dr-ui/ui/app.slint#L2100), [`ui/dr-ui/ui/app.slint:981`](../ui/dr-ui/ui/app.slint#L981) | +| FR-DEV-3a | [`core/dr-pipeline/build.rs:1814`](../core/dr-pipeline/build.rs#L1814), [`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:262`](../core/dr-pipeline/src/framing.rs#L262), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365), [`core/dr-pipeline/src/ops/curve.rs:319`](../core/dr-pipeline/src/ops/curve.rs#L319), [`ui/dr-ui/src/develop.rs:1177`](../ui/dr-ui/src/develop.rs#L1177), [`ui/dr-ui/src/lib.rs:595`](../ui/dr-ui/src/lib.rs#L595) | +| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365) | +| FR-DEV-3c | [`core/dr-pipeline/build.rs:1814`](../core/dr-pipeline/build.rs#L1814), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`ui/dr-ui/src/develop.rs:4578`](../ui/dr-ui/src/develop.rs#L4578) | +| FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`core/dr-gpu/tests/capture_sharpen.rs:434`](../core/dr-gpu/tests/capture_sharpen.rs#L434), [`core/dr-gpu/tests/detail_stage.rs:242`](../core/dr-gpu/tests/detail_stage.rs#L242), [`core/dr-gpu/tests/local_contrast.rs:476`](../core/dr-gpu/tests/local_contrast.rs#L476), [`core/dr-gpu/tests/noise_reduction.rs:556`](../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/graph.rs:616`](../core/dr-pipeline/src/graph.rs#L616), [`core/dr-pipeline/src/operation.rs:32`](../core/dr-pipeline/src/operation.rs#L32), [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389), [`core/dr-pipeline/src/operation.rs:53`](../core/dr-pipeline/src/operation.rs#L53), [`core/dr-pipeline/src/operation.rs:71`](../core/dr-pipeline/src/operation.rs#L71) | +| FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`core/dr-decode/src/profile.rs:102`](../core/dr-decode/src/profile.rs#L102), [`core/dr-decode/src/profile.rs:151`](../core/dr-decode/src/profile.rs#L151), [`core/dr-decode/src/profile.rs:1`](../core/dr-decode/src/profile.rs#L1), [`core/dr-decode/src/profile.rs:235`](../core/dr-decode/src/profile.rs#L235), [`core/dr-decode/src/profile.rs:286`](../core/dr-decode/src/profile.rs#L286), [`core/dr-decode/src/profile.rs:343`](../core/dr-decode/src/profile.rs#L343), [`core/dr-decode/src/profile.rs:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:967`](../core/dr-gpu/src/adjust.rs#L967), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`core/dr-gpu/src/demosaic.rs:86`](../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:1576`](../core/dr-pipeline/src/operation.rs#L1576), [`core/dr-pipeline/src/operation.rs:1601`](../core/dr-pipeline/src/operation.rs#L1601), [`core/dr-pipeline/src/operation.rs:1616`](../core/dr-pipeline/src/operation.rs#L1616), [`core/dr-pipeline/src/operation.rs:1640`](../core/dr-pipeline/src/operation.rs#L1640), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/operation.rs:440`](../core/dr-pipeline/src/operation.rs#L440), [`core/dr-pipeline/src/operation.rs:450`](../core/dr-pipeline/src/operation.rs#L450), [`core/dr-pipeline/src/operation.rs:600`](../core/dr-pipeline/src/operation.rs#L600) | +| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/boolean_grain.rs:1`](../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../core/dr-film/src/lib.rs#L160), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:101`](../core/dr-pipeline/src/graph.rs#L101), [`core/dr-pipeline/src/graph.rs:124`](../core/dr-pipeline/src/graph.rs#L124), [`core/dr-pipeline/src/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`core/dr-pipeline/src/operation.rs:1065`](../core/dr-pipeline/src/operation.rs#L1065), [`core/dr-pipeline/src/operation.rs:1094`](../core/dr-pipeline/src/operation.rs#L1094), [`core/dr-pipeline/src/operation.rs:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:296`](../core/dr-pipeline/src/operation.rs#L296), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/ops/film_sim.rs:129`](../core/dr-pipeline/src/ops/film_sim.rs#L129), [`core/dr-pipeline/src/ops/film_sim.rs:153`](../core/dr-pipeline/src/ops/film_sim.rs#L153), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:331`](../core/dr-pipeline/src/ops/film_sim.rs#L331), [`core/dr-pipeline/src/ops/film_sim.rs:43`](../core/dr-pipeline/src/ops/film_sim.rs#L43), [`core/dr-pipeline/src/ops/film_sim.rs:87`](../core/dr-pipeline/src/ops/film_sim.rs#L87), [`core/dr-pipeline/src/ops/film_sim.rs:92`](../core/dr-pipeline/src/ops/film_sim.rs#L92), [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:167`](../core/dr-pipeline/src/sidecar.rs#L167), [`core/dr-pipeline/src/sidecar.rs:1957`](../core/dr-pipeline/src/sidecar.rs#L1957), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:533`](../core/dr-pipeline/src/sidecar.rs#L533), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:792`](../core/dr-pipeline/src/sidecar.rs#L792), [`core/dr-pipeline/src/state.rs:100`](../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:2844`](../ui/dr-ui/src/develop.rs#L2844), [`ui/dr-ui/src/develop.rs:2856`](../ui/dr-ui/src/develop.rs#L2856), [`ui/dr-ui/src/develop.rs:2894`](../ui/dr-ui/src/develop.rs#L2894), [`ui/dr-ui/src/develop.rs:2903`](../ui/dr-ui/src/develop.rs#L2903), [`ui/dr-ui/src/develop.rs:3006`](../ui/dr-ui/src/develop.rs#L3006), [`ui/dr-ui/src/develop.rs:3324`](../ui/dr-ui/src/develop.rs#L3324), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/lib.rs:2098`](../ui/dr-ui/src/lib.rs#L2098), [`ui/dr-ui/src/lib.rs:528`](../ui/dr-ui/src/lib.rs#L528), [`ui/dr-ui/src/lib.rs:586`](../ui/dr-ui/src/lib.rs#L586), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:889`](../ui/dr-ui/ui/adjust.slint#L889), [`ui/dr-ui/ui/adjust.slint:959`](../ui/dr-ui/ui/adjust.slint#L959), [`ui/dr-ui/ui/app.slint:2712`](../ui/dr-ui/ui/app.slint#L2712), [`ui/dr-ui/ui/app.slint:775`](../ui/dr-ui/ui/app.slint#L775) | +| 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:205`](../core/dr-pipeline/src/framing.rs#L205), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:927`](../core/dr-pipeline/src/framing.rs#L927), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/lib.rs:456`](../core/dr-types/src/lib.rs#L456), [`core/dr-types/src/lib.rs:472`](../core/dr-types/src/lib.rs#L472), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) | | FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:2856`](../ui/dr-ui/src/develop.rs#L2856), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/develop.rs:3369`](../ui/dr-ui/src/develop.rs#L3369), [`ui/dr-ui/src/develop.rs:3382`](../ui/dr-ui/src/develop.rs#L3382), [`ui/dr-ui/src/develop.rs:3394`](../ui/dr-ui/src/develop.rs#L3394), [`ui/dr-ui/src/develop.rs:3410`](../ui/dr-ui/src/develop.rs#L3410), [`ui/dr-ui/src/develop.rs:3442`](../ui/dr-ui/src/develop.rs#L3442), [`ui/dr-ui/src/develop.rs:3446`](../ui/dr-ui/src/develop.rs#L3446), [`ui/dr-ui/src/develop.rs:3465`](../ui/dr-ui/src/develop.rs#L3465), [`ui/dr-ui/src/develop.rs:3481`](../ui/dr-ui/src/develop.rs#L3481), [`ui/dr-ui/src/develop.rs:610`](../ui/dr-ui/src/develop.rs#L610), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1386`](../ui/dr-ui/src/lib.rs#L1386), [`ui/dr-ui/src/lib.rs:1400`](../ui/dr-ui/src/lib.rs#L1400), [`ui/dr-ui/src/lib.rs:1408`](../ui/dr-ui/src/lib.rs#L1408), [`ui/dr-ui/src/lib.rs:2294`](../ui/dr-ui/src/lib.rs#L2294), [`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-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3281`](../ui/dr-ui/src/develop.rs#L3281), [`ui/dr-ui/src/develop.rs:3302`](../ui/dr-ui/src/develop.rs#L3302), [`ui/dr-ui/src/lib.rs:1365`](../ui/dr-ui/src/lib.rs#L1365), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:547`](../ui/dr-ui/src/settings_ui.rs#L547), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1315`](../ui/dr-ui/ui/library.slint#L1315), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:913`](../ui/dr-ui/ui/library.slint#L913), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100) | | 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:3410`](../ui/dr-ui/src/develop.rs#L3410), [`ui/dr-ui/src/develop.rs:3442`](../ui/dr-ui/src/develop.rs#L3442), [`ui/dr-ui/src/lib.rs:1408`](../ui/dr-ui/src/lib.rs#L1408), [`ui/dr-ui/src/lib.rs:2294`](../ui/dr-ui/src/lib.rs#L2294), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | -| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:681`](../core/dr-pipeline/src/graph.rs#L681), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:517`](../core/dr-pipeline/src/operation.rs#L517), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2127`](../ui/dr-ui/src/develop.rs#L2127), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/develop.rs:2230`](../ui/dr-ui/src/develop.rs#L2230), [`ui/dr-ui/src/develop.rs:2313`](../ui/dr-ui/src/develop.rs#L2313), [`ui/dr-ui/src/develop.rs:2327`](../ui/dr-ui/src/develop.rs#L2327), [`ui/dr-ui/src/develop.rs:658`](../ui/dr-ui/src/develop.rs#L658), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:2391`](../ui/dr-ui/src/lib.rs#L2391), [`ui/dr-ui/src/lib.rs:306`](../ui/dr-ui/src/lib.rs#L306), [`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:676`](../ui/dr-ui/ui/adjust.slint#L676), [`ui/dr-ui/ui/app.slint:1835`](../ui/dr-ui/ui/app.slint#L1835), [`ui/dr-ui/ui/app.slint:2352`](../ui/dr-ui/ui/app.slint#L2352), [`ui/dr-ui/ui/app.slint:2618`](../ui/dr-ui/ui/app.slint#L2618), [`ui/dr-ui/ui/app.slint:332`](../ui/dr-ui/ui/app.slint#L332), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) | -| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs: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:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`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:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:543`](../core/dr-pipeline/src/graph.rs#L543), [`core/dr-pipeline/src/graph.rs:573`](../core/dr-pipeline/src/graph.rs#L573), [`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:654`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L654), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:662`](../core/dr-pipeline/src/ops/local_contrast.rs#L662), [`core/dr-pipeline/src/ops/noise_reduction.rs:695`](../core/dr-pipeline/src/ops/noise_reduction.rs#L695), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2627`](../ui/dr-ui/src/develop.rs#L2627), [`ui/dr-ui/src/develop.rs:3640`](../ui/dr-ui/src/develop.rs#L3640), [`ui/dr-ui/src/develop.rs:4123`](../ui/dr-ui/src/develop.rs#L4123), [`ui/dr-ui/src/develop.rs:4157`](../ui/dr-ui/src/develop.rs#L4157), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/lib.rs:736`](../ui/dr-ui/src/lib.rs#L736), [`ui/dr-ui/src/lib.rs:795`](../ui/dr-ui/src/lib.rs#L795) | -| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1) | +| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:685`](../core/dr-pipeline/src/graph.rs#L685), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2127`](../ui/dr-ui/src/develop.rs#L2127), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/develop.rs:2230`](../ui/dr-ui/src/develop.rs#L2230), [`ui/dr-ui/src/develop.rs:2313`](../ui/dr-ui/src/develop.rs#L2313), [`ui/dr-ui/src/develop.rs:2327`](../ui/dr-ui/src/develop.rs#L2327), [`ui/dr-ui/src/develop.rs:658`](../ui/dr-ui/src/develop.rs#L658), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:2391`](../ui/dr-ui/src/lib.rs#L2391), [`ui/dr-ui/src/lib.rs:306`](../ui/dr-ui/src/lib.rs#L306), [`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:676`](../ui/dr-ui/ui/adjust.slint#L676), [`ui/dr-ui/ui/app.slint:1835`](../ui/dr-ui/ui/app.slint#L1835), [`ui/dr-ui/ui/app.slint:2352`](../ui/dr-ui/ui/app.slint#L2352), [`ui/dr-ui/ui/app.slint:2618`](../ui/dr-ui/ui/app.slint#L2618), [`ui/dr-ui/ui/app.slint:332`](../ui/dr-ui/ui/app.slint#L332), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) | +| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs: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:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`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:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:547`](../core/dr-pipeline/src/graph.rs#L547), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:657`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L657), [`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:672`](../core/dr-pipeline/src/ops/local_contrast.rs#L672), [`core/dr-pipeline/src/ops/noise_reduction.rs:698`](../core/dr-pipeline/src/ops/noise_reduction.rs#L698), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2627`](../ui/dr-ui/src/develop.rs#L2627), [`ui/dr-ui/src/develop.rs:3640`](../ui/dr-ui/src/develop.rs#L3640), [`ui/dr-ui/src/develop.rs:4123`](../ui/dr-ui/src/develop.rs#L4123), [`ui/dr-ui/src/develop.rs:4157`](../ui/dr-ui/src/develop.rs#L4157), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/lib.rs:736`](../ui/dr-ui/src/lib.rs#L736), [`ui/dr-ui/src/lib.rs:795`](../ui/dr-ui/src/lib.rs#L795) | +| 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) | | 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:2693`](../ui/dr-ui/src/develop.rs#L2693), [`ui/dr-ui/src/develop.rs:5225`](../ui/dr-ui/src/develop.rs#L5225), [`ui/dr-ui/src/develop.rs:5257`](../ui/dr-ui/src/develop.rs#L5257), [`ui/dr-ui/src/develop.rs:621`](../ui/dr-ui/src/develop.rs#L621), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1485`](../ui/dr-ui/src/lib.rs#L1485), [`ui/dr-ui/src/lib.rs:296`](../ui/dr-ui/src/lib.rs#L296), [`ui/dr-ui/ui/app.slint:292`](../ui/dr-ui/ui/app.slint#L292), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | | FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | -| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2398`](../core/dr-gpu/src/adjust.rs#L2398), [`core/dr-pipeline/src/graph.rs:533`](../core/dr-pipeline/src/graph.rs#L533), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:595`](../core/dr-types/src/settings.rs#L595), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | +| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2398`](../core/dr-gpu/src/adjust.rs#L2398), [`core/dr-pipeline/src/graph.rs:537`](../core/dr-pipeline/src/graph.rs#L537), [`core/dr-pipeline/src/graph.rs:591`](../core/dr-pipeline/src/graph.rs#L591), [`core/dr-pipeline/src/operation.rs:483`](../core/dr-pipeline/src/operation.rs#L483), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:595`](../core/dr-types/src/settings.rs#L595), [`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:1`](../core/dr-export/src/lib.rs#L1), [`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-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-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) | @@ -97,10 +97,12 @@ _None._ | FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1104`](../ui/dr-ui/src/lib.rs#L1104), [`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:123`](../ui/dr-ui/src/import.rs#L123), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import.rs:585`](../ui/dr-ui/src/import.rs#L585), [`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:1104`](../ui/dr-ui/src/lib.rs#L1104) | | FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1738`](../ui/dr-ui/src/lib.rs#L1738), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466) | -| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2024`](../core/dr-pipeline/src/sidecar.rs#L2024), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:880`](../ui/dr-ui/src/library.rs#L880) | +| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:880`](../ui/dr-ui/src/library.rs#L880) | | FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) | | FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/lib.rs:845`](../ui/dr-ui/src/lib.rs#L845), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.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:635`](../core/dr-pipeline/src/descriptor.rs#L635), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232) | +| 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:414`](../core/dr-pipeline/src/declared/decl.rs#L414), [`core/dr-pipeline/src/declared/decl.rs:67`](../core/dr-pipeline/src/declared/decl.rs#L67), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:384`](../core/dr-pipeline/src/declared/mod.rs#L384), [`core/dr-pipeline/src/declared/mod.rs:403`](../core/dr-pipeline/src/declared/mod.rs#L403) | | FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:129`](../core/dr-types/src/lib.rs#L129), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200) | | 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:186`](../ui/dr-ui/src/lib.rs#L186) | @@ -110,7 +112,7 @@ _None._ | FR-UI-3 | [`ui/dr-ui/src/develop.rs:2056`](../ui/dr-ui/src/develop.rs#L2056), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/library_ui.rs:4385`](../ui/dr-ui/src/library_ui.rs#L4385), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2100`](../ui/dr-ui/ui/app.slint#L2100), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682) | | FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4385`](../ui/dr-ui/src/library_ui.rs#L4385), [`ui/dr-ui/src/library_ui.rs:4430`](../ui/dr-ui/src/library_ui.rs#L4430), [`ui/dr-ui/src/library_ui.rs:4533`](../ui/dr-ui/src/library_ui.rs#L4533), [`ui/dr-ui/src/library_ui.rs:4561`](../ui/dr-ui/src/library_ui.rs#L4561), [`ui/dr-ui/src/library_ui.rs:5273`](../ui/dr-ui/src/library_ui.rs#L5273), [`ui/dr-ui/src/library_ui.rs:5285`](../ui/dr-ui/src/library_ui.rs#L5285), [`ui/dr-ui/ui/app.slint:1772`](../ui/dr-ui/ui/app.slint#L1772), [`ui/dr-ui/ui/app.slint:650`](../ui/dr-ui/ui/app.slint#L650), [`ui/dr-ui/ui/app.slint:681`](../ui/dr-ui/ui/app.slint#L681), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) | | 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:2356`](../ui/dr-ui/src/lib.rs#L2356), [`ui/dr-ui/src/lib.rs:2763`](../ui/dr-ui/src/lib.rs#L2763), [`ui/dr-ui/src/lib.rs:2948`](../ui/dr-ui/src/lib.rs#L2948), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:313`](../ui/dr-ui/ui/app.slint#L313), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | -| FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:100`](../core/dr-pipeline/src/descriptor.rs#L100), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259) | +| FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262) | | NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) | | NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1555`](../ui/dr-ui/src/export.rs#L1555), [`ui/dr-ui/src/export.rs:1581`](../ui/dr-ui/src/export.rs#L1581), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:2074`](../ui/dr-ui/src/lib.rs#L2074), [`ui/dr-ui/ui/app.slint:1128`](../ui/dr-ui/ui/app.slint#L1128), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) | | NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`ui/dr-ui/src/export.rs:500`](../ui/dr-ui/src/export.rs#L500) | @@ -134,7 +136,7 @@ _None._ ## Not yet tagged -79 of 177 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built. +77 of 177 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
Show untagged requirements @@ -162,11 +164,9 @@ _None._ - FR-PLG-11 - FR-PLG-12 - FR-PLG-1a -- FR-PLG-2 - FR-PLG-2a - FR-PLG-2b - FR-PLG-2c -- FR-PLG-2d - FR-PLG-3 - FR-PLG-3a - FR-PLG-4 diff --git a/ui/dr-ui/src/develop.rs b/ui/dr-ui/src/develop.rs index 90a46f9..2d4530b 100644 --- a/ui/dr-ui/src/develop.rs +++ b/ui/dr-ui/src/develop.rs @@ -1196,7 +1196,7 @@ fn curve_runs(op: &OpCapability, presentation: &Presentation) -> Option = Vec::new(); - for id in presentation.params { + for id in &presentation.params { // The widget addresses points by offset from the first of its run, so // a run has to be contiguous in the capability list. let at = op.params.iter().position(|p| p.id == *id)?; @@ -4480,15 +4480,15 @@ mod tests { presentation: Some(Presentation { // Prefers a gradient handle; this frontend has none, so it // falls back to the next entry, which the canvas does host. - widgets: &[WidgetKind::GradientHandle, WidgetKind::CropOverlay], + widgets: vec![WidgetKind::GradientHandle, WidgetKind::CropOverlay], demand: WidgetDemand { two_dimensional: true, precise_pointing: false, }, - params: &[ParamId("a"), ParamId("b")], + params: vec![ParamId("a"), ParamId("b")], }), params: vec![param("a"), param("b")], - attributes: &[dr_pipeline::Attribute::Tone], + attributes: vec![dr_pipeline::Attribute::Tone], }; assert!(rows_from(&[on_canvas]).is_empty()); @@ -4612,7 +4612,7 @@ mod tests { id: ParamId("method"), label: LocalizedKey("param.invented.method"), kind: ParamKind::Enum { - variants: &[ + variants: vec![ LocalizedKey("param.invented.method.fast"), LocalizedKey("param.invented.method.exact"), ], @@ -4622,7 +4622,7 @@ mod tests { facet: None, }, ], - attributes: &[dr_pipeline::Attribute::Tone], + attributes: vec![dr_pipeline::Attribute::Tone], }; let rows = rows_from(&[invented]); @@ -4660,12 +4660,12 @@ mod tests { label: LocalizedKey("op.grading"), active: false, presentation: Some(Presentation { - widgets: &[WidgetKind::ColourWheel], + widgets: vec![WidgetKind::ColourWheel], demand: WidgetDemand { two_dimensional: true, precise_pointing: false, }, - params: &[ParamId("hue"), ParamId("strength")], + params: vec![ParamId("hue"), ParamId("strength")], }), params: vec![ ParamCapability { @@ -4697,7 +4697,7 @@ mod tests { facet: None, }, ], - attributes: &[dr_pipeline::Attribute::Tone], + attributes: vec![dr_pipeline::Attribute::Tone], }; assert!(!supported(WidgetKind::ColourWheel), "precondition"); @@ -5146,15 +5146,15 @@ mod tests { label: LocalizedKey("op.invented_curve"), active: false, presentation: Some(Presentation { - widgets: &[WidgetKind::ToneCurve], + widgets: vec![WidgetKind::ToneCurve], demand: WidgetDemand { two_dimensional: true, precise_pointing: true, }, - params: &IDS, + params: IDS.to_vec(), }), params: IDS.iter().map(|id| param(*id)).collect(), - attributes: &[dr_pipeline::Attribute::Tone], + attributes: vec![dr_pipeline::Attribute::Tone], }; let presentation = plain.presentation.as_ref().expect("declares a widget");