anvilkit-render 0.1.0

Cross-platform rendering system built on wgpu and winit for AnvilKit game engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
//! # 文字渲染器
//!
//! 提供基于位图字体图集的 ASCII 文字渲染功能。
//!
//! ## 设计
//!
//! 使用内置的 8×16 像素等宽 ASCII 字体图集(覆盖 ASCII 32-126),
//! 每个字符渲染为一个纹理采样四边形。采用正交投影进行屏幕空间渲染。
//!
//! ## 使用示例
//!
//! ```rust,no_run
//! use anvilkit_render::renderer::text::TextRenderer;
//! use glam::Vec3;
//!
//! // 创建 TextRenderer(需要 GPU device 和 surface format)
//! // let renderer = TextRenderer::new(&device, format);
//!
//! // 渲染文字
//! // renderer.draw_text(&device, &mut encoder, &target, "Score: 42",
//! //     10.0, 10.0, 16.0, Vec3::ONE, 1280.0, 720.0);
//! ```

use glam::{Mat4, Vec3};
use wgpu::{
    BindGroup, Buffer, CommandEncoder, RenderPipeline,
    TextureView,
};

use crate::renderer::RenderDevice;
use crate::renderer::buffer::{create_uniform_buffer, Vertex};
use crate::renderer::pipeline::RenderPipelineBuilder;
use crate::renderer::sprite::SpriteVertex;

/// 字体图集 shader(复用 sprite shader:正交投影 + 纹理采样)
const SPRITE_SHADER: &str = include_str!("../shaders/sprite.wgsl");

/// 字形宽度(像素)
const GLYPH_W: u32 = 8;
/// 字形高度(像素)
const GLYPH_H: u32 = 16;
/// 每行字形数
const GLYPHS_PER_ROW: u32 = 16;
/// 总行数 (ceil(95 / 16) = 6)
const GLYPH_ROWS: u32 = 6;
/// 图集纹理宽度
const ATLAS_W: u32 = GLYPHS_PER_ROW * GLYPH_W; // 128
/// 图集纹理高度
const ATLAS_H: u32 = GLYPH_ROWS * GLYPH_H; // 96

/// ASCII 可打印字符起始码位
const FIRST_CHAR: u8 = 32;
/// ASCII 可打印字符结束码位(不含)
const LAST_CHAR: u8 = 127;

/// 正交投影 uniform(64 字节)
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct OrthoUniform {
    projection: [[f32; 4]; 4],
}

/// 文字渲染器
///
/// 使用内置位图字体图集渲染 ASCII 文本到屏幕空间。
pub struct TextRenderer {
    pipeline: RenderPipeline,
    font_bind_group: BindGroup,
    ortho_buffer: Buffer,
    ortho_bind_group: BindGroup,
    /// Cached vertex buffer for per-frame reuse
    cached_vb: Option<(Buffer, u64)>,
}

impl TextRenderer {
    /// 创建文字渲染器
    ///
    /// # 参数
    ///
    /// - `device`: GPU 渲染设备
    /// - `format`: 渲染目标纹理格式
    pub fn new(device: &RenderDevice, format: wgpu::TextureFormat) -> Self {
        // Generate font atlas RGBA data
        let atlas_data = generate_font_atlas();

        // Create font texture
        let texture_size = wgpu::Extent3d {
            width: ATLAS_W,
            height: ATLAS_H,
            depth_or_array_layers: 1,
        };
        let font_texture = device.device().create_texture(&wgpu::TextureDescriptor {
            label: Some("Font Atlas"),
            size: texture_size,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba8UnormSrgb,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });
        device.queue().write_texture(
            wgpu::ImageCopyTexture {
                texture: &font_texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            &atlas_data,
            wgpu::ImageDataLayout {
                offset: 0,
                bytes_per_row: Some(4 * ATLAS_W),
                rows_per_image: Some(ATLAS_H),
            },
            texture_size,
        );
        let font_texture_view = font_texture.create_view(&wgpu::TextureViewDescriptor::default());
        let font_sampler = device.device().create_sampler(&wgpu::SamplerDescriptor {
            label: Some("Font Sampler"),
            mag_filter: wgpu::FilterMode::Nearest,
            min_filter: wgpu::FilterMode::Nearest,
            ..Default::default()
        });

        // Ortho uniform
        let ortho_uniform = OrthoUniform {
            projection: Mat4::IDENTITY.to_cols_array_2d(),
        };
        let ortho_buffer = create_uniform_buffer(
            device,
            "Text Ortho Uniform",
            bytemuck::bytes_of(&ortho_uniform),
        );

        // Bind group layouts
        let ortho_bgl = device.device().create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("Text Ortho BGL"),
            entries: &[wgpu::BindGroupLayoutEntry {
                binding: 0,
                visibility: wgpu::ShaderStages::VERTEX,
                ty: wgpu::BindingType::Buffer {
                    ty: wgpu::BufferBindingType::Uniform,
                    has_dynamic_offset: false,
                    min_binding_size: None,
                },
                count: None,
            }],
        });

