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. Request
137 /// the device with [`device_limits`](crate::device_limits): with the
138 /// default limits a large scene fails validation, which wgpu reports as
139 /// a panic.
140 ///
141 /// # Errors
142 ///
143 /// Returns [`GpuError::UnsupportedFormat`] when the surface offers no
144 /// format the presenter can render into, and
145 /// [`GpuError::SurfaceTooLarge`] when `width` or `height` exceeds the
146 /// device's `max_texture_dimension_2d` limit.
147 ///
148 /// # Panics
149 ///
150 /// Panics when the device cannot create the vello pipelines, which means
151 /// the device cannot run the shaders vello needs. That is a broken device
152 /// state rather than input a caller can provoke.
153 pub fn new(
154 surface: Surface<'static>,
155 adapter: &Adapter,
156 device: Device,
157 queue: Queue,
158 width: u32,
159 height: u32,
160 ) -> Result<GpuSurface, GpuError> {
161 let limit = device.limits().max_texture_dimension_2d;
162 if width > limit || height > limit {
163 return Err(GpuError::SurfaceTooLarge {
164 width,
165 height,
166 limit,
167 });
168 }
169 let capabilities = surface.get_capabilities(adapter);
170 let format = pick_format(&capabilities.formats).ok_or(GpuError::UnsupportedFormat)?;
171 let config = SurfaceConfiguration {
172 usage: TextureUsages::RENDER_ATTACHMENT,
173 format,
174 color_space: SurfaceColorSpace::Auto,
175 width,
176 height,
177 present_mode: PresentMode::Fifo,
178 desired_maximum_frame_latency: 2,
179 alpha_mode: CompositeAlphaMode::Auto,
180 view_formats: Vec::new(),
181 };
182 surface.configure(&device, &config);
183 let renderer = vello::Renderer::new(&device, crate::renderer::vello_options())
184 .expect("the device cannot create the vello pipelines");
185 let blitter = TextureBlitter::new(&device, format);
186 let target = create_target(&device, width, height);
187 Ok(GpuSurface {
188 device,
189 queue,
190 surface,
191 config,
192 renderer,
193 encoder: Encoder::new(limit),
194 blitter,
195 target,
196 reconfigure: false,
197 })
198 }
199
200 /// Reconfigures the surface and the intermediate texture for a new size.
201 ///
202 /// # Errors
203 ///
204 /// Returns [`GpuError::SurfaceTooLarge`] when `width` or `height`
205 /// exceeds the device's `max_texture_dimension_2d` limit. The presenter
206 /// keeps its old size in that case.
207 ///
208 /// # Panics
209 ///
210 /// Panics when `width` or `height` is zero, which cannot present: the
211 /// caller keeps the surface at its last size while the window has no
212 /// extent. Also panics, inside wgpu, when a texture from
213 /// [`GpuSurface::acquire`] is still alive, because a surface cannot be
214 /// reconfigured while one of its textures is out.
215 pub fn resize(&mut self, width: u32, height: u32) -> Result<(), GpuError> {
216 assert!(
217 width > 0 && height > 0,
218 "a resized surface must keep a positive extent"
219 );
220 let limit = self.device.limits().max_texture_dimension_2d;
221 if width > limit || height > limit {
222 return Err(GpuError::SurfaceTooLarge {
223 width,
224 height,
225 limit,
226 });
227 }
228 self.config.width = width;
229 self.config.height = height;
230 self.surface.configure(&self.device, &self.config);
231 self.reconfigure = false;
232 self.target = create_target(&self.device, width, height);
233 Ok(())
234 }
235
236 /// Returns the width the surface presents at, in physical pixels.
237 pub fn width(&self) -> u32 {
238 self.config.width
239 }
240
241 /// Returns the height the surface presents at, in physical pixels.
242 pub fn height(&self) -> u32 {
243 self.config.height
244 }
245
246 /// Waits for the next surface texture to draw onto.
247 ///
248 /// Draw or drop the returned texture before the next
249 /// [`GpuSurface::resize`], which cannot reconfigure the surface while it
250 /// is out. A surface that went out of date is reconfigured and asked once
251 /// more.
252 /// Returns `Ok(None)` when the frame must be skipped: the surface is
253 /// still out of date, is temporarily out of textures, or is covered.
254 /// Returns [`GpuError::SurfaceLost`] when the surface cannot present
255 /// anymore.
256 pub fn acquire(&mut self) -> Result<Option<SurfaceTexture>, GpuError> {
257 // No surface texture of the last frame is alive here: a drawn one was
258 // handed to the queue, a skipped one was dropped, so a reconfigure is
259 // safe now.
260 if self.reconfigure {
261 self.reconfigure = false;
262 self.surface.configure(&self.device, &self.config);
263 }
264 match self.surface.get_current_texture() {
265 CurrentSurfaceTexture::Outdated => {
266 self.surface.configure(&self.device, &self.config);
267 self.texture_from(self.surface.get_current_texture())
268 }
269 other => self.texture_from(other),
270 }
271 }
272
273 /// Turns one answer of the surface into the texture to draw onto, a
274 /// skipped frame, or a lost surface.
275 fn texture_from(
276 &mut self,
277 current: CurrentSurfaceTexture,
278 ) -> Result<Option<SurfaceTexture>, GpuError> {
279 match current {
280 CurrentSurfaceTexture::Success(texture) => Ok(Some(texture)),
281 CurrentSurfaceTexture::Suboptimal(texture) => {
282 // The surface no longer matches its configuration, most often
283 // after a resize. Reconfigure once the texture is dropped, so
284 // the next frame goes out at the right size.
285 self.reconfigure = true;
286 Ok(Some(texture))
287 }
288 CurrentSurfaceTexture::Timeout
289 | CurrentSurfaceTexture::Occluded
290 | CurrentSurfaceTexture::Outdated => Ok(None),
291 _ => Err(GpuError::SurfaceLost),
292 }
293 }
294
295 /// Renders `frame` onto `texture` and presents it.
296 ///
297 /// The whole display list is drawn and the whole surface is presented,
298 /// whatever [`FrameOutput::damage`] holds. The draw reaches the screen
299 /// asynchronously: the submission is queued when `draw` returns.
300 ///
301 /// # Errors
302 ///
303 /// Returns [`GpuError::Render`] when the device rejects the vello render,
304 /// which means it cannot run the shaders vello needs or has become
305 /// unusable. Nothing is presented then.
306 pub fn draw(
307 &mut self,
308 frame: &FrameOutput<'_>,
309 texture: SurfaceTexture,
310 ) -> Result<(), GpuError> {
311 let params = RenderParams {
312 base_color: peniko_color(frame.clear_color),
313 width: self.config.width,
314 height: self.config.height,
315 antialiasing_method: vello::AaConfig::Area,
316 };
317 let GpuSurface {
318 device,
319 queue,
320 renderer,
321 encoder,
322 blitter,
323 target,
324 ..
325 } = self;
326 encoder.encode(frame, params.width, params.height);
327 renderer
328 .render_to_texture(device, queue, encoder.scene(), &target.1, ¶ms)
329 .map_err(GpuError::Render)?;
330 let surface_view = texture
331 .texture
332 .create_view(&TextureViewDescriptor::default());
333 let mut command = device.create_command_encoder(&CommandEncoderDescriptor {
334 label: Some("smudge present"),
335 });
336 blitter.copy(device, &mut command, &target.1, &surface_view);
337 queue.submit([command.finish()]);
338 queue.present(texture);
339 Ok(())
340 }
341}
342
343/// Creates the intermediate texture vello renders into.
344///
345/// The caller keeps the extent above zero, because a surface without pixels
346/// has nothing to present and wgpu rejects empty textures.
347fn create_target(device: &Device, width: u32, height: u32) -> (Texture, TextureView) {
348 let texture = device.create_texture(&TextureDescriptor {
349 label: Some("smudge gpu surface"),
350 size: Extent3d {
351 width,
352 height,
353 depth_or_array_layers: 1,
354 },
355 mip_level_count: 1,
356 sample_count: 1,
357 dimension: TextureDimension::D2,
358 format: TextureFormat::Rgba8Unorm,
359 usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
360 view_formats: &[],
361 });
362 let view = texture.create_view(&TextureViewDescriptor::default());
363 (texture, view)
364}
365
366#[cfg(test)]
367#[path = "surface_tests.rs"]
368mod tests;