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/image.rs
1use crate::image_cache::StaticImage;
2use crate::rendering::texture_cache::{TextureCache, TextureCacheIndex};
3use crate::rendering::wgpu::{resources, shader_types};
4use crate::scene::Layer;
5use crate::Scene;
6use std::borrow::Cow;
7use std::sync::{atomic::AtomicBool, Arc};
8use wgpu::util::BufferInitDescriptor;
9use wgpu::{
10 BindGroup, BindGroupDescriptor, BindGroupLayout, ColorTargetState, Device, Extent3d,
11 FilterMode, RenderPass, RenderPipeline, Sampler, TextureDescriptor, TextureFormat,
12 TextureUsages,
13};
14 
15use self::shaders::{ColorModifier, ImageInstanceData};
16 
17use super::util::create_buffer_init;
18use super::WGPUContext;
19 
20pub(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)]
28pub(super) struct PerFrameState {
29 image_data: Vec<shaders::ImageInstanceData>,
30 buffer: Option<wgpu::Buffer>,
31}
32 
33pub(super) struct LayerState {
34 start_offset: usize,
35 image_textures: Vec<TextureCacheIndex>,
36}
37 
38impl 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.
234struct 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 
240impl 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 
304mod 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