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}