From 753ca97a4d21e32ecbf275e0fdaa1a50745e73c2 Mon Sep 17 00:00:00 2001 From: Andrey Vyrodov Date: Sat, 10 Oct 2026 02:15:11 +0200 Subject: [PATCH] Issue #345: AI Denoise in the activity stack While a photo's AI Denoise picture is being made or waits its turn, the stack shows "AI Denoise" with the running photo's tiles as a percentage and its file name (no cross: what is made follows the Denoise amounts and Settings). The row goes when the queue is idle. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/denoise.rs | 1 + crates/engine/src/denoise.rs | 28 ++++++++++++++++++++++++++ crates/engine/src/tests_denoise.rs | 32 ++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/crates/engine/src/cmd/denoise.rs b/crates/engine/src/cmd/denoise.rs index 6962f9407..75c6c0a7a 100644 --- a/crates/engine/src/cmd/denoise.rs +++ b/crates/engine/src/cmd/denoise.rs @@ -614,6 +614,7 @@ fn status(s: &mut Session, p: &Value) -> Result { /// `denoise.pump {pace?}`: cheap to call every frame. Takes in what the background job has finished and keeps one going. fn pump(s: &mut Session, p: &Value) -> Result { s.denoise_pump(Pace::parse(p.get("pace").and_then(Value::as_str))); + s.denoise_track(); Ok(json!({ "active": s.denoise.active.is_some(), "running": s.denoise_running_json(), diff --git a/crates/engine/src/denoise.rs b/crates/engine/src/denoise.rs index 01bcdc784..30df142ca 100644 --- a/crates/engine/src/denoise.rs +++ b/crates/engine/src/denoise.rs @@ -890,6 +890,8 @@ pub(crate) struct State { pub generation: u64, /// Photos made since the session started. made: u64, + /// AI Denoise's row in the activity stack, while a photo is being made or waits its turn. + task: Option, } impl Default for State { @@ -909,6 +911,7 @@ impl Default for State { failed: HashMap::new(), generation: 0, made: 0, + task: None, } } } @@ -1354,6 +1357,31 @@ impl Session { self.denoise.running.is_some() || (!self.denoise.queue.is_empty() && self.denoise.active.is_some()) } + /// Keep AI Denoise's row in the activity stack up to date (each `denoise.pump`): there while a photo is being made or + /// waits its turn, with the running photo's tiles and its file name; gone when the queue is idle. It has no ✕: what is + /// made follows the photos' Denoise amounts and Settings ▸ AI Denoise. + pub(crate) fn denoise_track(&mut self) { + if !self.denoise_busy() { + self.denoise.task = None; + return; + } + let task = self.denoise.task.get_or_insert_with(|| { + let t = self.activity.start("denoise", "AI Denoise", crate::activity::Cancel::No); + t.set_unit(crate::activity::Unit::Percent); + t + }); + match &self.denoise.running { + Some(r) => { + task.progress(r.progress.done.load(Ordering::Relaxed) as u64, r.progress.total.load(Ordering::Relaxed) as u64); + task.detail(self.catalog.photo(r.photo).map_or("", |p| p.file_name.as_str())); + } + None => { + task.progress(0, 0); + task.detail(""); + } + } + } + /// Seconds the running job has been going, and its progress. pub(crate) fn denoise_running_json(&self) -> Value { match &self.denoise.running { diff --git a/crates/engine/src/tests_denoise.rs b/crates/engine/src/tests_denoise.rs index ef87747fc..a7d1c7c77 100644 --- a/crates/engine/src/tests_denoise.rs +++ b/crates/engine/src/tests_denoise.rs @@ -757,3 +757,35 @@ fn the_denoise_switch_validates_arguments_and_accepts_explicit_targets_without_s drop(x.s); let _ = std::fs::remove_dir_all(x.dir); } + +#[test] +fn denoise_row_while_queue_is_busy() { + let mut x = setup("activity", true); + let rows = |s: &Session| s.activity.list().into_iter().filter(|t| t.kind == "denoise").collect::>(); + let r = x.s.execute("denoise.pump", &json!({"pace": "full"})).unwrap(); + assert!(rows(&x.s).is_empty(), "nothing to make, no row: {r}"); + set_amount(&mut x.s, 100.0); + let mut shown = false; + for _ in 0..600 { + let r = x.s.execute("denoise.pump", &json!({"pace": "full"})).unwrap(); + let rows = rows(&x.s); + // (what the pump left running or queued is what the row reflects until the next pump) + if x.s.denoise_busy() { + assert_eq!(rows.len(), 1, "{r}"); + assert_eq!((rows[0].label.as_str(), rows[0].cancellable, rows[0].unit), ("AI Denoise", false, crate::activity::Unit::Percent), "{r}"); + if !r["running"].is_null() { + assert_eq!(rows[0].detail, "ramp.dng", "{r}"); + } + shown = true; + } else { + assert!(rows.is_empty(), "{r}"); + } + if r["ready"].as_u64() >= Some(1) { + break; + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + assert!(shown, "the photo never showed a row"); + assert!(rows(&x.s).is_empty(), "the row goes once the photo is made"); + let _ = std::fs::remove_dir_all(&x.dir); +}