        let font_bgl = device.device().create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("Text Font BGL"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                    count: None,
                },
            ],
        });

        // Bind groups
        let ortho_bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Text Ortho BG"),
            layout: &ortho_bgl,
            entries: &[wgpu::BindGroupEntry {
                binding: 0,
                resource: ortho_buffer.as_entire_binding(),
            }],
        });

        let font_bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Text Font BG"),
            layout: &font_bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&font_texture_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(&font_sampler),
                },
            ],
        });

        // Pipeline — reuse the bind group layouts created above (identical structure)
        let pipeline = RenderPipelineBuilder::new()
            .with_vertex_shader(SPRITE_SHADER)
            .with_fragment_shader(SPRITE_SHADER)
            .with_format(format)
            .with_vertex_layouts(vec![SpriteVertex::layout()])
            .with_bind_group_layouts(vec![ortho_bgl, font_bgl])
            .with_label("Text Pipeline")
            .build(device)
            .expect("创建 Text 管线失败")
            .into_pipeline();

        Self {
            pipeline,
            font_bind_group,
            ortho_buffer,
            ortho_bind_group,
            cached_vb: None,
        }
    }

    /// 渲染文本到屏幕
    ///
    /// # 参数
    ///
    /// - `device`: GPU 渲染设备
    /// - `encoder`: 命令编码器
    /// - `target`: 渲染目标纹理视图
    /// - `text`: 要渲染的 ASCII 文本
    /// - `x`, `y`: 屏幕坐标(左上角起点,像素)
    /// - `font_size`: 字体大小(像素高度)
    /// - `color`: 文字颜色 (RGB)
    /// - `screen_w`, `screen_h`: 屏幕尺寸(像素)
    pub fn draw_text(
        &mut self,
        device: &RenderDevice,
        encoder: &mut CommandEncoder,
        target: &TextureView,
        text: &str,
        x: f32,
        y: f32,
        font_size: f32,
        color: Vec3,
        screen_w: f32,
        screen_h: f32,
    ) {
        if text.is_empty() {
            return;
        }

        // Update orthographic projection
        let ortho = OrthoUniform {
            projection: Mat4::orthographic_lh(0.0, screen_w, screen_h, 0.0, -1.0, 1.0)
                .to_cols_array_2d(),
        };
        device
            .queue()
            .write_buffer(&self.ortho_buffer, 0, bytemuck::bytes_of(&ortho));

        // Build quads
        let glyph_w = font_size * (GLYPH_W as f32 / GLYPH_H as f32);
        let glyph_h = font_size;

        let mut vertices = Vec::with_capacity(text.len() * 6);
        let mut cursor_x = x;

        for ch in text.bytes() {
            if ch < FIRST_CHAR || ch >= LAST_CHAR {
                cursor_x += glyph_w;
                continue;
            }

            let (u0, v0, u1, v1) = char_uv(ch);

            // Two triangles per character (6 vertices)
            let x0 = cursor_x;
            let y0 = y;
            let x1 = cursor_x + glyph_w;
            let y1 = y + glyph_h;
            let c = [color.x, color.y, color.z];

            vertices.push(SpriteVertex { position: [x0, y0, 0.0], texcoord: [u0, v0], color: c });
            vertices.push(SpriteVertex { position: [x1, y0, 0.0], texcoord: [u1, v0], color: c });
            vertices.push(SpriteVertex { position: [x1, y1, 0.0], texcoord: [u1, v1], color: c });

            vertices.push(SpriteVertex { position: [x0, y0, 0.0], texcoord: [u0, v0], color: c });
            vertices.push(SpriteVertex { position: [x1, y1, 0.0], texcoord: [u1, v1], color: c });
            vertices.push(SpriteVertex { position: [x0, y1, 0.0], texcoord: [u0, v1], color: c });

            cursor_x += glyph_w;
        }

        if vertices.is_empty() {
            return;
        }

        // Reuse cached buffer if large enough
        let data: &[u8] = bytemuck::cast_slice(&vertices);
        let needed = data.len() as u64;
        let reuse = self.cached_vb.as_ref().map_or(false, |(_, cap)| *cap >= needed);
        if !reuse {
            self.cached_vb = Some((
                device.device().create_buffer(&wgpu::BufferDescriptor {
                    label: Some("Text VB (cached)"),
                    size: needed,
                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
                    mapped_at_creation: false,
                }),
                needed,
            ));
        }
        let vertex_buffer = &self.cached_vb.as_ref().unwrap().0;
        device.queue().write_buffer(vertex_buffer, 0, data);

        {
            let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Text Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: target,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Load,
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
            });

            rp.set_pipeline(&self.pipeline);
            rp.set_bind_group(0, &self.ortho_bind_group, &[]);
            rp.set_bind_group(1, &self.font_bind_group, &[]);
            rp.set_vertex_buffer(0, vertex_buffer.slice(..));
            rp.draw(0..vertices.len() as u32, 0..1);
        }
    }
}

