Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/rendering/wgpu/renderer.rs
1mod frame;
2mod glyph;
3mod image;
4mod rect;
5mod util;
6 
7use frame::Frame;
8use pathfinder_geometry::vector::Vector2F;
9use strato_ui_core::platform::CapturedFrame;
10use util::with_error_scope;
11use wgpu::wgc::{device::DeviceError, present::SurfaceError};
12 
13use crate::r#async::block_on;
14use crate::rendering::wgpu::Resources;
15use crate::rendering::{GlyphConfig, GlyphRasterBoundsFn, RasterizeGlyphFn};
16use crate::Scene;
17 
18pub use super::resources::{GetSurfaceTextureError, SurfaceConfigureError};
19 
20const ENCODER_DESCRIPTOR: wgpu::CommandEncoderDescriptor = wgpu::CommandEncoderDescriptor {
21 label: Some("Command encoder"),
22};
23 
24pub struct Renderer {
25 rect_pipeline: rect::Pipeline,
26 glyph_pipeline: glyph::Pipeline,
27 image_pipeline: image::Pipeline,
28}
29 
30impl Renderer {
31 pub fn new(resources: &Resources, glyph_config: GlyphConfig) -> Self {
32 let Resources { device, .. } = resources;
33 
34 let format = resources.surface_config.borrow().format;
35 let color_target = wgpu::ColorTargetState {
36 format,
37 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
38 write_mask: wgpu::ColorWrites::all(),
39 };
40 
41 let rect_pipeline = rect::Pipeline::new(
42 resources.uniform_bind_group_layout(),
43 device,
44 color_target.clone(),
45 );
46 
47 let glyph_pipeline = glyph::Pipeline::new(
48 resources.uniform_bind_group_layout(),
49 device,
50 color_target.clone(),
51 glyph_config,
52 );
53 
54 let image_pipeline =
55 image::Pipeline::new(resources.uniform_bind_group_layout(), device, color_target);
56 
57 Self {
58 rect_pipeline,
59 glyph_pipeline,
60 image_pipeline,
61 }
62 }
63 
64 #[allow(clippy::too_many_arguments)]
65 pub fn render<'a>(
66 &mut self,
67 scene: &Scene,
68 resources: &Resources,
69 rasterize_glyph_fn: &RasterizeGlyphFn,
70 glyph_raster_bounds_fn: &GlyphRasterBoundsFn,
71 window_size: Vector2F,
72 pre_present_callback: Option<Box<dyn FnOnce() + 'a>>,
73 capture_callback: Option<Box<dyn FnOnce(CapturedFrame) + Send + 'static>>,
74 ) -> Result<(), Error> {
75 let Resources { device, queue, .. } = resources;
76 
77 // Don't initiate the render if we are trying to render into a
78 // zero-sized window.
79 if window_size.is_zero() {
80 return Ok(());
81 }
82 
83 let mut ctx = WGPUContext {
84 resources,
85 rasterize_glyph_fn,
86 glyph_raster_bounds_fn,
87 };
88 
89 let frame = match with_error_scope(device, || {
90 Frame::new(
91 scene,
92 &mut ctx,
93 &self.rect_pipeline,
94 &mut self.glyph_pipeline,
95 &mut self.image_pipeline,
96 )
97 }) {
98 (_, Some(error)) => return Err(error),
99 (frame, _) => frame,
100 };
101 
102 let surface_texture = resources.get_surface_texture()?;
103 
104 let mut encoder = device.create_command_encoder(&ENCODER_DESCRIPTOR);
105 let (_, error) = with_error_scope(device, || {
106 frame.draw(resources, &mut encoder, &surface_texture);
107 queue.submit(Some(encoder.finish()));
108 });
109 
110 if let Some(callback) = capture_callback {
111 if let Err(err) =
112 capture_surface_texture(device, queue, resources, &surface_texture, callback)
113 {
114 log::warn!("Frame capture failed: {err}");
115 }
116 }
117 
118 if let Some(callback) = pre_present_callback {
119 callback();
120 }
121 
122 match error {
123 Some(error) => Err(error),
124 None => {
125 // Only present the surface if there were no errors, otherwise
126 // wgpu will print out an error that we attempted to present a
127 // texture without submitting any work to the GPU.
128 match with_error_scope(device, || {
129 surface_texture.present();
130 }) {
131 (_, None) => Ok(()),
132 (_, Some(error)) => Err(error),
133 }
134 }
135 }
136 }
137}
138 
139/// Errors that can occur while rendering a scene.
140#[derive(thiserror::Error, Debug)]
141pub enum Error {
142 #[error("Device was lost")]
143 DeviceLost,
144 #[error("Failed to acquire surface texture: {0:#}")]
145 SurfaceError(#[from] GetSurfaceTextureError),
146 #[error("Failed to configure surface: {0:#}")]
147 SurfaceConfigureError(#[from] SurfaceConfigureError),
148 #[error("{0:#}")]
149 Unknown(#[source] wgpu::Error),
150}
151 
152impl From<wgpu::Error> for Error {
153 fn from(value: wgpu::Error) -> Self {
154 for error in anyhow::Chain::new(&value) {
155 if let Some(DeviceError::Lost) = error.downcast_ref::<DeviceError>() {
156 return Error::DeviceLost;
157 }
158 
159 // The use of `#[transparent]` for many nested device errors breaks
160 // error chaining - the call to `source()` gets forwarded to the
161 // DeviceError::Lost, which returns None (it doesn't wrap an error).
162 // Ideally, these wrapped errors should use `#[from]` instead, but
163 // until then, we need to do this to properly catch DeviceError::Lost
164 // from within a call to present().
165 if let Some(SurfaceError::Device(DeviceError::Lost)) =
166 error.downcast_ref::<SurfaceError>()
167 {
168 return Error::DeviceLost;
169 }
170 }
171 Error::Unknown(value)
172 }
173}
174 
175/// Copies the current surface texture into a `CapturedFrame` and delivers it via `callback`.
176///
177/// **`callback` is invoked synchronously on the render thread** once the GPU readback
178/// completes. It must be lightweight (e.g., move the frame into a shared buffer and return
179/// immediately) to avoid stalling frame presentation.
180fn capture_surface_texture(
181 device: &wgpu::Device,
182 queue: &wgpu::Queue,
183 resources: &Resources,
184 surface_texture: &wgpu::SurfaceTexture,
185 callback: Box<dyn FnOnce(CapturedFrame) + Send + 'static>,
186) -> Result<(), String> {
187 let texture = &surface_texture.texture;
188 let width = texture.width();
189 let height = texture.height();
190 
191 if width == 0 || height == 0 {
192 return Err(format!("Invalid texture dimensions: {width}x{height}"));
193 }
194 
195 let format = resources.surface_config.borrow().format;
196 let bytes_per_pixel = 4u32;
197 let unpadded_bytes_per_row = width * bytes_per_pixel;
198 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
199 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
200 let buffer_size = (padded_bytes_per_row * height) as u64;
201 
202 let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
203 label: Some("Frame capture staging buffer"),
204 size: buffer_size,
205 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
206 mapped_at_creation: false,
207 });
208 
209 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
210 label: Some("Frame capture encoder"),
211 });
212 
213 encoder.copy_texture_to_buffer(
214 wgpu::TexelCopyTextureInfo {
215 texture,
216 mip_level: 0,
217 origin: wgpu::Origin3d::ZERO,
218 aspect: wgpu::TextureAspect::All,
219 },
220 wgpu::TexelCopyBufferInfo {
221 buffer: &staging_buffer,
222 layout: wgpu::TexelCopyBufferLayout {
223 offset: 0,
224 bytes_per_row: Some(padded_bytes_per_row),
225 rows_per_image: None,
226 },
227 },
228 wgpu::Extent3d {
229 width,
230 height,
231 depth_or_array_layers: 1,
232 },
233 );
234 
235 queue.submit(Some(encoder.finish()));
236 
237 let buffer_slice = staging_buffer.slice(..);
238 let (sender, receiver) = std::sync::mpsc::channel();
239 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
240 let _ = sender.send(result);
241 });
242 
243 block_on(async {
244 let _ = device.poll(wgpu::PollType::Wait {
245 submission_index: None,
246 timeout: None,
247 });
248 });
249 
250 let map_result = receiver
251 .recv()
252 .map_err(|e| format!("Failed to receive map result: {e}"))?
253 .map_err(|e| format!("Buffer mapping failed: {e}"));
254 
255 map_result?;
256 
257 let data = buffer_slice.get_mapped_range();
258 let mut rgba_data = Vec::with_capacity((width * height * bytes_per_pixel) as usize);
259 for row in 0..height {
260 let start = (row * padded_bytes_per_row) as usize;
261 let end = start + unpadded_bytes_per_row as usize;
262 rgba_data.extend_from_slice(&data[start..end]);
263 }
264 drop(data);
265 staging_buffer.unmap();
266 
267 if format == wgpu::TextureFormat::Bgra8Unorm || format == wgpu::TextureFormat::Bgra8UnormSrgb {
268 for chunk in rgba_data.chunks_exact_mut(4) {
269 chunk.swap(0, 2);
270 }
271 }
272 
273 callback(CapturedFrame::new(width, height, rgba_data));
274 Ok(())
275}
276 
277struct WGPUContext<'a> {
278 resources: &'a Resources,
279 rasterize_glyph_fn: &'a RasterizeGlyphFn<'a>,
280 glyph_raster_bounds_fn: &'a GlyphRasterBoundsFn<'a>,
281}
282