Skip to content

GBA 硬件模拟 — 练习

练习 1:将 GBA 硬件地址映射到 WASM 内存

目标:理解 GBA 的物理硬件地址如何对应到 WASM 线性内存地址。

任务

  1. app.js 中找到内存初始化代码,列出所有被映射的硬件区域及其 WASM 内存偏移
  2. 说明为什么这些地址可以与原始 GBA 地址相同
  3. 计算所有硬件区域在 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 RAM0x050000001 KB
VRAM0x0600000096 KB
OAM0x070000001 KB
Flash0x0E000000128 KB
合计~227 KB

注意:实际 WASM 内存占用远大于 227KB,因为地址空间中存在大量"空洞"(未使用的地址区域),但 256MB 的分配确保所有地址都有效可访问。


练习 2:追踪 VRAM 写入如何变成像素

目标:理解从游戏代码写入 VRAM 到屏幕上显示像素的完整流程。

任务

  1. 描述一个 8x8 Tile(4bpp,16 色)在 VRAM 中的存储格式
  2. 追踪以下操作链:游戏代码写入图块数据 → VRAM 存储 → WASM 内 wasm_display.c 读取 → 合成 RGBA → 宿主搬到 Canvas
  3. 解释调色板如何将 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 显示控制寄存器的每一位含义。

任务

  1. 解析以下 DISPCNT 值:0x0140,说明其配置含义
  2. 如果游戏要使用 Mode 0,启用 BG0 和 BG1,并启用 OBJ 精灵,DISPCNT 应设为什么值?
  3. 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。完整实现见 渲染代码走读


拓展挑战

  1. 研究 HBlank DMA 扫描线:阅读 src/wasm_display.cRefreshHblankDmaGpuRegs() / ScanlineGpuReg()。当游戏设置 HBlank 触发的 DMA 寄存器时,渲染器如何逐扫描线采样 GPU 寄存器(如逐行 BGxHOFSBLDY),还原水面波动等特效?

  2. 定时器精度分析:GBA 的定时器以 CPU 时钟频率(16.78 MHz)为基准。在 WASM 版本中,定时器如何保持正确的时间精度?分析 JS 侧的定时器模拟代码,讨论可能的计时偏差。

  3. 窗口功能模拟:GBA 支持 Window 0、Window 1 和 OBJ Window 三种窗口功能,可以在屏幕上创建局部显示区域。研究窗口寄存器(WIN0H, WIN0V, WININ, WINOUT)的工作方式,并设计 JS 模拟方案。

  4. 精灵渲染性能测量RenderSprites() 每帧遍历 OAM 中所有 128 个精灵。用 make native-bench 测量大世界 / 菜单 / 战斗三场景的真实 FPS(对照 bench_golden.json 的 FNV 哈希),思考软件 PPU 在编译后 C 下的性能边界。

  5. Mosaic 特效:GBA 支持 BG 和 OBJ 的马赛克特效。研究 Mosaic 寄存器(MOSAIC at 0x0400004C)的工作原理,思考它应如何在 wasm_display.cPutPixel 流程中实现像素化(注意:上游当前未必实现,可作为扩展课题)。

  6. 音频硬件模拟:除了图形,GBA 还有 4 个音频通道(2 个方波、1 个可编程波形、1 个噪音)。研究声音寄存器(0x04000060 - 0x040000A8),设计使用 Web Audio API 的模拟方案。