diff --git a/quest/m0/README.md b/quest/m0/README.md index 79eed49..196d230 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -102,7 +102,6 @@ repository. - [Browser sources on the GPU under Mesa](/quest/m0/cef-headless.md) - shared-texture pages paint on Intel and AMD under Wayland, and a page that cannot paint fails loudly - [A browser helper exits with the studio](/quest/m0/browser-helper-exit.md) - no orphaned CEF helper after a crash, kill, or timed-out test - [Nightly and the browser suite pass](/quest/m0/nightly-green.md) - render-sw, cookies, and the X11 clipboard test fixed at their causes -- [GPU tests exit cleanly on NVIDIA](/quest/m0/gpu-tests-exit.md) - compositor tests no longer crash on exit on NVIDIA - [The Apple signing identity is available](/quest/m0/signing.md) - the Apple credentials packaging and the macOS camera need; Windows ships unsigned - [Packaging](/quest/m0/packaging.md) - a signed macOS app, a Windows installer, and a Flatpak for Linux - [The persistable portal restore token is pinned](/quest/m0/pin-portal-token.md) - moq-video's restore token, for Wayland sources diff --git a/quest/m0/gpu-tests-exit.md b/quest/m0/gpu-tests-exit.md deleted file mode 100644 index 0bc3071..0000000 --- a/quest/m0/gpu-tests-exit.md +++ /dev/null @@ -1,17 +0,0 @@ -# [S] GPU tests exit cleanly on NVIDIA - -## Goal - -The compositor's GPU tests pass and exit cleanly on NVIDIA under Linux, as -they do on the software driver and on Metal. - -## Plan - -From [moq-dev/studio#74](https://github.com/moq-dev/studio/pull/74), picked -2026-11-07 (maintainer, /quest-complete), m0 by the rule that fixes join m0. - -On nixos (RTX 3070 Ti, NVIDIA 580), 7 compositor GPU tests, mostly the -overlay ones, pass and then crash when the process exits. The same happens -with `main`'s Vulkan setup and without CEF, and not on Mesa's software -driver. Reproduce, find the cause (teardown order of the wgpu device and what -holds it is a first suspect), and fix it rather than skipping the tests. diff --git a/rs/moq-scene/src/matte.rs b/rs/moq-scene/src/matte.rs index 9b8f980..1d554d1 100644 --- a/rs/moq-scene/src/matte.rs +++ b/rs/moq-scene/src/matte.rs @@ -66,8 +66,8 @@ fn segment(model: &Model, rgba: &[u8]) -> Result, String> { run().map_err(|err| format!("the segmentation model: {err}")) } -/// Cuts a person out of one source's pictures. Dropping it stops its thread -/// once the picture in hand is done. +/// Cuts a person out of one source's pictures. Dropping it waits for the +/// picture in hand, then stops its thread. pub(crate) struct Matte { device: wgpu::Device, queue: wgpu::Queue, @@ -83,8 +83,13 @@ pub(crate) struct Matte { mask_bind: wgpu::BindGroup, /// The newest picture started, and its cut-out, at its size. held: Option, - jobs: mpsc::Sender, + /// Dropped first on drop, which ends the thread. + jobs: Option>, results: mpsc::Receiver, String>>, + /// Joined on drop, since the thread frees GPU objects on its way out: one + /// left running past `main` does so while the driver tears down, which + /// crashes NVIDIA's. + thread: Option>, /// Whether the thread has a picture, which also has `readback` mapped. busy: bool, /// Whether the picture the thread has was overtaken, so its cut-out is @@ -211,13 +216,13 @@ impl Matte { let (jobs, inbox) = mpsc::channel(); let (outbox, results) = mpsc::channel(); - let thread = Thread { + let worker = Thread { device: device.clone(), readback: readback.clone(), }; - std::thread::Builder::new() + let thread = std::thread::Builder::new() .name("moq-scene matte".into()) - .spawn(move || thread.run(inbox, outbox)) + .spawn(move || worker.run(inbox, outbox)) .map_err(|err| Error::Unsupported(format!("cannot start background removal: {err}")))?; Ok(Self { @@ -233,8 +238,9 @@ impl Matte { mask, mask_bind, held: None, - jobs, + jobs: Some(jobs), results, + thread: Some(thread), busy: false, stale: false, #[cfg(test)] @@ -276,7 +282,8 @@ impl Matte { let submission = self.queue.submit([encoder.finish()]); self.held = Some(held); // A thread that is gone reports so through `finish`. - let _ = self.jobs.send(submission); + let jobs = self.jobs.as_ref().expect("the job sender lives until drop"); + let _ = jobs.send(submission); self.busy = true; } @@ -413,6 +420,15 @@ impl Matte { } } +impl Drop for Matte { + fn drop(&mut self) { + drop(self.jobs.take()); + if let Some(thread) = self.thread.take() { + let _ = thread.join(); + } + } +} + /// What a matte's thread holds: the device to wait on, and the buffer the /// shrunk picture lands in. struct Thread { diff --git a/rs/moq-scene/src/overlay.rs b/rs/moq-scene/src/overlay.rs index 15e32c0..a0cee60 100644 --- a/rs/moq-scene/src/overlay.rs +++ b/rs/moq-scene/src/overlay.rs @@ -904,13 +904,18 @@ fn fonts() -> FontContext { /// The overlays a compositor draws. Their documents live on a thread of their /// own, since a Blitz document cannot leave the thread that made it, and a -/// panic there fails the overlays rather than the compositor. +/// panic there fails the overlays rather than the compositor. Dropping it +/// stops the thread and waits for it. pub(crate) struct Overlays { device: wgpu::Device, requests: mpsc::Sender, /// What each paint failed on, by overlay. painted: mpsc::Receiver>, next: u64, + /// Joined on drop, since the thread frees GPU objects on its way out: one + /// left running past `main` does so while the driver tears down, which + /// crashes NVIDIA's. + thread: Option>, } enum Request { @@ -928,6 +933,8 @@ enum Request { Restart(u64, Instant), /// Paint these overlays as of this time on the program clock. Paint(Instant, Vec), + /// End the thread, though an [`Overlay`] may still hold a sender. + Stop, /// Stop the thread the way a panic in Blitz would. #[cfg(test)] Panic, @@ -939,7 +946,7 @@ impl Overlays { let (requests, received) = mpsc::channel(); let (reply, painted) = mpsc::channel(); let (device, queue) = (device.clone(), queue.clone()); - std::thread::Builder::new() + let thread = std::thread::Builder::new() .name("overlays".into()) .spawn({ let device = device.clone(); @@ -951,6 +958,7 @@ impl Overlays { requests, painted, next: 0, + thread: Some(thread), }) } @@ -1008,6 +1016,17 @@ impl Overlays { } } +impl Drop for Overlays { + fn drop(&mut self) { + // A thread that already stopped has nothing left to free, and one + // that panicked already failed every overlay. + let _ = self.requests.send(Request::Stop); + if let Some(thread) = self.thread.take() { + let _ = thread.join(); + } + } +} + #[cfg(test)] impl Overlays { /// Stop the thread as a panic in Blitz would. @@ -1056,7 +1075,7 @@ impl Drop for Overlay { } } -/// The overlay thread: serve requests until every sender is gone. +/// The overlay thread: serve requests until told to stop. fn run( device: &wgpu::Device, queue: &wgpu::Queue, @@ -1099,6 +1118,7 @@ fn run( document.painted = false; } } + Request::Stop => return, #[cfg(test)] Request::Panic => panic!("a test stopped the overlay thread"), Request::Paint(now, ids) => {