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