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