M5.6: with a GPU, a slider drag's preview is drawn in the frame of the drag tick
A drag tick requested the loupe's draft and the frame ended; the draft, done in 1.5-3 ms on the GPU, was picked up at the start of the next frame, a frame after the slider moved. Now, in a drag, the loupe waits up to 40 % of a frame for its draft when the last loupe render took well under that (not with a render offload, nor zoomed in, where what changes on screen is the window's draft). A CPU render, which would only make the frame late, is not waited for. JobPool::wait waits for the next finished job, which try_recv then hands out; Renderer::poll's collection of finished jobs is split out so the wait can turn the draft into the texture. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
701f78ea2e
commit
c8f676252f
|
|
@ -56,6 +56,8 @@ pub struct JobPool<S, R> {
|
|||
shared: Arc<Shared<S, R>>,
|
||||
tx: Sender<Done<S, R>>,
|
||||
rx: Receiver<Done<S, R>>,
|
||||
/// A finished job [`Self::wait`] took off the channel, handed out first by [`Self::try_recv`].
|
||||
early: Option<Done<S, R>>,
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
threads: usize,
|
||||
started: bool,
|
||||
|
|
@ -82,6 +84,7 @@ impl<S: Copy + Eq + Hash + Send + 'static, R: Send + 'static> JobPool<S, R> {
|
|||
}),
|
||||
tx,
|
||||
rx,
|
||||
early: None,
|
||||
threads,
|
||||
started: false,
|
||||
seq: 0,
|
||||
|
|
@ -210,8 +213,16 @@ impl<S: Copy + Eq + Hash + Send + 'static, R: Send + 'static> JobPool<S, R> {
|
|||
}
|
||||
|
||||
/// A finished job, if any.
|
||||
pub fn try_recv(&self) -> Option<Done<S, R>> {
|
||||
self.rx.try_recv().ok()
|
||||
pub fn try_recv(&mut self) -> Option<Done<S, R>> {
|
||||
self.early.take().or_else(|| self.rx.try_recv().ok())
|
||||
}
|
||||
|
||||
/// Wait up to `timeout` for a job to finish; whether one has (it is the next [`Self::try_recv`]).
|
||||
pub fn wait(&mut self, timeout: Duration) -> bool {
|
||||
if self.early.is_none() {
|
||||
self.early = self.rx.recv_timeout(timeout).ok();
|
||||
}
|
||||
self.early.is_some()
|
||||
}
|
||||
|
||||
/// Run up to `n` jobs on the calling thread (wasm, tests). Returns how many ran.
|
||||
|
|
|
|||
|
|
@ -351,3 +351,16 @@ fn disk_cache_never_touches_foreign_files() {
|
|||
assert!(!disk::is_cache_file("01", "0123456789abcdef0123456789abcdef.jpg.bak"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// [`JobPool::wait`] returns once a job finishes, which [`JobPool::try_recv`] then hands out, or
|
||||
/// after the timeout.
|
||||
#[test]
|
||||
fn pool_wait_keeps_the_finished_job_for_try_recv() {
|
||||
let mut p: JobPool<u8, u8> = JobPool::new(1);
|
||||
assert!(!p.wait(std::time::Duration::from_millis(10)), "nothing runs");
|
||||
p.submit(1, 0, 1, Box::new(|| 7));
|
||||
assert!(p.wait(std::time::Duration::from_secs(10)));
|
||||
assert!(p.wait(std::time::Duration::ZERO), "kept until taken");
|
||||
assert_eq!(p.try_recv().map(|d| d.result), Some(7));
|
||||
assert!(p.try_recv().is_none());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -473,6 +473,15 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
let job = job.with_overlay(overlay).with_proof(app.ui.soft_proof.then_some(app.ui.proof));
|
||||
let job = job.with_hidden_grading(app.ui.grading_hidden_for(id));
|
||||
app.renderer.request(Slot::Main, job, 100);
|
||||
// a drag's draft that renders in a few ms (GPU) is drawn in this frame, not the next (zoomed
|
||||
// in, what changes on screen is the window's draft)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if interacting && plan.window_edge.is_none() {
|
||||
let frame = ui.input(|i| i.predicted_dt).clamp(1.0 / 240.0, 1.0 / 30.0);
|
||||
// (a frame time that is not a number waits for nothing)
|
||||
let budget = std::time::Duration::try_from_secs_f32(frame * 0.4).unwrap_or_default();
|
||||
app.renderer.wait_for_draft(ui.ctx(), &mut app.session, budget);
|
||||
}
|
||||
}
|
||||
// hovering a preset or profile: the photo with that look, shown instead of the loupe render
|
||||
// once it is ready (nothing is committed)
|
||||
|
|
|
|||
|
|
@ -649,6 +649,37 @@ impl Renderer {
|
|||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let inline_ms = 0.0;
|
||||
let changed = self.collect(ctx, session, inline_ms);
|
||||
self.evict_variants();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.trim_when_idle(ctx);
|
||||
if !self.pending.is_empty() {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(16));
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// In a slider drag, give the loupe's draft (just requested) until `budget` to finish, so that it
|
||||
/// is drawn in this frame rather than picked up at the start of the next one. Only when the last
|
||||
/// loupe render took well under the budget (a GPU draft takes a few milliseconds; a CPU render
|
||||
/// that would only make the frame late is not waited for).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn wait_for_draft(&mut self, ctx: &egui::Context, session: &mut Session, budget: std::time::Duration) {
|
||||
if self.offload.is_some() || self.last_main_ms * 1.5 >= budget.as_secs_f64() * 1e3 {
|
||||
return;
|
||||
}
|
||||
let Some(deadline) = std::time::Instant::now().checked_add(budget) else { return };
|
||||
while self.is_pending(Slot::Main) {
|
||||
let left = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
if left.is_zero() || !self.pool.wait(left) {
|
||||
break;
|
||||
}
|
||||
self.collect(ctx, session, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn finished jobs into textures. Returns true if anything changed.
|
||||
fn collect(&mut self, ctx: &egui::Context, session: &mut Session, inline_ms: f64) -> bool {
|
||||
let mut finished = Vec::new();
|
||||
while let Some(done) = self.pool.try_recv() {
|
||||
finished.push((done.slot, done.result, if done.ms > 0.0 { done.ms } else { inline_ms }));
|
||||
|
|
@ -757,12 +788,6 @@ impl Renderer {
|
|||
}
|
||||
changed = true;
|
||||
}
|
||||
self.evict_variants();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.trim_when_idle(ctx);
|
||||
if !self.pending.is_empty() {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(16));
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
|
|
@ -953,6 +978,28 @@ mod thumbnail_tests {
|
|||
assert!(!r.thumb_current(&p, 384, 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fast_draft_is_drawn_in_the_frame_that_asked_for_it() {
|
||||
let mut s = Session::new();
|
||||
s.media.file_loader = Some(Arc::new(|_, _| Ok((lightcraft_raster::Rgb32f::new(8, 8), Default::default()))));
|
||||
s.catalog.apply(Op::AddPhoto { photo: Box::new((*photo()).clone()) }).unwrap();
|
||||
let mut r = Renderer::with_worker_threads(1);
|
||||
let ctx = egui::Context::default();
|
||||
let budget = std::time::Duration::from_secs(10);
|
||||
// the last loupe render took longer than the budget (a CPU render): not waited for
|
||||
r.last_main_ms = 1e6;
|
||||
r.request(Slot::Main, s.render_job(PhotoId(1), 64, 64, false, true).unwrap(), 100);
|
||||
r.wait_for_draft(&ctx, &mut s, budget);
|
||||
assert!(r.is_pending(Slot::Main) && !r.textures.contains_key(&Slot::Main));
|
||||
// a fast one (GPU drafts): the texture is the new render when the wait returns
|
||||
r.last_main_ms = 1.0;
|
||||
r.request(Slot::Main, s.render_job(PhotoId(1), 32, 32, false, true).unwrap(), 100);
|
||||
let want = r.wanted(Slot::Main);
|
||||
r.wait_for_draft(&ctx, &mut s, budget);
|
||||
assert!(!r.is_pending(Slot::Main));
|
||||
assert_eq!(r.textures.get(&Slot::Main).map(|t| t.key), want);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Work {
|
||||
jobs: Vec<(Slot, RenderJob)>,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
# What's new in LightCraft
|
||||
|
||||
## 2026-10-13
|
||||
|
||||
### Slider drags follow the pointer more closely
|
||||
- With a GPU, a slider drag's preview is drawn in the same frame as the slider moves instead of one frame later,
|
||||
when the preview renders in a few milliseconds (it waits at most 40 % of a frame for it; slower renders, such as
|
||||
on the CPU, are not waited for).
|
||||
|
||||
## 2026-10-11
|
||||
|
||||
### JPEG XL export
|
||||
|
|
|
|||
Loading…
Reference in New Issue