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    painters: Painters,
36    /// Where the last frame's rendering spent its time.
37    timings: RenderTimings,
38    /// The GPU's own timing of each frame, where the device has a clock (milestone 612).
39    gpu_timer: Option<crate::gpu_timer::GpuTimer>,
40    /// Whether the frames are composed with what is behind the window (milestone 643).
41    see_through: bool,
42}
43
44impl Renderer {
45    /// Initialises the GPU context for a given surface.
46    ///
47    /// `target` is typically an `Arc<Window>` supplied by the platform layer.
48    /// `width` and `height` must both be > 0.
49    pub async fn new(
50        target: impl Into<wgpu::SurfaceTarget<'static>>,
51        width: u32,
52        height: u32,
53    ) -> anyhow::Result<Self> {
54        Self::make(target, width, height, false).await
55    }
56
57    /// **A renderer whose frames let what is behind the window show** where they are
58    /// transparent (milestone 643): its frames start transparent, are composed with the
59    /// window's surroundings by the system's compositor, and are made of premultiplied
60    /// colours — which is what this renderer's blending produces over a transparent start.
61    ///
62    /// On Windows it presents through DirectComposition (Direct3D 12): where a frame is
63    /// transparent on the title bar's line, the system's own caption shows — its backdrop
64    /// and its three buttons. Fails where that is not available — another system, no
65    /// Direct3D 12 adapter, no premultiplied composition — and the caller then makes an
66    /// ordinary [`new`](Self::new) one. Its owner paints [`backdrop`] wherever the window is
67    /// not meant to be seen through.
68    pub async fn see_through(
69        target: impl Into<wgpu::SurfaceTarget<'static>>,
70        width: u32,
71        height: u32,
72    ) -> anyhow::Result<Self> {
73        Self::make(target, width, height, true).await
74    }
75
76    /// Whether this renderer's frames let what is behind the window show where they are
77    /// transparent: made by [`see_through`](Self::see_through).
78    pub fn sees_through(&self) -> bool {
79        self.see_through
80    }
81
82    async fn make(
83        target: impl Into<wgpu::SurfaceTarget<'static>>,
84        width: u32,
85        height: u32,
86        see_through: bool,
87    ) -> anyhow::Result<Self> {
88        if see_through && !cfg!(windows) {
89            anyhow::bail!("a see-through window is composed by DirectComposition, on Windows");
90        }
91        let mut descriptor = wgpu::InstanceDescriptor {
92            backends: wgpu::Backends::PRIMARY,
93            ..wgpu::InstanceDescriptor::new_without_display_handle()
94        };
95        if see_through {
96            // Presented through a DirectComposition visual made from the window: the one
97            // way a Direct3D swap chain is composed with what is behind it.
98            descriptor.backends = wgpu::Backends::DX12;
99            descriptor.backend_options.dx12.presentation_system =
100                wgpu::Dx12SwapchainKind::DxgiFromVisual;
101        }
102        let instance = wgpu::Instance::new(descriptor);
103
104        let surface = instance.create_surface(target)?;
105
106        let adapter = instance
107            .request_adapter(&wgpu::RequestAdapterOptions {
108                // The discrete GPU where there is a choice — but not in a browser, which
109                // ignores the hint and **says so** in the console (Chrome, on Windows) on
110                // every page load, for something the page cannot change.
111                power_preference: if cfg!(target_arch = "wasm32") {
112                    wgpu::PowerPreference::None
113                } else {
114                    wgpu::PowerPreference::HighPerformance
115                },
116                compatible_surface: Some(&surface),
117                force_fallback_adapter: false,
118                ..Default::default()
119            })
120            .await?;
121
122        log::info!("GPU adapter: {:?}", adapter.get_info());
123
124        // Downlevel limits, for GLES compatibility, but with the adapter's **real**
125        // resolution: on mobile a screen of, say, 1080x2340 exceeds the downlevel
126        // maximum texture size of 2048, and without this `surface.configure` panics.
127        let required_limits = wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits());
128
129        let (device, queue) = adapter
130            .request_device(&wgpu::DeviceDescriptor {
131                label: Some("frus.device"),
132                // The GPU's own clock where it has one, for the frame statistics
133                // (milestone 612). Nothing is asked of a device that has not.
134                required_features: adapter.features() & wgpu::Features::TIMESTAMP_QUERY,
135                required_limits,
136                memory_hints: wgpu::MemoryHints::Performance,
137                ..Default::default()
138            })
139            .await?;
140
141        let caps = surface.get_capabilities(&adapter);
142        let alpha_mode = if see_through {
143            if !caps
144                .alpha_modes
145                .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
146            {
147                anyhow::bail!(
148                    "no premultiplied composition on this surface: {:?}",
149                    caps.alpha_modes
150                );
151            }
152            wgpu::CompositeAlphaMode::PreMultiplied
153        } else {
154            caps.alpha_modes[0]
155        };
156        let format = caps
157            .formats
158            .iter()
159            .copied()
160            .find(|f| f.is_srgb())
161            .unwrap_or(caps.formats[0]);
162
163        let config = wgpu::SurfaceConfiguration {
164            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
165            format,
166            color_space: wgpu::SurfaceColorSpace::Auto,
167            width,
168            height,
169            // **Fifo, always** (milestone 610): one image per refresh of the display, in
170            // order. It was the first mode the surface offered, and on the test phone that
171            // was Mailbox, under which the application draws as fast as it can, the
172            // display shows whichever image is latest, and the rest are drawn for nothing:
173            // frames at 14 ms intervals on a 16.7 ms display, the battery spent on images
174            // never seen, and a motion that is not paced by the display. Fifo is the one
175            // mode every surface supports.
176            present_mode: wgpu::PresentMode::Fifo,
177            alpha_mode,
178            view_formats: vec![],
179            desired_maximum_frame_latency: 2,
180        };
181        log::info!(
182            "present mode: {:?}, of {:?}",
183            config.present_mode,
184            caps.present_modes
185        );
186        surface.configure(&device, &config);
187
188        // MSAA when the adapter supports it for this format; otherwise 1, disabled.
189        let sample_count = preferred_sample_count(&adapter, format);
190        log::info!("MSAA: {sample_count}×");
191
192        let mut painters = Painters::new(&device, &queue, format, sample_count);
193        // Warms every pipeline before the first real frame, to avoid jank.
194        painters.warm_up(&device, &queue, format);
195
196        let gpu_timer = crate::gpu_timer::GpuTimer::new(&device, &queue);
197        log::info!(
198            "GPU clock: {}",
199            if gpu_timer.is_some() {
200                "frames timed on the GPU"
201            } else {
202                "none, frames timed on the CPU only"
203            }
204        );
205        Ok(Self {
206            surface,
207            device,
208            queue,
209            config,
210            gpu_timer,
211            painters,
212            timings: RenderTimings::default(),
213            see_through,
214        })
215    }
216
217    /// Reconfigures the surface after the window is resized. The painters' viewports
218    /// are set every frame by `Painters::render`.
219    pub fn resize(&mut self, width: u32, height: u32) {
220        if width > 0 && height > 0 {
221            self.config.width = width;
222            self.config.height = height;
223            self.surface.configure(&self.device, &self.config);
224        }
225    }
226
227    /// Reapplies the current configuration, after a lost or outdated surface.
228    pub fn reconfigure(&mut self) {
229        self.surface.configure(&self.device, &self.config);
230    }
231
232    /// Where the last [`render`](Self::render) spent its time, in milliseconds: waiting for
233    /// a surface image to draw into, drawing into it (tessellating, encoding and submitting),
234    /// and handing it to the display (milestone 609). Zero on the web, which has no clock
235    /// here.
236    pub fn last_timings(&self) -> RenderTimings {
237        self.timings
238    }
239
240    /// Draws the scene — rectangles, images, paths, text, layers — and presents it.
241    pub fn render(&mut self, scene: &Scene) -> RenderOutcome {
242        let clock = Clock::start();
243        let frame = match self.surface.get_current_texture() {
244            wgpu::CurrentSurfaceTexture::Success(frame)
245            | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
246            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
247                return RenderOutcome::Skipped;
248            }
249            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
250                return RenderOutcome::NeedsReconfigure;
251            }
252            wgpu::CurrentSurfaceTexture::Validation => {
253                log::warn!("frame skipped: surface validation error");
254                return RenderOutcome::Skipped;
255            }
256        };
257        let acquired = clock.lap();
258        let timed = self
259            .gpu_timer
260            .as_mut()
261            .and_then(|timer| timer.begin(&self.device, &self.queue));
262        let view = frame
263            .texture
264            .create_view(&wgpu::TextureViewDescriptor::default());
265
266        self.painters.render(
267            &self.device,
268            &self.queue,
269            self.config.format,
270            &view,
271            self.config.width,
272            self.config.height,
273            scene,
274            // A see-through frame starts transparent: what its owner paints is all there is.
275            Some(if self.see_through {
276                wgpu::Color::TRANSPARENT
277            } else {
278                CLEAR_COLOR
279            }),
280        );
281
282        if let (Some(timer), Some(at)) = (self.gpu_timer.as_mut(), timed) {
283            timer.end(&self.device, &self.queue, at);
284        }
285        let drawn = clock.lap();
286        self.queue.present(frame);
287        let presented = clock.lap();
288        self.timings = RenderTimings {
289            acquire: acquired,
290            draw: drawn - acquired,
291            present: presented - drawn,
292            gpu: self.gpu_timer.as_ref().and_then(|timer| timer.last_ms()),
293        };
294        RenderOutcome::Presented
295    }
296}
297
298/// Where a frame's rendering spent its time, in milliseconds. See
299/// [`Renderer::last_timings`].
300#[derive(Clone, Copy, Debug, Default, PartialEq)]
301pub struct RenderTimings {
302    /// Waiting for a surface image: time the display, not the drawing, decides.
303    pub acquire: f32,
304    /// Tessellating, encoding and submitting the scene.
305    pub draw: f32,
306    /// Handing the image to the display.
307    pub present: f32,
308    /// What the GPU took over the latest frame it has finished, by its own clock, a few
309    /// frames behind: `None` where the device has no clock (milestone 612).
310    pub gpu: Option<f32>,
311}
312
313/// Milliseconds since it started; nothing on the web, where `std` has no clock.
314struct Clock {
315    #[cfg(not(target_arch = "wasm32"))]
316    start: std::time::Instant,
317}
318
319impl Clock {
320    fn start() -> Self {
321        Self {
322            #[cfg(not(target_arch = "wasm32"))]
323            start: std::time::Instant::now(),
324        }
325    }
326
327    fn lap(&self) -> f32 {
328        #[cfg(not(target_arch = "wasm32"))]
329        return self.start.elapsed().as_secs_f32() * 1000.0;
330        #[cfg(target_arch = "wasm32")]
331        0.0
332    }
333}
334
335/// What happened when [`Renderer::render`] tried to present a frame.
336pub enum RenderOutcome {
337    /// The frame was drawn and presented.
338    Presented,
339    /// The frame was skipped (occlusion, timeout, or a validation error); the caller
340    /// should just try again on the next redraw.
341    Skipped,
342    /// The surface is lost or outdated: call [`Renderer::reconfigure`] before retrying.
343    NeedsReconfigure,
344}