Skip to main content

frus_gpu/
renderer.rs

1//! The renderer bound to a surface (a window): it configures wgpu and delegates
2//! primitive drawing to the [`Painter`].
3
4use frus_core::Scene;
5
6use crate::compositor::{preferred_sample_count, Painters};
7
8/// The background colour, a midnight blue.
9const CLEAR_COLOR: wgpu::Color = wgpu::Color {
10    r: 0.05,
11    g: 0.05,
12    b: 0.08,
13    a: 1.0,
14};
15
16/// **The window's background** where nothing is drawn — the clear colour, as a scene colour —
17/// for a [`see-through`](Renderer::see_through) renderer, whose frames start transparent and
18/// whose owner paints it where the window is not meant to be seen through (milestone 643).
19pub fn backdrop() -> frus_core::Color {
20    // The clear colour is linear; a scene colour is sRGB.
21    frus_core::Color::rgb(
22        CLEAR_COLOR.r as f32,
23        CLEAR_COLOR.g as f32,
24        CLEAR_COLOR.b as f32,
25    )
26    .to_srgb()
27}
28
29/// Holds the GPU state bound to a surface and presents the frames.
30pub struct Renderer {
31    surface: wgpu::Surface<'static>,
32    device: wgpu::Device,
33    queue: wgpu::Queue,
34    config: wgpu::SurfaceConfiguration,
35    /// The format the frames are drawn in: the surface's own when it is sRGB, its sRGB twin
36    /// (a view of the same image) when the surface offers none, as a WebGPU canvas (#234).
37    render_format: wgpu::TextureFormat,
38    painters: Painters,
39    /// What making it took, phase by phase.
40    startup: StartupTimes,
41    /// Set by the device when it is lost.
42    lost: std::sync::Arc<std::sync::atomic::AtomicBool>,
43    /// Where the last frame's rendering spent its time.
44    timings: RenderTimings,
45    /// The GPU's own timing of each frame, where the device has a clock (milestone 612).
46    gpu_timer: Option<crate::gpu_timer::GpuTimer>,
47    /// Whether the frames are composed with what is behind the window (milestone 643).
48    see_through: bool,
49}
50
51impl Renderer {
52    /// Initialises the GPU context for a given surface.
53    ///
54    /// `target` is typically an `Arc<Window>` supplied by the platform layer.
55    /// `width` and `height` must both be > 0.
56    pub async fn new(
57        target: impl Into<wgpu::SurfaceTarget<'static>>,
58        width: u32,
59        height: u32,
60    ) -> anyhow::Result<Self> {
61        Self::make(target, width, height, false).await
62    }
63
64    /// **A renderer whose frames let what is behind the window show** where they are
65    /// transparent (milestone 643): its frames start transparent, are composed with the
66    /// window's surroundings by the system's compositor, and are made of premultiplied
67    /// colours — which is what this renderer's blending produces over a transparent start.
68    ///
69    /// On Windows it presents through DirectComposition (Direct3D 12): where a frame is
70    /// transparent on the title bar's line, the system's own caption shows — its backdrop
71    /// and its three buttons. Fails where that is not available — another system, no
72    /// Direct3D 12 adapter, no premultiplied composition — and the caller then makes an
73    /// ordinary [`new`](Self::new) one. Its owner paints [`backdrop`] wherever the window is
74    /// not meant to be seen through.
75    pub async fn see_through(
76        target: impl Into<wgpu::SurfaceTarget<'static>>,
77        width: u32,
78        height: u32,
79    ) -> anyhow::Result<Self> {
80        Self::make(target, width, height, true).await
81    }
82
83    /// Whether this renderer's frames let what is behind the window show where they are
84    /// transparent: made by [`see_through`](Self::see_through).
85    pub fn sees_through(&self) -> bool {
86        self.see_through
87    }
88
89    async fn make(
90        target: impl Into<wgpu::SurfaceTarget<'static>>,
91        width: u32,
92        height: u32,
93        see_through: bool,
94    ) -> anyhow::Result<Self> {
95        if see_through && !cfg!(windows) {
96            anyhow::bail!("a see-through window is composed by DirectComposition, on Windows");
97        }
98        let started = web_time::Instant::now();
99        let mut descriptor = wgpu::InstanceDescriptor {
100            backends: wgpu::Backends::PRIMARY,
101            ..wgpu::InstanceDescriptor::new_without_display_handle()
102        };
103        if see_through {
104            // Presented through a DirectComposition visual made from the window: the one
105            // way a Direct3D swap chain is composed with what is behind it.
106            descriptor.backends = wgpu::Backends::DX12;
107            descriptor.backend_options.dx12.presentation_system =
108                wgpu::Dx12SwapchainKind::DxgiFromVisual;
109        }
110        // No debug names for GPU objects on Android: a debug build asks for them, and the
111        // emulator's Vulkan driver crashes in `vkSetDebugUtilsObjectNameEXT` as the device is
112        // made (a release build never asks). `WGPU_DEBUG=1` still asks for them on a device.
113        #[cfg(target_os = "android")]
114        if std::env::var_os("WGPU_DEBUG").is_none() {
115            descriptor.flags.remove(wgpu::InstanceFlags::DEBUG);
116        }
117        // Android and Linux draw through Vulkan alone. Where there is none — an old device, a
118        // driver missing — or only the Android emulator's (gfxstream, which crashes in its own
119        // code at the first submission), the GPU is reached through OpenGL ES instead
120        // (milestones 681, 686).
121        #[cfg(any(target_os = "android", target_os = "linux"))]
122        if let Some(why) = vulkan_unusable(descriptor.flags).await {
123            log::info!("{why}: OpenGL ES instead");
124            descriptor.backends = wgpu::Backends::GL;
125        }
126        // `WGPU_BACKEND` (`vulkan`, `gl`, `dx12`, `metal`, `webgpu`, comma-separated) decides
127        // over all of the above: a device whose driver frus's choice fails on can be given
128        // another backend without rebuilding the application (milestone 686, #217).
129        if let Some(backends) = wgpu::Backends::from_env() {
130            log::info!("GPU backends from WGPU_BACKEND: {backends:?}");
131            descriptor.backends = backends;
132        }
133        // DirectX 12 compiles the shaders with DXC when its DLL is beside the program —
134        // `frus run` and `frus build desktop` put it there —, several times faster than
135        // Windows' own compiler (FXC): about half a second of a start (milestone 684, #217).
136        #[cfg(windows)]
137        {
138            let dxc = std::env::current_exe()
139                .ok()
140                .and_then(|exe| Some(exe.parent()?.join("dxcompiler.dll")))
141                .filter(|dll| dll.is_file());
142            match dxc {
143                Some(dll) => {
144                    log::info!("DirectX 12 shaders: DXC ({})", dll.display());
145                    descriptor.backend_options.dx12.shader_compiler =
146                        wgpu::Dx12Compiler::DynamicDxc {
147                            dxc_path: dll.display().to_string(),
148                        };
149                }
150                None => log::info!(
151                    "DirectX 12 shaders: Windows' own compiler (no dxcompiler.dll beside the \
152                     program: a slower start)"
153                ),
154            }
155        }
156        let instance = wgpu::Instance::new(descriptor);
157
158        let surface = instance.create_surface(target)?;
159
160        let adapter = instance
161            .request_adapter(&wgpu::RequestAdapterOptions {
162                // The discrete GPU where there is a choice — but not in a browser, which
163                // ignores the hint and **says so** in the console (Chrome, on Windows) on
164                // every page load, for something the page cannot change.
165                power_preference: if cfg!(target_arch = "wasm32") {
166                    wgpu::PowerPreference::None
167                } else {
168                    wgpu::PowerPreference::HighPerformance
169                },
170                compatible_surface: Some(&surface),
171                force_fallback_adapter: false,
172                ..Default::default()
173            })
174            .await?;
175
176        log::info!("GPU adapter: {:?}", adapter.get_info());
177
178        // Downlevel limits, for GLES compatibility, but with the adapter's **real**
179        // resolution: on mobile a screen of, say, 1080x2340 exceeds the downlevel
180        // maximum texture size of 2048, and without this `surface.configure` panics.
181        // A GPU without compute shaders (OpenGL ES 3.0, as the Android emulator's) cannot give
182        // even the downlevel compute limits: the WebGL 2 ones, which ask for none.
183        let downlevel = if adapter
184            .get_downlevel_capabilities()
185            .flags
186            .contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
187        {
188            wgpu::Limits::downlevel_defaults()
189        } else {
190            wgpu::Limits::downlevel_webgl2_defaults()
191        };
192        let required_limits = downlevel.using_resolution(adapter.limits());
193
194        let adapter_time = started.elapsed();
195        let device_started = web_time::Instant::now();
196        let (device, queue) = adapter
197            .request_device(&wgpu::DeviceDescriptor {
198                label: Some("frus.device"),
199                // The GPU's own clock where it has one, for the frame statistics
200                // (milestone 612). Nothing is asked of a device that has not.
201                required_features: adapter.features()
202                    & (wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::PIPELINE_CACHE),
203                required_limits,
204                memory_hints: wgpu::MemoryHints::Performance,
205                ..Default::default()
206            })
207            .await?;
208        let device_time = device_started.elapsed();
209        let surface_started = web_time::Instant::now();
210        // A lost device — a driver reset, a GPU removed — says so here; the renderer then
211        // answers `DeviceLost`, and its owner makes a new one (milestone 686).
212        let lost = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
213        {
214            let lost = lost.clone();
215            device.set_device_lost_callback(move |reason, message| {
216                log::warn!("the GPU device was lost ({reason:?}): {message}");
217                lost.store(true, std::sync::atomic::Ordering::SeqCst);
218            });
219        }
220
221        let caps = surface.get_capabilities(&adapter);
222        let alpha_mode = if see_through {
223            if !caps
224                .alpha_modes
225                .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
226            {
227                anyhow::bail!(
228                    "no premultiplied composition on this surface: {:?}",
229                    caps.alpha_modes
230                );
231            }
232            wgpu::CompositeAlphaMode::PreMultiplied
233        } else {
234            caps.alpha_modes[0]
235        };
236        let format = caps
237            .formats
238            .iter()
239            .copied()
240            .find(|f| f.is_srgb())
241            .unwrap_or(caps.formats[0]);
242        // The shaders write linear values, which an sRGB target encodes. A WebGPU canvas
243        // offers only plain formats (`bgra8unorm`): drawn as they are, the linear values are
244        // shown as if encoded, and every colour comes out darker (#234). The canvas lets its
245        // image be drawn through an sRGB view: the frames are drawn in that twin, and the
246        // hardware encodes, as everywhere else.
247        let can_view = adapter
248            .get_downlevel_capabilities()
249            .flags
250            .contains(wgpu::DownlevelFlags::SURFACE_VIEW_FORMATS);
251        let (render_format, view_formats) = surface_formats(format, can_view);
252        if render_format != format.add_srgb_suffix() {
253            // Asking for the view anyway is a validation error, which wgpu treats as fatal: the
254            // Android emulator's OpenGL ES offers neither an sRGB surface nor views of one, and
255            // every application closed as it opened there. Drawn plain, it runs, a little dark.
256            log::warn!(
257                "this surface has no sRGB format ({format:?}) and cannot be viewed as one: \
258                 colours are drawn a little dark here"
259            );
260        }
261
262        let config = wgpu::SurfaceConfiguration {
263            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
264            format,
265            color_space: wgpu::SurfaceColorSpace::Auto,
266            width,
267            height,
268            // **Fifo, always** (milestone 610): one image per refresh of the display, in
269            // order. It was the first mode the surface offered, and on the test phone that
270            // was Mailbox, under which the application draws as fast as it can, the
271            // display shows whichever image is latest, and the rest are drawn for nothing:
272            // frames at 14 ms intervals on a 16.7 ms display, the battery spent on images
273            // never seen, and a motion that is not paced by the display. Fifo is the one
274            // mode every surface supports.
275            present_mode: wgpu::PresentMode::Fifo,
276            alpha_mode,
277            view_formats,
278            desired_maximum_frame_latency: 2,
279        };
280        log::info!(
281            "present mode: {:?}, of {:?}",
282            config.present_mode,
283            caps.present_modes
284        );
285        surface.configure(&device, &config);
286
287        // MSAA when the adapter supports it for this format; otherwise 1, disabled.
288        let sample_count = preferred_sample_count(&adapter, render_format);
289        log::info!("MSAA: {sample_count}×");
290
291        let surface_time = surface_started.elapsed();
292        let pipelines_started = web_time::Instant::now();
293        let pipeline_cache =
294            crate::pipeline_cache::open(&adapter, &device, crate::pipeline_cache::cache_dir());
295        let mut painters = Painters::new(
296            &device,
297            &queue,
298            render_format,
299            sample_count,
300            pipeline_cache.as_ref().map(|(cache, _, _)| cache),
301        );
302        let pipelines = pipelines_started.elapsed();
303        // Warms every pipeline before the first real frame, to avoid jank.
304        let warm_up_started = web_time::Instant::now();
305        painters.warm_up(&device, &queue, render_format);
306        // Kept once the warm-up has built what drivers build lazily, so the next launch finds
307        // every pipeline of a frame.
308        if let Some((cache, file, read)) = &pipeline_cache {
309            crate::pipeline_cache::keep(cache, file, read.as_deref());
310        }
311        let startup = StartupTimes {
312            adapter: adapter_time,
313            device: device_time,
314            surface: surface_time,
315            pipelines,
316            warm_up: warm_up_started.elapsed(),
317        };
318
319        let gpu_timer = crate::gpu_timer::GpuTimer::new(&device, &queue);
320        log::info!(
321            "GPU clock: {}",
322            if gpu_timer.is_some() {
323                "frames timed on the GPU"
324            } else {
325                "none, frames timed on the CPU only"
326            }
327        );
328        Ok(Self {
329            surface,
330            device,
331            queue,
332            config,
333            render_format,
334            gpu_timer,
335            painters,
336            startup,
337            lost,
338            timings: RenderTimings::default(),
339            see_through,
340        })
341    }
342
343    /// Whether the device is gone: see [`RenderOutcome::DeviceLost`].
344    pub fn is_lost(&self) -> bool {
345        self.lost.load(std::sync::atomic::Ordering::SeqCst)
346    }
347
348    /// Destroys the device as a driver reset would, for testing what follows: the next
349    /// [`Renderer::render`] says [`RenderOutcome::DeviceLost`].
350    #[doc(hidden)]
351    pub fn lose_device_for_testing(&self) {
352        self.device.destroy();
353    }
354
355    /// What making this renderer took, phase by phase (#211).
356    pub fn startup_times(&self) -> StartupTimes {
357        self.startup
358    }
359
360    /// Reconfigures the surface after the window is resized. The painters' viewports
361    /// are set every frame by `Painters::render`.
362    pub fn resize(&mut self, width: u32, height: u32) {
363        if width > 0 && height > 0 {
364            self.config.width = width;
365            self.config.height = height;
366            self.surface.configure(&self.device, &self.config);
367        }
368    }
369
370    /// Reapplies the current configuration, after a lost or outdated surface.
371    pub fn reconfigure(&mut self) {
372        self.surface.configure(&self.device, &self.config);
373    }
374
375    /// Where the last [`render`](Self::render) spent its time, in milliseconds: waiting for
376    /// a surface image to draw into, drawing into it (tessellating, encoding and submitting),
377    /// and handing it to the display (milestone 609). Zero on the web, which has no clock
378    /// here.
379    pub fn last_timings(&self) -> RenderTimings {
380        self.timings
381    }
382
383    /// Draws the scene — rectangles, images, paths, text, layers — and presents it.
384    pub fn render(&mut self, scene: &Scene) -> RenderOutcome {
385        // Without waiting: a lost device is reported when the device is polled (milestone 686).
386        let _ = self.device.poll(wgpu::PollType::Poll);
387        if self.lost.load(std::sync::atomic::Ordering::SeqCst) {
388            return RenderOutcome::DeviceLost;
389        }
390        let clock = Clock::start();
391        let frame = match self.surface.get_current_texture() {
392            wgpu::CurrentSurfaceTexture::Success(frame)
393            | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
394            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
395                return RenderOutcome::Skipped;
396            }
397            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
398                return RenderOutcome::NeedsReconfigure;
399            }
400            wgpu::CurrentSurfaceTexture::Validation => {
401                log::warn!("frame skipped: surface validation error");
402                return RenderOutcome::Skipped;
403            }
404        };
405        let acquired = clock.lap();
406        let timed = self
407            .gpu_timer
408            .as_mut()
409            .and_then(|timer| timer.begin(&self.device, &self.queue));
410        let view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
411            format: Some(self.render_format),
412            ..Default::default()
413        });
414
415        self.painters.render(
416            &self.device,
417            &self.queue,
418            self.render_format,
419            &view,
420            self.config.width,
421            self.config.height,
422            scene,
423            // A see-through frame starts transparent: what its owner paints is all there is.
424            Some(if self.see_through {
425                wgpu::Color::TRANSPARENT
426            } else {
427                CLEAR_COLOR
428            }),
429        );
430
431        if let (Some(timer), Some(at)) = (self.gpu_timer.as_mut(), timed) {
432            timer.end(&self.device, &self.queue, at);
433        }
434        let drawn = clock.lap();
435        self.queue.present(frame);
436        let presented = clock.lap();
437        self.timings = RenderTimings {
438            acquire: acquired,
439            draw: drawn - acquired,
440            present: presented - drawn,
441            gpu: self.gpu_timer.as_ref().and_then(|timer| timer.last_ms()),
442        };
443        RenderOutcome::Presented
444    }
445}
446
447/// Whether the Vulkan this device offers is unusable: only the Android emulator's (gfxstream, the
448/// "Goldfish" GPU), which crashes in its own code at the first submission — or none at all:
449/// why, then. `WGPU_BACKEND`, when set, decides instead.
450#[cfg(any(target_os = "android", target_os = "linux"))]
451async fn vulkan_unusable(flags: wgpu::InstanceFlags) -> Option<&'static str> {
452    if std::env::var_os("WGPU_BACKEND").is_some() {
453        return None;
454    }
455    let probe = wgpu::Instance::new(wgpu::InstanceDescriptor {
456        backends: wgpu::Backends::VULKAN,
457        flags,
458        ..wgpu::InstanceDescriptor::new_without_display_handle()
459    });
460    let adapters = probe.enumerate_adapters(wgpu::Backends::VULKAN).await;
461    let emulator = |adapter: &wgpu::Adapter| {
462        let info = adapter.get_info();
463        info.driver.eq_ignore_ascii_case("gfxstream") || info.name.contains("Goldfish")
464    };
465    if adapters.is_empty() {
466        Some("no Vulkan driver on this device")
467    } else if adapters.iter().all(emulator) {
468        Some("the emulator's Vulkan driver fails at the first frame")
469    } else {
470        None
471    }
472}
473
474/// Where a frame's rendering spent its time, in milliseconds. See
475/// [`Renderer::last_timings`].
476#[derive(Clone, Copy, Debug, Default, PartialEq)]
477pub struct RenderTimings {
478    /// Waiting for a surface image: time the display, not the drawing, decides.
479    pub acquire: f32,
480    /// Tessellating, encoding and submitting the scene.
481    pub draw: f32,
482    /// Handing the image to the display.
483    pub present: f32,
484    /// What the GPU took over the latest frame it has finished, by its own clock, a few
485    /// frames behind: `None` where the device has no clock (milestone 612).
486    pub gpu: Option<f32>,
487}
488
489/// Milliseconds since it started; nothing on the web, where `std` has no clock.
490struct Clock {
491    #[cfg(not(target_arch = "wasm32"))]
492    start: std::time::Instant,
493}
494
495impl Clock {
496    fn start() -> Self {
497        Self {
498            #[cfg(not(target_arch = "wasm32"))]
499            start: std::time::Instant::now(),
500        }
501    }
502
503    fn lap(&self) -> f32 {
504        #[cfg(not(target_arch = "wasm32"))]
505        return self.start.elapsed().as_secs_f32() * 1000.0;
506        #[cfg(target_arch = "wasm32")]
507        0.0
508    }
509}
510
511/// What happened when [`Renderer::render`] tried to present a frame.
512pub enum RenderOutcome {
513    /// The frame was drawn and presented.
514    Presented,
515    /// The frame was skipped (occlusion, timeout, or a validation error); the caller
516    /// should just try again on the next redraw.
517    Skipped,
518    /// The surface is lost or outdated: call [`Renderer::reconfigure`] before retrying.
519    NeedsReconfigure,
520    /// The GPU device is gone — a driver reset, a GPU removed or restarted: this renderer can
521    /// draw nothing more, and a new one is to be made (milestone 686, #217).
522    DeviceLost,
523}
524
525/// What making the renderer took, phase by phase (milestone 683, #211): a part of an
526/// application's time to its first frame. See [`Renderer::startup_times`].
527#[derive(Clone, Copy, Debug, Default, PartialEq)]
528pub struct StartupTimes {
529    /// The GPU instance, the surface and the adapter.
530    pub adapter: std::time::Duration,
531    /// The device and its queue.
532    pub device: std::time::Duration,
533    /// The surface's capabilities read, and the surface configured for the device
534    /// (milestone 696).
535    pub surface: std::time::Duration,
536    /// The pipelines and resources every frame draws with.
537    pub pipelines: std::time::Duration,
538    /// A frame drawn before the first real one, which builds what drivers build lazily.
539    pub warm_up: std::time::Duration,
540}
541
542/// The format the frames are drawn in, and the view formats the surface is configured with:
543/// the surface's own format when it is sRGB; its sRGB twin, through a view, when the device can
544/// view a surface in another format (the Web); and the plain format itself when it cannot.
545fn surface_formats(
546    format: wgpu::TextureFormat,
547    can_view: bool,
548) -> (wgpu::TextureFormat, Vec<wgpu::TextureFormat>) {
549    let srgb = format.add_srgb_suffix();
550    if srgb == format {
551        (format, vec![])
552    } else if can_view {
553        (srgb, vec![srgb])
554    } else {
555        (format, vec![])
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    /// An sRGB surface is drawn as it is; a plain one through its sRGB twin where views are
564    /// supported, and plain where they are not (the emulator's OpenGL ES), rather than asking
565    /// for a view the device refuses.
566    #[test]
567    fn a_surface_is_drawn_in_srgb_when_it_can_be() {
568        use wgpu::TextureFormat::*;
569        assert_eq!(
570            surface_formats(Bgra8UnormSrgb, false),
571            (Bgra8UnormSrgb, vec![])
572        );
573        assert_eq!(
574            surface_formats(Bgra8Unorm, true),
575            (Bgra8UnormSrgb, vec![Bgra8UnormSrgb])
576        );
577        assert_eq!(surface_formats(Rgba8Unorm, false), (Rgba8Unorm, vec![]));
578    }
579}