Skip to main content

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}