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