StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | mod frame; |
| 2 | mod glyph; |
| 3 | mod image; |
| 4 | mod rect; |
| 5 | mod util; |
| 6 | |
| 7 | use frame::Frame; |
| 8 | use pathfinder_geometry::vector::Vector2F; |
| 9 | use strato_ui_core::platform::CapturedFrame; |
| 10 | use util::with_error_scope; |
| 11 | use wgpu::wgc::{device::DeviceError, present::SurfaceError}; |
| 12 | |
| 13 | use crate::r#async::block_on; |
| 14 | use crate::rendering::wgpu::Resources; |
| 15 | use crate::rendering::{GlyphConfig, GlyphRasterBoundsFn, RasterizeGlyphFn}; |
| 16 | use crate::Scene; |
| 17 | |
| 18 | pub use super::resources::{GetSurfaceTextureError, SurfaceConfigureError}; |
| 19 | |
| 20 | const ENCODER_DESCRIPTOR: wgpu::CommandEncoderDescriptor = wgpu::CommandEncoderDescriptor { |
| 21 | label: Some("Command encoder"), |
| 22 | }; |
| 23 | |
| 24 | pub struct Renderer { |
| 25 | rect_pipeline: rect::Pipeline, |
| 26 | glyph_pipeline: glyph::Pipeline, |
| 27 | image_pipeline: image::Pipeline, |
| 28 | } |
| 29 | |
| 30 | impl 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)] |
| 141 | pub 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 | |
| 152 | impl 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. |
| 180 | fn 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 | |
| 277 | struct WGPUContext<'a> { |
| 278 | resources: &'a Resources, |
| 279 | rasterize_glyph_fn: &'a RasterizeGlyphFn<'a>, |
| 280 | glyph_raster_bounds_fn: &'a GlyphRasterBoundsFn<'a>, |
| 281 | } |
| 282 |