mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-25 10:37:11 -06:00
A render still running on a worker thread while the process exits crashes in the driver (SIGSEGV in the NVIDIA Vulkan driver on Linux), where neither the panic hook nor catch_unwind helps: pool workers were detached, the devices are statics that are never dropped, and nothing on the way out waited for GPU work. - gpu: every entry into a device (render, device creation, pool trim, denoise runner and tiles) is counted as work in flight (exit.rs). begin_shutdown() lets nothing new in; wait_idle(timeout) waits for what is inside. - preview: JobPool keeps its workers. shutdown(timeout) and Drop discard the queued jobs and wait, bounded, for the running ones (2 s on drop); never for the calling thread, and not twice. - engine: a render job caught by the shutdown gives up instead of rendering on the CPU, so quitting doesn't wait for it. - app: on_exit ends with LightcraftApp::shutdown (2 s for everything), after settings and library are saved; the macOS menu-bar Quit goes through on_exit. Not changed: the export, merge, denoise and import threads are still detached; their GPU work goes through the gate, their CPU work is cut off at exit as before. Tests: pool drop waits / is bounded / survives a panicked job and its own job dropping it; the gate; GPU renders refused and the device not created after begin_shutdown; the app's shutdown leaves no render worker. The crash itself needs Linux + NVIDIA and was not reproduced here (Windows, DX12): 9 of 9 quits of the app mid-render exit with code 0 in under 0.8 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.7 KiB
Rust
59 lines
2.7 KiB
Rust
//! Issue #620: a render still inside the GPU driver while the process exits crashes there (a
|
|
//! SIGSEGV no panic hook catches). Once `begin_shutdown` is called nothing starts on the GPU, and
|
|
//! `wait_idle` returns when the renders in flight on other threads have ended.
|
|
//! One test: the shutdown holds for the whole process. Without a GPU adapter only the CPU side of
|
|
//! the contract is checked.
|
|
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
use std::time::Duration;
|
|
|
|
use lightcraft_develop::DevelopSettings;
|
|
use lightcraft_pipeline::{RenderRequest, SourceInfo};
|
|
|
|
#[test]
|
|
fn nothing_renders_on_the_gpu_once_the_process_is_ending() {
|
|
let (w, h) = (600, 400);
|
|
let src = Arc::new(lightcraft_scenes::demo_library()[0].render(w, h));
|
|
let info = SourceInfo { raw: true, ..Default::default() };
|
|
let mut s = DevelopSettings::default();
|
|
s.light.exposure = 0.3;
|
|
let req = RenderRequest::fit(w, h);
|
|
let has_gpu = lightcraft_gpu::available();
|
|
if !has_gpu {
|
|
eprintln!("no GPU adapter ({:?}): only the CPU side is checked", lightcraft_gpu::unavailable_reason());
|
|
}
|
|
assert!(!lightcraft_gpu::shutting_down());
|
|
assert!(lightcraft_gpu::wait_idle(Duration::ZERO), "nothing is in flight yet");
|
|
|
|
// a worker that renders until the GPU refuses, as the preview workers do while the app quits
|
|
let (rendered, refused) = (Arc::new(AtomicUsize::new(0)), Arc::new(AtomicBool::new(false)));
|
|
let worker = {
|
|
let (src, info, s) = (src.clone(), info.clone(), s.clone());
|
|
let (rendered, refused) = (rendered.clone(), refused.clone());
|
|
std::thread::spawn(move || {
|
|
while lightcraft_gpu::render(&src, &info, &s, &req, None).is_some() {
|
|
rendered.fetch_add(1, Ordering::SeqCst);
|
|
}
|
|
refused.store(true, Ordering::SeqCst);
|
|
})
|
|
};
|
|
if has_gpu {
|
|
while rendered.load(Ordering::SeqCst) == 0 && !refused.load(Ordering::SeqCst) {
|
|
std::thread::yield_now();
|
|
}
|
|
assert!(rendered.load(Ordering::SeqCst) > 0, "the GPU renders before the shutdown ({:?})", lightcraft_gpu::last_fallback());
|
|
}
|
|
|
|
lightcraft_gpu::begin_shutdown();
|
|
assert!(lightcraft_gpu::shutting_down());
|
|
assert!(lightcraft_gpu::wait_idle(Duration::from_secs(60)), "the render in flight ends");
|
|
worker.join().unwrap();
|
|
assert!(refused.load(Ordering::SeqCst));
|
|
assert!(lightcraft_gpu::render(&src, &info, &s, &req, None).is_none(), "no GPU render once the process is ending");
|
|
assert!(lightcraft_gpu::wait_idle(Duration::ZERO));
|
|
if has_gpu {
|
|
assert_eq!(lightcraft_gpu::unavailable_reason().as_deref(), Some("LightCraft is closing"));
|
|
}
|
|
}
|