Skip to content

GBA 硬件模拟 — 代码走读

app.js 硬件模拟架构

app.js 是浏览器端宿主运行时,负责加载 WASM 模块、建立内存视图、处理输入与 Flash 存档。注意:GBA 硬件地址空间本身位于 WASM 模块的线性内存中(游戏代码像在真机上一样按地址读写),图形渲染也由 WASM 内部的 src/wasm_display.c 完成(见 渲染代码走读);app.js 只在每帧把合成的 RGBA 搬到 Canvas。

整体架构

内存初始化

WASM 模块加载与内存设置

javascript
// 加载 WASM 模块并初始化
async function initWasm() {
    // 读取 WASM 二进制
    const response = await fetch('output.wasm');
    const bytes = await response.arrayBuffer();

    // 实例化 WASM 模块
    const { instance } = await WebAssembly.instantiate(bytes, {
        env: {
            // JS 导入函数:模拟 GBA BIOS 调用
            // 例如 LZ77 解压等函数
        }
    });

    // 获取 WASM 线性内存的视图
    const memory = instance.exports.memory;
    const memView = new Uint8Array(memory.buffer);
    const memView16 = new Uint16Array(memory.buffer);
    const memView32 = new Uint32Array(memory.buffer);

    return { instance, memView, memView16, memView32 };
}

关键点

  • Uint8Array / Uint16Array / Uint32Array 视图共享同一块内存缓冲区
  • 游戏代码中的 u8*u16*u32* 指针分别对应不同宽度的视图
  • 所有 GBA 硬件地址直接映射到这些数组的索引

硬件区域初始化

javascript
// 硬件地址常量定义
const ADDR = {
    IO_REGS:    0x04000000,  // I/O 寄存器
    PALETTE:    0x05000000,  // Palette RAM
    VRAM:       0x06000000,  // 视频内存
    OAM:        0x07000000,  // 精灵属性内存
    FLASH:      0x0E000000,  // Flash 存储
};

// 硬件区域大小
const SIZE = {
    IO_REGS:    0x400,       // 1KB
    PALETTE:    0x400,       // 1KB (BG + OBJ 各 256 色)
    VRAM:       0x18000,     // 96KB
    OAM:        0x400,       // 1KB (128 个精灵)
    FLASH:      0x20000,     // 128KB
};

寄存器读写处理

I/O 寄存器写入拦截

javascript
// WASM 中的游戏代码通过直接内存写入操作寄存器
// 由于使用 SharedArrayBuffer 或直接内存访问,JS 侧通过
// 读取内存视图来获取寄存器状态

function readIORegister(offset) {
    const addr = ADDR.IO_REGS + offset;
    return memView16[addr >> 1];  // 16位读取
}

function readIORegister32(offset) {
    const addr = ADDR.IO_REGS + offset;
    return memView32[addr >> 2];  // 32位读取
}

关键寄存器读取示例

javascript
// 读取显示控制寄存器
function getDISPCNT() {
    return readIORegister(0x00);  // 0x04000000
}

// 读取显示状态
function getDISPSTAT() {
    return readIORegister(0x04);  // 0x04000004
}

// 读取背景控制
function getBGCNT(bgIndex) {
    return readIORegister(0x08 + bgIndex * 2);
}

// 读取按键状态
function getKeyInput() {
    return readIORegister(0x130);  // 0x04000130
}

输入处理

javascript
// 按键映射:键盘 → GBA 按键
const KEY_MAP = {
    'KeyZ':       0x0001,  // A 键
    'KeyX':       0x0002,  // B 键
    'Backspace':  0x0004,  // Select
    'Enter':      0x0008,  // Start
    'ArrowRight': 0x0010,  // 右
    'ArrowLeft':  0x0020,  // 左
    'ArrowUp':    0x0040,  // 上
    'ArrowDown':  0x0080,  // 下
    'KeyS':       0x0100,  // R
    'KeyA':       0x0200,  // L
};

let keyState = 0x03FF;  // 所有按键未按下(高电平)

