iced_wgpu/
text.rs

1use crate::core::alignment;
2use crate::core::text::Alignment;
3use crate::core::{Rectangle, Size, Transformation};
4use crate::graphics::cache;
5use crate::graphics::color;
6use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache};
7use crate::graphics::text::{Editor, Paragraph, font_system, to_color};
8
9use rustc_hash::FxHashMap;
10use std::collections::hash_map;
11use std::sync::atomic::{self, AtomicU64};
12use std::sync::{self, Arc, RwLock};
13
14pub use crate::graphics::Text;
15
16const COLOR_MODE: cryoglyph::ColorMode = if color::GAMMA_CORRECTION {
17    cryoglyph::ColorMode::Accurate
18} else {
19    cryoglyph::ColorMode::Web
20};
21
22pub type Batch = Vec<Item>;
23
24#[derive(Debug)]
25pub enum Item {
26    Group {
27        transformation: Transformation,
28        text: Vec<Text>,
29    },
30    Cached {
31        transformation: Transformation,
32        cache: Cache,
33    },
34}
35
36#[derive(Debug, Clone)]
37pub struct Cache {
38    id: Id,
39    group: cache::Group,
40    text: Arc<[Text]>,
41    version: usize,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub struct Id(u64);
46
47impl Cache {
48    pub fn new(group: cache::Group, text: Vec<Text>) -> Option<Self> {
49        static NEXT_ID: AtomicU64 = AtomicU64::new(0);
50
51        if text.is_empty() {
52            return None;
53        }
54
55        Some(Self {
56            id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
57            group,
58            text: Arc::from(text),
59            version: 0,
60        })
61    }
62
63    pub fn update(&mut self, text: Vec<Text>) {
64        if self.text.is_empty() && text.is_empty() {
65            return;
66        }
67
68        self.text = Arc::from(text);
69        self.version += 1;
70    }
71}
72
73struct Upload {
74    renderer: cryoglyph::TextRenderer,
75    buffer_cache: BufferCache,
76    transformation: Transformation,
77    version: usize,
78    group_version: usize,
79    text: sync::Weak<[Text]>,
80    _atlas: sync::Weak<()>,
81}
82
83#[derive(Default)]
84pub struct Storage {
85    groups: FxHashMap<cache::Group, Group>,
86    uploads: FxHashMap<Id, Upload>,
87}
88
89struct Group {
90    atlas: cryoglyph::TextAtlas,
91    version: usize,
92    should_trim: bool,
93    handle: Arc<()>, // Keeps track of active uploads
94}
95
96impl Storage {
97    fn get(&self, cache: &Cache) -> Option<(&cryoglyph::TextAtlas, &Upload)> {
98        if cache.text.is_empty() {
99            return None;
100        }
101
102        self.groups
103            .get(&cache.group)
104            .map(|group| &group.atlas)
105            .zip(self.uploads.get(&cache.id))
106    }
107
108    fn prepare(
109        &mut self,
110        device: &wgpu::Device,
111        queue: &wgpu::Queue,
112        viewport: &cryoglyph::Viewport,
113        encoder: &mut wgpu::CommandEncoder,
114        format: wgpu::TextureFormat,
115        state: &cryoglyph::Cache,
116        cache: &Cache,
117        new_transformation: Transformation,
118        bounds: Rectangle,
119    ) {
120        let group_count = self.groups.len();
121
122        let group = self.groups.entry(cache.group).or_insert_with(|| {
123            log::debug!(
124                "New text atlas: {:?} (total: {})",
125                cache.group,
126                group_count + 1
127            );
128
129            Group {
130                atlas: cryoglyph::TextAtlas::with_color_mode(
131                    device, queue, state, format, COLOR_MODE,
132                ),
133                version: 0,
134                should_trim: false,
135                handle: Arc::new(()),
136            }
137        });
138
139        match self.uploads.entry(cache.id) {
140            hash_map::Entry::Occupied(entry) => {
141                let upload = entry.into_mut();
142
143                if upload.version != cache.version
144                    || upload.group_version != group.version
145                    || upload.transformation != new_transformation
146                {
147                    if !cache.text.is_empty() {
148                        let _ = prepare(
149                            device,
150                            queue,
151                            viewport,
152                            encoder,
153                            &mut upload.renderer,
154                            &mut group.atlas,
155                            &mut upload.buffer_cache,
156                            &cache.text,
157                            bounds,
158                            new_transformation,
159                        );
160                    }
161
162                    // Only trim if glyphs have changed
163                    group.should_trim =
164                        group.should_trim || upload.version != cache.version;
165
166                    upload.text = Arc::downgrade(&cache.text);
167                    upload.version = cache.version;
168                    upload.group_version = group.version;
169                    upload.transformation = new_transformation;
170
171                    upload.buffer_cache.trim();
172                }
173            }
174            hash_map::Entry::Vacant(entry) => {
175                let mut renderer = cryoglyph::TextRenderer::new(
176                    &mut group.atlas,
177                    device,
178                    wgpu::MultisampleState::default(),
179                    None,
180                );
181
182                let mut buffer_cache = BufferCache::new();
183
184                if !cache.text.is_empty() {
185                    let _ = prepare(
186                        device,
187                        queue,
188                        viewport,
189                        encoder,
190                        &mut renderer,
191                        &mut group.atlas,
192                        &mut buffer_cache,
193                        &cache.text,
194                        bounds,
195                        new_transformation,
196                    );
197                }
198
199                let _ = entry.insert(Upload {
200                    renderer,
201                    buffer_cache,
202                    transformation: new_transformation,
203                    version: 0,
204                    group_version: group.version,
205                    text: Arc::downgrade(&cache.text),
206                    _atlas: Arc::downgrade(&group.handle),
207                });
208
209                group.should_trim = cache.group.is_singleton();
210
211                log::debug!(
212                    "New text upload: {} (total: {})",
213                    cache.id.0,
214                    self.uploads.len()
215                );
216            }
217        }
218    }
219
220    pub fn trim(&mut self) {
221        self.uploads
222            .retain(|_id, upload| upload.text.strong_count() > 0);
223
224        self.groups.retain(|id, group| {
225            let active_uploads = Arc::weak_count(&group.handle);
226
227            if active_uploads == 0 {
228                log::debug!("Dropping text atlas: {id:?}");
229
230                return false;
231            }
232
233            if group.should_trim {
234                log::trace!("Trimming text atlas: {id:?}");
235
236                group.atlas.trim();
237                group.should_trim = false;
238
239                // We only need to worry about glyph fighting
240                // when the atlas may be shared by multiple
241                // uploads.
242                if !id.is_singleton() {
243                    log::debug!(
244                        "Invalidating text atlas: {id:?} \
245                        (uploads: {active_uploads})"
246                    );
247
248                    group.version += 1;
249                }
250            }
251
252            true
253        });
254    }
255}
256
257pub struct Viewport(cryoglyph::Viewport);
258
259impl Viewport {
260    pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) {
261        self.0.update(
262            queue,
263            cryoglyph::Resolution {
264                width: resolution.width,
265                height: resolution.height,
266            },
267        );
268    }
269}
270
271#[derive(Clone)]
272#[allow(missing_debug_implementations)]
273pub struct Pipeline {
274    format: wgpu::TextureFormat,
275    cache: cryoglyph::Cache,
276    atlas: Arc<RwLock<cryoglyph::TextAtlas>>,
277}
278
279impl Pipeline {
280    pub fn new(
281        device: &wgpu::Device,
282        queue: &wgpu::Queue,
283        format: wgpu::TextureFormat,
284    ) -> Self {
285        let cache = cryoglyph::Cache::new(device);
286        let atlas = cryoglyph::TextAtlas::with_color_mode(
287            device, queue, &cache, format, COLOR_MODE,
288        );
289
290        Pipeline {
291            format,
292            cache,
293            atlas: Arc::new(RwLock::new(atlas)),
294        }
295    }
296
297    pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport {
298        Viewport(cryoglyph::Viewport::new(device, &self.cache))
299    }
300
301    pub fn trim(&self) {
302        self.atlas.write().expect("Write text atlas").trim();
303    }
304}
305
306#[derive(Default)]
307pub struct State {
308    renderers: Vec<cryoglyph::TextRenderer>,
309    prepare_layer: usize,
310    cache: BufferCache,
311    storage: Storage,
312}
313
314impl State {
315    pub fn new() -> Self {
316        Self::default()
317    }
318
319    pub fn prepare(
320        &mut self,
321        pipeline: &Pipeline,
322        device: &wgpu::Device,
323        queue: &wgpu::Queue,
324        viewport: &Viewport,
325        encoder: &mut wgpu::CommandEncoder,
326        batch: &Batch,
327        layer_bounds: Rectangle,
328        layer_transformation: Transformation,
329    ) {
330        let mut atlas = pipeline.atlas.write().expect("Write to text atlas");
331
332        for item in batch {
333            match item {
334                Item::Group {
335                    transformation,
336                    text,
337                } => {
338                    if self.renderers.len() <= self.prepare_layer {
339                        self.renderers.push(cryoglyph::TextRenderer::new(
340                            &mut atlas,
341                            device,
342                            wgpu::MultisampleState::default(),
343                            None,
344                        ));
345                    }
346
347                    let renderer = &mut self.renderers[self.prepare_layer];
348                    let result = prepare(
349                        device,
350                        queue,
351                        &viewport.0,
352                        encoder,
353                        renderer,
354                        &mut atlas,
355                        &mut self.cache,
356                        text,
357                        layer_bounds * layer_transformation,
358                        layer_transformation * *transformation,
359                    );
360
361                    match result {
362                        Ok(()) => {
363                            self.prepare_layer += 1;
364                        }
365                        Err(cryoglyph::PrepareError::AtlasFull) => {
366                            // If the atlas cannot grow, then all bets are off.
367                            // Instead of panicking, we will just pray that the result
368                            // will be somewhat readable...
369                        }
370                    }
371                }
372                Item::Cached {
373                    transformation,
374                    cache,
375                } => {
376                    self.storage.prepare(
377                        device,
378                        queue,
379                        &viewport.0,
380                        encoder,
381                        pipeline.format,
382                        &pipeline.cache,
383                        cache,
384                        layer_transformation * *transformation,
385                        layer_bounds * layer_transformation,
386                    );
387                }
388            }
389        }
390    }
391
392    pub fn render<'a>(
393        &'a self,
394        pipeline: &'a Pipeline,
395        viewport: &'a Viewport,
396        start: usize,
397        batch: &'a Batch,
398        bounds: Rectangle<u32>,
399        render_pass: &mut wgpu::RenderPass<'a>,
400    ) -> usize {
401        let atlas = pipeline.atlas.read().expect("Read text atlas");
402        let mut layer_count = 0;
403
404        render_pass.set_scissor_rect(
405            bounds.x,
406            bounds.y,
407            bounds.width,
408            bounds.height,
409        );
410
411        for item in batch {
412            match item {
413                Item::Group { .. } => {
414                    let renderer = &self.renderers[start + layer_count];
415
416                    renderer
417                        .render(&atlas, &viewport.0, render_pass)
418                        .expect("Render text");
419
420                    layer_count += 1;
421                }
422                Item::Cached { cache, .. } => {
423                    if let Some((atlas, upload)) = self.storage.get(cache) {
424                        upload
425                            .renderer
426                            .render(atlas, &viewport.0, render_pass)
427                            .expect("Render cached text");
428                    }
429                }
430            }
431        }
432
433        layer_count
434    }
435
436    pub fn trim(&mut self) {
437        self.cache.trim();
438        self.storage.trim();
439
440        self.prepare_layer = 0;
441    }
442}
443
444fn prepare(
445    device: &wgpu::Device,
446    queue: &wgpu::Queue,
447    viewport: &cryoglyph::Viewport,
448    encoder: &mut wgpu::CommandEncoder,
449    renderer: &mut cryoglyph::TextRenderer,
450    atlas: &mut cryoglyph::TextAtlas,
451    buffer_cache: &mut BufferCache,
452    sections: &[Text],
453    layer_bounds: Rectangle,
454    layer_transformation: Transformation,
455) -> Result<(), cryoglyph::PrepareError> {
456    let mut font_system = font_system().write().expect("Write font system");
457    let font_system = font_system.raw();
458
459    enum Allocation {
460        Paragraph(Paragraph),
461        Editor(Editor),
462        Cache(text_cache::KeyHash),
463        Raw(Arc<cryoglyph::Buffer>),
464    }
465
466    let allocations: Vec<_> = sections
467        .iter()
468        .map(|section| match section {
469            Text::Paragraph { paragraph, .. } => {
470                paragraph.upgrade().map(Allocation::Paragraph)
471            }
472            Text::Editor { editor, .. } => {
473                editor.upgrade().map(Allocation::Editor)
474            }
475            Text::Cached {
476                content,
477                bounds,
478                size,
479                line_height,
480                font,
481                shaping,
482                ..
483            } => {
484                let (key, _) = buffer_cache.allocate(
485                    font_system,
486                    text_cache::Key {
487                        content,
488                        size: f32::from(*size),
489                        line_height: f32::from(*line_height),
490                        font: *font,
491                        bounds: Size {
492                            width: bounds.width,
493                            height: bounds.height,
494                        },
495                        shaping: *shaping,
496                    },
497                );
498
499                Some(Allocation::Cache(key))
500            }
501            Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw),
502        })
503        .collect();
504
505    let text_areas = sections.iter().zip(allocations.iter()).filter_map(
506        |(section, allocation)| {
507            let (
508                buffer,
509                bounds,
510                align_x,
511                align_y,
512                color,
513                clip_bounds,
514                transformation,
515            ) = match section {
516                Text::Paragraph {
517                    position,
518                    color,
519                    clip_bounds,
520                    transformation,
521                    ..
522                } => {
523                    use crate::core::text::Paragraph as _;
524
525                    let Some(Allocation::Paragraph(paragraph)) = allocation
526                    else {
527                        return None;
528                    };
529
530                    (
531                        paragraph.buffer(),
532                        Rectangle::new(*position, paragraph.min_bounds()),
533                        paragraph.align_x(),
534                        paragraph.align_y(),
535                        *color,
536                        *clip_bounds,
537                        *transformation,
538                    )
539                }
540                Text::Editor {
541                    position,
542                    color,
543                    clip_bounds,
544                    transformation,
545                    ..
546                } => {
547                    use crate::core::text::Editor as _;
548
549                    let Some(Allocation::Editor(editor)) = allocation else {
550                        return None;
551                    };
552
553                    (
554                        editor.buffer(),
555                        Rectangle::new(*position, editor.bounds()),
556                        Alignment::Default,
557                        alignment::Vertical::Top,
558                        *color,
559                        *clip_bounds,
560                        *transformation,
561                    )
562                }
563                Text::Cached {
564                    bounds,
565                    align_x,
566                    align_y,
567                    color,
568                    clip_bounds,
569                    ..
570                } => {
571                    let Some(Allocation::Cache(key)) = allocation else {
572                        return None;
573                    };
574
575                    let entry =
576                        buffer_cache.get(key).expect("Get cached buffer");
577
578                    (
579                        &entry.buffer,
580                        Rectangle::new(bounds.position(), entry.min_bounds),
581                        *align_x,
582                        *align_y,
583                        *color,
584                        *clip_bounds,
585                        Transformation::IDENTITY,
586                    )
587                }
588                Text::Raw {
589                    raw,
590                    transformation,
591                } => {
592                    let Some(Allocation::Raw(buffer)) = allocation else {
593                        return None;
594                    };
595
596                    let (width, height) = buffer.size();
597
598                    (
599                        buffer.as_ref(),
600                        Rectangle::new(
601                            raw.position,
602                            Size::new(
603                                width.unwrap_or(layer_bounds.width),
604                                height.unwrap_or(layer_bounds.height),
605                            ),
606                        ),
607                        Alignment::Default,
608                        alignment::Vertical::Top,
609                        raw.color,
610                        raw.clip_bounds,
611                        *transformation,
612                    )
613                }
614            };
615
616            let bounds = bounds * transformation * layer_transformation;
617
618            let left = match align_x {
619                Alignment::Default | Alignment::Left | Alignment::Justified => {
620                    bounds.x
621                }
622                Alignment::Center => bounds.x - bounds.width / 2.0,
623                Alignment::Right => bounds.x - bounds.width,
624            };
625
626            let top = match align_y {
627                alignment::Vertical::Top => bounds.y,
628                alignment::Vertical::Center => bounds.y - bounds.height / 2.0,
629                alignment::Vertical::Bottom => bounds.y - bounds.height,
630            };
631
632            let clip_bounds = layer_bounds.intersection(
633                &(clip_bounds * transformation * layer_transformation),
634            )?;
635
636            Some(cryoglyph::TextArea {
637                buffer,
638                left,
639                top,
640                scale: transformation.scale_factor()
641                    * layer_transformation.scale_factor(),
642                bounds: cryoglyph::TextBounds {
643                    left: clip_bounds.x as i32,
644                    top: clip_bounds.y as i32,
645                    right: (clip_bounds.x + clip_bounds.width) as i32,
646                    bottom: (clip_bounds.y + clip_bounds.height) as i32,
647                },
648                default_color: to_color(color),
649            })
650        },
651    );
652
653    renderer.prepare(
654        device,
655        queue,
656        encoder,
657        font_system,
658        atlas,
659        viewport,
660        text_areas,
661        &mut cryoglyph::SwashCache::new(),
662    )
663}