Hand out descriptors a declaration could produce
`Operation::descriptor()` returned `&'static OpDescriptor`, and that lifetime
is the whole reason a build-time node is free and a run-time node is
impossible: only a compile-time literal can satisfy it, so no amount of
reading `ops/*.yaml` at startup 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 outsiders cannot meet is exactly the second,
weaker format that requirement forbids.
So a descriptor is now owned and handed out as `Arc<OpDescriptor>`, with `Vec`
where it held `&'static` slices. `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 identifier newtypes deliberately did not follow. `ParamId` is `Copy`, is
compared in `match` arms against generated constants, is a map key in the
sidecar and history, and reaches Slint model rows; an `Arc<str>` there would
cost a refcount on every one of those and would take `match id { EXPOSURE =>
.. }` away from the generated code. They gain an interner instead, which is
honest about its lifetime rather than pretending to one — the set of ids is
bounded by deduplication and is process-lifetime by construction, because the
sidecar on disk names its parameters and an id has to stay resolvable for as
long as any edit naming it can be opened.
No behaviour changes. Every descriptor that was a `static` is a `LazyLock`
initialiser now, `Operation::helpers` borrows from `self` instead of being
`'static` so a future run-time node can own its list, and `Warp` and `Framing`
follow `Operation` so there is one shape rather than two.
The one place a descriptor is read per frame is `compose_full`, which takes
`descriptor().id` to prefix each active operation's uniforms, and `dr-ui`
composes on every frame it draws. That is a dozen atomic increments beside a
composition that is already building several kilobytes of WGSL on the same
call; it is noted at the trait method rather than left for a profiler to find.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,7 @@
|
||||
//! wearing the same lens, which defeats the point of a lens profile.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::descriptor::{OpDescriptor, ParamId};
|
||||
use crate::operation::{Helper, Uniform};
|
||||
@@ -62,8 +63,10 @@ use crate::operation::{Helper, Uniform};
|
||||
/// Object-safe for the same reason [`crate::operation::Operation`] is: the
|
||||
/// graph holds `Box<dyn Warp>` in order, so the geometry chain is data.
|
||||
pub trait Warp: Send + Sync {
|
||||
/// Static description, driving UI generation exactly as for an operation.
|
||||
fn descriptor(&self) -> &'static OpDescriptor;
|
||||
/// This warp's description, driving UI generation exactly as for an
|
||||
/// operation — including being owned rather than `&'static`, for the
|
||||
/// reason [`crate::descriptor::OpDescriptor`] gives.
|
||||
fn descriptor(&self) -> Arc<OpDescriptor>;
|
||||
|
||||
/// Set a parameter. Values arrive already clamped to the descriptor.
|
||||
fn set_param(&mut self, id: ParamId, value: f32);
|
||||
@@ -204,32 +207,38 @@ fn sanitise(id: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use super::*;
|
||||
use crate::descriptor::Attribute;
|
||||
use crate::descriptor::{LocalizedKey, OpDescriptor, OpId, ParamDescriptor};
|
||||
|
||||
static DESC_A: OpDescriptor = OpDescriptor {
|
||||
id: OpId("warp_a"),
|
||||
label: LocalizedKey("a"),
|
||||
params: &[ParamDescriptor::amount("amount", "a.amount")],
|
||||
attributes: &[Attribute::Tone],
|
||||
};
|
||||
static DESC_B: OpDescriptor = OpDescriptor {
|
||||
id: OpId("warp_b"),
|
||||
label: LocalizedKey("b"),
|
||||
params: &[ParamDescriptor::amount("amount", "b.amount")],
|
||||
attributes: &[Attribute::Tone],
|
||||
};
|
||||
static DESC_A: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
id: OpId("warp_a"),
|
||||
label: LocalizedKey("a"),
|
||||
params: vec![ParamDescriptor::amount("amount", "a.amount")],
|
||||
attributes: vec![Attribute::Tone],
|
||||
})
|
||||
});
|
||||
static DESC_B: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
id: OpId("warp_b"),
|
||||
label: LocalizedKey("b"),
|
||||
params: vec![ParamDescriptor::amount("amount", "b.amount")],
|
||||
attributes: vec![Attribute::Tone],
|
||||
})
|
||||
});
|
||||
|
||||
struct Fake {
|
||||
desc: &'static OpDescriptor,
|
||||
desc: Arc<OpDescriptor>,
|
||||
amount: f32,
|
||||
splits: bool,
|
||||
}
|
||||
|
||||
impl Warp for Fake {
|
||||
fn descriptor(&self) -> &'static OpDescriptor {
|
||||
self.desc
|
||||
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||
self.desc.clone()
|
||||
}
|
||||
fn set_param(&mut self, _id: ParamId, value: f32) {
|
||||
self.amount = value;
|
||||
@@ -254,7 +263,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn fake(desc: &'static OpDescriptor, amount: f32, splits: bool) -> Box<dyn Warp> {
|
||||
fn fake(desc: Arc<OpDescriptor>, amount: f32, splits: bool) -> Box<dyn Warp> {
|
||||
Box::new(Fake {
|
||||
desc,
|
||||
amount,
|
||||
@@ -266,7 +275,7 @@ mod tests {
|
||||
fn no_active_warp_composes_to_nothing() {
|
||||
// The property that keeps the common case free: an image with no lens
|
||||
// correction must not pay for a bilinear sample.
|
||||
let composed = compose_warps(&[fake(&DESC_A, 0.0, false)]);
|
||||
let composed = compose_warps(&[fake(DESC_A.clone(), 0.0, false)]);
|
||||
assert!(!composed.is_active());
|
||||
assert!(composed.uniforms.is_empty());
|
||||
assert!(!composed.splits_channels);
|
||||
@@ -274,7 +283,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn an_active_warp_appears_once() {
|
||||
let composed = compose_warps(&[fake(&DESC_A, 2.0, false)]);
|
||||
let composed = compose_warps(&[fake(DESC_A.clone(), 2.0, false)]);
|
||||
assert!(composed.is_active());
|
||||
assert!(composed.body.contains("---- warp: warp_a ----"));
|
||||
assert!(composed.body.contains("u.warp_a_amount"));
|
||||
@@ -284,7 +293,10 @@ mod tests {
|
||||
fn uniforms_are_prefixed_so_warps_cannot_collide() {
|
||||
// Both fakes declare `amount`; without prefixing the generated struct
|
||||
// would carry a duplicate field and fail to compile.
|
||||
let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 2.0, false)]);
|
||||
let composed = compose_warps(&[
|
||||
fake(DESC_A.clone(), 1.0, false),
|
||||
fake(DESC_B.clone(), 2.0, false),
|
||||
]);
|
||||
assert!(composed.uniform_fields.contains("warp_a_amount: f32"));
|
||||
assert!(composed.uniform_fields.contains("warp_b_amount: f32"));
|
||||
assert_eq!(composed.uniforms, vec![1.0, 2.0]);
|
||||
@@ -294,7 +306,10 @@ mod tests {
|
||||
fn channel_splitting_is_requested_by_any_active_warp() {
|
||||
// One CA warp among several must switch the whole stage to the
|
||||
// three-sample path.
|
||||
let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, true)]);
|
||||
let composed = compose_warps(&[
|
||||
fake(DESC_A.clone(), 1.0, false),
|
||||
fake(DESC_B.clone(), 1.0, true),
|
||||
]);
|
||||
assert!(composed.splits_channels);
|
||||
}
|
||||
|
||||
@@ -302,14 +317,20 @@ mod tests {
|
||||
fn an_inactive_splitting_warp_does_not_force_three_samples() {
|
||||
// CA present but at neutral must cost nothing — otherwise every image
|
||||
// with the panel visible pays triple bandwidth.
|
||||
let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 0.0, true)]);
|
||||
let composed = compose_warps(&[
|
||||
fake(DESC_A.clone(), 1.0, false),
|
||||
fake(DESC_B.clone(), 0.0, true),
|
||||
]);
|
||||
assert!(composed.is_active());
|
||||
assert!(!composed.splits_channels);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warps_compose_in_order() {
|
||||
let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, false)]);
|
||||
let composed = compose_warps(&[
|
||||
fake(DESC_A.clone(), 1.0, false),
|
||||
fake(DESC_B.clone(), 1.0, false),
|
||||
]);
|
||||
let a = composed.body.find("warp_a").expect("a present");
|
||||
let b = composed.body.find("warp_b").expect("b present");
|
||||
assert!(a < b, "warps must chain in graph order");
|
||||
|
||||
Reference in New Issue
Block a user