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:
@@ -31,6 +31,7 @@
|
||||
//! untouched means a mis-set correction shifts the channels that contribute
|
||||
//! least to perceived sharpness. Scaling all three about a virtual reference
|
||||
//! would soften the image even when the correction is right.
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::descriptor::{
|
||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
||||
@@ -49,36 +50,38 @@ pub const BLUE: ParamId = ParamId("blue");
|
||||
/// resolution left to tune by eye at 100%.
|
||||
const MAX_SCALE: f32 = 0.005;
|
||||
|
||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
attributes: &[Attribute::Optics],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.aberration"),
|
||||
params: &[
|
||||
// Two independent controls rather than one: the red and blue
|
||||
// displacements are caused by different ends of the spectrum and are
|
||||
// not symmetric, so a single "fringing" slider could not remove both.
|
||||
ParamDescriptor::scalar(
|
||||
"red",
|
||||
"param.aberration.red",
|
||||
-100.0,
|
||||
100.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
0,
|
||||
),
|
||||
ParamDescriptor::scalar(
|
||||
"blue",
|
||||
"param.aberration.blue",
|
||||
-100.0,
|
||||
100.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
0,
|
||||
),
|
||||
],
|
||||
};
|
||||
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
attributes: vec![Attribute::Optics],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.aberration"),
|
||||
params: vec![
|
||||
// Two independent controls rather than one: the red and blue
|
||||
// displacements are caused by different ends of the spectrum and are
|
||||
// not symmetric, so a single "fringing" slider could not remove both.
|
||||
ParamDescriptor::scalar(
|
||||
"red",
|
||||
"param.aberration.red",
|
||||
-100.0,
|
||||
100.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
0,
|
||||
),
|
||||
ParamDescriptor::scalar(
|
||||
"blue",
|
||||
"param.aberration.blue",
|
||||
-100.0,
|
||||
100.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
0,
|
||||
),
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Aberration {
|
||||
@@ -113,8 +116,8 @@ impl Aberration {
|
||||
}
|
||||
|
||||
impl Warp for Aberration {
|
||||
fn descriptor(&self) -> &'static OpDescriptor {
|
||||
&DESCRIPTOR
|
||||
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||
DESCRIPTOR.clone()
|
||||
}
|
||||
|
||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||
@@ -300,7 +303,7 @@ mod tests {
|
||||
#[test]
|
||||
fn every_default_is_neutral() {
|
||||
let mut a = Aberration::new();
|
||||
for p in DESCRIPTOR.params {
|
||||
for p in &DESCRIPTOR.params {
|
||||
a.set_param(p.id, p.default);
|
||||
}
|
||||
assert!(!a.is_active(), "descriptor defaults must be neutral");
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
//! would be a guess dressed as a number; `resolves` is the line the stage
|
||||
//! already draws, and drawing it in two places differently is worse than a
|
||||
//! visible step.
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::descriptor::{
|
||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
||||
@@ -163,46 +164,48 @@ const MAX_KERNEL: f32 = 48.0;
|
||||
/// every editor's capture sharpening starts.
|
||||
const DEFAULT_RADIUS: f32 = 1.0;
|
||||
|
||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
id: ID,
|
||||
label: LocalizedKey("op.capture_sharpen"),
|
||||
params: &[
|
||||
// Amount carries the neutral, which is why it is first: the operation
|
||||
// is off when this is zero regardless of the other two, so a reset is
|
||||
// one control and the panel's ordering matches the way it is used.
|
||||
ParamDescriptor::amount("amount", "param.amount"),
|
||||
// In **source pixels** — see the module documentation. Half a photosite
|
||||
// is the smallest radius that means anything on a Bayer sensor, and
|
||||
// three is already past the point where an unsharp mask is sharpening
|
||||
// rather than adding local contrast; a photographer wanting the latter
|
||||
// wants clarity, which is a different operation with a different unit.
|
||||
ParamDescriptor::scalar(
|
||||
"radius",
|
||||
"param.radius",
|
||||
0.5,
|
||||
3.0,
|
||||
DEFAULT_RADIUS,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
2,
|
||||
),
|
||||
// A fraction, but declared as a scalar rather than through
|
||||
// `ParamDescriptor::fraction` for its precision alone: four decimal
|
||||
// places on a control whose whole useful travel is a dozen steps
|
||||
// reads as noise, and invites fiddling with digits that do nothing.
|
||||
ParamDescriptor::scalar(
|
||||
"threshold",
|
||||
"param.threshold",
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
2,
|
||||
),
|
||||
],
|
||||
attributes: &[Attribute::Detail],
|
||||
};
|
||||
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
id: ID,
|
||||
label: LocalizedKey("op.capture_sharpen"),
|
||||
params: vec![
|
||||
// Amount carries the neutral, which is why it is first: the operation
|
||||
// is off when this is zero regardless of the other two, so a reset is
|
||||
// one control and the panel's ordering matches the way it is used.
|
||||
ParamDescriptor::amount("amount", "param.amount"),
|
||||
// In **source pixels** — see the module documentation. Half a photosite
|
||||
// is the smallest radius that means anything on a Bayer sensor, and
|
||||
// three is already past the point where an unsharp mask is sharpening
|
||||
// rather than adding local contrast; a photographer wanting the latter
|
||||
// wants clarity, which is a different operation with a different unit.
|
||||
ParamDescriptor::scalar(
|
||||
"radius",
|
||||
"param.radius",
|
||||
0.5,
|
||||
3.0,
|
||||
DEFAULT_RADIUS,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
2,
|
||||
),
|
||||
// A fraction, but declared as a scalar rather than through
|
||||
// `ParamDescriptor::fraction` for its precision alone: four decimal
|
||||
// places on a control whose whole useful travel is a dozen steps
|
||||
// reads as noise, and invites fiddling with digits that do nothing.
|
||||
ParamDescriptor::scalar(
|
||||
"threshold",
|
||||
"param.threshold",
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
2,
|
||||
),
|
||||
],
|
||||
attributes: vec![Attribute::Detail],
|
||||
})
|
||||
});
|
||||
|
||||
/// TRACES: FR-DEV-3
|
||||
/// Capture sharpening: a separable unsharp mask with a contrast threshold.
|
||||
@@ -275,8 +278,8 @@ impl CaptureSharpen {
|
||||
}
|
||||
|
||||
impl Operation for CaptureSharpen {
|
||||
fn descriptor(&self) -> &'static OpDescriptor {
|
||||
&DESCRIPTOR
|
||||
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||
DESCRIPTOR.clone()
|
||||
}
|
||||
|
||||
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
|
||||
//! adjusted, each one's share depends on what the other is set to, so turning
|
||||
//! up one colour's saturation quietly weakened its neighbour's hue shift.
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::descriptor::{
|
||||
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId,
|
||||
@@ -123,8 +124,9 @@ impl Channel {
|
||||
}
|
||||
|
||||
// Parameter descriptors, one per band per channel. Written out rather than
|
||||
// generated because `ParamDescriptor` must be `const` to live in a `static`,
|
||||
// and a const loop cannot build a slice. The macro keeps it honest.
|
||||
// looped because `concat!` needs literals: every id is built from its band's
|
||||
// key, and a runtime loop has no way to spell `orange_sat`. The macro keeps it
|
||||
// honest.
|
||||
//
|
||||
// **Every one of them is faceted**, and that is what makes the operation
|
||||
// legible in a panel. Thirty-six parameters presented as a flat list are
|
||||
@@ -137,7 +139,7 @@ impl Channel {
|
||||
// §4.3a); this only says the parameter acts on the band centred there.
|
||||
macro_rules! band_params {
|
||||
($(($key:literal, $hue:literal)),* $(,)?) => {
|
||||
&[
|
||||
vec![
|
||||
$(
|
||||
ParamDescriptor::amount(
|
||||
concat!($key, "_hue"),
|
||||
@@ -175,25 +177,27 @@ macro_rules! band_params {
|
||||
// the same reason as those: `concat!` needs literals, so the keys and hues
|
||||
// cannot be read out of `BANDS` here. `facets_match_their_bands` below is
|
||||
// what keeps them from drifting.
|
||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
attributes: &[Attribute::Colour],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.colour_mixer"),
|
||||
params: band_params![
|
||||
("red", 0.0),
|
||||
("orange", 30.0),
|
||||
("yellow", 60.0),
|
||||
("chartreuse", 90.0),
|
||||
("green", 120.0),
|
||||
("spring", 150.0),
|
||||
("cyan", 180.0),
|
||||
("azure", 210.0),
|
||||
("blue", 240.0),
|
||||
("violet", 270.0),
|
||||
("magenta", 300.0),
|
||||
("rose", 330.0),
|
||||
],
|
||||
};
|
||||
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
attributes: vec![Attribute::Colour],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.colour_mixer"),
|
||||
params: band_params![
|
||||
("red", 0.0),
|
||||
("orange", 30.0),
|
||||
("yellow", 60.0),
|
||||
("chartreuse", 90.0),
|
||||
("green", 120.0),
|
||||
("spring", 150.0),
|
||||
("cyan", 180.0),
|
||||
("azure", 210.0),
|
||||
("blue", 240.0),
|
||||
("violet", 270.0),
|
||||
("magenta", 300.0),
|
||||
("rose", 330.0),
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
static MIXER_HELPERS: &[Helper] = &[
|
||||
helpers::LUMINANCE,
|
||||
@@ -306,8 +310,8 @@ impl ColourMixer {
|
||||
}
|
||||
|
||||
impl Operation for ColourMixer {
|
||||
fn descriptor(&self) -> &'static OpDescriptor {
|
||||
&DESCRIPTOR
|
||||
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||
DESCRIPTOR.clone()
|
||||
}
|
||||
|
||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||
@@ -505,7 +509,7 @@ mod tests {
|
||||
fn every_descriptor_id_resolves_to_a_band_and_channel() {
|
||||
// The link between the descriptor list and the value array. A
|
||||
// mismatch would make a slider silently adjust nothing.
|
||||
for p in DESCRIPTOR.params {
|
||||
for p in &DESCRIPTOR.params {
|
||||
assert!(
|
||||
ColourMixer::index_of(p.id).is_some(),
|
||||
"{} does not map to a band",
|
||||
@@ -547,7 +551,7 @@ mod tests {
|
||||
// and a hue mistyped there would put a row's swatch on a colour the
|
||||
// band does not act on — a control that lies about what it edits,
|
||||
// which is worse than one with no swatch at all.
|
||||
for p in DESCRIPTOR.params {
|
||||
for p in &DESCRIPTOR.params {
|
||||
let facet = p.facet.expect("every mixer parameter is faceted");
|
||||
let (band_key, _) = p.id.0.rsplit_once('_').expect("id is band_channel");
|
||||
let band = BANDS
|
||||
@@ -577,7 +581,7 @@ mod tests {
|
||||
// the aspect keyed per band, grouping by it would produce thirty-six
|
||||
// groups of one and nothing would have been gained.
|
||||
let mut per_aspect = std::collections::BTreeMap::new();
|
||||
for p in DESCRIPTOR.params {
|
||||
for p in &DESCRIPTOR.params {
|
||||
let facet = p.facet.expect("faceted");
|
||||
*per_aspect.entry(facet.aspect.0).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
//! whole composition scheme rests on (ARCH §5.6).
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::descriptor::{
|
||||
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation,
|
||||
@@ -341,11 +342,12 @@ const fn facet_of(aspect: &'static str, channel: Channel) -> Facet {
|
||||
|
||||
/// One channel's ten descriptors, defaulted onto the identity diagonal.
|
||||
///
|
||||
/// Written out per point rather than looped because a `ParamDescriptor` has to
|
||||
/// be `const` to live in a `static`, and a const loop cannot build a slice.
|
||||
/// Written out per point rather than looped because `concat!` needs literals:
|
||||
/// the parameter ids are built from the channel's prefix, and a runtime loop
|
||||
/// has no way to spell `r_p0_x`.
|
||||
macro_rules! channel_params {
|
||||
($(($prefix:literal, $channel:expr)),* $(,)?) => {
|
||||
&[$(
|
||||
vec![$(
|
||||
coord(concat!($prefix, "p0_x"), "param.curve.p0_x", 0.0)
|
||||
.faceted(facet_of("param.curve.p0_x", $channel)),
|
||||
coord(concat!($prefix, "p0_y"), "param.curve.p0_y", 0.0)
|
||||
@@ -370,26 +372,28 @@ macro_rules! channel_params {
|
||||
};
|
||||
}
|
||||
|
||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
// Both, and this is the case the plural exists for: the master curve is
|
||||
// tonal and the per-channel curves are chromatic. Filing it under one
|
||||
// would hide it from half the people looking for it.
|
||||
attributes: &[Attribute::Tone, Attribute::Colour],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.tone_curve"),
|
||||
// Defaults lie on y = x, so a fresh curve is the identity and the
|
||||
// operation reports itself inactive — on every channel.
|
||||
//
|
||||
// The master's ten come first, and stay first: a frontend addresses a
|
||||
// point by its offset from the first parameter of the run it is drawing,
|
||||
// and this is also the order one falling back to sliders reads them in.
|
||||
params: channel_params![
|
||||
("", Channel::Master),
|
||||
("r_", Channel::Red),
|
||||
("g_", Channel::Green),
|
||||
("b_", Channel::Blue),
|
||||
],
|
||||
};
|
||||
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
// Both, and this is the case the plural exists for: the master curve is
|
||||
// tonal and the per-channel curves are chromatic. Filing it under one
|
||||
// would hide it from half the people looking for it.
|
||||
attributes: vec![Attribute::Tone, Attribute::Colour],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.tone_curve"),
|
||||
// Defaults lie on y = x, so a fresh curve is the identity and the
|
||||
// operation reports itself inactive — on every channel.
|
||||
//
|
||||
// The master's ten come first, and stay first: a frontend addresses a
|
||||
// point by its offset from the first parameter of the run it is drawing,
|
||||
// and this is also the order one falling back to sliders reads them in.
|
||||
params: channel_params![
|
||||
("", Channel::Master),
|
||||
("r_", Channel::Red),
|
||||
("g_", Channel::Green),
|
||||
("b_", Channel::Blue),
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
/// One span of a monotone cubic Hermite spline. Shared by all four curves.
|
||||
const CURVE_SPAN: Helper = Helper {
|
||||
@@ -699,8 +703,8 @@ impl ToneCurve {
|
||||
}
|
||||
|
||||
impl Operation for ToneCurve {
|
||||
fn descriptor(&self) -> &'static OpDescriptor {
|
||||
&DESCRIPTOR
|
||||
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||
DESCRIPTOR.clone()
|
||||
}
|
||||
|
||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||
@@ -727,7 +731,7 @@ impl Operation for ToneCurve {
|
||||
Some(Presentation {
|
||||
// One entry: there is no second way to draw a tone curve that is
|
||||
// better than the sliders the frontend falls back to anyway.
|
||||
widgets: &[WidgetKind::ToneCurve],
|
||||
widgets: vec![WidgetKind::ToneCurve],
|
||||
demand: WidgetDemand {
|
||||
// A point is dragged in x and y together — that is what a
|
||||
// curve *is*, and a frontend that can only move one axis at a
|
||||
@@ -739,7 +743,7 @@ impl Operation for ToneCurve {
|
||||
// leave the other thirty stranded as sliders beneath the plot;
|
||||
// which of the four it draws at a time is its own affair, and the
|
||||
// facets are what let it decide without naming a channel.
|
||||
params: &CURVE_PARAMS,
|
||||
params: CURVE_PARAMS.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -910,7 +914,7 @@ mod tests {
|
||||
// Opening an unedited image must show the image.
|
||||
let c = ToneCurve::new();
|
||||
assert!(!c.is_active());
|
||||
for p in DESCRIPTOR.params {
|
||||
for p in &DESCRIPTOR.params {
|
||||
assert_eq!(c.param(p.id), p.default);
|
||||
}
|
||||
}
|
||||
@@ -927,7 +931,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_parameter_id_maps_to_a_point() {
|
||||
for p in DESCRIPTOR.params {
|
||||
for p in &DESCRIPTOR.params {
|
||||
assert!(
|
||||
ToneCurve::index_of(p.id).is_some(),
|
||||
"{} does not map to a point",
|
||||
@@ -1196,7 +1200,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(presentation.choose(|_| false), None);
|
||||
assert_eq!(presentation.params.len(), DESCRIPTOR.params.len());
|
||||
for p in DESCRIPTOR.params {
|
||||
for p in &DESCRIPTOR.params {
|
||||
assert!(
|
||||
presentation.params.contains(&p.id),
|
||||
"{} is not owned by the widget",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
//! set three correlated coefficients, and hand-correcting a lens with no
|
||||
//! profile is a "make the horizon straight" task, which one term does well.
|
||||
//! The full triple is reachable by loading a profile.
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::descriptor::{
|
||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
||||
@@ -35,25 +36,27 @@ use crate::operation::{Helper, Uniform};
|
||||
pub const ID: OpId = OpId("distortion");
|
||||
pub const AMOUNT: ParamId = ParamId("amount");
|
||||
|
||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
attributes: &[Attribute::Optics],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.distortion"),
|
||||
// ±100 maps to a ±0.25 cubic coefficient. That covers an uncorrected
|
||||
// fisheye at one end and strong pincushion at the other; beyond it the
|
||||
// inverse mapping stops being single-valued near the corners and the
|
||||
// correction folds the image over itself.
|
||||
params: &[ParamDescriptor::scalar(
|
||||
"amount",
|
||||
"param.distortion.amount",
|
||||
-100.0,
|
||||
100.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
0,
|
||||
)],
|
||||
};
|
||||
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
attributes: vec![Attribute::Optics],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.distortion"),
|
||||
// ±100 maps to a ±0.25 cubic coefficient. That covers an uncorrected
|
||||
// fisheye at one end and strong pincushion at the other; beyond it the
|
||||
// inverse mapping stops being single-valued near the corners and the
|
||||
// correction folds the image over itself.
|
||||
params: vec![ParamDescriptor::scalar(
|
||||
"amount",
|
||||
"param.distortion.amount",
|
||||
-100.0,
|
||||
100.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
0,
|
||||
)],
|
||||
})
|
||||
});
|
||||
|
||||
/// The cubic coefficient at full slider travel.
|
||||
const MAX_COEFF: f32 = 0.25;
|
||||
@@ -108,8 +111,8 @@ impl Distortion {
|
||||
}
|
||||
|
||||
impl Warp for Distortion {
|
||||
fn descriptor(&self) -> &'static OpDescriptor {
|
||||
&DESCRIPTOR
|
||||
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||
DESCRIPTOR.clone()
|
||||
}
|
||||
|
||||
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
|
||||
//! keeps its no-dependency property (ARCH §6.5a) exactly as `vignetting` does
|
||||
//! with `Pa`.
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
||||
use crate::operation::{Operation, Uniform};
|
||||
@@ -73,29 +74,31 @@ static MATRIX_FIELDS: [[&str; 3]; 3] = [
|
||||
["m20", "m21", "m22"],
|
||||
];
|
||||
|
||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
// Tone and colour both, and not `Effect`: a stock is not something applied
|
||||
// on top of a photograph, it is what the photograph was made on.
|
||||
attributes: &[Attribute::Tone, Attribute::Colour],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.film_sim"),
|
||||
params: &[
|
||||
ParamDescriptor::stops("exposure", "param.film_sim.exposure", -3.0, 3.0),
|
||||
ParamDescriptor::stops("print_exposure", "param.film_sim.print_exposure", -3.0, 3.0),
|
||||
// TRACES: FR-DEV-3f
|
||||
// Development, in stops of push. Bounded by what the manufacturers
|
||||
// actually published: Double-X's measured axis spans about -1 to +2,
|
||||
// and beyond a range like that a curve would have to be invented.
|
||||
ParamDescriptor::stops("push", "param.film_sim.push", -1.0, 3.0),
|
||||
// TRACES: FR-DEV-3f
|
||||
// Which frame this was taken on — the half of the enlargement a
|
||||
// photograph cannot supply. A crystal is a fixed size in micrometres,
|
||||
// so how grainy a picture looks is film size against output size, and
|
||||
// the same emulsion on 4x5 renders about three times smoother than on
|
||||
// 35mm at the same print.
|
||||
ParamDescriptor::choice("format", "param.film_sim.format", &FORMATS),
|
||||
],
|
||||
};
|
||||
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
// Tone and colour both, and not `Effect`: a stock is not something applied
|
||||
// on top of a photograph, it is what the photograph was made on.
|
||||
attributes: vec![Attribute::Tone, Attribute::Colour],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.film_sim"),
|
||||
params: vec![
|
||||
ParamDescriptor::stops("exposure", "param.film_sim.exposure", -3.0, 3.0),
|
||||
ParamDescriptor::stops("print_exposure", "param.film_sim.print_exposure", -3.0, 3.0),
|
||||
// TRACES: FR-DEV-3f
|
||||
// Development, in stops of push. Bounded by what the manufacturers
|
||||
// actually published: Double-X's measured axis spans about -1 to +2,
|
||||
// and beyond a range like that a curve would have to be invented.
|
||||
ParamDescriptor::stops("push", "param.film_sim.push", -1.0, 3.0),
|
||||
// TRACES: FR-DEV-3f
|
||||
// Which frame this was taken on — the half of the enlargement a
|
||||
// photograph cannot supply. A crystal is a fixed size in micrometres,
|
||||
// so how grainy a picture looks is film size against output size, and
|
||||
// the same emulsion on 4x5 renders about three times smoother than on
|
||||
// 35mm at the same print.
|
||||
ParamDescriptor::choice("format", "param.film_sim.format", FORMATS.to_vec()),
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
/// A stock reduced to what a shader runs, as `dr-film` bakes it.
|
||||
///
|
||||
@@ -190,8 +193,8 @@ impl FilmSim {
|
||||
}
|
||||
|
||||
impl Operation for FilmSim {
|
||||
fn descriptor(&self) -> &'static OpDescriptor {
|
||||
&DESCRIPTOR
|
||||
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||
DESCRIPTOR.clone()
|
||||
}
|
||||
|
||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
//! this file.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
||||
use crate::detail::{DetailPass, DetailStage, RenderScale};
|
||||
@@ -181,7 +182,12 @@ const TRUNCATION: f32 = 2.0;
|
||||
/// between clarity and texture can be read side by side, which is the one
|
||||
/// thing a reader comes to this file to do.
|
||||
pub struct Recipe {
|
||||
descriptor: &'static OpDescriptor,
|
||||
/// The static this operation's descriptor is built in. A `LazyLock`
|
||||
/// rather than a reference to a descriptor, because a descriptor is an
|
||||
/// owned value handed out as an `Arc` now (FR-PLG-2), and a `const`
|
||||
/// recipe cannot hold an `Arc` — only a reference to the static that
|
||||
/// makes one.
|
||||
descriptor: &'static LazyLock<Arc<OpDescriptor>>,
|
||||
helpers: &'static [Helper],
|
||||
/// The Gaussian's σ, as a fraction of the frame's shorter edge.
|
||||
sigma: f32,
|
||||
@@ -253,19 +259,23 @@ impl Band for Fine {
|
||||
};
|
||||
}
|
||||
|
||||
static CLARITY_DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
id: CLARITY,
|
||||
label: LocalizedKey("op.clarity"),
|
||||
params: &[ParamDescriptor::amount("amount", "param.clarity.amount")],
|
||||
attributes: &[Attribute::Detail],
|
||||
};
|
||||
static CLARITY_DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
id: CLARITY,
|
||||
label: LocalizedKey("op.clarity"),
|
||||
params: vec![ParamDescriptor::amount("amount", "param.clarity.amount")],
|
||||
attributes: vec![Attribute::Detail],
|
||||
})
|
||||
});
|
||||
|
||||
static TEXTURE_DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
id: TEXTURE,
|
||||
label: LocalizedKey("op.texture"),
|
||||
params: &[ParamDescriptor::amount("amount", "param.texture.amount")],
|
||||
attributes: &[Attribute::Detail],
|
||||
};
|
||||
static TEXTURE_DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
id: TEXTURE,
|
||||
label: LocalizedKey("op.texture"),
|
||||
params: vec![ParamDescriptor::amount("amount", "param.texture.amount")],
|
||||
attributes: vec![Attribute::Detail],
|
||||
})
|
||||
});
|
||||
|
||||
/// Luminance as a position on a logarithmic scale, floored.
|
||||
///
|
||||
@@ -394,8 +404,8 @@ impl<B: Band> LocalContrast<B> {
|
||||
}
|
||||
|
||||
impl<B: Band> Operation for LocalContrast<B> {
|
||||
fn descriptor(&self) -> &'static OpDescriptor {
|
||||
B::RECIPE.descriptor
|
||||
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||
Arc::clone(B::RECIPE.descriptor)
|
||||
}
|
||||
|
||||
fn set_param(&mut self, _id: ParamId, value: f32) {
|
||||
|
||||
@@ -179,7 +179,7 @@ mod tests {
|
||||
// perfectly and silently breaks the sidecar.
|
||||
for mut op in chain() {
|
||||
let descriptor = op.descriptor();
|
||||
for p in descriptor.params {
|
||||
for p in &descriptor.params {
|
||||
let crate::descriptor::ParamKind::Scalar { min, max, .. } = p.kind else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
//! lie. The chroma radius, ten times larger, still resolves — which is also
|
||||
//! true of the fault it treats, since a blotch twenty pixels across survives
|
||||
//! being halved.
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::descriptor::{
|
||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
||||
@@ -158,38 +159,40 @@ pub const ID: OpId = OpId("noise_reduction");
|
||||
pub const LUMINANCE: ParamId = ParamId("luminance");
|
||||
pub const CHROMA: ParamId = ParamId("chroma");
|
||||
|
||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
id: ID,
|
||||
label: LocalizedKey("op.noise_reduction"),
|
||||
attributes: &[Attribute::Detail],
|
||||
// Zero to a hundred rather than the symmetric `amount` shape the tonal
|
||||
// controls use. There is no meaningful negative: "minus fifty noise
|
||||
// reduction" would be adding grain, which is a look rather than a repair
|
||||
// and belongs to a different operation carrying `Attribute::Effect`. A
|
||||
// control whose left half does nothing is worse than one that stops.
|
||||
params: &[
|
||||
ParamDescriptor::scalar(
|
||||
"luminance",
|
||||
"param.noise_reduction.luminance",
|
||||
0.0,
|
||||
100.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
0,
|
||||
),
|
||||
ParamDescriptor::scalar(
|
||||
"chroma",
|
||||
"param.noise_reduction.chroma",
|
||||
0.0,
|
||||
100.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
0,
|
||||
),
|
||||
],
|
||||
};
|
||||
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
id: ID,
|
||||
label: LocalizedKey("op.noise_reduction"),
|
||||
attributes: vec![Attribute::Detail],
|
||||
// Zero to a hundred rather than the symmetric `amount` shape the tonal
|
||||
// controls use. There is no meaningful negative: "minus fifty noise
|
||||
// reduction" would be adding grain, which is a look rather than a repair
|
||||
// and belongs to a different operation carrying `Attribute::Effect`. A
|
||||
// control whose left half does nothing is worse than one that stops.
|
||||
params: vec![
|
||||
ParamDescriptor::scalar(
|
||||
"luminance",
|
||||
"param.noise_reduction.luminance",
|
||||
0.0,
|
||||
100.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
0,
|
||||
),
|
||||
ParamDescriptor::scalar(
|
||||
"chroma",
|
||||
"param.noise_reduction.chroma",
|
||||
0.0,
|
||||
100.0,
|
||||
0.0,
|
||||
Unit::None,
|
||||
Scale::Linear,
|
||||
0,
|
||||
),
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
/// The luminance radius at the lowest and the highest amount, in **source**
|
||||
/// pixels.
|
||||
@@ -365,8 +368,8 @@ fn inv_spatial(kernel: u32) -> f32 {
|
||||
}
|
||||
|
||||
impl Operation for NoiseReduction {
|
||||
fn descriptor(&self) -> &'static OpDescriptor {
|
||||
&DESCRIPTOR
|
||||
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||
DESCRIPTOR.clone()
|
||||
}
|
||||
|
||||
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
|
||||
//! stages: a corner recovered by two stops has to be recovered while the
|
||||
//! highlight headroom to hold it still exists (ARCH §5.2).
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
||||
use crate::operation::{Helper, Operation, Uniform};
|
||||
@@ -45,19 +46,21 @@ pub const AMOUNT: ParamId = ParamId("amount");
|
||||
/// fast prime wide open — the case that actually needs correcting.
|
||||
const MAX_K1: f32 = -0.5;
|
||||
|
||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
// Optics rather than effect: this carries lens-profile coefficients
|
||||
// and corrects what the lens did. A *creative* vignette is a different
|
||||
// operation that does not exist yet, and would be `Effect`.
|
||||
attributes: &[Attribute::Optics],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.vignetting"),
|
||||
// Bidirectional deliberately. Negative values *add* falloff, which is a
|
||||
// legitimate creative choice as well as a correction, and a control that
|
||||
// only removed vignetting would need a second one beside it to put any
|
||||
// back.
|
||||
params: &[ParamDescriptor::amount("amount", "param.vignetting.amount")],
|
||||
};
|
||||
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||
Arc::new(OpDescriptor {
|
||||
// Optics rather than effect: this carries lens-profile coefficients
|
||||
// and corrects what the lens did. A *creative* vignette is a different
|
||||
// operation that does not exist yet, and would be `Effect`.
|
||||
attributes: vec![Attribute::Optics],
|
||||
id: ID,
|
||||
label: LocalizedKey("op.vignetting"),
|
||||
// Bidirectional deliberately. Negative values *add* falloff, which is a
|
||||
// legitimate creative choice as well as a correction, and a control that
|
||||
// only removed vignetting would need a second one beside it to put any
|
||||
// back.
|
||||
params: vec![ParamDescriptor::amount("amount", "param.vignetting.amount")],
|
||||
})
|
||||
});
|
||||
|
||||
/// The `pa` polynomial coefficients, as Lensfun stores them.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
@@ -107,8 +110,8 @@ impl Vignetting {
|
||||
}
|
||||
|
||||
impl Operation for Vignetting {
|
||||
fn descriptor(&self) -> &'static OpDescriptor {
|
||||
&DESCRIPTOR
|
||||
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||
DESCRIPTOR.clone()
|
||||
}
|
||||
|
||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||
@@ -371,7 +374,7 @@ mod tests {
|
||||
#[test]
|
||||
fn every_default_is_neutral() {
|
||||
let mut v = Vignetting::new();
|
||||
for p in DESCRIPTOR.params {
|
||||
for p in &DESCRIPTOR.params {
|
||||
v.set_param(p.id, p.default);
|
||||
}
|
||||
assert!(!v.is_active());
|
||||
|
||||
Reference in New Issue
Block a user