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::{RenderCacheKey, RenderError, RenderSize, Renderer, SourceView, UploadTexture};
23use alpha2_text::{ShapedText, TextFont, TextRasterError, TextRasterRequest, TextRasterizer};
24
25use crate::layout::PreparedLayout;
26
27const TEXT_CACHE_DOMAIN: u64 = 0x6361_7074_696f_6e73;
32
33static NEXT_FONT_SET: AtomicU64 = AtomicU64::new(1);
37
38const RETAINED_TEXTS: usize = 16;
41
42const RETAINED_LAYOUTS: usize = 8;
45
46const RETAINED_SHAPED: usize = 256;
49
50pub(crate) const MAX_FONT_PIXELS: f32 = 2_048.0;
52pub(crate) const MAX_OUTLINE_PIXELS: f32 = 64.0;
53const MAX_PADDING_PIXELS: f32 = 256.0;
54
55#[derive(Clone, Copy, Debug)]
58pub struct TextTexture<'a> {
59 pub texture: SourceView<'a>,
61 pub width: u32,
63 pub height: u32,
65 pub margin: [u32; 2],
70 pub cache_key: Option<RenderCacheKey>,
77}
78
79#[derive(Copy, Clone, Debug, PartialEq)]
84pub struct TextBounds {
85 pub layout: CanvasRect,
89 pub content: CanvasRect,
94 pub ink: Option<CanvasRect>,
100 pub line_count: u32,
102}
103
104#[derive(Copy, Clone, Debug, PartialEq)]
106pub struct CanvasRect {
107 pub left: f32,
109 pub top: f32,
111 pub right: f32,
113 pub bottom: f32,
115}
116
117#[derive(Clone, Debug, Eq, PartialEq)]
119#[non_exhaustive]
120pub enum TextError {
121 Raster(TextRasterError),
124 Render(RenderError),
126}
127
128impl fmt::Display for TextError {
129 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::Raster(TextRasterError::NoFonts) => {
132 formatter.write_str("the project shows text, but no fonts were registered")
133 }
134 Self::Raster(error) => write!(formatter, "text failed: {error}"),
135 Self::Render(error) => write!(formatter, "text upload failed: {error}"),
136 }
137 }
138}
139
140impl std::error::Error for TextError {}
141
142struct TextEntry {
143 texture: UploadTexture,
144 width: u32,
145 height: u32,
146 margin: [u32; 2],
147 corner: [u32; 2],
149 last_used: u64,
150}
151
152struct LayoutEntry {
155 prepared: Arc<PreparedLayout>,
156 requests: Vec<(u64, TextRasterRequest)>,
157 last_used: u64,
158}
159
160struct ShapedEntry {
162 shaped: Arc<ShapedText>,
163 last_used: u64,
164}
165
166pub struct TextTextures {
169 rasterizer: TextRasterizer,
170 font_set: u64,
173 entries: HashMap<u64, TextEntry>,
174 layouts: HashMap<u128, LayoutEntry>,
176 shaped: HashMap<u64, ShapedEntry>,
178 resolves: u64,
180 frame: u64,
181}
182
183impl fmt::Debug for TextTextures {
184 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185 formatter
186 .debug_struct("TextTextures")
187 .field("fonts", &self.rasterizer.font_count())
188 .field("texts", &self.entries.len())
189 .field("layouts", &self.layouts.len())
190 .finish_non_exhaustive()
191 }
192}
193
194impl Default for TextTextures {
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200impl TextTextures {
201 #[must_use]
203 pub fn new() -> Self {
204 Self {
205 rasterizer: TextRasterizer::empty(),
206 font_set: NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed),
207 entries: HashMap::new(),
208 layouts: HashMap::new(),
209 shaped: HashMap::new(),
210 resolves: 0,
211 frame: 0,
212 }
213 }
214
215 pub fn add_font(
221 &mut self,
222 family: impl Into<String>,
223 bytes: impl Into<Arc<[u8]>>,
224 face_index: u32,
225 ) -> Result<(), TextError> {
226 self.rasterizer
227 .add_font(TextFont {
228 family: family.into(),
229 bytes: bytes.into(),
230 face_index,
231 })
232 .map_err(TextError::Raster)?;
233 self.entries.clear();
234 self.layouts.clear();
235 self.shaped.clear();
236 self.font_set = NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed);
238 Ok(())
239 }
240
241 #[must_use]
243 pub fn font_count(&self) -> usize {
244 self.rasterizer.font_count()
245 }
246
247 pub fn prepare(
257 &mut self,
258 renderer: &Renderer,
259 frame: &FrameDesc,
260 size: RenderSize,
261 ) -> Result<(), TextError> {
262 self.frame += 1;
263 for (text, style) in frame_texts(frame) {
264 let key = text_key(text, style, size);
265 if let Some(entry) = self.entries.get_mut(&key) {
266 entry.last_used = self.frame;
267 continue;
268 }
269 let request = text_request(text, style, size, key);
270 self.rasterize(renderer, key, &request)?;
271 }
272 for (plan, time) in frame_layouts(frame) {
273 self.prepare_layout(renderer, plan, time, size)?;
274 }
275 while self.layouts.len() > RETAINED_LAYOUTS {
276 let Some(oldest) = self
277 .layouts
278 .iter()
279 .filter(|(_, entry)| entry.last_used < self.frame)
280 .min_by_key(|(_, entry)| entry.last_used)
281 .map(|(key, _)| *key)
282 else {
283 break;
284 };
285 self.layouts.remove(&oldest);
286 }
287 while self.entries.len() > RETAINED_TEXTS {
288 let Some(oldest) = self
289 .entries
290 .iter()
291 .filter(|(_, entry)| entry.last_used < self.frame)
292 .min_by_key(|(_, entry)| entry.last_used)
293 .map(|(key, _)| *key)
294 else {
295 break;
296 };
297 self.entries.remove(&oldest);
298 }
299 Ok(())
300 }
301
302 fn rasterize(
304 &mut self,
305 renderer: &Renderer,
306 key: u64,
307 request: &TextRasterRequest,
308 ) -> Result<(), TextError> {
309 let raster = self
310 .rasterizer
311 .rasterize(request)
312 .map_err(TextError::Raster)?;
313 self.rasterizer.invalidate(key);
315 let texture_size = &raster.descriptor.texture;
316 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
317 let corner =
318 [raster.descriptor.layout_x, raster.descriptor.layout_y].map(|corner| corner as u32);
319 let margin = corner.map(|corner| corner.saturating_sub(request.decoration_margin()));
322 let texture = renderer
323 .upload_rgba8(
324 RenderSize::new(texture_size.width, texture_size.height)
325 .map_err(TextError::Render)?,
326 &raster.pixels.rgba8,
327 )
328 .map_err(TextError::Render)?;
329 self.entries.insert(
330 key,
331 TextEntry {
332 texture,
333 width: texture_size.width,
334 height: texture_size.height,
335 margin,
336 corner,
337 last_used: self.frame,
338 },
339 );
340 Ok(())
341 }
342
343 fn prepare_layout(
347 &mut self,
348 renderer: &Renderer,
349 plan: &LayoutPlan,
350 time: alpha2_time::MediaTime,
351 size: RenderSize,
352 ) -> Result<(), TextError> {
353 let evaluated = plan.evaluate(time);
354 let key = layout_key(evaluated.key, size);
355 if let Some(entry) = self.layouts.get_mut(&key) {
356 entry.last_used = self.frame;
357 let requests = entry.requests.clone();
358 for (text, request) in &requests {
359 match self.entries.get_mut(text) {
360 Some(held) => held.last_used = self.frame,
361 None => self.rasterize(renderer, *text, request)?,
362 }
363 }
364 return Ok(());
365 }
366 self.resolves += 1;
367 let frame = self.frame;
368 let shaped =
369 crate::layout::shape_texts(&evaluated, size, |request| self.shape(request, frame))?;
370 let resolved = crate::layout::resolve(&evaluated, size, &shaped);
371 let (texts, requests) = crate::layout::layout_texts(&evaluated, &resolved, size);
372 let mut boxes = Vec::with_capacity(texts.len());
373 for text in &texts {
374 boxes.push(match text {
375 None => None,
376 Some(text_key) => {
377 let request = &requests
378 .iter()
379 .find(|(key, _)| key == text_key)
380 .expect("a request per text")
381 .1;
382 match self.entries.get_mut(text_key) {
383 Some(held) => held.last_used = self.frame,
384 None => self.rasterize(renderer, *text_key, request)?,
385 }
386 let entry = &self.entries[text_key];
387 Some(crate::layout::paint::TextBox {
388 corner: entry.corner,
389 size: [entry.width, entry.height],
390 })
391 }
392 });
393 }
394 let items = crate::layout::paint::paint_list(&evaluated, &resolved, size, &boxes);
395 self.layouts.insert(
396 key,
397 LayoutEntry {
398 prepared: Arc::new(PreparedLayout { items, texts }),
399 requests,
400 last_used: self.frame,
401 },
402 );
403 Ok(())
404 }
405
406 fn shape(
408 &mut self,
409 request: &TextRasterRequest,
410 frame: u64,
411 ) -> Result<Arc<ShapedText>, TextRasterError> {
412 let key = shaped_key(request);
413 if let Some(entry) = self.shaped.get_mut(&key) {
414 entry.last_used = frame;
415 return Ok(Arc::clone(&entry.shaped));
416 }
417 let shaped = Arc::new(self.rasterizer.shape(request)?);
418 if self.shaped.len() >= RETAINED_SHAPED
419 && let Some(oldest) = self
420 .shaped
421 .iter()
422 .min_by_key(|(_, entry)| entry.last_used)
423 .map(|(key, _)| *key)
424 {
425 self.shaped.remove(&oldest);
426 }
427 self.shaped.insert(
428 key,
429 ShapedEntry {
430 shaped: Arc::clone(&shaped),
431 last_used: frame,
432 },
433 );
434 Ok(shaped)
435 }
436
437 #[cfg(test)]
439 #[must_use]
440 pub(crate) const fn layout_resolves(&self) -> u64 {
441 self.resolves
442 }
443
444 #[must_use]
446 pub fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
447 self.layouts
448 .get(&key)
449 .map(|entry| Arc::clone(&entry.prepared))
450 }
451
452 pub fn measure_layout(
460 &self,
461 root: &alpha2_model::LayoutNode,
462 time: alpha2_time::MediaTime,
463 size: RenderSize,
464 canvas: (u32, u32),
465 ) -> Result<crate::LayoutBounds, TextError> {
466 crate::measure_layout(&self.rasterizer, root, time, size, canvas)
467 }
468
469 pub fn prepare_at(
477 &mut self,
478 renderer: &Renderer,
479 project: &crate::EngineProject,
480 time: alpha2_time::MediaTime,
481 size: RenderSize,
482 ) -> Result<(), crate::EngineError> {
483 if !project.draws_text() {
484 return Ok(());
485 }
486 let frame = project.evaluate(time)?;
487 self.prepare(renderer, &frame, size)
488 .map_err(crate::EngineError::Text)
489 }
490
491 pub fn measure(
497 &self,
498 text: &str,
499 style: &TextStyle,
500 size: RenderSize,
501 ) -> Result<TextBounds, TextError> {
502 measure_text(&self.rasterizer, text, style, size)
503 }
504
505 #[must_use]
507 pub fn get(&self, key: u64) -> Option<TextTexture<'_>> {
508 self.entries.get(&key).map(|entry| TextTexture {
509 texture: SourceView::from(&entry.texture),
510 width: entry.width,
511 height: entry.height,
512 margin: entry.margin,
513 cache_key: Some(self.cache_key(key)),
514 })
515 }
516
517 fn cache_key(&self, key: u64) -> RenderCacheKey {
522 let high = TEXT_CACHE_DOMAIN ^ self.font_set;
523 RenderCacheKey::from_value((u128::from(high) << 64) | u128::from(key))
524 }
525}
526
527pub(crate) fn frame_texts<'f>(
531 frame: &'f FrameDesc<'_>,
532) -> impl Iterator<Item = (&'f str, &'f TextStyle)> {
533 let captions = frame
534 .captions
535 .iter()
536 .map(|caption| (caption.text, caption.style));
537 let generators = frame
538 .visual
539 .iter()
540 .filter(|clip| frame.draws(clip))
541 .flat_map(|clip| {
542 let steps = clip
543 .composition
544 .iter()
545 .chain(&clip.source_composition)
546 .flat_map(|composition| &composition.plan().steps)
547 .filter_map(|step| match &step.op {
548 StepOp::Generator { generator, .. } => Some(generator),
549 _ => None,
550 });
551 clip.generator.into_iter().chain(steps)
552 })
553 .filter_map(|generator| match generator {
554 Generator::Text { text, style } => Some((text.as_str(), style)),
555 _ => None,
556 });
557 captions.chain(generators)
558}
559
560pub struct FramesWithText<'a> {
563 frames: &'a dyn crate::FrameProvider,
564 texts: &'a TextTextures,
565}
566
567impl<'a> FramesWithText<'a> {
568 #[must_use]
570 pub fn new(frames: &'a dyn crate::FrameProvider, texts: &'a TextTextures) -> Self {
571 Self { frames, texts }
572 }
573}
574
575impl crate::FrameProvider for FramesWithText<'_> {
576 fn texture_for(
577 &self,
578 source: &alpha2_media::SourceId,
579 time: alpha2_time::MediaTime,
580 ) -> Option<SourceView<'_>> {
581 self.frames.texture_for(source, time)
582 }
583
584 fn pending_source_work(
585 &self,
586 source: &alpha2_media::SourceId,
587 time: alpha2_time::MediaTime,
588 ) -> Option<&alpha2_render::PendingSourceWork> {
589 self.frames.pending_source_work(source, time)
590 }
591
592 fn source_key(
593 &self,
594 source: &alpha2_media::SourceId,
595 time: alpha2_time::MediaTime,
596 ) -> Option<alpha2_render::RenderCacheKey> {
597 self.frames.source_key(source, time)
598 }
599
600 fn text(&self, key: u64) -> Option<TextTexture<'_>> {
601 self.texts.get(key)
602 }
603
604 fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
605 self.texts.layout(key)
606 }
607}
608
609pub(crate) fn frame_layouts<'f>(
613 frame: &'f FrameDesc<'_>,
614) -> impl Iterator<Item = (&'f LayoutPlan, alpha2_time::MediaTime)> {
615 frame
616 .visual
617 .iter()
618 .filter(|clip| frame.draws(clip))
619 .flat_map(|clip| {
620 let own = clip
621 .plan
622 .and_then(GeneratorPlan::layout)
623 .map(|plan| (plan, clip.source_time));
624 let steps = clip
625 .composition
626 .iter()
627 .chain(&clip.source_composition)
628 .flat_map(|composition| {
629 let time = composition.time();
630 composition
631 .plan()
632 .steps
633 .iter()
634 .filter_map(move |step| match &step.op {
635 StepOp::Generator {
636 plan: Some(plan),
637 start,
638 ..
639 } => plan.layout().map(|plan| {
640 (
641 plan,
642 alpha2_time::MediaTime(
643 step.time(time).ticks().saturating_add(start.ticks()),
644 ),
645 )
646 }),
647 _ => None,
648 })
649 });
650 own.into_iter().chain(steps)
651 })
652}
653
654#[must_use]
657pub fn layout_key(evaluated: u128, size: RenderSize) -> u128 {
658 let mut hash = alpha2_geometry::StableHash::new();
659 hash.bytes(b"layout");
660 hash.bytes(&evaluated.to_le_bytes());
661 hash.u32(size.width);
662 hash.u32(size.height);
663 hash.finish()
664}
665
666pub(crate) fn layout_text_key(request: &TextRasterRequest) -> u64 {
669 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
670 let mut feed = |bytes: &[u8]| {
671 for byte in bytes {
672 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
673 }
674 };
675 feed(&[0x4c]);
677 feed_request(&mut feed, request);
678 hash.max(1)
679}
680
681fn shaped_key(request: &TextRasterRequest) -> u64 {
683 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
684 let mut feed = |bytes: &[u8]| {
685 for byte in bytes {
686 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
687 }
688 };
689 feed(request.text.as_bytes());
690 feed(&[0xff]);
691 feed(
692 request
693 .font_family
694 .as_deref()
695 .unwrap_or_default()
696 .as_bytes(),
697 );
698 feed(&[0xff, u8::from(request.font_family.is_some())]);
699 feed(&request.font_size.to_bits().to_le_bytes());
700 feed(&request.line_height.to_bits().to_le_bytes());
701 hash
702}
703
704fn feed_request(feed: &mut impl FnMut(&[u8]), request: &TextRasterRequest) {
705 feed(request.text.as_bytes());
706 feed(&[0xff]);
707 feed(
708 request
709 .font_family
710 .as_deref()
711 .unwrap_or_default()
712 .as_bytes(),
713 );
714 feed(&[0xff, u8::from(request.font_family.is_some())]);
715 for value in [request.font_size, request.max_width, request.line_height] {
716 feed(&value.to_bits().to_le_bytes());
717 }
718 feed(&request.color);
719 feed(&[match request.align {
720 alpha2_media::TextAlign::Start => 0,
721 alpha2_media::TextAlign::Center => 1,
722 alpha2_media::TextAlign::End => 2,
723 }]);
724 match request.outline {
725 Some(outline) => {
726 feed(&[1]);
727 feed(&outline.color);
728 feed(&outline.width.to_bits().to_le_bytes());
729 }
730 None => feed(&[0]),
731 }
732}
733
734#[must_use]
737pub fn text_key(text: &str, style: &TextStyle, size: RenderSize) -> u64 {
738 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
739 let mut feed = |bytes: &[u8]| {
740 for byte in bytes {
741 hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
742 }
743 };
744 feed(text.as_bytes());
745 feed(&[0xff]);
746 feed(style.font_family.as_deref().unwrap_or_default().as_bytes());
748 feed(&[0xff, u8::from(style.font_family.is_some())]);
749 for value in [
750 style.font_size,
751 style.line_height,
752 style.max_width,
753 style.position.x,
754 style.position.y,
755 style.anchor.x,
756 style.anchor.y,
757 ] {
758 feed(&value.to_bits().to_le_bytes());
759 }
760 feed(&rgba8(style.color));
761 feed(&[match style.align {
762 TextAlign::Start => 0,
763 TextAlign::Center => 1,
764 TextAlign::End => 2,
765 }]);
766 match style.outline {
767 Some(outline) => {
768 feed(&[1]);
769 feed(&rgba8(outline.color));
770 feed(&outline.width.to_bits().to_le_bytes());
771 }
772 None => feed(&[0]),
773 }
774 match style.background {
775 Some(background) => {
776 feed(&[1]);
777 feed(&rgba8(background.color));
778 feed(&background.padding.to_bits().to_le_bytes());
779 }
780 None => feed(&[0]),
781 }
782 feed(&size.width.to_le_bytes());
783 feed(&size.height.to_le_bytes());
784 hash.max(1)
785}
786
787#[allow(clippy::cast_precision_loss)]
797pub fn measure_text(
798 rasterizer: &TextRasterizer,
799 text: &str,
800 style: &TextStyle,
801 size: RenderSize,
802) -> Result<TextBounds, TextError> {
803 let request = text_request(text, style, size, text_key(text, style, size));
804 let measure = rasterizer.measure(&request).map_err(TextError::Raster)?;
805 let (width, height) = (size.width as f32, size.height as f32);
806 let margin = measure.decoration_margin as f32;
807 let (box_width, box_height) = (measure.box_width as f32, measure.box_height as f32);
808 let origin = [
811 style.position.x * width - style.anchor.x * (box_width + 2.0 * margin) + margin,
812 style.position.y * height - style.anchor.y * (box_height + 2.0 * margin) + margin,
813 ];
814 let rect = |[left, top, right, bottom]: [f32; 4]| CanvasRect {
815 left: (origin[0] + left) / width,
816 top: (origin[1] + top) / height,
817 right: (origin[0] + right) / width,
818 bottom: (origin[1] + bottom) / height,
819 };
820 Ok(TextBounds {
821 layout: rect([-margin, -margin, box_width + margin, box_height + margin]),
822 content: rect(measure.content),
823 ink: measure.ink.map(rect),
824 line_count: measure.line_count,
825 })
826}
827
828#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
830pub(crate) fn rgba8(color: Rgba) -> [u8; 4] {
831 [color.red, color.green, color.blue, color.alpha]
832 .map(|value| (value.clamp(0.0, 1.0) * 255.0).round() as u8)
833}
834
835#[allow(clippy::cast_precision_loss)]
838fn text_request(text: &str, style: &TextStyle, size: RenderSize, key: u64) -> TextRasterRequest {
839 let font_size = (style.font_size * size.height as f32).clamp(1.0, MAX_FONT_PIXELS);
840 TextRasterRequest {
841 text: text.to_owned(),
842 font_size,
843 max_width: (style.max_width * size.width as f32).max(1.0),
844 line_height: style.line_height,
845 align: match style.align {
846 TextAlign::Start => alpha2_media::TextAlign::Start,
847 TextAlign::Center => alpha2_media::TextAlign::Center,
848 TextAlign::End => alpha2_media::TextAlign::End,
849 },
850 color: rgba8(style.color),
851 emoji_policy: EmojiPolicy::Color,
852 content_version: key,
853 outline: style.outline.map(|outline| alpha2_text::TextOutline {
854 color: rgba8(outline.color),
855 width: (outline.width * font_size).clamp(f32::MIN_POSITIVE, MAX_OUTLINE_PIXELS),
856 }),
857 background: style
858 .background
859 .map(|background| alpha2_text::TextBackground {
860 color: rgba8(background.color),
861 padding: (background.padding * font_size).clamp(0.0, MAX_PADDING_PIXELS),
862 }),
863 font_family: style.font_family.clone(),
864 }
865}
866
867#[cfg(test)]
868#[path = "text_tests.rs"]
869mod tests;