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/glyph.rs
1use crate::fonts::SubpixelAlignment;
2use crate::rendering::atlas::TextureId;
3use crate::rendering::wgpu::renderer::WGPUContext;
4use crate::rendering::wgpu::texture_with_bind_group::TextureWithBindGroup;
5use crate::rendering::wgpu::{resources, shader_types};
6use crate::rendering::{GlyphCache, GlyphConfig};
7use crate::scene::{GlyphFade, Layer};
8use crate::Scene;
9use pathfinder_geometry::rect::RectF;
10use std::borrow::Cow;
11use std::collections::HashMap;
12use std::sync::{atomic::AtomicBool, Arc};
13use wgpu::util::BufferInitDescriptor;
14use wgpu::{
15 BindGroupLayout, BufferUsages, ColorTargetState, Device, FilterMode, RenderPass,
16 RenderPipeline, Sampler,
17};
18 
19use super::util::create_buffer_init;
20 
21pub(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)]
29pub(super) struct PerFrameState {
30 glyph_data: Vec<shaders::GlyphInstanceData>,
31 buffer: Option<wgpu::Buffer>,
32}
33 
34pub(super) struct LayerState {
35 textures: Vec<PerTextureState>,
36}
37 
38pub(super) struct PerTextureState {
39 texture_id: TextureId,
40 start_offset: usize,
41 len: usize,
42}
43impl 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 
294mod 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