Merge remote-tracking branch 'origin/master' into worktree-spot-removal
🐳 Android image / Build and push (push) Successful in 2s
Build and test / android-image (push) Successful in 2s
Build and test / Desktop (Linux) (push) Successful in 20m9s
Build and test / Layer separation (push) Successful in 39s
Traceability / Requirement traces (push) Successful in 26s
Build and test / Android (aarch64) (push) Failing after 33m10s

# Conflicts:
#	docs/traceability.md
This commit is contained in:
2026-08-26 21:56:32 +02:00
7 changed files with 582 additions and 24 deletions
+52 -2
View File
@@ -770,6 +770,41 @@ fn no_choices() -> slint::ModelRc<slint::SharedString> {
EMPTY.with(Clone::clone)
}
/// The choices model for one enum parameter, built once per variant list.
///
/// Memoised for exactly the reason [`no_choices`] is shared: `ModelRc` compares
/// by *identity*, so building a fresh one each call makes the row differ from
/// itself on every parameter event. `sync_rows` would then replace the row —
/// destroying the elements built from it, including whichever `TouchArea` is
/// holding the current gesture — and the enum's own control would fight every
/// slider drag elsewhere in the panel.
///
/// Curve rows solve the same problem the other way, by writing new values
/// through the existing model. That is not available here: a variant list is
/// fixed at compile time, so the model never needs updating and can simply be
/// the same one every time.
///
/// Keyed on the labels rather than the slice's address, because they are
/// resolved through the UI's catalogue and two operations offering the same
/// choices should share one model.
fn choices_model(labels: &[slint::SharedString]) -> slint::ModelRc<slint::SharedString> {
use std::cell::RefCell;
use std::collections::HashMap;
thread_local! {
static CACHE: RefCell<HashMap<String, slint::ModelRc<slint::SharedString>>> =
RefCell::new(HashMap::new());
}
let key = labels.join("\u{1f}");
CACHE.with(|cache| {
cache
.borrow_mut()
.entry(key)
.or_insert_with(|| slint::ModelRc::new(slint::VecModel::from(labels.to_vec())))
.clone()
})
}
/// Whether this frontend has an implementation of `widget` **anywhere**.
///
/// "Anywhere" is doing real work: a widget may be drawn in the panel, as the
@@ -989,7 +1024,7 @@ pub(crate) fn rows_filtered(
choices: if choices.is_empty() {
no_choices()
} else {
slint::ModelRc::new(slint::VecModel::from(choices))
choices_model(&choices)
},
});
}
@@ -2655,8 +2690,23 @@ impl DevelopSession {
// stock looks comes from. The sensor's width in pixels then says how
// much film one pixel covers, and the grain model needs nothing else
// to be correct at any zoom.
// TRACES: FR-DEV-3f
// The frame this is being simulated on, against the pixels it is being
// rendered to: together they are the enlargement, and the enlargement
// is what decides how grainy the result looks. A crystal is a fixed
// size in micrometres — the same emulsion on a sheet averages far more
// of them into each pixel than it does on 35 mm.
let format = dr_film::Format::from_index(
self.graph
.param(
dr_pipeline::ops::film_sim::ID,
dr_pipeline::ops::film_sim::FORMAT,
)
.unwrap_or(0.0)
.max(0.0) as usize,
);
let (source_width, _) = self.demosaiced.size();
let pixel_size_um = dr_film::grain::FRAME_WIDTH_UM / source_width.max(1) as f32;
let pixel_size_um = format.width_um() / source_width.max(1) as f32;
let grain = dr_film::Grain::for_pixel_size(profile, pixel_size_um);
let baked = dr_film::bake(&dr_film::Recipe {