1use 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
31const TEXT_CACHE_DOMAIN: u64 = 0x6361_7074_696f_6e73;
34
35static NEXT_FONT_SET: AtomicU64 = AtomicU64::new(1);
37
38const RETAINED_TEXTS: usize = 16;
40
41const RETAINED_LAYOUTS: usize = 8;
43
44const RETAINED_SHAPED: usize = 256;
46
47pub trait TextUpload {
49 fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError>;
55}
56
57#[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#[derive(Clone, Copy, Debug)]
94pub struct TextTexture<'a> {
95 pub texture: SourceView<'a>,
97 pub width: u32,
99 pub height: u32,
101 pub margin: [u32; 2],
104 pub cache_key: Option<RenderCacheKey>,
107 pub background: Option<(SourceView<'a>, Option<RenderCacheKey>)>,
110 pub shadows: &'a [ShadowPixels],
112}
113
114#[derive(Clone, Debug, Eq, PartialEq)]
116#[non_exhaustive]
117pub enum TextError {
118 Raster(TextRasterError),
120 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 content: u64,
145 width: u32,
146 height: u32,
147 margin: [u32; 2],
148 corner: [u32; 2],
150 last_used: u64,
151}
152
153struct 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
165pub struct TextTextures {
167 rasterizer: TextRasterizer,
168 font_set: u64,
170 entries: HashMap<u64, TextEntry>,
171 layouts: HashMap<u128, LayoutEntry>,
173 shaped: HashMap<u64, ShapedEntry>,
175 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 #[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 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 #[must_use]
238 pub fn font_count(&self) -> usize {
239 self.rasterizer.font_count()
240 }
241
242 #[must_use]
245 pub fn unmatched_font_families(&self) -> Vec<String> {
246 self.rasterizer.unmatched_families()
247 }
248
249 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 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 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 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 #[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 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 #[cfg(test)]
490 #[must_use]
491 pub(crate) const fn layout_resolves(&self) -> u64 {
492 self.resolves
493 }
494
495 #[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 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 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 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 #[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 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 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#[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
592pub(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
637pub(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
645fn 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
654fn 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
666pub struct FramesWithText<'a> {
668 frames: &'a dyn crate::FrameProvider,
669 texts: &'a TextTextures,
670}
671
672impl<'a> FramesWithText<'a> {
673 #[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
714pub(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#[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}