Skip to main content

anvilkit_render/renderer/
text.rs

1//! # 文字渲染器
2//!
3//! 提供基于位图字体图集的 ASCII 文字渲染功能。
4//!
5//! ## 设计
6//!
7//! 使用内置的 8×16 像素等宽 ASCII 字体图集(覆盖 ASCII 32-126),
8//! 每个字符渲染为一个纹理采样四边形。采用正交投影进行屏幕空间渲染。
9//!
10//! ## 使用示例
11//!
12//! ```rust,no_run
13//! use anvilkit_render::renderer::text::TextRenderer;
14//! use glam::Vec3;
15//!
16//! // 创建 TextRenderer(需要 GPU device 和 surface format)
17//! // let renderer = TextRenderer::new(&device, format);
18//!
19//! // 渲染文字
20//! // renderer.draw_text(&device, &mut encoder, &target, "Score: 42",
21//! //     10.0, 10.0, 16.0, Vec3::ONE, 1280.0, 720.0);
22//! ```
23
24use glam::{Mat4, Vec3};
25use wgpu::{
26    BindGroup, Buffer, CommandEncoder, RenderPipeline,
27    TextureView,
28};
29
30use crate::renderer::RenderDevice;
31use crate::renderer::buffer::{create_uniform_buffer, Vertex};
32use crate::renderer::pipeline::RenderPipelineBuilder;
33use crate::renderer::sprite::SpriteVertex;
34
35/// 字体图集 shader(复用 sprite shader:正交投影 + 纹理采样)
36const SPRITE_SHADER: &str = include_str!("../shaders/sprite.wgsl");
37
38/// 字形宽度(像素)
39const GLYPH_W: u32 = 8;
40/// 字形高度(像素)
41const GLYPH_H: u32 = 16;
42/// 每行字形数
43const GLYPHS_PER_ROW: u32 = 16;
44/// 总行数 (ceil(95 / 16) = 6)
45const GLYPH_ROWS: u32 = 6;
46/// 图集纹理宽度
47const ATLAS_W: u32 = GLYPHS_PER_ROW * GLYPH_W; // 128
48/// 图集纹理高度
49const ATLAS_H: u32 = GLYPH_ROWS * GLYPH_H; // 96
50
51/// ASCII 可打印字符起始码位
52const FIRST_CHAR: u8 = 32;
53/// ASCII 可打印字符结束码位(不含)
54const LAST_CHAR: u8 = 127;
55
56/// 正交投影 uniform(64 字节)
57#[repr(C)]
58#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
59struct OrthoUniform {
60    projection: [[f32; 4]; 4],
61}
62
63/// 文字渲染器
64///
65/// 使用内置位图字体图集渲染 ASCII 文本到屏幕空间。
66pub struct TextRenderer {
67    pipeline: RenderPipeline,
68    font_bind_group: BindGroup,
69    ortho_buffer: Buffer,
70    ortho_bind_group: BindGroup,
71    /// Cached vertex buffer for per-frame reuse
72    cached_vb: Option<(Buffer, u64)>,
73}
74
75impl TextRenderer {
76    /// 创建文字渲染器
77    ///
78    /// # 参数
79    ///
80    /// - `device`: GPU 渲染设备
81    /// - `format`: 渲染目标纹理格式
82    pub fn new(device: &RenderDevice, format: wgpu::TextureFormat) -> Self {
83        // Generate font atlas RGBA data
84        let atlas_data = generate_font_atlas();
85
86        // Create font texture
87        let texture_size = wgpu::Extent3d {
88            width: ATLAS_W,
89            height: ATLAS_H,
90            depth_or_array_layers: 1,
91        };
92        let font_texture = device.device().create_texture(&wgpu::TextureDescriptor {
93            label: Some("Font Atlas"),
94            size: texture_size,
95            mip_level_count: 1,
96            sample_count: 1,
97            dimension: wgpu::TextureDimension::D2,
98            format: wgpu::TextureFormat::Rgba8UnormSrgb,
99            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
100            view_formats: &[],
101        });
102        device.queue().write_texture(
103            wgpu::ImageCopyTexture {
104                texture: &font_texture,
105                mip_level: 0,
106                origin: wgpu::Origin3d::ZERO,
107                aspect: wgpu::TextureAspect::All,
108            },
109            &atlas_data,
110            wgpu::ImageDataLayout {
111                offset: 0,
112                bytes_per_row: Some(4 * ATLAS_W),
113                rows_per_image: Some(ATLAS_H),
114            },
115            texture_size,
116        );
117        let font_texture_view = font_texture.create_view(&wgpu::TextureViewDescriptor::default());
118        let font_sampler = device.device().create_sampler(&wgpu::SamplerDescriptor {
119            label: Some("Font Sampler"),
120            mag_filter: wgpu::FilterMode::Nearest,
121            min_filter: wgpu::FilterMode::Nearest,
122            ..Default::default()
123        });
124
125        // Ortho uniform
126        let ortho_uniform = OrthoUniform {
127            projection: Mat4::IDENTITY.to_cols_array_2d(),
128        };
129        let ortho_buffer = create_uniform_buffer(
130            device,
131            "Text Ortho Uniform",
132            bytemuck::bytes_of(&ortho_uniform),
133        );
134
135        // Bind group layouts
136        let ortho_bgl = device.device().create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
137            label: Some("Text Ortho BGL"),
138            entries: &[wgpu::BindGroupLayoutEntry {
139                binding: 0,
140                visibility: wgpu::ShaderStages::VERTEX,
141                ty: wgpu::BindingType::Buffer {
142                    ty: wgpu::BufferBindingType::Uniform,
143                    has_dynamic_offset: false,
144                    min_binding_size: None,
145                },
146                count: None,
147            }],
148        });
149
150        let font_bgl = device.device().create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
151            label: Some("Text Font BGL"),
152            entries: &[
153                wgpu::BindGroupLayoutEntry {
154                    binding: 0,
155                    visibility: wgpu::ShaderStages::FRAGMENT,
156                    ty: wgpu::BindingType::Texture {
157                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
158                        view_dimension: wgpu::TextureViewDimension::D2,
159                        multisampled: false,
160                    },
161                    count: None,
162                },
163                wgpu::BindGroupLayoutEntry {
164                    binding: 1,
165                    visibility: wgpu::ShaderStages::FRAGMENT,
166                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
167                    count: None,
168                },
169            ],
170        });
171
172        // Bind groups
173        let ortho_bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
174            label: Some("Text Ortho BG"),
175            layout: &ortho_bgl,
176            entries: &[wgpu::BindGroupEntry {
177                binding: 0,
178                resource: ortho_buffer.as_entire_binding(),
179            }],
180        });
181
182        let font_bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
183            label: Some("Text Font BG"),
184            layout: &font_bgl,
185            entries: &[
186                wgpu::BindGroupEntry {
187                    binding: 0,
188                    resource: wgpu::BindingResource::TextureView(&font_texture_view),
189                },
190                wgpu::BindGroupEntry {
191                    binding: 1,
192                    resource: wgpu::BindingResource::Sampler(&font_sampler),
193                },
194            ],
195        });
196
197        // Pipeline — reuse the bind group layouts created above (identical structure)
198        let pipeline = RenderPipelineBuilder::new()
199            .with_vertex_shader(SPRITE_SHADER)
200            .with_fragment_shader(SPRITE_SHADER)
201            .with_format(format)
202            .with_vertex_layouts(vec![SpriteVertex::layout()])
203            .with_bind_group_layouts(vec![ortho_bgl, font_bgl])
204            .with_label("Text Pipeline")
205            .build(device)
206            .expect("创建 Text 管线失败")
207            .into_pipeline();
208
209        Self {
210            pipeline,
211            font_bind_group,
212            ortho_buffer,
213            ortho_bind_group,
214            cached_vb: None,
215        }
216    }
217
218    /// 渲染文本到屏幕
219    ///
220    /// # 参数
221    ///
222    /// - `device`: GPU 渲染设备
223    /// - `encoder`: 命令编码器
224    /// - `target`: 渲染目标纹理视图
225    /// - `text`: 要渲染的 ASCII 文本
226    /// - `x`, `y`: 屏幕坐标(左上角起点,像素)
227    /// - `font_size`: 字体大小(像素高度)
228    /// - `color`: 文字颜色 (RGB)
229    /// - `screen_w`, `screen_h`: 屏幕尺寸(像素)
230    pub fn draw_text(
231        &mut self,
232        device: &RenderDevice,
233        encoder: &mut CommandEncoder,
234        target: &TextureView,
235        text: &str,
236        x: f32,
237        y: f32,
238        font_size: f32,
239        color: Vec3,
240        screen_w: f32,
241        screen_h: f32,
242    ) {
243        if text.is_empty() {
244            return;
245        }
246
247        // Update orthographic projection
248        let ortho = OrthoUniform {
249            projection: Mat4::orthographic_lh(0.0, screen_w, screen_h, 0.0, -1.0, 1.0)
250                .to_cols_array_2d(),
251        };
252        device
253            .queue()
254            .write_buffer(&self.ortho_buffer, 0, bytemuck::bytes_of(&ortho));
255
256        // Build quads
257        let glyph_w = font_size * (GLYPH_W as f32 / GLYPH_H as f32);
258        let glyph_h = font_size;
259
260        let mut vertices = Vec::with_capacity(text.len() * 6);
261        let mut cursor_x = x;
262
263        for ch in text.bytes() {
264            if ch < FIRST_CHAR || ch >= LAST_CHAR {
265                cursor_x += glyph_w;
266                continue;
267            }
268
269            let (u0, v0, u1, v1) = char_uv(ch);
270
271            // Two triangles per character (6 vertices)
272            let x0 = cursor_x;
273            let y0 = y;
274            let x1 = cursor_x + glyph_w;
275            let y1 = y + glyph_h;
276            let c = [color.x, color.y, color.z];
277
278            vertices.push(SpriteVertex { position: [x0, y0, 0.0], texcoord: [u0, v0], color: c });
279            vertices.push(SpriteVertex { position: [x1, y0, 0.0], texcoord: [u1, v0], color: c });
280            vertices.push(SpriteVertex { position: [x1, y1, 0.0], texcoord: [u1, v1], color: c });
281
282            vertices.push(SpriteVertex { position: [x0, y0, 0.0], texcoord: [u0, v0], color: c });
283            vertices.push(SpriteVertex { position: [x1, y1, 0.0], texcoord: [u1, v1], color: c });
284            vertices.push(SpriteVertex { position: [x0, y1, 0.0], texcoord: [u0, v1], color: c });
285
286            cursor_x += glyph_w;
287        }
288
289        if vertices.is_empty() {
290            return;
291        }
292
293        // Reuse cached buffer if large enough
294        let data: &[u8] = bytemuck::cast_slice(&vertices);
295        let needed = data.len() as u64;
296        let reuse = self.cached_vb.as_ref().map_or(false, |(_, cap)| *cap >= needed);
297        if !reuse {
298            self.cached_vb = Some((
299                device.device().create_buffer(&wgpu::BufferDescriptor {
300                    label: Some("Text VB (cached)"),
301                    size: needed,
302                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
303                    mapped_at_creation: false,
304                }),
305                needed,
306            ));
307        }
308        let vertex_buffer = &self.cached_vb.as_ref().unwrap().0;
309        device.queue().write_buffer(vertex_buffer, 0, data);
310
311        {
312            let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
313                label: Some("Text Pass"),
314                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
315                    view: target,
316                    resolve_target: None,
317                    ops: wgpu::Operations {
318                        load: wgpu::LoadOp::Load,
319                        store: wgpu::StoreOp::Store,
320                    },
321                })],
322                depth_stencil_attachment: None,
323                timestamp_writes: None,
324                occlusion_query_set: None,
325            });
326
327            rp.set_pipeline(&self.pipeline);
328            rp.set_bind_group(0, &self.ortho_bind_group, &[]);
329            rp.set_bind_group(1, &self.font_bind_group, &[]);
330            rp.set_vertex_buffer(0, vertex_buffer.slice(..));
331            rp.draw(0..vertices.len() as u32, 0..1);
332        }
333    }
334}
335
336/// 计算字符在图集中的 UV 坐标
337///
338/// # 返回
339///
340/// `(u0, v0, u1, v1)` — 左上角和右下角 UV 坐标
341fn char_uv(ascii: u8) -> (f32, f32, f32, f32) {
342    let idx = (ascii - FIRST_CHAR) as u32;
343    let col = idx % GLYPHS_PER_ROW;
344    let row = idx / GLYPHS_PER_ROW;
345
346    let u0 = (col * GLYPH_W) as f32 / ATLAS_W as f32;
347    let v0 = (row * GLYPH_H) as f32 / ATLAS_H as f32;
348    let u1 = ((col + 1) * GLYPH_W) as f32 / ATLAS_W as f32;
349    let v1 = ((row + 1) * GLYPH_H) as f32 / ATLAS_H as f32;
350
351    (u0, v0, u1, v1)
352}
353
354/// 生成内置 8×16 ASCII 位图字体图集
355///
356/// 使用硬编码的像素数据生成 128×96 RGBA 纹理。
357/// 覆盖 ASCII 32-126(95 个可打印字符)。
358fn generate_font_atlas() -> Vec<u8> {
359    // Use a minimal embedded bitmap font (CP437 style, 8x16 per glyph)
360    // Each character is stored as 16 bytes (one byte per row, MSB = leftmost pixel)
361    let font_data = include_cp437_font();
362
363    let mut rgba = vec![0u8; (ATLAS_W * ATLAS_H * 4) as usize];
364
365    for char_idx in 0..95u32 {
366        let col = char_idx % GLYPHS_PER_ROW;
367        let row = char_idx / GLYPHS_PER_ROW;
368        let glyph = &font_data[char_idx as usize];
369
370        for gy in 0..GLYPH_H {
371            let bits = glyph[gy as usize];
372            for gx in 0..GLYPH_W {
373                let px = col * GLYPH_W + gx;
374                let py = row * GLYPH_H + gy;
375                let offset = ((py * ATLAS_W + px) * 4) as usize;
376
377                let on = (bits >> (7 - gx)) & 1 != 0;
378                let val = if on { 255u8 } else { 0u8 };
379                rgba[offset] = val;
380                rgba[offset + 1] = val;
381                rgba[offset + 2] = val;
382                rgba[offset + 3] = val; // alpha = val (transparent for off pixels)
383            }
384        }
385    }
386
387    rgba
388}
389
390/// 内置 CP437 风格 8×16 位图字体
391///
392/// 返回 95 个字形(ASCII 32-126),每个字形 16 行,每行 1 字节(MSB = 左侧像素)。
393fn include_cp437_font() -> Vec<[u8; 16]> {
394    // Minimal readable 8x16 bitmap font data
395    // Each glyph is 16 bytes: one byte per row, MSB = leftmost pixel
396    let raw: &[u8] = &[
397        // Space (32)
398        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
399        // ! (33)
400        0x00,0x00,0x18,0x3C,0x3C,0x3C,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00,
401        // " (34)
402        0x00,0x66,0x66,0x66,0x24,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
403        // # (35)
404        0x00,0x00,0x00,0x6C,0x6C,0xFE,0x6C,0x6C,0x6C,0xFE,0x6C,0x6C,0x00,0x00,0x00,0x00,
405        // $ (36)
406        0x18,0x18,0x7C,0xC6,0xC2,0xC0,0x7C,0x06,0x06,0x86,0xC6,0x7C,0x18,0x18,0x00,0x00,
407        // % (37)
408        0x00,0x00,0x00,0x00,0xC2,0xC6,0x0C,0x18,0x30,0x60,0xC6,0x86,0x00,0x00,0x00,0x00,
409        // & (38)
410        0x00,0x00,0x38,0x6C,0x6C,0x38,0x76,0xDC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00,
411        // ' (39)
412        0x00,0x30,0x30,0x30,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
413        // ( (40)
414        0x00,0x00,0x0C,0x18,0x30,0x30,0x30,0x30,0x30,0x30,0x18,0x0C,0x00,0x00,0x00,0x00,
415        // ) (41)
416        0x00,0x00,0x30,0x18,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x18,0x30,0x00,0x00,0x00,0x00,
417        // * (42)
418        0x00,0x00,0x00,0x00,0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00,0x00,0x00,0x00,0x00,
419        // + (43)
420        0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,
421        // , (44)
422        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x30,0x00,0x00,0x00,
423        // - (45)
424        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
425        // . (46)
426        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,
427        // / (47)
428        0x00,0x00,0x00,0x00,0x02,0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00,0x00,0x00,0x00,
429        // 0 (48)
430        0x00,0x00,0x7C,0xC6,0xC6,0xCE,0xDE,0xF6,0xE6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
431        // 1 (49)
432        0x00,0x00,0x18,0x38,0x78,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x00,0x00,0x00,0x00,
433        // 2 (50)
434        0x00,0x00,0x7C,0xC6,0x06,0x0C,0x18,0x30,0x60,0xC0,0xC6,0xFE,0x00,0x00,0x00,0x00,
435        // 3 (51)
436        0x00,0x00,0x7C,0xC6,0x06,0x06,0x3C,0x06,0x06,0x06,0xC6,0x7C,0x00,0x00,0x00,0x00,
437        // 4 (52)
438        0x00,0x00,0x0C,0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x0C,0x1E,0x00,0x00,0x00,0x00,
439        // 5 (53)
440        0x00,0x00,0xFE,0xC0,0xC0,0xC0,0xFC,0x06,0x06,0x06,0xC6,0x7C,0x00,0x00,0x00,0x00,
441        // 6 (54)
442        0x00,0x00,0x38,0x60,0xC0,0xC0,0xFC,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
443        // 7 (55)
444        0x00,0x00,0xFE,0xC6,0x06,0x06,0x0C,0x18,0x30,0x30,0x30,0x30,0x00,0x00,0x00,0x00,
445        // 8 (56)
446        0x00,0x00,0x7C,0xC6,0xC6,0xC6,0x7C,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
447        // 9 (57)
448        0x00,0x00,0x7C,0xC6,0xC6,0xC6,0x7E,0x06,0x06,0x06,0x0C,0x78,0x00,0x00,0x00,0x00,
449        // : (58)
450        0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x00,
451        // ; (59)
452        0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x30,0x00,0x00,0x00,0x00,
453        // < (60)
454        0x00,0x00,0x00,0x06,0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x06,0x00,0x00,0x00,0x00,
455        // = (61)
456        0x00,0x00,0x00,0x00,0x00,0x7E,0x00,0x00,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
457        // > (62)
458        0x00,0x00,0x00,0x60,0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x60,0x00,0x00,0x00,0x00,
459        // ? (63)
460        0x00,0x00,0x7C,0xC6,0xC6,0x0C,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00,
461        // @ (64)
462        0x00,0x00,0x00,0x7C,0xC6,0xC6,0xDE,0xDE,0xDE,0xDC,0xC0,0x7C,0x00,0x00,0x00,0x00,
463        // A (65)
464        0x00,0x00,0x10,0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00,
465        // B (66)
466        0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x66,0x66,0x66,0x66,0xFC,0x00,0x00,0x00,0x00,
467        // C (67)
468        0x00,0x00,0x3C,0x66,0xC2,0xC0,0xC0,0xC0,0xC0,0xC2,0x66,0x3C,0x00,0x00,0x00,0x00,
469        // D (68)
470        0x00,0x00,0xF8,0x6C,0x66,0x66,0x66,0x66,0x66,0x66,0x6C,0xF8,0x00,0x00,0x00,0x00,
471        // E (69)
472        0x00,0x00,0xFE,0x66,0x62,0x68,0x78,0x68,0x60,0x62,0x66,0xFE,0x00,0x00,0x00,0x00,
473        // F (70)
474        0x00,0x00,0xFE,0x66,0x62,0x68,0x78,0x68,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00,
475        // G (71)
476        0x00,0x00,0x3C,0x66,0xC2,0xC0,0xC0,0xDE,0xC6,0xC6,0x66,0x3A,0x00,0x00,0x00,0x00,
477        // H (72)
478        0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00,
479        // I (73)
480        0x00,0x00,0x3C,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00,
481        // J (74)
482        0x00,0x00,0x1E,0x0C,0x0C,0x0C,0x0C,0x0C,0xCC,0xCC,0xCC,0x78,0x00,0x00,0x00,0x00,
483        // K (75)
484        0x00,0x00,0xE6,0x66,0x66,0x6C,0x78,0x78,0x6C,0x66,0x66,0xE6,0x00,0x00,0x00,0x00,
485        // L (76)
486        0x00,0x00,0xF0,0x60,0x60,0x60,0x60,0x60,0x60,0x62,0x66,0xFE,0x00,0x00,0x00,0x00,
487        // M (77)
488        0x00,0x00,0xC6,0xEE,0xFE,0xFE,0xD6,0xC6,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00,
489        // N (78)
490        0x00,0x00,0xC6,0xE6,0xF6,0xFE,0xDE,0xCE,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00,
491        // O (79)
492        0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
493        // P (80)
494        0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x60,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00,
495        // Q (81)
496        0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xD6,0xDE,0x7C,0x0C,0x0E,0x00,0x00,
497        // R (82)
498        0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x6C,0x66,0x66,0x66,0xE6,0x00,0x00,0x00,0x00,
499        // S (83)
500        0x00,0x00,0x7C,0xC6,0xC6,0x60,0x38,0x0C,0x06,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
501        // T (84)
502        0x00,0x00,0xFF,0xDB,0x99,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00,
503        // U (85)
504        0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
505        // V (86)
506        0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00,0x00,0x00,0x00,
507        // W (87)
508        0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xD6,0xD6,0xD6,0xFE,0xEE,0x6C,0x00,0x00,0x00,0x00,
509        // X (88)
510        0x00,0x00,0xC6,0xC6,0x6C,0x7C,0x38,0x38,0x7C,0x6C,0xC6,0xC6,0x00,0x00,0x00,0x00,
511        // Y (89)
512        0x00,0x00,0xC6,0xC6,0xC6,0x6C,0x38,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00,
513        // Z (90)
514        0x00,0x00,0xFE,0xC6,0x86,0x0C,0x18,0x30,0x60,0xC2,0xC6,0xFE,0x00,0x00,0x00,0x00,
515        // [ (91)
516        0x00,0x00,0x3C,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x3C,0x00,0x00,0x00,0x00,
517        // \ (92)
518        0x00,0x00,0x00,0x80,0xC0,0xE0,0x70,0x38,0x1C,0x0E,0x06,0x02,0x00,0x00,0x00,0x00,
519        // ] (93)
520        0x00,0x00,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00,0x00,0x00,0x00,
521        // ^ (94)
522        0x10,0x38,0x6C,0xC6,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
523        // _ (95)
524        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,
525        // ` (96)
526        0x30,0x30,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
527        // a (97)
528        0x00,0x00,0x00,0x00,0x00,0x78,0x0C,0x7C,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00,
529        // b (98)
530        0x00,0x00,0xE0,0x60,0x60,0x78,0x6C,0x66,0x66,0x66,0x66,0x7C,0x00,0x00,0x00,0x00,
531        // c (99)
532        0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00,0x00,0x00,0x00,
533        // d (100)
534        0x00,0x00,0x1C,0x0C,0x0C,0x3C,0x6C,0xCC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00,
535        // e (101)
536        0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xFE,0xC0,0xC0,0xC6,0x7C,0x00,0x00,0x00,0x00,
537        // f (102)
538        0x00,0x00,0x38,0x6C,0x64,0x60,0xF0,0x60,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00,
539        // g (103)
540        0x00,0x00,0x00,0x00,0x00,0x76,0xCC,0xCC,0xCC,0xCC,0xCC,0x7C,0x0C,0xCC,0x78,0x00,
541        // h (104)
542        0x00,0x00,0xE0,0x60,0x60,0x6C,0x76,0x66,0x66,0x66,0x66,0xE6,0x00,0x00,0x00,0x00,
543        // i (105)
544        0x00,0x00,0x18,0x18,0x00,0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00,
545        // j (106)
546        0x00,0x00,0x06,0x06,0x00,0x0E,0x06,0x06,0x06,0x06,0x06,0x06,0x66,0x66,0x3C,0x00,
547        // k (107)
548        0x00,0x00,0xE0,0x60,0x60,0x66,0x6C,0x78,0x78,0x6C,0x66,0xE6,0x00,0x00,0x00,0x00,
549        // l (108)
550        0x00,0x00,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00,
551        // m (109)
552        0x00,0x00,0x00,0x00,0x00,0xEC,0xFE,0xD6,0xD6,0xD6,0xD6,0xC6,0x00,0x00,0x00,0x00,
553        // n (110)
554        0x00,0x00,0x00,0x00,0x00,0xDC,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00,0x00,
555        // o (111)
556        0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
557        // p (112)
558        0x00,0x00,0x00,0x00,0x00,0xDC,0x66,0x66,0x66,0x66,0x66,0x7C,0x60,0x60,0xF0,0x00,
559        // q (113)
560        0x00,0x00,0x00,0x00,0x00,0x76,0xCC,0xCC,0xCC,0xCC,0xCC,0x7C,0x0C,0x0C,0x1E,0x00,
561        // r (114)
562        0x00,0x00,0x00,0x00,0x00,0xDC,0x76,0x66,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00,
563        // s (115)
564        0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0x60,0x38,0x0C,0xC6,0x7C,0x00,0x00,0x00,0x00,
565        // t (116)
566        0x00,0x00,0x10,0x30,0x30,0xFC,0x30,0x30,0x30,0x30,0x36,0x1C,0x00,0x00,0x00,0x00,
567        // u (117)
568        0x00,0x00,0x00,0x00,0x00,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00,
569        // v (118)
570        0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00,0x00,0x00,0x00,
571        // w (119)
572        0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xD6,0xD6,0xD6,0xFE,0x6C,0x00,0x00,0x00,0x00,
573        // x (120)
574        0x00,0x00,0x00,0x00,0x00,0xC6,0x6C,0x38,0x38,0x38,0x6C,0xC6,0x00,0x00,0x00,0x00,
575        // y (121)
576        0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7E,0x06,0x0C,0xF8,0x00,
577        // z (122)
578        0x00,0x00,0x00,0x00,0x00,0xFE,0xCC,0x18,0x30,0x60,0xC6,0xFE,0x00,0x00,0x00,0x00,
579        // { (123)
580        0x00,0x00,0x0E,0x18,0x18,0x18,0x70,0x18,0x18,0x18,0x18,0x0E,0x00,0x00,0x00,0x00,
581        // | (124)
582        0x00,0x00,0x18,0x18,0x18,0x18,0x00,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00,
583        // } (125)
584        0x00,0x00,0x70,0x18,0x18,0x18,0x0E,0x18,0x18,0x18,0x18,0x70,0x00,0x00,0x00,0x00,
585        // ~ (126)
586        0x00,0x00,0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
587    ];
588
589    let mut glyphs = Vec::with_capacity(95);
590    for i in 0..95 {
591        let mut glyph = [0u8; 16];
592        glyph.copy_from_slice(&raw[i * 16..(i + 1) * 16]);
593        glyphs.push(glyph);
594    }
595    glyphs
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    #[test]
603    fn test_char_uv_space() {
604        let (u0, v0, u1, v1) = char_uv(b' ');
605        assert_eq!(u0, 0.0);
606        assert_eq!(v0, 0.0);
607        assert!((u1 - GLYPH_W as f32 / ATLAS_W as f32).abs() < 1e-5);
608        assert!((v1 - GLYPH_H as f32 / ATLAS_H as f32).abs() < 1e-5);
609    }
610
611    #[test]
612    fn test_char_uv_a() {
613        let idx = b'A' - FIRST_CHAR;
614        let col = idx as u32 % GLYPHS_PER_ROW;
615        let row = idx as u32 / GLYPHS_PER_ROW;
616
617        let (u0, v0, _, _) = char_uv(b'A');
618        assert!((u0 - (col * GLYPH_W) as f32 / ATLAS_W as f32).abs() < 1e-5);
619        assert!((v0 - (row * GLYPH_H) as f32 / ATLAS_H as f32).abs() < 1e-5);
620    }
621
622    #[test]
623    fn test_font_atlas_size() {
624        let atlas = generate_font_atlas();
625        assert_eq!(atlas.len(), (ATLAS_W * ATLAS_H * 4) as usize);
626    }
627
628    #[test]
629    fn test_font_data_glyph_count() {
630        let glyphs = include_cp437_font();
631        assert_eq!(glyphs.len(), 95);
632    }
633
634    #[test]
635    fn test_space_glyph_is_empty() {
636        let glyphs = include_cp437_font();
637        let space = &glyphs[0]; // Space is first char (32 - 32 = 0)
638        for &row in space.iter() {
639            assert_eq!(row, 0, "Space glyph should be all zeros");
640        }
641    }
642
643    #[test]
644    fn test_char_uv_bounds() {
645        for ch in FIRST_CHAR..LAST_CHAR {
646            let (u0, v0, u1, v1) = char_uv(ch);
647            assert!(u0 >= 0.0 && u0 <= 1.0, "u0 out of bounds for char {}", ch);
648            assert!(v0 >= 0.0 && v0 <= 1.0, "v0 out of bounds for char {}", ch);
649            assert!(u1 >= 0.0 && u1 <= 1.0, "u1 out of bounds for char {}", ch);
650            assert!(v1 >= 0.0 && v1 <= 1.0, "v1 out of bounds for char {}", ch);
651            assert!(u1 > u0);
652            assert!(v1 > v0);
653        }
654    }
655}