teksilo_render/device_health.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Whether a GPU device is still alive.
5//!
6//! wgpu reports a lost device through the one callback a device holds, and
7//! drops the error otherwise. Teksilo shares one device between every window
8//! (and every offscreen renderer) of a process, so the latch belongs to the
9//! device: [`DeviceHealth::install`] sets the callback once, where the device
10//! is opened, and every renderer built on that device reads the same flag
11//! (`Renderer::with_device_health`). A device the process destroys on purpose
12//! is not lost.
13//!
14//! The latch is carried with the device rather than looked up by it: wgpu
15//! compares devices by an id that two instances in one process can share, so
16//! a registry keyed by `wgpu::Device` would let one device's loss mark
17//! another.
18//!
19//! It carries the device's live textures too: the renderers that share a
20//! latch publish theirs after each render, and one whose texture lacks a
21//! commit another already holds copies that texture on the device instead of
22//! uploading the frame again.
23
24use std::sync::Arc;
25use std::sync::atomic::{AtomicBool, Ordering};
26
27use teksilo_canvas::live_image::internal::DeviceTextures;
28
29/// The lost-device latch of one device. `Clone` shares it.
30#[derive(Clone, Debug)]
31pub struct DeviceHealth {
32 lost: Arc<AtomicBool>,
33 /// The live textures of the renderers on the device.
34 live_textures: DeviceTextures<wgpu::Texture>,
35}
36
37impl DeviceHealth {
38 /// Install a lost-device latch on `device`. It replaces any lost-device
39 /// callback the device had: install it once per device, where it is
40 /// opened, and hand the latch to every renderer on that device.
41 pub fn install(device: &wgpu::Device) -> Self {
42 let health = Self {
43 lost: Arc::new(AtomicBool::new(false)),
44 live_textures: DeviceTextures::new(),
45 };
46 let lost = health.lost.clone();
47 device.set_device_lost_callback(move |reason, message| {
48 if reason == wgpu::DeviceLostReason::Destroyed {
49 return;
50 }
51 if !lost.swap(true, Ordering::AcqRel) {
52 eprintln!(
53 "teksilo-render: the GPU device was lost ({message}): nothing more reaches \
54 the screen, and live images stop counting frames"
55 );
56 }
57 });
58 health
59 }
60
61 /// Whether the device was lost. Once it is, it stays lost.
62 pub fn is_lost(&self) -> bool {
63 self.lost.load(Ordering::Acquire)
64 }
65
66 /// The live textures of the renderers built with this latch.
67 pub(crate) fn live_textures(&self) -> &DeviceTextures<wgpu::Texture> {
68 &self.live_textures
69 }
70
71 /// Whether `self` and `other` are the latch of one device.
72 pub fn same_device(&self, other: &Self) -> bool {
73 Arc::ptr_eq(&self.lost, &other.lost)
74 }
75
76 /// Test hook: behave as if the device were lost.
77 #[doc(hidden)]
78 pub fn mark_lost_for_testing(&self) {
79 self.lost.store(true, Ordering::Release);
80 }
81}