Skip to main content

vk_graph_fx/
bitmap_font.rs

1use {
2    anyhow::Context,
3    bmfont::BMFont,
4    bytemuck::{cast, cast_slice},
5    glam::{Mat4, vec3},
6    std::sync::Arc,
7    vk_graph::{
8        Graph,
9        cmd::{LoadOp, StoreOp},
10        driver::{
11            ash::vk,
12            buffer::{Buffer, BufferInfo},
13            device::Device,
14            graphics::{BlendInfo, GraphicsPipeline, GraphicsPipelineInfo},
15            image::Image,
16            shader::{Shader, SpecializationMap},
17            sync::AccessType,
18        },
19        node::{AnyImageNode, ImageNode},
20        pool::{Pool as _, lazy::LazyPool},
21    },
22    vk_shader_macros::include_glsl,
23};
24
25type Color = [u8; 4];
26
27fn color_to_unorm(color: Color) -> [u8; 16] {
28    cast([
29        (color[0] as f32 / u8::MAX as f32).to_ne_bytes(),
30        (color[1] as f32 / u8::MAX as f32).to_ne_bytes(),
31        (color[2] as f32 / u8::MAX as f32).to_ne_bytes(),
32        (color[3] as f32 / u8::MAX as f32).to_ne_bytes(),
33    ])
34}
35
36/// Holds a decoded bitmap Font.
37#[derive(Debug)]
38pub struct BitmapFont {
39    font: BMFont,
40    pages: Vec<Arc<Image>>,
41    pipeline: GraphicsPipeline,
42    pool: LazyPool,
43}
44
45impl BitmapFont {
46    /// Creates a bitmap font renderer from decoded font metadata and page images.
47    pub fn new(
48        device: &Device,
49        font: BMFont,
50        pages: impl Into<Vec<Arc<Image>>>,
51    ) -> anyhow::Result<Self> {
52        let pool = LazyPool::new(device);
53        let pages = pages.into();
54        let num_pages = pages.len() as u32;
55        let pipeline = GraphicsPipeline::create(
56            device,
57            GraphicsPipelineInfo::builder().blend(BlendInfo::ALPHA),
58            [
59                Shader::new_vertex(include_glsl!("res/shader/graphics/font.vert").as_slice()),
60                Shader::new_fragment(include_glsl!("res/shader/graphics/font.frag").as_slice())
61                    .specialization(
62                        SpecializationMap::new(num_pages.to_ne_bytes()).constant(0, 0, 4),
63                    ),
64            ],
65        )
66        .context("Unable to create bitmap font pipeline")?;
67
68        Ok(Self {
69            font,
70            pages,
71            pipeline,
72            pool,
73        })
74    }
75
76    // TODO: Add description and example showing layout area, top/bottom explanation, etc
77    /// Returns the position and area, in pixels, required to render the given text.
78    ///
79    /// **_NOTE:_** The 'start' of the render area is at the zero coordinate, however it may extend
80    /// into the negative x direction due to ligatures.
81    pub fn measure(&self, text: &str) -> ([i32; 2], [u32; 2]) {
82        let parse = self.font.parse(text);
83
84        // TODO: Use if we enable parsing errors on bmfont library
85        // if parse.is_err() {
86        //     return (IVec2::ZERO, UVec2::ZERO);
87        // }
88        // let parse = parse.unwrap();
89
90        let mut min_x = 0;
91        let mut max_x = 0;
92        let mut max_y = 0;
93        for char in parse {
94            if char.screen_rect.x < min_x {
95                min_x = char.screen_rect.x;
96            }
97
98            let screen_x = char.screen_rect.max_x();
99            if screen_x > max_x {
100                max_x = screen_x;
101            }
102
103            let screen_y = char.screen_rect.max_y();
104            if screen_y > max_y {
105                max_y = screen_y;
106            }
107        }
108
109        let position = [min_x, 0];
110        let size = [(max_x - min_x) as _, max_y as _];
111
112        (position, size)
113    }
114
115    /// Prints text at the given position using the default scale factor of `1.0`.
116    pub fn print(
117        &mut self,
118        graph: &mut Graph,
119        image: impl Into<AnyImageNode>,
120        x: f32,
121        y: f32,
122        color: impl Into<BitmapGlyphColor>,
123        text: impl AsRef<str>,
124    ) {
125        self.print_scale(graph, image, x, y, color, text, 1.0);
126    }
127
128    // TODO: Better API, but not sure what, probably builder-something
129    /// Prints text at the given position using a caller-specified scale factor.
130    #[allow(clippy::too_many_arguments)]
131    pub fn print_scale(
132        &mut self,
133        graph: &mut Graph,
134        image: impl Into<AnyImageNode>,
135        x: f32,
136        y: f32,
137        color: impl Into<BitmapGlyphColor>,
138        text: impl AsRef<str>,
139        scale: f32,
140    ) {
141        self.print_scale_scissor(graph, image, x, y, color, text, scale, None);
142    }
143
144    // TODO: Better API, but not sure what, probably builder-something
145    /// Prints text with an optional scissor rectangle and caller-specified scale factor.
146    #[allow(clippy::too_many_arguments)]
147    pub fn print_scale_scissor(
148        &mut self,
149        graph: &mut Graph,
150        image: impl Into<AnyImageNode>,
151        x: f32,
152        y: f32,
153        color: impl Into<BitmapGlyphColor>,
154        text: impl AsRef<str>,
155        scale: f32,
156        scissor: Option<(i32, i32, u32, u32)>,
157    ) {
158        let color = color.into();
159        let image = image.into();
160        let text = text.as_ref();
161        let image_info = graph.resource(image).info;
162        let transform = Mat4::from_translation(vec3(-1.0, -1.0, 0.0))
163            * Mat4::from_scale(vec3(2.0 * scale, 2.0 * scale, 1.0))
164            * Mat4::from_translation(vec3(
165                x / image_info.width as f32,
166                y / image_info.height as f32,
167                0.0,
168            ));
169
170        let vertex_buf_len = 120 * text.chars().count() as vk::DeviceSize;
171        let mut vertex_buf = self
172            .pool
173            .resource(BufferInfo::host_mem(
174                vertex_buf_len,
175                vk::BufferUsageFlags::VERTEX_BUFFER,
176            ))
177            .expect("missing bitmap font vertex buffer");
178
179        let mut vertex_count = 0;
180
181        {
182            let vertex_buf =
183                &mut Buffer::mapped_slice_mut(&mut vertex_buf)[0..vertex_buf_len as usize];
184
185            let mut offset = 0;
186            for (data, char) in self.font.parse(text).map(|char| (char.tessellate(), char)) {
187                vertex_buf[offset..offset + 16].copy_from_slice(&data[0]);
188                vertex_buf[offset + 20..offset + 36].copy_from_slice(&data[1]);
189                vertex_buf[offset + 40..offset + 56].copy_from_slice(&data[2]);
190                vertex_buf[offset + 60..offset + 76].copy_from_slice(&data[3]);
191                vertex_buf[offset + 80..offset + 96].copy_from_slice(&data[4]);
192                vertex_buf[offset + 100..offset + 116].copy_from_slice(&data[5]);
193
194                let page_idx = char.page_index as i32;
195                let page_idx = page_idx.to_ne_bytes();
196                vertex_buf[offset + 16..offset + 20].copy_from_slice(&page_idx);
197                vertex_buf[offset + 36..offset + 40].copy_from_slice(&page_idx);
198                vertex_buf[offset + 56..offset + 60].copy_from_slice(&page_idx);
199                vertex_buf[offset + 76..offset + 80].copy_from_slice(&page_idx);
200                vertex_buf[offset + 96..offset + 100].copy_from_slice(&page_idx);
201                vertex_buf[offset + 116..offset + 120].copy_from_slice(&page_idx);
202
203                vertex_count += 6;
204                offset += 120;
205            }
206        }
207
208        let vertex_buf = graph.bind_resource(vertex_buf);
209
210        let mut page_nodes: Vec<ImageNode> = Vec::with_capacity(self.pages.len());
211        for page in self.pages.iter() {
212            page_nodes.push(graph.bind_resource(page));
213        }
214
215        let mut cmd = graph
216            .begin_cmd()
217            .debug_name("text")
218            .bind_pipeline(&self.pipeline)
219            .resource_access(vertex_buf, AccessType::VertexBuffer)
220            .color_attachment_image(0, image, LoadOp::Load, StoreOp::Store);
221
222        for (idx, page_node) in page_nodes.iter().copied().enumerate() {
223            let descriptor = (0, [idx as _]);
224            cmd.set_shader_resource_access(
225                descriptor,
226                page_node,
227                AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer,
228            );
229        }
230
231        cmd.record_cmd(move |cmd| {
232            if let Some((x, y, width, height)) = scissor {
233                cmd.set_scissor(
234                    0,
235                    &[vk::Rect2D {
236                        offset: vk::Offset2D { x, y },
237                        extent: vk::Extent2D { width, height },
238                    }],
239                );
240            }
241
242            cmd.push_constants(0, cast_slice(&transform.to_cols_array()))
243                .push_constants(64, &(1.0 / image_info.width as f32).to_ne_bytes())
244                .push_constants(68, &(1.0 / image_info.height as f32).to_ne_bytes())
245                .push_constants(80, &color_to_unorm(color.solid()))
246                .push_constants(96, &color_to_unorm(color.outline()))
247                .bind_vertex_buffer(0, vertex_buf, 0)
248                .draw(vertex_count, 1, 0, 0);
249        });
250    }
251}
252
253/// Color selection modes for bitmap glyph rendering.
254#[derive(Debug)]
255pub enum BitmapGlyphColor {
256    /// Render only the glyph outline color.
257    Outline(Color),
258
259    /// Render only the glyph fill color.
260    Solid(Color),
261
262    /// Render both glyph fill and outline colors.
263    SolidOutline(Color, Color),
264}
265
266impl BitmapGlyphColor {
267    const TRANSPARENT: Color = [0, 0, 0, u8::MAX];
268
269    fn outline(&self) -> Color {
270        match self {
271            Self::Outline(color) => *color,
272            _ => Self::TRANSPARENT,
273        }
274    }
275
276    fn solid(&self) -> Color {
277        match self {
278            Self::Outline(_) => Self::TRANSPARENT,
279            Self::Solid(color) => *color,
280            Self::SolidOutline(color, _) => *color,
281        }
282    }
283}
284
285impl From<[f32; 3]> for BitmapGlyphColor {
286    fn from(color: [f32; 3]) -> Self {
287        Self::Solid([
288            (color[0].clamp(0.0, 1.0) * u8::MAX as f32) as _,
289            (color[1].clamp(0.0, 1.0) * u8::MAX as f32) as _,
290            (color[2].clamp(0.0, 1.0) * u8::MAX as f32) as _,
291            u8::MAX,
292        ])
293    }
294}
295
296impl From<[f32; 4]> for BitmapGlyphColor {
297    fn from(color: [f32; 4]) -> Self {
298        Self::Solid([
299            (color[0].clamp(0.0, 1.0) * u8::MAX as f32) as _,
300            (color[1].clamp(0.0, 1.0) * u8::MAX as f32) as _,
301            (color[2].clamp(0.0, 1.0) * u8::MAX as f32) as _,
302            (color[3].clamp(0.0, 1.0) * u8::MAX as f32) as _,
303        ])
304    }
305}
306
307impl From<[u8; 3]> for BitmapGlyphColor {
308    fn from(color: [u8; 3]) -> Self {
309        Self::Solid([color[0], color[1], color[2], u8::MAX])
310    }
311}
312
313impl From<[u8; 4]> for BitmapGlyphColor {
314    fn from(color: [u8; 4]) -> Self {
315        Self::Solid(color)
316    }
317}
318
319pub use bmfont::CharPosition as BitmapGlyph;
320
321/// Common accessors for glyph geometry and atlas placement.
322pub trait Glyph {
323    fn page_height(&self) -> u32;
324    fn page_width(&self) -> u32;
325    fn page_x(&self) -> u32;
326    fn page_y(&self) -> u32;
327    fn screen_height(&self) -> f32;
328    fn screen_width(&self) -> f32;
329    fn screen_x(&self) -> f32;
330    fn screen_y(&self) -> f32;
331
332    fn tessellate(&self) -> [[u8; 16]; 6] {
333        let x1 = self.screen_x();
334        let y1 = self.screen_y();
335        let x2 = self.screen_x() + self.screen_width();
336        let y2 = self.screen_y() + self.screen_height();
337
338        let u1 = self.page_x() as f32;
339        let u2 = (self.page_x() + self.page_width()) as f32;
340        let v1 = self.page_y() as f32;
341        let v2 = (self.page_y() + self.page_height()) as f32;
342
343        let x1 = x1.to_ne_bytes();
344        let x2 = x2.to_ne_bytes();
345        let y1 = y1.to_ne_bytes();
346        let y2 = y2.to_ne_bytes();
347        let u1 = u1.to_ne_bytes();
348        let u2 = u2.to_ne_bytes();
349        let v1 = v1.to_ne_bytes();
350        let v2 = v2.to_ne_bytes();
351
352        let mut top_left = [0u8; 16];
353        top_left[0..4].copy_from_slice(&x1);
354        top_left[4..8].copy_from_slice(&y1);
355        top_left[8..12].copy_from_slice(&u1);
356        top_left[12..16].copy_from_slice(&v1);
357
358        let mut bottom_right = [0u8; 16];
359        bottom_right[0..4].copy_from_slice(&x2);
360        bottom_right[4..8].copy_from_slice(&y2);
361        bottom_right[8..12].copy_from_slice(&u2);
362        bottom_right[12..16].copy_from_slice(&v2);
363
364        let mut top_right = [0u8; 16];
365        top_right[0..4].copy_from_slice(&x2);
366        top_right[4..8].copy_from_slice(&y1);
367        top_right[8..12].copy_from_slice(&u2);
368        top_right[12..16].copy_from_slice(&v1);
369
370        let mut bottom_left = [0u8; 16];
371        bottom_left[0..4].copy_from_slice(&x1);
372        bottom_left[4..8].copy_from_slice(&y2);
373        bottom_left[8..12].copy_from_slice(&u1);
374        bottom_left[12..16].copy_from_slice(&v2);
375
376        [
377            // First triangle
378            top_left,
379            bottom_right,
380            top_right,
381            // Second triangle
382            top_left,
383            bottom_left,
384            bottom_right,
385        ]
386    }
387}
388
389impl Glyph for BitmapGlyph {
390    #[inline(always)]
391    fn page_height(&self) -> u32 {
392        self.page_rect.height
393    }
394
395    #[inline(always)]
396    fn page_width(&self) -> u32 {
397        self.page_rect.width
398    }
399
400    #[inline(always)]
401    fn page_x(&self) -> u32 {
402        debug_assert!(self.page_rect.x >= 0);
403
404        self.page_rect.x as _
405    }
406
407    #[inline(always)]
408    fn page_y(&self) -> u32 {
409        debug_assert!(self.page_rect.y >= 0);
410
411        self.page_rect.y as _
412    }
413
414    #[inline(always)]
415    fn screen_height(&self) -> f32 {
416        self.screen_rect.height as _
417    }
418
419    #[inline(always)]
420    fn screen_width(&self) -> f32 {
421        self.screen_rect.width as _
422    }
423
424    #[inline(always)]
425    fn screen_x(&self) -> f32 {
426        self.screen_rect.x as _
427    }
428
429    #[inline(always)]
430    fn screen_y(&self) -> f32 {
431        self.screen_rect.y as _
432    }
433}
434
435#[cfg(test)]
436mod test {
437    use super::BitmapGlyphColor;
438
439    #[test]
440    fn glyph_color_from_f32_rgb_defaults_alpha_to_opaque() {
441        let color = BitmapGlyphColor::from([0.5, 0.25, 1.0]);
442
443        match color {
444            BitmapGlyphColor::Solid([127, 63, 255, 255]) => {}
445            other => panic!("unexpected glyph color: {other:?}"),
446        }
447    }
448
449    #[test]
450    fn glyph_color_from_f32_rgba_clamps_values() {
451        let color = BitmapGlyphColor::from([2.0, -1.0, 0.5, 0.25]);
452
453        match color {
454            BitmapGlyphColor::Solid([255, 0, 127, 63]) => {}
455            other => panic!("unexpected glyph color: {other:?}"),
456        }
457    }
458
459    #[test]
460    fn glyph_color_from_u8_rgb_defaults_alpha_to_opaque() {
461        let color = BitmapGlyphColor::from([1, 2, 3]);
462
463        match color {
464            BitmapGlyphColor::Solid([1, 2, 3, 255]) => {}
465            other => panic!("unexpected glyph color: {other:?}"),
466        }
467    }
468
469    #[test]
470    fn glyph_color_from_u8_rgba_preserves_channels() {
471        let color = BitmapGlyphColor::from([1, 2, 3, 4]);
472
473        match color {
474            BitmapGlyphColor::Solid([1, 2, 3, 4]) => {}
475            other => panic!("unexpected glyph color: {other:?}"),
476        }
477    }
478}