StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use crate::fonts::SubpixelAlignment; |
| 2 | use crate::rendering::atlas::TextureId; |
| 3 | use crate::rendering::wgpu::renderer::WGPUContext; |
| 4 | use crate::rendering::wgpu::texture_with_bind_group::TextureWithBindGroup; |
| 5 | use crate::rendering::wgpu::{resources, shader_types}; |
| 6 | use crate::rendering::{GlyphCache, GlyphConfig}; |
| 7 | use crate::scene::{GlyphFade, Layer}; |
| 8 | use crate::Scene; |
| 9 | use pathfinder_geometry::rect::RectF; |
| 10 | use std::borrow::Cow; |
| 11 | use std::collections::HashMap; |
| 12 | use std::sync::{atomic::AtomicBool, Arc}; |
| 13 | use wgpu::util::BufferInitDescriptor; |
| 14 | use wgpu::{ |
| 15 | BindGroupLayout, BufferUsages, ColorTargetState, Device, FilterMode, RenderPass, |
| 16 | RenderPipeline, Sampler, |
| 17 | }; |
| 18 | |
| 19 | use super::util::create_buffer_init; |
| 20 | |
| 21 | pub(super) struct Pipeline { |
| 22 | glyph_cache: GlyphCache<TextureWithBindGroup>, |
| 23 | render_pipeline: RenderPipeline, |
| 24 | texture_bind_group_layout: BindGroupLayout, |
| 25 | sampler: Sampler, |
| 26 | } |
| 27 | |
| 28 | #[derive(Default)] |
| 29 | pub(super) struct PerFrameState { |
| 30 | glyph_data: Vec<shaders::GlyphInstanceData>, |
| 31 | buffer: Option<wgpu::Buffer>, |
| 32 | } |
| 33 | |
| 34 | pub(super) struct LayerState { |
| 35 | textures: Vec<PerTextureState>, |
| 36 | } |
| 37 | |
| 38 | pub(super) struct PerTextureState { |
| 39 | texture_id: TextureId, |
| 40 | start_offset: usize, |
| 41 | len: usize, |
| 42 | } |
| 43 | impl Pipeline { |
| 44 | pub(super) fn new( |
| 45 | uniform_bind_group_layout: &BindGroupLayout, |
| 46 | device: &Device, |
| 47 | color_target: ColorTargetState, |
| 48 | glyph_config: GlyphConfig, |
| 49 | ) -> Self { |
| 50 | let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { |
| 51 | label: Some("Glyph Shader"), |
| 52 | source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!( |
| 53 | "../shaders/glyph_shader.wgsl" |
| 54 | ))), |
| 55 | }); |
| 56 | |
| 57 | let texture_bind_group_layout = |
| 58 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { |
| 59 | entries: &[ |
| 60 | wgpu::BindGroupLayoutEntry { |
| 61 | binding: 0, |
| 62 | visibility: wgpu::ShaderStages::FRAGMENT, |
| 63 | ty: wgpu::BindingType::Texture { |
| 64 | multisampled: false, |
| 65 | view_dimension: wgpu::TextureViewDimension::D2, |
| 66 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, |
| 67 | }, |
| 68 | count: None, |
| 69 | }, |
| 70 | wgpu::BindGroupLayoutEntry { |
| 71 | binding: 1, |
| 72 | visibility: wgpu::ShaderStages::FRAGMENT, |
| 73 | // This should match the filterable field of the |
| 74 | // corresponding Texture entry above. |
| 75 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), |
| 76 | count: None, |
| 77 | }, |
| 78 | ], |
| 79 | label: Some("texture_bind_group_layout"), |
| 80 | }); |
| 81 | |
| 82 | let glyph_pipeline_layout = |
| 83 | device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { |
| 84 | label: Some("Glyph pipeline layout"), |
| 85 | bind_group_layouts: &[ |
| 86 | Some(uniform_bind_group_layout), |
| 87 | Some(&texture_bind_group_layout), |
| 88 | ], |
| 89 | immediate_size: 0, |
| 90 | }); |
| 91 | |
| 92 | let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { |
| 93 | label: Some("Glyph Render pipeline"), |
| 94 | layout: Some(&glyph_pipeline_layout), |
| 95 | vertex: wgpu::VertexState { |
| 96 | module: &shader, |
| 97 | entry_point: Some("vs_main"), |
| 98 | buffers: &[ |
| 99 | shader_types::Vertex::desc(), |
| 100 | shaders::GlyphInstanceData::desc(), |
| 101 | ], |
| 102 | compilation_options: Default::default(), |
| 103 | }, |
| 104 | fragment: Some(wgpu::FragmentState { |
| 105 | module: &shader, |
| 106 | entry_point: Some("fs_main"), |
| 107 | targets: &[Some(color_target)], |
| 108 | compilation_options: Default::default(), |
| 109 | }), |
| 110 | primitive: wgpu::PrimitiveState::default(), |
| 111 | depth_stencil: None, |
| 112 | multisample: wgpu::MultisampleState::default(), |
| 113 | multiview_mask: None, |
| 114 | // Don't use a pipeline cache. Most desktop GPU drivers have their own internal caches, |
| 115 | // so we are unlikely to get much value out of this for the platforms Warp supports. |
| 116 | cache: None, |
| 117 | }); |
| 118 | |
| 119 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { |
| 120 | mag_filter: FilterMode::Linear, |
| 121 | min_filter: FilterMode::Linear, |
| 122 | ..Default::default() |
| 123 | }); |
| 124 | |
| 125 | Self { |
| 126 | glyph_cache: GlyphCache::new(glyph_config), |
| 127 | render_pipeline, |
| 128 | texture_bind_group_layout, |
| 129 | sampler, |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | pub(super) fn update_config(&mut self, glyph_config: &GlyphConfig) { |
| 134 | self.glyph_cache.update_config(glyph_config); |
| 135 | } |
| 136 | |
| 137 | pub(super) fn initialize_for_layer( |
| 138 | &mut self, |
| 139 | layer: &Layer, |
| 140 | scene: &Scene, |
| 141 | per_frame_state: &mut PerFrameState, |
| 142 | ctx: &WGPUContext, |
| 143 | ) -> Option<LayerState> { |
| 144 | if layer.glyphs.is_empty() { |
| 145 | // There are no glyphs to render, exit early. |
| 146 | return None; |
| 147 | } |
| 148 | |
| 149 | let scale_factor = scene.scale_factor(); |
| 150 | |
| 151 | let mut texture_to_glyph: HashMap<TextureId, Vec<shaders::GlyphInstanceData>> = |
| 152 | HashMap::new(); |
| 153 | for glyph in &layer.glyphs { |
| 154 | let glyph_position = glyph.position * scale_factor; |
| 155 | let subpixel_alignment = SubpixelAlignment::new(glyph_position); |
| 156 | match self.glyph_cache.get( |
| 157 | glyph.glyph_key, |
| 158 | scene.scale_factor(), |
| 159 | subpixel_alignment, |
| 160 | &|size| { |
| 161 | TextureWithBindGroup::new( |
| 162 | size, |
| 163 | &ctx.resources.device, |
| 164 | &self.texture_bind_group_layout, |
| 165 | &self.sampler, |
| 166 | ) |
| 167 | }, |
| 168 | &|region, rasterized_glyph, texture| { |
| 169 | texture.insert_glyph_into_texture( |
| 170 | region, |
| 171 | rasterized_glyph, |
| 172 | &ctx.resources.queue, |
| 173 | ) |
| 174 | }, |
| 175 | ctx.glyph_raster_bounds_fn, |
| 176 | ctx.rasterize_glyph_fn, |
| 177 | ) { |
| 178 | Ok(Some(gto)) => { |
| 179 | let (fade_start, fade_end) = match &glyph.fade { |
| 180 | None => (&0.0, &-1.0), |
| 181 | Some(GlyphFade::Horizontal { start, end }) => (start, end), |
| 182 | }; |
| 183 | |
| 184 | // Adjust the horizontal position by the subpixel alignment |
| 185 | // so that we only shift the glyph over by the amount that |
| 186 | // isn't accounted for in the subpixel-rasterized glyph. |
| 187 | let glyph_position = glyph_position - subpixel_alignment.to_offset(); |
| 188 | |
| 189 | // Make sure to pass the glyph size in the atlas |
| 190 | // Not the size of the render bounds (which may be smaller) |
| 191 | // If you pass the render bounds as the size, the shader |
| 192 | // will try to sample from a smaller area than the size |
| 193 | // in the atlas, leading to artifacts. |
| 194 | let glyph_instance_data = shaders::GlyphInstanceData::new( |
| 195 | RectF::new( |
| 196 | glyph_position + gto.raster_bounds.origin(), |
| 197 | gto.allocated_region.pixel_region.size().to_f32(), |
| 198 | ), |
| 199 | gto.allocated_region.uv_region, |
| 200 | fade_start * scale_factor, |
| 201 | fade_end * scale_factor, |
| 202 | glyph.color, |
| 203 | gto.is_emoji, |
| 204 | ); |
| 205 | |
| 206 | texture_to_glyph |
| 207 | .entry(gto.texture_id) |
| 208 | .or_default() |
| 209 | .push(glyph_instance_data); |
| 210 | } |
| 211 | Ok(None) => {} |
| 212 | Err(err) => { |
| 213 | log::warn!("Unable to get glyph out of glyph cache: {err:?}, {glyph:?}"); |
| 214 | return None; |
| 215 | } |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | if texture_to_glyph.is_empty() { |
| 220 | // Early exit if there are no glyphs to render, as it causes a debug assert |
| 221 | // failure in the metal code to create an empty metal buffer. |
| 222 | return None; |
| 223 | } |
| 224 | |
| 225 | let mut start_offset = per_frame_state.glyph_data.len(); |
| 226 | let per_texture_data = texture_to_glyph |
| 227 | .into_iter() |
| 228 | .map(|(texture_id, mut glyph_instance_data)| { |
| 229 | let len = glyph_instance_data.len(); |
| 230 | per_frame_state.glyph_data.append(&mut glyph_instance_data); |
| 231 | |
| 232 | let state = PerTextureState { |
| 233 | texture_id, |
| 234 | start_offset, |
| 235 | len, |
| 236 | }; |
| 237 | start_offset += len; |
| 238 | state |
| 239 | }) |
| 240 | .collect(); |
| 241 | |
| 242 | Some(LayerState { |
| 243 | textures: per_texture_data, |
| 244 | }) |
| 245 | } |
| 246 | |
| 247 | pub(super) fn finalize_per_frame_state( |
| 248 | per_frame_state: &mut PerFrameState, |
| 249 | device: &Device, |
| 250 | device_lost: &Arc<AtomicBool>, |
| 251 | ) { |
| 252 | per_frame_state.buffer = create_buffer_init( |
| 253 | device, |
| 254 | device_lost, |
| 255 | &BufferInitDescriptor { |
| 256 | label: Some("Glyph instance buffer"), |
| 257 | contents: bytemuck::cast_slice(&per_frame_state.glyph_data), |
| 258 | usage: BufferUsages::VERTEX, |
| 259 | }, |
| 260 | ) |
| 261 | .ok(); |
| 262 | } |
| 263 | |
| 264 | pub(super) fn draw<'a>( |
| 265 | &'a self, |
| 266 | render_pass: &mut RenderPass<'a>, |
| 267 | layer_state: &LayerState, |
| 268 | per_frame_state: &'a PerFrameState, |
| 269 | ) { |
| 270 | let Some(buffer) = per_frame_state.buffer.as_ref() else { |
| 271 | return; |
| 272 | }; |
| 273 | |
| 274 | render_pass.set_pipeline(&self.render_pipeline); |
| 275 | render_pass.set_vertex_buffer(1, buffer.slice(..)); |
| 276 | |
| 277 | for per_texture_state in &layer_state.textures { |
| 278 | let texture_with_view = self |
| 279 | .glyph_cache |
| 280 | .texture(&per_texture_state.texture_id) |
| 281 | .expect("texture ID should be in atlas"); |
| 282 | |
| 283 | render_pass.set_bind_group(1, texture_with_view.bind_group(), &[]); |
| 284 | let end_offset = per_texture_state.start_offset + per_texture_state.len; |
| 285 | render_pass.draw_indexed( |
| 286 | 0..resources::quad::INDICES.len() as u32, |
| 287 | 0, |
| 288 | per_texture_state.start_offset as u32..end_offset as u32, |
| 289 | ); |
| 290 | } |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | mod shaders { |
| 295 | use crate::rendering::wgpu::shader_types::{ColorF, Vector4F}; |
| 296 | use pathfinder_color::ColorU; |
| 297 | use pathfinder_geometry::rect::RectF; |
| 298 | |
| 299 | #[repr(C)] |
| 300 | #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] |
| 301 | pub struct GlyphInstanceData { |
| 302 | bounds: Vector4F, |
| 303 | uv_bounds: Vector4F, |
| 304 | fade_start: f32, |
| 305 | fade_end: f32, |
| 306 | color: ColorF, |
| 307 | is_emoji: i32, |
| 308 | } |
| 309 | |
| 310 | impl GlyphInstanceData { |
| 311 | const ATTRIBS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![ |
| 312 | 1 => Float32x4, // Bounds |
| 313 | 2 => Float32x4, // UV Bounds |
| 314 | 3 => Float32, // Fade Start |
| 315 | 4 => Float32, // Fade end |
| 316 | 5 => Float32x4, // Color |
| 317 | 6 => Sint32, // Is Emoji |
| 318 | ]; |
| 319 | |
| 320 | pub(super) fn new( |
| 321 | bounds: RectF, |
| 322 | uv_left: RectF, |
| 323 | fade_start: f32, |
| 324 | fade_end: f32, |
| 325 | color: ColorU, |
| 326 | is_emoji: bool, |
| 327 | ) -> Self { |
| 328 | Self { |
| 329 | bounds: bounds.into(), |
| 330 | uv_bounds: uv_left.into(), |
| 331 | fade_start, |
| 332 | fade_end, |
| 333 | color: color.into(), |
| 334 | is_emoji: is_emoji as i32, |
| 335 | } |
| 336 | } |
| 337 | |
| 338 | pub(super) fn desc() -> wgpu::VertexBufferLayout<'static> { |
| 339 | use std::mem; |
| 340 | |
| 341 | wgpu::VertexBufferLayout { |
| 342 | array_stride: mem::size_of::<Self>() as wgpu::BufferAddress, |
| 343 | step_mode: wgpu::VertexStepMode::Instance, |
| 344 | attributes: &Self::ATTRIBS, |
| 345 | } |
| 346 | } |
| 347 | } |
| 348 | } |
| 349 |