/// 计算字符在图集中的 UV 坐标
///
/// # 返回
///
/// `(u0, v0, u1, v1)` — 左上角和右下角 UV 坐标
fn char_uv(ascii: u8) -> (f32, f32, f32, f32) {
    let idx = (ascii - FIRST_CHAR) as u32;
    let col = idx % GLYPHS_PER_ROW;
    let row = idx / GLYPHS_PER_ROW;

    let u0 = (col * GLYPH_W) as f32 / ATLAS_W as f32;
    let v0 = (row * GLYPH_H) as f32 / ATLAS_H as f32;
    let u1 = ((col + 1) * GLYPH_W) as f32 / ATLAS_W as f32;
    let v1 = ((row + 1) * GLYPH_H) as f32 / ATLAS_H as f32;

    (u0, v0, u1, v1)
}

/// 生成内置 8×16 ASCII 位图字体图集
///
/// 使用硬编码的像素数据生成 128×96 RGBA 纹理。
/// 覆盖 ASCII 32-126(95 个可打印字符)。
fn generate_font_atlas() -> Vec<u8> {
    // Use a minimal embedded bitmap font (CP437 style, 8x16 per glyph)
    // Each character is stored as 16 bytes (one byte per row, MSB = leftmost pixel)
    let font_data = include_cp437_font();

    let mut rgba = vec![0u8; (ATLAS_W * ATLAS_H * 4) as usize];

    for char_idx in 0..95u32 {
        let col = char_idx % GLYPHS_PER_ROW;
        let row = char_idx / GLYPHS_PER_ROW;
        let glyph = &font_data[char_idx as usize];

        for gy in 0..GLYPH_H {
            let bits = glyph[gy as usize];
            for gx in 0..GLYPH_W {
                let px = col * GLYPH_W + gx;
                let py = row * GLYPH_H + gy;
                let offset = ((py * ATLAS_W + px) * 4) as usize;

                let on = (bits >> (7 - gx)) & 1 != 0;
                let val = if on { 255u8 } else { 0u8 };
                rgba[offset] = val;
                rgba[offset + 1] = val;
                rgba[offset + 2] = val;
                rgba[offset + 3] = val; // alpha = val (transparent for off pixels)
            }
        }
    }

    rgba
}

