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