1use std::sync::Arc;
47
48use astrelis_core::alloc::HashMap;
49use astrelis_core::math::Vec2;
50use astrelis_core::profiling::profile_function;
51use cosmic_text::{CacheKey, Color as CosmicColor, Metrics};
52
53use astrelis_render::{AsWgpu, GpuTexture, GraphicsContext, UniformBuffer, Viewport, wgpu};
54
55use crate::effects::TextEffects;
56use crate::font::FontSystem;
57use crate::sdf::{SdfConfig, generate_sdf};
58use crate::text::{Text, TextMetrics};
59
60use super::shared::{
61 AtlasEntry, AtlasPacker, GlyphPlacement, SdfAtlasEntry, SdfCacheKey, SdfParams, SharedContext,
62 TextBuffer, TextRender, TextRendererConfig, TextVertex,
63};
64use super::{SDF_BASE_SIZE, SDF_DEFAULT_SPREAD, orthographic_projection};
65
66pub(crate) struct SdfBackend {
70 pub(crate) pipeline: wgpu::RenderPipeline,
72 #[allow(dead_code)]
73 pub(crate) bind_group_layout: wgpu::BindGroupLayout,
74 pub(crate) atlas: GpuTexture,
76 #[allow(dead_code)]
77 pub(crate) sampler: wgpu::Sampler,
78 pub(crate) bind_group: wgpu::BindGroup,
79 pub(crate) params_buffer: UniformBuffer<SdfParams>,
81 #[allow(dead_code)]
82 pub(crate) params_bind_group_layout: wgpu::BindGroupLayout,
83 pub(crate) params_bind_group: wgpu::BindGroup,
84
85 pub(crate) atlas_data: Vec<u8>,
87 pub(crate) atlas_entries: HashMap<SdfCacheKey, SdfAtlasEntry>,
88 pub(crate) atlas_packer: AtlasPacker,
89 pub(crate) atlas_dirty: bool,
90
91 pub(crate) config: SdfConfig,
93}
94
95impl SdfBackend {
96 pub fn new(shared: &SharedContext, atlas_size: u32, config: SdfConfig) -> Self {
98 let renderer = &shared.renderer;
99
100 let shader = renderer.create_shader(
102 Some("Text SDF Shader"),
103 include_str!("../../shaders/text_sdf.wgsl"),
104 );
105
106 let atlas = renderer.create_gpu_texture_2d(
108 Some("SDF Text Atlas"),
109 atlas_size,
110 atlas_size,
111 wgpu::TextureFormat::R8Unorm,
112 wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
113 );
114
115 let atlas_data = vec![0u8; (atlas_size * atlas_size) as usize];
116 let sampler = renderer.create_linear_sampler(Some("SDF Text Sampler"));
117
118 let bind_group_layout = renderer.create_bind_group_layout(
120 Some("SDF Text Bind Group Layout"),
121 &[
122 wgpu::BindGroupLayoutEntry {
123 binding: 0,
124 visibility: wgpu::ShaderStages::FRAGMENT,
125 ty: wgpu::BindingType::Texture {
126 multisampled: false,
127 view_dimension: wgpu::TextureViewDimension::D2,
128 sample_type: wgpu::TextureSampleType::Float { filterable: true },
129 },
130 count: None,
131 },
132 wgpu::BindGroupLayoutEntry {
133 binding: 1,
134 visibility: wgpu::ShaderStages::FRAGMENT,
135 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
136 count: None,
137 },
138 ],
139 );
140
141 let bind_group = renderer.create_bind_group(
142 Some("SDF Text Bind Group"),
143 &bind_group_layout,
144 &[
145 wgpu::BindGroupEntry {
146 binding: 0,
147 resource: wgpu::BindingResource::TextureView(atlas.view()),
148 },
149 wgpu::BindGroupEntry {
150 binding: 1,
151 resource: wgpu::BindingResource::Sampler(&sampler),
152 },
153 ],
154 );
155
156 let params_buffer =
158 renderer.create_typed_uniform(Some("SDF Params Buffer"), &SdfParams::default());
159
160 let params_bind_group_layout = renderer.create_bind_group_layout(
162 Some("SDF Params Bind Group Layout"),
163 &[wgpu::BindGroupLayoutEntry {
164 binding: 0,
165 visibility: wgpu::ShaderStages::FRAGMENT,
166 ty: wgpu::BindingType::Buffer {
167 ty: wgpu::BufferBindingType::Uniform,
168 has_dynamic_offset: false,
169 min_binding_size: None,
170 },
171 count: None,
172 }],
173 );
174
175 let params_bind_group = renderer.create_bind_group(
176 Some("SDF Params Bind Group"),
177 ¶ms_bind_group_layout,
178 &[wgpu::BindGroupEntry {
179 binding: 0,
180 resource: params_buffer.as_binding(),
181 }],
182 );
183
184 let pipeline_layout = renderer.create_pipeline_layout(
186 Some("SDF Text Pipeline Layout"),
187 &[
188 &bind_group_layout,
189 &shared.uniform_bind_group_layout,
190 ¶ms_bind_group_layout,
191 ],
192 &[],
193 );
194
195 let pipeline = renderer.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
197 label: Some("SDF Text Pipeline"),
198 layout: Some(&pipeline_layout),
199 vertex: wgpu::VertexState {
200 module: &shader,
201 entry_point: Some("vs_main"),
202 buffers: &[wgpu::VertexBufferLayout {
203 array_stride: std::mem::size_of::<TextVertex>() as u64,
204 step_mode: wgpu::VertexStepMode::Vertex,
205 attributes: &wgpu::vertex_attr_array![
206 0 => Float32x2,
207 1 => Float32x2,
208 2 => Float32x4,
209 ],
210 }],
211 compilation_options: wgpu::PipelineCompilationOptions::default(),
212 },
213 fragment: Some(wgpu::FragmentState {
214 module: &shader,
215 entry_point: Some("fs_main"),
216 targets: &[Some(wgpu::ColorTargetState {
217 format: wgpu::TextureFormat::Bgra8UnormSrgb,
218 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
219 write_mask: wgpu::ColorWrites::ALL,
220 })],
221 compilation_options: wgpu::PipelineCompilationOptions::default(),
222 }),
223 primitive: wgpu::PrimitiveState {
224 topology: wgpu::PrimitiveTopology::TriangleList,
225 strip_index_format: None,
226 front_face: wgpu::FrontFace::Ccw,
227 cull_mode: None,
228 polygon_mode: wgpu::PolygonMode::Fill,
229 unclipped_depth: false,
230 conservative: false,
231 },
232 depth_stencil: None,
233 multisample: wgpu::MultisampleState {
234 count: 1,
235 mask: !0,
236 alpha_to_coverage_enabled: false,
237 },
238 multiview: None,
239 cache: None,
240 });
241
242 Self {
243 pipeline,
244 bind_group_layout,
245 atlas,
246 sampler,
247 bind_group,
248 params_buffer,
249 params_bind_group_layout,
250 params_bind_group,
251 atlas_data,
252 atlas_entries: HashMap::new(),
253 atlas_packer: AtlasPacker::new(atlas_size),
254 atlas_dirty: false,
255 config,
256 }
257 }
258
259 pub fn ensure_glyph(
261 &mut self,
262 shared: &SharedContext,
263 cache_key: CacheKey,
264 ) -> Option<&SdfAtlasEntry> {
265 let sdf_key = SdfCacheKey::from_cache_key(cache_key);
266
267 if self.atlas_entries.contains_key(&sdf_key) {
269 return self.atlas_entries.get(&sdf_key);
270 }
271
272 let base_cache_key = CacheKey {
274 font_id: cache_key.font_id,
275 glyph_id: cache_key.glyph_id,
276 font_size_bits: SDF_BASE_SIZE.to_bits(),
277 x_bin: cache_key.x_bin,
278 y_bin: cache_key.y_bin,
279 flags: cache_key.flags,
280 };
281
282 let mut font_system = shared
284 .font_system
285 .write()
286 .map_err(|e| crate::error::TextError::LockPoisoned(e.to_string()))
287 .ok()?;
288 let mut swash_cache = shared
289 .swash_cache
290 .write()
291 .map_err(|e| crate::error::TextError::LockPoisoned(e.to_string()))
292 .ok()?;
293 let image = match swash_cache.get_image(&mut font_system, base_cache_key) {
294 Some(img) => img.clone(),
295 None => return None,
296 };
297
298 drop(font_system);
299 drop(swash_cache);
300
301 let width = image.placement.width;
302 let height = image.placement.height;
303
304 if width == 0 || height == 0 {
305 return None;
306 }
307
308 let spread = self.config.mode.spread().max(SDF_DEFAULT_SPREAD);
310 let sdf_data = generate_sdf(&image, spread);
311
312 if sdf_data.is_empty() {
313 return None;
314 }
315
316 let padding = (spread.ceil() as u32) * 2;
318 let padded_width = width + padding * 2;
319 let padded_height = height + padding * 2;
320
321 let atlas_entry = self.atlas_packer.pack(padded_width, padded_height)?;
323
324 let atlas_size = self.atlas.width();
326 for y in 0..height {
327 for x in 0..width {
328 let src_idx = (y * width + x) as usize;
329 let dst_x = atlas_entry.x + padding + x;
330 let dst_y = atlas_entry.y + padding + y;
331 let dst_idx = (dst_y * atlas_size + dst_x) as usize;
332 if src_idx < sdf_data.len() && dst_idx < self.atlas_data.len() {
333 self.atlas_data[dst_idx] = sdf_data[src_idx];
334 }
335 }
336 }
337
338 let base_placement = GlyphPlacement {
340 left: image.placement.left as f32,
341 top: image.placement.top as f32,
342 width: width as f32,
343 height: height as f32,
344 };
345
346 let sdf_entry = SdfAtlasEntry {
347 entry: AtlasEntry {
348 x: atlas_entry.x + padding,
349 y: atlas_entry.y + padding,
350 width,
351 height,
352 },
353 spread,
354 base_size: SDF_BASE_SIZE,
355 base_placement,
356 };
357
358 self.atlas_dirty = true;
359 self.atlas_entries.insert(sdf_key, sdf_entry);
360 self.atlas_entries.get(&sdf_key)
361 }
362
363 pub fn upload_atlas(&mut self, shared: &SharedContext) {
365 if !self.atlas_dirty {
366 return;
367 }
368
369 let atlas_size = self.atlas.width();
370 shared.renderer.queue().write_texture(
371 wgpu::TexelCopyTextureInfo {
372 texture: self.atlas.as_wgpu(),
373 mip_level: 0,
374 origin: wgpu::Origin3d::ZERO,
375 aspect: wgpu::TextureAspect::All,
376 },
377 &self.atlas_data,
378 wgpu::TexelCopyBufferLayout {
379 offset: 0,
380 bytes_per_row: Some(atlas_size),
381 rows_per_image: Some(atlas_size),
382 },
383 wgpu::Extent3d {
384 width: atlas_size,
385 height: atlas_size,
386 depth_or_array_layers: 1,
387 },
388 );
389
390 self.atlas_dirty = false;
391 }
392
393 pub fn update_params(&self, shared: &SharedContext, params: &SdfParams) {
395 self.params_buffer
396 .write_uniform(shared.renderer.queue(), params);
397 }
398}
399
400pub struct SdfTextRenderer {
407 shared: SharedContext,
408 backend: SdfBackend,
409
410 vertices: Vec<TextVertex>,
412 indices: Vec<u16>,
413}
414
415impl SdfTextRenderer {
416 pub fn new(context: Arc<GraphicsContext>, font_system: FontSystem) -> Self {
418 Self::with_config(context, font_system, TextRendererConfig::default())
419 }
420
421 pub fn with_config(
423 context: Arc<GraphicsContext>,
424 font_system: FontSystem,
425 config: TextRendererConfig,
426 ) -> Self {
427 let shared = SharedContext::new(context, font_system.inner());
428 let backend = SdfBackend::new(&shared, config.atlas_size, config.sdf);
429
430 Self {
431 shared,
432 backend,
433 vertices: Vec::new(),
434 indices: Vec::new(),
435 }
436 }
437
438 pub fn measure_text(&self, text: &Text) -> (f32, f32) {
440 profile_function!();
441 let scale = self.shared.scale_factor();
442
443 let mut font_system = match self.shared.font_system.write() {
445 Ok(guard) => guard,
446 Err(e) => {
447 tracing::error!("Font system lock poisoned: {}. Returning zero size.", e);
448 return (0.0, 0.0);
449 }
450 };
451
452 let mut buffer = TextBuffer::new(&mut font_system);
453 buffer.set_text(&mut font_system, text, scale);
454 buffer.layout(&mut font_system);
455 let (width, height) = buffer.bounds();
456 (width / scale, height / scale)
457 }
458
459 pub fn buffer_bounds(&self, buffer: &TextBuffer) -> (f32, f32) {
461 let scale = self.shared.scale_factor();
462 let (width, height) = buffer.bounds();
463 (width / scale, height / scale)
464 }
465
466 pub fn get_text_metrics(&self, text: &Text) -> TextMetrics {
468 profile_function!();
469 let font_size = text.get_font_size();
470 let line_height_multiplier = text.get_line_height();
471 let scale = self.shared.scale_factor();
472
473 let metrics = Metrics::new(
474 font_size * scale,
475 font_size * scale * line_height_multiplier,
476 );
477
478 let line_height = metrics.line_height / scale;
479 let ascent = font_size * 0.8;
480 let descent = font_size * 0.2;
481
482 TextMetrics {
483 ascent,
484 descent,
485 line_height,
486 baseline_offset: ascent,
487 }
488 }
489
490 pub fn set_viewport(&mut self, viewport: Viewport) {
495 self.shared.set_viewport(viewport);
496 }
497
498 pub fn set_sdf_config(&mut self, config: SdfConfig) {
500 self.backend.config = config;
501 }
502
503 pub fn sdf_config(&self) -> &SdfConfig {
505 &self.backend.config
506 }
507
508 pub fn prepare(&mut self, text: &Text) -> TextBuffer {
510 profile_function!();
511
512 let mut font_system = match self.shared.font_system.write() {
514 Ok(guard) => guard,
515 Err(e) => {
516 tracing::error!(
517 "Font system lock poisoned during prepare: {}. Attempting recovery.",
518 e
519 );
520 self.shared.font_system.write().unwrap_or_else(|poisoned| {
522 tracing::warn!("Clearing poisoned lock and continuing");
523 poisoned.into_inner()
524 })
525 }
526 };
527
528 let mut buffer = TextBuffer::new(&mut font_system);
529 buffer.set_text(&mut font_system, text, self.shared.scale_factor());
530 buffer.layout(&mut font_system);
531 buffer
532 }
533
534 pub fn draw_text(&mut self, buffer: &mut TextBuffer, position: Vec2) {
536 let params = SdfParams::default();
538 self.backend.update_params(&self.shared, ¶ms);
539 self.draw_text_internal(buffer, position);
540 }
541
542 pub fn draw_text_with_effects(
544 &mut self,
545 buffer: &mut TextBuffer,
546 position: Vec2,
547 effects: &TextEffects,
548 ) {
549 profile_function!();
550
551 let sdf_params = SdfParams::from_effects(effects, &self.backend.config);
553 self.backend.update_params(&self.shared, &sdf_params);
554
555 self.draw_text_internal(buffer, position);
557 }
558
559 fn draw_text_internal(&mut self, buffer: &mut TextBuffer, position: Vec2) {
561 profile_function!();
562
563 let scale = self.shared.scale_factor();
564
565 let mut font_system = match self.shared.font_system.write() {
567 Ok(guard) => guard,
568 Err(e) => {
569 tracing::error!(
570 "Font system lock poisoned during draw: {}. Skipping layout.",
571 e
572 );
573 return; }
575 };
576
577 buffer.layout(&mut font_system);
578 drop(font_system);
579
580 for run in buffer.buffer.layout_runs() {
582 for glyph in run.glyphs.iter() {
583 let physical_glyph = glyph.physical((position.x, position.y + run.line_y), 1.0);
584 let cache_key = physical_glyph.cache_key;
585
586 let sdf_entry = match self.backend.ensure_glyph(&self.shared, cache_key) {
588 Some(e) => e.clone(),
589 None => continue,
590 };
591
592 let target_size = f32::from_bits(cache_key.font_size_bits);
594 let size_scale = target_size / sdf_entry.base_size;
595
596 let scaled_left = sdf_entry.base_placement.left * size_scale;
598 let scaled_top = sdf_entry.base_placement.top * size_scale;
599 let scaled_width = sdf_entry.base_placement.width * size_scale;
600 let scaled_height = sdf_entry.base_placement.height * size_scale;
601
602 let x = physical_glyph.x as f32 + scaled_left;
603 let y = physical_glyph.y as f32 - scaled_top;
604 let w = scaled_width;
605 let h = scaled_height;
606
607 let x = x / scale;
608 let y = y / scale;
609 let w = w / scale;
610 let h = h / scale;
611
612 let (u0, v0, u1, v1) = sdf_entry.entry.uv_coords(self.backend.atlas.width());
613
614 let color = glyph.color_opt.unwrap_or(CosmicColor::rgb(255, 255, 255));
615 let color_f = [
616 color.r() as f32 / 255.0,
617 color.g() as f32 / 255.0,
618 color.b() as f32 / 255.0,
619 color.a() as f32 / 255.0,
620 ];
621
622 let x = (x * scale).round() / scale;
624 let y = (y * scale).round() / scale;
625
626 let idx = self.vertices.len() as u16;
628
629 self.vertices.push(TextVertex {
630 position: [x, y],
631 tex_coords: [u0, v0],
632 color: color_f,
633 });
634 self.vertices.push(TextVertex {
635 position: [x + w, y],
636 tex_coords: [u1, v0],
637 color: color_f,
638 });
639 self.vertices.push(TextVertex {
640 position: [x + w, y + h],
641 tex_coords: [u1, v1],
642 color: color_f,
643 });
644 self.vertices.push(TextVertex {
645 position: [x, y + h],
646 tex_coords: [u0, v1],
647 color: color_f,
648 });
649
650 self.indices
651 .extend_from_slice(&[idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]);
652 }
653 }
654 }
655
656 pub fn render(&mut self, render_pass: &mut wgpu::RenderPass) {
658 profile_function!();
659
660 debug_assert!(
661 self.shared.viewport.is_valid(),
662 "Viewport size must be set before rendering text."
663 );
664
665 if self.vertices.is_empty() {
666 return;
667 }
668
669 self.backend.upload_atlas(&self.shared);
670
671 let vertex_buffer = self
673 .shared
674 .renderer
675 .create_vertex_buffer(Some("SDF Text Vertex Buffer"), &self.vertices);
676
677 let index_buffer = self
678 .shared
679 .renderer
680 .create_index_buffer(Some("SDF Text Index Buffer"), &self.indices);
681
682 let size = self.shared.viewport.to_logical();
684 let projection = orthographic_projection(size.width, size.height);
685 let uniform_buffer = self
686 .shared
687 .renderer
688 .create_uniform_buffer(Some("SDF Text Projection"), &projection);
689
690 let uniform_bind_group = self.shared.renderer.create_bind_group(
692 Some("SDF Text Uniform Bind Group"),
693 &self.shared.uniform_bind_group_layout,
694 &[wgpu::BindGroupEntry {
695 binding: 0,
696 resource: uniform_buffer.as_entire_binding(),
697 }],
698 );
699
700 render_pass.set_pipeline(&self.backend.pipeline);
702 render_pass.set_bind_group(0, &self.backend.bind_group, &[]);
703 render_pass.set_bind_group(1, &uniform_bind_group, &[]);
704 render_pass.set_bind_group(2, &self.backend.params_bind_group, &[]);
705 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
706 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
707 render_pass.draw_indexed(0..self.indices.len() as u32, 0, 0..1);
708
709 self.vertices.clear();
711 self.indices.clear();
712 }
713
714 pub fn font_system(&self) -> std::sync::Arc<std::sync::RwLock<cosmic_text::FontSystem>> {
716 self.shared.font_system.clone()
717 }
718
719 pub fn swash_cache(&self) -> std::sync::Arc<std::sync::RwLock<cosmic_text::SwashCache>> {
721 self.shared.swash_cache.clone()
722 }
723
724 pub fn atlas_size(&self) -> u32 {
726 self.backend.atlas.width()
727 }
728}
729
730impl TextRender for SdfTextRenderer {
731 fn prepare(&mut self, text: &Text) -> TextBuffer {
732 SdfTextRenderer::prepare(self, text)
733 }
734
735 fn draw_text(&mut self, buffer: &mut TextBuffer, position: Vec2) {
736 SdfTextRenderer::draw_text(self, buffer, position)
737 }
738
739 fn render(&mut self, render_pass: &mut wgpu::RenderPass) {
740 SdfTextRenderer::render(self, render_pass)
741 }
742
743 fn measure_text(&self, text: &Text) -> (f32, f32) {
744 SdfTextRenderer::measure_text(self, text)
745 }
746
747 fn set_viewport(&mut self, viewport: Viewport) {
748 SdfTextRenderer::set_viewport(self, viewport)
749 }
750
751 fn buffer_bounds(&self, buffer: &TextBuffer) -> (f32, f32) {
752 SdfTextRenderer::buffer_bounds(self, buffer)
753 }
754}