Skip to main content

ashlar_app/
renderer.rs

1//! The pixels behind [`Driver`](crate::Driver) screenshots. By default there are none: a
2//! screenshot is a flat grey image of the screen's size, which keeps GPU-less tests fast.
3//! [`Renderer::wgpu`] renders real frames on a wgpu device with no window or surface, through
4//! the same off-screen path a covered window uses, so `ui.screenshot`, annotate and trace give
5//! real pixels in tests and CI.
6
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9
10/// How a [`Driver`](crate::Driver) renders screenshots. Cheap to clone (the device is shared).
11#[derive(Clone, Default)]
12pub struct Renderer {
13    #[cfg(not(target_arch = "wasm32"))]
14    state: Option<eframe::egui_wgpu::RenderState>,
15    /// The last off-screen render worked (false until the first one).
16    ok: Arc<AtomicBool>,
17    /// Why [`Renderer::wgpu`] gave no GPU renderer.
18    reason: Option<String>,
19}
20
21impl std::fmt::Debug for Renderer {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("Renderer").field("name", &self.name()).field("adapter", &self.adapter()).field("reason", &self.reason).finish()
24    }
25}
26
27impl Renderer {
28    /// No GPU: screenshots are flat grey (the default).
29    pub fn none() -> Self {
30        Self::default()
31    }
32
33    /// Render on a wgpu device with no window. Without an adapter (or on the web) this is
34    /// [`Renderer::none`], with [`Renderer::reason`] saying why; it never panics.
35    pub fn wgpu() -> Self {
36        #[cfg(not(target_arch = "wasm32"))]
37        {
38            let made = std::panic::catch_unwind(gpu::create).unwrap_or_else(|_| Err("creating the wgpu device panicked".into()));
39            match made {
40                Ok(state) => Renderer { state: Some(state), ..Self::default() },
41                Err(e) => {
42                    log::info!("headless renderer: no GPU ({e}); screenshots are grey");
43                    Renderer { reason: Some(e), ..Self::default() }
44                }
45            }
46        }
47        #[cfg(target_arch = "wasm32")]
48        Renderer { reason: Some("no headless GPU renderer on the web".into()), ..Self::default() }
49    }
50
51    /// A GPU renderer is set up.
52    pub fn is_gpu(&self) -> bool {
53        #[cfg(not(target_arch = "wasm32"))]
54        return self.state.is_some();
55        #[cfg(target_arch = "wasm32")]
56        false
57    }
58
59    /// `"wgpu"` or `"none"`.
60    pub fn name(&self) -> &'static str {
61        if self.is_gpu() { "wgpu" } else { "none" }
62    }
63
64    /// Why [`Renderer::wgpu`] gave no GPU renderer.
65    pub fn reason(&self) -> Option<&str> {
66        self.reason.as_deref()
67    }
68
69    /// The adapter in use, e.g. `"Apple M2 (Metal)"`.
70    pub fn adapter(&self) -> Option<String> {
71        #[cfg(not(target_arch = "wasm32"))]
72        return self.state.as_ref().map(|s| {
73            let i = s.adapter.get_info();
74            format!("{} ({:?})", i.name, i.backend)
75        });
76        #[cfg(target_arch = "wasm32")]
77        None
78    }
79
80    /// The adapter's name and backend, e.g. `("Apple M2", "Metal")`.
81    pub(crate) fn adapter_parts(&self) -> Option<(String, String)> {
82        #[cfg(not(target_arch = "wasm32"))]
83        return self.state.as_ref().map(|s| {
84            let i = s.adapter.get_info();
85            (i.name, format!("{:?}", i.backend))
86        });
87        #[cfg(target_arch = "wasm32")]
88        None
89    }
90
91    /// What the last screenshot was rendered with: `"wgpu"`, or `"none"` when there is no GPU
92    /// renderer or its last render failed (the grey image was used).
93    pub(crate) fn last_used(&self) -> &'static str {
94        if self.is_gpu() && self.ok.load(Ordering::Relaxed) { "wgpu" } else { "none" }
95    }
96
97    /// The debugger's off-screen renderer on this device (`None`: no GPU).
98    pub(crate) fn offscreen(&self) -> Option<ashlar_ui::devtools::Offscreen> {
99        #[cfg(not(target_arch = "wasm32"))]
100        if let Some(state) = &self.state {
101            let inner = crate::offscreen::renderer(state.clone());
102            let ok = Arc::clone(&self.ok);
103            return Some(Arc::new(move |f: &ashlar_ui::devtools::OffscreenFrame<'_>| {
104                let r = inner(f);
105                ok.store(r.is_ok(), Ordering::Relaxed);
106                r
107            }));
108        }
109        None
110    }
111
112    /// Hand a frame's texture changes to the device, as a window's backend does after each frame.
113    pub(crate) fn textures(&self, delta: &egui::TexturesDelta) {
114        #[cfg(not(target_arch = "wasm32"))]
115        if let Some(state) = &self.state {
116            let applied = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| crate::offscreen::apply_textures(state, delta)));
117            if applied.is_err() {
118                log::warn!("headless renderer: uploading textures panicked");
119                self.ok.store(false, Ordering::Relaxed);
120            }
121        }
122        #[cfg(target_arch = "wasm32")]
123        let _ = delta;
124    }
125}
126
127#[cfg(not(target_arch = "wasm32"))]
128mod gpu {
129    use std::future::Future;
130    use std::sync::Arc;
131    use std::task::{Context, Poll, Wake, Waker};
132
133    use eframe::egui_wgpu::{RenderState, RendererOptions, WgpuConfiguration, WgpuSetup, WgpuSetupCreateNew, wgpu};
134
135    /// A device with no surface. Software adapters first (the same pixels on every machine that
136    /// has one), then a discrete GPU, then any other.
137    pub(super) fn create() -> Result<RenderState, String> {
138        let mut setup = WgpuSetupCreateNew::without_display_handle();
139        setup.instance_descriptor.backends.remove(wgpu::Backends::BROWSER_WEBGPU);
140        setup.native_adapter_selector = Some(Arc::new(|adapters, _surface| {
141            let mut adapters: Vec<&wgpu::Adapter> = adapters.iter().filter(|a| a.get_info().backend != wgpu::Backend::Noop).collect();
142            adapters.sort_by_key(|a| match a.get_info().device_type {
143                wgpu::DeviceType::Cpu => 0,
144                wgpu::DeviceType::DiscreteGpu => 1,
145                _ => 2,
146            });
147            adapters.first().map(|a| (*a).clone()).ok_or_else(|| "no wgpu adapter found".to_owned())
148        }));
149        let setup = WgpuSetup::CreateNew(setup);
150        let instance = block_on(setup.new_instance());
151        let config = WgpuConfiguration { wgpu_setup: setup, ..Default::default() };
152        block_on(RenderState::create(&config, &instance, None, RendererOptions::PREDICTABLE)).map_err(|e| e.to_string())
153    }
154
155    struct Unpark(std::thread::Thread);
156
157    impl Wake for Unpark {
158        fn wake(self: Arc<Self>) {
159            self.0.unpark();
160        }
161    }
162
163    /// Run a future to completion on this thread (wgpu's setup futures resolve without an
164    /// executor on native).
165    fn block_on<F: Future>(f: F) -> F::Output {
166        let waker = Waker::from(Arc::new(Unpark(std::thread::current())));
167        let mut cx = Context::from_waker(&waker);
168        let mut f = std::pin::pin!(f);
169        loop {
170            if let Poll::Ready(v) = f.as_mut().poll(&mut cx) {
171                return v;
172            }
173            std::thread::park();
174        }
175    }
176}