#![cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
use frust_engine::SceneCompiler;
use frust_engine::compile::{CompiledFrame, MAX_RESIDENT_GLYPH_RUNS};
use frust_scene::{FontHandle, Glyph, GlyphRun, Scene, SceneBuilder};
use kurbo::{Affine, Rect};
use peniko::{Blob, Brush, Color, FontData};
use vello_common::encode::EncodedPaint;
const LATIN_FONT: &[u8] = include_bytes!("../../../testing/fonts/NotoSans-Subset.ttf");
const HELLO: [u32; 5] = [5, 6, 7, 7, 8];
const OTHERS: [u32; 4] = [9, 10, 11, 12];
const SIZE: u16 = 256;
const SETTLING_FRAMES: usize = 12;
const PAST_TWO_EVICTIONS: usize = 140;
fn latin_font() -> FontHandle {
static FONT: std::sync::OnceLock<FontHandle> = std::sync::OnceLock::new();
FONT.get_or_init(|| FontHandle::new(FontData::new(Blob::new(Arc::new(LATIN_FONT)), 0)))
.clone()
}
fn run(ids: &[u32], size: f32, x0: f32, advance: f32, color: Color, transform: Affine) -> GlyphRun {
GlyphRun {
font: latin_font(),
font_size: size,
brush: Brush::Solid(color),
transform,
glyphs: ids
.iter()
.enumerate()
.map(|(index, id)| Glyph {
id: *id,
x: x0 + index as f32 * advance,
y: 0.0,
})
.collect(),
}
}
fn scene_of(record: impl FnOnce(&mut SceneBuilder<'_>)) -> Scene {
let mut scene = Scene::new();
{
let mut builder = SceneBuilder::new(&mut scene);
builder.fill_rect(
Rect::new(0.0, 0.0, f64::from(SIZE), f64::from(SIZE)),
Brush::Solid(Color::WHITE),
);
record(&mut builder);
}
scene
}
fn compiler(cache: bool, hint: bool) -> SceneCompiler {
let mut compiler = SceneCompiler::new(SIZE, SIZE);
compiler.set_hint_text(hint);
compiler.set_glyph_quads(true);
compiler.set_glyph_run_cache(cache);
compiler
}
fn compile(compiler: &mut SceneCompiler, scene: &Scene) -> (CompiledFrame, bool) {
let frame = compiler
.compile(scene, Affine::IDENTITY, (SIZE, SIZE))
.expect("an in-range scene compiles");
let rasterized = compiler.glyph_replay_pending();
compiler.acknowledge_glyph_clears();
compiler.acknowledge_glyph_replay();
compiler.acknowledge_image_plan();
(frame, rasterized)
}
struct Arms {
cached: SceneCompiler,
probing: SceneCompiler,
}
impl Arms {
fn new(hint: bool) -> Self {
Self {
cached: compiler(true, hint),
probing: compiler(false, hint),
}
}
fn frame(&mut self, label: &str, scene: &Scene) -> CompiledFrame {
let (mut cached, _) = compile(&mut self.cached, scene);
let (probing, _) = compile(&mut self.probing, scene);
assert_eq!(
probing.resident_glyph_runs, 0,
"{label}: the probing arm never reads the cache"
);
let resident = cached.resident_glyph_runs;
cached.resident_glyph_runs = 0;
assert_eq!(
format!("{cached:?}"),
format!("{probing:?}"),
"{label}: the two arms compiled different frames"
);
assert_eq!(
self.cached.glyph_atlas_entries(),
self.probing.glyph_atlas_entries(),
"{label}: the two arms hold different atlas residency"
);
cached.resident_glyph_runs = resident;
cached
}
}
fn runs_scene(shift: (f64, f64), scale: f64, color: Color) -> Scene {
scene_of(|builder| {
for step in 0..16_u32 {
let fraction = step as f32 / 16.0;
let size = [14.0, 18.5, 23.0][step as usize % 3];
let baseline = 14.0 + f64::from(step) * 14.5;
builder.draw_glyph_run(run(
&HELLO,
size,
3.0 + fraction,
11.0 + fraction,
color,
Affine::translate((f64::from(step % 4) * 0.5 + shift.0, baseline + shift.1))
* Affine::scale(scale),
));
}
})
}
fn single(ids: &[u32], x: f64, size: f32, color: Color) -> Scene {
scene_of(|builder| {
builder.draw_glyph_run(run(
ids,
size,
0.25,
12.5,
color,
Affine::translate((x, 30.0)),
));
})
}
fn record_tint(frame: &CompiledFrame) -> Option<Color> {
let index = *frame.glyph_run_paints.first()?;
match &frame.encoded_paints[index] {
EncodedPaint::Image(image) => image.tint.map(|tint| tint.color),
_ => None,
}
}
#[test]
fn a_repeated_run_is_drawn_from_the_cache_exactly_as_glifo_draws_it() {
for hint in [false, true] {
let mut arms = Arms::new(hint);
let scene = runs_scene((0.0, 0.0), 1.0, Color::BLACK);
let mut last = 0;
for index in 0..SETTLING_FRAMES + 4 {
last = arms
.frame(&format!("hint {hint}, frame {index}"), &scene)
.resident_glyph_runs;
}
assert_eq!(
last, 16,
"hint {hint}: every run of a settled frame is resident"
);
}
}
#[test]
fn a_moving_run_is_placed_and_bucketed_where_glifo_places_it() {
for hint in [false, true] {
for scale in [1.0, 1.5, 2.25] {
let mut arms = Arms::new(hint);
let mut resident = 0;
for index in 0..SETTLING_FRAMES + 48 {
let step = index as f64;
let shift = ((step * 0.3713).sin() * 6.0, (step * 0.2171).cos() * 3.0);
let frame = arms.frame(
&format!("hint {hint}, scale {scale}, frame {index}"),
&runs_scene(shift, scale, Color::BLACK),
);
resident += frame.resident_glyph_runs;
}
assert!(
resident > 0,
"hint {hint}, scale {scale}: a moving run is still served once its buckets are known"
);
}
}
}
#[test]
fn a_whole_pixel_move_hits_and_moves_every_quad_by_the_same_amount() {
let mut arms = Arms::new(false);
let still = single(&HELLO, 20.0, 18.0, Color::BLACK);
for index in 0..SETTLING_FRAMES {
arms.frame(&format!("frame {index}"), &still);
}
let before = arms.frame("before", &still);
let after = arms.frame("after", &single(&HELLO, 27.0, 18.0, Color::BLACK));
assert_eq!(before.resident_glyph_runs, 1);
assert_eq!(
after.resident_glyph_runs, 1,
"a translation is not a new run"
);
assert_eq!(before.glyph_quads.len(), after.glyph_quads.len());
for (a, b) in before.glyph_quads.iter().zip(&after.glyph_quads) {
assert_eq!(b.x, a.x + 7);
assert_eq!((b.y, b.width, b.height), (a.y, a.width, a.height));
assert_eq!((b.atlas_x, b.atlas_y), (a.atlas_x, a.atlas_y));
}
}
#[test]
fn a_changed_size_or_scale_misses_and_a_changed_colour_hits() {
let mut arms = Arms::new(false);
let black = single(&HELLO, 20.0, 18.0, Color::BLACK);
for index in 0..SETTLING_FRAMES {
arms.frame(&format!("settle {index}"), &black);
}
assert_eq!(arms.frame("settled", &black).resident_glyph_runs, 1);
let red = Color::from_rgb8(200, 20, 20);
let recoloured = arms.frame("recoloured", &single(&HELLO, 20.0, 18.0, red));
assert_eq!(recoloured.resident_glyph_runs, 1, "a colour change hits");
assert_eq!(record_tint(&recoloured), Some(red));
let larger = arms.frame("larger", &single(&HELLO, 20.0, 20.0, Color::BLACK));
assert_eq!(larger.resident_glyph_runs, 0, "a size change misses");
let mut arms = Arms::new(false);
for index in 0..SETTLING_FRAMES {
arms.frame(&format!("settle {index}"), &black);
}
let scaled = scene_of(|builder| {
builder.draw_glyph_run(run(
&HELLO,
18.0,
0.25,
12.5,
Color::BLACK,
Affine::translate((20.0, 30.0)) * Affine::scale(1.25),
));
});
assert_eq!(
arms.frame("scaled", &scaled).resident_glyph_runs,
0,
"a scale change misses"
);
for index in 0..SETTLING_FRAMES {
arms.frame(&format!("scaled {index}"), &scaled);
}
assert_eq!(
arms.frame("scaled, settled", &scaled).resident_glyph_runs,
1,
"and is remembered once it settles"
);
}
#[test]
fn an_eviction_invalidates_the_slots_it_reaped() {
let mut arms = Arms::new(false);
let hello = single(&HELLO, 20.0, 18.0, Color::BLACK);
let others = single(&OTHERS, 20.0, 18.0, Color::BLACK);
for index in 0..SETTLING_FRAMES {
arms.frame(&format!("hello {index}"), &hello);
}
assert_eq!(arms.frame("hello, settled", &hello).resident_glyph_runs, 1);
let with_hello = arms.cached.glyph_atlas_entries();
for index in 0..PAST_TWO_EVICTIONS {
arms.frame(&format!("others {index}"), &others);
}
assert!(
arms.cached.glyph_atlas_entries() < with_hello + OTHERS.len(),
"the eviction passes reaped the run that left the screen"
);
let (back, rasterized) = compile(&mut arms.cached, &hello);
let (probing, probing_rasterized) = compile(&mut arms.probing, &hello);
assert_eq!(
back.resident_glyph_runs, 0,
"a reaped run is not drawn from the cache"
);
assert!(
rasterized && probing_rasterized,
"both arms rasterize it again"
);
let mut back = back;
back.resident_glyph_runs = 0;
assert_eq!(format!("{back:?}"), format!("{probing:?}"));
assert_eq!(
arms.frame("hello, again", &hello).resident_glyph_runs,
1,
"and is remembered again once resident"
);
}
#[test]
fn the_atlas_lru_still_sees_a_resident_run() {
let mut arms = Arms::new(false);
let both = scene_of(|builder| {
builder.draw_glyph_run(run(
&HELLO,
18.0,
0.25,
12.5,
Color::BLACK,
Affine::translate((20.0, 30.0)),
));
builder.draw_glyph_run(run(
&OTHERS,
18.0,
0.25,
12.5,
Color::BLACK,
Affine::translate((20.0, 80.0)),
));
});
let hello = single(&HELLO, 20.0, 18.0, Color::BLACK);
for index in 0..SETTLING_FRAMES {
arms.frame(&format!("both {index}"), &both);
}
let mut resident = 0;
for index in 0..3 * PAST_TWO_EVICTIONS {
resident += arms
.frame(&format!("hello {index}"), &hello)
.resident_glyph_runs;
}
assert_eq!(
resident,
3 * PAST_TWO_EVICTIONS as u32,
"the resident run was served from the cache on every frame"
);
let (frame, rasterized) = compile(&mut arms.cached, &hello);
assert_eq!(frame.resident_glyph_runs, 1);
assert!(!rasterized, "nothing of the resident run was reaped");
let (_, rasterized) = compile(&mut arms.cached, &single(&OTHERS, 20.0, 18.0, Color::BLACK));
assert!(rasterized, "the stale run was reaped");
}
#[test]
fn the_bound_forgets_the_least_recently_drawn_runs() {
let mut compiler = compiler(true, false);
let mut serial = 0_u32;
let mut latest = Vec::new();
for frame in 0..30 {
let scene = scene_of(|builder| {
for slot in 0..100_u32 {
let ids: Vec<u32> = (0..4)
.map(|digit| 1 + serial / 21_u32.pow(digit) % 21)
.collect();
serial += 1;
latest = ids.clone();
builder.draw_glyph_run(run(
&ids,
14.0,
0.0,
9.0,
Color::BLACK,
Affine::translate((
f64::from(slot % 5) * 50.0,
12.0 + f64::from(slot / 5) * 12.0,
)),
));
}
});
compile(&mut compiler, &scene);
assert!(
compiler.resident_glyph_run_count() <= MAX_RESIDENT_GLYPH_RUNS,
"frame {frame}: {} runs remembered",
compiler.resident_glyph_run_count()
);
}
assert!(
compiler.resident_glyph_run_count() >= 100,
"the runs of the frame just drawn are kept"
);
let last = scene_of(|builder| {
builder.draw_glyph_run(run(
&latest,
14.0,
0.0,
9.0,
Color::BLACK,
Affine::translate((200.0, 240.0)),
));
});
let (frame, _) = compile(&mut compiler, &last);
assert_eq!(frame.resident_glyph_runs, 1);
}
#[test]
fn the_kill_switch_restores_per_glyph_probing() {
let mut arms = Arms::new(false);
let scene = runs_scene((0.0, 0.0), 1.0, Color::BLACK);
for index in 0..SETTLING_FRAMES {
arms.frame(&format!("frame {index}"), &scene);
}
assert!(arms.cached.glyph_run_cache_enabled());
assert!(arms.frame("cached", &scene).resident_glyph_runs > 0);
arms.cached.set_glyph_run_cache(false);
assert!(!arms.cached.glyph_run_cache_enabled());
for index in 0..4 {
assert_eq!(
arms.frame(&format!("switched off {index}"), &scene)
.resident_glyph_runs,
0,
"every run goes back through `glifo`"
);
}
arms.cached.set_glyph_run_cache(true);
arms.cached.set_glyph_quads(false);
arms.probing.set_glyph_quads(false);
for index in 0..4 {
assert_eq!(
arms.frame(&format!("quads off {index}"), &scene)
.resident_glyph_runs,
0
);
}
}