document.addEventListener('keydown', (e) => {
    if (KEY_MAP[e.code] !== undefined) {
        keyState &= ~KEY_MAP[e.code];  // 对应位置 0(按下)
        updateKeyInput();
    }
});

document.addEventListener('keyup', (e) => {
    if (KEY_MAP[e.code] !== undefined) {
        keyState |= KEY_MAP[e.code];   // 对应位置 1(释放)
        updateKeyInput();
    }
});

function updateKeyInput() {
    memView16[0x04000130 >> 1] = keyState;
}

帧循环与渲染

架构变更

早期版本中 app.js 自行读取 VRAM/OAM/Palette 并在 JS 中渲染。上游已把整条渲染管线搬进 WASM 模块(src/wasm_display.c),宿主不再触碰像素。下面的 renderFrame() / renderBackground() / renderTile() / renderSprites() 等 JS 函数已不存在;完整的渲染实现请见 渲染代码走读

宿主侧的帧循环现在非常精简——推进游戏逻辑、合成 RGBA、搬到 Canvas:

javascript
// web/app.js — 宿主侧渲染(极简)
const ptr  = instance.exports.WasmDisplayBuffer();       // WASM 内的 RGBA 缓冲区
const size = instance.exports.WasmDisplayBufferSize();
image = new ImageData(new Uint8ClampedArray(memory.buffer, ptr, size), 240, 160);

function render() {
  instance.exports.WasmRenderFrame();   // WASM 内部合成整帧
  ctx.putImageData(image, 0, 0);        // 搬运到 Canvas
}

DISPCNT / BGxCNT / Palette / VRAM / OAM 的读取与像素合成,全部发生在 wasm_display.cWasmRenderFrame()RenderTiled() / RenderBitmapMode3/4()TextBgPixel / AffineBgPixel / RenderSprites 中,详见 渲染概念

Flash 存储模拟

javascript
// Flash 存储使用 localStorage 模拟
const FLASH_KEY = 'pokeemerald-wasm-save';

function initFlash() {
    // 尝试从 localStorage 加载存档
    const saved = localStorage.getItem(FLASH_KEY);
    if (saved) {
        const data = JSON.parse(saved);
        for (let i = 0; i < data.length; i++) {
            memView[ADDR.FLASH + i] = data[i];
        }
    }
}

function saveFlash() {
    // 将 Flash 区域保存到 localStorage
    const data = Array.from(
        memView.slice(ADDR.FLASH, ADDR.FLASH + SIZE.FLASH)
    );
    localStorage.setItem(FLASH_KEY, JSON.stringify(data));
}

关键函数索引

函数名说明涉及硬件区域
initWasm()加载 WASM 模块,初始化内存视图全局
readIORegister(offset)读取 16 位 I/O 寄存器I/O 寄存器
readIORegister32(offset)读取 32 位 I/O 寄存器I/O 寄存器
getDISPCNT()获取显示控制寄存器值I/O (0x04000000)
getBGCNT(bg)获取背景控制寄存器值I/O (0x04000008+)
updateKeyInput()将按键状态写入 KEYINPUTI/O (0x04000130)
render()宿主侧:调用 WasmRenderFrame()putImageDataWASM 导出
(渲染实现已迁移至 src/wasm_display.cWasmRenderFrame / TextBgPixel / RenderSpritesVRAM + OAM + Palette
initFlash()从 localStorage 加载存档Flash (0x0E000000)
saveFlash()保存存档到 localStorageFlash (0x0E000000)

性能考量

把渲染搬进 WASM 后,每帧的像素合成(240×160 = 38,400 像素,逐层 Tile 解码 + 精灵叠加 + 特效)作为编译后的 C 执行,不再走解释执行的 JS。宿主侧唯一剩余的开销是一次 putImageData(搬运约 150KB RGBA)。

make native-bench 提供精确测量:从空白存档回放到大世界 / 菜单 / 战斗三场景,预热后计时,并用渲染前后的 FNV 哈希校验正确性。详见 构建部署概念 的原生构建目标一节。

相关阅读