1use 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_media::EmojiPolicy;
21use alpha2_model::{Generator, Rgba, TextAlign, 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
30const TEXT_CACHE_DOMAIN: u64 = 0x6361_7074_696f_6e73;
35
36static NEXT_FONT_SET: AtomicU64 = AtomicU64::new(1);
40
41const RETAINED_TEXTS: usize = 16;
44
45const RETAINED_LAYOUTS: usize = 8;
48
49const RETAINED_SHAPED: usize = 256;
52
53pub(crate) const MAX_FONT_PIXELS: f32 = 2_048.0;
55pub(crate) const MAX_OUTLINE_PIXELS: f32 = 64.0;
56const MAX_PADDING_PIXELS: f32 = 256.0;
57
58pub trait TextUpload {
61 fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError>;
68}
69
70#[derive(Debug)]
73pub struct TextPixels(TextStorage);
74
75#[derive(Debug)]
76enum TextStorage {
77 Gpu(UploadTexture),
78 Cpu(CpuSource),
79}
80
81impl TextPixels {
82 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#[derive(Clone, Copy, Debug)]
110pub struct TextTexture<'a> {
111 pub texture: SourceView<'a>,
113 pub width: u32,
115 pub height: u32,
117 pub margin: [u32; 2],
122 pub cache_key: Option<RenderCacheKey>,
129}
130
131#[derive(Copy, Clone, Debug, PartialEq)]
136pub struct TextBounds {
137 pub layout: CanvasRect,
141 pub content: CanvasRect,
146 pub ink: Option<CanvasRect>,
152 pub line_count: u32,
154}
155
156#[derive(Copy, Clone, Debug, PartialEq)]
158pub struct CanvasRect {
159 pub left: f32,
161 pub top: f32,
163 pub right: f32,
165 pub bottom: f32,
167}
168
169#[derive(Clone, Debug, Eq, PartialEq)]
171#[non_exhaustive]
172pub enum TextError {
173 Raster(TextRasterError),
176 Render(RenderError),
178}
179
180impl fmt::Display for TextError {
181 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
182 match self {
183 Self::Raster(TextRasterError::NoFonts) => {
184 formatter.write_str("the project shows text, but no fonts were registered")
185 }
186 Self::Raster(error) => write!(formatter, "text failed: {error}"),
187 Self::Render(error) => write!(formatter, "text upload failed: {error}"),
188 }
189 }
190}
191
192impl std::error::Error for TextError {}
193
194struct TextEntry {
195 texture: TextPixels,
196 width: u32,
197 height: u32,
198 margin: [u32; 2],
199 corner: [u32; 2],
201 last_used: u64,
202}
203
204struct LayoutEntry {
207 prepared: Arc<PreparedLayout>,
208 requests: Vec<(u64, TextRasterRequest)>,
209 last_used: u64,
210}
211
212struct ShapedEntry {
214 shaped: Arc<ShapedText>,
215 last_used: u64,
216}
217
218pub struct TextTextures {
221 rasterizer: TextRasterizer,
222 font_set: u64,
225 entries: HashMap<u64, TextEntry>,
226 layouts: HashMap<u128, LayoutEntry>,
228 shaped: HashMap<u64, ShapedEntry>,
230 resolves: u64,
232 frame: u64,
233}
234
235impl fmt::Debug for TextTextures {
236 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
237 formatter
238 .debug_struct("TextTextures")
239 .field("fonts", &self.rasterizer.font_count())
240 .field("texts", &self.entries.len())
241 .field("layouts", &self.layouts.len())
242 .finish_non_exhaustive()
243 }
244}
245
246impl Default for TextTextures {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252impl TextTextures {
253 #[must_use]
255 pub fn new() -> Self {
256 Self {
257 rasterizer: TextRasterizer::empty(),
258 font_set: NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed),
259 entries: HashMap::new(),
260 layouts: HashMap::new(),
261 shaped: HashMap::new(),
262 resolves: 0,
263 frame: 0,
264 }
265 }
266
267 pub fn add_font(
273 &mut self,
274 family: impl Into<String>,
275 bytes: impl Into<Arc<[u8]>>,
276 face_index: u32,
277 ) -> Result<(), TextError> {
278 self.rasterizer
279 .add_font(TextFont {
280 family: family.into(),
281 bytes: bytes.into(),
282 face_index,
283 })
284 .map_err(TextError::Raster)?;
285 self.entries.clear();
286 self.layouts.clear();
287 self.shaped.clear();
288 self.font_set = NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed);
290 Ok(())
291 }
292
293 #[must_use]
295 pub fn font_count(&self) -> usize {
296 self.rasterizer.font_count()
297 }
298
299 pub fn prepare(
309 &mut self,
310 renderer: &dyn TextUpload,
311 frame: &FrameDesc,
312 size: RenderSize,
313 ) -> Result<(), TextError> {
314 self.frame += 1;
315 for (text, style) in frame_texts(frame) {
316 let key = text_key(text, style, size);
317 if let Some(entry) = self.entries.get_mut(&key) {
318 entry.last_used = self.frame;
319 continue;
320 }
321 let request = text_request(text, style, size, key);
322 self.rasterize(renderer, key, &request)?;
323 }
324 for (plan, time) in frame_layouts(frame) {
325 self.prepare_layout(renderer, plan, time, size)?;
326 }
327 while self.layouts.len() > RETAINED_LAYOUTS {
328 let Some(oldest) = self
329 .layouts
330 .iter()
331 .filter(|(_, entry)| entry.last_used < self.frame)
332 .min_by_key(|(_, entry)| entry.last_used)
333 .map(|(key, _)| *key)
334 else {
335 break;
336 };
337 self.layouts.remove(&oldest);
338 }
339 while self.entries.len() > RETAINED_TEXTS {
340 let Some(oldest) = self
341 .entries
342 .iter()
343 .filter(|(_, entry)| entry.last_used < self.frame)
344 .min_by_key(|(_, entry)| entry.last_used)
345 .map(|(key, _)| *key)
346 else {
347 break;
348 };
349 self.entries.remove(&oldest);
350 }
351 Ok(())
352 }
353
354 fn rasterize(
356 &mut self,
357 renderer: &dyn TextUpload,
358 key: u64,
359 request: &TextRasterRequest,
360 ) -> Result<(), TextError> {
361 let raster = self
362 .rasterizer
363 .rasterize(request)
364 .map_err(TextError::Raster)?;
365 self.rasterizer.invalidate(key);
367 let texture_size = &raster.descriptor.texture;
368 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
369 let corner =
370 [raster.descriptor.layout_x, raster.descriptor.layout_y].map(|corner| corner as u32);
371 let margin = corner.map(|corner| corner.saturating_sub(request.decoration_margin()));
374 let texture = renderer
375 .upload_text(
376 RenderSize::new(texture_size.width, texture_size.height)
377 .map_err(TextError::Render)?,
378 &raster.pixels.rgba8,
379 )
380 .map_err(TextError::Render)?;
381 self.entries.insert(
382 key,
383 TextEntry {
384 texture,
385 width: texture_size.width,
386 height: texture_size.height,
387 margin,
388 corner,
389 last_used: self.frame,
390 },
391 );
392 Ok(())
393 }
394
395 fn prepare_layout(
399 &mut self,
400 renderer: &dyn TextUpload,
401 plan: &LayoutPlan,
402 time: alpha2_time::MediaTime,
403 size: RenderSize,
404 ) -> Result<(), TextError> {
405 let evaluated = plan.evaluate(time);
406 let key = layout_key(evaluated.key, size);
407 if let Some(entry) = self.layouts.get_mut(&key) {
408 entry.last_used = self.frame;
409 let requests = entry.requests.clone();
410 for (text, request) in &requests {
411 match self.entries.get_mut(text) {
412 Some(held) => held.last_used = self.frame,
413 None => self.rasterize(renderer, *text, request)?,
414 }
415 }
416 return Ok(());
417 }
418 self.resolves += 1;
419 let frame = self.frame;
420 let shaped =
421 crate::layout::shape_texts(&evaluated, size, |request| self.shape(request, frame))?;
422 let resolved = crate::layout::resolve(&evaluated, size, &shaped);
423 let (texts, requests) = crate::layout::layout_texts(&evaluated, &resolved, size);
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 request = &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, request)?,
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(
460 &mut self,
461 request: &TextRasterRequest,
462 frame: u64,
463 ) -> Result<Arc<ShapedText>, TextRasterError> {
464 let key = shaped_key(request);
465 if let Some(entry) = self.shaped.get_mut(&key) {
466 entry.last_used = frame;
467 return Ok(Arc::clone(&entry.shaped));
468 }
469 let shaped = Arc::new(self.rasterizer.shape(request)?);
470 if self.shaped.len() >= RETAINED_SHAPED
471 && let Some(oldest) = self
472 .shaped
473 .iter()
474 .min_by_key(|(_, entry)| entry.last_used)
475 .map(|(key, _)| *key)
476 {
477 self.shaped.remove(&oldest);
478 }
479 self.shaped.insert(
480 key,
481 ShapedEntry {
482 shaped: Arc::clone(&shaped),
483 last_used: frame,
484 },
485 );
486 Ok(shaped)
487 }
488
489 #[cfg(test)]
491 #[must_use]
492 pub(crate) const fn layout_resolves(&self) -> u64 {
493 self.resolves
494 }
495
496 #[must_use]
498 pub fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
499 self.layouts
500 .get(&key)
501 .map(|entry| Arc::clone(&entry.prepared))
502 }
503
504 pub fn measure_layout(
512 &self,
513 root: &alpha2_model::LayoutNode,
514 time: alpha2_time::MediaTime,
515 size: RenderSize,
516 canvas: (u32, u32),
517 ) -> Result<crate::LayoutBounds, TextError> {
518 crate::measure_layout(&self.rasterizer, root, time, size, canvas)
519 }
520
521 pub fn prepare_at(
529 &mut self,
530 renderer: &dyn TextUpload,
531 project: &crate::EngineProject,
532 time: alpha2_time::MediaTime,
533 size: RenderSize,
534 ) -> Result<(), crate::EngineError> {
535 if !project.draws_text() {
536 return Ok(());
537 }
538 let frame = project.evaluate(time)?;
539 self.prepare(renderer, &frame, size)
540 .map_err(crate::EngineError::Text)
541 }
542
543 pub fn measure(
549 &self,
550 text: &str,
551 style: &TextStyle,
552 size: RenderSize,
553 ) -> Result<TextBounds, TextError> {
554 measure_text(&self.rasterizer, text, style, size)
555 }
556
557 #[must_use]
559 pub fn get(&self, key: u64) -> Option<TextTexture<'_>> {
560 self.entries.get(&key).map(|entry| TextTexture {
561 texture: entry.texture.view(),
562 width: entry.width,
563 height: entry.height,
564 margin: entry.margin,
565 cache_key: Some(self.cache_key(key)),
566 })
567 }
568
569 fn cache_key(&self, key: u64) -> RenderCacheKey {
574 let high = TEXT_CACHE_DOMAIN ^ self.font_set;
575 RenderCacheKey::from_value((u128::from(high) << 64) | u128::from(key))
576 }
577}
578
579pub(crate) fn frame_texts<'f>(
583 frame: &'f FrameDesc<'_>,
584) -> impl Iterator<Item = (&'f str, &'f TextStyle)> {
585 let captions = frame
586 .captions
587 .iter()
588 .map(|caption| (caption.text, caption.style));
589 let generators = frame
590 .visual
591 .iter()
592 .filter(|clip| frame.draws(clip))
593 .flat_map(|clip| {
594 let steps = clip
595 .composition
596 .iter()
597 .chain(&clip.source_composition)
598 .flat_map(|composition| &composition.plan().steps)
599 .filter_map(|step| match &step.op {
600 StepOp::Generator { generator, .. } => Some(generator),
601 _ => None,
602 });
603 clip.generator.into_iter().chain(steps)
604 })
605 .filter_map(|generator| match generator {
606 Generator::Text { text, style } => Some((text.as_str(), style)),
607 _ => None,
608 });
609 captions.chain(generators)
610}
611
612pub struct FramesWithText<'a> {
615 frames: &'a dyn crate::FrameProvider,
616 texts: &'a TextTextures,
617}
618
619impl<'a> FramesWithText<'a> {
620 #[must_use]
622 pub fn new(frames: &'a dyn crate::FrameProvider, texts: &'a TextTextures) -> Self {
623 Self { frames, texts }
624 }
625}
626
627impl crate::FrameProvider for FramesWithText<'_> {
628 fn texture_for(
629 &self,
630 source: &alpha2_media::SourceId,
631 time: alpha2_time::MediaTime,
632 ) -> Option<SourceView<'_>> {
633 self.frames.texture_for(source, time)
634 }
635
636 fn pending_source_work(
637 &self,
638 source: &alpha2_media::SourceId,
639 time: alpha2_time::MediaTime,
640 ) -> Option<&alpha2_render::PendingSourceWork> {
641 self.frames.pending_source_work(source, time)
642 }
643
644 fn source_key(
645 &self,
646 source: &alpha2_media::SourceId,
647 time: alpha2_time::MediaTime,
648 ) -> Option<alpha2_render::RenderCacheKey> {
649 self.frames.source_key(source, time)
650 }
651
652 fn text(&self, key: u64) -> Option<TextTexture<'_>> {
653 self.texts.get(key)
654 }
655
656 fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
657 self.texts.layout(key)
658 }
659}
660
661pub(crate) fn frame_layouts<'f>(
665 frame: &'f FrameDesc<'_>,
666) -> impl Iterator<Item = (&'f LayoutPlan, alpha2_time::MediaTime)> {
667 frame
668 .visual
669 .iter()
670 .filter(|clip| frame.draws(clip))
671 .flat_map(|clip| {
672 let own = clip
673 .plan
674 .and_then(GeneratorPlan::layout)
675 .map(|plan| (plan, clip.source_time));
676 let steps = clip
677 .composition
678 .iter()
679 .chain(&clip.source_composition)
680 .flat_map(|composition| {
681 let time = composition.time();
682 composition
683 .plan()
684 .steps
685 .iter()
686 .filter_map(move |step| match &step.op {
687 StepOp::Generator {
688 plan: Some(plan),
689 start,
690 ..
691 } => plan.layout().map(|plan| {
692 (
693 plan,
694 alpha2_time::MediaTime(
695 step.time(time).ticks().saturating_add(start.ticks()),
696 ),
697 )
698 }),
699 _ => None,
700 })
701 });
702 own.into_iter().chain(steps)
703 })
704}
705
706#[must_use]
709pub fn layout_key(evaluated: u128, size: RenderSize) -> u128 {
710 let mut hash = alpha2_geometry::StableHash::new();
711 hash.bytes(b"layout");
712 hash.bytes(&evaluated.to_le_bytes());
713 hash.u32(size.width);
714 hash.u32(size.height);
715 hash.finish()
716}
717
718pub(crate) fn layout_text_key(request: &TextRasterRequest) -> u64 {
721 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
722 let mut feed = |bytes: &[u8]| {
723 for byte in bytes {
724 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
725 }
726 };
727 feed(&[0x4c]);
729 feed_request(&mut feed, request);
730 hash.max(1)
731}
732
733fn shaped_key(request: &TextRasterRequest) -> u64 {
735 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
736 let mut feed = |bytes: &[u8]| {
737 for byte in bytes {
738 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
739 }
740 };
741 feed(request.text.as_bytes());
742 feed(&[0xff]);
743 feed(
744 request
745 .font_family
746 .as_deref()
747 .unwrap_or_default()
748 .as_bytes(),
749 );
750 feed(&[0xff, u8::from(request.font_family.is_some())]);
751 feed(&request.font_size.to_bits().to_le_bytes());
752 feed(&request.line_height.to_bits().to_le_bytes());
753 hash
754}
755
756fn feed_request(feed: &mut impl FnMut(&[u8]), request: &TextRasterRequest) {
757 feed(request.text.as_bytes());
758 feed(&[0xff]);
759 feed(
760 request
761 .font_family
762 .as_deref()
763 .unwrap_or_default()
764 .as_bytes(),
765 );
766 feed(&[0xff, u8::from(request.font_family.is_some())]);
767 for value in [request.font_size, request.max_width, request.line_height] {
768 feed(&value.to_bits().to_le_bytes());
769 }
770 feed(&request.color);
771 feed(&[match request.align {
772 alpha2_media::TextAlign::Start => 0,
773 alpha2_media::TextAlign::Center => 1,
774 alpha2_media::TextAlign::End => 2,
775 }]);
776 match request.outline {
777 Some(outline) => {
778 feed(&[1]);
779 feed(&outline.color);
780 feed(&outline.width.to_bits().to_le_bytes());
781 }
782 None => feed(&[0]),
783 }
784}
785
786#[must_use]
789pub fn text_key(text: &str, style: &TextStyle, size: RenderSize) -> u64 {
790 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
791 let mut feed = |bytes: &[u8]| {
792 for byte in bytes {
793 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
794 }
795 };
796 feed(text.as_bytes());
797 feed(&[0xff]);
798 feed(style.font_family.as_deref().unwrap_or_default().as_bytes());
800 feed(&[0xff, u8::from(style.font_family.is_some())]);
801 for value in [
802 style.font_size,
803 style.line_height,
804 style.max_width,
805 style.position.x,
806 style.position.y,
807 style.anchor.x,
808 style.anchor.y,
809 ] {
810 feed(&value.to_bits().to_le_bytes());
811 }
812 feed(&rgba8(style.color));
813 feed(&[match style.align {
814 TextAlign::Start => 0,
815 TextAlign::Center => 1,
816 TextAlign::End => 2,
817 }]);
818 match style.outline {
819 Some(outline) => {
820 feed(&[1]);
821 feed(&rgba8(outline.color));
822 feed(&outline.width.to_bits().to_le_bytes());
823 }
824 None => feed(&[0]),
825 }
826 match style.background {
827 Some(background) => {
828 feed(&[1]);
829 feed(&rgba8(background.color));
830 feed(&background.padding.to_bits().to_le_bytes());
831 }
832 None => feed(&[0]),
833 }
834 feed(&size.width.to_le_bytes());
835 feed(&size.height.to_le_bytes());
836 hash.max(1)
837}
838
839#[allow(clippy::cast_precision_loss)]
849pub fn measure_text(
850 rasterizer: &TextRasterizer,
851 text: &str,
852 style: &TextStyle,
853 size: RenderSize,
854) -> Result<TextBounds, TextError> {
855 let request = text_request(text, style, size, text_key(text, style, size));
856 let measure = rasterizer.measure(&request).map_err(TextError::Raster)?;
857 let (width, height) = (size.width as f32, size.height as f32);
858 let margin = measure.decoration_margin as f32;
859 let (box_width, box_height) = (measure.box_width as f32, measure.box_height as f32);
860 let origin = [
863 style.position.x * width - style.anchor.x * (box_width + 2.0 * margin) + margin,
864 style.position.y * height - style.anchor.y * (box_height + 2.0 * margin) + margin,
865 ];
866 let rect = |[left, top, right, bottom]: [f32; 4]| CanvasRect {
867 left: (origin[0] + left) / width,
868 top: (origin[1] + top) / height,
869 right: (origin[0] + right) / width,
870 bottom: (origin[1] + bottom) / height,
871 };
872 Ok(TextBounds {
873 layout: rect([-margin, -margin, box_width + margin, box_height + margin]),
874 content: rect(measure.content),
875 ink: measure.ink.map(rect),
876 line_count: measure.line_count,
877 })
878}
879
880#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
882pub(crate) fn rgba8(color: Rgba) -> [u8; 4] {
883 [color.red, color.green, color.blue, color.alpha]
884 .map(|value| (value.clamp(0.0, 1.0) * 255.0).round() as u8)
885}
886
887#[allow(clippy::cast_precision_loss)]
890fn text_request(text: &str, style: &TextStyle, size: RenderSize, key: u64) -> TextRasterRequest {
891 let font_size = (style.font_size * size.height as f32).clamp(1.0, MAX_FONT_PIXELS);
892 TextRasterRequest {
893 text: text.to_owned(),
894 font_size,
895 max_width: (style.max_width * size.width as f32).max(1.0),
896 line_height: style.line_height,
897 align: match style.align {
898 TextAlign::Start => alpha2_media::TextAlign::Start,
899 TextAlign::Center => alpha2_media::TextAlign::Center,
900 TextAlign::End => alpha2_media::TextAlign::End,
901 },
902 color: rgba8(style.color),
903 emoji_policy: EmojiPolicy::Color,
904 content_version: key,
905 outline: style.outline.map(|outline| alpha2_text::TextOutline {
906 color: rgba8(outline.color),
907 width: (outline.width * font_size).clamp(f32::MIN_POSITIVE, MAX_OUTLINE_PIXELS),
908 }),
909 background: style
910 .background
911 .map(|background| alpha2_text::TextBackground {
912 color: rgba8(background.color),
913 padding: (background.padding * font_size).clamp(0.0, MAX_PADDING_PIXELS),
914 }),
915 font_family: style.font_family.clone(),
916 }
917}
918
919#[cfg(test)]
920#[path = "text_tests.rs"]
921mod tests;