Files
lightcraft/crates/ui-egui/tests/shutdown.rs
hxwandClaude Opus 5.5 5a537d5d39 LR-BEHAV-GPU: no render is inside the GPU driver when the app quits (issue #620)
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>
2026-10-10 00:46:24 -05:00

41 lines
2.0 KiB
Rust

//! Issue #620: quitting while previews render must not leave a render worker running (on Linux
//! with the NVIDIA driver, one inside the GPU when the process exits is a SIGSEGV).
//! `LightcraftApp::shutdown` stops the render workers and closes the GPU for the whole process,
//! hence one test in a binary of its own.
use std::time::{Duration, Instant};
use lightcraft_ui_egui::headless::Headless;
use lightcraft_ui_egui::{LightcraftApp, Services};
#[test]
fn shutdown_leaves_no_render_worker_running() {
let services = Services { png: None, ..Default::default() };
let mut h = Headless::new(LightcraftApp::new(lightcraft_engine::Session::with_demo(), services), [1200.0, 760.0], 1.0);
// frames until the grid's thumbnails are being rendered on the workers
let t0 = Instant::now();
while h.app.renderer.live_workers() == 0 || h.app.renderer.in_flight() == 0 {
assert!(t0.elapsed() < Duration::from_secs(120), "no render started");
h.step();
}
assert!(!lightcraft_engine::gpu::shutting_down());
assert!(h.app.shutdown(Duration::from_secs(120)), "the jobs that were running ended in time");
assert_eq!(h.app.renderer.live_workers(), 0);
assert_eq!(h.app.renderer.queued(), 0, "jobs that had not started are dropped");
assert!(lightcraft_engine::gpu::shutting_down());
assert!(lightcraft_engine::gpu::wait_idle(Duration::ZERO), "nothing is inside the GPU");
// frames after it (the window's last ones) ask for renders that are not started any more
for _ in 0..3 {
h.step();
}
assert_eq!(h.app.renderer.live_workers(), 0);
assert_eq!(h.app.renderer.queued(), 0);
// a job that was caught before its render gives up: quitting doesn't wait for a CPU render
let id = h.app.session.active().or_else(|| h.app.session.catalog.photos().next().map(|p| p.id)).expect("a demo photo");
let job = h.app.session.render_job(id, 300, 200, false, true).expect("a render job");
assert_eq!(job.run().rendered.err().as_deref(), Some("LightCraft is closing"));
}