StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use crate::image_cache::StaticImage; |
| 2 | use crate::rendering::texture_cache::{TextureCache, TextureCacheIndex}; |
| 3 | use crate::rendering::wgpu::{resources, shader_types}; |
| 4 | use crate::scene::Layer; |
| 5 | use crate::Scene; |
| 6 | use std::borrow::Cow; |
| 7 | use std::sync::{atomic::AtomicBool, Arc}; |
| 8 | use wgpu::util::BufferInitDescriptor; |
| 9 | use wgpu::{ |
| 10 | BindGroup, BindGroupDescriptor, BindGroupLayout, ColorTargetState, Device, Extent3d, |
| 11 | FilterMode, RenderPass, RenderPipeline, Sampler, TextureDescriptor, TextureFormat, |
| 12 | TextureUsages, |
| 13 | }; |
| 14 | |
| 15 | use self::shaders::{ColorModifier, ImageInstanceData}; |
| 16 | |
| 17 | use super::util::create_buffer_init; |
| 18 | use super::WGPUContext; |
| 19 | |
| 20 | pub(super) struct Pipeline { |
| 21 | render_pipeline: RenderPipeline, |
| 22 | texture_cache: TextureCache<TextureInfo>, |
| 23 | texture_bind_group_layout: BindGroupLayout, |
| 24 | sampler: Sampler, |
| 25 | } |
| 26 | |
| 27 | #[derive(Default)] |
| 28 | pub(super) struct PerFrameState { |
| 29 | image_data: Vec<shaders::ImageInstanceData>, |
| 30 | buffer: Option<wgpu::Buffer>, |
| 31 | } |
| 32 | |
| 33 | pub(super) struct LayerState { |
| 34 | start_offset: usize, |
| 35 | image_textures: Vec<TextureCacheIndex>, |
| 36 | } |
| 37 | |
| 38 | impl Pipeline { |
| 39 | pub(super) fn new( |
| 40 | uniform_bind_group_layout: &BindGroupLayout, |
| 41 | device: &Device, |
| 42 | color_target: ColorTargetState, |
| 43 | ) -> Self { |
| 44 | let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { |
| 45 | label: Some("Image Shader"), |
| 46 | source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!( |
| 47 | "../shaders/image_shader.wgsl" |
| 48 | ))), |
| 49 | }); |
| 50 | |
| 51 | let texture_bind_group_layout = |
| 52 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { |
| 53 | entries: &[ |
| 54 | wgpu::BindGroupLayoutEntry { |
| 55 | binding: 0, |
| 56 | visibility: wgpu::ShaderStages::FRAGMENT, |
| 57 | ty: wgpu::BindingType::Texture { |
| 58 | multisampled: false, |
| 59 | view_dimension: wgpu::TextureViewDimension::D2, |
| 60 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, |
| 61 | }, |
| 62 | count: None, |
| 63 | }, |
| 64 | wgpu::BindGroupLayoutEntry { |
| 65 | binding: 1, |
| 66 | visibility: wgpu::ShaderStages::FRAGMENT, |
| 67 | // This should match the filterable field of the |
| 68 | // corresponding Texture entry above. |
| 69 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), |
| 70 | count: None, |
| 71 | }, |
| 72 | ], |
| 73 | label: Some("texture_bind_group_layout"), |
| 74 | }); |
| 75 | |
| 76 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { |
| 77 | label: Some("Image pipeline layout"), |
| 78 | bind_group_layouts: &[ |
| 79 | Some(uniform_bind_group_layout), |
| 80 | Some(&texture_bind_group_layout), |
| 81 | ], |
| 82 | immediate_size: 0, |
| 83 | }); |
| 84 | |
| 85 | let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { |
| 86 | label: Some("Image render pipeline"), |
| 87 | layout: Some(&pipeline_layout), |
| 88 | vertex: wgpu::VertexState { |
| 89 | module: &shader, |
| 90 | entry_point: Some("vs_main"), |
| 91 | buffers: &[shader_types::Vertex::desc(), ImageInstanceData::desc()], |
| 92 | compilation_options: Default::default(), |
| 93 | }, |
| 94 | fragment: Some(wgpu::FragmentState { |
| 95 | module: &shader, |
| 96 | entry_point: Some("fs_main"), |
| 97 | targets: &[Some(color_target)], |
| 98 | compilation_options: Default::default(), |
| 99 | }), |
| 100 | primitive: wgpu::PrimitiveState::default(), |
| 101 | depth_stencil: None, |
| 102 | multisample: wgpu::MultisampleState::default(), |
| 103 | multiview_mask: None, |
| 104 | // Don't use a pipeline cache. Most desktop GPU drivers have their own internal caches, |
| 105 | // so we are unlikely to get much value out of this for the platforms Warp supports. |
| 106 | cache: None, |
| 107 | }); |
| 108 | |
| 109 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { |
| 110 | mag_filter: FilterMode::Linear, |
| 111 | min_filter: FilterMode::Linear, |
| 112 | ..Default::default() |
| 113 | }); |
| 114 | |
| 115 | Self { |
| 116 | render_pipeline, |
| 117 | texture_cache: TextureCache::new(), |
| 118 | texture_bind_group_layout, |
| 119 | sampler, |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | pub(super) fn initialize_for_layer( |
| 124 | &mut self, |
| 125 | layer: &Layer, |
| 126 | scene: &Scene, |
| 127 | per_frame_state: &mut PerFrameState, |
| 128 | ctx: &WGPUContext, |
| 129 | ) -> Option<LayerState> { |
| 130 | if layer.images.is_empty() && layer.icons.is_empty() { |
| 131 | return None; |
| 132 | } |
| 133 | |
| 134 | let start_offset = per_frame_state.image_data.len(); |
| 135 | let mut layer_state = LayerState { |
| 136 | start_offset, |
| 137 | image_textures: Vec::with_capacity(layer.images.len() + layer.icons.len()), |
| 138 | }; |
| 139 | let scale_factor = scene.scale_factor(); |
| 140 | for image in &layer.images { |
| 141 | let bounds = image.bounds * scale_factor; |
| 142 | let min_dimension = f32::min(bounds.height(), bounds.width()); |
| 143 | let corner_radius = crate::rendering::CornerRadius::from_ui_corner_radius( |
| 144 | image.corner_radius, |
| 145 | scale_factor, |
| 146 | min_dimension, |
| 147 | ); |
| 148 | |
| 149 | per_frame_state.image_data.push(ImageInstanceData::new( |
| 150 | image.bounds * scale_factor, |
| 151 | ColorModifier::Image { |
| 152 | opacity: (image.opacity * 255.) as u8, |
| 153 | }, |
| 154 | corner_radius, |
| 155 | )); |
| 156 | let (texture_id, _) = |
| 157 | self.texture_cache |
| 158 | .get_or_insert_by_asset(&image.asset, |asset| { |
| 159 | TextureInfo::new(asset, &self.texture_bind_group_layout, &self.sampler, ctx) |
| 160 | }); |
| 161 | layer_state.image_textures.push(texture_id); |
| 162 | } |
| 163 | |
| 164 | for icon in &layer.icons { |
| 165 | per_frame_state.image_data.push(ImageInstanceData::new( |
| 166 | icon.bounds * scale_factor, |
| 167 | ColorModifier::Icon { color: icon.color }, |
| 168 | crate::rendering::CornerRadius::default(), |
| 169 | )); |
| 170 | let (texture_id, _) = self |
| 171 | .texture_cache |
| 172 | .get_or_insert_by_asset(&icon.asset, |asset| { |
| 173 | TextureInfo::new(asset, &self.texture_bind_group_layout, &self.sampler, ctx) |
| 174 | }); |
| 175 | layer_state.image_textures.push(texture_id); |
| 176 | } |
| 177 | |
| 178 | Some(layer_state) |
| 179 | } |
| 180 | |
| 181 | pub(super) fn finalize_per_frame_state( |
| 182 | per_frame_state: &mut PerFrameState, |
| 183 | device: &Device, |
| 184 | device_lost: &Arc<AtomicBool>, |
| 185 | ) { |
| 186 | per_frame_state.buffer = create_buffer_init( |
| 187 | device, |
| 188 | device_lost, |
| 189 | &BufferInitDescriptor { |
| 190 | label: Some("Image instance buffer"), |
| 191 | contents: bytemuck::cast_slice(&per_frame_state.image_data), |
| 192 | usage: wgpu::BufferUsages::VERTEX, |
| 193 | }, |
| 194 | ) |
| 195 | .ok(); |
| 196 | } |
| 197 | |
| 198 | pub(super) fn draw<'a>( |
| 199 | &'a self, |
| 200 | render_pass: &mut RenderPass<'a>, |
| 201 | layer_state: &LayerState, |
| 202 | per_frame_state: &'a PerFrameState, |
| 203 | ) { |
| 204 | let Some(buffer) = per_frame_state.buffer.as_ref() else { |
| 205 | return; |
| 206 | }; |
| 207 | |
| 208 | render_pass.set_pipeline(&self.render_pipeline); |
| 209 | render_pass.set_vertex_buffer(1, buffer.slice(..)); |
| 210 | |
| 211 | for (index, texture_id) in layer_state.image_textures.iter().enumerate() { |
| 212 | let TextureInfo { bind_group, .. } = self |
| 213 | .texture_cache |
| 214 | .get(*texture_id) |
| 215 | .expect("texture should not leave cache between generating layer data and drawing"); |
| 216 | render_pass.set_bind_group(1, bind_group, &[]); |
| 217 | |
| 218 | let start_offset = layer_state.start_offset + index; |
| 219 | render_pass.draw_indexed( |
| 220 | 0..resources::quad::INDICES.len() as u32, |
| 221 | 0, |
| 222 | start_offset as u32..(start_offset + 1) as u32, |
| 223 | ); |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | pub(super) fn end_frame(&mut self) { |
| 228 | self.texture_cache.end_frame(); |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | /// A structure containing info about a GPU texture from which we can render |
| 233 | /// a particular static image asset. |
| 234 | struct TextureInfo { |
| 235 | /// A handle to the set of resources that are needed to bind the texture |
| 236 | /// in a shader. |
| 237 | bind_group: BindGroup, |
| 238 | } |
| 239 | |
| 240 | impl TextureInfo { |
| 241 | fn new( |
| 242 | asset: &Arc<StaticImage>, |
| 243 | bind_group_layout: &BindGroupLayout, |
| 244 | sampler: &Sampler, |
| 245 | ctx: &WGPUContext, |
| 246 | ) -> Self { |
| 247 | let texture_size = Extent3d { |
| 248 | width: asset.width(), |
| 249 | height: asset.height(), |
| 250 | depth_or_array_layers: 1, |
| 251 | }; |
| 252 | let desc = TextureDescriptor { |
| 253 | label: Some("Image texture"), |
| 254 | size: texture_size, |
| 255 | mip_level_count: 1, |
| 256 | sample_count: 1, |
| 257 | dimension: wgpu::TextureDimension::D2, |
| 258 | format: TextureFormat::Rgba8Unorm, |
| 259 | usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, |
| 260 | view_formats: &[], |
| 261 | }; |
| 262 | |
| 263 | let texture = ctx.resources.device.create_texture(&desc); |
| 264 | let bytes_per_row: u32 = 4 * asset.width(); |
| 265 | ctx.resources.queue.write_texture( |
| 266 | wgpu::TexelCopyTextureInfo { |
| 267 | texture: &texture, |
| 268 | mip_level: 0, |
| 269 | origin: wgpu::Origin3d::ZERO, |
| 270 | aspect: wgpu::TextureAspect::All, |
| 271 | }, |
| 272 | asset.rgba_bytes(), |
| 273 | wgpu::TexelCopyBufferLayout { |
| 274 | offset: 0, |
| 275 | bytes_per_row: Some(bytes_per_row), |
| 276 | rows_per_image: None, |
| 277 | }, |
| 278 | texture_size, |
| 279 | ); |
| 280 | |
| 281 | let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); |
| 282 | let bind_group = ctx |
| 283 | .resources |
| 284 | .device |
| 285 | .create_bind_group(&BindGroupDescriptor { |
| 286 | layout: bind_group_layout, |
| 287 | entries: &[ |
| 288 | wgpu::BindGroupEntry { |
| 289 | binding: 0, |
| 290 | resource: wgpu::BindingResource::TextureView(&view), |
| 291 | }, |
| 292 | wgpu::BindGroupEntry { |
| 293 | binding: 1, |
| 294 | resource: wgpu::BindingResource::Sampler(sampler), |
| 295 | }, |
| 296 | ], |
| 297 | label: None, |
| 298 | }); |
| 299 | |
| 300 | Self { bind_group } |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | mod shaders { |
| 305 | use crate::rendering::wgpu::shader_types::{vec4f, ColorF, Vector4F}; |
| 306 | use crate::rendering::CornerRadius; |
| 307 | use pathfinder_color::ColorU; |
| 308 | use pathfinder_geometry::rect::RectF; |
| 309 | |
| 310 | /// Icons support overriding the color, whereas images only allow setting the opacity. |
| 311 | pub(super) enum ColorModifier { |
| 312 | Icon { color: ColorU }, |
| 313 | Image { opacity: u8 }, |
| 314 | } |
| 315 | |
| 316 | impl From<ColorModifier> for ColorF { |
| 317 | fn from(color_mod: ColorModifier) -> Self { |
| 318 | match color_mod { |
| 319 | ColorModifier::Icon { color } => color.to_f32().into(), |
| 320 | ColorModifier::Image { opacity } => ColorU::new(0, 0, 0, opacity).to_f32().into(), |
| 321 | } |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | #[repr(C)] |
| 326 | #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] |
| 327 | pub(super) struct ImageInstanceData { |
| 328 | bounds: Vector4F, |
| 329 | color: ColorF, |
| 330 | is_icon: u32, |
| 331 | corner_radius: Vector4F, |
| 332 | } |
| 333 | |
| 334 | impl ImageInstanceData { |
| 335 | const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![ |
| 336 | 1 => Float32x4, // Bounds |
| 337 | 2 => Float32x4, // Color |
| 338 | 3 => Uint32, // Boolean, image or icon |
| 339 | 4 => Float32x4, // Corner radius |
| 340 | ]; |
| 341 | |
| 342 | pub(super) fn new( |
| 343 | bounds: RectF, |
| 344 | color_modifier: ColorModifier, |
| 345 | corner_radius: CornerRadius, |
| 346 | ) -> Self { |
| 347 | Self { |
| 348 | bounds: bounds.into(), |
| 349 | is_icon: matches!(color_modifier, ColorModifier::Icon { .. }).into(), |
| 350 | color: color_modifier.into(), |
| 351 | corner_radius: vec4f( |
| 352 | corner_radius.top_left, |
| 353 | corner_radius.top_right, |
| 354 | corner_radius.bottom_left, |
| 355 | corner_radius.bottom_right, |
| 356 | ), |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | pub(super) fn desc() -> wgpu::VertexBufferLayout<'static> { |
| 361 | use std::mem; |
| 362 | |
| 363 | wgpu::VertexBufferLayout { |
| 364 | array_stride: mem::size_of::<Self>() as wgpu::BufferAddress, |
| 365 | step_mode: wgpu::VertexStepMode::Instance, |
| 366 | attributes: &Self::ATTRIBS, |
| 367 | } |
| 368 | } |
| 369 | } |
| 370 | } |
| 371 |