Skip to main content

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}