Appearance
GBA 硬件模拟 — 练习
练习 1:将 GBA 硬件地址映射到 WASM 内存
目标:理解 GBA 的物理硬件地址如何对应到 WASM 线性内存地址。
任务:
- 在
app.js中找到内存初始化代码,列出所有被映射的硬件区域及其 WASM 内存偏移 - 说明为什么这些地址可以与原始 GBA 地址相同
- 计算所有硬件区域在 WASM 内存中占用的总字节数
参考答案
1. 硬件区域映射表:
在 app.js 中,通常会看到类似以下的初始化代码:
javascript
// WASM 线性内存视图
const memory = new Uint8Array(wasmInstance.exports.memory.buffer);
// 关键硬件区域的偏移常量
const IO_REGS_BASE = 0x04000000; // I/O 寄存器
const PALETTE_BASE = 0x05000000; // Palette RAM
const VRAM_BASE = 0x06000000; // VRAM
const OAM_BASE = 0x07000000; // OAM
const FLASH_BASE = 0x0E000000; // Flash 存储2. 地址相同的原因:
WASM 线性内存是一个从地址 0 开始的连续字节数组。由于 --initial-memory=268435456(256MB = 0x10000000),WASM 内存足以容纳 GBA 的完整 32 位地址空间中的关键区域。
游戏代码中的指针和地址计算都使用原始 GBA 地址(如 *(volatile u32*)0x04000000),这些地址直接作为 WASM 线性内存的偏移量使用。这就是为什么编译时不需要修改任何地址常量——--target=wasm32 的 32 位地址空间与 GBA 的地址模型天然兼容。
3. 硬件区域占用统计:
| 区域 | 起始地址 | 大小 |
|---|---|---|
| I/O 寄存器 | 0x04000000 | ~1 KB |
| Palette RAM | 0x05000000 | 1 KB |
| VRAM | 0x06000000 | 96 KB |
| OAM | 0x07000000 | 1 KB |
| Flash | 0x0E000000 | 128 KB |
| 合计 | ~227 KB |
注意:实际 WASM 内存占用远大于 227KB,因为地址空间中存在大量"空洞"(未使用的地址区域),但 256MB 的分配确保所有地址都有效可访问。
练习 2:追踪 VRAM 写入如何变成像素
目标:理解从游戏代码写入 VRAM 到屏幕上显示像素的完整流程。
任务:
- 描述一个 8x8 Tile(4bpp,16 色)在 VRAM 中的存储格式
- 追踪以下操作链:游戏代码写入图块数据 → VRAM 存储 → WASM 内
wasm_display.c读取 → 合成 RGBA → 宿主搬到 Canvas - 解释调色板如何将 4 位颜色索引转换为 RGB 像素
参考答案
1. 4bpp Tile 存储格式:
一个 8x8 像素、4bpp(每像素 4 位 = 16 色)的 Tile 在 VRAM 中占用 32 字节:
字节偏移 0: [像素0低4位 | 像素1低4位] ← 第 0 行的前 2 个像素
字节偏移 1: [像素2低4位 | 像素3低4位] ← 第 0 行的后 2 个像素
...
字节偏移 3: [像素6低4位 | 像素7低4位] ← 第 0 行的最后 2 个像素
字节偏移 4: [第 1 行数据...]
...
字节偏移 31: [第 7 行最后 2 个像素]每个字节存储 2 个像素:高 4 位是偶数像素,低 4 位是奇数像素。4 位值(0-15)是调色板索引。
2. 完整操作链:
3. 调色板索引到 RGB 转换:
步骤 1: 从 Tile 数据提取 4 位索引 (0-15)
步骤 2: 从 Palette RAM 读取 16 位颜色值
例: Palette[2] = 0x7BEF
步骤 3: 解码 BGR555 格式
Bit 0-4: R = 0x7BEF & 0x1F = 15 (红)
Bit 5-9: G = (0x7BEF >> 5) & 0x1F = 23 (绿)
Bit 10-14: B = (0x7BEF >> 10) & 0x1F = 30 (蓝)
步骤 4: 转换为 8 位 RGB
R8 = 15 × 8 = 120 (或使用精确公式: R * 255 / 31)
G8 = 23 × 8 = 184
B8 = 30 × 8 = 240
步骤 5: 写入 Canvas ImageData
pixelData[offset] = R8
pixelData[offset + 1] = G8
pixelData[offset + 2] = B8
pixelData[offset + 3] = 255 (Alpha)特殊处理:索引 0 始终为透明色,不绘制任何像素(Sprite 场景下)或显示为黑色(BG 场景下)。
练习 3:分析 DISPCNT 寄存器位域
目标:掌握 GBA 显示控制寄存器的每一位含义。
任务:
- 解析以下 DISPCNT 值:
0x0140,说明其配置含义 - 如果游戏要使用 Mode 0,启用 BG0 和 BG1,并启用 OBJ 精灵,DISPCNT 应设为什么值?
- 在
src/wasm_display.c中找到读取 DISPCNT 的代码(WasmRenderFrame()),说明它如何根据不同显示模式(Mode 0-4)分发到 Tile / 位图渲染路径
参考答案
1. 解析 DISPCNT = 0x0140:
0x0140 = 0000 0001 0100 0000 (二进制)
Bit 0-2 (Mode): 000 = Mode 0 (4 个文本背景)
Bit 3: 0 = 保留
Bit 4 (FrameSel): 0 = 使用帧 0
Bit 5: 0 = H-Blank Interval Free 禁用
Bit 6 (OBJChar): 1 = OBJ VRAM 映射为 1D 模式
Bit 7: 0 = 强制空白禁用
Bit 8 (BG0): 1 = BG0 使能 ← 来自 0x0140
Bit 9 (BG1): 0 = BG1 禁用
Bit 10 (BG2): 0 = BG2 禁用
Bit 11 (BG3): 0 = BG3 禁用
Bit 12 (OBJ): 0 = OBJ 禁用
Bit 13: 0 = Window 0 禁用
Bit 14: 0 = Window 1 禁用
Bit 15: 0 = OBJ Window 禁用总结:Mode 0,1D OBJ 映射,启用 BG0。
2. 计算目标 DISPCNT 值:
要求:Mode 0 + BG0 使能 + BG1 使能 + OBJ 使能
Mode 0: Bit 0-2 = 000
BG0 使能: Bit 8 = 1
BG1 使能: Bit 9 = 1
OBJ 使能: Bit 12 = 1
DISPCNT = (1 << 8) | (1 << 9) | (1 << 12) = 0x1100加上常见的 1D OBJ 映射(Bit 6): DISPCNT = 0x1100 | 0x0040 = 0x1140
3. wasm_display.c 中的 DISPCNT 处理:
c
// src/wasm_display.c — WasmRenderFrame() (第 600 行) 简化逻辑
void WasmRenderFrame(void)
{
const u16 dispcnt = REG_DISPCNT; // 读取 0x04000000
const u8 mode = dispcnt & 0x07; // Bit 0-2: 显示模式
WasmRefreshHblankDmaGpuRegs();
if (mode == 3) RenderBitmapMode3(); // Mode 3: 16位位图
else if (mode == 4) RenderBitmapMode4(dispcnt); // Mode 4: 8位位图双缓冲
else RenderTiled(dispcnt); // Mode 0/1/2: Tile 背景
if (mode == 3 || mode == 4)
RenderSprites(dispcnt, -1); // 位图模式叠加全部精灵
}WASM 内的渲染器根据 DISPCNT 的位域解析当前显示配置,分发到对应的渲染路径(Tile / 位图),再由 RenderTiled() 按优先级交替合成 BG 与精灵。app.js 不参与这些判断,只调用 WasmRenderFrame() 并把结果搬到 Canvas。完整实现见 渲染代码走读。
拓展挑战
研究 HBlank DMA 扫描线:阅读
src/wasm_display.c的RefreshHblankDmaGpuRegs()/ScanlineGpuReg()。当游戏设置 HBlank 触发的 DMA 寄存器时,渲染器如何逐扫描线采样 GPU 寄存器(如逐行BGxHOFS、BLDY),还原水面波动等特效?定时器精度分析:GBA 的定时器以 CPU 时钟频率(16.78 MHz)为基准。在 WASM 版本中,定时器如何保持正确的时间精度?分析 JS 侧的定时器模拟代码,讨论可能的计时偏差。
窗口功能模拟:GBA 支持 Window 0、Window 1 和 OBJ Window 三种窗口功能,可以在屏幕上创建局部显示区域。研究窗口寄存器(WIN0H, WIN0V, WININ, WINOUT)的工作方式,并设计 JS 模拟方案。
精灵渲染性能测量:
RenderSprites()每帧遍历 OAM 中所有 128 个精灵。用make native-bench测量大世界 / 菜单 / 战斗三场景的真实 FPS(对照bench_golden.json的 FNV 哈希),思考软件 PPU 在编译后 C 下的性能边界。Mosaic 特效:GBA 支持 BG 和 OBJ 的马赛克特效。研究 Mosaic 寄存器(MOSAIC at
0x0400004C)的工作原理,思考它应如何在wasm_display.c的PutPixel流程中实现像素化(注意:上游当前未必实现,可作为扩展课题)。音频硬件模拟:除了图形,GBA 还有 4 个音频通道(2 个方波、1 个可编程波形、1 个噪音)。研究声音寄存器(
0x04000060-0x040000A8),设计使用 Web Audio API 的模拟方案。