use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use alpha2_eval::{FrameDesc, GeneratorPlan, LayoutPlan, StepOp};
use alpha2_layout::{
PictureSize, ShadowPixels, TextAnimation, TextBounds, TextDraw, animated_text_key, shaped_key,
text_draw,
};
use alpha2_model::{Generator, TextContent, TextStyle};
use alpha2_render::{
CpuImage, CpuRenderer, CpuSource, RenderCacheKey, RenderError, RenderSize, Renderer,
SourceView, UploadTexture,
};
use alpha2_text::{ShapedText, TextFont, TextRasterError, TextRasterRequest, TextRasterizer};
use crate::layout::PreparedLayout;
pub(crate) fn picture(size: RenderSize) -> PictureSize {
PictureSize::new(size.width, size.height).expect("a render size is a picture size")
}
const TEXT_CACHE_DOMAIN: u64 = 0x6361_7074_696f_6e73;
static NEXT_FONT_SET: AtomicU64 = AtomicU64::new(1);
const RETAINED_TEXTS: usize = 16;
const RETAINED_LAYOUTS: usize = 8;
const RETAINED_SHAPED: usize = 256;
pub trait TextUpload {
fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError>;
}
#[derive(Debug)]
pub struct TextPixels(TextStorage);
#[derive(Debug)]
enum TextStorage {
Gpu(UploadTexture),
Cpu(CpuSource),
}
impl TextPixels {
fn view(&self) -> SourceView<'_> {
match &self.0 {
TextStorage::Gpu(texture) => SourceView::from(texture),
TextStorage::Cpu(source) => SourceView::cpu(source),
}
}
}
impl TextUpload for Renderer {
fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError> {
self.upload_rgba8(size, rgba8)
.map(|texture| TextPixels(TextStorage::Gpu(texture)))
}
}
impl TextUpload for CpuRenderer {
fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError> {
let picture = CpuImage::from_premultiplied_rgba8(size, rgba8)?;
Ok(TextPixels(TextStorage::Cpu(CpuSource::image(Arc::new(
picture,
)))))
}
}
#[derive(Clone, Copy, Debug)]
pub struct TextTexture<'a> {
pub texture: SourceView<'a>,
pub width: u32,
pub height: u32,
pub margin: [u32; 2],
pub cache_key: Option<RenderCacheKey>,
pub background: Option<(SourceView<'a>, Option<RenderCacheKey>)>,
pub shadows: &'a [ShadowPixels],
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum TextError {
Raster(TextRasterError),
Render(RenderError),
}
impl fmt::Display for TextError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Raster(TextRasterError::NoFonts) => {
formatter.write_str("the project shows text, but no fonts were registered")
}
Self::Raster(error) => write!(formatter, "text failed: {error}"),
Self::Render(error) => write!(formatter, "text upload failed: {error}"),
}
}
}
impl std::error::Error for TextError {}
struct TextEntry {
texture: Arc<TextPixels>,
background: Option<Arc<TextPixels>>,
shadows: Vec<ShadowPixels>,
content: u64,
width: u32,
height: u32,
margin: [u32; 2],
corner: [u32; 2],
last_used: u64,
}
struct LayoutEntry {
prepared: Arc<PreparedLayout>,
requests: Vec<(u64, TextDraw)>,
last_used: u64,
}
struct ShapedEntry {
shaped: Arc<ShapedText>,
last_used: u64,
}
pub struct TextTextures {
rasterizer: TextRasterizer,
font_set: u64,
entries: HashMap<u64, TextEntry>,
layouts: HashMap<u128, LayoutEntry>,
shaped: HashMap<u64, ShapedEntry>,
resolves: u64,
frame: u64,
}
impl fmt::Debug for TextTextures {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TextTextures")
.field("fonts", &self.rasterizer.font_count())
.field("texts", &self.entries.len())
.field("layouts", &self.layouts.len())
.finish_non_exhaustive()
}
}
impl Default for TextTextures {
fn default() -> Self {
Self::new()
}
}
impl TextTextures {
#[must_use]
pub fn new() -> Self {
Self {
rasterizer: TextRasterizer::empty(),
font_set: NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed),
entries: HashMap::new(),
layouts: HashMap::new(),
shaped: HashMap::new(),
resolves: 0,
frame: 0,
}
}
pub fn add_font(
&mut self,
family: impl Into<String>,
bytes: impl Into<Arc<[u8]>>,
face_index: u32,
) -> Result<(), TextError> {
self.rasterizer
.add_font(TextFont {
family: family.into(),
bytes: bytes.into(),
face_index,
})
.map_err(TextError::Raster)?;
self.entries.clear();
self.layouts.clear();
self.shaped.clear();
self.font_set = NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed);
Ok(())
}
#[must_use]
pub fn font_count(&self) -> usize {
self.rasterizer.font_count()
}
#[must_use]
pub fn unmatched_font_families(&self) -> Vec<String> {
self.rasterizer.unmatched_families()
}
pub fn prepare(
&mut self,
renderer: &dyn TextUpload,
frame: &FrameDesc,
size: RenderSize,
) -> Result<(), TextError> {
self.frame += 1;
let picture = picture(size);
for text in frame_texts(frame) {
let key = animated_text_key(text.content, text.style, picture, text.time);
if let Some(entry) = self.entries.get_mut(&key) {
entry.last_used = self.frame;
continue;
}
let mut draw = text_draw(&text.content.text(), text.style, picture, key);
if !text.style.animators.is_empty() {
draw.animation = Some(TextAnimation {
animators: alpha2_eval::sample_text_animators(&text.style.animators, text.time),
runs: text.content.runs().to_vec(),
time: text.time,
});
}
self.rasterize(renderer, key, &draw)?;
}
for (plan, time) in frame_layouts(frame) {
self.prepare_layout(renderer, plan, time, size)?;
}
while self.layouts.len() > RETAINED_LAYOUTS {
let Some(oldest) = self
.layouts
.iter()
.filter(|(_, entry)| entry.last_used < self.frame)
.min_by_key(|(_, entry)| entry.last_used)
.map(|(key, _)| *key)
else {
break;
};
self.layouts.remove(&oldest);
}
while self.entries.len() > RETAINED_TEXTS {
let Some(oldest) = self
.entries
.iter()
.filter(|(_, entry)| entry.last_used < self.frame)
.min_by_key(|(_, entry)| entry.last_used)
.map(|(key, _)| *key)
else {
break;
};
self.entries.remove(&oldest);
}
Ok(())
}
fn rasterize(
&mut self,
renderer: &dyn TextUpload,
key: u64,
draw: &TextDraw,
) -> Result<(), TextError> {
let request = &draw.request;
let states = draw.states(&self.rasterizer).map_err(TextError::Raster)?;
let content = match &states {
Some((_, hash)) => {
let content = match *hash {
0 => text_key_of(request),
hash => mix_key(text_key_of(request), hash),
};
if let Some(held) = self.entries.values().find(|entry| entry.content == content) {
let shared = TextEntry {
texture: Arc::clone(&held.texture),
background: held.background.clone(),
shadows: draw.shadows.clone(),
content,
width: held.width,
height: held.height,
margin: held.margin,
corner: held.corner,
last_used: self.frame,
};
self.entries.insert(key, shared);
return Ok(());
}
content
}
None => key,
};
let raster = match &states {
Some((states, hash)) if *hash != 0 => self
.rasterizer
.rasterize_animated(request, states)
.map_err(TextError::Raster)?,
_ => self
.rasterizer
.rasterize(request)
.map_err(TextError::Raster)?,
};
self.rasterizer.invalidate(key);
let texture_size = &raster.descriptor.texture;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let corner =
[raster.descriptor.layout_x, raster.descriptor.layout_y].map(|corner| corner as u32);
let margin = corner.map(|corner| corner.saturating_sub(request.decoration_margin()));
let size =
RenderSize::new(texture_size.width, texture_size.height).map_err(TextError::Render)?;
let texture = renderer
.upload_text(size, &raster.pixels.rgba8)
.map_err(TextError::Render)?;
let background = raster
.background
.as_deref()
.map(|rgba8| renderer.upload_text(size, rgba8))
.transpose()
.map_err(TextError::Render)?;
#[cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
let (texture, background) = (Arc::new(texture), background.map(Arc::new));
self.entries.insert(
key,
TextEntry {
texture,
background,
shadows: draw.shadows.clone(),
content,
width: texture_size.width,
height: texture_size.height,
margin,
corner,
last_used: self.frame,
},
);
Ok(())
}
fn prepare_layout(
&mut self,
renderer: &dyn TextUpload,
plan: &LayoutPlan,
time: alpha2_time::MediaTime,
size: RenderSize,
) -> Result<(), TextError> {
let evaluated = plan.evaluate(time);
let key = layout_key(evaluated.key, size);
if let Some(entry) = self.layouts.get_mut(&key) {
entry.last_used = self.frame;
let requests = entry.requests.clone();
for (text, request) in &requests {
match self.entries.get_mut(text) {
Some(held) => held.last_used = self.frame,
None => self.rasterize(renderer, *text, request)?,
}
}
return Ok(());
}
self.resolves += 1;
let frame = self.frame;
let picture = picture(size);
let shaped =
alpha2_layout::shape_texts(&evaluated, picture, |request| self.shape(request, frame))
.map_err(TextError::Raster)?;
let resolved = alpha2_layout::resolve(&evaluated, picture, &shaped);
let (texts, requests) = alpha2_layout::layout_texts(&evaluated, &resolved, picture);
let mut boxes = Vec::with_capacity(texts.len());
for text in &texts {
boxes.push(match text {
None => None,
Some(text_key) => {
let draw = &requests
.iter()
.find(|(key, _)| key == text_key)
.expect("a request per text")
.1;
match self.entries.get_mut(text_key) {
Some(held) => held.last_used = self.frame,
None => self.rasterize(renderer, *text_key, draw)?,
}
let entry = &self.entries[text_key];
Some(crate::layout::paint::TextBox {
corner: entry.corner,
size: [entry.width, entry.height],
})
}
});
}
let items = crate::layout::paint::paint_list(&evaluated, &resolved, size, &boxes);
self.layouts.insert(
key,
LayoutEntry {
prepared: Arc::new(PreparedLayout { items, texts }),
requests,
last_used: self.frame,
},
);
Ok(())
}
fn shape(
&mut self,
request: &TextRasterRequest,
frame: u64,
) -> Result<Arc<ShapedText>, TextRasterError> {
let key = shaped_key(request);
if let Some(entry) = self.shaped.get_mut(&key) {
entry.last_used = frame;
return Ok(Arc::clone(&entry.shaped));
}
let shaped = Arc::new(self.rasterizer.shape(request)?);
if self.shaped.len() >= RETAINED_SHAPED
&& let Some(oldest) = self
.shaped
.iter()
.min_by_key(|(_, entry)| entry.last_used)
.map(|(key, _)| *key)
{
self.shaped.remove(&oldest);
}
self.shaped.insert(
key,
ShapedEntry {
shaped: Arc::clone(&shaped),
last_used: frame,
},
);
Ok(shaped)
}
#[cfg(test)]
#[must_use]
pub(crate) const fn layout_resolves(&self) -> u64 {
self.resolves
}
#[must_use]
pub fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
self.layouts
.get(&key)
.map(|entry| Arc::clone(&entry.prepared))
}
pub fn measure_layout(
&self,
root: &alpha2_model::LayoutNode,
time: alpha2_time::MediaTime,
size: RenderSize,
canvas: (u32, u32),
) -> Result<crate::LayoutBounds, TextError> {
crate::measure_layout(&self.rasterizer, root, time, picture(size), canvas)
.map_err(TextError::Raster)
}
pub fn prepare_at(
&mut self,
renderer: &dyn TextUpload,
project: &crate::EngineProject,
time: alpha2_time::MediaTime,
size: RenderSize,
) -> Result<(), crate::EngineError> {
if !project.draws_text() {
return Ok(());
}
let frame = project.evaluate(time)?;
self.prepare(renderer, &frame, size)
.map_err(crate::EngineError::Text)
}
pub fn measure(
&self,
text: &str,
style: &TextStyle,
size: RenderSize,
) -> Result<TextBounds, TextError> {
crate::measure_text(&self.rasterizer, text, style, picture(size)).map_err(TextError::Raster)
}
#[must_use]
pub fn get(&self, key: u64) -> Option<TextTexture<'_>> {
self.entries.get(&key).map(|entry| TextTexture {
texture: entry.texture.view(),
width: entry.width,
height: entry.height,
margin: entry.margin,
cache_key: Some(self.cache_key(entry.content)),
background: entry.background.as_ref().map(|background| {
(
background.view(),
Some(self.background_cache_key(entry.content)),
)
}),
shadows: &entry.shadows,
})
}
fn cache_key(&self, key: u64) -> RenderCacheKey {
let high = TEXT_CACHE_DOMAIN ^ self.font_set;
RenderCacheKey::from_value((u128::from(high) << 64) | u128::from(key))
}
fn background_cache_key(&self, key: u64) -> RenderCacheKey {
let high = (TEXT_CACHE_DOMAIN ^ self.font_set) ^ (1 << 62);
RenderCacheKey::from_value((u128::from(high) << 64) | u128::from(key))
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct FrameText<'f> {
pub(crate) content: &'f TextContent,
pub(crate) style: &'f TextStyle,
pub(crate) time: alpha2_time::MediaTime,
}
pub(crate) fn frame_texts<'f>(frame: &'f FrameDesc<'_>) -> impl Iterator<Item = FrameText<'f>> {
let captions = frame.captions.iter().map(|caption| FrameText {
content: caption.text,
style: caption.style,
time: caption.local_time,
});
let generators = frame
.visual
.iter()
.filter(|clip| frame.draws(clip))
.flat_map(|clip| {
let own = clip
.generator
.map(|generator| (generator, clip.source_time));
let steps = clip
.composition
.iter()
.chain(&clip.source_composition)
.flat_map(|composition| {
let time = composition.time();
composition
.plan()
.steps
.iter()
.filter_map(move |step| match &step.op {
StepOp::Generator {
generator, start, ..
} => Some((generator, generator_time(step.time(time), *start))),
_ => None,
})
});
own.into_iter().chain(steps)
})
.filter_map(|(generator, time)| match generator {
Generator::Text { text, style } => Some(FrameText {
content: text,
style,
time,
}),
_ => None,
});
captions.chain(generators)
}
pub(crate) const fn generator_time(
step: alpha2_time::MediaTime,
start: alpha2_time::MediaTime,
) -> alpha2_time::MediaTime {
alpha2_time::MediaTime(step.ticks().saturating_add(start.ticks()))
}
fn mix_key(key: u64, hash: u64) -> u64 {
let mut mixed = key;
for byte in hash.to_le_bytes() {
mixed = (mixed ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3);
}
mixed.max(1)
}
fn text_key_of(request: &TextRasterRequest) -> u64 {
alpha2_layout::layout_text_key(
&TextRasterRequest {
content_version: 1,
..request.clone()
},
&[],
&[],
)
}
pub struct FramesWithText<'a> {
frames: &'a dyn crate::FrameProvider,
texts: &'a TextTextures,
}
impl<'a> FramesWithText<'a> {
#[must_use]
pub fn new(frames: &'a dyn crate::FrameProvider, texts: &'a TextTextures) -> Self {
Self { frames, texts }
}
}
impl crate::FrameProvider for FramesWithText<'_> {
fn texture_for(
&self,
source: &alpha2_media::SourceId,
time: alpha2_time::MediaTime,
) -> Option<SourceView<'_>> {
self.frames.texture_for(source, time)
}
fn pending_source_work(
&self,
source: &alpha2_media::SourceId,
time: alpha2_time::MediaTime,
) -> Option<&alpha2_render::PendingSourceWork> {
self.frames.pending_source_work(source, time)
}
fn source_key(
&self,
source: &alpha2_media::SourceId,
time: alpha2_time::MediaTime,
) -> Option<alpha2_render::RenderCacheKey> {
self.frames.source_key(source, time)
}
fn text(&self, key: u64) -> Option<TextTexture<'_>> {
self.texts.get(key)
}
fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
self.texts.layout(key)
}
}
pub(crate) fn frame_layouts<'f>(
frame: &'f FrameDesc<'_>,
) -> impl Iterator<Item = (&'f LayoutPlan, alpha2_time::MediaTime)> {
frame
.visual
.iter()
.filter(|clip| frame.draws(clip))
.flat_map(|clip| {
let own = clip
.plan
.and_then(GeneratorPlan::layout)
.map(|plan| (plan, clip.source_time));
let steps = clip
.composition
.iter()
.chain(&clip.source_composition)
.flat_map(|composition| {
let time = composition.time();
composition
.plan()
.steps
.iter()
.filter_map(move |step| match &step.op {
StepOp::Generator {
plan: Some(plan),
start,
..
} => plan.layout().map(|plan| {
(
plan,
alpha2_time::MediaTime(
step.time(time).ticks().saturating_add(start.ticks()),
),
)
}),
_ => None,
})
});
own.into_iter().chain(steps)
})
}
#[must_use]
pub fn layout_key(evaluated: u128, size: RenderSize) -> u128 {
let mut hash = alpha2_geometry::StableHash::new();
hash.bytes(b"layout");
hash.bytes(&evaluated.to_le_bytes());
hash.u32(size.width);
hash.u32(size.height);
hash.finish()
}