Skip to main content

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, &params)
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;