Skip to main content

alpha2_engine/
text.rs

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