Merge branch 'worktree-agent-a1e5c8cb565255f5b' into master
# Conflicts: # docs/traceability.md
This commit is contained in:
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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, ¶ms())
|
||||||
|
.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", ¶ms()).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)", ¶ms()).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)", ¶ms()).unwrap_err();
|
||||||
|
assert!(err.contains("takes 2 argument(s), given 1"), "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user