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