diff --git a/core/dr-gpu/src/adjust.rs b/core/dr-gpu/src/adjust.rs index 245100c..7d7cda0 100644 --- a/core/dr-gpu/src/adjust.rs +++ b/core/dr-gpu/src/adjust.rs @@ -31,6 +31,15 @@ use crate::{DemosaicedImage, GpuContext, GpuError}; /// reads them. const RESERVED_FIELDS: usize = dr_pipeline::RESERVED_UNIFORM_FIELDS; +/// How many non-blocking polls a readback gets before it is called failed. +/// +/// A bound rather than a spin forever: if the device is lost the map callback +/// never arrives, and an unbounded loop would hang the interface rather than +/// surfacing the error. Set far above any plausible completion — the copy this +/// waits on is milliseconds — so it is reached only when something is wrong. +#[cfg(any(test, feature = "readback"))] +const READBACK_POLL_LIMIT: u32 = 100_000; + /// Runs composed operation chains against demosaiced images. pub struct AdjustPass { ctx: GpuContext, @@ -353,9 +362,32 @@ impl AdjustPass { slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); }); - self.ctx.device.poll(wgpu::Maintain::Wait); - rx.recv() - .map_err(|e| GpuError::Readback(e.to_string()))? + + // **Polled without blocking, then checked.** + // + // `Maintain::Wait` parks the calling thread until the GPU has finished, + // and this is called from the UI thread — so that park was a frozen + // interface for the duration of the copy (~7 ms at 4K, per the note + // above). `Poll` drives the same callbacks without sleeping, so the + // loop below stays interruptible and the mapping still completes. + // + // The bounded spin matters: a lost device would otherwise never + // deliver the callback and this would hang the app instead of + // reporting an error. + let mut mapped = None; + for _ in 0..READBACK_POLL_LIMIT { + self.ctx.device.poll(wgpu::Maintain::Poll); + match rx.try_recv() { + Ok(r) => { + mapped = Some(r); + break; + } + Err(std::sync::mpsc::TryRecvError::Empty) => continue, + Err(e) => return Err(GpuError::Readback(e.to_string())), + } + } + mapped + .ok_or_else(|| GpuError::Readback("readback did not complete".into()))? .map_err(|e| GpuError::Readback(e.to_string()))?; let data = slice.get_mapped_range();