smudge_gpu/surface.rs
1//! Presenting frames on a wgpu surface, one full redraw at a time.
2//!
3//! Vello renders through a compute pass into a storage texture, and a
4//! surface texture cannot be bound as a storage texture on most backends.
5//! This module therefore renders the frame into an intermediate RGBA8
6//! texture of the surface's size and copies it onto the surface texture with
7//! a blit pipeline.
8
9use std::error::Error;
10use std::fmt;
11
12use smudge_core::app::FrameOutput;
13use vello::RenderParams;
14use vello::wgpu::util::TextureBlitter;
15use vello::wgpu::{
16 Adapter, CommandEncoderDescriptor, CompositeAlphaMode, CurrentSurfaceTexture, Device, Extent3d,
17 PresentMode, Queue, Surface, SurfaceColorSpace, SurfaceConfiguration, SurfaceTexture, Texture,
18 TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,
19 TextureViewDescriptor,
20};
21
22use crate::encoder::{Encoder, peniko_color};
23
24/// The surface formats the blit pattern supports: the intermediate texture is
25/// always RGBA8 and the blit converts while it samples.
26fn is_vello_format(format: &TextureFormat) -> bool {
27 matches!(
28 format,
29 TextureFormat::Rgba8Unorm | TextureFormat::Bgra8Unorm
30 )
31}
32
33/// Returns the first of `formats` the presenter can render into, keeping the
34/// surface's own preference order.
35fn pick_format(formats: &[TextureFormat]) -> Option<TextureFormat> {
36 formats.iter().copied().find(is_vello_format)
37}
38
39/// Something that stopped a [`GpuSurface`] from being created or from drawing
40/// a frame.
41#[derive(Debug)]
42pub enum GpuError {
43 /// The surface offered no format the presenter can render into.
44 UnsupportedFormat,
45 /// The surface is larger than the device can texture.
46 SurfaceTooLarge {
47 /// The width of the surface in physical pixels.
48 width: u32,
49 /// The height of the surface in physical pixels.
50 height: u32,
51 /// The largest side length the device supports.
52 limit: u32,
53 },
54 /// The window surface or the device was lost.
55 ///
56 /// The window must be rebuilt with a new surface to keep presenting.
57 SurfaceLost,
58 /// The device rejected a vello render, which means the device cannot run
59 /// the shaders vello needs or has become unusable.
60 Render(vello::Error),
61}
62
63impl fmt::Display for GpuError {
64 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 GpuError::UnsupportedFormat => write!(
67 formatter,
68 "the window surface offers no format the GPU renderer supports"
69 ),
70 GpuError::SurfaceTooLarge {
71 width,
72 height,
73 limit,
74 } => write!(
75 formatter,
76 "a {width}x{height} surface exceeds the device's texture limit of {limit}"
77 ),
78 GpuError::SurfaceLost => {
79 write!(formatter, "the window surface or the device was lost")
80 }
81 GpuError::Render(error) => write!(formatter, "the vello render failed: {error}"),
82 }
83 }
84}
85
86impl Error for GpuError {
87 fn source(&self) -> Option<&(dyn Error + 'static)> {
88 match self {
89 GpuError::Render(error) => Some(error),
90 GpuError::UnsupportedFormat
91 | GpuError::SurfaceTooLarge { .. }
92 | GpuError::SurfaceLost => None,
93 }
94 }
95}
96
97/// A GPU presenter that draws every frame onto a wgpu surface.
98///
99/// The presenter renders the frame's whole display list on every
100/// [`GpuSurface::draw`] call, whatever its damage says: the GPU path redraws
101/// the full frame. It owns the surface's configuration and hands out surface
102/// textures through [`GpuSurface::acquire`], so a caller can time the wait
103/// for a texture separately from the draw.
104///
105/// The surface is configured with the first format in the adapter's
106/// capability order that the presenter supports, with FIFO present mode, a
107/// latency of two frames, and automatic alpha composition.
108pub struct GpuSurface {
109 /// The device the textures and the pipelines live on.
110 device: Device,
111 /// The queue draws are submitted to.
112 queue: Queue,
113 /// The surface the frames present to.
114 surface: Surface<'static>,
115 /// The configuration the surface is presented under.
116 config: SurfaceConfiguration,
117 /// The vello renderer, which holds the compiled pipelines.
118 renderer: vello::Renderer,
119 /// The scene encoder of the current frame, reused across frames.
120 encoder: Encoder,
121 /// The pipeline that copies the rendered texture onto the surface.
122 blitter: TextureBlitter,
123 /// The intermediate texture vello renders into and its view.
124 target: (Texture, TextureView),
125 /// Whether the next [`GpuSurface::acquire`] must reconfigure the surface
126 /// first, set when a texture came back suboptimal. That texture must be
127 /// dropped before a reconfigure, and wgpu rejects a configure while one
128 /// is alive.
129 reconfigure: bool,
130}
131
132impl GpuSurface {
133 /// Configures `surface` for `adapter` and returns a presenter for it.
134 ///
135 /// The device's texture limit must fit the surface; a larger surface
136 /// returns [`GpuError::SurfaceTooLarge`] instead of a renderer.
137 ///
138 /// # Errors
139 ///
140 /// Returns [`GpuError::UnsupportedFormat`] when the surface offers no
141 /// format the presenter can render into, and
142 /// [`GpuError::SurfaceTooLarge`] when `width` or `height` exceeds the
143 /// device's `max_texture_dimension_2d` limit.
144 ///
145 /// # Panics
146 ///
147 /// Panics when the device cannot create the vello pipelines, which means
148 /// the device cannot run the shaders vello needs. That is a broken device
149 /// state rather than input a caller can provoke.
150 pub fn new(
151 surface: Surface<'static>,
152 adapter: &Adapter,
153 device: Device,
154 queue: Queue,
155 width: u32,
156 height: u32,
157 ) -> Result<GpuSurface, GpuError> {
158 let limit = device.limits().max_texture_dimension_2d;
159 if width > limit || height > limit {
160 return Err(GpuError::SurfaceTooLarge {
161 width,
162 height,
163 limit,
164 });
165 }
166 let capabilities = surface.get_capabilities(adapter);
167 let format = pick_format(&capabilities.formats).ok_or(GpuError::UnsupportedFormat)?;
168 let config = SurfaceConfiguration {
169 usage: TextureUsages::RENDER_ATTACHMENT,
170 format,
171 color_space: SurfaceColorSpace::Auto,
172 width,
173 height,
174 present_mode: PresentMode::Fifo,
175 desired_maximum_frame_latency: 2,
176 alpha_mode: CompositeAlphaMode::Auto,
177 view_formats: Vec::new(),
178 };
179 surface.configure(&device, &config);
180 let renderer = vello::Renderer::new(&device, vello::RendererOptions::default())
181 .expect("the device cannot create the vello pipelines");
182 let blitter = TextureBlitter::new(&device, format);
183 let target = create_target(&device, width, height);
184 Ok(GpuSurface {
185 device,
186 queue,
187 surface,
188 config,
189 renderer,
190 encoder: Encoder::new(),
191 blitter,
192 target,
193 reconfigure: false,
194 })
195 }
196
197 /// Reconfigures the surface and the intermediate texture for a new size.
198 ///
199 /// # Errors
200 ///
201 /// Returns [`GpuError::SurfaceTooLarge`] when `width` or `height`
202 /// exceeds the device's `max_texture_dimension_2d` limit. The presenter
203 /// keeps its old size in that case.
204 ///
205 /// # Panics
206 ///
207 /// Panics when `width` or `height` is zero, which cannot present: the
208 /// caller keeps the surface at its last size while the window has no
209 /// extent. Also panics, inside wgpu, when a texture from
210 /// [`GpuSurface::acquire`] is still alive, because a surface cannot be
211 /// reconfigured while one of its textures is out.
212 pub fn resize(&mut self, width: u32, height: u32) -> Result<(), GpuError> {
213 assert!(
214 width > 0 && height > 0,
215 "a resized surface must keep a positive extent"
216 );
217 let limit = self.device.limits().max_texture_dimension_2d;
218 if width > limit || height > limit {
219 return Err(GpuError::SurfaceTooLarge {
220 width,
221 height,
222 limit,
223 });
224 }
225 self.config.width = width;
226 self.config.height = height;
227 self.surface.configure(&self.device, &self.config);
228 self.reconfigure = false;
229 self.target = create_target(&self.device, width, height);
230 Ok(())
231 }
232
233 /// Returns the width the surface presents at, in physical pixels.
234 pub fn width(&self) -> u32 {
235 self.config.width
236 }
237
238 /// Returns the height the surface presents at, in physical pixels.
239 pub fn height(&self) -> u32 {
240 self.config.height
241 }
242
243 /// Waits for the next surface texture to draw onto.
244 ///
245 /// Draw or drop the returned texture before the next
246 /// [`GpuSurface::resize`], which cannot reconfigure the surface while it
247 /// is out. A surface that went out of date is reconfigured and asked once
248 /// more.
249 /// Returns `Ok(None)` when the frame must be skipped: the surface is
250 /// still out of date, is temporarily out of textures, or is covered.
251 /// Returns [`GpuError::SurfaceLost`] when the surface cannot present
252 /// anymore.
253 pub fn acquire(&mut self) -> Result<Option<SurfaceTexture>, GpuError> {
254 // No surface texture of the last frame is alive here: a drawn one was
255 // handed to the queue, a skipped one was dropped, so a reconfigure is
256 // safe now.
257 if self.reconfigure {
258 self.reconfigure = false;
259 self.surface.configure(&self.device, &self.config);
260 }
261 match self.surface.get_current_texture() {
262 CurrentSurfaceTexture::Outdated => {
263 self.surface.configure(&self.device, &self.config);
264 self.texture_from(self.surface.get_current_texture())
265 }
266 other => self.texture_from(other),
267 }
268 }
269
270 /// Turns one answer of the surface into the texture to draw onto, a
271 /// skipped frame, or a lost surface.
272 fn texture_from(
273 &mut self,
274 current: CurrentSurfaceTexture,
275 ) -> Result<Option<SurfaceTexture>, GpuError> {
276 match current {
277 CurrentSurfaceTexture::Success(texture) => Ok(Some(texture)),
278 CurrentSurfaceTexture::Suboptimal(texture) => {
279 // The surface no longer matches its configuration, most often
280 // after a resize. Reconfigure once the texture is dropped, so
281 // the next frame goes out at the right size.
282 self.reconfigure = true;
283 Ok(Some(texture))
284 }
285 CurrentSurfaceTexture::Timeout
286 | CurrentSurfaceTexture::Occluded
287 | CurrentSurfaceTexture::Outdated => Ok(None),
288 _ => Err(GpuError::SurfaceLost),
289 }
290 }
291
292 /// Renders `frame` onto `texture` and presents it.
293 ///
294 /// The whole display list is drawn and the whole surface is presented,
295 /// whatever [`FrameOutput::damage`] holds. The draw reaches the screen
296 /// asynchronously: the submission is queued when `draw` returns.
297 ///
298 /// # Errors
299 ///
300 /// Returns [`GpuError::Render`] when the device rejects the vello render,
301 /// which means it cannot run the shaders vello needs or has become
302 /// unusable. Nothing is presented then.
303 pub fn draw(
304 &mut self,
305 frame: &FrameOutput<'_>,
306 texture: SurfaceTexture,
307 ) -> Result<(), GpuError> {
308 let params = RenderParams {
309 base_color: peniko_color(frame.clear_color),
310 width: self.config.width,
311 height: self.config.height,
312 antialiasing_method: vello::AaConfig::Area,
313 };
314 let GpuSurface {
315 device,
316 queue,
317 renderer,
318 encoder,
319 blitter,
320 target,
321 ..
322 } = self;
323 encoder.encode(frame);
324 renderer
325 .render_to_texture(device, queue, encoder.scene(), &target.1, ¶ms)
326 .map_err(GpuError::Render)?;
327 let surface_view = texture
328 .texture
329 .create_view(&TextureViewDescriptor::default());
330 let mut command = device.create_command_encoder(&CommandEncoderDescriptor {
331 label: Some("smudge present"),
332 });
333 blitter.copy(device, &mut command, &target.1, &surface_view);
334 queue.submit([command.finish()]);
335 queue.present(texture);
336 Ok(())
337 }
338}
339
340/// Creates the intermediate texture vello renders into.
341///
342/// The caller keeps the extent above zero, because a surface without pixels
343/// has nothing to present and wgpu rejects empty textures.
344fn create_target(device: &Device, width: u32, height: u32) -> (Texture, TextureView) {
345 let texture = device.create_texture(&TextureDescriptor {
346 label: Some("smudge gpu surface"),
347 size: Extent3d {
348 width,
349 height,
350 depth_or_array_layers: 1,
351 },
352 mip_level_count: 1,
353 sample_count: 1,
354 dimension: TextureDimension::D2,
355 format: TextureFormat::Rgba8Unorm,
356 usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
357 view_formats: &[],
358 });
359 let view = texture.create_view(&TextureViewDescriptor::default());
360 (texture, view)
361}
362
363#[cfg(test)]
364#[path = "surface_tests.rs"]
365mod tests;