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 <noreply@anthropic.com>
This commit is contained in:
Andrey Vyrodov
2026-10-10 08:50:20 +02:00
co-authored by Claude Opus 5.5
parent 3f3c204987
commit 753ca97a4d
3 changed files with 61 additions and 0 deletions
+1
View File
@@ -614,6 +614,7 @@ fn status(s: &mut Session, p: &Value) -> Result<Value> {
/// `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<Value> {
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(),
+28
View File
@@ -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<crate::activity::TaskGuard>,
}
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 {
+32
View File
@@ -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::<Vec<_>>();
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);
}