Merge branch 'worktree-agent-a1e5c8cb565255f5b' into master

# Conflicts:
#	docs/traceability.md
This commit is contained in:
2026-08-27 19:21:10 +02:00
29 changed files with 3530 additions and 1768 deletions
+6
View File
@@ -12,6 +12,12 @@ license.workspace = true
dr-types.workspace = true dr-types.workspace = true
log.workspace = true log.workspace = true
# A dependency of the library, not only of the build script, since FR-PLG-2:
# `src/declared/` reads the same declaration format at *load* time, so that an
# operation found in a file at startup is the same kind of thing as one found
# at compile time. The reader itself is one file shared by both.
serde_norway.workspace = true
# Nodes are declared in `ops/*.yaml` and compiled to Rust by `build.rs` # Nodes are declared in `ops/*.yaml` and compiled to Rust by `build.rs`
# (ARCH §5.7). The same reasoning as `ui/dr-ui`'s style.yaml: the declaration # (ARCH §5.7). The same reasoning as `ui/dr-ui`'s style.yaml: the declaration
# is the source of truth, the Rust is generated into OUT_DIR where it cannot # is the source of truth, the Rust is generated into OUT_DIR where it cannot
+216 -1295
View File
File diff suppressed because it is too large Load Diff
+15 -1
View File
@@ -6,9 +6,23 @@ directory — there is no list to extend, no shader to edit, and no UI change.
`../build.rs` compiles each declaration into Rust implementing `../build.rs` compiles each declaration into Rust implementing
[`Operation`](../src/operation.rs), generated into `OUT_DIR`. The result is [`Operation`](../src/operation.rs), generated into `OUT_DIR`. The result is
indistinguishable downstream from a hand-written operation: the same indistinguishable downstream from a hand-written operation: the same
`&'static OpDescriptor`, the same fused-shader composition, the same sidecar `Arc<OpDescriptor>`, the same fused-shader composition, the same sidecar
round-trip. round-trip.
**The same declaration also runs without being compiled** (FR-PLG-2).
[`DeclaredOp`](../src/declared/mod.rs) reads this format at *load* time and
implements `Operation` from it directly — one interpreter over many
declarations, where `build.rs` emits generated code per node. Both paths exist
on purpose: the built-ins stay compiled, because a generated `match` is faster
than an interpreted one and because the `tests:` blocks below have to run under
`cargo test`.
The reading half is one file, [`src/declared/decl.rs`](../src/declared/decl.rs),
shared by both — so everything documented here means exactly one thing, and
`tests/declared_parity.rs` asserts the two backends compose byte-identical WGSL
for every node in this directory. Nothing in this document is specific to the
build-time path.
## `attributes:` — what the operation is about ## `attributes:` — what the operation is about
Required, one or more of `tone`, `colour`, `detail`, `optics`, `geometry`, Required, one or more of `tone`, `colour`, `detail`, `optics`, `geometry`,
File diff suppressed because it is too large Load Diff
+447
View File
@@ -0,0 +1,447 @@
//! TRACES: FR-PLG-2
//! The little expression language a node declaration derives its uniforms in.
//!
//! Uniforms are functions of parameters — `exp2(exposure)`, `blacks / 100 *
//! 0.02` — and that derivation is the one piece of a node that is genuinely
//! computation rather than description. The language is arithmetic over the
//! node's own parameters plus a fixed set of maths functions: enough for every
//! operation in the chain, and small enough that a reader of the YAML can see
//! exactly what will happen.
//!
//! # Why this file is compiled twice
//!
//! It is `#[path]`-included by `build.rs` as well as being a module of the
//! crate, and it deliberately depends on nothing but `std` so that it can be.
//!
//! There are two backends over one grammar. `build.rs` renders an [`Expr`] to
//! Rust source, so a built-in node's arithmetic costs nothing at run time and
//! an unknown name is a build error naming the file. [`Expr::eval`] evaluates
//! the same tree directly, which is what lets a declaration loaded at run time
//! produce uniforms without a compiler.
//!
//! **The two must agree bit for bit**, or a plugin is not the same kind of
//! thing as a built-in and `tests/declared_parity.rs` says so. Sharing the
//! tokeniser and the parser removes the larger half of the ways they could
//! drift; the remaining half is the pair of backends, and each entry in
//! [`FUNCTIONS`] below is written twice on purpose, once here and once in
//! `build.rs`, with the parity test standing between them.
use std::collections::BTreeSet;
/// A number in a declaration, as the `f32` the arithmetic will actually use.
///
/// **The route through the decimal string is load-bearing, not clumsiness.**
/// `build.rs` renders a number as a Rust literal — `0.02f32` — and `rustc`
/// rounds that decimal text to the nearest `f32` exactly once. Writing
/// `n as f32` here would instead round the `f64` YAML parsed to the nearest
/// `f32`, which is a *second* rounding on top of the one that produced the
/// `f64`, and double rounding does not always land where single rounding does.
///
/// So this reproduces what the compiler sees: `{:?}` is the shortest decimal
/// that round-trips the `f64`, which is exactly the literal `build.rs` emits,
/// and parsing it as `f32` is exactly what `rustc` does with it.
pub fn as_f32(n: f64) -> f32 {
// Infallible in practice: `{:?}` on a finite `f64` is always a parseable
// decimal, and the infinities and NaN it can also print all parse back.
// The fallback is the direct cast rather than a panic, because a bad
// number in a declaration is the reader's error to report, not this
// function's to crash on.
format!("{n:?}").parse::<f32>().unwrap_or(n as f32)
}
/// The maths functions a declaration may call, and how many arguments each
/// takes.
///
/// A closed list rather than a passthrough to `f32`: a node is a description,
/// and letting it name arbitrary Rust would make the YAML a second, worse
/// place to write code. Closed for the same reason `WidgetKind` and
/// `attributes` are closed (FR-PLG-2d) — a typo must be an error rather than a
/// silent new category of one.
///
/// Shared by both backends so that the *set* of callable functions cannot
/// drift even though the two translations of each must be written separately.
pub const FUNCTIONS: &[(&str, usize)] = &[
("exp2", 1),
("log2", 1),
("exp", 1),
("sqrt", 1),
("abs", 1),
("floor", 1),
("ceil", 1),
("round", 1),
("pow", 2),
("min", 2),
("max", 2),
("clamp", 3),
("mix", 3),
];
/// A parsed expression over a node's parameters.
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Num(f64),
Param(String),
Neg(Box<Expr>),
Bin(char, Box<Expr>, Box<Expr>),
Call(String, Vec<Expr>),
}
/// Parse and validate an expression against the parameters a node declares.
///
/// Validation happens here rather than in either backend, so that "this names
/// a parameter that does not exist" is one error message in one place and
/// cannot be reported at build time but missed at load time.
pub fn parse(src: &str, params: &BTreeSet<&str>) -> Result<Expr, String> {
let tokens = tokenise(src)?;
let mut parser = Parser { tokens, at: 0 };
let expr = parser.expr()?;
if parser.at < parser.tokens.len() {
return Err(format!(
"unexpected `{}` after the end of the expression",
parser.tokens[parser.at]
));
}
check(&expr, params)?;
Ok(expr)
}
/// Every name and arity in the tree resolves.
fn check(expr: &Expr, params: &BTreeSet<&str>) -> Result<(), String> {
match expr {
Expr::Num(_) => Ok(()),
Expr::Param(name) => {
if params.contains(name.as_str()) {
return Ok(());
}
let known: Vec<&str> = params.iter().copied().collect();
Err(format!(
"`{name}` is not a parameter of this node. Its parameters \
are: {}",
known.join(", ")
))
}
Expr::Neg(inner) => check(inner, params),
Expr::Bin(_, l, r) => {
check(l, params)?;
check(r, params)
}
Expr::Call(name, args) => {
let Some((_, arity)) = FUNCTIONS.iter().find(|(f, _)| *f == name) else {
let known: Vec<&str> = FUNCTIONS.iter().map(|(f, _)| *f).collect();
return Err(format!(
"`{name}` is not one of the maths functions a node may \
call. Available: {}",
known.join(", ")
));
};
if args.len() != *arity {
return Err(format!(
"`{name}` takes {arity} argument(s), given {}",
args.len()
));
}
for a in args {
check(a, params)?;
}
Ok(())
}
}
}
impl Expr {
/// TRACES: FR-PLG-2
/// Evaluate this expression for a set of parameter values.
///
/// **Every step is an `f32` operation in the same order `build.rs` renders
/// it**, which is what makes the interpreted result bit-identical to the
/// compiled one rather than merely close. Rust's `f32` arithmetic is IEEE
/// 754 with no excess precision, so `(a * b) + c` here and `(a * b) + c`
/// in generated source are the same number down to the last bit — and the
/// parity test asserts exactly that rather than an epsilon, because a
/// tolerance is how a real divergence gets to hide.
///
/// `param` is asked for a parameter's current value. It is a closure
/// rather than a map so the caller can serve the values out of whatever it
/// already has, which for [`super::DeclaredOp`] is a plain `Vec<f32>`
/// indexed in declaration order.
///
/// Infallible: [`parse`] has already established that every name resolves
/// and every call has the right arity. An unknown parameter reaching here
/// would be a reader that let one through, so `param` decides what to do
/// about it rather than this returning a `Result` every caller would
/// unwrap.
pub fn eval(&self, param: &dyn Fn(&str) -> f32) -> f32 {
match self {
Expr::Num(n) => as_f32(*n),
Expr::Param(name) => param(name),
Expr::Neg(inner) => -inner.eval(param),
Expr::Bin(op, l, r) => {
let (l, r) = (l.eval(param), r.eval(param));
match op {
'+' => l + r,
'-' => l - r,
'*' => l * r,
'/' => l / r,
// `tokenise` only ever produces these four as binary
// operators, and `Parser` only ever builds `Bin` from what
// `tokenise` produced.
_ => unreachable!("`{op}` is not a binary operator"),
}
}
Expr::Call(name, args) => {
let a = |i: usize| args[i].eval(param);
match name.as_str() {
"exp2" => f32::exp2(a(0)),
"log2" => f32::log2(a(0)),
"exp" => f32::exp(a(0)),
"sqrt" => f32::sqrt(a(0)),
"abs" => f32::abs(a(0)),
"floor" => f32::floor(a(0)),
"ceil" => f32::ceil(a(0)),
"round" => f32::round(a(0)),
"pow" => f32::powf(a(0), a(1)),
"min" => f32::min(a(0), a(1)),
"max" => f32::max(a(0), a(1)),
"clamp" => f32::clamp(a(0), a(1), a(2)),
// Spelled out rather than called, matching what `build.rs`
// renders: Rust has no `mix`, and this linear form is what
// WGSL's `mix` means. The association matters — `a + (b -
// a) * t` and `a * (1 - t) + b * t` are the same value in
// real arithmetic and different ones in `f32`.
"mix" => {
let (x, y, t) = (a(0), a(1), a(2));
x + (y - x) * t
}
// `parse` rejects anything not in `FUNCTIONS`.
_ => unreachable!("`{name}` is not a declared maths function"),
}
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Tok {
Num(f64),
Ident(String),
Sym(char),
}
impl std::fmt::Display for Tok {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Tok::Num(n) => write!(f, "{n}"),
Tok::Ident(s) => write!(f, "{s}"),
Tok::Sym(c) => write!(f, "{c}"),
}
}
}
fn tokenise(src: &str) -> Result<Vec<Tok>, String> {
let bytes: Vec<char> = src.chars().collect();
let mut out = Vec::new();
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c.is_whitespace() {
i += 1;
} else if c.is_ascii_digit()
|| (c == '.' && bytes.get(i + 1).is_some_and(char::is_ascii_digit))
{
let start = i;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == '.') {
i += 1;
}
let text: String = bytes[start..i].iter().collect();
let n = text
.parse::<f64>()
.map_err(|_| format!("`{text}` is not a number"))?;
out.push(Tok::Num(n));
} else if c.is_ascii_alphabetic() || c == '_' {
let start = i;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == '_') {
i += 1;
}
out.push(Tok::Ident(bytes[start..i].iter().collect()));
} else if "+-*/(),".contains(c) {
out.push(Tok::Sym(c));
i += 1;
} else {
return Err(format!(
"`{c}` is not valid in an expression; the language is \
arithmetic (+ - * /), parentheses, numbers, this node's \
parameters, and the maths functions"
));
}
}
if out.is_empty() {
return Err("the expression is empty".into());
}
Ok(out)
}
struct Parser {
tokens: Vec<Tok>,
at: usize,
}
impl Parser {
fn peek(&self) -> Option<&Tok> {
self.tokens.get(self.at)
}
fn eat(&mut self, sym: char) -> bool {
if self.peek() == Some(&Tok::Sym(sym)) {
self.at += 1;
return true;
}
false
}
fn expr(&mut self) -> Result<Expr, String> {
let mut left = self.term()?;
loop {
if self.eat('+') {
left = Expr::Bin('+', Box::new(left), Box::new(self.term()?));
} else if self.eat('-') {
left = Expr::Bin('-', Box::new(left), Box::new(self.term()?));
} else {
return Ok(left);
}
}
}
fn term(&mut self) -> Result<Expr, String> {
let mut left = self.unary()?;
loop {
if self.eat('*') {
left = Expr::Bin('*', Box::new(left), Box::new(self.unary()?));
} else if self.eat('/') {
left = Expr::Bin('/', Box::new(left), Box::new(self.unary()?));
} else {
return Ok(left);
}
}
}
fn unary(&mut self) -> Result<Expr, String> {
if self.eat('-') {
return Ok(Expr::Neg(Box::new(self.unary()?)));
}
self.primary()
}
fn primary(&mut self) -> Result<Expr, String> {
match self.peek().cloned() {
Some(Tok::Num(n)) => {
self.at += 1;
Ok(Expr::Num(n))
}
Some(Tok::Ident(name)) => {
self.at += 1;
if !self.eat('(') {
return Ok(Expr::Param(name));
}
let mut args = Vec::new();
if !self.eat(')') {
loop {
args.push(self.expr()?);
if self.eat(',') {
continue;
}
if self.eat(')') {
break;
}
return Err(format!("expected `,` or `)` in the call to `{name}`"));
}
}
Ok(Expr::Call(name, args))
}
Some(Tok::Sym('(')) => {
self.at += 1;
let inner = self.expr()?;
if !self.eat(')') {
return Err("unclosed `(`".into());
}
Ok(inner)
}
Some(t) => Err(format!("unexpected `{t}`")),
None => Err("the expression ends early".into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn params() -> BTreeSet<&'static str> {
["a", "b"].into_iter().collect()
}
fn eval(src: &str, a: f32, b: f32) -> f32 {
parse(src, &params())
.expect("parses")
.eval(&|name| match name {
"a" => a,
"b" => b,
other => panic!("no parameter {other}"),
})
}
#[test]
fn arithmetic_follows_the_usual_precedence() {
assert_eq!(eval("a + b * 2", 1.0, 3.0), 7.0);
assert_eq!(eval("(a + b) * 2", 1.0, 3.0), 8.0);
}
#[test]
fn unary_minus_binds_tighter_than_addition() {
// `-(a + b)` and `-a + b` are different numbers, and the parser has to
// agree with the renderer about which one `-a + b` is.
assert_eq!(eval("-a + b", 1.0, 3.0), 2.0);
assert_eq!(eval("-(a + b)", 1.0, 3.0), -4.0);
}
#[test]
fn a_number_is_rounded_once_the_way_the_compiler_rounds_a_literal() {
// The whole reason `as_f32` goes through the decimal string. If this
// ever regresses to `n as f32`, the values a declared node produces
// drift from the generated one in the last bit, and every parity
// assertion has to become a tolerance to keep passing — which is
// exactly the silent disagreement the test exists to catch.
assert_eq!(as_f32(0.02), 0.02f32);
assert_eq!(as_f32(0.1), 0.1f32);
// A third: eight significant digits, which is past what an `f32`
// resolves, so this is the case where a second rounding could land
// somewhere the compiler's single one does not.
assert_eq!(as_f32(1.0 / 3.0), 0.333_333_34_f32);
}
#[test]
fn mix_is_the_linear_form_wgsl_means() {
assert_eq!(eval("mix(a, b, 0.25)", 0.0, 4.0), 1.0);
}
#[test]
fn an_unknown_parameter_names_the_ones_that_exist() {
// The error is the whole value of validating in the parser: whoever
// wrote the typo needs the list, and needs it identically whether the
// declaration was read by the build script or at load time.
let err = parse("c * 2", &params()).unwrap_err();
assert!(err.contains("`c` is not a parameter"), "{err}");
assert!(err.contains("a, b"), "{err}");
}
#[test]
fn an_unknown_function_is_rejected_rather_than_passed_through() {
let err = parse("tan(a)", &params()).unwrap_err();
assert!(err.contains("not one of the maths functions"), "{err}");
}
#[test]
fn a_wrong_arity_is_caught_where_it_is_written() {
let err = parse("pow(a)", &params()).unwrap_err();
assert!(err.contains("takes 2 argument(s), given 1"), "{err}");
}
}
+487
View File
@@ -0,0 +1,487 @@
//! TRACES: FR-PLG-2 | FR-PLG-2d
//! Running a node declaration without compiling it.
//!
//! # The format already existed
//!
//! `ops/*.yaml` plus `build.rs` has been the class-1 plugin format since the
//! declarative nodes landed — it was simply resolved at build time:
//!
//! ```text
//! ops/exposure.yaml ──build.rs──▶ generated impl Operation ──▶ fused shader
//! ```
//!
//! Nothing about that requires the declaration to be present when the compiler
//! runs. Everything a declaration produces is *data plus a WGSL string*, and
//! the composer already assembles WGSL at run time from whichever operations
//! are active. So this module is not a new mechanism; it is the existing one,
//! loaded later.
//!
//! [`DeclaredOp`] is **one interpreter over many declarations**, where
//! `build.rs` emits generated code per node. It implements [`Operation`] from
//! an owned [`Declaration`], which is only possible because descriptors became
//! owned — see [`crate::descriptor::OpDescriptor`] for why a `&'static`
//! descriptor made a run-time node impossible.
//!
//! # Both paths stay
//!
//! The generated path is not removed and should not be. FR-PLG-2 says so, and
//! the reasons are good ones: a generated `match` is faster than an
//! interpreted one, the built-ins' declared tests have to run under `cargo
//! test`, and generated source is *inspectable* in a way an interpreter's
//! internal state is not.
//!
//! What matters is that the two are **indistinguishable downstream**, and that
//! is a test rather than an intention: `tests/declared_parity.rs` parses every
//! built-in `ops/*.yaml` at run time and asserts the composed WGSL is
//! byte-for-byte identical to what the generated implementation produces, for
//! the same parameter values. If the two ever disagree, a plugin is not the
//! same kind of thing as a built-in and the premise of the whole plugin plan
//! has failed quietly.
//!
//! # What this is not, yet
//!
//! Not load-time WGSL validation (FR-PLG-11), not id namespacing (FR-PLG-2's
//! `author.name`), and not a plugin directory read at startup. Those are
//! separate work and are deliberately absent — a declaration reaching
//! [`DeclaredOp`] here is one that ships in this repository, so its WGSL has
//! already been compiled by the build and its id has already been checked for
//! collisions.
pub mod decl;
pub mod expr;
use std::sync::{Arc, LazyLock};
pub use decl::{Declaration, Node, SharedHelpers};
use crate::descriptor::{
intern, Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation,
Scale, Unit, WidgetDemand, WidgetKind,
};
use crate::operation::{Helper, Operation, Uniform};
use expr::{as_f32, Expr};
/// The shared WGSL helper library, as the built-in nodes see it.
///
/// The very same `_helpers.yaml` `build.rs` reads, embedded rather than read
/// from disk: there is no file path an installed application could look it up
/// at, and embedding is what makes it impossible for the compiled helpers and
/// the interpreted ones to be two different files.
///
/// Panics if the bundled file does not parse, which is a build-time fact about
/// this repository rather than anything a user can cause — `build.rs` reads the
/// same text on the same build and fails first.
pub fn builtin_helpers() -> &'static SharedHelpers {
static LIBRARY: LazyLock<SharedHelpers> = LazyLock::new(|| {
decl::read_helpers(include_str!("../../ops/_helpers.yaml"))
.unwrap_or_else(|e| panic!("the bundled {}: {e}", decl::HELPERS_FILE))
});
&LIBRARY
}
/// TRACES: FR-PLG-2
/// An operation built from a declaration at run time.
///
/// Holds everything the trait has to answer with, resolved once when the
/// declaration is read: the descriptor, the uniform expressions, the helper
/// list and the fragment text. Nothing is re-parsed per call, so the per-frame
/// cost of an interpreted node is the arithmetic in [`Expr::eval`] and nothing
/// else — the same arithmetic the generated node does, simply walked rather
/// than inlined.
#[derive(Debug, Clone)]
pub struct DeclaredOp {
descriptor: Arc<OpDescriptor>,
/// Parameter ids in declaration order, parallel to `defaults` and
/// `values`.
///
/// Three parallel `Vec`s rather than one of triples because the hot read
/// is `values` alone, and because `set_param` writes exactly one of them.
/// They are built together and never resized.
params: Vec<ParamId>,
defaults: Vec<f32>,
values: Vec<f32>,
uniforms: Vec<DeclaredUniform>,
/// The declared `active:` rule, or `None` for the default one.
active: Option<Expr>,
wgsl: String,
helpers: Vec<Helper>,
presentation: Option<Presentation>,
order: i64,
}
/// One uniform: the name the fragment reads it by, and how to compute it.
#[derive(Debug, Clone)]
struct DeclaredUniform {
/// Interned when the declaration was read, not on every `uniforms()` call.
/// See [`crate::operation::Uniform::name`].
name: &'static str,
expr: Expr,
}
impl DeclaredOp {
/// Read one `ops/<id>.yaml` and build the operation it declares.
///
/// `ctx` names the source in error messages — a path, usually.
///
/// A `rust:` node is an error here rather than a silent `None`: it names a
/// hand-written type in this crate, which is by definition not something a
/// declaration can produce, and a caller that got one back as "nothing to
/// do" would drop an operation out of the chain without saying so.
pub fn from_yaml(text: &str, ctx: &str, library: &SharedHelpers) -> Result<Self, String> {
let names = library.names();
match decl::read_node(text, ctx, &names)? {
Node::Declared(d) => Self::new(&d, library),
Node::Rust { id, ty, .. } => Err(format!(
"`{id}` declares `rust: {ty}`, which names a hand-written type \
rather than describing an operation. Only a full declaration \
can be read at run time."
)),
}
}
/// Build the operation a parsed declaration describes.
pub fn new(declaration: &Declaration, library: &SharedHelpers) -> Result<Self, String> {
let params: Vec<ParamId> = declaration
.params
.iter()
.map(|p| ParamId::interned(&p.id))
.collect();
let defaults: Vec<f32> = declaration
.params
.iter()
.map(|p| as_f32(p.default))
.collect();
// Helpers in the order the composer will see them: the shared ones the
// node asked for, in the order it asked, then its own definitions.
// Order decides emission order in the generated shader, so it is part
// of the output rather than an implementation detail.
let mut helpers =
Vec::with_capacity(declaration.shared_helpers.len() + declaration.local_helpers.len());
for name in &declaration.shared_helpers {
// `decl::read_node` has already rejected a name the library does
// not define, so this is a library that changed underneath a
// declaration rather than a declaration with a typo in it.
let helper = library.get(name).ok_or_else(|| {
format!(
"`{}` asks for the shared helper `{name}`, which this \
helper library does not define",
declaration.id
)
})?;
helpers.push(Helper {
name: intern(&helper.name),
source: intern(&helper.source()),
});
}
for helper in &declaration.local_helpers {
helpers.push(Helper {
name: intern(&helper.name),
source: intern(&helper.source()),
});
}
Ok(Self {
descriptor: Arc::new(OpDescriptor {
id: OpId::interned(&declaration.id),
label: LocalizedKey::interned(&declaration.label),
params: declaration.params.iter().map(param_descriptor).collect(),
attributes: declaration
.attributes
.iter()
.copied()
.map(attribute)
.collect(),
}),
values: defaults.clone(),
params,
defaults,
uniforms: declaration
.uniforms
.iter()
.map(|u| DeclaredUniform {
name: intern(&u.name),
expr: u.expr.clone(),
})
.collect(),
active: declaration.active.clone(),
wgsl: declaration.wgsl_body(),
helpers,
presentation: declaration.presentation.as_deref().map(presentation),
order: declaration.order,
})
}
/// Where this node sits in the chain, from its `order:`.
///
/// Not part of [`Operation`] — the graph holds operations in a `Vec` and
/// order *is* that position (ARCH §3.4). Exposed so whoever assembles a
/// chain out of declarations can sort them, which is what `build.rs` does
/// at the other end.
pub fn order(&self) -> i64 {
self.order
}
fn index_of(&self, id: ParamId) -> Option<usize> {
self.params.iter().position(|p| *p == id)
}
/// One parameter's current value, by the name an expression calls it.
///
/// Returns 0.0 for a name that is not a parameter, matching what the
/// generated `param()` does with an unknown id. It cannot happen —
/// [`expr::parse`] rejects a name that is not declared — but a silent zero
/// is a better failure here than a panic inside a render.
fn value_named(&self, name: &str) -> f32 {
self.params
.iter()
.position(|p| p.0 == name)
.map_or(0.0, |i| self.values[i])
}
}
impl Operation for DeclaredOp {
fn descriptor(&self) -> Arc<OpDescriptor> {
self.descriptor.clone()
}
fn set_param(&mut self, id: ParamId, value: f32) {
match self.index_of(id) {
Some(i) => self.values[i] = value,
// The same complaint the generated `set_param` makes, for the same
// reason: a parameter that does not exist is a sidecar or a UI
// naming something this build does not have, and dropping it
// silently is how an edit comes to be half-applied.
None => log::warn!("{}: unknown parameter {id}", self.descriptor.id),
}
}
fn param(&self, id: ParamId) -> f32 {
self.index_of(id).map_or(0.0, |i| self.values[i])
}
fn is_active(&self) -> bool {
match &self.active {
Some(expr) => expr.eval(&|name| self.value_named(name)) != 0.0,
// The default rule, and the honest one: the operation is doing
// something exactly when a parameter has moved off its default.
// Short-circuiting in declaration order, which is what the
// generated `a != d || b != d` does.
None => self.values.iter().zip(&self.defaults).any(|(v, d)| v != d),
}
}
fn wgsl_body(&self) -> String {
self.wgsl.clone()
}
fn uniforms(&self) -> Vec<Uniform> {
self.uniforms
.iter()
.map(|u| Uniform {
name: u.name,
value: u.expr.eval(&|name| self.value_named(name)),
})
.collect()
}
fn helpers(&self) -> &[Helper] {
&self.helpers
}
fn presentation(&self) -> Option<Presentation> {
self.presentation.clone()
}
}
/// A declared parameter as the descriptor the panel reads.
///
/// Every arm calls the constructor `build.rs` renders a call to, so the two
/// produce the same `ParamDescriptor` by construction rather than by
/// coincidence.
fn param_descriptor(p: &decl::ParamDef) -> ParamDescriptor {
let id = intern(&p.id);
let label = intern(&p.label);
match &p.kind {
decl::Kind::Stops { min, max } => {
ParamDescriptor::stops(id, label, as_f32(*min), as_f32(*max))
}
decl::Kind::Amount => ParamDescriptor::amount(id, label),
decl::Kind::Switch => ParamDescriptor::switch(id, label),
decl::Kind::Fraction { default } => ParamDescriptor::fraction(id, label, as_f32(*default)),
decl::Kind::Scalar {
min,
max,
default,
unit,
scale,
precision,
} => ParamDescriptor::scalar(
id,
label,
as_f32(*min),
as_f32(*max),
as_f32(*default),
match unit {
decl::Unit::None => Unit::None,
decl::Unit::Stops => Unit::Stops,
decl::Unit::Kelvin => Unit::Kelvin,
decl::Unit::Percent => Unit::Percent,
},
match scale {
decl::Scale::Linear => Scale::Linear,
decl::Scale::Perceptual => Scale::Perceptual,
},
// `decl::read_kind` refuses a precision that does not fit, so this
// cast cannot lose anything.
*precision as u8,
),
decl::Kind::Enum { variants } => ParamDescriptor::choice(
id,
label,
variants.iter().map(|v| LocalizedKey::interned(v)).collect(),
),
}
}
fn attribute(a: decl::Attr) -> Attribute {
match a {
decl::Attr::Tone => Attribute::Tone,
decl::Attr::Colour => Attribute::Colour,
decl::Attr::Detail => Attribute::Detail,
decl::Attr::Optics => Attribute::Optics,
decl::Attr::Geometry => Attribute::Geometry,
decl::Attr::Effect => Attribute::Effect,
}
}
fn widget(w: decl::Widget) -> WidgetKind {
match w {
decl::Widget::ToneCurve => WidgetKind::ToneCurve,
decl::Widget::ColourWheel => WidgetKind::ColourWheel,
decl::Widget::CropOverlay => WidgetKind::CropOverlay,
decl::Widget::GradientHandle => WidgetKind::GradientHandle,
decl::Widget::BrushMask => WidgetKind::BrushMask,
decl::Widget::WhitePoint => WidgetKind::WhitePoint,
}
}
fn presentation(p: &decl::PresentationDef) -> Presentation {
Presentation {
widgets: p.widgets.iter().copied().map(widget).collect(),
demand: WidgetDemand {
two_dimensional: p.two_dimensional,
precise_pointing: p.precise_pointing,
},
params: p.params.iter().map(|n| ParamId::interned(n)).collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
/// TRACES: FR-PLG-2d
/// The two spellings of the attribute vocabulary are one vocabulary.
///
/// `decl::Attr` exists because the reader is compiled by `build.rs`, which
/// cannot see `crate::descriptor`. That is a duplicated closed list, and a
/// duplicated closed list is exactly the thing FR-PLG-2d warns about: an
/// attribute added to one and not the other would put an operation in a
/// category the panel does not know it has.
#[test]
fn the_attribute_vocabulary_is_the_same_on_both_sides() {
assert_eq!(decl::Attr::ALL.len(), Attribute::ALL.len());
for (a, b) in decl::Attr::ALL.iter().zip(Attribute::ALL) {
// Same order, so an index into one indexes the other.
assert_eq!(attribute(*a), b);
// And the name a declaration writes resolves to the same variant.
assert_eq!(Attribute::from_name(a.name()), Some(b), "{}", a.name());
}
}
/// TRACES: FR-PLG-2d
/// Every `WidgetKind` is nameable from a declaration.
///
/// A widget the core can ask for but a declaration cannot name is a widget
/// only a hand-written operation may have, which would make the two kinds
/// of node unequal in exactly the way FR-PLG-2 forbids.
#[test]
fn every_widget_kind_can_be_declared() {
assert_eq!(decl::Widget::ALL.len(), 6);
let named: Vec<WidgetKind> = decl::Widget::ALL.iter().copied().map(widget).collect();
for kind in [
WidgetKind::ToneCurve,
WidgetKind::ColourWheel,
WidgetKind::CropOverlay,
WidgetKind::GradientHandle,
WidgetKind::BrushMask,
WidgetKind::WhitePoint,
] {
assert!(named.contains(&kind), "{kind:?} cannot be declared");
}
}
#[test]
fn the_bundled_helper_library_parses() {
// It is `include_str!`'d, so a syntax error in it is a panic at first
// use rather than a build failure. This is the first use.
assert!(!builtin_helpers().helpers.is_empty());
}
fn exposure() -> DeclaredOp {
DeclaredOp::from_yaml(
include_str!("../../ops/exposure.yaml"),
"ops/exposure.yaml",
builtin_helpers(),
)
.expect("exposure declares an operation")
}
#[test]
fn a_declaration_becomes_an_operation_with_its_declared_descriptor() {
let op = exposure();
let d = op.descriptor();
assert_eq!(d.id, OpId("exposure"));
assert_eq!(d.label, LocalizedKey("op.exposure"));
assert_eq!(d.params.len(), 1);
assert_eq!(d.params[0].id, ParamId("exposure"));
assert_eq!(d.attributes, vec![Attribute::Tone]);
}
#[test]
fn a_declared_operation_is_neutral_until_a_parameter_moves() {
let mut op = exposure();
assert!(!op.is_active());
assert_eq!(op.uniforms()[0].value, 1.0);
op.set_param(ParamId("exposure"), 1.0);
assert!(op.is_active());
// A stop is a doubling — the same assertion `exposure.yaml`'s own
// declared test makes against the generated implementation.
assert_eq!(op.uniforms()[0].value, 2.0);
}
#[test]
fn an_interned_id_matches_a_literal_one() {
// The property that lets a declared operation be addressed by the same
// `ParamId` constants the generated code matches on. If interning ever
// stopped deduplicating, this would still pass — `ParamId` compares
// string contents — but the point is that the two are interchangeable
// at every call site.
let mut op = exposure();
op.set_param(ParamId::interned("exposure"), 2.0);
assert_eq!(op.param(ParamId("exposure")), 2.0);
}
#[test]
fn a_rust_node_is_refused_rather_than_silently_dropped() {
let err = DeclaredOp::from_yaml(
include_str!("../../ops/tone_curve.yaml"),
"ops/tone_curve.yaml",
builtin_helpers(),
)
.expect_err("a `rust:` node is not a declaration");
assert!(err.contains("hand-written type"), "{err}");
}
}
+142 -29
View File
@@ -8,12 +8,75 @@
//! Labels are keys, not strings: resolving them needs a localiser, and //! Labels are keys, not strings: resolving them needs a localiser, and
//! `core/` must not depend on one (NFR-A11Y-1). //! `core/` must not depend on one (NFR-A11Y-1).
use std::collections::HashSet;
use std::fmt; 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<str>` 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<Mutex<HashSet<&'static str>>> =
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. /// Identifies a parameter within an operation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ParamId(pub &'static str); 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 { impl fmt::Display for ParamId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0) f.write_str(self.0)
@@ -24,6 +87,13 @@ impl fmt::Display for ParamId {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct OpId(pub &'static str); 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 { impl fmt::Display for OpId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0) f.write_str(self.0)
@@ -34,6 +104,13 @@ impl fmt::Display for OpId {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocalizedKey(pub &'static str); 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. /// What a slider's travel means.
/// ///
/// Photographic controls are rarely linear in their underlying quantity: /// Photographic controls are rarely linear in their underlying quantity:
@@ -170,7 +247,11 @@ pub enum ParamKind {
Enum { Enum {
/// In index order. The label is a localisation key, resolved by the /// In index order. The label is a localisation key, resolved by the
/// frontend — `core/` must not depend on a localiser (NFR-A11Y-1). /// 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<LocalizedKey>,
}, },
} }
@@ -204,7 +285,7 @@ pub struct Presentation {
/// pair of sliders, and an operation that can say so gets a good control /// 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 /// on a workstation and a usable one on a phone without the core knowing
/// which it is talking to. /// which it is talking to.
pub widgets: &'static [WidgetKind], pub widgets: Vec<WidgetKind>,
/// What the preferred widget needs in order to be worth drawing. /// 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 /// 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 /// Parameters absent from this list are presented normally, so an
/// operation can pair a curve with an ordinary strength slider. /// operation can pair a curve with an ordinary strength slider.
pub params: &'static [ParamId], pub params: Vec<ParamId>,
} }
impl Presentation { impl Presentation {
@@ -328,11 +409,13 @@ impl ParamDescriptor {
/// holds the way it does for every other kind: index 0 is the neutral /// 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 /// choice, and an operation whose default is not its first variant has
/// listed them in the wrong order. /// listed them in the wrong order.
pub const fn choice( //
id: &'static str, // Not `const`, unlike its four siblings, and the reason is the `Vec` in
label: &'static str, // [`ParamKind::Enum`]: a heap allocation cannot happen in a const context.
variants: &'static [LocalizedKey], // Nothing is lost — every descriptor now lives inside a `LazyLock`
) -> Self { // 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<LocalizedKey>) -> Self {
Self { Self {
id: ParamId(id), id: ParamId(id),
label: LocalizedKey(label), label: LocalizedKey(label),
@@ -366,13 +449,15 @@ impl ParamDescriptor {
/// A general scalar with an explicit range and default. /// A general scalar with an explicit range and default.
// //
// Eight arguments, and a builder would be the usual answer — but this has // Eight arguments, and a builder would be the usual answer — but eight
// to be `const` so descriptors can be `static`, which rules out the // `&mut self` steps would be eight methods to say what one call already
// `&mut self` builder the pattern usually takes. (A `self`-by-value step // says, and each one would be a place for a caller to forget a field. The
// *is* const-callable — `faceted` below is one — but eight of them would // two common shapes have their own constructors above; this is the escape
// be eight methods to say what one call already says.) The two common // hatch for the rest.
// 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)] #[allow(clippy::too_many_arguments)]
pub const fn scalar( pub const fn scalar(
id: &'static str, id: &'static str,
@@ -404,10 +489,13 @@ impl ParamDescriptor {
/// A method rather than a sixth constructor, because a facet is orthogonal /// 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 /// 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 /// 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 /// twin would double the list above to say one thing.
/// value is what keeps it usable in the `static` descriptors — a `&mut //
/// self` builder is what cannot be `const`. // No longer `const`: a `ParamDescriptor` can now carry a `Vec` (an enum's
pub const fn faceted(mut self, facet: Facet) -> Self { // 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.facet = Some(facet);
self self
} }
@@ -418,10 +506,10 @@ impl ParamDescriptor {
/// bounds, or a sidecar written by a newer version with a wider range, /// bounds, or a sidecar written by a newer version with a wider range,
/// must not produce out-of-range uniforms. /// must not produce out-of-range uniforms.
pub fn clamp(&self, value: f32) -> f32 { pub fn clamp(&self, value: f32) -> f32 {
match self.kind { match &self.kind {
ParamKind::Scalar { min, max, .. } => { ParamKind::Scalar { min, max, .. } => {
if value.is_finite() { if value.is_finite() {
value.clamp(min, max) value.clamp(*min, *max)
} else { } else {
// A NaN from a corrupt sidecar would otherwise poison the // A NaN from a corrupt sidecar would otherwise poison the
// uniform block and blank the image. // 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<OpDescriptor>`. 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)] #[derive(Debug, Clone, PartialEq)]
pub struct OpDescriptor { pub struct OpDescriptor {
pub id: OpId, pub id: OpId,
pub label: LocalizedKey, pub label: LocalizedKey,
pub params: &'static [ParamDescriptor], pub params: Vec<ParamDescriptor>,
/// What this operation is about (ARCH §4.3a). /// What this operation is about (ARCH §4.3a).
/// ///
/// **Never empty**, and `build.rs` refuses to generate an operation that /// **Never empty**, and both the build-time and the load-time reader
/// declares none. An operation with no attribute would be invisible to a /// refuse an operation that declares none. An operation with no attribute
/// frontend that filters by them, and a control that silently does not /// would be invisible to a frontend that filters by them, and a control
/// exist is a worse failure than a build that stops — particularly when /// that silently does not exist is a worse failure than a build that stops
/// the cause would be a missing line in a YAML file nobody looked at. /// — particularly when the cause would be a missing line in a YAML file
pub attributes: &'static [Attribute], /// nobody looked at.
pub attributes: Vec<Attribute>,
} }
impl OpDescriptor { impl OpDescriptor {
+7 -1
View File
@@ -519,7 +519,13 @@ pub fn compose_detail_with(
) -> ComposedDetail { ) -> ComposedDetail {
// Every pass of every active detail operation, flattened, carrying the // Every pass of every active detail operation, flattened, carrying the
// operation it came from for the uniform prefix and the helper set. // 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() { for (index, pass) in spots.iter().enumerate() {
planned.push(( planned.push((
crate::spot::SPOT_ID, crate::spot::SPOT_ID,
+27 -24
View File
@@ -29,6 +29,7 @@
//! was built for: a horizontal pass then a vertical one is mathematically a 2D //! 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 //! 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. //! output the result is visibly not a box blur rather than subtly wrong.
use std::sync::{Arc, LazyLock};
use crate::descriptor::{ use crate::descriptor::{
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind, Scale, Unit, Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind, Scale, Unit,
@@ -36,28 +37,30 @@ use crate::descriptor::{
use crate::detail::{DetailPass, DetailStage, RenderScale}; use crate::detail::{DetailPass, DetailStage, RenderScale};
use crate::operation::{Affects, Operation, Uniform}; use crate::operation::{Affects, Operation, Uniform};
static DESCRIPTOR: OpDescriptor = OpDescriptor { static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
id: OpId("detail_probe"), Arc::new(OpDescriptor {
label: LocalizedKey("op.detail_probe"), id: OpId("detail_probe"),
params: &[ParamDescriptor { label: LocalizedKey("op.detail_probe"),
id: ParamId("radius"), params: vec![ParamDescriptor {
label: LocalizedKey("param.detail_probe.radius"), id: ParamId("radius"),
// A fraction of the frame's shorter edge, which is the unit label: LocalizedKey("param.detail_probe.radius"),
// `RenderScale::frame_fraction` converts and the unit a mask feather // A fraction of the frame's shorter edge, which is the unit
// is already stored in. Stating it in pixels is the mistake this // `RenderScale::frame_fraction` converts and the unit a mask feather
// whole stage is arranged to make impossible. // is already stored in. Stating it in pixels is the mistake this
kind: ParamKind::Scalar { // whole stage is arranged to make impossible.
min: 0.0, kind: ParamKind::Scalar {
max: 0.25, min: 0.0,
scale: Scale::Linear, max: 0.25,
unit: Unit::None, scale: Scale::Linear,
precision: 4, unit: Unit::None,
}, precision: 4,
default: 0.0, },
facet: None, default: 0.0,
}], facet: None,
attributes: &[Attribute::Detail], }],
}; attributes: vec![Attribute::Detail],
})
});
/// A separable box blur whose radius is a fraction of the frame's shorter edge. /// A separable box blur whose radius is a fraction of the frame's shorter edge.
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
@@ -85,8 +88,8 @@ impl BoxBlur {
} }
impl Operation for BoxBlur { impl Operation for BoxBlur {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
&DESCRIPTOR DESCRIPTOR.clone()
} }
fn set_param(&mut self, _id: ParamId, value: f32) { fn set_param(&mut self, _id: ParamId, value: f32) {
+54 -51
View File
@@ -40,6 +40,7 @@
use std::f32::consts::PI; use std::f32::consts::PI;
use std::fmt::Write as _; use std::fmt::Write as _;
use std::sync::{Arc, LazyLock};
use crate::descriptor::{ use crate::descriptor::{
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation, Scale, 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, CROP_X, CROP_Y, CROP_W, CROP_H, ANGLE, ROTATION, FLIP_H, FLIP_V,
]; ];
static DESCRIPTOR: OpDescriptor = OpDescriptor { static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
// The shape of the frame, and the only operation that changes the Arc::new(OpDescriptor {
// output's dimensions. // The shape of the frame, and the only operation that changes the
attributes: &[Attribute::Geometry], // output's dimensions.
id: ID, attributes: vec![Attribute::Geometry],
label: LocalizedKey("op.framing"), id: ID,
params: &[ label: LocalizedKey("op.framing"),
// Straightening. Degrees rather than a normalised amount because a params: vec![
// photographer reading "-1.4°" off a horizon knows what it means. // Straightening. Degrees rather than a normalised amount because a
ParamDescriptor::scalar( // photographer reading "-1.4°" off a horizon knows what it means.
"angle", ParamDescriptor::scalar(
"param.angle", "angle",
-MAX_STRAIGHTEN, "param.angle",
MAX_STRAIGHTEN, -MAX_STRAIGHTEN,
0.0, MAX_STRAIGHTEN,
Unit::None, 0.0,
Scale::Linear, Unit::None,
2, Scale::Linear,
), 2,
// Quarter turns, 0..3. Separate from `angle` because these are exact ),
// and lossless, and because reorienting a frame is a different // Quarter turns, 0..3. Separate from `angle` because these are exact
// gesture from nudging a horizon. // and lossless, and because reorienting a frame is a different
ParamDescriptor::scalar( // gesture from nudging a horizon.
"rotation", ParamDescriptor::scalar(
"param.rotation", "rotation",
0.0, "param.rotation",
3.0, 0.0,
0.0, 3.0,
Unit::None, 0.0,
Scale::Linear, Unit::None,
0, Scale::Linear,
), 0,
ParamDescriptor::switch("flip_h", "param.flip_h"), ),
ParamDescriptor::switch("flip_v", "param.flip_v"), ParamDescriptor::switch("flip_h", "param.flip_h"),
// The crop rect, in fractions of the source. Normalised rather than ParamDescriptor::switch("flip_v", "param.flip_v"),
// in pixels so a crop survives being applied to a proxy, a full // The crop rect, in fractions of the source. Normalised rather than
// resolution render, or an export at another size — the same reason // in pixels so a crop survives being applied to a proxy, a full
// the viewport renders at display resolution (FR-DSP-1). // resolution render, or an export at another size — the same reason
ParamDescriptor::fraction("crop_x", "param.crop_x", 0.0), // the viewport renders at display resolution (FR-DSP-1).
ParamDescriptor::fraction("crop_y", "param.crop_y", 0.0), ParamDescriptor::fraction("crop_x", "param.crop_x", 0.0),
ParamDescriptor::fraction("crop_w", "param.crop_w", 1.0), ParamDescriptor::fraction("crop_y", "param.crop_y", 0.0),
ParamDescriptor::fraction("crop_h", "param.crop_h", 1.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. /// A normalised crop rectangle, in fractions of the source image.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
@@ -252,8 +255,8 @@ impl Framing {
Self::default() Self::default()
} }
pub fn descriptor(&self) -> &'static OpDescriptor { pub fn descriptor(&self) -> Arc<OpDescriptor> {
&DESCRIPTOR DESCRIPTOR.clone()
} }
/// TRACES: FR-DEV-3a | FR-DEV-3b | FR-UI-7 /// 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. /// into the generated panel underneath a crop control that already exists.
pub fn presentation(&self) -> Option<Presentation> { pub fn presentation(&self) -> Option<Presentation> {
Some(Presentation { Some(Presentation {
widgets: &[WidgetKind::CropOverlay], widgets: vec![WidgetKind::CropOverlay],
demand: WidgetDemand { demand: WidgetDemand {
// A crop rect is dragged by its corners; nothing about that // A crop rect is dragged by its corners; nothing about that
// reduces to one axis at a time. // reduces to one axis at a time.
@@ -290,7 +293,7 @@ impl Framing {
// modality, so a thumb is as workable as a mouse. // modality, so a thumb is as workable as a mouse.
precise_pointing: false, 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() { fn every_parameter_at_its_extremes_is_survivable() {
// The whole descriptor driven to both ends, which is what a codegen // The whole descriptor driven to both ends, which is what a codegen
// test does and what a corrupt sidecar can do. // 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] { for value in [-1e9, -1.0, 0.0, 1.0, 1e9, f32::NAN] {
let mut f = Framing::new(); let mut f = Framing::new();
f.set_param(p.id, p.clamp(value)); f.set_param(p.id, p.clamp(value));
@@ -1610,7 +1613,7 @@ mod tests {
// The same contract the operations honour, checked against the // The same contract the operations honour, checked against the
// descriptor rather than a literal. // descriptor rather than a literal.
let mut f = Framing::new(); let mut f = Framing::new();
for p in DESCRIPTOR.params { for p in &DESCRIPTOR.params {
f.set_param(p.id, p.default); f.set_param(p.id, p.default);
} }
assert!(!f.is_active(), "descriptor defaults must be neutral"); assert!(!f.is_active(), "descriptor defaults must be neutral");
@@ -1618,7 +1621,7 @@ mod tests {
#[test] #[test]
fn every_default_is_within_its_declared_range() { 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); assert_eq!(p.clamp(p.default), p.default, "{} is out of range", p.id);
} }
} }
+13 -9
View File
@@ -51,8 +51,8 @@ pub struct OpCapability {
/// other, so a new operation joins the right group by declaring what it /// 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. /// is — which is the only thing its author is well placed to say.
/// ///
/// Never empty; `build.rs` refuses an operation that declares none. /// Never empty; both readers refuse an operation that declares none.
pub attributes: &'static [Attribute], pub attributes: Vec<Attribute>,
} }
/// TRACES: FR-DEV-3a | FR-DEV-3b /// TRACES: FR-DEV-3a | FR-DEV-3b
@@ -243,7 +243,7 @@ impl EditGraph {
/// because this is what the codegen tests count `---- ` shader blocks /// because this is what the codegen tests count `---- ` shader blocks
/// against, and framing generates a prologue rather than a colour block. /// against, and framing generates a prologue rather than a colour block.
/// A UI wanting everything should read [`Self::capabilities`] (FR-DEV-3a). /// A UI wanting everything should read [`Self::capabilities`] (FR-DEV-3a).
pub fn descriptors(&self) -> Vec<&'static OpDescriptor> { pub fn descriptors(&self) -> Vec<Arc<OpDescriptor>> {
self.ops.iter().map(|o| o.descriptor()).collect() self.ops.iter().map(|o| o.descriptor()).collect()
} }
@@ -281,7 +281,7 @@ impl EditGraph {
}) })
.collect(), .collect(),
presentation: op.presentation(), presentation: op.presentation(),
attributes: desc.attributes, attributes: desc.attributes.clone(),
} }
}); });
@@ -310,7 +310,7 @@ impl EditGraph {
// sliders for framing *without naming framing* — see // sliders for framing *without naming framing* — see
// `Framing::presentation`. // `Framing::presentation`.
presentation: self.framing.presentation(), presentation: self.framing.presentation(),
attributes: desc.attributes, attributes: desc.attributes.clone(),
}; };
ops.chain(std::iter::once(framing)).collect() 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) { pub fn set_param(&mut self, op: OpId, param: ParamId, value: f32) {
if op == crate::framing::ID { 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"); log::warn!("unknown parameter {param} on {op}; ignoring");
return; return;
}; };
@@ -469,7 +473,7 @@ impl EditGraph {
/// Reset every parameter of every operation, and the framing, to default. /// Reset every parameter of every operation, and the framing, to default.
pub fn reset(&mut self) { pub fn reset(&mut self) {
for op in &mut self.ops { 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); op.set_param(p.id, p.default);
} }
} }
@@ -625,7 +629,7 @@ impl EditGraph {
// structure key deliberately omits because they do not recompile a // structure key deliberately omits because they do not recompile a
// shader. Both matter to a cached *result*, so both are here. // shader. Both matter to a cached *result*, so both are here.
let mut geometry = mix(FNV_OFFSET, self.framing.structure_key()); 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 = hash_bytes(geometry, p.id.0.as_bytes());
geometry = mix( geometry = mix(
geometry, geometry,
@@ -944,7 +948,7 @@ mod tests {
.capabilities() .capabilities()
.iter() .iter()
.flat_map(|c| { .flat_map(|c| {
c.params.iter().map(move |p| match p.kind { c.params.iter().map(move |p| match &p.kind {
ParamKind::Scalar { ParamKind::Scalar {
min, min,
max, max,
+45 -24
View File
@@ -53,6 +53,7 @@
//! wearing the same lens, which defeats the point of a lens profile. //! wearing the same lens, which defeats the point of a lens profile.
use std::fmt::Write as _; use std::fmt::Write as _;
use std::sync::Arc;
use crate::descriptor::{OpDescriptor, ParamId}; use crate::descriptor::{OpDescriptor, ParamId};
use crate::operation::{Helper, Uniform}; 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 /// Object-safe for the same reason [`crate::operation::Operation`] is: the
/// graph holds `Box<dyn Warp>` in order, so the geometry chain is data. /// graph holds `Box<dyn Warp>` in order, so the geometry chain is data.
pub trait Warp: Send + Sync { pub trait Warp: Send + Sync {
/// Static description, driving UI generation exactly as for an operation. /// This warp's description, driving UI generation exactly as for an
fn descriptor(&self) -> &'static OpDescriptor; /// operation — including being owned rather than `&'static`, for the
/// reason [`crate::descriptor::OpDescriptor`] gives.
fn descriptor(&self) -> Arc<OpDescriptor>;
/// Set a parameter. Values arrive already clamped to the descriptor. /// Set a parameter. Values arrive already clamped to the descriptor.
fn set_param(&mut self, id: ParamId, value: f32); fn set_param(&mut self, id: ParamId, value: f32);
@@ -204,32 +207,38 @@ fn sanitise(id: &str) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::sync::LazyLock;
use super::*; use super::*;
use crate::descriptor::Attribute; use crate::descriptor::Attribute;
use crate::descriptor::{LocalizedKey, OpDescriptor, OpId, ParamDescriptor}; use crate::descriptor::{LocalizedKey, OpDescriptor, OpId, ParamDescriptor};
static DESC_A: OpDescriptor = OpDescriptor { static DESC_A: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
id: OpId("warp_a"), Arc::new(OpDescriptor {
label: LocalizedKey("a"), id: OpId("warp_a"),
params: &[ParamDescriptor::amount("amount", "a.amount")], label: LocalizedKey("a"),
attributes: &[Attribute::Tone], params: vec![ParamDescriptor::amount("amount", "a.amount")],
}; attributes: vec![Attribute::Tone],
static DESC_B: OpDescriptor = OpDescriptor { })
id: OpId("warp_b"), });
label: LocalizedKey("b"), static DESC_B: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
params: &[ParamDescriptor::amount("amount", "b.amount")], Arc::new(OpDescriptor {
attributes: &[Attribute::Tone], id: OpId("warp_b"),
}; label: LocalizedKey("b"),
params: vec![ParamDescriptor::amount("amount", "b.amount")],
attributes: vec![Attribute::Tone],
})
});
struct Fake { struct Fake {
desc: &'static OpDescriptor, desc: Arc<OpDescriptor>,
amount: f32, amount: f32,
splits: bool, splits: bool,
} }
impl Warp for Fake { impl Warp for Fake {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
self.desc self.desc.clone()
} }
fn set_param(&mut self, _id: ParamId, value: f32) { fn set_param(&mut self, _id: ParamId, value: f32) {
self.amount = value; self.amount = value;
@@ -254,7 +263,7 @@ mod tests {
} }
} }
fn fake(desc: &'static OpDescriptor, amount: f32, splits: bool) -> Box<dyn Warp> { fn fake(desc: Arc<OpDescriptor>, amount: f32, splits: bool) -> Box<dyn Warp> {
Box::new(Fake { Box::new(Fake {
desc, desc,
amount, amount,
@@ -266,7 +275,7 @@ mod tests {
fn no_active_warp_composes_to_nothing() { fn no_active_warp_composes_to_nothing() {
// The property that keeps the common case free: an image with no lens // The property that keeps the common case free: an image with no lens
// correction must not pay for a bilinear sample. // 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.is_active());
assert!(composed.uniforms.is_empty()); assert!(composed.uniforms.is_empty());
assert!(!composed.splits_channels); assert!(!composed.splits_channels);
@@ -274,7 +283,7 @@ mod tests {
#[test] #[test]
fn an_active_warp_appears_once() { 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.is_active());
assert!(composed.body.contains("---- warp: warp_a ----")); assert!(composed.body.contains("---- warp: warp_a ----"));
assert!(composed.body.contains("u.warp_a_amount")); assert!(composed.body.contains("u.warp_a_amount"));
@@ -284,7 +293,10 @@ mod tests {
fn uniforms_are_prefixed_so_warps_cannot_collide() { fn uniforms_are_prefixed_so_warps_cannot_collide() {
// Both fakes declare `amount`; without prefixing the generated struct // Both fakes declare `amount`; without prefixing the generated struct
// would carry a duplicate field and fail to compile. // 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_a_amount: f32"));
assert!(composed.uniform_fields.contains("warp_b_amount: f32")); assert!(composed.uniform_fields.contains("warp_b_amount: f32"));
assert_eq!(composed.uniforms, vec![1.0, 2.0]); assert_eq!(composed.uniforms, vec![1.0, 2.0]);
@@ -294,7 +306,10 @@ mod tests {
fn channel_splitting_is_requested_by_any_active_warp() { fn channel_splitting_is_requested_by_any_active_warp() {
// One CA warp among several must switch the whole stage to the // One CA warp among several must switch the whole stage to the
// three-sample path. // 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); assert!(composed.splits_channels);
} }
@@ -302,14 +317,20 @@ mod tests {
fn an_inactive_splitting_warp_does_not_force_three_samples() { fn an_inactive_splitting_warp_does_not_force_three_samples() {
// CA present but at neutral must cost nothing — otherwise every image // CA present but at neutral must cost nothing — otherwise every image
// with the panel visible pays triple bandwidth. // 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.is_active());
assert!(!composed.splits_channels); assert!(!composed.splits_channels);
} }
#[test] #[test]
fn warps_compose_in_order() { 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 a = composed.body.find("warp_a").expect("a present");
let b = composed.body.find("warp_b").expect("b present"); let b = composed.body.find("warp_b").expect("b present");
assert!(a < b, "warps must chain in graph order"); assert!(a < b, "warps must chain in graph order");
+6 -4
View File
@@ -31,6 +31,7 @@
//! single multiply and white balance a per-channel scale; on gamma-encoded //! single multiply and white balance a per-channel scale; on gamma-encoded
//! data neither would be physically meaningful (ARCH §5.2). //! data neither would be physically meaningful (ARCH §5.2).
pub mod declared;
pub mod descriptor; pub mod descriptor;
pub mod detail; pub mod detail;
pub mod framing; pub mod framing;
@@ -45,6 +46,7 @@ pub mod sidecar;
pub mod spot; pub mod spot;
pub mod state; pub mod state;
pub use declared::{Declaration, DeclaredOp};
pub use descriptor::{ pub use descriptor::{
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind, Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind,
Presentation, Scale, Unit, WidgetDemand, WidgetKind, Presentation, Scale, Unit, WidgetDemand, WidgetKind,
@@ -81,13 +83,13 @@ mod tests {
let mut g = EditGraph::default_chain(); let mut g = EditGraph::default_chain();
for desc in g.descriptors() { for desc in g.descriptors() {
for (i, p) in desc.params.iter().enumerate() { for (i, p) in desc.params.iter().enumerate() {
let v = match p.kind { let v = match &p.kind {
ParamKind::Scalar { min, max, .. } => { ParamKind::Scalar { min, max, .. } => {
// A fraction that differs per parameter, so no two // A fraction that differs per parameter, so no two
// move in lockstep. // move in lockstep.
let fraction = 0.15 + 0.05 * (i % 4) as f32; let fraction = 0.15 + 0.05 * (i % 4) as f32;
let step = (max - min) * fraction; let step = (max - min) * fraction;
if p.default + step <= max { if p.default + step <= *max {
p.default + step p.default + step
} else { } else {
p.default - step p.default - step
@@ -217,7 +219,7 @@ mod tests {
// A default outside its own range would mean a fresh image opens // A default outside its own range would mean a fresh image opens
// with a value the UI cannot represent. // with a value the UI cannot represent.
for desc in EditGraph::default_chain().descriptors() { for desc in EditGraph::default_chain().descriptors() {
for p in desc.params { for p in &desc.params {
assert_eq!( assert_eq!(
p.clamp(p.default), p.clamp(p.default),
p.default, p.default,
@@ -236,7 +238,7 @@ mod tests {
// operation must read its own default as neutral. // operation must read its own default as neutral.
let g = EditGraph::default_chain(); let g = EditGraph::default_chain();
for desc in g.descriptors() { for desc in g.descriptors() {
for p in desc.params { for p in &desc.params {
assert_eq!( assert_eq!(
g.param(desc.id, p.id), g.param(desc.id, p.id),
Some(p.default), Some(p.default),
+17 -8
View File
@@ -36,6 +36,7 @@
//! see there for what happens when it does not match. //! see there for what happens when it does not match.
use std::fmt::Write as _; use std::fmt::Write as _;
use std::sync::Arc;
use crate::descriptor::{OpDescriptor, ParamId}; use crate::descriptor::{OpDescriptor, ParamId};
use crate::operation::Operation; use crate::operation::Operation;
@@ -692,7 +693,7 @@ impl Clone for MaskLayer {
fn clone(&self) -> Self { fn clone(&self) -> Self {
let mut ops = layer_chain(); let mut ops = layer_chain();
for (dst, src) in ops.iter_mut().zip(&self.ops) { 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)); 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<Arc<OpDescriptor>> {
self.ops.iter().map(|o| o.descriptor()).collect() self.ops.iter().map(|o| o.descriptor()).collect()
} }
@@ -995,7 +996,7 @@ impl MaskLayer {
}) })
.collect(), .collect(),
presentation: op.presentation(), presentation: op.presentation(),
attributes: desc.attributes, attributes: desc.attributes.clone(),
} }
}) })
.collect() .collect()
@@ -1007,7 +1008,7 @@ impl MaskLayer {
/// arrive at — so "start this layer's edit again" must not throw it away. /// arrive at — so "start this layer's edit again" must not throw it away.
pub fn reset_adjustments(&mut self) { pub fn reset_adjustments(&mut self) {
for op in &mut self.ops { 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); op.set_param(p.id, p.default);
} }
} }
@@ -1026,10 +1027,18 @@ impl MaskLayer {
/// Every non-default parameter, for the sidecar. /// Every non-default parameter, for the sidecar.
pub fn params(&self) -> impl Iterator<Item = (&'static str, &'static str, f32)> + '_ { pub fn params(&self) -> impl Iterator<Item = (&'static str, &'static str, f32)> + '_ {
self.ops.iter().flat_map(|o| { self.ops.iter().flat_map(|o| {
let id = o.descriptor().id.0; let desc = o.descriptor();
o.descriptor().params.iter().filter_map(move |p| { let id = desc.id.0;
let v = o.param(p.id); // Collected rather than borrowed from `desc`: a descriptor is an
(v != p.default).then_some((id, p.id.0, v)) // `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))
}) })
}) })
} }
+111 -40
View File
@@ -21,6 +21,7 @@
//! generator emits readable, commented output — see [`compose`]. //! generator emits readable, commented output — see [`compose`].
use std::fmt::Write as _; use std::fmt::Write as _;
use std::sync::Arc;
use dr_types::{ColourSpace, Transfer}; use dr_types::{ColourSpace, Transfer};
@@ -171,7 +172,7 @@ impl Invalidation {
pub(crate) fn hash_op(h: u64, op: &dyn Operation) -> u64 { pub(crate) fn hash_op(h: u64, op: &dyn Operation) -> u64 {
let desc = op.descriptor(); let desc = op.descriptor();
let mut h = hash_bytes(h, desc.id.0.as_bytes()); 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 = hash_bytes(h, p.id.0.as_bytes());
h = mix(h, u64::from(canonical_bits(op.param(p.id)))); 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 { pub struct Uniform {
/// Field name as it appears in WGSL. Prefixed with the op id by the /// Field name as it appears in WGSL. Prefixed with the op id by the
/// composer, so two operations may both declare `amount`. /// 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 name: &'static str,
pub value: f32, pub value: f32,
} }
@@ -222,8 +229,32 @@ pub struct Uniform {
/// Object-safe: the pipeline holds `Box<dyn Operation>` in graph order, so /// Object-safe: the pipeline holds `Box<dyn Operation>` in graph order, so
/// order is data rather than code (ARCH §3.4). /// order is data rather than code (ARCH §3.4).
pub trait Operation: Send + Sync { pub trait Operation: Send + Sync {
/// Static description, driving UI generation (FR-DEV-3a). /// TRACES: FR-DEV-3a | FR-PLG-2
fn descriptor(&self) -> &'static OpDescriptor; /// 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<OpDescriptor>;
/// Set a parameter. Values arrive already clamped to the descriptor. /// Set a parameter. Values arrive already clamped to the descriptor.
fn set_param(&mut self, id: ParamId, value: f32); 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 /// Emitted once per *distinct* function name even if several operations
/// request it, so shared helpers (luminance, soft clipping) are declared /// request it, so shared helpers (luminance, soft clipping) are declared
/// exactly once. /// 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::sync::LazyLock;
use crate::descriptor::Attribute; use crate::descriptor::Attribute;
use crate::descriptor::{LocalizedKey, OpId, ParamDescriptor}; use crate::descriptor::{LocalizedKey, OpId, ParamDescriptor};
static DESC_A: OpDescriptor = OpDescriptor { static DESC_A: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
id: OpId("op_a"), Arc::new(OpDescriptor {
label: LocalizedKey("a"), id: OpId("op_a"),
params: &[ParamDescriptor::amount("amount", "a.amount")], label: LocalizedKey("a"),
attributes: &[Attribute::Tone], params: vec![ParamDescriptor::amount("amount", "a.amount")],
}; attributes: vec![Attribute::Tone],
static DESC_B: OpDescriptor = OpDescriptor { })
id: OpId("op_b"), });
label: LocalizedKey("b"), static DESC_B: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
params: &[ParamDescriptor::amount("amount", "b.amount")], Arc::new(OpDescriptor {
attributes: &[Attribute::Tone], id: OpId("op_b"),
}; label: LocalizedKey("b"),
params: vec![ParamDescriptor::amount("amount", "b.amount")],
attributes: vec![Attribute::Tone],
})
});
struct Fake { struct Fake {
desc: &'static OpDescriptor, desc: Arc<OpDescriptor>,
amount: f32, amount: f32,
helper: Option<Helper>, helper: Option<Helper>,
} }
impl Operation for Fake { impl Operation for Fake {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
self.desc self.desc.clone()
} }
fn set_param(&mut self, _id: ParamId, value: f32) { fn set_param(&mut self, _id: ParamId, value: f32) {
self.amount = value; self.amount = value;
@@ -1241,7 +1284,7 @@ mod tests {
source: "fn luma(c: vec3<f32>) -> f32 { return c.g; }", source: "fn luma(c: vec3<f32>) -> f32 { return c.g; }",
}]; }];
fn fake(desc: &'static OpDescriptor, amount: f32, helper: bool) -> Box<dyn Operation> { fn fake(desc: Arc<OpDescriptor>, amount: f32, helper: bool) -> Box<dyn Operation> {
Box::new(Fake { Box::new(Fake {
desc, desc,
amount, amount,
@@ -1253,7 +1296,7 @@ mod tests {
fn an_inactive_operation_contributes_nothing() { fn an_inactive_operation_contributes_nothing() {
// The point of composing rather than branching: an op at neutral // The point of composing rather than branching: an op at neutral
// must not appear in the source at all. // 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); let shader = compose(&ops);
assert!( assert!(
!shader.source.contains("op_a"), !shader.source.contains("op_a"),
@@ -1274,7 +1317,7 @@ mod tests {
#[test] #[test]
fn an_active_operation_appears_once() { 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); let shader = compose(&ops);
assert!(shader.source.contains("---- op_a ----")); assert!(shader.source.contains("---- op_a ----"));
assert!(shader.source.contains("u.op_a_amount")); assert!(shader.source.contains("u.op_a_amount"));
@@ -1285,7 +1328,10 @@ mod tests {
// Both fakes declare a uniform called `amount`. Without prefixing, // Both fakes declare a uniform called `amount`. Without prefixing,
// the generated struct would have a duplicate field and fail to // the generated struct would have a duplicate field and fail to
// compile — the failure mode that makes naive concatenation fragile. // 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); let shader = compose(&ops);
assert!(shader.source.contains("op_a_amount: f32")); assert!(shader.source.contains("op_a_amount: f32"));
assert!(shader.source.contains("op_b_amount: f32")); assert!(shader.source.contains("op_b_amount: f32"));
@@ -1295,7 +1341,10 @@ mod tests {
#[test] #[test]
fn uniform_values_follow_declaration_order() { 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); let shader = compose(&ops);
assert_eq!(shader.uniforms[PREAMBLE_FIELDS], 1.5); assert_eq!(shader.uniforms[PREAMBLE_FIELDS], 1.5);
assert_eq!(shader.uniforms[PREAMBLE_FIELDS + 1], 2.5); assert_eq!(shader.uniforms[PREAMBLE_FIELDS + 1], 2.5);
@@ -1305,7 +1354,10 @@ mod tests {
fn a_shared_helper_is_emitted_once() { fn a_shared_helper_is_emitted_once() {
// Two operations wanting the same helper must not produce a // Two operations wanting the same helper must not produce a
// duplicate function definition. // 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); let shader = compose(&ops);
assert_eq!( assert_eq!(
shader.source.matches("fn luma(").count(), 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. // WGSL rejects a uniform struct whose size is not a multiple of 16.
for n in 0..6 { for n in 0..6 {
let ops: Vec<Box<dyn Operation>> = (0..n) let ops: Vec<Box<dyn Operation>> = (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(); .collect();
let shader = compose(&ops); let shader = compose(&ops);
assert_eq!( assert_eq!(
@@ -1335,11 +1397,14 @@ mod tests {
fn structure_hash_ignores_values_but_tracks_the_op_set() { fn structure_hash_ignores_values_but_tracks_the_op_set() {
// The property the shader cache depends on: moving a slider must not // The property the shader cache depends on: moving a slider must not
// trigger a recompile, but enabling an operation must. // trigger a recompile, but enabling an operation must.
let a1 = compose(&[fake(&DESC_A, 1.0, false)]).structure_hash; let a1 = compose(&[fake(DESC_A.clone(), 1.0, false)]).structure_hash;
let a2 = compose(&[fake(&DESC_A, 9.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"); 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"); assert_ne!(a1, both.structure_hash, "a different op-set must recompile");
} }
@@ -1347,8 +1412,14 @@ mod tests {
fn structure_hash_is_order_sensitive() { fn structure_hash_is_order_sensitive() {
// Operation order is data (ARCH §3.4); two orders are different // Operation order is data (ARCH §3.4); two orders are different
// shaders and must not share a cache entry. // shaders and must not share a cache entry.
let ab = compose(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, false)]); let ab = compose(&[
let ba = compose(&[fake(&DESC_B, 1.0, false), fake(&DESC_A, 1.0, false)]); 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); 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 // Exposure and the tonal controls act on white-balanced values; if
// the multiply came afterwards, every operation would be reasoning // the multiply came afterwards, every operation would be reasoning
// about a green-cast image. // 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 source = compose(&ops).source;
let wb = source.find("u.as_shot_wb").expect("wb applied"); let wb = source.find("u.as_shot_wb").expect("wb applied");
let op = source.find("---- op_a ----").expect("op present"); 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 // The other half, and the one that would fail silently: a bug that
// suppressed the tail unconditionally renders every ordinary edit // suppressed the tail unconditionally renders every ordinary edit
// flat and uncorrected, which reads as a broken camera profile. // 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("base_curve_last.z > 0.5"));
assert!(source.contains("Camera space -> linear sRGB")); assert!(source.contains("Camera space -> linear sRGB"));
} }
@@ -1484,7 +1555,7 @@ mod tests {
// stock loaded is the default state of every photograph in the // stock loaded is the default state of every photograph in the
// catalogue, and it must not disturb the camera's own rendering. // catalogue, and it must not disturb the camera's own rendering.
let film: Box<dyn Operation> = Box::new(crate::ops::FilmSim::new()); let film: Box<dyn Operation> = 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("base_curve_last.z > 0.5"));
assert!(source.contains("Camera space -> linear sRGB")); assert!(source.contains("Camera space -> linear sRGB"));
} }
@@ -1493,7 +1564,7 @@ mod tests {
fn the_camera_matrix_is_applied_after_the_operations() { fn the_camera_matrix_is_applied_after_the_operations() {
// Adjustments are meaningful in sensor-native space, where highlight // Adjustments are meaningful in sensor-native space, where highlight
// headroom still exists; converting first would clip it away. // 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 source = compose(&ops).source;
let op = source.find("---- op_a ----").expect("op present"); let op = source.find("---- op_a ----").expect("op present");
let matrix = source.find("u.cam_to_srgb_0").expect("matrix applied"); 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 // Before the matrix: the curve was tuned against this body's own
// primaries. Applied after the conversion it would be a Canon // primaries. Applied after the conversion it would be a Canon
// rendering acting on sRGB values, which is a different curve. // 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 source = compose(&ops).source;
let op = source.find("---- op_a ----").expect("op present"); let op = source.find("---- op_a ----").expect("op present");
let curve = source let curve = source
@@ -1592,7 +1663,7 @@ mod tests {
// it must not pick up an identity matrix multiply for the sake of // it must not pick up an identity matrix multiply for the sake of
// generality. Asserted against the source rather than against timing, // generality. Asserted against the source rather than against timing,
// which would not fail reliably. // 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; let srgb = compose_to(&ops, ColourSpace::Srgb).source;
assert!( assert!(
!srgb.contains("Linear sRGB -> linear sRGB"), !srgb.contains("Linear sRGB -> linear sRGB"),
@@ -1607,7 +1678,7 @@ mod tests {
// in linear sRGB, the primaries conversion carries it into the wider // in linear sRGB, the primaries conversion carries it into the wider
// space, and only then is it clipped — clipping first would discard // space, and only then is it clipped — clipping first would discard
// exactly the colours the wider space was chosen to keep. // 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 camera = source.find("u.cam_to_srgb_0").expect("camera matrix");
let convert = source let convert = source
.find("Linear sRGB -> linear Display P3") .find("Linear sRGB -> linear Display P3")
@@ -1659,7 +1730,7 @@ mod tests {
// an export that came out sRGB and claimed to be Display P3. // an export that came out sRGB and claimed to be Display P3.
let mut seen: Vec<u64> = Vec::new(); let mut seen: Vec<u64> = Vec::new();
for space in ColourSpace::ALL { 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"); assert!(!seen.contains(&h), "{space:?} collides with another space");
seen.push(h); seen.push(h);
} }
@@ -1669,7 +1740,7 @@ mod tests {
fn generated_source_carries_a_do_not_edit_banner() { fn generated_source_carries_a_do_not_edit_banner() {
// Someone will eventually find this in a debugger and try to fix it // Someone will eventually find this in a debugger and try to fix it
// in place. // 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")); assert!(shader.source.starts_with("// GENERATED"));
} }
+36 -33
View File
@@ -31,6 +31,7 @@
//! untouched means a mis-set correction shifts the channels that contribute //! untouched means a mis-set correction shifts the channels that contribute
//! least to perceived sharpness. Scaling all three about a virtual reference //! least to perceived sharpness. Scaling all three about a virtual reference
//! would soften the image even when the correction is right. //! would soften the image even when the correction is right.
use std::sync::{Arc, LazyLock};
use crate::descriptor::{ use crate::descriptor::{
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit, 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%. /// resolution left to tune by eye at 100%.
const MAX_SCALE: f32 = 0.005; const MAX_SCALE: f32 = 0.005;
static DESCRIPTOR: OpDescriptor = OpDescriptor { static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
attributes: &[Attribute::Optics], Arc::new(OpDescriptor {
id: ID, attributes: vec![Attribute::Optics],
label: LocalizedKey("op.aberration"), id: ID,
params: &[ label: LocalizedKey("op.aberration"),
// Two independent controls rather than one: the red and blue params: vec![
// displacements are caused by different ends of the spectrum and are // Two independent controls rather than one: the red and blue
// not symmetric, so a single "fringing" slider could not remove both. // displacements are caused by different ends of the spectrum and are
ParamDescriptor::scalar( // not symmetric, so a single "fringing" slider could not remove both.
"red", ParamDescriptor::scalar(
"param.aberration.red", "red",
-100.0, "param.aberration.red",
100.0, -100.0,
0.0, 100.0,
Unit::None, 0.0,
Scale::Linear, Unit::None,
0, Scale::Linear,
), 0,
ParamDescriptor::scalar( ),
"blue", ParamDescriptor::scalar(
"param.aberration.blue", "blue",
-100.0, "param.aberration.blue",
100.0, -100.0,
0.0, 100.0,
Unit::None, 0.0,
Scale::Linear, Unit::None,
0, Scale::Linear,
), 0,
], ),
}; ],
})
});
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Aberration { pub struct Aberration {
@@ -113,8 +116,8 @@ impl Aberration {
} }
impl Warp for Aberration { impl Warp for Aberration {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
&DESCRIPTOR DESCRIPTOR.clone()
} }
fn set_param(&mut self, id: ParamId, value: f32) { fn set_param(&mut self, id: ParamId, value: f32) {
@@ -300,7 +303,7 @@ mod tests {
#[test] #[test]
fn every_default_is_neutral() { fn every_default_is_neutral() {
let mut a = Aberration::new(); let mut a = Aberration::new();
for p in DESCRIPTOR.params { for p in &DESCRIPTOR.params {
a.set_param(p.id, p.default); a.set_param(p.id, p.default);
} }
assert!(!a.is_active(), "descriptor defaults must be neutral"); assert!(!a.is_active(), "descriptor defaults must be neutral");
+45 -42
View File
@@ -124,6 +124,7 @@
//! would be a guess dressed as a number; `resolves` is the line the stage //! 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 //! already draws, and drawing it in two places differently is worse than a
//! visible step. //! visible step.
use std::sync::{Arc, LazyLock};
use crate::descriptor::{ use crate::descriptor::{
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit, Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
@@ -163,46 +164,48 @@ const MAX_KERNEL: f32 = 48.0;
/// every editor's capture sharpening starts. /// every editor's capture sharpening starts.
const DEFAULT_RADIUS: f32 = 1.0; const DEFAULT_RADIUS: f32 = 1.0;
static DESCRIPTOR: OpDescriptor = OpDescriptor { static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
id: ID, Arc::new(OpDescriptor {
label: LocalizedKey("op.capture_sharpen"), id: ID,
params: &[ label: LocalizedKey("op.capture_sharpen"),
// Amount carries the neutral, which is why it is first: the operation params: vec![
// is off when this is zero regardless of the other two, so a reset is // Amount carries the neutral, which is why it is first: the operation
// one control and the panel's ordering matches the way it is used. // is off when this is zero regardless of the other two, so a reset is
ParamDescriptor::amount("amount", "param.amount"), // one control and the panel's ordering matches the way it is used.
// In **source pixels** — see the module documentation. Half a photosite ParamDescriptor::amount("amount", "param.amount"),
// is the smallest radius that means anything on a Bayer sensor, and // In **source pixels** — see the module documentation. Half a photosite
// three is already past the point where an unsharp mask is sharpening // is the smallest radius that means anything on a Bayer sensor, and
// rather than adding local contrast; a photographer wanting the latter // three is already past the point where an unsharp mask is sharpening
// wants clarity, which is a different operation with a different unit. // rather than adding local contrast; a photographer wanting the latter
ParamDescriptor::scalar( // wants clarity, which is a different operation with a different unit.
"radius", ParamDescriptor::scalar(
"param.radius", "radius",
0.5, "param.radius",
3.0, 0.5,
DEFAULT_RADIUS, 3.0,
Unit::None, DEFAULT_RADIUS,
Scale::Linear, Unit::None,
2, Scale::Linear,
), 2,
// A fraction, but declared as a scalar rather than through ),
// `ParamDescriptor::fraction` for its precision alone: four decimal // A fraction, but declared as a scalar rather than through
// places on a control whose whole useful travel is a dozen steps // `ParamDescriptor::fraction` for its precision alone: four decimal
// reads as noise, and invites fiddling with digits that do nothing. // places on a control whose whole useful travel is a dozen steps
ParamDescriptor::scalar( // reads as noise, and invites fiddling with digits that do nothing.
"threshold", ParamDescriptor::scalar(
"param.threshold", "threshold",
0.0, "param.threshold",
1.0, 0.0,
0.0, 1.0,
Unit::None, 0.0,
Scale::Linear, Unit::None,
2, Scale::Linear,
), 2,
], ),
attributes: &[Attribute::Detail], ],
}; attributes: vec![Attribute::Detail],
})
});
/// TRACES: FR-DEV-3 /// TRACES: FR-DEV-3
/// Capture sharpening: a separable unsharp mask with a contrast threshold. /// Capture sharpening: a separable unsharp mask with a contrast threshold.
@@ -275,8 +278,8 @@ impl CaptureSharpen {
} }
impl Operation for CaptureSharpen { impl Operation for CaptureSharpen {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
&DESCRIPTOR DESCRIPTOR.clone()
} }
fn set_param(&mut self, id: ParamId, value: f32) { fn set_param(&mut self, id: ParamId, value: f32) {
+31 -27
View File
@@ -29,6 +29,7 @@
//! the band acts at full strength right up to a hard edge; and with two bands //! 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 //! 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. //! up one colour's saturation quietly weakened its neighbour's hue shift.
use std::sync::{Arc, LazyLock};
use crate::descriptor::{ use crate::descriptor::{
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId,
@@ -123,8 +124,9 @@ impl Channel {
} }
// Parameter descriptors, one per band per channel. Written out rather than // Parameter descriptors, one per band per channel. Written out rather than
// generated because `ParamDescriptor` must be `const` to live in a `static`, // looped because `concat!` needs literals: every id is built from its band's
// and a const loop cannot build a slice. The macro keeps it honest. // 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 // **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 // 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. // §4.3a); this only says the parameter acts on the band centred there.
macro_rules! band_params { macro_rules! band_params {
($(($key:literal, $hue:literal)),* $(,)?) => { ($(($key:literal, $hue:literal)),* $(,)?) => {
&[ vec![
$( $(
ParamDescriptor::amount( ParamDescriptor::amount(
concat!($key, "_hue"), concat!($key, "_hue"),
@@ -175,25 +177,27 @@ macro_rules! band_params {
// the same reason as those: `concat!` needs literals, so the keys and hues // 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 // cannot be read out of `BANDS` here. `facets_match_their_bands` below is
// what keeps them from drifting. // what keeps them from drifting.
static DESCRIPTOR: OpDescriptor = OpDescriptor { static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
attributes: &[Attribute::Colour], Arc::new(OpDescriptor {
id: ID, attributes: vec![Attribute::Colour],
label: LocalizedKey("op.colour_mixer"), id: ID,
params: band_params![ label: LocalizedKey("op.colour_mixer"),
("red", 0.0), params: band_params![
("orange", 30.0), ("red", 0.0),
("yellow", 60.0), ("orange", 30.0),
("chartreuse", 90.0), ("yellow", 60.0),
("green", 120.0), ("chartreuse", 90.0),
("spring", 150.0), ("green", 120.0),
("cyan", 180.0), ("spring", 150.0),
("azure", 210.0), ("cyan", 180.0),
("blue", 240.0), ("azure", 210.0),
("violet", 270.0), ("blue", 240.0),
("magenta", 300.0), ("violet", 270.0),
("rose", 330.0), ("magenta", 300.0),
], ("rose", 330.0),
}; ],
})
});
static MIXER_HELPERS: &[Helper] = &[ static MIXER_HELPERS: &[Helper] = &[
helpers::LUMINANCE, helpers::LUMINANCE,
@@ -306,8 +310,8 @@ impl ColourMixer {
} }
impl Operation for ColourMixer { impl Operation for ColourMixer {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
&DESCRIPTOR DESCRIPTOR.clone()
} }
fn set_param(&mut self, id: ParamId, value: f32) { 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() { fn every_descriptor_id_resolves_to_a_band_and_channel() {
// The link between the descriptor list and the value array. A // The link between the descriptor list and the value array. A
// mismatch would make a slider silently adjust nothing. // mismatch would make a slider silently adjust nothing.
for p in DESCRIPTOR.params { for p in &DESCRIPTOR.params {
assert!( assert!(
ColourMixer::index_of(p.id).is_some(), ColourMixer::index_of(p.id).is_some(),
"{} does not map to a band", "{} 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 // 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, // band does not act on — a control that lies about what it edits,
// which is worse than one with no swatch at all. // 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 facet = p.facet.expect("every mixer parameter is faceted");
let (band_key, _) = p.id.0.rsplit_once('_').expect("id is band_channel"); let (band_key, _) = p.id.0.rsplit_once('_').expect("id is band_channel");
let band = BANDS let band = BANDS
@@ -577,7 +581,7 @@ mod tests {
// the aspect keyed per band, grouping by it would produce thirty-six // the aspect keyed per band, grouping by it would produce thirty-six
// groups of one and nothing would have been gained. // groups of one and nothing would have been gained.
let mut per_aspect = std::collections::BTreeMap::new(); let mut per_aspect = std::collections::BTreeMap::new();
for p in DESCRIPTOR.params { for p in &DESCRIPTOR.params {
let facet = p.facet.expect("faceted"); let facet = p.facet.expect("faceted");
*per_aspect.entry(facet.aspect.0).or_insert(0) += 1; *per_aspect.entry(facet.aspect.0).or_insert(0) += 1;
} }
+34 -30
View File
@@ -79,6 +79,7 @@
//! whole composition scheme rests on (ARCH §5.6). //! whole composition scheme rests on (ARCH §5.6).
use std::fmt::Write as _; use std::fmt::Write as _;
use std::sync::{Arc, LazyLock};
use crate::descriptor::{ use crate::descriptor::{
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation, 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. /// One channel's ten descriptors, defaulted onto the identity diagonal.
/// ///
/// Written out per point rather than looped because a `ParamDescriptor` has to /// Written out per point rather than looped because `concat!` needs literals:
/// be `const` to live in a `static`, and a const loop cannot build a slice. /// 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 { macro_rules! channel_params {
($(($prefix:literal, $channel:expr)),* $(,)?) => { ($(($prefix:literal, $channel:expr)),* $(,)?) => {
&[$( vec![$(
coord(concat!($prefix, "p0_x"), "param.curve.p0_x", 0.0) coord(concat!($prefix, "p0_x"), "param.curve.p0_x", 0.0)
.faceted(facet_of("param.curve.p0_x", $channel)), .faceted(facet_of("param.curve.p0_x", $channel)),
coord(concat!($prefix, "p0_y"), "param.curve.p0_y", 0.0) coord(concat!($prefix, "p0_y"), "param.curve.p0_y", 0.0)
@@ -370,26 +372,28 @@ macro_rules! channel_params {
}; };
} }
static DESCRIPTOR: OpDescriptor = OpDescriptor { static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
// Both, and this is the case the plural exists for: the master curve is Arc::new(OpDescriptor {
// tonal and the per-channel curves are chromatic. Filing it under one // Both, and this is the case the plural exists for: the master curve is
// would hide it from half the people looking for it. // tonal and the per-channel curves are chromatic. Filing it under one
attributes: &[Attribute::Tone, Attribute::Colour], // would hide it from half the people looking for it.
id: ID, attributes: vec![Attribute::Tone, Attribute::Colour],
label: LocalizedKey("op.tone_curve"), id: ID,
// Defaults lie on y = x, so a fresh curve is the identity and the label: LocalizedKey("op.tone_curve"),
// operation reports itself inactive — on every channel. // 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, // The master's ten come first, and stay first: a frontend addresses a
// and this is also the order one falling back to sliders reads them in. // point by its offset from the first parameter of the run it is drawing,
params: channel_params![ // and this is also the order one falling back to sliders reads them in.
("", Channel::Master), params: channel_params![
("r_", Channel::Red), ("", Channel::Master),
("g_", Channel::Green), ("r_", Channel::Red),
("b_", Channel::Blue), ("g_", Channel::Green),
], ("b_", Channel::Blue),
}; ],
})
});
/// One span of a monotone cubic Hermite spline. Shared by all four curves. /// One span of a monotone cubic Hermite spline. Shared by all four curves.
const CURVE_SPAN: Helper = Helper { const CURVE_SPAN: Helper = Helper {
@@ -699,8 +703,8 @@ impl ToneCurve {
} }
impl Operation for ToneCurve { impl Operation for ToneCurve {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
&DESCRIPTOR DESCRIPTOR.clone()
} }
fn set_param(&mut self, id: ParamId, value: f32) { fn set_param(&mut self, id: ParamId, value: f32) {
@@ -727,7 +731,7 @@ impl Operation for ToneCurve {
Some(Presentation { Some(Presentation {
// One entry: there is no second way to draw a tone curve that is // One entry: there is no second way to draw a tone curve that is
// better than the sliders the frontend falls back to anyway. // better than the sliders the frontend falls back to anyway.
widgets: &[WidgetKind::ToneCurve], widgets: vec![WidgetKind::ToneCurve],
demand: WidgetDemand { demand: WidgetDemand {
// A point is dragged in x and y together — that is what a // 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 // 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; // 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 // 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. // 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. // Opening an unedited image must show the image.
let c = ToneCurve::new(); let c = ToneCurve::new();
assert!(!c.is_active()); assert!(!c.is_active());
for p in DESCRIPTOR.params { for p in &DESCRIPTOR.params {
assert_eq!(c.param(p.id), p.default); assert_eq!(c.param(p.id), p.default);
} }
} }
@@ -927,7 +931,7 @@ mod tests {
#[test] #[test]
fn every_parameter_id_maps_to_a_point() { fn every_parameter_id_maps_to_a_point() {
for p in DESCRIPTOR.params { for p in &DESCRIPTOR.params {
assert!( assert!(
ToneCurve::index_of(p.id).is_some(), ToneCurve::index_of(p.id).is_some(),
"{} does not map to a point", "{} does not map to a point",
@@ -1196,7 +1200,7 @@ mod tests {
); );
assert_eq!(presentation.choose(|_| false), None); assert_eq!(presentation.choose(|_| false), None);
assert_eq!(presentation.params.len(), DESCRIPTOR.params.len()); assert_eq!(presentation.params.len(), DESCRIPTOR.params.len());
for p in DESCRIPTOR.params { for p in &DESCRIPTOR.params {
assert!( assert!(
presentation.params.contains(&p.id), presentation.params.contains(&p.id),
"{} is not owned by the widget", "{} is not owned by the widget",
+24 -21
View File
@@ -25,6 +25,7 @@
//! set three correlated coefficients, and hand-correcting a lens with no //! set three correlated coefficients, and hand-correcting a lens with no
//! profile is a "make the horizon straight" task, which one term does well. //! profile is a "make the horizon straight" task, which one term does well.
//! The full triple is reachable by loading a profile. //! The full triple is reachable by loading a profile.
use std::sync::{Arc, LazyLock};
use crate::descriptor::{ use crate::descriptor::{
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit, 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 ID: OpId = OpId("distortion");
pub const AMOUNT: ParamId = ParamId("amount"); pub const AMOUNT: ParamId = ParamId("amount");
static DESCRIPTOR: OpDescriptor = OpDescriptor { static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
attributes: &[Attribute::Optics], Arc::new(OpDescriptor {
id: ID, attributes: vec![Attribute::Optics],
label: LocalizedKey("op.distortion"), id: ID,
// ±100 maps to a ±0.25 cubic coefficient. That covers an uncorrected label: LocalizedKey("op.distortion"),
// fisheye at one end and strong pincushion at the other; beyond it the // ±100 maps to a ±0.25 cubic coefficient. That covers an uncorrected
// inverse mapping stops being single-valued near the corners and the // fisheye at one end and strong pincushion at the other; beyond it the
// correction folds the image over itself. // inverse mapping stops being single-valued near the corners and the
params: &[ParamDescriptor::scalar( // correction folds the image over itself.
"amount", params: vec![ParamDescriptor::scalar(
"param.distortion.amount", "amount",
-100.0, "param.distortion.amount",
100.0, -100.0,
0.0, 100.0,
Unit::None, 0.0,
Scale::Linear, Unit::None,
0, Scale::Linear,
)], 0,
}; )],
})
});
/// The cubic coefficient at full slider travel. /// The cubic coefficient at full slider travel.
const MAX_COEFF: f32 = 0.25; const MAX_COEFF: f32 = 0.25;
@@ -108,8 +111,8 @@ impl Distortion {
} }
impl Warp for Distortion { impl Warp for Distortion {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
&DESCRIPTOR DESCRIPTOR.clone()
} }
fn set_param(&mut self, id: ParamId, value: f32) { fn set_param(&mut self, id: ParamId, value: f32) {
+28 -25
View File
@@ -29,6 +29,7 @@
//! Declared as a plain struct here rather than imported, so that dr-pipeline //! 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 //! keeps its no-dependency property (ARCH §6.5a) exactly as `vignetting` does
//! with `Pa`. //! with `Pa`.
use std::sync::{Arc, LazyLock};
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId}; use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
use crate::operation::{Operation, Uniform}; use crate::operation::{Operation, Uniform};
@@ -73,29 +74,31 @@ static MATRIX_FIELDS: [[&str; 3]; 3] = [
["m20", "m21", "m22"], ["m20", "m21", "m22"],
]; ];
static DESCRIPTOR: OpDescriptor = OpDescriptor { static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
// Tone and colour both, and not `Effect`: a stock is not something applied Arc::new(OpDescriptor {
// on top of a photograph, it is what the photograph was made on. // Tone and colour both, and not `Effect`: a stock is not something applied
attributes: &[Attribute::Tone, Attribute::Colour], // on top of a photograph, it is what the photograph was made on.
id: ID, attributes: vec![Attribute::Tone, Attribute::Colour],
label: LocalizedKey("op.film_sim"), id: ID,
params: &[ label: LocalizedKey("op.film_sim"),
ParamDescriptor::stops("exposure", "param.film_sim.exposure", -3.0, 3.0), params: vec![
ParamDescriptor::stops("print_exposure", "param.film_sim.print_exposure", -3.0, 3.0), ParamDescriptor::stops("exposure", "param.film_sim.exposure", -3.0, 3.0),
// TRACES: FR-DEV-3f ParamDescriptor::stops("print_exposure", "param.film_sim.print_exposure", -3.0, 3.0),
// Development, in stops of push. Bounded by what the manufacturers // TRACES: FR-DEV-3f
// actually published: Double-X's measured axis spans about -1 to +2, // Development, in stops of push. Bounded by what the manufacturers
// and beyond a range like that a curve would have to be invented. // actually published: Double-X's measured axis spans about -1 to +2,
ParamDescriptor::stops("push", "param.film_sim.push", -1.0, 3.0), // and beyond a range like that a curve would have to be invented.
// TRACES: FR-DEV-3f ParamDescriptor::stops("push", "param.film_sim.push", -1.0, 3.0),
// Which frame this was taken on — the half of the enlargement a // TRACES: FR-DEV-3f
// photograph cannot supply. A crystal is a fixed size in micrometres, // Which frame this was taken on — the half of the enlargement a
// so how grainy a picture looks is film size against output size, and // photograph cannot supply. A crystal is a fixed size in micrometres,
// the same emulsion on 4x5 renders about three times smoother than on // so how grainy a picture looks is film size against output size, and
// 35mm at the same print. // the same emulsion on 4x5 renders about three times smoother than on
ParamDescriptor::choice("format", "param.film_sim.format", &FORMATS), // 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. /// A stock reduced to what a shader runs, as `dr-film` bakes it.
/// ///
@@ -190,8 +193,8 @@ impl FilmSim {
} }
impl Operation for FilmSim { impl Operation for FilmSim {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
&DESCRIPTOR DESCRIPTOR.clone()
} }
fn set_param(&mut self, id: ParamId, value: f32) { fn set_param(&mut self, id: ParamId, value: f32) {
+25 -15
View File
@@ -153,6 +153,7 @@
//! this file. //! this file.
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::{Arc, LazyLock};
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId}; use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
use crate::detail::{DetailPass, DetailStage, RenderScale}; 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 /// between clarity and texture can be read side by side, which is the one
/// thing a reader comes to this file to do. /// thing a reader comes to this file to do.
pub struct Recipe { 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<Arc<OpDescriptor>>,
helpers: &'static [Helper], helpers: &'static [Helper],
/// The Gaussian's σ, as a fraction of the frame's shorter edge. /// The Gaussian's σ, as a fraction of the frame's shorter edge.
sigma: f32, sigma: f32,
@@ -253,19 +259,23 @@ impl Band for Fine {
}; };
} }
static CLARITY_DESCRIPTOR: OpDescriptor = OpDescriptor { static CLARITY_DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
id: CLARITY, Arc::new(OpDescriptor {
label: LocalizedKey("op.clarity"), id: CLARITY,
params: &[ParamDescriptor::amount("amount", "param.clarity.amount")], label: LocalizedKey("op.clarity"),
attributes: &[Attribute::Detail], params: vec![ParamDescriptor::amount("amount", "param.clarity.amount")],
}; attributes: vec![Attribute::Detail],
})
});
static TEXTURE_DESCRIPTOR: OpDescriptor = OpDescriptor { static TEXTURE_DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
id: TEXTURE, Arc::new(OpDescriptor {
label: LocalizedKey("op.texture"), id: TEXTURE,
params: &[ParamDescriptor::amount("amount", "param.texture.amount")], label: LocalizedKey("op.texture"),
attributes: &[Attribute::Detail], params: vec![ParamDescriptor::amount("amount", "param.texture.amount")],
}; attributes: vec![Attribute::Detail],
})
});
/// Luminance as a position on a logarithmic scale, floored. /// Luminance as a position on a logarithmic scale, floored.
/// ///
@@ -394,8 +404,8 @@ impl<B: Band> LocalContrast<B> {
} }
impl<B: Band> Operation for LocalContrast<B> { impl<B: Band> Operation for LocalContrast<B> {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
B::RECIPE.descriptor Arc::clone(B::RECIPE.descriptor)
} }
fn set_param(&mut self, _id: ParamId, value: f32) { fn set_param(&mut self, _id: ParamId, value: f32) {
+1 -1
View File
@@ -179,7 +179,7 @@ mod tests {
// perfectly and silently breaks the sidecar. // perfectly and silently breaks the sidecar.
for mut op in chain() { for mut op in chain() {
let descriptor = op.descriptor(); let descriptor = op.descriptor();
for p in descriptor.params { for p in &descriptor.params {
let crate::descriptor::ParamKind::Scalar { min, max, .. } = p.kind else { let crate::descriptor::ParamKind::Scalar { min, max, .. } = p.kind else {
continue; continue;
}; };
+37 -34
View File
@@ -147,6 +147,7 @@
//! lie. The chroma radius, ten times larger, still resolves — which is also //! 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 //! true of the fault it treats, since a blotch twenty pixels across survives
//! being halved. //! being halved.
use std::sync::{Arc, LazyLock};
use crate::descriptor::{ use crate::descriptor::{
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit, 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 LUMINANCE: ParamId = ParamId("luminance");
pub const CHROMA: ParamId = ParamId("chroma"); pub const CHROMA: ParamId = ParamId("chroma");
static DESCRIPTOR: OpDescriptor = OpDescriptor { static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
id: ID, Arc::new(OpDescriptor {
label: LocalizedKey("op.noise_reduction"), id: ID,
attributes: &[Attribute::Detail], label: LocalizedKey("op.noise_reduction"),
// Zero to a hundred rather than the symmetric `amount` shape the tonal attributes: vec![Attribute::Detail],
// controls use. There is no meaningful negative: "minus fifty noise // Zero to a hundred rather than the symmetric `amount` shape the tonal
// reduction" would be adding grain, which is a look rather than a repair // controls use. There is no meaningful negative: "minus fifty noise
// and belongs to a different operation carrying `Attribute::Effect`. A // reduction" would be adding grain, which is a look rather than a repair
// control whose left half does nothing is worse than one that stops. // and belongs to a different operation carrying `Attribute::Effect`. A
params: &[ // control whose left half does nothing is worse than one that stops.
ParamDescriptor::scalar( params: vec![
"luminance", ParamDescriptor::scalar(
"param.noise_reduction.luminance", "luminance",
0.0, "param.noise_reduction.luminance",
100.0, 0.0,
0.0, 100.0,
Unit::None, 0.0,
Scale::Linear, Unit::None,
0, Scale::Linear,
), 0,
ParamDescriptor::scalar( ),
"chroma", ParamDescriptor::scalar(
"param.noise_reduction.chroma", "chroma",
0.0, "param.noise_reduction.chroma",
100.0, 0.0,
0.0, 100.0,
Unit::None, 0.0,
Scale::Linear, Unit::None,
0, Scale::Linear,
), 0,
], ),
}; ],
})
});
/// The luminance radius at the lowest and the highest amount, in **source** /// The luminance radius at the lowest and the highest amount, in **source**
/// pixels. /// pixels.
@@ -365,8 +368,8 @@ fn inv_spatial(kernel: u32) -> f32 {
} }
impl Operation for NoiseReduction { impl Operation for NoiseReduction {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
&DESCRIPTOR DESCRIPTOR.clone()
} }
fn set_param(&mut self, id: ParamId, value: f32) { fn set_param(&mut self, id: ParamId, value: f32) {
+19 -16
View File
@@ -32,6 +32,7 @@
//! division is the whole reason this operation must run before the tonal //! 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 //! stages: a corner recovered by two stops has to be recovered while the
//! highlight headroom to hold it still exists (ARCH §5.2). //! 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::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
use crate::operation::{Helper, Operation, Uniform}; 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. /// fast prime wide open — the case that actually needs correcting.
const MAX_K1: f32 = -0.5; const MAX_K1: f32 = -0.5;
static DESCRIPTOR: OpDescriptor = OpDescriptor { static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
// Optics rather than effect: this carries lens-profile coefficients Arc::new(OpDescriptor {
// and corrects what the lens did. A *creative* vignette is a different // Optics rather than effect: this carries lens-profile coefficients
// operation that does not exist yet, and would be `Effect`. // and corrects what the lens did. A *creative* vignette is a different
attributes: &[Attribute::Optics], // operation that does not exist yet, and would be `Effect`.
id: ID, attributes: vec![Attribute::Optics],
label: LocalizedKey("op.vignetting"), id: ID,
// Bidirectional deliberately. Negative values *add* falloff, which is a label: LocalizedKey("op.vignetting"),
// legitimate creative choice as well as a correction, and a control that // Bidirectional deliberately. Negative values *add* falloff, which is a
// only removed vignetting would need a second one beside it to put any // legitimate creative choice as well as a correction, and a control that
// back. // only removed vignetting would need a second one beside it to put any
params: &[ParamDescriptor::amount("amount", "param.vignetting.amount")], // back.
}; params: vec![ParamDescriptor::amount("amount", "param.vignetting.amount")],
})
});
/// The `pa` polynomial coefficients, as Lensfun stores them. /// The `pa` polynomial coefficients, as Lensfun stores them.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
@@ -107,8 +110,8 @@ impl Vignetting {
} }
impl Operation for Vignetting { impl Operation for Vignetting {
fn descriptor(&self) -> &'static OpDescriptor { fn descriptor(&self) -> Arc<OpDescriptor> {
&DESCRIPTOR DESCRIPTOR.clone()
} }
fn set_param(&mut self, id: ParamId, value: f32) { fn set_param(&mut self, id: ParamId, value: f32) {
@@ -371,7 +374,7 @@ mod tests {
#[test] #[test]
fn every_default_is_neutral() { fn every_default_is_neutral() {
let mut v = Vignetting::new(); let mut v = Vignetting::new();
for p in DESCRIPTOR.params { for p in &DESCRIPTOR.params {
v.set_param(p.id, p.default); v.set_param(p.id, p.default);
} }
assert!(!v.is_active()); assert!(!v.is_active());
+11 -2
View File
@@ -1297,8 +1297,17 @@ impl PartialMask {
.ops .ops
.iter() .iter()
.find(|o| o.descriptor().id.0 == op) .find(|o| o.descriptor().id.0 == op)
.and_then(|o| o.descriptor().params.iter().find(|p| p.id.0 == param)) // The descriptor is bound inside the closure rather than
.map(|p| p.id) // 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 { else {
log::warn!("sidecar: unknown mask parameter {op}.{param}; ignoring"); log::warn!("sidecar: unknown mask parameter {op}.{param}; ignoring");
continue; continue;
+435
View File
@@ -0,0 +1,435 @@
//! TRACES: FR-PLG-2
//! The generated path and the interpreted path produce the same operation.
//!
//! # Why this test is the point
//!
//! 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. Two
//! implementations sit behind that claim: `build.rs` compiles `ops/*.yaml`
//! into Rust, and [`DeclaredOp`] interprets the same declaration at run time.
//!
//! **If the two ever disagree, the claim fails quietly.** A plugin would be a
//! second-class kind of node — one whose colours come out fractionally
//! different, or whose fragment lands in the shader with a different comment,
//! or whose uniform arrives in a different slot — and nothing would say so.
//! The photographer would see a look they could not reproduce with a built-in
//! and would have no way to find out why.
//!
//! So this parses every built-in declaration at run time and asserts the
//! composed WGSL is **byte for byte** what the generated implementation
//! produces, with the uniform block bit for bit identical, at a spread of
//! parameter values.
//!
//! # Byte-for-byte, and bit-for-bit, on purpose
//!
//! Not "equivalent", not "within an epsilon". A tolerance is where a real
//! divergence hides: the arithmetic in a declaration is `f32` at both ends and
//! there is no reason for a single bit to differ, so any difference at all is
//! a bug in one of the two backends and should read as one. The one place
//! this bites is number literals, which is why `expr::as_f32` rounds a decimal
//! exactly once — see its own documentation.
//!
//! # What is not covered, and why that is honest
//!
//! A `rust:` node — `tone_curve`, `colour_mixer`, `film_sim`,
//! `capture_sharpen`, `noise_reduction`, `clarity`, `texture` — names a
//! hand-written type and has no declaration to interpret. It is not skipped
//! silently: [`every_declared_node_is_checked`] asserts the two sets partition
//! `ops/` between them, so a node that stops being declared cannot quietly
//! drop out of this file's coverage.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use dr_pipeline::declared::{builtin_helpers, decl, DeclaredOp, Node};
use dr_pipeline::descriptor::ParamKind;
use dr_pipeline::operation::{compose, ComposedShader, Operation};
use dr_pipeline::ops;
use dr_pipeline::ParamId;
/// The declarations this crate ships, read from disk rather than embedded.
///
/// From disk deliberately: `build.rs` reads these very files, so reading the
/// same bytes is what makes the comparison a comparison of the two *readers*
/// rather than of two snapshots that were taken at different times.
fn ops_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("ops")
}
/// Every `<id>.yaml` in `ops/`, keyed by id, excluding `_helpers.yaml`.
fn declaration_files() -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
for entry in std::fs::read_dir(ops_dir()).expect("ops/ is readable") {
let path = entry.expect("a directory entry").path();
if path.extension().and_then(|e| e.to_str()) != Some("yaml") {
continue;
}
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.expect("a file name")
.to_string();
if stem.starts_with('_') {
continue;
}
out.insert(stem, std::fs::read_to_string(&path).expect("readable"));
}
assert!(!out.is_empty(), "ops/ declares no nodes");
out
}
/// Parse one declaration the way both backends do.
fn read(id: &str, text: &str) -> Node {
let library = builtin_helpers();
decl::read_node(text, &format!("ops/{id}.yaml"), &library.names())
.unwrap_or_else(|e| panic!("ops/{id}.yaml: {e}"))
}
/// The generated implementation of one node, taken out of the default chain.
///
/// Out of `chain()` rather than constructed by name, because that is how the
/// application gets one: if the two ever differed, the chain's version is the
/// one a photograph would be developed with.
fn generated(id: &str) -> Box<dyn Operation> {
let chain = ops::chain();
let index = chain
.iter()
.position(|o| o.descriptor().id.0 == id)
.unwrap_or_else(|| panic!("`{id}` is not in the default chain"));
let mut chain = chain;
chain.remove(index)
}
/// Values worth setting a parameter to, spanning its declared range.
///
/// Both ends, because that is where a rounding difference in a `min:` or a
/// `max:` would show; the default, because that is the neutral every
/// `is_active` is written against; and two interior points that are not
/// round numbers, because a value like 37.5 exercises the arithmetic in a way
/// 0 and 100 do not.
fn probe_values(kind: &ParamKind, default: f32) -> Vec<f32> {
match kind {
ParamKind::Scalar { min, max, .. } => {
let span = max - min;
vec![
default,
*min,
*max,
min + span * 0.375,
min + span * 0.8125,
// A value that is not representable as a short decimal, to
// catch a backend that round-trips a uniform through text.
min + span / 3.0,
]
}
ParamKind::Bool => vec![0.0, 1.0],
ParamKind::Enum { variants } => (0..variants.len()).map(|i| i as f32).collect(),
}
}
/// Put every parameter back where it started.
fn reset(op: &mut dyn Operation) {
let descriptor = op.descriptor();
for p in &descriptor.params {
op.set_param(p.id, p.default);
}
}
/// Assert the two operations are indistinguishable at their current settings.
fn assert_same(id: &str, setting: &str, generated: &dyn Operation, declared: &dyn Operation) {
let g = generated.descriptor();
let d = declared.descriptor();
assert_eq!(*g, *d, "{id} [{setting}]: the descriptors differ");
assert_eq!(
generated.is_active(),
declared.is_active(),
"{id} [{setting}]: the two disagree about whether the operation is doing anything"
);
assert_eq!(
generated.wgsl_body(),
declared.wgsl_body(),
"{id} [{setting}]: the fragment bodies differ"
);
assert_eq!(
generated.helpers(),
declared.helpers(),
"{id} [{setting}]: the helper sets differ"
);
let (gu, du) = (generated.uniforms(), declared.uniforms());
assert_eq!(
gu.len(),
du.len(),
"{id} [{setting}]: different numbers of uniforms"
);
for (a, b) in gu.iter().zip(&du) {
assert_eq!(
a.name, b.name,
"{id} [{setting}]: uniforms in a different order"
);
// Bits, not values: `assert_eq!` on `f32` would call two NaNs unequal
// and would call `0.0` and `-0.0` equal, and both of those are
// differences worth failing on.
assert_eq!(
a.value.to_bits(),
b.value.to_bits(),
"{id} [{setting}]: uniform `{}` is {} generated and {} declared",
a.name,
a.value,
b.value
);
}
}
/// Assert two compositions are the same shader.
fn assert_same_shader(what: &str, a: &ComposedShader, b: &ComposedShader) {
// The source first, and compared as whole strings: a diff in the middle of
// several kilobytes of WGSL is unreadable as an assertion message, so the
// failure below points at the first differing line instead.
if a.source != b.source {
let line = a
.source
.lines()
.zip(b.source.lines())
.position(|(x, y)| x != y);
match line {
Some(n) => panic!(
"{what}: the composed WGSL differs at line {}:\n generated: {:?}\n declared: {:?}",
n + 1,
a.source.lines().nth(n).unwrap_or(""),
b.source.lines().nth(n).unwrap_or(""),
),
None => panic!(
"{what}: the composed WGSL differs in length: {} generated, {} declared",
a.source.len(),
b.source.len()
),
}
}
assert_eq!(
a.uniforms.len(),
b.uniforms.len(),
"{what}: different uniform block sizes"
);
for (i, (x, y)) in a.uniforms.iter().zip(&b.uniforms).enumerate() {
assert_eq!(
x.to_bits(),
y.to_bits(),
"{what}: uniform slot {i} is {x} generated and {y} declared"
);
}
// The hash is taken over the source, so it follows — but it is what the
// pipeline cache keys on, and asserting it says that a declared node and
// its generated twin would share a compiled pipeline rather than quietly
// splitting the cache in two.
assert_eq!(a.structure_hash, b.structure_hash, "{what}: structure hash");
assert_eq!(a.output_mode, b.output_mode, "{what}: output mode");
}
/// Compose a single operation, the way the display path composes a chain.
fn compose_one(op: Box<dyn Operation>) -> (ComposedShader, Box<dyn Operation>) {
let shader = compose(std::slice::from_ref(&op));
(shader, op)
}
/// TRACES: FR-PLG-2
/// Every declared built-in composes to the same shader either way.
#[test]
fn a_declaration_read_at_run_time_composes_byte_for_byte_as_the_generated_one() {
let library = builtin_helpers();
let mut checked = 0;
for (id, text) in declaration_files() {
let Node::Declared(declaration) = read(&id, &text) else {
continue;
};
let mut declared =
DeclaredOp::new(&declaration, library).unwrap_or_else(|e| panic!("ops/{id}.yaml: {e}"));
let mut generated = generated(&id);
// The neutral state first: it is the state every image starts in, and
// an operation that is inactive in one path and active in the other
// would put a whole fragment into one shader and not the other.
assert_same(&id, "neutral", generated.as_ref(), &declared);
let descriptor = declared.descriptor();
for p in &descriptor.params {
for value in probe_values(&p.kind, p.default) {
reset(generated.as_mut());
reset(&mut declared);
generated.set_param(p.id, value);
declared.set_param(p.id, value);
let setting = format!("{} = {value}", p.id);
assert_same(&id, &setting, generated.as_ref(), &declared);
let (gs, back) = compose_one(generated);
generated = back;
let (ds, _) = compose_one(Box::new(declared.clone()));
assert_same_shader(&format!("{id} [{setting}]"), &gs, &ds);
checked += 1;
}
}
// And every parameter moved at once, which is the only case that
// exercises the *order* uniforms are emitted in.
reset(generated.as_mut());
reset(&mut declared);
for p in &descriptor.params {
let value =
probe_values(&p.kind, p.default)[3.min(probe_values(&p.kind, p.default).len() - 1)];
generated.set_param(p.id, value);
declared.set_param(p.id, value);
}
assert_same(&id, "all parameters moved", generated.as_ref(), &declared);
let (gs, _) = compose_one(generated);
let (ds, _) = compose_one(Box::new(declared));
assert_same_shader(&format!("{id} [all parameters moved]"), &gs, &ds);
checked += 1;
}
// A test that silently checked nothing would pass forever. There are eight
// declared nodes and several settings each, so this is a floor rather than
// a count anybody has to maintain.
assert!(
checked > 20,
"only {checked} comparisons ran; the declarations were not found"
);
}
/// TRACES: FR-PLG-2
/// The whole chain composes identically with the declared nodes swapped in.
///
/// The single-operation test above is the sharper one — it isolates each node
/// — but it cannot see an interaction. This composes the *default develop
/// chain*, with every declared node replaced by its interpreted twin and the
/// `rust:` nodes left alone, so it covers uniform slot ordering across
/// operations, helper de-duplication between them, and the order the fragments
/// land in the shader.
#[test]
fn the_whole_chain_composes_identically_with_interpreted_nodes() {
let library = builtin_helpers();
let files = declaration_files();
let mut generated_chain = ops::chain();
let mut declared_chain = ops::chain();
let mut swapped = 0;
for i in 0..declared_chain.len() {
let id = declared_chain[i].descriptor().id.0.to_string();
let text = files
.get(&id)
.unwrap_or_else(|| panic!("`{id}` is in the chain but has no ops/{id}.yaml"));
if let Node::Declared(declaration) = read(&id, text) {
declared_chain[i] = Box::new(
DeclaredOp::new(&declaration, library)
.unwrap_or_else(|e| panic!("ops/{id}.yaml: {e}")),
);
swapped += 1;
}
// Move every operation off neutral, declared or not, so the chain is
// not a list of fragments that were all omitted. A neutral chain
// composes to a shader with no operation blocks in it at all, which
// would make this test pass while asserting nothing.
let descriptor = generated_chain[i].descriptor();
for p in &descriptor.params {
let value = probe_values(&p.kind, p.default)[1];
generated_chain[i].set_param(p.id, value);
declared_chain[i].set_param(p.id, value);
}
}
assert!(
swapped >= 8,
"only {swapped} nodes were swapped for declared ones"
);
assert_same_shader(
"the default chain",
&compose(&generated_chain),
&compose(&declared_chain),
);
}
/// TRACES: FR-PLG-2
/// Nothing in `ops/` escapes this file unnoticed.
///
/// The coverage guard. A declaration that stopped parsing, or a node that
/// quietly became `rust:`, would otherwise reduce what the parity test covers
/// without anything failing — which is exactly the silent divergence the whole
/// file exists to prevent.
#[test]
fn every_declared_node_is_checked() {
let files = declaration_files();
let mut declared = Vec::new();
let mut hand_written = Vec::new();
for (id, text) in &files {
match read(id, text) {
Node::Declared(_) => declared.push(id.clone()),
Node::Rust { ty, .. } => hand_written.push((id.clone(), ty)),
}
}
// Every file is one or the other, and the chain holds exactly them.
assert_eq!(declared.len() + hand_written.len(), files.len());
assert_eq!(
ops::DECLARED_IDS.len(),
files.len(),
"the chain and ops/ hold different numbers of nodes"
);
for id in ops::DECLARED_IDS {
assert!(
files.contains_key(*id),
"`{id}` is in the chain but not in ops/"
);
}
// Named rather than counted, so that a node changing sides is a failure
// somebody reads rather than a number they update.
let hand: Vec<&str> = hand_written.iter().map(|(id, _)| id.as_str()).collect();
assert_eq!(
hand,
[
"capture_sharpen",
"clarity",
"colour_mixer",
"film_sim",
"noise_reduction",
"texture",
"tone_curve",
],
"the set of hand-written nodes changed; if that is deliberate, update \
this list and the module documentation above"
);
assert!(
declared.len() >= 8,
"only {} declared nodes: {declared:?}",
declared.len()
);
}
/// TRACES: FR-PLG-2
/// A declared operation is addressed by the ids the generated one uses.
///
/// The practical form of "indistinguishable downstream": the sidecar stores
/// parameters by `(op_id, param_id)` text, so a declared node whose interned
/// ids did not compare equal to the generated constants would load an edit
/// that silently did nothing.
#[test]
fn an_interpreted_node_answers_to_the_generated_parameter_ids() {
let mut declared = DeclaredOp::from_yaml(
&std::fs::read_to_string(ops_dir().join("exposure.yaml")).expect("readable"),
"ops/exposure.yaml",
builtin_helpers(),
)
.expect("exposure is a declaration");
declared.set_param(ops::exposure::EXPOSURE, 1.5);
assert_eq!(declared.param(ParamId("exposure")), 1.5);
assert_eq!(declared.descriptor().id, ops::exposure::ID);
}
+24 -24
View File
@@ -9,17 +9,17 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
| Metric | Value | | Metric | Value |
|---|---| |---|---|
| Source files scanned | 268 | | Source files scanned | 272 |
| TRACES tags found | 770 | | TRACES tags found | 789 |
| Requirements defined | 177 | | Requirements defined | 177 |
| Requirements covered | 104 | | Requirements covered | 106 |
| **Coverage** | **58.8%** (104/177) | | **Coverage** | **59.9%** (106/177) |
### By type ### By type
| Type | Covered | Defined | | Type | Covered | Defined |
|---|---|---| |---|---|---|
| FR | 82 | 122 | | FR | 84 | 122 |
| NFR | 20 | 49 | | NFR | 20 | 49 |
| R | 2 | 6 | | 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:3143`](../ui/dr-ui/src/library.rs#L3143), [`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:492`](../ui/dr-ui/ui/app.slint#L492), [`ui/dr-ui/ui/app.slint:665`](../ui/dr-ui/ui/app.slint#L665), [`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-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:3143`](../ui/dr-ui/src/library.rs#L3143), [`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:492`](../ui/dr-ui/ui/app.slint#L492), [`ui/dr-ui/ui/app.slint:665`](../ui/dr-ui/ui/app.slint#L665), [`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:3389`](../ui/dr-ui/src/library.rs#L3389), [`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:495`](../ui/dr-ui/ui/app.slint#L495), [`ui/dr-ui/ui/app.slint:665`](../ui/dr-ui/ui/app.slint#L665), [`ui/dr-ui/ui/app.slint:904`](../ui/dr-ui/ui/app.slint#L904), [`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:147`](../ui/dr-ui/ui/settings.slint#L147) | | 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:3389`](../ui/dr-ui/src/library.rs#L3389), [`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:495`](../ui/dr-ui/ui/app.slint#L495), [`ui/dr-ui/ui/app.slint:665`](../ui/dr-ui/ui/app.slint#L665), [`ui/dr-ui/ui/app.slint:904`](../ui/dr-ui/ui/app.slint#L904), [`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:147`](../ui/dr-ui/ui/settings.slint#L147) |
| 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:660`](../ui/dr-ui/ui/app.slint#L660), [`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-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:660`](../ui/dr-ui/ui/app.slint#L660), [`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:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:3374`](../ui/dr-ui/src/develop.rs#L3374), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1366`](../ui/dr-ui/src/lib.rs#L1366), [`ui/dr-ui/src/lib.rs:1767`](../ui/dr-ui/src/lib.rs#L1767), [`ui/dr-ui/src/lib.rs:1903`](../ui/dr-ui/src/lib.rs#L1903), [`ui/dr-ui/src/lib.rs:481`](../ui/dr-ui/src/lib.rs#L481), [`ui/dr-ui/src/lib.rs:906`](../ui/dr-ui/src/lib.rs#L906), [`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:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:3374`](../ui/dr-ui/src/develop.rs#L3374), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1366`](../ui/dr-ui/src/lib.rs#L1366), [`ui/dr-ui/src/lib.rs:1767`](../ui/dr-ui/src/lib.rs#L1767), [`ui/dr-ui/src/lib.rs:1903`](../ui/dr-ui/src/lib.rs#L1903), [`ui/dr-ui/src/lib.rs:481`](../ui/dr-ui/src/lib.rs#L481), [`ui/dr-ui/src/lib.rs:906`](../ui/dr-ui/src/lib.rs#L906), [`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:2870`](../ui/dr-ui/src/develop.rs#L2870), [`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:3496`](../ui/dr-ui/src/library.rs#L3496), [`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-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:2870`](../ui/dr-ui/src/develop.rs#L2870), [`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:3496`](../ui/dr-ui/src/library.rs#L3496), [`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-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:1793`](../ui/dr-ui/src/develop.rs#L1793), [`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:1875`](../ui/dr-ui/src/lib.rs#L1875), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs: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:1793`](../ui/dr-ui/src/develop.rs#L1793), [`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:1875`](../ui/dr-ui/src/lib.rs#L1875), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
@@ -56,28 +56,28 @@ _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-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:404`](../ui/dr-ui/ui/settings.slint#L404), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) | | 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:404`](../ui/dr-ui/ui/settings.slint#L404), [`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-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-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: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:1297`](../ui/dr-ui/src/develop.rs#L1297), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1775`](../ui/dr-ui/src/develop.rs#L1775), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:1807`](../ui/dr-ui/src/develop.rs#L1807), [`ui/dr-ui/src/develop.rs:1829`](../ui/dr-ui/src/develop.rs#L1829), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3963`](../ui/dr-ui/src/develop.rs#L3963), [`ui/dr-ui/src/develop.rs:4007`](../ui/dr-ui/src/develop.rs#L4007), [`ui/dr-ui/src/develop.rs:4057`](../ui/dr-ui/src/develop.rs#L4057), [`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:1451`](../ui/dr-ui/src/lib.rs#L1451), [`ui/dr-ui/src/lib.rs:2153`](../ui/dr-ui/src/lib.rs#L2153), [`ui/dr-ui/src/lib.rs:302`](../ui/dr-ui/src/lib.rs#L302), [`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:2118`](../ui/dr-ui/ui/app.slint#L2118), [`ui/dr-ui/ui/app.slint:991`](../ui/dr-ui/ui/app.slint#L991) | | 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:1297`](../ui/dr-ui/src/develop.rs#L1297), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1775`](../ui/dr-ui/src/develop.rs#L1775), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:1807`](../ui/dr-ui/src/develop.rs#L1807), [`ui/dr-ui/src/develop.rs:1829`](../ui/dr-ui/src/develop.rs#L1829), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3963`](../ui/dr-ui/src/develop.rs#L3963), [`ui/dr-ui/src/develop.rs:4007`](../ui/dr-ui/src/develop.rs#L4007), [`ui/dr-ui/src/develop.rs:4057`](../ui/dr-ui/src/develop.rs#L4057), [`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:1451`](../ui/dr-ui/src/lib.rs#L1451), [`ui/dr-ui/src/lib.rs:2153`](../ui/dr-ui/src/lib.rs#L2153), [`ui/dr-ui/src/lib.rs:302`](../ui/dr-ui/src/lib.rs#L302), [`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:2118`](../ui/dr-ui/ui/app.slint#L2118), [`ui/dr-ui/ui/app.slint:991`](../ui/dr-ui/ui/app.slint#L991) |
| 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:1194`](../ui/dr-ui/src/develop.rs#L1194), [`ui/dr-ui/src/lib.rs:596`](../ui/dr-ui/src/lib.rs#L596) | | FR-DEV-3a | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:194`](../core/dr-pipeline/src/descriptor.rs#L194), [`core/dr-pipeline/src/descriptor.rs:234`](../core/dr-pipeline/src/descriptor.rs#L234), [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/descriptor.rs:313`](../core/dr-pipeline/src/descriptor.rs#L313), [`core/dr-pipeline/src/framing.rs: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:1194`](../ui/dr-ui/src/develop.rs#L1194), [`ui/dr-ui/src/lib.rs:596`](../ui/dr-ui/src/lib.rs#L596) |
| 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-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: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:4636`](../ui/dr-ui/src/develop.rs#L4636) | | FR-DEV-3c | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`ui/dr-ui/src/develop.rs:4636`](../ui/dr-ui/src/develop.rs#L4636) |
| 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-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: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-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: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:2885`](../ui/dr-ui/src/develop.rs#L2885), [`ui/dr-ui/src/develop.rs:2902`](../ui/dr-ui/src/develop.rs#L2902), [`ui/dr-ui/src/develop.rs:2914`](../ui/dr-ui/src/develop.rs#L2914), [`ui/dr-ui/src/develop.rs:2952`](../ui/dr-ui/src/develop.rs#L2952), [`ui/dr-ui/src/develop.rs:2961`](../ui/dr-ui/src/develop.rs#L2961), [`ui/dr-ui/src/develop.rs:3064`](../ui/dr-ui/src/develop.rs#L3064), [`ui/dr-ui/src/develop.rs:3382`](../ui/dr-ui/src/develop.rs#L3382), [`ui/dr-ui/src/develop.rs:3397`](../ui/dr-ui/src/develop.rs#L3397), [`ui/dr-ui/src/lib.rs:2127`](../ui/dr-ui/src/lib.rs#L2127), [`ui/dr-ui/src/lib.rs:529`](../ui/dr-ui/src/lib.rs#L529), [`ui/dr-ui/src/lib.rs:587`](../ui/dr-ui/src/lib.rs#L587), [`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:2730`](../ui/dr-ui/ui/app.slint#L2730), [`ui/dr-ui/ui/app.slint:785`](../ui/dr-ui/ui/app.slint#L785) | | 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:2885`](../ui/dr-ui/src/develop.rs#L2885), [`ui/dr-ui/src/develop.rs:2902`](../ui/dr-ui/src/develop.rs#L2902), [`ui/dr-ui/src/develop.rs:2914`](../ui/dr-ui/src/develop.rs#L2914), [`ui/dr-ui/src/develop.rs:2952`](../ui/dr-ui/src/develop.rs#L2952), [`ui/dr-ui/src/develop.rs:2961`](../ui/dr-ui/src/develop.rs#L2961), [`ui/dr-ui/src/develop.rs:3064`](../ui/dr-ui/src/develop.rs#L3064), [`ui/dr-ui/src/develop.rs:3382`](../ui/dr-ui/src/develop.rs#L3382), [`ui/dr-ui/src/develop.rs:3397`](../ui/dr-ui/src/develop.rs#L3397), [`ui/dr-ui/src/lib.rs:2127`](../ui/dr-ui/src/lib.rs#L2127), [`ui/dr-ui/src/lib.rs:529`](../ui/dr-ui/src/lib.rs#L529), [`ui/dr-ui/src/lib.rs:587`](../ui/dr-ui/src/lib.rs#L587), [`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:2730`](../ui/dr-ui/ui/app.slint#L2730), [`ui/dr-ui/ui/app.slint:785`](../ui/dr-ui/ui/app.slint#L785) |
| 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:1992`](../ui/dr-ui/src/develop.rs#L1992), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) | | 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:1992`](../ui/dr-ui/src/develop.rs#L1992), [`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-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:2914`](../ui/dr-ui/src/develop.rs#L2914), [`ui/dr-ui/src/develop.rs:3397`](../ui/dr-ui/src/develop.rs#L3397), [`ui/dr-ui/src/develop.rs:3427`](../ui/dr-ui/src/develop.rs#L3427), [`ui/dr-ui/src/develop.rs:3440`](../ui/dr-ui/src/develop.rs#L3440), [`ui/dr-ui/src/develop.rs:3452`](../ui/dr-ui/src/develop.rs#L3452), [`ui/dr-ui/src/develop.rs:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/develop.rs:3504`](../ui/dr-ui/src/develop.rs#L3504), [`ui/dr-ui/src/develop.rs:3523`](../ui/dr-ui/src/develop.rs#L3523), [`ui/dr-ui/src/develop.rs:3539`](../ui/dr-ui/src/develop.rs#L3539), [`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:1399`](../ui/dr-ui/src/lib.rs#L1399), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:1437`](../ui/dr-ui/src/lib.rs#L1437), [`ui/dr-ui/src/lib.rs:2323`](../ui/dr-ui/src/lib.rs#L2323), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | | FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:2914`](../ui/dr-ui/src/develop.rs#L2914), [`ui/dr-ui/src/develop.rs:3397`](../ui/dr-ui/src/develop.rs#L3397), [`ui/dr-ui/src/develop.rs:3427`](../ui/dr-ui/src/develop.rs#L3427), [`ui/dr-ui/src/develop.rs:3440`](../ui/dr-ui/src/develop.rs#L3440), [`ui/dr-ui/src/develop.rs:3452`](../ui/dr-ui/src/develop.rs#L3452), [`ui/dr-ui/src/develop.rs:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/develop.rs:3504`](../ui/dr-ui/src/develop.rs#L3504), [`ui/dr-ui/src/develop.rs:3523`](../ui/dr-ui/src/develop.rs#L3523), [`ui/dr-ui/src/develop.rs:3539`](../ui/dr-ui/src/develop.rs#L3539), [`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:1399`](../ui/dr-ui/src/lib.rs#L1399), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:1437`](../ui/dr-ui/src/lib.rs#L1437), [`ui/dr-ui/src/lib.rs:2323`](../ui/dr-ui/src/lib.rs#L2323), [`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:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/develop.rs:3360`](../ui/dr-ui/src/develop.rs#L3360), [`ui/dr-ui/src/lib.rs:1366`](../ui/dr-ui/src/lib.rs#L1366), [`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-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:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/develop.rs:3360`](../ui/dr-ui/src/develop.rs#L3360), [`ui/dr-ui/src/lib.rs:1366`](../ui/dr-ui/src/lib.rs#L1366), [`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:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:1437`](../ui/dr-ui/src/lib.rs#L1437), [`ui/dr-ui/src/lib.rs:2323`](../ui/dr-ui/src/lib.rs#L2323), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | | 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:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:1437`](../ui/dr-ui/src/lib.rs#L1437), [`ui/dr-ui/src/lib.rs:2323`](../ui/dr-ui/src/lib.rs#L2323), [`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:2144`](../ui/dr-ui/src/develop.rs#L2144), [`ui/dr-ui/src/develop.rs:2182`](../ui/dr-ui/src/develop.rs#L2182), [`ui/dr-ui/src/develop.rs:2247`](../ui/dr-ui/src/develop.rs#L2247), [`ui/dr-ui/src/develop.rs:2330`](../ui/dr-ui/src/develop.rs#L2330), [`ui/dr-ui/src/develop.rs:2344`](../ui/dr-ui/src/develop.rs#L2344), [`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:1458`](../ui/dr-ui/src/lib.rs#L1458), [`ui/dr-ui/src/lib.rs:2420`](../ui/dr-ui/src/lib.rs#L2420), [`ui/dr-ui/src/lib.rs:307`](../ui/dr-ui/src/lib.rs#L307), [`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:1853`](../ui/dr-ui/ui/app.slint#L1853), [`ui/dr-ui/ui/app.slint:2370`](../ui/dr-ui/ui/app.slint#L2370), [`ui/dr-ui/ui/app.slint:2636`](../ui/dr-ui/ui/app.slint#L2636), [`ui/dr-ui/ui/app.slint:339`](../ui/dr-ui/ui/app.slint#L339), [`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-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:2144`](../ui/dr-ui/src/develop.rs#L2144), [`ui/dr-ui/src/develop.rs:2182`](../ui/dr-ui/src/develop.rs#L2182), [`ui/dr-ui/src/develop.rs:2247`](../ui/dr-ui/src/develop.rs#L2247), [`ui/dr-ui/src/develop.rs:2330`](../ui/dr-ui/src/develop.rs#L2330), [`ui/dr-ui/src/develop.rs:2344`](../ui/dr-ui/src/develop.rs#L2344), [`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:1458`](../ui/dr-ui/src/lib.rs#L1458), [`ui/dr-ui/src/lib.rs:2420`](../ui/dr-ui/src/lib.rs#L2420), [`ui/dr-ui/src/lib.rs:307`](../ui/dr-ui/src/lib.rs#L307), [`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:1853`](../ui/dr-ui/ui/app.slint#L1853), [`ui/dr-ui/ui/app.slint:2370`](../ui/dr-ui/ui/app.slint#L2370), [`ui/dr-ui/ui/app.slint:2636`](../ui/dr-ui/ui/app.slint#L2636), [`ui/dr-ui/ui/app.slint:339`](../ui/dr-ui/ui/app.slint#L339), [`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:2668`](../ui/dr-ui/src/develop.rs#L2668), [`ui/dr-ui/src/develop.rs:3698`](../ui/dr-ui/src/develop.rs#L3698), [`ui/dr-ui/src/develop.rs:4181`](../ui/dr-ui/src/develop.rs#L4181), [`ui/dr-ui/src/develop.rs:4215`](../ui/dr-ui/src/develop.rs#L4215), [`ui/dr-ui/src/lib.rs:71`](../ui/dr-ui/src/lib.rs#L71), [`ui/dr-ui/src/lib.rs:737`](../ui/dr-ui/src/lib.rs#L737), [`ui/dr-ui/src/lib.rs:796`](../ui/dr-ui/src/lib.rs#L796) | | 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:2668`](../ui/dr-ui/src/develop.rs#L2668), [`ui/dr-ui/src/develop.rs:3698`](../ui/dr-ui/src/develop.rs#L3698), [`ui/dr-ui/src/develop.rs:4181`](../ui/dr-ui/src/develop.rs#L4181), [`ui/dr-ui/src/develop.rs:4215`](../ui/dr-ui/src/develop.rs#L4215), [`ui/dr-ui/src/lib.rs:71`](../ui/dr-ui/src/lib.rs#L71), [`ui/dr-ui/src/lib.rs:737`](../ui/dr-ui/src/lib.rs#L737), [`ui/dr-ui/src/lib.rs:796`](../ui/dr-ui/src/lib.rs#L796) |
| FR-DSP-3 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101) | | FR-DSP-3 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101) |
| FR-DSP-5 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101), [`core/dr-gpu/tests/zoom_resolution.rs:135`](../core/dr-gpu/tests/zoom_resolution.rs#L135), [`core/dr-gpu/tests/zoom_resolution.rs:166`](../core/dr-gpu/tests/zoom_resolution.rs#L166), [`core/dr-gpu/tests/zoom_resolution.rs:1`](../core/dr-gpu/tests/zoom_resolution.rs#L1), [`core/dr-gpu/tests/zoom_resolution.rs:216`](../core/dr-gpu/tests/zoom_resolution.rs#L216) | | FR-DSP-5 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101), [`core/dr-gpu/tests/zoom_resolution.rs:135`](../core/dr-gpu/tests/zoom_resolution.rs#L135), [`core/dr-gpu/tests/zoom_resolution.rs:166`](../core/dr-gpu/tests/zoom_resolution.rs#L166), [`core/dr-gpu/tests/zoom_resolution.rs:1`](../core/dr-gpu/tests/zoom_resolution.rs#L1), [`core/dr-gpu/tests/zoom_resolution.rs:216`](../core/dr-gpu/tests/zoom_resolution.rs#L216) |
| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`ui/dr-ui/src/develop.rs:2698`](../ui/dr-ui/src/develop.rs#L2698), [`ui/dr-ui/src/develop.rs:5473`](../ui/dr-ui/src/develop.rs#L5473), [`ui/dr-ui/src/lib.rs:1414`](../ui/dr-ui/src/lib.rs#L1414), [`ui/dr-ui/src/lib.rs:2626`](../ui/dr-ui/src/lib.rs#L2626) | | FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:483`](../core/dr-pipeline/src/operation.rs#L483), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`ui/dr-ui/src/develop.rs:2698`](../ui/dr-ui/src/develop.rs#L2698), [`ui/dr-ui/src/develop.rs:5473`](../ui/dr-ui/src/develop.rs#L5473), [`ui/dr-ui/src/lib.rs:1414`](../ui/dr-ui/src/lib.rs#L1414), [`ui/dr-ui/src/lib.rs:2626`](../ui/dr-ui/src/lib.rs#L2626) |
| 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:2747`](../ui/dr-ui/src/develop.rs#L2747), [`ui/dr-ui/src/develop.rs:5283`](../ui/dr-ui/src/develop.rs#L5283), [`ui/dr-ui/src/develop.rs:5315`](../ui/dr-ui/src/develop.rs#L5315), [`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:1514`](../ui/dr-ui/src/lib.rs#L1514), [`ui/dr-ui/src/lib.rs:297`](../ui/dr-ui/src/lib.rs#L297), [`ui/dr-ui/ui/app.slint:299`](../ui/dr-ui/ui/app.slint#L299), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | | FR-DSP-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:2747`](../ui/dr-ui/src/develop.rs#L2747), [`ui/dr-ui/src/develop.rs:5283`](../ui/dr-ui/src/develop.rs#L5283), [`ui/dr-ui/src/develop.rs:5315`](../ui/dr-ui/src/develop.rs#L5315), [`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:1514`](../ui/dr-ui/src/lib.rs#L1514), [`ui/dr-ui/src/lib.rs:297`](../ui/dr-ui/src/lib.rs#L297), [`ui/dr-ui/ui/app.slint:299`](../ui/dr-ui/ui/app.slint#L299), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) |
| FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:2644`](../ui/dr-ui/src/develop.rs#L2644), [`ui/dr-ui/src/develop.rs:2698`](../ui/dr-ui/src/develop.rs#L2698), [`ui/dr-ui/src/develop.rs:5473`](../ui/dr-ui/src/develop.rs#L5473), [`ui/dr-ui/src/develop.rs:5519`](../ui/dr-ui/src/develop.rs#L5519), [`ui/dr-ui/src/develop.rs:5538`](../ui/dr-ui/src/develop.rs#L5538), [`ui/dr-ui/src/develop.rs:689`](../ui/dr-ui/src/develop.rs#L689), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1388`](../ui/dr-ui/src/lib.rs#L1388), [`ui/dr-ui/src/lib.rs:1414`](../ui/dr-ui/src/lib.rs#L1414), [`ui/dr-ui/src/lib.rs:2603`](../ui/dr-ui/src/lib.rs#L2603), [`ui/dr-ui/src/lib.rs:2626`](../ui/dr-ui/src/lib.rs#L2626), [`ui/dr-ui/ui/app.slint:1713`](../ui/dr-ui/ui/app.slint#L1713), [`ui/dr-ui/ui/app.slint:278`](../ui/dr-ui/ui/app.slint#L278), [`ui/dr-ui/ui/settings.slint:120`](../ui/dr-ui/ui/settings.slint#L120), [`ui/dr-ui/ui/settings.slint:730`](../ui/dr-ui/ui/settings.slint#L730) | | FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:2644`](../ui/dr-ui/src/develop.rs#L2644), [`ui/dr-ui/src/develop.rs:2698`](../ui/dr-ui/src/develop.rs#L2698), [`ui/dr-ui/src/develop.rs:5473`](../ui/dr-ui/src/develop.rs#L5473), [`ui/dr-ui/src/develop.rs:5519`](../ui/dr-ui/src/develop.rs#L5519), [`ui/dr-ui/src/develop.rs:5538`](../ui/dr-ui/src/develop.rs#L5538), [`ui/dr-ui/src/develop.rs:689`](../ui/dr-ui/src/develop.rs#L689), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1388`](../ui/dr-ui/src/lib.rs#L1388), [`ui/dr-ui/src/lib.rs:1414`](../ui/dr-ui/src/lib.rs#L1414), [`ui/dr-ui/src/lib.rs:2603`](../ui/dr-ui/src/lib.rs#L2603), [`ui/dr-ui/src/lib.rs:2626`](../ui/dr-ui/src/lib.rs#L2626), [`ui/dr-ui/ui/app.slint:1713`](../ui/dr-ui/ui/app.slint#L1713), [`ui/dr-ui/ui/app.slint:278`](../ui/dr-ui/ui/app.slint#L278), [`ui/dr-ui/ui/settings.slint:120`](../ui/dr-ui/ui/settings.slint#L120), [`ui/dr-ui/ui/settings.slint:730`](../ui/dr-ui/ui/settings.slint#L730) |
| 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-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/develop.rs:5538`](../ui/dr-ui/src/develop.rs#L5538), [`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/develop.rs:5538`](../ui/dr-ui/src/develop.rs#L5538), [`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-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-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
| FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
@@ -100,11 +100,13 @@ _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:1105`](../ui/dr-ui/src/lib.rs#L1105), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | 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:1105`](../ui/dr-ui/src/lib.rs#L1105), [`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:1105`](../ui/dr-ui/src/lib.rs#L1105) | | 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:1105`](../ui/dr-ui/src/lib.rs#L1105) |
| 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:1767`](../ui/dr-ui/src/lib.rs#L1767), [`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-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:1767`](../ui/dr-ui/src/lib.rs#L1767), [`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), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) | | FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) |
| FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) |
| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:846`](../ui/dr-ui/src/lib.rs#L846), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:846`](../ui/dr-ui/src/lib.rs#L846), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
| FR-PLAT-LIN-2 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1) | | FR-PLAT-LIN-2 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1) |
| FR-PLG-2 | [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/expr.rs:152`](../core/dr-pipeline/src/declared/expr.rs#L152), [`core/dr-pipeline/src/declared/expr.rs:1`](../core/dr-pipeline/src/declared/expr.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:82`](../core/dr-pipeline/src/declared/mod.rs#L82), [`core/dr-pipeline/src/descriptor.rs:15`](../core/dr-pipeline/src/descriptor.rs#L15), [`core/dr-pipeline/src/descriptor.rs:635`](../core/dr-pipeline/src/descriptor.rs#L635), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/tests/declared_parity.rs:1`](../core/dr-pipeline/tests/declared_parity.rs#L1), [`core/dr-pipeline/tests/declared_parity.rs:240`](../core/dr-pipeline/tests/declared_parity.rs#L240), [`core/dr-pipeline/tests/declared_parity.rs:305`](../core/dr-pipeline/tests/declared_parity.rs#L305), [`core/dr-pipeline/tests/declared_parity.rs:358`](../core/dr-pipeline/tests/declared_parity.rs#L358), [`core/dr-pipeline/tests/declared_parity.rs:416`](../core/dr-pipeline/tests/declared_parity.rs#L416) |
| FR-PLG-2d | [`core/dr-pipeline/src/declared/decl.rs:112`](../core/dr-pipeline/src/declared/decl.rs#L112), [`core/dr-pipeline/src/declared/decl.rs:152`](../core/dr-pipeline/src/declared/decl.rs#L152), [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/decl.rs:420`](../core/dr-pipeline/src/declared/decl.rs#L420), [`core/dr-pipeline/src/declared/decl.rs:67`](../core/dr-pipeline/src/declared/decl.rs#L67), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:384`](../core/dr-pipeline/src/declared/mod.rs#L384), [`core/dr-pipeline/src/declared/mod.rs:403`](../core/dr-pipeline/src/declared/mod.rs#L403) |
| FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:129`](../core/dr-types/src/lib.rs#L129), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200) | | 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-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:187`](../ui/dr-ui/src/lib.rs#L187) | | FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:187`](../ui/dr-ui/src/lib.rs#L187) |
@@ -114,7 +116,7 @@ _None._
| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:2182`](../ui/dr-ui/src/develop.rs#L2182), [`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:2118`](../ui/dr-ui/ui/app.slint#L2118), [`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-3 | [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:2182`](../ui/dr-ui/src/develop.rs#L2182), [`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:2118`](../ui/dr-ui/ui/app.slint#L2118), [`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:1790`](../ui/dr-ui/ui/app.slint#L1790), [`ui/dr-ui/ui/app.slint:660`](../ui/dr-ui/ui/app.slint#L660), [`ui/dr-ui/ui/app.slint:691`](../ui/dr-ui/ui/app.slint#L691), [`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-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:1790`](../ui/dr-ui/ui/app.slint#L1790), [`ui/dr-ui/ui/app.slint:660`](../ui/dr-ui/ui/app.slint#L660), [`ui/dr-ui/ui/app.slint:691`](../ui/dr-ui/ui/app.slint#L691), [`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:2385`](../ui/dr-ui/src/lib.rs#L2385), [`ui/dr-ui/src/lib.rs:2805`](../ui/dr-ui/src/lib.rs#L2805), [`ui/dr-ui/src/lib.rs:2990`](../ui/dr-ui/src/lib.rs#L2990), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:320`](../ui/dr-ui/ui/app.slint#L320), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | | 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:2385`](../ui/dr-ui/src/lib.rs#L2385), [`ui/dr-ui/src/lib.rs:2805`](../ui/dr-ui/src/lib.rs#L2805), [`ui/dr-ui/src/lib.rs:2990`](../ui/dr-ui/src/lib.rs#L2990), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:320`](../ui/dr-ui/ui/app.slint#L320), [`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-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:2103`](../ui/dr-ui/src/lib.rs#L2103), [`ui/dr-ui/ui/app.slint:1138`](../ui/dr-ui/ui/app.slint#L1138), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) | | 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:2103`](../ui/dr-ui/src/lib.rs#L2103), [`ui/dr-ui/ui/app.slint:1138`](../ui/dr-ui/ui/app.slint#L1138), [`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), [`platform/dr-plat/src/storage.rs:148`](../platform/dr-plat/src/storage.rs#L148), [`ui/dr-ui/src/export.rs:500`](../ui/dr-ui/src/export.rs#L500) | | NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`platform/dr-plat/src/storage.rs:148`](../platform/dr-plat/src/storage.rs#L148), [`ui/dr-ui/src/export.rs:500`](../ui/dr-ui/src/export.rs#L500) |
@@ -140,7 +142,7 @@ _None._
## Not yet tagged ## Not yet tagged
73 of 177 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built. 71 of 177 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
<details><summary>Show untagged requirements</summary> <details><summary>Show untagged requirements</summary>
@@ -164,11 +166,9 @@ _None._
- FR-PLG-11 - FR-PLG-11
- FR-PLG-12 - FR-PLG-12
- FR-PLG-1a - FR-PLG-1a
- FR-PLG-2
- FR-PLG-2a - FR-PLG-2a
- FR-PLG-2b - FR-PLG-2b
- FR-PLG-2c - FR-PLG-2c
- FR-PLG-2d
- FR-PLG-3 - FR-PLG-3
- FR-PLG-3a - FR-PLG-3a
- FR-PLG-4 - FR-PLG-4
+12 -12
View File
@@ -1213,7 +1213,7 @@ fn curve_runs(op: &OpCapability, presentation: &Presentation) -> Option<Vec<Curv
} }
let mut runs: Vec<CurveRun> = Vec::new(); let mut runs: Vec<CurveRun> = 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 // The widget addresses points by offset from the first of its run, so
// a run has to be contiguous in the capability list. // a run has to be contiguous in the capability list.
let at = op.params.iter().position(|p| p.id == *id)?; let at = op.params.iter().position(|p| p.id == *id)?;
@@ -4538,15 +4538,15 @@ mod tests {
presentation: Some(Presentation { presentation: Some(Presentation {
// Prefers a gradient handle; this frontend has none, so it // Prefers a gradient handle; this frontend has none, so it
// falls back to the next entry, which the canvas does host. // falls back to the next entry, which the canvas does host.
widgets: &[WidgetKind::GradientHandle, WidgetKind::CropOverlay], widgets: vec![WidgetKind::GradientHandle, WidgetKind::CropOverlay],
demand: WidgetDemand { demand: WidgetDemand {
two_dimensional: true, two_dimensional: true,
precise_pointing: false, precise_pointing: false,
}, },
params: &[ParamId("a"), ParamId("b")], params: vec![ParamId("a"), ParamId("b")],
}), }),
params: vec![param("a"), param("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()); assert!(rows_from(&[on_canvas]).is_empty());
@@ -4670,7 +4670,7 @@ mod tests {
id: ParamId("method"), id: ParamId("method"),
label: LocalizedKey("param.invented.method"), label: LocalizedKey("param.invented.method"),
kind: ParamKind::Enum { kind: ParamKind::Enum {
variants: &[ variants: vec![
LocalizedKey("param.invented.method.fast"), LocalizedKey("param.invented.method.fast"),
LocalizedKey("param.invented.method.exact"), LocalizedKey("param.invented.method.exact"),
], ],
@@ -4680,7 +4680,7 @@ mod tests {
facet: None, facet: None,
}, },
], ],
attributes: &[dr_pipeline::Attribute::Tone], attributes: vec![dr_pipeline::Attribute::Tone],
}; };
let rows = rows_from(&[invented]); let rows = rows_from(&[invented]);
@@ -4718,12 +4718,12 @@ mod tests {
label: LocalizedKey("op.grading"), label: LocalizedKey("op.grading"),
active: false, active: false,
presentation: Some(Presentation { presentation: Some(Presentation {
widgets: &[WidgetKind::ColourWheel], widgets: vec![WidgetKind::ColourWheel],
demand: WidgetDemand { demand: WidgetDemand {
two_dimensional: true, two_dimensional: true,
precise_pointing: false, precise_pointing: false,
}, },
params: &[ParamId("hue"), ParamId("strength")], params: vec![ParamId("hue"), ParamId("strength")],
}), }),
params: vec![ params: vec![
ParamCapability { ParamCapability {
@@ -4755,7 +4755,7 @@ mod tests {
facet: None, facet: None,
}, },
], ],
attributes: &[dr_pipeline::Attribute::Tone], attributes: vec![dr_pipeline::Attribute::Tone],
}; };
assert!(!supported(WidgetKind::ColourWheel), "precondition"); assert!(!supported(WidgetKind::ColourWheel), "precondition");
@@ -5204,15 +5204,15 @@ mod tests {
label: LocalizedKey("op.invented_curve"), label: LocalizedKey("op.invented_curve"),
active: false, active: false,
presentation: Some(Presentation { presentation: Some(Presentation {
widgets: &[WidgetKind::ToneCurve], widgets: vec![WidgetKind::ToneCurve],
demand: WidgetDemand { demand: WidgetDemand {
two_dimensional: true, two_dimensional: true,
precise_pointing: true, precise_pointing: true,
}, },
params: &IDS, params: IDS.to_vec(),
}), }),
params: IDS.iter().map(|id| param(*id)).collect(), 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"); let presentation = plain.presentation.as_ref().expect("declares a widget");