/// 内置 CP437 风格 8×16 位图字体
///
/// 返回 95 个字形(ASCII 32-126),每个字形 16 行,每行 1 字节(MSB = 左侧像素)。
fn include_cp437_font() -> Vec<[u8; 16]> {
    // Minimal readable 8x16 bitmap font data
    // Each glyph is 16 bytes: one byte per row, MSB = leftmost pixel
    let raw: &[u8] = &[
        // Space (32)
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        // ! (33)
        0x00,0x00,0x18,0x3C,0x3C,0x3C,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00,
        // " (34)
        0x00,0x66,0x66,0x66,0x24,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        // # (35)
        0x00,0x00,0x00,0x6C,0x6C,0xFE,0x6C,0x6C,0x6C,0xFE,0x6C,0x6C,0x00,0x00,0x00,0x00,
        // $ (36)
        0x18,0x18,0x7C,0xC6,0xC2,0xC0,0x7C,0x06,0x06,0x86,0xC6,0x7C,0x18,0x18,0x00,0x00,
        // % (37)
        0x00,0x00,0x00,0x00,0xC2,0xC6,0x0C,0x18,0x30,0x60,0xC6,0x86,0x00,0x00,0x00,0x00,
        // & (38)
        0x00,0x00,0x38,0x6C,0x6C,0x38,0x76,0xDC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00,
        // ' (39)
        0x00,0x30,0x30,0x30,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        // ( (40)
        0x00,0x00,0x0C,0x18,0x30,0x30,0x30,0x30,0x30,0x30,0x18,0x0C,0x00,0x00,0x00,0x00,
        // ) (41)
        0x00,0x00,0x30,0x18,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x18,0x30,0x00,0x00,0x00,0x00,
        // * (42)
        0x00,0x00,0x00,0x00,0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00,0x00,0x00,0x00,0x00,
        // + (43)
        0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,
        // , (44)
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x30,0x00,0x00,0x00,
        // - (45)
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        // . (46)
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,
        // / (47)
        0x00,0x00,0x00,0x00,0x02,0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00,0x00,0x00,0x00,
        // 0 (48)
        0x00,0x00,0x7C,0xC6,0xC6,0xCE,0xDE,0xF6,0xE6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // 1 (49)
        0x00,0x00,0x18,0x38,0x78,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x00,0x00,0x00,0x00,
        // 2 (50)
        0x00,0x00,0x7C,0xC6,0x06,0x0C,0x18,0x30,0x60,0xC0,0xC6,0xFE,0x00,0x00,0x00,0x00,
        // 3 (51)
        0x00,0x00,0x7C,0xC6,0x06,0x06,0x3C,0x06,0x06,0x06,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // 4 (52)
        0x00,0x00,0x0C,0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x0C,0x1E,0x00,0x00,0x00,0x00,
        // 5 (53)
        0x00,0x00,0xFE,0xC0,0xC0,0xC0,0xFC,0x06,0x06,0x06,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // 6 (54)
        0x00,0x00,0x38,0x60,0xC0,0xC0,0xFC,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // 7 (55)
        0x00,0x00,0xFE,0xC6,0x06,0x06,0x0C,0x18,0x30,0x30,0x30,0x30,0x00,0x00,0x00,0x00,
        // 8 (56)
        0x00,0x00,0x7C,0xC6,0xC6,0xC6,0x7C,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // 9 (57)
        0x00,0x00,0x7C,0xC6,0xC6,0xC6,0x7E,0x06,0x06,0x06,0x0C,0x78,0x00,0x00,0x00,0x00,
        // : (58)
        0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x00,
        // ; (59)
        0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x30,0x00,0x00,0x00,0x00,
        // < (60)
        0x00,0x00,0x00,0x06,0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x06,0x00,0x00,0x00,0x00,
        // = (61)
        0x00,0x00,0x00,0x00,0x00,0x7E,0x00,0x00,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        // > (62)
        0x00,0x00,0x00,0x60,0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x60,0x00,0x00,0x00,0x00,
        // ? (63)
        0x00,0x00,0x7C,0xC6,0xC6,0x0C,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00,
        // @ (64)
        0x00,0x00,0x00,0x7C,0xC6,0xC6,0xDE,0xDE,0xDE,0xDC,0xC0,0x7C,0x00,0x00,0x00,0x00,
        // A (65)
        0x00,0x00,0x10,0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00,
        // B (66)
        0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x66,0x66,0x66,0x66,0xFC,0x00,0x00,0x00,0x00,
        // C (67)
        0x00,0x00,0x3C,0x66,0xC2,0xC0,0xC0,0xC0,0xC0,0xC2,0x66,0x3C,0x00,0x00,0x00,0x00,
        // D (68)
        0x00,0x00,0xF8,0x6C,0x66,0x66,0x66,0x66,0x66,0x66,0x6C,0xF8,0x00,0x00,0x00,0x00,
        // E (69)
        0x00,0x00,0xFE,0x66,0x62,0x68,0x78,0x68,0x60,0x62,0x66,0xFE,0x00,0x00,0x00,0x00,
        // F (70)
        0x00,0x00,0xFE,0x66,0x62,0x68,0x78,0x68,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00,
        // G (71)
        0x00,0x00,0x3C,0x66,0xC2,0xC0,0xC0,0xDE,0xC6,0xC6,0x66,0x3A,0x00,0x00,0x00,0x00,
        // H (72)
        0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00,
        // I (73)
        0x00,0x00,0x3C,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00,
        // J (74)
        0x00,0x00,0x1E,0x0C,0x0C,0x0C,0x0C,0x0C,0xCC,0xCC,0xCC,0x78,0x00,0x00,0x00,0x00,
        // K (75)
        0x00,0x00,0xE6,0x66,0x66,0x6C,0x78,0x78,0x6C,0x66,0x66,0xE6,0x00,0x00,0x00,0x00,
        // L (76)
        0x00,0x00,0xF0,0x60,0x60,0x60,0x60,0x60,0x60,0x62,0x66,0xFE,0x00,0x00,0x00,0x00,
        // M (77)
        0x00,0x00,0xC6,0xEE,0xFE,0xFE,0xD6,0xC6,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00,
        // N (78)
        0x00,0x00,0xC6,0xE6,0xF6,0xFE,0xDE,0xCE,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00,
        // O (79)
        0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // P (80)
        0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x60,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00,
        // Q (81)
        0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xD6,0xDE,0x7C,0x0C,0x0E,0x00,0x00,
        // R (82)
        0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x6C,0x66,0x66,0x66,0xE6,0x00,0x00,0x00,0x00,
        // S (83)
        0x00,0x00,0x7C,0xC6,0xC6,0x60,0x38,0x0C,0x06,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // T (84)
        0x00,0x00,0xFF,0xDB,0x99,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00,
        // U (85)
        0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // V (86)
        0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00,0x00,0x00,0x00,
        // W (87)
        0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xD6,0xD6,0xD6,0xFE,0xEE,0x6C,0x00,0x00,0x00,0x00,
        // X (88)
        0x00,0x00,0xC6,0xC6,0x6C,0x7C,0x38,0x38,0x7C,0x6C,0xC6,0xC6,0x00,0x00,0x00,0x00,
        // Y (89)
        0x00,0x00,0xC6,0xC6,0xC6,0x6C,0x38,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00,
        // Z (90)
        0x00,0x00,0xFE,0xC6,0x86,0x0C,0x18,0x30,0x60,0xC2,0xC6,0xFE,0x00,0x00,0x00,0x00,
        // [ (91)
        0x00,0x00,0x3C,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x3C,0x00,0x00,0x00,0x00,
        // \ (92)
        0x00,0x00,0x00,0x80,0xC0,0xE0,0x70,0x38,0x1C,0x0E,0x06,0x02,0x00,0x00,0x00,0x00,
        // ] (93)
        0x00,0x00,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00,0x00,0x00,0x00,
        // ^ (94)
        0x10,0x38,0x6C,0xC6,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        // _ (95)
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,
        // ` (96)
        0x30,0x30,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        // a (97)
        0x00,0x00,0x00,0x00,0x00,0x78,0x0C,0x7C,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00,
        // b (98)
        0x00,0x00,0xE0,0x60,0x60,0x78,0x6C,0x66,0x66,0x66,0x66,0x7C,0x00,0x00,0x00,0x00,
        // c (99)
        0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // d (100)
        0x00,0x00,0x1C,0x0C,0x0C,0x3C,0x6C,0xCC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00,
        // e (101)
        0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xFE,0xC0,0xC0,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // f (102)
        0x00,0x00,0x38,0x6C,0x64,0x60,0xF0,0x60,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00,
        // g (103)
        0x00,0x00,0x00,0x00,0x00,0x76,0xCC,0xCC,0xCC,0xCC,0xCC,0x7C,0x0C,0xCC,0x78,0x00,
        // h (104)
        0x00,0x00,0xE0,0x60,0x60,0x6C,0x76,0x66,0x66,0x66,0x66,0xE6,0x00,0x00,0x00,0x00,
        // i (105)
        0x00,0x00,0x18,0x18,0x00,0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00,
        // j (106)
        0x00,0x00,0x06,0x06,0x00,0x0E,0x06,0x06,0x06,0x06,0x06,0x06,0x66,0x66,0x3C,0x00,
        // k (107)
        0x00,0x00,0xE0,0x60,0x60,0x66,0x6C,0x78,0x78,0x6C,0x66,0xE6,0x00,0x00,0x00,0x00,
        // l (108)
        0x00,0x00,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00,
        // m (109)
        0x00,0x00,0x00,0x00,0x00,0xEC,0xFE,0xD6,0xD6,0xD6,0xD6,0xC6,0x00,0x00,0x00,0x00,
        // n (110)
        0x00,0x00,0x00,0x00,0x00,0xDC,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00,0x00,
        // o (111)
        0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // p (112)
        0x00,0x00,0x00,0x00,0x00,0xDC,0x66,0x66,0x66,0x66,0x66,0x7C,0x60,0x60,0xF0,0x00,
        // q (113)
        0x00,0x00,0x00,0x00,0x00,0x76,0xCC,0xCC,0xCC,0xCC,0xCC,0x7C,0x0C,0x0C,0x1E,0x00,
        // r (114)
        0x00,0x00,0x00,0x00,0x00,0xDC,0x76,0x66,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00,
        // s (115)
        0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0x60,0x38,0x0C,0xC6,0x7C,0x00,0x00,0x00,0x00,
        // t (116)
        0x00,0x00,0x10,0x30,0x30,0xFC,0x30,0x30,0x30,0x30,0x36,0x1C,0x00,0x00,0x00,0x00,
        // u (117)
        0x00,0x00,0x00,0x00,0x00,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00,
        // v (118)
        0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00,0x00,0x00,0x00,
        // w (119)
        0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xD6,0xD6,0xD6,0xFE,0x6C,0x00,0x00,0x00,0x00,
        // x (120)
        0x00,0x00,0x00,0x00,0x00,0xC6,0x6C,0x38,0x38,0x38,0x6C,0xC6,0x00,0x00,0x00,0x00,
        // y (121)
        0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7E,0x06,0x0C,0xF8,0x00,
        // z (122)
        0x00,0x00,0x00,0x00,0x00,0xFE,0xCC,0x18,0x30,0x60,0xC6,0xFE,0x00,0x00,0x00,0x00,
        // { (123)
        0x00,0x00,0x0E,0x18,0x18,0x18,0x70,0x18,0x18,0x18,0x18,0x0E,0x00,0x00,0x00,0x00,
        // | (124)
        0x00,0x00,0x18,0x18,0x18,0x18,0x00,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00,
        // } (125)
        0x00,0x00,0x70,0x18,0x18,0x18,0x0E,0x18,0x18,0x18,0x18,0x70,0x00,0x00,0x00,0x00,
        // ~ (126)
        0x00,0x00,0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    ];

    let mut glyphs = Vec::with_capacity(95);
    for i in 0..95 {
        let mut glyph = [0u8; 16];
        glyph.copy_from_slice(&raw[i * 16..(i + 1) * 16]);
        glyphs.push(glyph);
    }
    glyphs
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_char_uv_space() {
        let (u0, v0, u1, v1) = char_uv(b' ');
        assert_eq!(u0, 0.0);
        assert_eq!(v0, 0.0);
        assert!((u1 - GLYPH_W as f32 / ATLAS_W as f32).abs() < 1e-5);
        assert!((v1 - GLYPH_H as f32 / ATLAS_H as f32).abs() < 1e-5);
    }

    #[test]
    fn test_char_uv_a() {
        let idx = b'A' - FIRST_CHAR;
        let col = idx as u32 % GLYPHS_PER_ROW;
        let row = idx as u32 / GLYPHS_PER_ROW;

        let (u0, v0, _, _) = char_uv(b'A');
        assert!((u0 - (col * GLYPH_W) as f32 / ATLAS_W as f32).abs() < 1e-5);
        assert!((v0 - (row * GLYPH_H) as f32 / ATLAS_H as f32).abs() < 1e-5);
    }

    #[test]
    fn test_font_atlas_size() {
        let atlas = generate_font_atlas();
        assert_eq!(atlas.len(), (ATLAS_W * ATLAS_H * 4) as usize);
    }

    #[test]
    fn test_font_data_glyph_count() {
        let glyphs = include_cp437_font();
        assert_eq!(glyphs.len(), 95);
    }

    #[test]
    fn test_space_glyph_is_empty() {
        let glyphs = include_cp437_font();
        let space = &glyphs[0]; // Space is first char (32 - 32 = 0)
        for &row in space.iter() {
            assert_eq!(row, 0, "Space glyph should be all zeros");
        }
    }

    #[test]
    fn test_char_uv_bounds() {
        for ch in FIRST_CHAR..LAST_CHAR {
            let (u0, v0, u1, v1) = char_uv(ch);
            assert!(u0 >= 0.0 && u0 <= 1.0, "u0 out of bounds for char {}", ch);
            assert!(v0 >= 0.0 && v0 <= 1.0, "v0 out of bounds for char {}", ch);
            assert!(u1 >= 0.0 && u1 <= 1.0, "u1 out of bounds for char {}", ch);
            assert!(v1 >= 0.0 && v1 <= 1.0, "v1 out of bounds for char {}", ch);
            assert!(u1 > u0);
            assert!(v1 > v0);
        }
    }
}