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::text::msdf_snapshot::{SnapshotError, font_token_hash};
34use damascene_core::tree::{FontFamily, Rect, TextWrap};
35
36use bytemuck::{Pod, Zeroable};
37use cosmic_text::fontdb;
38use ttf_parser::Face;
39
40use damascene_core::color::ColorSpace;
41use damascene_core::paint::{DEFAULT_WORKING_COLOR_SPACE, PhysicalScissor, rgba_f32_in};
42use damascene_core::runtime::TextRecorder;
43
44const INITIAL_INSTANCE_CAPACITY: usize = 256;
45
46const COLOR_INSTANCE_ATTRS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
47 1 => Float32x4, 2 => Float32x4, 3 => Float32x4, ];
51
52const MSDF_INSTANCE_ATTRS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
53 1 => Float32x4, 2 => Float32x4, 3 => Float32x4, 4 => Float32x4, ];
58
59const HIGHLIGHT_INSTANCE_ATTRS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
60 1 => Float32x4, 2 => Float32x4, ];
63
64#[repr(C)]
65#[derive(Copy, Clone, Pod, Zeroable, Debug)]
66pub(crate) struct ColorGlyphInstance {
67 pub rect: [f32; 4],
68 pub uv: [f32; 4],
69 pub color: [f32; 4],
70}
71
72#[repr(C)]
73#[derive(Copy, Clone, Pod, Zeroable, Debug)]
74pub(crate) struct MsdfGlyphInstance {
75 pub rect: [f32; 4],
76 pub uv: [f32; 4],
77 pub color: [f32; 4],
78 pub params: [f32; 4],
79}
80
81#[repr(C)]
82#[derive(Copy, Clone, Pod, Zeroable, Debug)]
83pub(crate) struct HighlightInstance {
84 pub rect: [f32; 4],
85 pub color: [f32; 4],
86}
87
88#[derive(Clone, Copy, PartialEq, Eq)]
89pub(crate) enum TextRunKind {
90 Color,
91 Msdf,
92 Highlight,
93}
94
95#[derive(Clone, Copy)]
96pub(crate) struct TextRun {
97 pub kind: TextRunKind,
98 pub page: u32,
99 pub scissor: Option<PhysicalScissor>,
100 pub first: u32,
101 pub count: u32,
102}
103
104struct PageTexture {
105 texture: wgpu::Texture,
106 bind_group: wgpu::BindGroup,
107}
108
109#[derive(Clone)]
130pub struct SharedText(pub(crate) std::sync::Arc<std::sync::Mutex<SharedTextInner>>);
131
132pub(crate) struct SharedTextInner {
133 pub(crate) atlas: GlyphAtlas,
134 pub(crate) msdf_atlas: MsdfAtlas,
135
136 color_pages: Vec<PageTexture>,
137 color_page_bind_layout: wgpu::BindGroupLayout,
138 color_sampler: wgpu::Sampler,
139
140 msdf_pages: Vec<PageTexture>,
141 msdf_page_bind_layout: wgpu::BindGroupLayout,
142 msdf_sampler: wgpu::Sampler,
143
144 attached: u32,
148}
149
150impl SharedText {
151 pub fn new(device: &wgpu::Device) -> Self {
156 let color_page_bind_layout = create_page_bind_layout(device, "color");
157 let msdf_page_bind_layout = create_page_bind_layout(device, "msdf");
158 let color_sampler = create_page_sampler(device, "color");
159 let msdf_sampler = create_page_sampler(device, "msdf");
160 Self(std::sync::Arc::new(std::sync::Mutex::new(
161 SharedTextInner {
162 atlas: GlyphAtlas::new(),
163 msdf_atlas: MsdfAtlas::new(DEFAULT_BASE_EM, DEFAULT_SPREAD),
164 color_pages: Vec::new(),
165 color_page_bind_layout,
166 color_sampler,
167 msdf_pages: Vec::new(),
168 msdf_page_bind_layout,
169 msdf_sampler,
170 attached: 0,
171 },
172 )))
173 }
174
175 pub fn warm_default_glyphs(&self) {
183 self.lock().warm_default_glyphs();
184 }
185
186 pub fn warm_glyphs(&self, families: &[FontFamily], chars: &[char]) {
193 self.lock().warm_msdf_for_chars(chars, families);
194 }
195
196 pub fn export_msdf_snapshot(&self) -> Vec<u8> {
207 self.lock().export_msdf_snapshot()
208 }
209
210 pub fn import_msdf_snapshot(&self, bytes: &[u8]) -> Result<usize, SnapshotError> {
218 self.lock().import_msdf_snapshot(bytes)
219 }
220
221 pub(crate) fn lock(&self) -> std::sync::MutexGuard<'_, SharedTextInner> {
222 match self.0.lock() {
226 Ok(g) => g,
227 Err(poisoned) => poisoned.into_inner(),
228 }
229 }
230}
231
232pub(crate) struct TextPaint {
233 shared: SharedText,
235
236 color_page_bgs: Vec<wgpu::BindGroup>,
242 msdf_page_bgs: Vec<wgpu::BindGroup>,
243
244 color_instances: Vec<ColorGlyphInstance>,
246 color_instance_buf: wgpu::Buffer,
247 color_instance_capacity: usize,
248 color_pipeline: wgpu::RenderPipeline,
249
250 msdf_instances: Vec<MsdfGlyphInstance>,
252 msdf_instance_buf: wgpu::Buffer,
253 msdf_instance_capacity: usize,
254 msdf_pipeline: wgpu::RenderPipeline,
255
256 highlight_instances: Vec<HighlightInstance>,
258 highlight_instance_buf: wgpu::Buffer,
259 highlight_instance_capacity: usize,
260 highlight_pipeline: wgpu::RenderPipeline,
261
262 color_pipeline_layout: wgpu::PipelineLayout,
268 msdf_pipeline_layout: wgpu::PipelineLayout,
269 highlight_pipeline_layout: wgpu::PipelineLayout,
270 sample_count: u32,
271
272 runs: Vec<TextRun>,
273
274 working_color_space: ColorSpace,
279}
280
281impl Drop for TextPaint {
282 fn drop(&mut self) {
283 let mut inner = self.shared.lock();
284 inner.attached = inner.attached.saturating_sub(1);
285 let n = inner.attached.max(1);
286 inner.atlas.set_lru_protection_window(n);
287 inner.msdf_atlas.set_lru_protection_window(n);
288 }
289}
290
291fn create_page_bind_layout(device: &wgpu::Device, kind: &str) -> wgpu::BindGroupLayout {
294 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
295 label: Some(&format!("damascene_wgpu::text::{kind}_page_bind_layout")),
296 entries: &[
297 wgpu::BindGroupLayoutEntry {
298 binding: 0,
299 visibility: wgpu::ShaderStages::FRAGMENT,
300 ty: wgpu::BindingType::Texture {
301 sample_type: wgpu::TextureSampleType::Float { filterable: true },
302 view_dimension: wgpu::TextureViewDimension::D2,
303 multisampled: false,
304 },
305 count: None,
306 },
307 wgpu::BindGroupLayoutEntry {
308 binding: 1,
309 visibility: wgpu::ShaderStages::FRAGMENT,
310 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
311 count: None,
312 },
313 ],
314 })
315}
316
317fn create_page_sampler(device: &wgpu::Device, kind: &str) -> wgpu::Sampler {
318 device.create_sampler(&wgpu::SamplerDescriptor {
319 label: Some(&format!("damascene_wgpu::text::{kind}_sampler")),
320 address_mode_u: wgpu::AddressMode::ClampToEdge,
321 address_mode_v: wgpu::AddressMode::ClampToEdge,
322 address_mode_w: wgpu::AddressMode::ClampToEdge,
323 mag_filter: wgpu::FilterMode::Linear,
324 min_filter: wgpu::FilterMode::Linear,
325 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
326 ..Default::default()
327 })
328}
329
330impl TextPaint {
331 pub(crate) fn new(
332 device: &wgpu::Device,
333 target_format: wgpu::TextureFormat,
334 sample_count: u32,
335 frame_bind_layout: &wgpu::BindGroupLayout,
336 ) -> Self {
337 Self::with_shared(
338 device,
339 target_format,
340 sample_count,
341 frame_bind_layout,
342 SharedText::new(device),
343 )
344 }
345
346 pub(crate) fn with_shared(
351 device: &wgpu::Device,
352 target_format: wgpu::TextureFormat,
353 sample_count: u32,
354 frame_bind_layout: &wgpu::BindGroupLayout,
355 shared: SharedText,
356 ) -> Self {
357 let (color_pipeline_layout, msdf_pipeline_layout) = {
358 let mut inner = shared.lock();
359 inner.attached += 1;
360 let n = inner.attached;
361 inner.atlas.set_lru_protection_window(n);
362 inner.msdf_atlas.set_lru_protection_window(n);
363 (
364 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
365 label: Some("damascene_wgpu::text::color_pipeline_layout"),
366 bind_group_layouts: &[
367 Some(frame_bind_layout),
368 Some(&inner.color_page_bind_layout),
369 ],
370 immediate_size: 0,
371 }),
372 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
373 label: Some("damascene_wgpu::text::msdf_pipeline_layout"),
374 bind_group_layouts: &[
375 Some(frame_bind_layout),
376 Some(&inner.msdf_page_bind_layout),
377 ],
378 immediate_size: 0,
379 }),
380 )
381 };
382
383 let color_pipeline =
384 build_color_pipeline(device, &color_pipeline_layout, target_format, sample_count);
385 let msdf_pipeline =
386 build_msdf_pipeline(device, &msdf_pipeline_layout, target_format, sample_count);
387
388 let color_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
389 label: Some("damascene_wgpu::text::color_instance_buf"),
390 size: (INITIAL_INSTANCE_CAPACITY * std::mem::size_of::<ColorGlyphInstance>()) as u64,
391 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
392 mapped_at_creation: false,
393 });
394 let msdf_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
395 label: Some("damascene_wgpu::text::msdf_instance_buf"),
396 size: (INITIAL_INSTANCE_CAPACITY * std::mem::size_of::<MsdfGlyphInstance>()) as u64,
397 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
398 mapped_at_creation: false,
399 });
400
401 let highlight_pipeline_layout =
404 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
405 label: Some("damascene_wgpu::text::highlight_pipeline_layout"),
406 bind_group_layouts: &[Some(frame_bind_layout)],
407 immediate_size: 0,
408 });
409 let highlight_pipeline = build_highlight_pipeline(
410 device,
411 &highlight_pipeline_layout,
412 target_format,
413 sample_count,
414 );
415 let highlight_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
416 label: Some("damascene_wgpu::text::highlight_instance_buf"),
417 size: (INITIAL_INSTANCE_CAPACITY * std::mem::size_of::<HighlightInstance>()) as u64,
418 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
419 mapped_at_creation: false,
420 });
421
422 Self {
423 shared,
424 color_page_bgs: Vec::new(),
425 msdf_page_bgs: Vec::new(),
426 color_instances: Vec::with_capacity(INITIAL_INSTANCE_CAPACITY),
427 color_instance_buf,
428 color_instance_capacity: INITIAL_INSTANCE_CAPACITY,
429 color_pipeline,
430 msdf_instances: Vec::with_capacity(INITIAL_INSTANCE_CAPACITY),
431 msdf_instance_buf,
432 msdf_instance_capacity: INITIAL_INSTANCE_CAPACITY,
433 msdf_pipeline,
434 highlight_instances: Vec::with_capacity(INITIAL_INSTANCE_CAPACITY),
435 highlight_instance_buf,
436 highlight_instance_capacity: INITIAL_INSTANCE_CAPACITY,
437 highlight_pipeline,
438 color_pipeline_layout,
439 msdf_pipeline_layout,
440 highlight_pipeline_layout,
441 sample_count,
442 runs: Vec::new(),
443 working_color_space: DEFAULT_WORKING_COLOR_SPACE,
444 }
445 }
446
447 pub(crate) fn shared(&self) -> &SharedText {
450 &self.shared
451 }
452
453 pub(crate) fn set_working_color_space(&mut self, space: ColorSpace) {
456 self.working_color_space = space;
457 }
458
459 pub(crate) fn set_target_format(
467 &mut self,
468 device: &wgpu::Device,
469 target_format: wgpu::TextureFormat,
470 ) {
471 self.color_pipeline = build_color_pipeline(
472 device,
473 &self.color_pipeline_layout,
474 target_format,
475 self.sample_count,
476 );
477 self.msdf_pipeline = build_msdf_pipeline(
478 device,
479 &self.msdf_pipeline_layout,
480 target_format,
481 self.sample_count,
482 );
483 self.highlight_pipeline = build_highlight_pipeline(
484 device,
485 &self.highlight_pipeline_layout,
486 target_format,
487 self.sample_count,
488 );
489 }
490
491 pub(crate) fn frame_begin(&mut self) {
492 self.color_instances.clear();
493 self.msdf_instances.clear();
494 self.highlight_instances.clear();
495 self.runs.clear();
496 }
497
498 #[allow(clippy::too_many_arguments)]
499 fn record_inner(
500 &mut self,
501 rect: Rect,
502 scissor: Option<PhysicalScissor>,
503 runs: &[(String, RunStyle)],
504 size: f32,
505 line_height: f32,
506 wrap: TextWrap,
507 anchor: TextAnchor,
508 scale_factor: f32,
509 ) -> std::ops::Range<usize> {
510 let avail = wrap_available_width(rect.w, scale_factor, wrap, anchor);
515 let runs_ref: Vec<(&str, RunStyle)> = runs
516 .iter()
517 .map(|(text, style)| (text.as_str(), style.clone()))
518 .collect();
519 let shared = self.shared.clone();
525 let mut inner = shared.lock();
526 let shaped = {
527 damascene_core::profile_span!("paint::text::shape_runs");
528 inner.atlas.shape_runs_with_line_height(
529 &runs_ref,
530 size,
531 line_height,
532 wrap,
533 anchor,
534 avail,
535 )
536 };
537 damascene_core::profile_span!("paint::text::emit_shaped");
538 self.emit_shaped_glyphs(&mut inner, rect, scissor, &shaped, wrap, scale_factor)
539 }
540
541 fn emit_shaped_glyphs(
542 &mut self,
543 inner: &mut SharedTextInner,
544 rect: Rect,
545 scissor: Option<PhysicalScissor>,
546 shaped: &ShapedRun,
547 wrap: TextWrap,
548 scale_factor: f32,
549 ) -> std::ops::Range<usize> {
550 let runs_start = self.runs.len();
551 if shaped.glyphs.is_empty() && shaped.highlights.is_empty() && shaped.decorations.is_empty()
552 {
553 return runs_start..runs_start;
554 }
555
556 let v_offset = match wrap {
565 TextWrap::NoWrap => ((rect.h - shaped.layout.height).max(0.0)) * 0.5,
566 TextWrap::Wrap => 0.0,
567 };
568 let origin_x = rect.x;
569 let origin_y = rect.y + v_offset;
570
571 if !shaped.highlights.is_empty() {
577 let first = self.highlight_instances.len() as u32;
578 for h in &shaped.highlights {
579 self.highlight_instances.push(HighlightInstance {
580 rect: [origin_x + h.x, origin_y + h.y, h.w, h.h],
581 color: rgba_f32_in(h.color, self.working_color_space),
582 });
583 }
584 let count = self.highlight_instances.len() as u32 - first;
585 if count > 0 {
586 self.runs.push(TextRun {
587 kind: TextRunKind::Highlight,
588 page: 0,
589 scissor,
590 first,
591 count,
592 });
593 }
594 }
595
596 let mut current: Option<(TextRunKind, u32, u32)> = None; for glyph in &shaped.glyphs {
602 let font_id = glyph.key.font;
603 let is_color = inner.atlas.is_color_font(font_id);
604 if is_color {
605 inner.atlas.ensure_color_glyph(glyph.key);
606 let Some(slot) = inner.atlas.slot(glyph.key) else {
607 continue;
608 };
609 if slot.rect.w == 0 || slot.rect.h == 0 {
610 continue;
611 }
612 let page = slot.page;
613 let next_kind = TextRunKind::Color;
614 self.maybe_close_run(&mut current, next_kind, page, scissor);
615 self.push_color_glyph(inner, glyph, slot, origin_x, origin_y, scale_factor);
616 } else {
617 let mkey = MsdfGlyphKey {
618 font: font_id,
619 glyph_id: glyph.key.glyph_id,
620 };
621 let Some(slot) = ensure_msdf(inner, mkey, font_id, glyph.key.weight) else {
622 continue;
625 };
626 let page = slot.page;
627 let next_kind = TextRunKind::Msdf;
628 self.maybe_close_run(&mut current, next_kind, page, scissor);
629 self.push_msdf_glyph(inner, glyph, slot, origin_x, origin_y);
630 }
631 }
632
633 if let Some((kind, page, first)) = current {
635 let count = self.instance_count_after(kind, first);
636 if count > 0 {
637 self.runs.push(TextRun {
638 kind,
639 page,
640 scissor,
641 first,
642 count,
643 });
644 }
645 }
646
647 if !shaped.decorations.is_empty() {
652 let first = self.highlight_instances.len() as u32;
653 for d in &shaped.decorations {
654 self.highlight_instances.push(HighlightInstance {
655 rect: [origin_x + d.x, origin_y + d.y, d.w, d.h],
656 color: rgba_f32_in(d.color, self.working_color_space),
657 });
658 }
659 let count = self.highlight_instances.len() as u32 - first;
660 if count > 0 {
661 self.runs.push(TextRun {
662 kind: TextRunKind::Highlight,
663 page: 0,
664 scissor,
665 first,
666 count,
667 });
668 }
669 }
670
671 runs_start..self.runs.len()
672 }
673
674 fn maybe_close_run(
675 &mut self,
676 current: &mut Option<(TextRunKind, u32, u32)>,
677 next_kind: TextRunKind,
678 next_page: u32,
679 scissor: Option<PhysicalScissor>,
680 ) {
681 let new_start = match next_kind {
682 TextRunKind::Color => self.color_instances.len() as u32,
683 TextRunKind::Msdf => self.msdf_instances.len() as u32,
684 TextRunKind::Highlight => self.highlight_instances.len() as u32,
685 };
686 let needs_close = match current {
687 Some((kind, page, _)) => !same_kind(*kind, next_kind) || *page != next_page,
688 None => false,
689 };
690 if needs_close {
691 let (kind, page, first) = current.take().unwrap();
692 let count = self.instance_count_after(kind, first);
693 if count > 0 {
694 self.runs.push(TextRun {
695 kind,
696 page,
697 scissor,
698 first,
699 count,
700 });
701 }
702 }
703 if current.is_none() {
704 *current = Some((next_kind, next_page, new_start));
705 }
706 }
707
708 fn instance_count_after(&self, kind: TextRunKind, first: u32) -> u32 {
709 let len = match kind {
710 TextRunKind::Color => self.color_instances.len() as u32,
711 TextRunKind::Msdf => self.msdf_instances.len() as u32,
712 TextRunKind::Highlight => self.highlight_instances.len() as u32,
713 };
714 len.saturating_sub(first)
715 }
716
717 fn push_color_glyph(
718 &mut self,
719 inner: &SharedTextInner,
720 glyph: &ShapedGlyph,
721 slot: damascene_core::text::atlas::GlyphSlot,
722 origin_x: f32,
723 origin_y: f32,
724 scale_factor: f32,
725 ) {
726 let ratio = if slot.raster_size > 0.0 {
738 glyph.key.size() / slot.raster_size
739 } else {
740 1.0
741 };
742 let bx = origin_x + glyph.x + slot.offset.0 as f32 * ratio / scale_factor;
743 let by = origin_y + glyph.y - slot.offset.1 as f32 * ratio / scale_factor;
744 let bw = slot.rect.w as f32 * ratio / scale_factor;
745 let bh = slot.rect.h as f32 * ratio / scale_factor;
746 let atlas_page = inner
747 .atlas
748 .page(slot.page)
749 .expect("shaped glyph references missing colour atlas page");
750 let page_w = atlas_page.width as f32;
751 let page_h = atlas_page.height as f32;
752 let uv = [
753 slot.rect.x as f32 / page_w,
754 slot.rect.y as f32 / page_h,
755 slot.rect.w as f32 / page_w,
756 slot.rect.h as f32 / page_h,
757 ];
758 let inst_color = if slot.is_color {
759 [1.0, 1.0, 1.0, 1.0]
760 } else {
761 rgba_f32_in(glyph.color, self.working_color_space)
762 };
763 self.color_instances.push(ColorGlyphInstance {
764 rect: [bx, by, bw, bh],
765 uv,
766 color: inst_color,
767 });
768 }
769
770 fn push_msdf_glyph(
771 &mut self,
772 inner: &SharedTextInner,
773 glyph: &ShapedGlyph,
774 slot: MsdfSlot,
775 origin_x: f32,
776 origin_y: f32,
777 ) {
778 let logical_em = glyph.key.size();
781 let base_em = inner.msdf_atlas.base_em() as f32;
782 let scale = logical_em / base_em;
783 let bx = origin_x + glyph.x + slot.bearing_x * scale;
784 let by = origin_y + glyph.y + slot.bearing_y * scale;
785 let bw = slot.rect.w as f32 * scale;
786 let bh = slot.rect.h as f32 * scale;
787 let atlas_page = inner
788 .msdf_atlas
789 .page(slot.page)
790 .expect("shaped glyph references missing MSDF atlas page");
791 let page_w = atlas_page.width as f32;
792 let page_h = atlas_page.height as f32;
793 let uv = [
794 slot.rect.x as f32 / page_w,
795 slot.rect.y as f32 / page_h,
796 slot.rect.w as f32 / page_w,
797 slot.rect.h as f32 / page_h,
798 ];
799 let color = rgba_f32_in(glyph.color, self.working_color_space);
800 self.msdf_instances.push(MsdfGlyphInstance {
801 rect: [bx, by, bw, bh],
802 uv,
803 color,
804 params: [slot.spread, 0.0, 0.0, 0.0],
805 });
806 }
807
808 pub fn warm_default_glyphs(&mut self) {
821 self.shared.clone().lock().warm_default_glyphs();
822 }
823
824 pub fn warm_glyphs(&mut self, families: &[FontFamily], chars: &[char]) {
827 self.shared
828 .clone()
829 .lock()
830 .warm_msdf_for_chars(chars, families);
831 }
832
833 pub fn export_msdf_snapshot(&self) -> Vec<u8> {
836 self.shared.clone().lock().export_msdf_snapshot()
837 }
838
839 pub fn import_msdf_snapshot(&self, bytes: &[u8]) -> Result<usize, SnapshotError> {
842 self.shared.clone().lock().import_msdf_snapshot(bytes)
843 }
844
845 pub(crate) fn flush(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
848 {
849 let shared = self.shared.clone();
850 let mut inner = shared.lock();
851 inner.flush_pages(device, queue);
852 self.color_page_bgs = inner
857 .color_pages
858 .iter()
859 .map(|p| p.bind_group.clone())
860 .collect();
861 self.msdf_page_bgs = inner
862 .msdf_pages
863 .iter()
864 .map(|p| p.bind_group.clone())
865 .collect();
866 }
867
868 if self.color_instances.len() > self.color_instance_capacity {
870 let new_cap = self.color_instances.len().next_power_of_two();
871 self.color_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
872 label: Some("damascene_wgpu::text::color_instance_buf (resized)"),
873 size: (new_cap * std::mem::size_of::<ColorGlyphInstance>()) as u64,
874 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
875 mapped_at_creation: false,
876 });
877 self.color_instance_capacity = new_cap;
878 }
879 if !self.color_instances.is_empty() {
880 queue.write_buffer(
881 &self.color_instance_buf,
882 0,
883 bytemuck::cast_slice(&self.color_instances),
884 );
885 }
886
887 if self.msdf_instances.len() > self.msdf_instance_capacity {
889 let new_cap = self.msdf_instances.len().next_power_of_two();
890 self.msdf_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
891 label: Some("damascene_wgpu::text::msdf_instance_buf (resized)"),
892 size: (new_cap * std::mem::size_of::<MsdfGlyphInstance>()) as u64,
893 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
894 mapped_at_creation: false,
895 });
896 self.msdf_instance_capacity = new_cap;
897 }
898 if !self.msdf_instances.is_empty() {
899 queue.write_buffer(
900 &self.msdf_instance_buf,
901 0,
902 bytemuck::cast_slice(&self.msdf_instances),
903 );
904 }
905
906 if self.highlight_instances.len() > self.highlight_instance_capacity {
908 let new_cap = self.highlight_instances.len().next_power_of_two();
909 self.highlight_instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
910 label: Some("damascene_wgpu::text::highlight_instance_buf (resized)"),
911 size: (new_cap * std::mem::size_of::<HighlightInstance>()) as u64,
912 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
913 mapped_at_creation: false,
914 });
915 self.highlight_instance_capacity = new_cap;
916 }
917 if !self.highlight_instances.is_empty() {
918 queue.write_buffer(
919 &self.highlight_instance_buf,
920 0,
921 bytemuck::cast_slice(&self.highlight_instances),
922 );
923 }
924 }
925
926 pub(crate) fn run(&self, index: usize) -> TextRun {
927 self.runs[index]
928 }
929
930 pub(crate) fn pipeline_for(&self, kind: TextRunKind) -> &wgpu::RenderPipeline {
931 match kind {
932 TextRunKind::Color => &self.color_pipeline,
933 TextRunKind::Msdf => &self.msdf_pipeline,
934 TextRunKind::Highlight => &self.highlight_pipeline,
935 }
936 }
937
938 pub(crate) fn instance_buf_for(&self, kind: TextRunKind) -> &wgpu::Buffer {
939 match kind {
940 TextRunKind::Color => &self.color_instance_buf,
941 TextRunKind::Msdf => &self.msdf_instance_buf,
942 TextRunKind::Highlight => &self.highlight_instance_buf,
943 }
944 }
945
946 pub(crate) fn page_bind_group(&self, kind: TextRunKind, page: u32) -> &wgpu::BindGroup {
951 match kind {
952 TextRunKind::Color => &self.color_page_bgs[page as usize],
953 TextRunKind::Msdf => &self.msdf_page_bgs[page as usize],
954 TextRunKind::Highlight => unreachable!("highlight runs carry no page binding"),
955 }
956 }
957}
958
959impl SharedTextInner {
960 fn flush_pages(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
966 let color_dirty = self.atlas.take_dirty();
968 while self.color_pages.len() < self.atlas.pages().len() {
969 let i = self.color_pages.len();
970 let page = &self.atlas.pages()[i];
971 self.color_pages.push(create_color_page(
972 device,
973 &self.color_page_bind_layout,
974 &self.color_sampler,
975 page.width,
976 page.height,
977 ));
978 }
979 for (page_idx, rect) in color_dirty {
980 let page = &self.atlas.pages()[page_idx];
981 upload_color_region(queue, &self.color_pages[page_idx].texture, page, rect);
982 }
983
984 let msdf_dirty = self.msdf_atlas.take_dirty();
986 while self.msdf_pages.len() < self.msdf_atlas.pages().len() {
987 let i = self.msdf_pages.len();
988 let page = &self.msdf_atlas.pages()[i];
989 self.msdf_pages.push(create_msdf_page(
990 device,
991 &self.msdf_page_bind_layout,
992 &self.msdf_sampler,
993 page.width,
994 page.height,
995 ));
996 }
997 for (page_idx, rect) in msdf_dirty {
998 let page = &self.msdf_atlas.pages()[page_idx];
999 upload_msdf_region(queue, &self.msdf_pages[page_idx].texture, page, rect);
1000 }
1001 }
1002
1003 pub(crate) fn warm_default_glyphs(&mut self) {
1005 #[cfg(feature = "prebaked-default-fonts")]
1009 if self.warm_from_prebaked() {
1010 return;
1011 }
1012 const FAMILIES: &[FontFamily] = &[FontFamily::Inter, FontFamily::JetBrainsMono];
1013 let chars: Vec<char> = (0x20u32..=0x7Eu32).filter_map(char::from_u32).collect();
1014 self.warm_msdf_for_chars(&chars, FAMILIES);
1015 }
1016
1017 #[cfg(feature = "prebaked-default-fonts")]
1021 fn warm_from_prebaked(&mut self) -> bool {
1022 use damascene_core::prebaked::{DEFAULT_ATLAS, TOKEN_INTER, TOKEN_JETBRAINS_MONO};
1023 let inter = self.resolve_family_font_id(FontFamily::Inter);
1024 let mono = self.resolve_family_font_id(FontFamily::JetBrainsMono);
1025 let id_of = |token| match token {
1026 TOKEN_INTER => inter,
1027 TOKEN_JETBRAINS_MONO => mono,
1028 _ => None,
1029 };
1030 matches!(self.msdf_atlas.import_snapshot(DEFAULT_ATLAS, id_of), Ok(n) if n > 0)
1031 }
1032
1033 fn resolve_family_font_id(&self, family: FontFamily) -> Option<fontdb::ID> {
1036 self.atlas.font_system().db().query(&fontdb::Query {
1037 families: &[fontdb::Family::Name(family.family_name())],
1038 weight: fontdb::Weight::NORMAL,
1039 ..fontdb::Query::default()
1040 })
1041 }
1042
1043 pub(crate) fn warm_msdf_for_chars(&mut self, chars: &[char], families: &[FontFamily]) {
1050 for family in families {
1051 let Some(font_id) = self.resolve_family_font_id(*family) else {
1052 continue;
1053 };
1054 let face_index = self
1055 .atlas
1056 .font_system()
1057 .db()
1058 .face(font_id)
1059 .map(|f| f.index)
1060 .unwrap_or(0);
1061 let Some(font) = self
1062 .atlas
1063 .font_system_mut()
1064 .get_font(font_id, fontdb::Weight::NORMAL)
1065 else {
1066 continue;
1067 };
1068 let Ok(face) = Face::parse(font.data(), face_index) else {
1069 continue;
1070 };
1071 let keys: Vec<MsdfGlyphKey> = chars
1072 .iter()
1073 .filter_map(|&ch| face.glyph_index(ch))
1074 .map(|glyph_id| MsdfGlyphKey {
1075 font: font_id,
1076 glyph_id: glyph_id.0,
1077 })
1078 .collect();
1079 self.msdf_atlas.ensure_many(&keys, &face);
1083 }
1084 }
1085
1086 pub(crate) fn export_msdf_snapshot(&self) -> Vec<u8> {
1090 let db = self.atlas.font_system().db();
1091 self.msdf_atlas
1092 .export_snapshot(|id| db.with_face_data(id, |data, _| font_token_hash(data)))
1093 }
1094
1095 pub(crate) fn import_msdf_snapshot(&mut self, bytes: &[u8]) -> Result<usize, SnapshotError> {
1098 let by_hash: std::collections::HashMap<u64, fontdb::ID> = {
1102 let db = self.atlas.font_system().db();
1103 db.faces()
1104 .filter_map(|f| {
1105 db.with_face_data(f.id, |data, _| font_token_hash(data))
1106 .map(|h| (h, f.id))
1107 })
1108 .collect()
1109 };
1110 self.msdf_atlas
1111 .import_snapshot(bytes, |t| by_hash.get(&t).copied())
1112 }
1113}
1114
1115fn ensure_msdf(
1117 inner: &mut SharedTextInner,
1118 key: MsdfGlyphKey,
1119 font_id: fontdb::ID,
1120 weight: fontdb::Weight,
1121) -> Option<MsdfSlot> {
1122 if let Some(slot) = inner.msdf_atlas.touch(key) {
1125 return Some(slot);
1126 }
1127 let font = inner.atlas.font_system_mut().get_font(font_id, weight)?;
1133 let face_index = inner.atlas.font_system().db().face(font_id)?.index;
1134 let face = Face::parse(font.data(), face_index).ok()?;
1135 inner.msdf_atlas.ensure(key, &face)
1136}
1137
1138fn same_kind(a: TextRunKind, b: TextRunKind) -> bool {
1139 a == b
1140}
1141
1142fn build_color_pipeline(
1146 device: &wgpu::Device,
1147 layout: &wgpu::PipelineLayout,
1148 target_format: wgpu::TextureFormat,
1149 sample_count: u32,
1150) -> wgpu::RenderPipeline {
1151 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1152 label: Some("stock::text"),
1153 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(stock_wgsl::TEXT)),
1154 });
1155 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1156 label: Some("damascene_wgpu::text::color_pipeline"),
1157 layout: Some(layout),
1158 vertex: wgpu::VertexState {
1159 module: &shader,
1160 entry_point: Some("vs_main"),
1161 compilation_options: Default::default(),
1162 buffers: &[
1163 wgpu::VertexBufferLayout {
1164 array_stride: (2 * std::mem::size_of::<f32>()) as u64,
1165 step_mode: wgpu::VertexStepMode::Vertex,
1166 attributes: &[wgpu::VertexAttribute {
1167 shader_location: 0,
1168 format: wgpu::VertexFormat::Float32x2,
1169 offset: 0,
1170 }],
1171 },
1172 wgpu::VertexBufferLayout {
1173 array_stride: std::mem::size_of::<ColorGlyphInstance>() as u64,
1174 step_mode: wgpu::VertexStepMode::Instance,
1175 attributes: &COLOR_INSTANCE_ATTRS,
1176 },
1177 ],
1178 },
1179 fragment: Some(wgpu::FragmentState {
1180 module: &shader,
1181 entry_point: Some("fs_main"),
1182 compilation_options: Default::default(),
1183 targets: &[Some(wgpu::ColorTargetState {
1184 format: target_format,
1185 blend: Some(premultiplied_blend()),
1186 write_mask: wgpu::ColorWrites::ALL,
1187 })],
1188 }),
1189 primitive: triangle_strip(),
1190 depth_stencil: None,
1191 multisample: wgpu::MultisampleState {
1192 count: sample_count,
1193 mask: !0,
1194 alpha_to_coverage_enabled: false,
1195 },
1196 multiview_mask: None,
1197 cache: None,
1198 })
1199}
1200
1201fn build_msdf_pipeline(
1204 device: &wgpu::Device,
1205 layout: &wgpu::PipelineLayout,
1206 target_format: wgpu::TextureFormat,
1207 sample_count: u32,
1208) -> wgpu::RenderPipeline {
1209 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1210 label: Some("stock::text_msdf"),
1211 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(stock_wgsl::TEXT_MSDF)),
1212 });
1213 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1214 label: Some("damascene_wgpu::text::msdf_pipeline"),
1215 layout: Some(layout),
1216 vertex: wgpu::VertexState {
1217 module: &shader,
1218 entry_point: Some("vs_main"),
1219 compilation_options: Default::default(),
1220 buffers: &[
1221 wgpu::VertexBufferLayout {
1222 array_stride: (2 * std::mem::size_of::<f32>()) as u64,
1223 step_mode: wgpu::VertexStepMode::Vertex,
1224 attributes: &[wgpu::VertexAttribute {
1225 shader_location: 0,
1226 format: wgpu::VertexFormat::Float32x2,
1227 offset: 0,
1228 }],
1229 },
1230 wgpu::VertexBufferLayout {
1231 array_stride: std::mem::size_of::<MsdfGlyphInstance>() as u64,
1232 step_mode: wgpu::VertexStepMode::Instance,
1233 attributes: &MSDF_INSTANCE_ATTRS,
1234 },
1235 ],
1236 },
1237 fragment: Some(wgpu::FragmentState {
1238 module: &shader,
1239 entry_point: Some("fs_main"),
1240 compilation_options: Default::default(),
1241 targets: &[Some(wgpu::ColorTargetState {
1242 format: target_format,
1243 blend: Some(premultiplied_blend()),
1244 write_mask: wgpu::ColorWrites::ALL,
1245 })],
1246 }),
1247 primitive: triangle_strip(),
1248 depth_stencil: None,
1249 multisample: wgpu::MultisampleState {
1250 count: sample_count,
1251 mask: !0,
1252 alpha_to_coverage_enabled: false,
1253 },
1254 multiview_mask: None,
1255 cache: None,
1256 })
1257}
1258
1259fn build_highlight_pipeline(
1262 device: &wgpu::Device,
1263 layout: &wgpu::PipelineLayout,
1264 target_format: wgpu::TextureFormat,
1265 sample_count: u32,
1266) -> wgpu::RenderPipeline {
1267 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1268 label: Some("stock::text_highlight"),
1269 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(stock_wgsl::TEXT_HIGHLIGHT)),
1270 });
1271 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1272 label: Some("damascene_wgpu::text::highlight_pipeline"),
1273 layout: Some(layout),
1274 vertex: wgpu::VertexState {
1275 module: &shader,
1276 entry_point: Some("vs_main"),
1277 compilation_options: Default::default(),
1278 buffers: &[
1279 wgpu::VertexBufferLayout {
1280 array_stride: (2 * std::mem::size_of::<f32>()) as u64,
1281 step_mode: wgpu::VertexStepMode::Vertex,
1282 attributes: &[wgpu::VertexAttribute {
1283 shader_location: 0,
1284 format: wgpu::VertexFormat::Float32x2,
1285 offset: 0,
1286 }],
1287 },
1288 wgpu::VertexBufferLayout {
1289 array_stride: std::mem::size_of::<HighlightInstance>() as u64,
1290 step_mode: wgpu::VertexStepMode::Instance,
1291 attributes: &HIGHLIGHT_INSTANCE_ATTRS,
1292 },
1293 ],
1294 },
1295 fragment: Some(wgpu::FragmentState {
1296 module: &shader,
1297 entry_point: Some("fs_main"),
1298 compilation_options: Default::default(),
1299 targets: &[Some(wgpu::ColorTargetState {
1300 format: target_format,
1301 blend: Some(premultiplied_blend()),
1302 write_mask: wgpu::ColorWrites::ALL,
1303 })],
1304 }),
1305 primitive: triangle_strip(),
1306 depth_stencil: None,
1307 multisample: wgpu::MultisampleState {
1308 count: sample_count,
1309 mask: !0,
1310 alpha_to_coverage_enabled: false,
1311 },
1312 multiview_mask: None,
1313 cache: None,
1314 })
1315}
1316
1317fn premultiplied_blend() -> wgpu::BlendState {
1318 wgpu::BlendState {
1319 color: wgpu::BlendComponent {
1320 src_factor: wgpu::BlendFactor::One,
1321 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1322 operation: wgpu::BlendOperation::Add,
1323 },
1324 alpha: wgpu::BlendComponent {
1325 src_factor: wgpu::BlendFactor::One,
1326 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1327 operation: wgpu::BlendOperation::Add,
1328 },
1329 }
1330}
1331
1332fn triangle_strip() -> wgpu::PrimitiveState {
1333 wgpu::PrimitiveState {
1334 topology: wgpu::PrimitiveTopology::TriangleStrip,
1335 strip_index_format: None,
1336 front_face: wgpu::FrontFace::Ccw,
1337 cull_mode: None,
1338 polygon_mode: wgpu::PolygonMode::Fill,
1339 unclipped_depth: false,
1340 conservative: false,
1341 }
1342}
1343
1344fn create_color_page(
1345 device: &wgpu::Device,
1346 layout: &wgpu::BindGroupLayout,
1347 sampler: &wgpu::Sampler,
1348 width: u32,
1349 height: u32,
1350) -> PageTexture {
1351 let texture = device.create_texture(&wgpu::TextureDescriptor {
1352 label: Some("damascene_wgpu::text::color_page"),
1353 size: wgpu::Extent3d {
1354 width,
1355 height,
1356 depth_or_array_layers: 1,
1357 },
1358 mip_level_count: 1,
1359 sample_count: 1,
1360 dimension: wgpu::TextureDimension::D2,
1361 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1362 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1363 view_formats: &[],
1364 });
1365 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1366 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1367 label: Some("damascene_wgpu::text::color_page_bg"),
1368 layout,
1369 entries: &[
1370 wgpu::BindGroupEntry {
1371 binding: 0,
1372 resource: wgpu::BindingResource::TextureView(&view),
1373 },
1374 wgpu::BindGroupEntry {
1375 binding: 1,
1376 resource: wgpu::BindingResource::Sampler(sampler),
1377 },
1378 ],
1379 });
1380 PageTexture {
1381 texture,
1382 bind_group,
1383 }
1384}
1385
1386fn create_msdf_page(
1387 device: &wgpu::Device,
1388 layout: &wgpu::BindGroupLayout,
1389 sampler: &wgpu::Sampler,
1390 width: u32,
1391 height: u32,
1392) -> PageTexture {
1393 let texture = device.create_texture(&wgpu::TextureDescriptor {
1394 label: Some("damascene_wgpu::text::msdf_page"),
1395 size: wgpu::Extent3d {
1396 width,
1397 height,
1398 depth_or_array_layers: 1,
1399 },
1400 mip_level_count: 1,
1401 sample_count: 1,
1402 dimension: wgpu::TextureDimension::D2,
1403 format: wgpu::TextureFormat::Rgba8Unorm,
1406 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1407 view_formats: &[],
1408 });
1409 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1410 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1411 label: Some("damascene_wgpu::text::msdf_page_bg"),
1412 layout,
1413 entries: &[
1414 wgpu::BindGroupEntry {
1415 binding: 0,
1416 resource: wgpu::BindingResource::TextureView(&view),
1417 },
1418 wgpu::BindGroupEntry {
1419 binding: 1,
1420 resource: wgpu::BindingResource::Sampler(sampler),
1421 },
1422 ],
1423 });
1424 PageTexture {
1425 texture,
1426 bind_group,
1427 }
1428}
1429
1430impl TextRecorder for TextPaint {
1431 fn record(
1432 &mut self,
1433 rect: Rect,
1434 scissor: Option<PhysicalScissor>,
1435 style: &RunStyle,
1436 text: &str,
1437 size: f32,
1438 line_height: f32,
1439 wrap: TextWrap,
1440 anchor: TextAnchor,
1441 scale_factor: f32,
1442 ) -> std::ops::Range<usize> {
1443 self.record_inner(
1444 rect,
1445 scissor,
1446 &[(text.to_string(), style.clone())],
1447 size,
1448 line_height,
1449 wrap,
1450 anchor,
1451 scale_factor,
1452 )
1453 }
1454
1455 fn record_runs(
1456 &mut self,
1457 rect: Rect,
1458 scissor: Option<PhysicalScissor>,
1459 runs: &[(String, RunStyle)],
1460 size: f32,
1461 line_height: f32,
1462 wrap: TextWrap,
1463 anchor: TextAnchor,
1464 scale_factor: f32,
1465 ) -> std::ops::Range<usize> {
1466 self.record_inner(
1467 rect,
1468 scissor,
1469 runs,
1470 size,
1471 line_height,
1472 wrap,
1473 anchor,
1474 scale_factor,
1475 )
1476 }
1477}
1478
1479fn wrap_available_width(
1480 rect_w: f32,
1481 _scale_factor: f32,
1482 wrap: TextWrap,
1483 anchor: TextAnchor,
1484) -> Option<f32> {
1485 match (wrap, anchor) {
1488 (TextWrap::Wrap, _) => Some(rect_w),
1489 (TextWrap::NoWrap, TextAnchor::Start) => None,
1490 (TextWrap::NoWrap, TextAnchor::Middle | TextAnchor::End) => Some(rect_w),
1491 }
1492}
1493
1494fn upload_color_region(
1495 queue: &wgpu::Queue,
1496 texture: &wgpu::Texture,
1497 page: &AtlasPage,
1498 rect: AtlasRect,
1499) {
1500 if rect.w == 0 || rect.h == 0 {
1501 return;
1502 }
1503 let bpp = ATLAS_BYTES_PER_PIXEL as usize;
1504 let row_bytes = rect.w as usize * bpp;
1505 let mut bytes = Vec::with_capacity(row_bytes * rect.h as usize);
1506 for row in 0..rect.h {
1507 let y = rect.y + row;
1508 let start = (y as usize * page.width as usize + rect.x as usize) * bpp;
1509 let end = start + row_bytes;
1510 bytes.extend_from_slice(&page.pixels[start..end]);
1511 }
1512 queue.write_texture(
1513 wgpu::TexelCopyTextureInfo {
1514 texture,
1515 mip_level: 0,
1516 origin: wgpu::Origin3d {
1517 x: rect.x,
1518 y: rect.y,
1519 z: 0,
1520 },
1521 aspect: wgpu::TextureAspect::All,
1522 },
1523 &bytes,
1524 wgpu::TexelCopyBufferLayout {
1525 offset: 0,
1526 bytes_per_row: Some(rect.w * ATLAS_BYTES_PER_PIXEL),
1527 rows_per_image: Some(rect.h),
1528 },
1529 wgpu::Extent3d {
1530 width: rect.w,
1531 height: rect.h,
1532 depth_or_array_layers: 1,
1533 },
1534 );
1535}
1536
1537fn upload_msdf_region(
1538 queue: &wgpu::Queue,
1539 texture: &wgpu::Texture,
1540 page: &MsdfAtlasPage,
1541 rect: MsdfRect,
1542) {
1543 if rect.w == 0 || rect.h == 0 {
1544 return;
1545 }
1546 let bpp = MSDF_BYTES_PER_PIXEL as usize;
1547 let row_bytes = rect.w as usize * bpp;
1548 let mut bytes = Vec::with_capacity(row_bytes * rect.h as usize);
1549 for row in 0..rect.h {
1550 let y = rect.y + row;
1551 let start = (y as usize * page.width as usize + rect.x as usize) * bpp;
1552 let end = start + row_bytes;
1553 bytes.extend_from_slice(&page.pixels[start..end]);
1554 }
1555 queue.write_texture(
1556 wgpu::TexelCopyTextureInfo {
1557 texture,
1558 mip_level: 0,
1559 origin: wgpu::Origin3d {
1560 x: rect.x,
1561 y: rect.y,
1562 z: 0,
1563 },
1564 aspect: wgpu::TextureAspect::All,
1565 },
1566 &bytes,
1567 wgpu::TexelCopyBufferLayout {
1568 offset: 0,
1569 bytes_per_row: Some(rect.w * MSDF_BYTES_PER_PIXEL),
1570 rows_per_image: Some(rect.h),
1571 },
1572 wgpu::Extent3d {
1573 width: rect.w,
1574 height: rect.h,
1575 depth_or_array_layers: 1,
1576 },
1577 );
1578}