Skip to main content

alpha2_engine/
text.rs

1//! Text rendering: caption tracks' cues and text generators, rasterized by
2//! the portable text host.
3//!
4//! The engine draws text itself, from font bytes its host registers once,
5//! so a browser preview and a native export of one project, given the same
6//! fonts, draw the same text pixels. A host keeps one [`TextTextures`]
7//! beside its frame provider, calls [`TextTextures::prepare`] for each frame
8//! before rendering it, and answers [`crate::FrameProvider::text`] from it.
9//! A caption is composited as a layer between tracks; a text generator's
10//! texture is placed on the transparent picture the generator draws. A
11//! layout generator is resolved here too, with the same fonts, and its
12//! texts rasterized beside the others ([`crate::layout`]).
13
14use std::collections::HashMap;
15use std::fmt;
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, Ordering};
18
19use alpha2_eval::{FrameDesc, GeneratorPlan, LayoutPlan, StepOp};
20use alpha2_layout::{PictureSize, TextBounds, shaped_key, text_key, text_request};
21use alpha2_model::{Generator, TextStyle};
22use alpha2_render::{
23    CpuImage, CpuRenderer, CpuSource, RenderCacheKey, RenderError, RenderSize, Renderer,
24    SourceView, UploadTexture,
25};
26use alpha2_text::{ShapedText, TextFont, TextRasterError, TextRasterRequest, TextRasterizer};
27
28use crate::layout::PreparedLayout;
29
30/// `size` as the picture text and layouts are placed on.
31pub(crate) fn picture(size: RenderSize) -> PictureSize {
32    PictureSize::new(size.width, size.height).expect("a render size is a picture size")
33}
34
35/// The high 64 bits of a text's result cache key, before its font set is
36/// mixed in: font sets are counted from 1 and stay far below this, so those
37/// bits are never zero, as a host's source keys ([`RenderCacheKey::source`])
38/// leave them, and the two never meet.
39const TEXT_CACHE_DOMAIN: u64 = 0x6361_7074_696f_6e73;
40
41/// The next font set's identity: each store, and each font a store adds,
42/// takes one, so no two font sets that might draw text differently in one
43/// process share a text cache key.
44static NEXT_FONT_SET: AtomicU64 = AtomicU64::new(1);
45
46/// How many rasterized texts a host keeps between frames; the current
47/// frame's are kept however many it shows.
48const RETAINED_TEXTS: usize = 16;
49
50/// How many resolved layouts a host keeps between frames; the current
51/// frame's are kept however many it shows.
52const RETAINED_LAYOUTS: usize = 8;
53
54/// How many shaped texts a host keeps for layout, least recently used
55/// first out.
56const RETAINED_SHAPED: usize = 256;
57
58/// Where [`TextTextures`] keeps the pixels of a rasterized text: the GPU
59/// renderer uploads them to a texture, the CPU renderer keeps them.
60pub trait TextUpload {
61    /// Takes `rgba8`, premultiplied sRGB-encoded RGBA of `size` with rows
62    /// packed, as the renderer's source for that text.
63    ///
64    /// # Errors
65    /// Returns [`RenderError::InvalidImageData`] when `rgba8` does not hold
66    /// `size`, or the GPU's upload failure.
67    fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError>;
68}
69
70/// The pixels of one prepared text, as the renderer that drew it keeps
71/// them.
72#[derive(Debug)]
73pub struct TextPixels(TextStorage);
74
75#[derive(Debug)]
76enum TextStorage {
77    Gpu(UploadTexture),
78    Cpu(CpuSource),
79}
80
81impl TextPixels {
82    /// The text as a graph source.
83    fn view(&self) -> SourceView<'_> {
84        match &self.0 {
85            TextStorage::Gpu(texture) => SourceView::from(texture),
86            TextStorage::Cpu(source) => SourceView::cpu(source),
87        }
88    }
89}
90
91impl TextUpload for Renderer {
92    fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError> {
93        self.upload_rgba8(size, rgba8)
94            .map(|texture| TextPixels(TextStorage::Gpu(texture)))
95    }
96}
97
98impl TextUpload for CpuRenderer {
99    fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError> {
100        let picture = CpuImage::from_premultiplied_rgba8(size, rgba8)?;
101        Ok(TextPixels(TextStorage::Cpu(CpuSource::image(Arc::new(
102            picture,
103        )))))
104    }
105}
106
107/// One prepared text: its texture, pixel size, and where its layout box
108/// lies in it.
109#[derive(Clone, Copy, Debug)]
110pub struct TextTexture<'a> {
111    /// Premultiplied, sRGB-encoded RGBA, as every source texture.
112    pub texture: SourceView<'a>,
113    /// Width in pixels.
114    pub width: u32,
115    /// Height in pixels.
116    pub height: u32,
117    /// The margin, in pixels, the same on both sides of each axis, that the
118    /// texture adds around the text and its decorations for ink reaching
119    /// past them: an accent above the first line, say. Placement leaves it
120    /// out, so such ink never moves the text.
121    pub margin: [u32; 2],
122    /// The key the renderer's result cache holds results drawn from this
123    /// texture under, or `None` to keep every frame showing it out of that
124    /// cache. It is the provider's promise that the texture's pixels depend
125    /// on nothing it leaves out, as [`crate::FrameProvider::source_key`] is
126    /// for a source: [`TextTextures`] keys each text by its [`text_key`] and
127    /// the fonts it was drawn with.
128    pub cache_key: Option<RenderCacheKey>,
129}
130
131/// Why text could not be prepared.
132#[derive(Clone, Debug, Eq, PartialEq)]
133#[non_exhaustive]
134pub enum TextError {
135    /// The text host refused: no fonts registered, a malformed font, or a
136    /// text too large to rasterize.
137    Raster(TextRasterError),
138    /// The rasterized text could not be uploaded.
139    Render(RenderError),
140}
141
142impl fmt::Display for TextError {
143    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144        match self {
145            Self::Raster(TextRasterError::NoFonts) => {
146                formatter.write_str("the project shows text, but no fonts were registered")
147            }
148            Self::Raster(error) => write!(formatter, "text failed: {error}"),
149            Self::Render(error) => write!(formatter, "text upload failed: {error}"),
150        }
151    }
152}
153
154impl std::error::Error for TextError {}
155
156struct TextEntry {
157    texture: TextPixels,
158    width: u32,
159    height: u32,
160    margin: [u32; 2],
161    /// Where the layout box's top-left corner lies in the texture.
162    corner: [u32; 2],
163    last_used: u64,
164}
165
166/// A layout resolved at one size, with the requests of its texts, to
167/// rasterize again one the text store has let go.
168struct LayoutEntry {
169    prepared: Arc<PreparedLayout>,
170    requests: Vec<(u64, TextRasterRequest)>,
171    last_used: u64,
172}
173
174/// A text shaped for layout, and when it was last asked for.
175struct ShapedEntry {
176    shaped: Arc<ShapedText>,
177    last_used: u64,
178}
179
180/// Fonts and the texts rasterized with them, keyed by content and render
181/// size.
182pub struct TextTextures {
183    rasterizer: TextRasterizer,
184    /// The identity of the fonts the texts are drawn with, from
185    /// [`NEXT_FONT_SET`].
186    font_set: u64,
187    entries: HashMap<u64, TextEntry>,
188    /// Resolved layouts by [`layout_key`].
189    layouts: HashMap<u128, LayoutEntry>,
190    /// Texts shaped for layout, by [`shaped_key`].
191    shaped: HashMap<u64, ShapedEntry>,
192    /// How many layouts have been resolved: a static layout resolves once.
193    resolves: u64,
194    frame: u64,
195}
196
197impl fmt::Debug for TextTextures {
198    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
199        formatter
200            .debug_struct("TextTextures")
201            .field("fonts", &self.rasterizer.font_count())
202            .field("texts", &self.entries.len())
203            .field("layouts", &self.layouts.len())
204            .finish_non_exhaustive()
205    }
206}
207
208impl Default for TextTextures {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214impl TextTextures {
215    /// No fonts and no texts.
216    #[must_use]
217    pub fn new() -> Self {
218        Self {
219            rasterizer: TextRasterizer::empty(),
220            font_set: NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed),
221            entries: HashMap::new(),
222            layouts: HashMap::new(),
223            shaped: HashMap::new(),
224            resolves: 0,
225            frame: 0,
226        }
227    }
228
229    /// Appends an OpenType font to the fallback stack text draws with. Texts
230    /// already rasterized are dropped.
231    ///
232    /// # Errors
233    /// Returns [`TextError::Raster`] for malformed font bytes.
234    pub fn add_font(
235        &mut self,
236        family: impl Into<String>,
237        bytes: impl Into<Arc<[u8]>>,
238        face_index: u32,
239    ) -> Result<(), TextError> {
240        self.rasterizer
241            .add_font(TextFont {
242                family: family.into(),
243                bytes: bytes.into(),
244                face_index,
245            })
246            .map_err(TextError::Raster)?;
247        self.entries.clear();
248        self.layouts.clear();
249        self.shaped.clear();
250        // The same text may now draw other glyphs.
251        self.font_set = NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed);
252        Ok(())
253    }
254
255    /// Fonts in the fallback stack.
256    #[must_use]
257    pub fn font_count(&self) -> usize {
258        self.rasterizer.font_count()
259    }
260
261    /// Rasterizes and uploads every text `frame` shows at `size` that is not
262    /// already held: its captions and the text generators its clips draw.
263    /// Resolves every layout it draws that is not already held, and
264    /// rasterizes its texts. Forgets the least recently shown beyond a small
265    /// number.
266    ///
267    /// # Errors
268    /// Returns [`TextError`] when a text cannot be rasterized or uploaded;
269    /// with no fonts registered, only if the frame shows one.
270    pub fn prepare(
271        &mut self,
272        renderer: &dyn TextUpload,
273        frame: &FrameDesc,
274        size: RenderSize,
275    ) -> Result<(), TextError> {
276        self.frame += 1;
277        let picture = picture(size);
278        for (text, style) in frame_texts(frame) {
279            let key = text_key(text, style, picture);
280            if let Some(entry) = self.entries.get_mut(&key) {
281                entry.last_used = self.frame;
282                continue;
283            }
284            let request = text_request(text, style, picture, key);
285            self.rasterize(renderer, key, &request)?;
286        }
287        for (plan, time) in frame_layouts(frame) {
288            self.prepare_layout(renderer, plan, time, size)?;
289        }
290        while self.layouts.len() > RETAINED_LAYOUTS {
291            let Some(oldest) = self
292                .layouts
293                .iter()
294                .filter(|(_, entry)| entry.last_used < self.frame)
295                .min_by_key(|(_, entry)| entry.last_used)
296                .map(|(key, _)| *key)
297            else {
298                break;
299            };
300            self.layouts.remove(&oldest);
301        }
302        while self.entries.len() > RETAINED_TEXTS {
303            let Some(oldest) = self
304                .entries
305                .iter()
306                .filter(|(_, entry)| entry.last_used < self.frame)
307                .min_by_key(|(_, entry)| entry.last_used)
308                .map(|(key, _)| *key)
309            else {
310                break;
311            };
312            self.entries.remove(&oldest);
313        }
314        Ok(())
315    }
316
317    /// Rasterizes `request` and uploads it as the text with `key`.
318    fn rasterize(
319        &mut self,
320        renderer: &dyn TextUpload,
321        key: u64,
322        request: &TextRasterRequest,
323    ) -> Result<(), TextError> {
324        let raster = self
325            .rasterizer
326            .rasterize(request)
327            .map_err(TextError::Raster)?;
328        // Each text is rasterized once per key; keep no copy.
329        self.rasterizer.invalidate(key);
330        let texture_size = &raster.descriptor.texture;
331        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
332        let corner =
333            [raster.descriptor.layout_x, raster.descriptor.layout_y].map(|corner| corner as u32);
334        // The anchor places the decorated text; the margin, which holds only ink reaching past
335        // it, takes no part in placement.
336        let margin = corner.map(|corner| corner.saturating_sub(request.decoration_margin()));
337        let texture = renderer
338            .upload_text(
339                RenderSize::new(texture_size.width, texture_size.height)
340                    .map_err(TextError::Render)?,
341                &raster.pixels.rgba8,
342            )
343            .map_err(TextError::Render)?;
344        self.entries.insert(
345            key,
346            TextEntry {
347                texture,
348                width: texture_size.width,
349                height: texture_size.height,
350                margin,
351                corner,
352                last_used: self.frame,
353            },
354        );
355        Ok(())
356    }
357
358    /// Resolves `plan` at picture time `time` and `size`, and rasterizes its
359    /// texts, unless that layout is already held: then only texts the store
360    /// let go are rasterized again.
361    fn prepare_layout(
362        &mut self,
363        renderer: &dyn TextUpload,
364        plan: &LayoutPlan,
365        time: alpha2_time::MediaTime,
366        size: RenderSize,
367    ) -> Result<(), TextError> {
368        let evaluated = plan.evaluate(time);
369        let key = layout_key(evaluated.key, size);
370        if let Some(entry) = self.layouts.get_mut(&key) {
371            entry.last_used = self.frame;
372            let requests = entry.requests.clone();
373            for (text, request) in &requests {
374                match self.entries.get_mut(text) {
375                    Some(held) => held.last_used = self.frame,
376                    None => self.rasterize(renderer, *text, request)?,
377                }
378            }
379            return Ok(());
380        }
381        self.resolves += 1;
382        let frame = self.frame;
383        let picture = picture(size);
384        let shaped =
385            alpha2_layout::shape_texts(&evaluated, picture, |request| self.shape(request, frame))
386                .map_err(TextError::Raster)?;
387        let resolved = alpha2_layout::resolve(&evaluated, picture, &shaped);
388        let (texts, requests) = alpha2_layout::layout_texts(&evaluated, &resolved, picture);
389        let mut boxes = Vec::with_capacity(texts.len());
390        for text in &texts {
391            boxes.push(match text {
392                None => None,
393                Some(text_key) => {
394                    let request = &requests
395                        .iter()
396                        .find(|(key, _)| key == text_key)
397                        .expect("a request per text")
398                        .1;
399                    match self.entries.get_mut(text_key) {
400                        Some(held) => held.last_used = self.frame,
401                        None => self.rasterize(renderer, *text_key, request)?,
402                    }
403                    let entry = &self.entries[text_key];
404                    Some(crate::layout::paint::TextBox {
405                        corner: entry.corner,
406                        size: [entry.width, entry.height],
407                    })
408                }
409            });
410        }
411        let items = crate::layout::paint::paint_list(&evaluated, &resolved, size, &boxes);
412        self.layouts.insert(
413            key,
414            LayoutEntry {
415                prepared: Arc::new(PreparedLayout { items, texts }),
416                requests,
417                last_used: self.frame,
418            },
419        );
420        Ok(())
421    }
422
423    /// `request`'s text shaped for layout, from the cache when it holds it.
424    fn shape(
425        &mut self,
426        request: &TextRasterRequest,
427        frame: u64,
428    ) -> Result<Arc<ShapedText>, TextRasterError> {
429        let key = shaped_key(request);
430        if let Some(entry) = self.shaped.get_mut(&key) {
431            entry.last_used = frame;
432            return Ok(Arc::clone(&entry.shaped));
433        }
434        let shaped = Arc::new(self.rasterizer.shape(request)?);
435        if self.shaped.len() >= RETAINED_SHAPED
436            && let Some(oldest) = self
437                .shaped
438                .iter()
439                .min_by_key(|(_, entry)| entry.last_used)
440                .map(|(key, _)| *key)
441        {
442            self.shaped.remove(&oldest);
443        }
444        self.shaped.insert(
445            key,
446            ShapedEntry {
447                shaped: Arc::clone(&shaped),
448                last_used: frame,
449            },
450        );
451        Ok(shaped)
452    }
453
454    /// How many layouts have been resolved, held or not.
455    #[cfg(test)]
456    #[must_use]
457    pub(crate) const fn layout_resolves(&self) -> u64 {
458        self.resolves
459    }
460
461    /// The prepared layout with `key`, as [`layout_key`] names it.
462    #[must_use]
463    pub fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
464        self.layouts
465            .get(&key)
466            .map(|entry| Arc::clone(&entry.prepared))
467    }
468
469    /// Where each node of `root` lies on a picture of `size` at picture
470    /// time `time`, its pixels those of a `canvas` canvas, laid out with
471    /// these fonts: [`crate::measure_layout`].
472    ///
473    /// # Errors
474    /// Returns [`TextError::Raster`] as [`Self::prepare`] would for its
475    /// texts.
476    pub fn measure_layout(
477        &self,
478        root: &alpha2_model::LayoutNode,
479        time: alpha2_time::MediaTime,
480        size: RenderSize,
481        canvas: (u32, u32),
482    ) -> Result<crate::LayoutBounds, TextError> {
483        crate::measure_layout(&self.rasterizer, root, time, picture(size), canvas)
484            .map_err(TextError::Raster)
485    }
486
487    /// [`Self::prepare`] for the frame of `project` at `time`, rendered at
488    /// `size`; nothing to do, and no evaluation, when the project draws no
489    /// text ([`crate::EngineProject::draws_text`]).
490    ///
491    /// # Errors
492    /// Returns [`crate::EngineError::Evaluate`] or
493    /// [`crate::EngineError::Text`].
494    pub fn prepare_at(
495        &mut self,
496        renderer: &dyn TextUpload,
497        project: &crate::EngineProject,
498        time: alpha2_time::MediaTime,
499        size: RenderSize,
500    ) -> Result<(), crate::EngineError> {
501        if !project.draws_text() {
502            return Ok(());
503        }
504        let frame = project.evaluate(time)?;
505        self.prepare(renderer, &frame, size)
506            .map_err(crate::EngineError::Text)
507    }
508
509    /// Where `text` in `style` lies on a canvas of `size`, drawn with these
510    /// fonts: [`crate::measure_text`].
511    ///
512    /// # Errors
513    /// Returns [`TextError::Raster`] as [`Self::prepare`] would for the text.
514    pub fn measure(
515        &self,
516        text: &str,
517        style: &TextStyle,
518        size: RenderSize,
519    ) -> Result<TextBounds, TextError> {
520        crate::measure_text(&self.rasterizer, text, style, picture(size)).map_err(TextError::Raster)
521    }
522
523    /// The prepared text with `key`, as [`crate::text_key`] names it.
524    #[must_use]
525    pub fn get(&self, key: u64) -> Option<TextTexture<'_>> {
526        self.entries.get(&key).map(|entry| TextTexture {
527            texture: entry.texture.view(),
528            width: entry.width,
529            height: entry.height,
530            margin: entry.margin,
531            cache_key: Some(self.cache_key(key)),
532        })
533    }
534
535    /// The result cache key of the text with `key`, drawn with these fonts:
536    /// the font set in the high half, beside the text domain, and the text
537    /// in the low half, so two keys are equal only for the same text drawn
538    /// with the same fonts.
539    fn cache_key(&self, key: u64) -> RenderCacheKey {
540        let high = TEXT_CACHE_DOMAIN ^ self.font_set;
541        RenderCacheKey::from_value((u128::from(high) << 64) | u128::from(key))
542    }
543}
544
545/// Every text `frame` may draw, with its style: each caption, then each text
546/// generator of a clip it draws, the clip's own source or a step of its
547/// compositions. A text drawn twice is named twice.
548pub(crate) fn frame_texts<'f>(
549    frame: &'f FrameDesc<'_>,
550) -> impl Iterator<Item = (&'f str, &'f TextStyle)> {
551    let captions = frame
552        .captions
553        .iter()
554        .map(|caption| (caption.text, caption.style));
555    let generators = frame
556        .visual
557        .iter()
558        .filter(|clip| frame.draws(clip))
559        .flat_map(|clip| {
560            let steps = clip
561                .composition
562                .iter()
563                .chain(&clip.source_composition)
564                .flat_map(|composition| &composition.plan().steps)
565                .filter_map(|step| match &step.op {
566                    StepOp::Generator { generator, .. } => Some(generator),
567                    _ => None,
568                });
569            clip.generator.into_iter().chain(steps)
570        })
571        .filter_map(|generator| match generator {
572            Generator::Text { text, style } => Some((text.as_str(), style)),
573            _ => None,
574        });
575    captions.chain(generators)
576}
577
578/// A host's frame provider together with its text store: frames come from
579/// the one and text from the other.
580pub struct FramesWithText<'a> {
581    frames: &'a dyn crate::FrameProvider,
582    texts: &'a TextTextures,
583}
584
585impl<'a> FramesWithText<'a> {
586    /// `frames` with the texts `texts` holds.
587    #[must_use]
588    pub fn new(frames: &'a dyn crate::FrameProvider, texts: &'a TextTextures) -> Self {
589        Self { frames, texts }
590    }
591}
592
593impl crate::FrameProvider for FramesWithText<'_> {
594    fn texture_for(
595        &self,
596        source: &alpha2_media::SourceId,
597        time: alpha2_time::MediaTime,
598    ) -> Option<SourceView<'_>> {
599        self.frames.texture_for(source, time)
600    }
601
602    fn pending_source_work(
603        &self,
604        source: &alpha2_media::SourceId,
605        time: alpha2_time::MediaTime,
606    ) -> Option<&alpha2_render::PendingSourceWork> {
607        self.frames.pending_source_work(source, time)
608    }
609
610    fn source_key(
611        &self,
612        source: &alpha2_media::SourceId,
613        time: alpha2_time::MediaTime,
614    ) -> Option<alpha2_render::RenderCacheKey> {
615        self.frames.source_key(source, time)
616    }
617
618    fn text(&self, key: u64) -> Option<TextTexture<'_>> {
619        self.texts.get(key)
620    }
621
622    fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
623        self.texts.layout(key)
624    }
625}
626
627/// Every layout `frame` may draw, with its picture time: each drawn clip's
628/// layout source at its source time, then each layout step of its
629/// compositions at the step's time plus its start.
630pub(crate) fn frame_layouts<'f>(
631    frame: &'f FrameDesc<'_>,
632) -> impl Iterator<Item = (&'f LayoutPlan, alpha2_time::MediaTime)> {
633    frame
634        .visual
635        .iter()
636        .filter(|clip| frame.draws(clip))
637        .flat_map(|clip| {
638            let own = clip
639                .plan
640                .and_then(GeneratorPlan::layout)
641                .map(|plan| (plan, clip.source_time));
642            let steps = clip
643                .composition
644                .iter()
645                .chain(&clip.source_composition)
646                .flat_map(|composition| {
647                    let time = composition.time();
648                    composition
649                        .plan()
650                        .steps
651                        .iter()
652                        .filter_map(move |step| match &step.op {
653                            StepOp::Generator {
654                                plan: Some(plan),
655                                start,
656                                ..
657                            } => plan.layout().map(|plan| {
658                                (
659                                    plan,
660                                    alpha2_time::MediaTime(
661                                        step.time(time).ticks().saturating_add(start.ticks()),
662                                    ),
663                                )
664                            }),
665                            _ => None,
666                        })
667                });
668            own.into_iter().chain(steps)
669        })
670}
671
672/// A layout's identity at a render size: its evaluated key and the size.
673/// Equal keys lay out and draw alike with the same fonts.
674#[must_use]
675pub fn layout_key(evaluated: u128, size: RenderSize) -> u128 {
676    let mut hash = alpha2_geometry::StableHash::new();
677    hash.bytes(b"layout");
678    hash.bytes(&evaluated.to_le_bytes());
679    hash.u32(size.width);
680    hash.u32(size.height);
681    hash.finish()
682}