1use std::borrow::Cow;
23
24use damascene_core::ir::TextAnchor;
25use damascene_core::shader::stock_wgsl;
26use damascene_core::text::atlas::{
27 ATLAS_BYTES_PER_PIXEL, AtlasPage, AtlasRect, GlyphAtlas, RunStyle, ShapedGlyph, ShapedRun,
28};
29use damascene_core::text::msdf_atlas::{
30 DEFAULT_BASE_EM, DEFAULT_SPREAD, MSDF_BYTES_PER_PIXEL, MsdfAtlas, MsdfAtlasPage, MsdfGlyphKey,
31 MsdfRect, MsdfSlot,
32};
33use damascene_core::tree::{FontFamily, Rect, TextWrap};
34
35use bytemuck::{Pod, Zeroable};
36use cosmic_text::fontdb;
37use ttf_parser::Face;
38
39use damascene_core::color::ColorSpace;
40use damascene_core::paint::{DEFAULT_WORKING_COLOR_SPACE, PhysicalScissor, rgba_f32_in};
41use damascene_core::runtime::TextRecorder;
42
43const INITIAL_INSTANCE_CAPACITY: usize = 256;
44
45const COLOR_INSTANCE_ATTRS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
46 1 => Float32x4, 2 => Float32x4, 3 => Float32x4, ];
50
51const MSDF_INSTANCE_ATTRS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
52 1 => Float32x4, 2 => Float32x4, 3 => Float32x4, 4 => Float32x4, ];
57
58const HIGHLIGHT_INSTANCE_ATTRS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
59 1 => Float32x4, 2 => Float32x4, ];
62
63#[repr(C)]
64#[derive(Copy, Clone, Pod, Zeroable, Debug)]
65pub(crate) struct ColorGlyphInstance {
66 pub rect: [f32; 4],
67 pub uv: [f32; 4],
68 pub color: [f32; 4],
69}
70
71#[repr(C)]
72#[derive(Copy, Clone, Pod, Zeroable, Debug)]
73pub(crate) struct MsdfGlyphInstance {
74 pub rect: [f32; 4],
75 pub uv: [f32; 4],
76 pub color: [f32; 4],
77 pub params: [f32; 4],
78}
79
80#[repr(C)]
81#[derive(Copy, Clone, Pod, Zeroable, Debug)]
82pub(crate) struct HighlightInstance {
83 pub rect: [f32; 4],
84 pub color: [f32; 4],
85}
86
87#[derive(Clone, Copy, PartialEq, Eq)]
88pub(crate) enum TextRunKind {
89 Color,
90 Msdf,
91 Highlight,
92}
93
94#[derive(Clone, Copy)]
95pub(crate) struct TextRun {
96 pub kind: TextRunKind,
97 pub page: u32,
98 pub scissor: Option<PhysicalScissor>,
99 pub first: u32,
100 pub count: u32,
101}
102
103struct PageTexture {
104 texture: wgpu::Texture,
105 bind_group: wgpu::BindGroup,
106}
107
108#[derive(Clone)]
129pub struct SharedText(pub(crate) std::sync::Arc<std::sync::Mutex<SharedTextInner>>);
130
131pub(crate) struct SharedTextInner {
132 pub(crate) atlas: GlyphAtlas,
133 pub(crate) msdf_atlas: MsdfAtlas,
134
135 color_pages: Vec<PageTexture>,
136 color_page_bind_layout: wgpu::BindGroupLayout,
137 color_sampler: wgpu::Sampler,
138
139 msdf_pages: Vec<PageTexture>,
140 msdf_page_bind_layout: wgpu::BindGroupLayout,
141 msdf_sampler: wgpu::Sampler,
142
143 attached: u32,
147}
148
149impl SharedText {
150 pub fn new(device: &wgpu::Device) -> Self {
155 let color_page_bind_layout = create_page_bind_layout(device, "color");
156 let msdf_page_bind_layout = create_page_bind_layout(device, "msdf");
157 let color_sampler = create_page_sampler(device, "color");
158 let msdf_sampler = create_page_sampler(device, "msdf");
159 Self(std::sync::Arc::new(std::sync::Mutex::new(
160 SharedTextInner {
161 atlas: GlyphAtlas::new(),
162 msdf_atlas: MsdfAtlas::new(DEFAULT_BASE_EM, DEFAULT_SPREAD),
163 color_pages: Vec::new(),
164 color_page_bind_layout,
165 color_sampler,
166 msdf_pages: Vec::new(),
167 msdf_page_bind_layout,
168 msdf_sampler,
169 attached: 0,
170 },
171 )))
172 }
173
174 pub fn warm_default_glyphs(&self) {
182 self.lock().warm_default_glyphs();
183 }
184
185 pub(crate) fn lock(&self) -> std::sync::MutexGuard<'_, SharedTextInner> {
186 match self.0.lock() {
190 Ok(g) => g,
191 Err(poisoned) => poisoned.into_inner(),
192 }
193 }
194}
195
196pub(crate) struct TextPaint {
197 shared: SharedText,
199
200 color_page_bgs: Vec<wgpu::BindGroup>,
206 msdf_page_bgs: Vec<wgpu::BindGroup>,
207
208 color_instances: Vec<ColorGlyphInstance>,
210 color_instance_buf: wgpu::Buffer,
211 color_instance_capacity: usize,
212 color_pipeline: wgpu::RenderPipeline,
213
214 msdf_instances: Vec<MsdfGlyphInstance>,
216 msdf_instance_buf: wgpu::Buffer,
217 msdf_instance_capacity: usize,
218 msdf_pipeline: wgpu::RenderPipeline,
219
220 highlight_instances: Vec<HighlightInstance>,
222 highlight_instance_buf: wgpu::Buffer,
223 highlight_instance_capacity: usize,
224 highlight_pipeline: wgpu::RenderPipeline,
225
226 color_pipeline_layout: wgpu::PipelineLayout,
232 msdf_pipeline_layout: wgpu::PipelineLayout,
233 highlight_pipeline_layout: wgpu::PipelineLayout,
234 sample_count: u32,
235
236 runs: Vec<TextRun>,
237
238 working_color_space: ColorSpace,
243}
244
245impl Drop for TextPaint {
246 fn drop(&mut self) {
247 let mut inner = self.shared.lock();
248 inner.attached = inner.attached.saturating_sub(1);
249 let n = inner.attached.max(1);
250 inner.atlas.set_lru_protection_window(n);
251 inner.msdf_atlas.set_lru_protection_window(n);
252 }
253}
254
255fn create_page_bind_layout(device: &wgpu::Device, kind: &str) -> wgpu::BindGroupLayout {
258 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
259 label: Some(&format!("damascene_wgpu::text::{kind}_page_bind_layout")),
260 entries: &[
261 wgpu::BindGroupLayoutEntry {
262 binding: 0,
263 visibility: wgpu::ShaderStages::FRAGMENT,
264 ty: wgpu::BindingType::Texture {
265 sample_type: wgpu::TextureSampleType::Float { filterable: true },
266 view_dimension: wgpu::TextureViewDimension::D2,
267 multisampled: false,
268 },
269 count: None,
270 },
271 wgpu::BindGroupLayoutEntry {
272 binding: 1,
273 visibility: wgpu::ShaderStages::FRAGMENT,
274 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
275 count: None,
276 },
277 ],
278 })
279}
280
281fn create_page_sampler(device: &wgpu::Device, kind: &str) -> wgpu::Sampler {
282 device.create_sampler(&wgpu::SamplerDescriptor {
283 label: Some(&format!("damascene_wgpu::text::{kind}_sampler")),
284 address_mode_u: wgpu::AddressMode::ClampToEdge,
285 address_mode_v: wgpu::AddressMode::ClampToEdge,
286 address_mode_w: wgpu::AddressMode::ClampToEdge,
287 mag_filter: wgpu::FilterMode::Linear,
288 min_filter: wgpu::FilterMode::Linear,
289 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
290 ..Default::default()
291 })
292}
293
294impl TextPaint {
295 pub(crate) fn new(
296 device: &wgpu::Device,
297 target_format: wgpu::TextureFormat,
298 sample_count: u32,
299 frame_bind_layout: &wgpu::BindGroupLayout,
300 ) -> Self {
301 Self::with_shared(
302 device,
303 target_format,
304 sample_count,
305 frame_bind_layout,
306 SharedText::new(device),
307 )
308 }
309
310 pub(crate) fn with_shared(
315 device: &wgpu::Device,
316 target_format: wgpu::TextureFormat,
317 sample_count: u32,
318 frame_bind_layout: &wgpu::BindGroupLayout,
319 shared: SharedText,
320 ) -> Self {
321 let (color_pipeline_layout, msdf_pipeline_layout) = {
322 let mut inner = shared.lock();
323 inner.attached += 1;
324 let n = inner.attached;
325 inner.atlas.set_lru_protection_window(n);
326 inner.msdf_atlas.set_lru_protection_window(n);
327 (
328 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
329 label: Some("damascene_wgpu::text::color_pipeline_layout"),
330 bind_group_layouts: &[
331 Some(frame_bind_layout),
332 Some(&inner.color_page_bind_layout),
333 ],
334 immediate_size: 0,
335 }),
336 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
337 label: Some("damascene_wgpu::text::msdf_pipeline_layout"),
338 bind_group_layouts: &[
339 Some(frame_bind_layout),
340 Some(&inner.msdf_page_bind_layout),
341 ],
342 immediate_size: 0,
343 }),
344 )
345 };
346
347 let color_pipeline =
348 build_color_pipeline(device, &color_pipeline_layout, target_format, sample_count);
349 let msdf_pipeline =
350 build_msdf_pipeline(device, &msdf_pipeline_layout, target_format, sample_count);
351
352 let color_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
353 label: Some("damascene_wgpu::text::color_instance_buf"),
354 size: (INITIAL_INSTANCE_CAPACITY * std::mem::size_of::<ColorGlyphInstance>()) as u64,
355 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
356 mapped_at_creation: false,
357 });
358 let msdf_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
359 label: Some("damascene_wgpu::text::msdf_instance_buf"),
360 size: (INITIAL_INSTANCE_CAPACITY * std::mem::size_of::<MsdfGlyphInstance>()) as u64,
361 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
362 mapped_at_creation: false,
363 });
364
365 let highlight_pipeline_layout =
368 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
369 label: Some("damascene_wgpu::text::highlight_pipeline_layout"),
370 bind_group_layouts: &[Some(frame_bind_layout)],
371 immediate_size: 0,
372 });
373 let highlight_pipeline = build_highlight_pipeline(
374 device,
375 &highlight_pipeline_layout,
376 target_format,
377 sample_count,
378 );
379 let highlight_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
380 label: Some("damascene_wgpu::text::highlight_instance_buf"),
381 size: (INITIAL_INSTANCE_CAPACITY * std::mem::size_of::<HighlightInstance>()) as u64,
382 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
383 mapped_at_creation: false,
384 });
385
386 Self {
387 shared,
388 color_page_bgs: Vec::new(),
389 msdf_page_bgs: Vec::new(),
390 color_instances: Vec::with_capacity(INITIAL_INSTANCE_CAPACITY),
391 color_instance_buf,
392 color_instance_capacity: INITIAL_INSTANCE_CAPACITY,
393 color_pipeline,
394 msdf_instances: Vec::with_capacity(INITIAL_INSTANCE_CAPACITY),
395 msdf_instance_buf,
396 msdf_instance_capacity: INITIAL_INSTANCE_CAPACITY,
397 msdf_pipeline,
398 highlight_instances: Vec::with_capacity(INITIAL_INSTANCE_CAPACITY),
399 highlight_instance_buf,
400 highlight_instance_capacity: INITIAL_INSTANCE_CAPACITY,
401 highlight_pipeline,
402 color_pipeline_layout,
403 msdf_pipeline_layout,
404 highlight_pipeline_layout,
405 sample_count,
406 runs: Vec::new(),
407 working_color_space: DEFAULT_WORKING_COLOR_SPACE,
408 }
409 }
410
411 pub(crate) fn shared(&self) -> &SharedText {
414 &self.shared
415 }
416
417 pub(crate) fn set_working_color_space(&mut self, space: ColorSpace) {
420 self.working_color_space = space;
421 }
422
423 pub(crate) fn set_target_format(
431 &mut self,
432 device: &wgpu::Device,
433 target_format: wgpu::TextureFormat,
434 ) {
435 self.color_pipeline = build_color_pipeline(
436 device,
437 &self.color_pipeline_layout,
438 target_format,
439 self.sample_count,
440 );
441 self.msdf_pipeline = build_msdf_pipeline(
442 device,
443 &self.msdf_pipeline_layout,
444 target_format,
445 self.sample_count,
446 );
447 self.highlight_pipeline = build_highlight_pipeline(
448 device,
449 &self.highlight_pipeline_layout,
450 target_format,
451 self.sample_count,
452 );
453 }
454
455 pub(crate) fn frame_begin(&mut self) {
456 self.color_instances.clear();
457 self.msdf_instances.clear();
458 self.highlight_instances.clear();
459 self.runs.clear();
460 }
461
462 #[allow(clippy::too_many_arguments)]
463 fn record_inner(
464 &mut self,
465 rect: Rect,
466 scissor: Option<PhysicalScissor>,
467 runs: &[(String, RunStyle)],
468 size: f32,
469 line_height: f32,
470 wrap: TextWrap,
471 anchor: TextAnchor,
472 scale_factor: f32,
473 ) -> std::ops::Range<usize> {
474 let avail = wrap_available_width(rect.w, scale_factor, wrap, anchor);
479 let runs_ref: Vec<(&str, RunStyle)> = runs
480 .iter()
481 .map(|(text, style)| (text.as_str(), style.clone()))
482 .collect();
483 let shared = self.shared.clone();
489 let mut inner = shared.lock();
490 let shaped = {
491 damascene_core::profile_span!("paint::text::shape_runs");
492 inner.atlas.shape_runs_with_line_height(
493 &runs_ref,
494 size,
495 line_height,
496 wrap,
497 anchor,
498 avail,
499 )
500 };
501 damascene_core::profile_span!("paint::text::emit_shaped");
502 self.emit_shaped_glyphs(&mut inner, rect, scissor, &shaped, wrap, scale_factor)
503 }
504
505 fn emit_shaped_glyphs(
506 &mut self,
507 inner: &mut SharedTextInner,
508 rect: Rect,
509 scissor: Option<PhysicalScissor>,
510 shaped: &ShapedRun,
511 wrap: TextWrap,
512 scale_factor: f32,
513 ) -> std::ops::Range<usize> {
514 let runs_start = self.runs.len();
515 if shaped.glyphs.is_empty() && shaped.highlights.is_empty() && shaped.decorations.is_empty()
516 {
517 return runs_start..runs_start;
518 }
519
520 let v_offset = match wrap {
529 TextWrap::NoWrap => ((rect.h - shaped.layout.height).max(0.0)) * 0.5,
530 TextWrap::Wrap => 0.0,
531 };
532 let origin_x = rect.x;
533 let origin_y = rect.y + v_offset;
534
535 if !shaped.highlights.is_empty() {
541 let first = self.highlight_instances.len() as u32;
542 for h in &shaped.highlights {
543 self.highlight_instances.push(HighlightInstance {
544 rect: [origin_x + h.x, origin_y + h.y, h.w, h.h],
545 color: rgba_f32_in(h.color, self.working_color_space),
546 });
547 }
548 let count = self.highlight_instances.len() as u32 - first;
549 if count > 0 {
550 self.runs.push(TextRun {
551 kind: TextRunKind::Highlight,
552 page: 0,
553 scissor,
554 first,
555 count,
556 });
557 }
558 }
559
560 let mut current: Option<(TextRunKind, u32, u32)> = None; for glyph in &shaped.glyphs {
566 let font_id = glyph.key.font;
567 let is_color = inner.atlas.is_color_font(font_id);
568 if is_color {
569 inner.atlas.ensure_color_glyph(glyph.key);
570 let Some(slot) = inner.atlas.slot(glyph.key) else {
571 continue;
572 };
573 if slot.rect.w == 0 || slot.rect.h == 0 {
574 continue;
575 }
576 let page = slot.page;
577 let next_kind = TextRunKind::Color;
578 self.maybe_close_run(&mut current, next_kind, page, scissor);
579 self.push_color_glyph(inner, glyph, slot, origin_x, origin_y, scale_factor);
580 } else {
581 let mkey = MsdfGlyphKey {
582 font: font_id,
583 glyph_id: glyph.key.glyph_id,
584 };
585 let Some(slot) = ensure_msdf(inner, mkey, font_id, glyph.key.weight) else {
586 continue;
589 };
590 let page = slot.page;
591 let next_kind = TextRunKind::Msdf;
592 self.maybe_close_run(&mut current, next_kind, page, scissor);
593 self.push_msdf_glyph(inner, glyph, slot, origin_x, origin_y);
594 }
595 }
596
597 if let Some((kind, page, first)) = current {
599 let count = self.instance_count_after(kind, first);
600 if count > 0 {
601 self.runs.push(TextRun {
602 kind,
603 page,
604 scissor,
605 first,
606 count,
607 });
608 }
609 }
610
611 if !shaped.decorations.is_empty() {
616 let first = self.highlight_instances.len() as u32;
617 for d in &shaped.decorations {
618 self.highlight_instances.push(HighlightInstance {
619 rect: [origin_x + d.x, origin_y + d.y, d.w, d.h],
620 color: rgba_f32_in(d.color, self.working_color_space),
621 });
622 }
623 let count = self.highlight_instances.len() as u32 - first;
624 if count > 0 {
625 self.runs.push(TextRun {
626 kind: TextRunKind::Highlight,
627 page: 0,
628 scissor,
629 first,
630 count,
631 });
632 }
633 }
634
635 runs_start..self.runs.len()
636 }
637
638 fn maybe_close_run(
639 &mut self,
640 current: &mut Option<(TextRunKind, u32, u32)>,
641 next_kind: TextRunKind,
642 next_page: u32,
643 scissor: Option<PhysicalScissor>,
644 ) {
645 let new_start = match next_kind {
646 TextRunKind::Color => self.color_instances.len() as u32,
647 TextRunKind::Msdf => self.msdf_instances.len() as u32,
648 TextRunKind::Highlight => self.highlight_instances.len() as u32,
649 };
650 let needs_close = match current {
651 Some((kind, page, _)) => !same_kind(*kind, next_kind) || *page != next_page,
652 None => false,
653 };
654 if needs_close {
655 let (kind, page, first) = current.take().unwrap();
656 let count = self.instance_count_after(kind, first);
657 if count > 0 {
658 self.runs.push(TextRun {
659 kind,
660 page,
661 scissor,
662 first,
663 count,
664 });
665 }
666 }
667 if current.is_none() {
668 *current = Some((next_kind, next_page, new_start));
669 }
670 }
671
672 fn instance_count_after(&self, kind: TextRunKind, first: u32) -> u32 {
673 let len = match kind {
674 TextRunKind::Color => self.color_instances.len() as u32,
675 TextRunKind::Msdf => self.msdf_instances.len() as u32,
676 TextRunKind::Highlight => self.highlight_instances.len() as u32,
677 };
678 len.saturating_sub(first)
679 }
680
681 fn push_color_glyph(
682 &mut self,
683 inner: &SharedTextInner,
684 glyph: &ShapedGlyph,
685 slot: damascene_core::text::atlas::GlyphSlot,
686 origin_x: f32,
687 origin_y: f32,
688 scale_factor: f32,
689 ) {
690 let ratio = if slot.raster_size > 0.0 {
702 glyph.key.size() / slot.raster_size
703 } else {
704 1.0
705 };
706 let bx = origin_x + glyph.x + slot.offset.0 as f32 * ratio / scale_factor;
707 let by = origin_y + glyph.y - slot.offset.1 as f32 * ratio / scale_factor;
708 let bw = slot.rect.w as f32 * ratio / scale_factor;
709 let bh = slot.rect.h as f32 * ratio / scale_factor;
710 let atlas_page = inner
711 .atlas
712 .page(slot.page)
713 .expect("shaped glyph references missing colour atlas page");
714 let page_w = atlas_page.width as f32;
715 let page_h = atlas_page.height as f32;
716 let uv = [
717 slot.rect.x as f32 / page_w,
718 slot.rect.y as f32 / page_h,
719 slot.rect.w as f32 / page_w,
720 slot.rect.h as f32 / page_h,
721 ];
722 let inst_color = if slot.is_color {
723 [1.0, 1.0, 1.0, 1.0]
724 } else {
725 rgba_f32_in(glyph.color, self.working_color_space)
726 };
727 self.color_instances.push(ColorGlyphInstance {
728 rect: [bx, by, bw, bh],
729 uv,
730 color: inst_color,
731 });
732 }
733
734 fn push_msdf_glyph(
735 &mut self,
736 inner: &SharedTextInner,
737 glyph: &ShapedGlyph,
738 slot: MsdfSlot,
739 origin_x: f32,
740 origin_y: f32,
741 ) {
742 let logical_em = glyph.key.size();
745 let base_em = inner.msdf_atlas.base_em() as f32;
746 let scale = logical_em / base_em;
747 let bx = origin_x + glyph.x + slot.bearing_x * scale;
748 let by = origin_y + glyph.y + slot.bearing_y * scale;
749 let bw = slot.rect.w as f32 * scale;
750 let bh = slot.rect.h as f32 * scale;
751 let atlas_page = inner
752 .msdf_atlas
753 .page(slot.page)
754 .expect("shaped glyph references missing MSDF atlas page");
755 let page_w = atlas_page.width as f32;
756 let page_h = atlas_page.height as f32;
757 let uv = [
758 slot.rect.x as f32 / page_w,
759 slot.rect.y as f32 / page_h,
760 slot.rect.w as f32 / page_w,
761 slot.rect.h as f32 / page_h,
762 ];
763 let color = rgba_f32_in(glyph.color, self.working_color_space);
764 self.msdf_instances.push(MsdfGlyphInstance {
765 rect: [bx, by, bw, bh],
766 uv,
767 color,
768 params: [slot.spread, 0.0, 0.0, 0.0],
769 });
770 }
771
772 pub fn warm_default_glyphs(&mut self) {
785 self.shared.clone().lock().warm_default_glyphs();
786 }
787
788 pub(crate) fn flush(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
791 {
792 let shared = self.shared.clone();
793 let mut inner = shared.lock();
794 inner.flush_pages(device, queue);
795 self.color_page_bgs = inner
800 .color_pages
801 .iter()
802 .map(|p| p.bind_group.clone())
803 .collect();
804 self.msdf_page_bgs = inner
805 .msdf_pages
806 .iter()
807 .map(|p| p.bind_group.clone())
808 .collect();
809 }
810
811 if self.color_instances.len() > self.color_instance_capacity {
813 let new_cap = self.color_instances.len().next_power_of_two();
814 self.color_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
815 label: Some("damascene_wgpu::text::color_instance_buf (resized)"),
816 size: (new_cap * std::mem::size_of::<ColorGlyphInstance>()) as u64,
817 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
818 mapped_at_creation: false,
819 });
820 self.color_instance_capacity = new_cap;
821 }
822 if !self.color_instances.is_empty() {
823 queue.write_buffer(
824 &self.color_instance_buf,
825 0,
826 bytemuck::cast_slice(&self.color_instances),
827 );
828 }
829
830 if self.msdf_instances.len() > self.msdf_instance_capacity {
832 let new_cap = self.msdf_instances.len().next_power_of_two();
833 self.msdf_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
834 label: Some("damascene_wgpu::text::msdf_instance_buf (resized)"),
835 size: (new_cap * std::mem::size_of::<MsdfGlyphInstance>()) as u64,
836 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
837 mapped_at_creation: false,
838 });
839 self.msdf_instance_capacity = new_cap;
840 }
841 if !self.msdf_instances.is_empty() {
842 queue.write_buffer(
843 &self.msdf_instance_buf,
844 0,
845 bytemuck::cast_slice(&self.msdf_instances),
846 );
847 }
848
849 if self.highlight_instances.len() > self.highlight_instance_capacity {
851 let new_cap = self.highlight_instances.len().next_power_of_two();
852 self.highlight_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
853 label: Some("damascene_wgpu::text::highlight_instance_buf (resized)"),
854 size: (new_cap * std::mem::size_of::<HighlightInstance>()) as u64,
855 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
856 mapped_at_creation: false,
857 });
858 self.highlight_instance_capacity = new_cap;
859 }
860 if !self.highlight_instances.is_empty() {
861 queue.write_buffer(
862 &self.highlight_instance_buf,
863 0,
864 bytemuck::cast_slice(&self.highlight_instances),
865 );
866 }
867 }
868
869 pub(crate) fn run(&self, index: usize) -> TextRun {
870 self.runs[index]
871 }
872
873 pub(crate) fn pipeline_for(&self, kind: TextRunKind) -> &wgpu::RenderPipeline {
874 match kind {
875 TextRunKind::Color => &self.color_pipeline,
876 TextRunKind::Msdf => &self.msdf_pipeline,
877 TextRunKind::Highlight => &self.highlight_pipeline,
878 }
879 }
880
881 pub(crate) fn instance_buf_for(&self, kind: TextRunKind) -> &wgpu::Buffer {
882 match kind {
883 TextRunKind::Color => &self.color_instance_buf,
884 TextRunKind::Msdf => &self.msdf_instance_buf,
885 TextRunKind::Highlight => &self.highlight_instance_buf,
886 }
887 }
888
889 pub(crate) fn page_bind_group(&self, kind: TextRunKind, page: u32) -> &wgpu::BindGroup {
894 match kind {
895 TextRunKind::Color => &self.color_page_bgs[page as usize],
896 TextRunKind::Msdf => &self.msdf_page_bgs[page as usize],
897 TextRunKind::Highlight => unreachable!("highlight runs carry no page binding"),
898 }
899 }
900}
901
902impl SharedTextInner {
903 fn flush_pages(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
909 let color_dirty = self.atlas.take_dirty();
911 while self.color_pages.len() < self.atlas.pages().len() {
912 let i = self.color_pages.len();
913 let page = &self.atlas.pages()[i];
914 self.color_pages.push(create_color_page(
915 device,
916 &self.color_page_bind_layout,
917 &self.color_sampler,
918 page.width,
919 page.height,
920 ));
921 }
922 for (page_idx, rect) in color_dirty {
923 let page = &self.atlas.pages()[page_idx];
924 upload_color_region(queue, &self.color_pages[page_idx].texture, page, rect);
925 }
926
927 let msdf_dirty = self.msdf_atlas.take_dirty();
929 while self.msdf_pages.len() < self.msdf_atlas.pages().len() {
930 let i = self.msdf_pages.len();
931 let page = &self.msdf_atlas.pages()[i];
932 self.msdf_pages.push(create_msdf_page(
933 device,
934 &self.msdf_page_bind_layout,
935 &self.msdf_sampler,
936 page.width,
937 page.height,
938 ));
939 }
940 for (page_idx, rect) in msdf_dirty {
941 let page = &self.msdf_atlas.pages()[page_idx];
942 upload_msdf_region(queue, &self.msdf_pages[page_idx].texture, page, rect);
943 }
944 }
945
946 pub(crate) fn warm_default_glyphs(&mut self) {
948 const FAMILIES: &[FontFamily] = &[FontFamily::Inter, FontFamily::JetBrainsMono];
949 let chars: Vec<char> = (0x20u32..=0x7Eu32).filter_map(char::from_u32).collect();
950 self.warm_msdf_for_chars(&chars, FAMILIES);
951 }
952
953 pub(crate) fn warm_msdf_for_chars(&mut self, chars: &[char], families: &[FontFamily]) {
960 for family in families {
961 let name = family.family_name();
962 let font_id = self.atlas.font_system().db().query(&fontdb::Query {
963 families: &[fontdb::Family::Name(name)],
964 weight: fontdb::Weight::NORMAL,
965 ..fontdb::Query::default()
966 });
967 let Some(font_id) = font_id else { continue };
968 let face_index = self
969 .atlas
970 .font_system()
971 .db()
972 .face(font_id)
973 .map(|f| f.index)
974 .unwrap_or(0);
975 let Some(font) = self
976 .atlas
977 .font_system_mut()
978 .get_font(font_id, fontdb::Weight::NORMAL)
979 else {
980 continue;
981 };
982 let Ok(face) = Face::parse(font.data(), face_index) else {
983 continue;
984 };
985 for &ch in chars {
986 if let Some(glyph_id) = face.glyph_index(ch) {
987 let key = MsdfGlyphKey {
988 font: font_id,
989 glyph_id: glyph_id.0,
990 };
991 let _ = self.msdf_atlas.ensure(key, &face);
992 }
993 }
994 }
995 }
996}
997
998fn ensure_msdf(
1000 inner: &mut SharedTextInner,
1001 key: MsdfGlyphKey,
1002 font_id: fontdb::ID,
1003 weight: fontdb::Weight,
1004) -> Option<MsdfSlot> {
1005 if let Some(slot) = inner.msdf_atlas.touch(key) {
1008 return Some(slot);
1009 }
1010 let font = inner.atlas.font_system_mut().get_font(font_id, weight)?;
1016 let face_index = inner.atlas.font_system().db().face(font_id)?.index;
1017 let face = Face::parse(font.data(), face_index).ok()?;
1018 inner.msdf_atlas.ensure(key, &face)
1019}
1020
1021fn same_kind(a: TextRunKind, b: TextRunKind) -> bool {
1022 a == b
1023}
1024
1025fn build_color_pipeline(
1029 device: &wgpu::Device,
1030 layout: &wgpu::PipelineLayout,
1031 target_format: wgpu::TextureFormat,
1032 sample_count: u32,
1033) -> wgpu::RenderPipeline {
1034 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1035 label: Some("stock::text"),
1036 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(stock_wgsl::TEXT)),
1037 });
1038 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1039 label: Some("damascene_wgpu::text::color_pipeline"),
1040 layout: Some(layout),
1041 vertex: wgpu::VertexState {
1042 module: &shader,
1043 entry_point: Some("vs_main"),
1044 compilation_options: Default::default(),
1045 buffers: &[
1046 wgpu::VertexBufferLayout {
1047 array_stride: (2 * std::mem::size_of::<f32>()) as u64,
1048 step_mode: wgpu::VertexStepMode::Vertex,
1049 attributes: &[wgpu::VertexAttribute {
1050 shader_location: 0,
1051 format: wgpu::VertexFormat::Float32x2,
1052 offset: 0,
1053 }],
1054 },
1055 wgpu::VertexBufferLayout {
1056 array_stride: std::mem::size_of::<ColorGlyphInstance>() as u64,
1057 step_mode: wgpu::VertexStepMode::Instance,
1058 attributes: &COLOR_INSTANCE_ATTRS,
1059 },
1060 ],
1061 },
1062 fragment: Some(wgpu::FragmentState {
1063 module: &shader,
1064 entry_point: Some("fs_main"),
1065 compilation_options: Default::default(),
1066 targets: &[Some(wgpu::ColorTargetState {
1067 format: target_format,
1068 blend: Some(premultiplied_blend()),
1069 write_mask: wgpu::ColorWrites::ALL,
1070 })],
1071 }),
1072 primitive: triangle_strip(),
1073 depth_stencil: None,
1074 multisample: wgpu::MultisampleState {
1075 count: sample_count,
1076 mask: !0,
1077 alpha_to_coverage_enabled: false,
1078 },
1079 multiview_mask: None,
1080 cache: None,
1081 })
1082}
1083
1084fn build_msdf_pipeline(
1087 device: &wgpu::Device,
1088 layout: &wgpu::PipelineLayout,
1089 target_format: wgpu::TextureFormat,
1090 sample_count: u32,
1091) -> wgpu::RenderPipeline {
1092 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1093 label: Some("stock::text_msdf"),
1094 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(stock_wgsl::TEXT_MSDF)),
1095 });
1096 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1097 label: Some("damascene_wgpu::text::msdf_pipeline"),
1098 layout: Some(layout),
1099 vertex: wgpu::VertexState {
1100 module: &shader,
1101 entry_point: Some("vs_main"),
1102 compilation_options: Default::default(),
1103 buffers: &[
1104 wgpu::VertexBufferLayout {
1105 array_stride: (2 * std::mem::size_of::<f32>()) as u64,
1106 step_mode: wgpu::VertexStepMode::Vertex,
1107 attributes: &[wgpu::VertexAttribute {
1108 shader_location: 0,
1109 format: wgpu::VertexFormat::Float32x2,
1110 offset: 0,
1111 }],
1112 },
1113 wgpu::VertexBufferLayout {
1114 array_stride: std::mem::size_of::<MsdfGlyphInstance>() as u64,
1115 step_mode: wgpu::VertexStepMode::Instance,
1116 attributes: &MSDF_INSTANCE_ATTRS,
1117 },
1118 ],
1119 },
1120 fragment: Some(wgpu::FragmentState {
1121 module: &shader,
1122 entry_point: Some("fs_main"),
1123 compilation_options: Default::default(),
1124 targets: &[Some(wgpu::ColorTargetState {
1125 format: target_format,
1126 blend: Some(premultiplied_blend()),
1127 write_mask: wgpu::ColorWrites::ALL,
1128 })],
1129 }),
1130 primitive: triangle_strip(),
1131 depth_stencil: None,
1132 multisample: wgpu::MultisampleState {
1133 count: sample_count,
1134 mask: !0,
1135 alpha_to_coverage_enabled: false,
1136 },
1137 multiview_mask: None,
1138 cache: None,
1139 })
1140}
1141
1142fn build_highlight_pipeline(
1145 device: &wgpu::Device,
1146 layout: &wgpu::PipelineLayout,
1147 target_format: wgpu::TextureFormat,
1148 sample_count: u32,
1149) -> wgpu::RenderPipeline {
1150 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1151 label: Some("stock::text_highlight"),
1152 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(stock_wgsl::TEXT_HIGHLIGHT)),
1153 });
1154 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1155 label: Some("damascene_wgpu::text::highlight_pipeline"),
1156 layout: Some(layout),
1157 vertex: wgpu::VertexState {
1158 module: &shader,
1159 entry_point: Some("vs_main"),
1160 compilation_options: Default::default(),
1161 buffers: &[
1162 wgpu::VertexBufferLayout {
1163 array_stride: (2 * std::mem::size_of::<f32>()) as u64,
1164 step_mode: wgpu::VertexStepMode::Vertex,
1165 attributes: &[wgpu::VertexAttribute {
1166 shader_location: 0,
1167 format: wgpu::VertexFormat::Float32x2,
1168 offset: 0,
1169 }],
1170 },
1171 wgpu::VertexBufferLayout {
1172 array_stride: std::mem::size_of::<HighlightInstance>() as u64,
1173 step_mode: wgpu::VertexStepMode::Instance,
1174 attributes: &HIGHLIGHT_INSTANCE_ATTRS,
1175 },
1176 ],
1177 },
1178 fragment: Some(wgpu::FragmentState {
1179 module: &shader,
1180 entry_point: Some("fs_main"),
1181 compilation_options: Default::default(),
1182 targets: &[Some(wgpu::ColorTargetState {
1183 format: target_format,
1184 blend: Some(premultiplied_blend()),
1185 write_mask: wgpu::ColorWrites::ALL,
1186 })],
1187 }),
1188 primitive: triangle_strip(),
1189 depth_stencil: None,
1190 multisample: wgpu::MultisampleState {
1191 count: sample_count,
1192 mask: !0,
1193 alpha_to_coverage_enabled: false,
1194 },
1195 multiview_mask: None,
1196 cache: None,
1197 })
1198}
1199
1200fn premultiplied_blend() -> wgpu::BlendState {
1201 wgpu::BlendState {
1202 color: wgpu::BlendComponent {
1203 src_factor: wgpu::BlendFactor::One,
1204 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1205 operation: wgpu::BlendOperation::Add,
1206 },
1207 alpha: wgpu::BlendComponent {
1208 src_factor: wgpu::BlendFactor::One,
1209 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1210 operation: wgpu::BlendOperation::Add,
1211 },
1212 }
1213}
1214
1215fn triangle_strip() -> wgpu::PrimitiveState {
1216 wgpu::PrimitiveState {
1217 topology: wgpu::PrimitiveTopology::TriangleStrip,
1218 strip_index_format: None,
1219 front_face: wgpu::FrontFace::Ccw,
1220 cull_mode: None,
1221 polygon_mode: wgpu::PolygonMode::Fill,
1222 unclipped_depth: false,
1223 conservative: false,
1224 }
1225}
1226
1227fn create_color_page(
1228 device: &wgpu::Device,
1229 layout: &wgpu::BindGroupLayout,
1230 sampler: &wgpu::Sampler,
1231 width: u32,
1232 height: u32,
1233) -> PageTexture {
1234 let texture = device.create_texture(&wgpu::TextureDescriptor {
1235 label: Some("damascene_wgpu::text::color_page"),
1236 size: wgpu::Extent3d {
1237 width,
1238 height,
1239 depth_or_array_layers: 1,
1240 },
1241 mip_level_count: 1,
1242 sample_count: 1,
1243 dimension: wgpu::TextureDimension::D2,
1244 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1245 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1246 view_formats: &[],
1247 });
1248 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1249 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1250 label: Some("damascene_wgpu::text::color_page_bg"),
1251 layout,
1252 entries: &[
1253 wgpu::BindGroupEntry {
1254 binding: 0,
1255 resource: wgpu::BindingResource::TextureView(&view),
1256 },
1257 wgpu::BindGroupEntry {
1258 binding: 1,
1259 resource: wgpu::BindingResource::Sampler(sampler),
1260 },
1261 ],
1262 });
1263 PageTexture {
1264 texture,
1265 bind_group,
1266 }
1267}
1268
1269fn create_msdf_page(
1270 device: &wgpu::Device,
1271 layout: &wgpu::BindGroupLayout,
1272 sampler: &wgpu::Sampler,
1273 width: u32,
1274 height: u32,
1275) -> PageTexture {
1276 let texture = device.create_texture(&wgpu::TextureDescriptor {
1277 label: Some("damascene_wgpu::text::msdf_page"),
1278 size: wgpu::Extent3d {
1279 width,
1280 height,
1281 depth_or_array_layers: 1,
1282 },
1283 mip_level_count: 1,
1284 sample_count: 1,
1285 dimension: wgpu::TextureDimension::D2,
1286 format: wgpu::TextureFormat::Rgba8Unorm,
1289 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1290 view_formats: &[],
1291 });
1292 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1293 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1294 label: Some("damascene_wgpu::text::msdf_page_bg"),
1295 layout,
1296 entries: &[
1297 wgpu::BindGroupEntry {
1298 binding: 0,
1299 resource: wgpu::BindingResource::TextureView(&view),
1300 },
1301 wgpu::BindGroupEntry {
1302 binding: 1,
1303 resource: wgpu::BindingResource::Sampler(sampler),
1304 },
1305 ],
1306 });
1307 PageTexture {
1308 texture,
1309 bind_group,
1310 }
1311}
1312
1313impl TextRecorder for TextPaint {
1314 fn record(
1315 &mut self,
1316 rect: Rect,
1317 scissor: Option<PhysicalScissor>,
1318 style: &RunStyle,
1319 text: &str,
1320 size: f32,
1321 line_height: f32,
1322 wrap: TextWrap,
1323 anchor: TextAnchor,
1324 scale_factor: f32,
1325 ) -> std::ops::Range<usize> {
1326 self.record_inner(
1327 rect,
1328 scissor,
1329 &[(text.to_string(), style.clone())],
1330 size,
1331 line_height,
1332 wrap,
1333 anchor,
1334 scale_factor,
1335 )
1336 }
1337
1338 fn record_runs(
1339 &mut self,
1340 rect: Rect,
1341 scissor: Option<PhysicalScissor>,
1342 runs: &[(String, RunStyle)],
1343 size: f32,
1344 line_height: f32,
1345 wrap: TextWrap,
1346 anchor: TextAnchor,
1347 scale_factor: f32,
1348 ) -> std::ops::Range<usize> {
1349 self.record_inner(
1350 rect,
1351 scissor,
1352 runs,
1353 size,
1354 line_height,
1355 wrap,
1356 anchor,
1357 scale_factor,
1358 )
1359 }
1360}
1361
1362fn wrap_available_width(
1363 rect_w: f32,
1364 _scale_factor: f32,
1365 wrap: TextWrap,
1366 anchor: TextAnchor,
1367) -> Option<f32> {
1368 match (wrap, anchor) {
1371 (TextWrap::Wrap, _) => Some(rect_w),
1372 (TextWrap::NoWrap, TextAnchor::Start) => None,
1373 (TextWrap::NoWrap, TextAnchor::Middle | TextAnchor::End) => Some(rect_w),
1374 }
1375}
1376
1377fn upload_color_region(
1378 queue: &wgpu::Queue,
1379 texture: &wgpu::Texture,
1380 page: &AtlasPage,
1381 rect: AtlasRect,
1382) {
1383 if rect.w == 0 || rect.h == 0 {
1384 return;
1385 }
1386 let bpp = ATLAS_BYTES_PER_PIXEL as usize;
1387 let row_bytes = rect.w as usize * bpp;
1388 let mut bytes = Vec::with_capacity(row_bytes * rect.h as usize);
1389 for row in 0..rect.h {
1390 let y = rect.y + row;
1391 let start = (y as usize * page.width as usize + rect.x as usize) * bpp;
1392 let end = start + row_bytes;
1393 bytes.extend_from_slice(&page.pixels[start..end]);
1394 }
1395 queue.write_texture(
1396 wgpu::TexelCopyTextureInfo {
1397 texture,
1398 mip_level: 0,
1399 origin: wgpu::Origin3d {
1400 x: rect.x,
1401 y: rect.y,
1402 z: 0,
1403 },
1404 aspect: wgpu::TextureAspect::All,
1405 },
1406 &bytes,
1407 wgpu::TexelCopyBufferLayout {
1408 offset: 0,
1409 bytes_per_row: Some(rect.w * ATLAS_BYTES_PER_PIXEL),
1410 rows_per_image: Some(rect.h),
1411 },
1412 wgpu::Extent3d {
1413 width: rect.w,
1414 height: rect.h,
1415 depth_or_array_layers: 1,
1416 },
1417 );
1418}
1419
1420fn upload_msdf_region(
1421 queue: &wgpu::Queue,
1422 texture: &wgpu::Texture,
1423 page: &MsdfAtlasPage,
1424 rect: MsdfRect,
1425) {
1426 if rect.w == 0 || rect.h == 0 {
1427 return;
1428 }
1429 let bpp = MSDF_BYTES_PER_PIXEL as usize;
1430 let row_bytes = rect.w as usize * bpp;
1431 let mut bytes = Vec::with_capacity(row_bytes * rect.h as usize);
1432 for row in 0..rect.h {
1433 let y = rect.y + row;
1434 let start = (y as usize * page.width as usize + rect.x as usize) * bpp;
1435 let end = start + row_bytes;
1436 bytes.extend_from_slice(&page.pixels[start..end]);
1437 }
1438 queue.write_texture(
1439 wgpu::TexelCopyTextureInfo {
1440 texture,
1441 mip_level: 0,
1442 origin: wgpu::Origin3d {
1443 x: rect.x,
1444 y: rect.y,
1445 z: 0,
1446 },
1447 aspect: wgpu::TextureAspect::All,
1448 },
1449 &bytes,
1450 wgpu::TexelCopyBufferLayout {
1451 offset: 0,
1452 bytes_per_row: Some(rect.w * MSDF_BYTES_PER_PIXEL),
1453 rows_per_image: Some(rect.h),
1454 },
1455 wgpu::Extent3d {
1456 width: rect.w,
1457 height: rect.h,
1458 depth_or_array_layers: 1,
1459 },
1460 );
1461}