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, ¶ms)
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}