concinnity_core/defaults/font.rs
1// The font the injected HUD chips and the loading label draw with: the face
2// bundled in the binary, rasterised once for the process and appended to each
3// world's font table. Appending leaves every handle the build assigned where it
4// was.
5
6use alloc::vec::Vec;
7
8use crate::bake;
9use crate::ecs::{FontHandle, PipelineContext};
10use crate::resource::{FontTable, ResourceEntry};
11use crate::result::CnResult;
12
13/// Pixel size the injected HUD face is rasterised at. Chips draw it minified,
14/// so the atlas is supersampled from here rather than authored larger.
15pub const HUD_FONT_SIZE_PX: u32 = 20;
16
17/// The face baked for this world, so a second caller shares it rather than
18/// paying for the atlas again.
19#[derive(Debug, Clone, Copy)]
20pub struct HudFont(pub FontHandle);
21
22// The bundled face at one fixed size compiles to one fixed atlas, so the
23// signed-distance pass over its glyphs runs once for the process rather than
24// once per world. A host that rebuilds a world repeatedly -- the editor's live
25// preview, and every test that injects the HUD -- is otherwise paying for the
26// same bytes each time.
27//
28// A racing caller waits for the first rather than compiling its own copy: the
29// pass is long enough that the duplicates are the whole cost worth avoiding.
30static PAYLOAD: spin::Once<Vec<u8>> = spin::Once::new();
31
32// The compiled atlas for the bundled face, computed on the first call. A
33// failure is not stored: it can only mean the bundled face itself is broken,
34// and the caller decides what to do about that.
35fn payload() -> Result<&'static [u8], CnResult> {
36 PAYLOAD
37 .try_call_once(|| {
38 bake::font::compile(
39 bake::font::BUILTIN_FONT_BYTES,
40 HUD_FONT_SIZE_PX,
41 bake::font::BUILTIN_FONT_FILE,
42 )
43 .map_err(|_| CnResult::InvalidArgument)
44 })
45 .map(Vec::as_slice)
46}
47
48/// The font the engine's own HUD text draws with, baked into `ctx`'s font table
49/// on the first call and returned as it stands afterwards.
50///
51/// A host that draws HUD text of its own before the world starts (the editor's
52/// panels) reaches it here, so one atlas serves both.
53pub fn hud_font(ctx: &mut PipelineContext) -> Result<FontHandle, CnResult> {
54 if let Some(HudFont(handle)) = ctx.resource::<HudFont>().copied() {
55 return Ok(handle);
56 }
57 let payload = payload()?.to_vec();
58 // The table is created when the world carries none: one assembled in code
59 // rather than loaded from a blob.
60 if ctx.resource::<FontTable>().is_none() {
61 ctx.insert_resource(FontTable::default());
62 }
63 let table = ctx
64 .resource_mut::<FontTable>()
65 .ok_or(CnResult::InvalidState)?;
66 let handle = FontHandle(table.append(ResourceEntry::baked(payload)));
67 ctx.insert_resource(HudFont(handle));
68 Ok(handle)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::ecs::World;
75
76 // The signed-distance pass over the bundled glyphs is the expensive half of
77 // a bake, and it runs on a constant: the same face at the same size. A
78 // second call reads what the first stored rather than repeating it.
79 #[test]
80 fn the_bundled_atlas_compiles_once_for_the_process() {
81 let first = payload().expect("the bundled face compiles");
82 let second = payload().expect("the bundled face compiles");
83
84 assert!(!first.is_empty());
85 assert_eq!(
86 first.as_ptr(),
87 second.as_ptr(),
88 "the second call reads the stored atlas"
89 );
90 }
91
92 // Sharing the compiled bytes must not share the table entry: each world
93 // owns its own, at whatever handle its own table hands out.
94 #[test]
95 fn each_world_gets_its_own_entry_holding_the_same_atlas() {
96 let mut first = World::new();
97 let mut second = World::new();
98 hud_font(&mut first.context()).expect("a face for the first world");
99 hud_font(&mut second.context()).expect("a face for the second world");
100
101 let bytes = |world: &World| {
102 world
103 .resource::<FontTable>()
104 .expect("a font table")
105 .0
106 .first()
107 .expect("the appended face")
108 .baked_bytes()
109 .expect("the face is baked")
110 .to_vec()
111 };
112 assert_eq!(bytes(&first), bytes(&second));
113 assert!(!bytes(&first).is_empty());
114 }
115
116 // A world that already carries the face reuses its handle rather than
117 // appending a second entry for the same atlas.
118 #[test]
119 fn a_second_call_on_one_world_reuses_the_handle() {
120 let mut world = World::new();
121 let first = hud_font(&mut world.context()).expect("a face");
122 let second = hud_font(&mut world.context()).expect("the same face");
123
124 assert_eq!(first, second);
125 assert_eq!(
126 world.resource::<FontTable>().expect("a font table").len(),
127 1
128 );
129 }
130}