Skip to main content

smudge_gpu/
renderer.rs

1//! The GPU renderer that encodes a display list into a vello scene and
2//! renders it to an owned texture.
3
4use futures_executor::block_on;
5use smudge_core::app::FrameOutput;
6use smudge_core::renderer::Renderer;
7use vello::RenderParams;
8use vello::wgpu;
9use vello::wgpu::{
10    Adapter, Device, DeviceType, Extent3d, Instance, InstanceDescriptor, Limits, MapMode, PollType,
11    Queue, RequestAdapterOptions, TexelCopyBufferInfo, Texture, TextureDescriptor,
12    TextureDimension, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor,
13};
14
15use crate::encoder::{Encoder, peniko_color};
16
17/// Returns the device limits a renderer's device should be created with.
18///
19/// Vello's internal storage buffers can outgrow the default limits of 128
20/// MiB for one storage binding and 256 MiB for one buffer on large scenes,
21/// and a device created with the defaults then fails validation. The
22/// request raises both to what the adapter offers and leaves every other
23/// limit at its default. A device for [`GpuRenderer::new`] or
24/// [`GpuSurface::new`](crate::GpuSurface::new) should be requested with
25/// these limits.
26pub fn device_limits(adapter: &Adapter) -> Limits {
27    let offered = adapter.limits();
28    Limits {
29        max_storage_buffer_binding_size: offered.max_storage_buffer_binding_size,
30        max_buffer_size: offered.max_buffer_size,
31        ..Limits::default()
32    }
33}
34
35/// Creates the wgpu instance a renderer's adapter should come from.
36///
37/// The instance enables the primary backends only: Vulkan, Metal and DX12,
38/// and WebGPU on the web. The renderer needs compute shaders, which the
39/// OpenGL backend serves poorly, and enabling it loads the system's OpenGL
40/// and EGL drivers next to the Vulkan one. A machine without a primary
41/// backend is usually better served by the CPU renderer. The `WGPU_BACKEND`
42/// environment variable still picks the backends when it is set.
43///
44/// When this instance finds no adapter, [`GpuRenderer::headless`] retries
45/// with every backend, OpenGL included, and so does a window that asks for
46/// the GPU renderer explicitly. A caller that requests its own adapter can
47/// do the same with an instance from
48/// [`InstanceDescriptor::new_without_display_handle_from_env`].
49pub fn instance() -> Instance {
50    let mut descriptor = InstanceDescriptor::new_without_display_handle_from_env();
51    if std::env::var_os("WGPU_BACKEND").is_none() {
52        descriptor.backends = wgpu::Backends::PRIMARY;
53    }
54    Instance::new(descriptor)
55}
56
57/// The vello options both renderers are created with.
58///
59/// Only area antialiasing is compiled, because it is the only mode the
60/// renderers draw with. Compiling all three modes, vello's default, cost
61/// 950 ms of CPU and 49 MB at startup on a GTX 1060; area alone cost 580 ms
62/// and 42 MB.
63pub(crate) fn vello_options() -> vello::RendererOptions {
64    vello::RendererOptions {
65        antialiasing_support: vello::AaSupport::area_only(),
66        ..vello::RendererOptions::default()
67    }
68}
69
70/// How many bytes one row of a readback buffer is aligned to.
71///
72/// wgpu requires the row stride of a texture copy to be a multiple of this.
73const ROW_ALIGNMENT: u32 = 256;
74
75/// A renderer that draws every frame on a GPU through vello.
76///
77/// The renderer owns one texture of its configured size and repaints the
78/// whole texture on every [`Renderer::render`] call. Damage is ignored: the
79/// GPU path redraws the full frame, so [`FrameOutput::damage`] and
80/// [`FrameOutput::full`] carry no meaning for it. [`FrameOutput::clear_color`]
81/// becomes the color the frame is cleared with, and [`FrameOutput::scale`]
82/// scales the logical display list coordinates into physical pixels.
83///
84/// To draw onto a window instead, use [`GpuSurface`], which presents through
85/// a wgpu surface.
86///
87/// # Panics
88///
89/// Panics when the device cannot create or run the vello pipelines, which
90/// means the device cannot run the shaders vello needs. That is a broken
91/// device state rather than input a caller can provoke.
92///
93/// # Examples
94///
95/// ```
96/// use smudge_core::app::FrameOutput;
97/// use smudge_core::geometry::{Color, Corners, Rect};
98/// use smudge_core::paint::{DisplayList, PaintOp};
99/// use smudge_core::renderer::Renderer;
100/// use smudge_gpu::GpuRenderer;
101///
102/// // On a machine without a GPU adapter this draws nothing.
103/// let Some(mut renderer) = GpuRenderer::headless(64, 64) else {
104///     return;
105/// };
106/// let mut list = DisplayList::new();
107/// list.push_op(PaintOp::FillRect {
108///     rect: Rect::new(0.0, 0.0, 64.0, 64.0),
109///     radius: Corners::ZERO,
110///     color: Color::BLACK,
111/// });
112/// list.finish();
113/// renderer.render(&FrameOutput {
114///     list: &list,
115///     damage: &[],
116///     full: true,
117///     clear_color: Color::WHITE,
118///     scale: 1.0,
119/// });
120/// let pixels = renderer.read_pixels();
121/// assert_eq!(pixels.len(), 64 * 64 * 4);
122/// ```
123///
124/// [`FrameOutput`]: smudge_core::app::FrameOutput
125/// [`GpuSurface`]: crate::GpuSurface
126pub struct GpuRenderer {
127    /// The device the texture and the pipelines live on.
128    device: Device,
129    /// The queue renders are submitted to.
130    queue: Queue,
131    /// The width of the texture in physical pixels.
132    width: u32,
133    /// The height of the texture in physical pixels.
134    height: u32,
135    /// The texture vello renders into and its view, or `None` when the
136    /// surface has no pixel.
137    target: Option<(Texture, TextureView)>,
138    /// The vello renderer, which holds the compiled pipelines.
139    renderer: vello::Renderer,
140    /// The scene encoder of the current frame, reused across frames.
141    encoder: Encoder,
142}
143
144/// The adapter a GPU renderer draws on, as plain strings a report can carry.
145///
146/// [`GpuRenderer::adapter_summary`] reads it from the adapter its device was
147/// created from. A caller that requests adapters itself builds one from
148/// wgpu's own adapter info with [`AdapterSummary::of`].
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct AdapterSummary {
151    /// The adapter's name, as its driver reports it.
152    pub name: String,
153    /// The graphics backend the device runs on, in the backend's own lower
154    /// case name: `vulkan`, `metal`, `dx12`, `gl`, `webgpu` or `noop`.
155    pub backend: &'static str,
156    /// The kind of device: `discrete-gpu`, `integrated-gpu`, `virtual-gpu`,
157    /// `cpu` or `other`.
158    pub device_type: &'static str,
159}
160
161impl AdapterSummary {
162    /// Reads the summary out of the info of an adapter.
163    pub fn of(info: &wgpu::AdapterInfo) -> AdapterSummary {
164        AdapterSummary {
165            name: info.name.clone(),
166            backend: info.backend.to_str(),
167            device_type: match info.device_type {
168                DeviceType::DiscreteGpu => "discrete-gpu",
169                DeviceType::IntegratedGpu => "integrated-gpu",
170                DeviceType::VirtualGpu => "virtual-gpu",
171                DeviceType::Cpu => "cpu",
172                DeviceType::Other => "other",
173            },
174        }
175    }
176}
177
178impl GpuRenderer {
179    /// Creates a renderer for a device, drawing onto a texture of `width` by
180    /// `height` physical pixels.
181    ///
182    /// A surface with a zero extent holds no texture, and
183    /// [`GpuRenderer::render`] and [`GpuRenderer::read_pixels`] then do
184    /// nothing. Request the device with [`device_limits`]: with the default
185    /// limits a large scene fails validation, which wgpu reports as a panic.
186    ///
187    /// # Panics
188    ///
189    /// Panics when `width` or `height` exceeds the device's
190    /// `max_texture_dimension_2d` limit, which a caller can read from
191    /// [`Device::limits`] beforehand, and under the conditions of the
192    /// [`# Panics`](Self#panics) section.
193    pub fn new(device: Device, queue: Queue, width: u32, height: u32) -> GpuRenderer {
194        let limit = device.limits().max_texture_dimension_2d;
195        assert!(
196            width <= limit && height <= limit,
197            "a {width}x{height} surface exceeds the device's texture limit of {limit}"
198        );
199        let renderer = vello::Renderer::new(&device, vello_options())
200            .expect("the device cannot create the vello pipelines");
201        let target = create_target(&device, width, height);
202        GpuRenderer {
203            device,
204            queue,
205            width,
206            height,
207            target,
208            renderer,
209            encoder: Encoder::new(limit),
210        }
211    }
212
213    /// Creates a renderer on a GPU adapter chosen without a window, so tests
214    /// and offscreen uses need no surface.
215    ///
216    /// The adapter comes from [`instance`], whose backends the
217    /// `WGPU_BACKEND` variable picks when it is set, exactly as the window
218    /// path does. That lets a machine pin the backend without a rebuild, for
219    /// example to measure a software rasterizer. When the primary backends
220    /// find no adapter, the request is retried with every backend, so a
221    /// machine whose only adapter is an OpenGL one still gets a renderer.
222    ///
223    /// Returns `None` when no adapter can be created, or when the chosen
224    /// adapter cannot provide a device, which is how a machine without a GPU
225    /// reports itself.
226    ///
227    /// # Panics
228    ///
229    /// Panics under the conditions of [`GpuRenderer::new`]. The device has
230    /// the limits of [`device_limits`], which allow 8192 pixels on each side
231    /// and the adapter's maximum storage buffer binding size.
232    pub fn headless(width: u32, height: u32) -> Option<GpuRenderer> {
233        // The environment override matches the window path, so one set of
234        // variables picks the backend for both.
235        let options = RequestAdapterOptions::default();
236        let adapter = match block_on(instance().request_adapter(&options)) {
237            Ok(adapter) => adapter,
238            Err(_) => {
239                let every =
240                    Instance::new(InstanceDescriptor::new_without_display_handle_from_env());
241                block_on(every.request_adapter(&options)).ok()?
242            }
243        };
244        let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
245            required_limits: device_limits(&adapter),
246            ..Default::default()
247        }))
248        .ok()?;
249        Some(GpuRenderer::new(device, queue, width, height))
250    }
251
252    /// Returns the adapter the renderer draws on, as plain strings.
253    ///
254    /// The renderer keeps no adapter of its own: wgpu reports the adapter its
255    /// device was created from, and that is the adapter every render runs on.
256    pub fn adapter_summary(&self) -> AdapterSummary {
257        AdapterSummary::of(&self.device.adapter_info())
258    }
259
260    /// Returns the rendered frame as straight-alpha RGBA8 pixels, row-major
261    /// with no padding between rows, which is the layout vello writes.
262    ///
263    /// The result is empty when the surface has no pixel.
264    ///
265    /// # Panics
266    ///
267    /// Panics when the device is lost, or when the readback buffer cannot be
268    /// mapped after the copy completed. Both are broken device states rather
269    /// than input a caller can provoke.
270    pub fn read_pixels(&self) -> Vec<u8> {
271        let Some((texture, _)) = &self.target else {
272            return Vec::new();
273        };
274        let stride = self.stride();
275        let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
276            label: Some("smudge readback"),
277            size: u64::from(stride) * u64::from(self.height),
278            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
279            mapped_at_creation: false,
280        });
281        let mut encoder = self
282            .device
283            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
284                label: Some("smudge readback"),
285            });
286        encoder.copy_texture_to_buffer(
287            texture.as_image_copy(),
288            TexelCopyBufferInfo {
289                buffer: &buffer,
290                layout: wgpu::TexelCopyBufferLayout {
291                    offset: 0,
292                    bytes_per_row: Some(stride),
293                    rows_per_image: Some(self.height),
294                },
295            },
296            Extent3d {
297                width: self.width,
298                height: self.height,
299                depth_or_array_layers: 1,
300            },
301        );
302        self.queue.submit([encoder.finish()]);
303
304        let (sender, receiver) = std::sync::mpsc::channel();
305        buffer.slice(..).map_async(MapMode::Read, move |result| {
306            let _ = sender.send(result);
307        });
308        self.device
309            .poll(PollType::wait_indefinitely())
310            .expect("the device was lost while reading pixels back");
311        receiver
312            .recv()
313            .expect("the readback callback ran during the poll")
314            .expect("the readback buffer could not be mapped");
315
316        let mapped = buffer
317            .slice(..)
318            .get_mapped_range()
319            .expect("the readback buffer was already mapped");
320        let mut pixels = Vec::with_capacity(self.width as usize * self.height as usize * 4);
321        for row in 0..self.height {
322            let start = row as usize * stride as usize;
323            pixels.extend_from_slice(&mapped[start..start + self.width as usize * 4]);
324        }
325        drop(mapped);
326        buffer.unmap();
327        pixels
328    }
329
330    /// Blocks until the GPU has executed every render submitted so far.
331    ///
332    /// A render returns once its work is submitted, so a caller that times a
333    /// frame calls this to count the GPU's execution and not only the
334    /// submission.
335    ///
336    /// # Panics
337    ///
338    /// Panics when the device is lost, which is a broken device state rather
339    /// than input a caller can provoke.
340    pub fn wait(&self) {
341        self.device
342            .poll(PollType::wait_indefinitely())
343            .expect("the device was lost while waiting for renders");
344    }
345
346    /// Returns the distance in bytes between the start of two consecutive
347    /// rows of a readback buffer.
348    fn stride(&self) -> u32 {
349        (self.width * 4).next_multiple_of(ROW_ALIGNMENT)
350    }
351}
352
353impl Renderer for GpuRenderer {
354    fn render(&mut self, frame: &FrameOutput<'_>) {
355        let Some((_, view)) = &self.target else {
356            return;
357        };
358        let params = RenderParams {
359            base_color: peniko_color(frame.clear_color),
360            width: self.width,
361            height: self.height,
362            antialiasing_method: vello::AaConfig::Area,
363        };
364        let GpuRenderer {
365            device,
366            queue,
367            renderer,
368            encoder,
369            ..
370        } = self;
371        encoder.encode(frame, params.width, params.height);
372        renderer
373            .render_to_texture(device, queue, encoder.scene(), view, &params)
374            .expect("the device rejected a vello render");
375    }
376}
377
378/// Creates the texture vello renders into.
379///
380/// Returns `None` when the surface has no pixel, because wgpu cannot create a
381/// texture with a zero extent.
382fn create_target(device: &Device, width: u32, height: u32) -> Option<(Texture, TextureView)> {
383    if width == 0 || height == 0 {
384        return None;
385    }
386    let texture = device.create_texture(&TextureDescriptor {
387        label: Some("smudge gpu surface"),
388        size: Extent3d {
389            width,
390            height,
391            depth_or_array_layers: 1,
392        },
393        mip_level_count: 1,
394        sample_count: 1,
395        dimension: TextureDimension::D2,
396        format: TextureFormat::Rgba8Unorm,
397        usage: TextureUsages::STORAGE_BINDING | TextureUsages::COPY_SRC,
398        view_formats: &[],
399    });
400    let view = texture.create_view(&TextureViewDescriptor::default());
401    Some((texture, view))
402}