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_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
30pub(crate) fn picture(size: RenderSize) -> PictureSize {
32 PictureSize::new(size.width, size.height).expect("a render size is a picture size")
33}
34
35const TEXT_CACHE_DOMAIN: u64 = 0x6361_7074_696f_6e73;
40
41static NEXT_FONT_SET: AtomicU64 = AtomicU64::new(1);
45
46const RETAINED_TEXTS: usize = 16;
49
50const RETAINED_LAYOUTS: usize = 8;
53
54const RETAINED_SHAPED: usize = 256;
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(Clone, Debug, Eq, PartialEq)]
133#[non_exhaustive]
134pub enum TextError {
135 Raster(TextRasterError),
138 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 corner: [u32; 2],
163 last_used: u64,
164}
165
166struct LayoutEntry {
169 prepared: Arc<PreparedLayout>,
170 requests: Vec<(u64, TextRasterRequest)>,
171 last_used: u64,
172}
173
174struct ShapedEntry {
176 shaped: Arc<ShapedText>,
177 last_used: u64,
178}
179
180pub struct TextTextures {
183 rasterizer: TextRasterizer,
184 font_set: u64,
187 entries: HashMap<u64, TextEntry>,
188 layouts: HashMap<u128, LayoutEntry>,
190 shaped: HashMap<u64, ShapedEntry>,
192 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 #[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 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 self.font_set = NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed);
252 Ok(())
253 }
254
255 #[must_use]
257 pub fn font_count(&self) -> usize {
258 self.rasterizer.font_count()
259 }
260
261 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 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 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 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 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 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 #[cfg(test)]
456 #[must_use]
457 pub(crate) const fn layout_resolves(&self) -> u64 {
458 self.resolves
459 }
460
461 #[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 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 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 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 #[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 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
545pub(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
578pub struct FramesWithText<'a> {
581 frames: &'a dyn crate::FrameProvider,
582 texts: &'a TextTextures,
583}
584
585impl<'a> FramesWithText<'a> {
586 #[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
627pub(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#[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}