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#[derive(Debug)]
38pub struct BitmapFont {
39 font: BMFont,
40 pages: Vec<Arc<Image>>,
41 pipeline: GraphicsPipeline,
42 pool: LazyPool,
43}
44
45impl BitmapFont {
46 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 pub fn measure(&self, text: &str) -> ([i32; 2], [u32; 2]) {
82 let parse = self.font.parse(text);
83
84 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 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 #[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 #[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#[derive(Debug)]
255pub enum BitmapGlyphColor {
256 Outline(Color),
258
259 Solid(Color),
261
262 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
321pub 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 top_left,
379 bottom_right,
380 top_right,
381 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}