mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 10:20:47 -06:00
#3028: keep dedicated preview lanes waiting on GPU admission
This commit is contained in:
@@ -101,6 +101,12 @@ impl GpuRenderSlots {
|
||||
/// A Rayon helper may be executing a nested frame while an admitted render waits for
|
||||
/// its work. Blocking that helper on a permit would deadlock the render (#3028).
|
||||
fn for_render(&self, max: usize) -> Option<GpuRenderSlot<'_>> {
|
||||
// Dedicated preview pools have one thread, protected from nested drains. They cannot
|
||||
// be helpers of another admitted frame, so waiting preserves GPU preview throughput.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if DRAINING_LANE.get() {
|
||||
return Some(self.acquire(max));
|
||||
}
|
||||
if rayon::current_thread_index().is_some() { self.try_acquire(max) } else { Some(self.acquire(max)) }
|
||||
}
|
||||
|
||||
@@ -1686,15 +1692,29 @@ struct Worker {
|
||||
thread_local! {
|
||||
/// This pool thread is inside [`Worker::drain`] (rendering a frame).
|
||||
static DRAINING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
/// Only the single thread of a dedicated preview lane may wait for GPU admission.
|
||||
static DRAINING_LANE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
/// Clears [`DRAINING`] when the drain ends, even by a panic.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
struct Draining;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Draining {
|
||||
fn enter(lane: Option<usize>) -> Option<Self> {
|
||||
if DRAINING.replace(true) {
|
||||
return None;
|
||||
}
|
||||
DRAINING_LANE.set(lane.is_some());
|
||||
Some(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Drop for Draining {
|
||||
fn drop(&mut self) {
|
||||
DRAINING_LANE.set(false);
|
||||
DRAINING.set(false);
|
||||
}
|
||||
}
|
||||
@@ -1723,11 +1743,7 @@ impl Worker {
|
||||
/// `lane`: the lane this runs on (see [`Queue::idle_lanes`]), `None` on the pool.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn drain(&self, lane: Option<usize>) {
|
||||
if DRAINING.get() {
|
||||
return;
|
||||
}
|
||||
DRAINING.set(true);
|
||||
let _reset = Draining;
|
||||
let Some(_reset) = Draining::enter(lane) else { return };
|
||||
while self.run_next(lane) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -3237,6 +3237,42 @@ fn gpu_worker_routes_admit_display_readback_and_auto_effects() {
|
||||
assert!(worker.gpu_ready());
|
||||
assert!(gpu.context().check_health().is_ok());
|
||||
|
||||
// The real dedicated-lane drain waits, then caches a GPU frame instead of silently
|
||||
// switching a healthy preview to CPU rendering while the other lanes are busy.
|
||||
frames.queue.lock().unwrap().jobs.push(Job {
|
||||
key: job.key,
|
||||
src: job.src.clone(),
|
||||
comp,
|
||||
t: job.t,
|
||||
opts: RenderOpts { backend: Backend::Gpu, ..job.opts },
|
||||
urgent: false,
|
||||
background: true,
|
||||
seq: 1,
|
||||
cancel: Arc::default(),
|
||||
});
|
||||
let first = GPU_RENDER_SLOTS.acquire(MAX_GPU_RENDERS);
|
||||
let second = GPU_RENDER_SLOTS.acquire(MAX_GPU_RENDERS);
|
||||
let pool = rayon::ThreadPoolBuilder::new().num_threads(1).build().unwrap();
|
||||
let (started, ready) = std::sync::mpsc::channel();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let (early, finished) = std::thread::scope(|scope| {
|
||||
scope.spawn(|| {
|
||||
pool.install(|| {
|
||||
started.send(()).unwrap();
|
||||
worker.drain(Some(0));
|
||||
tx.send(()).unwrap();
|
||||
})
|
||||
});
|
||||
ready.recv_timeout(std::time::Duration::from_secs(2)).unwrap();
|
||||
let early = rx.recv_timeout(std::time::Duration::from_millis(100));
|
||||
drop((first, second));
|
||||
let finished = early.or_else(|_| rx.recv_timeout(std::time::Duration::from_secs(2)));
|
||||
(early, finished)
|
||||
});
|
||||
assert!(early.is_err(), "preview lane must wait for an available GPU permit");
|
||||
assert_eq!(finished, Ok(()));
|
||||
assert!(frames.cache.lock().unwrap().map.get(&job.key).unwrap().img.is_gpu());
|
||||
|
||||
// A retirement found inside the scope releases the permit before the fresh CPU retry.
|
||||
gpu.context().device_failure_notifier()("synthetic worker retirement".into());
|
||||
let before = admissions();
|
||||
@@ -3288,3 +3324,45 @@ fn gpu_render_slots_never_block_rayon_helpers() {
|
||||
assert_eq!(declined, Ok(true), "Rayon helpers must not wait on GPU permits");
|
||||
assert_eq!(*slots.busy.lock().unwrap(), 0);
|
||||
}
|
||||
|
||||
/// Dedicated preview lanes have one protected thread and can wait without creating the
|
||||
/// shared-pool helper dependency cycle. They must keep rendering on a healthy GPU.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn gpu_preview_lane_waits_for_admission_and_resets_its_marker() {
|
||||
let slots = GpuRenderSlots::new();
|
||||
let first = slots.acquire(2);
|
||||
let second = slots.acquire(2);
|
||||
let pool = rayon::ThreadPoolBuilder::new().num_threads(1).build().unwrap();
|
||||
let (started, ready) = std::sync::mpsc::channel();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let (early, admitted) = std::thread::scope(|scope| {
|
||||
scope.spawn(|| {
|
||||
pool.install(|| {
|
||||
let _drain = Draining::enter(Some(0)).unwrap();
|
||||
assert!(Draining::enter(None).is_none(), "a lane cannot start a nested drain");
|
||||
assert!(DRAINING_LANE.get(), "nested rejection must not clear the lane marker");
|
||||
started.send(()).unwrap();
|
||||
tx.send(slots.for_render(2).is_some()).unwrap();
|
||||
});
|
||||
});
|
||||
ready.recv_timeout(std::time::Duration::from_secs(2)).unwrap();
|
||||
let early = rx.recv_timeout(std::time::Duration::from_millis(100));
|
||||
drop((first, second));
|
||||
let admitted = early.or_else(|_| rx.recv_timeout(std::time::Duration::from_secs(2)));
|
||||
(early, admitted)
|
||||
});
|
||||
assert!(early.is_err(), "busy dedicated lane must wait instead of falling back to CPU");
|
||||
assert_eq!(admitted, Ok(true));
|
||||
pool.install(|| {
|
||||
assert!(!DRAINING.get());
|
||||
assert!(!DRAINING_LANE.get());
|
||||
let panic = std::panic::catch_unwind(|| {
|
||||
let _drain = Draining::enter(Some(0)).unwrap();
|
||||
panic!("synthetic lane failure");
|
||||
});
|
||||
assert!(panic.is_err());
|
||||
assert!(!DRAINING.get());
|
||||
assert!(!DRAINING_LANE.get());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -414,7 +414,8 @@ tests, device errors are logged rather than fatal. A viewer frame that runs out
|
||||
(`Gpu::within_memory`) renders on the CPU, the uploaded layers and pooled textures are freed, and
|
||||
the RAM preview keeps half as many GPU frames from then on. However many frame threads the viewer
|
||||
runs, at most two of them start GPU render attempts at once. Shared-pool workers use the CPU
|
||||
when both slots are busy rather than blocking Rayon helpers; other render threads wait, so
|
||||
when both slots are busy rather than blocking Rayon helpers; dedicated single-thread preview
|
||||
lanes wait for admission and remain on the GPU, so
|
||||
working-texture pressure is bounded by concurrent attempts rather than the thread count (#3028).
|
||||
A single large frame or retained frames can still exceed the available video memory. The same guard catches a frame's
|
||||
validation errors (an invalid texture, often left by a failed allocation): that frame renders on
|
||||
|
||||
Reference in New Issue
Block a user