Appearance
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.c 的 WasmRenderFrame() → 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() | 将按键状态写入 KEYINPUT | I/O (0x04000130) |
render() | 宿主侧:调用 WasmRenderFrame() 并 putImageData | WASM 导出 |
(渲染实现已迁移至 src/wasm_display.c) | WasmRenderFrame / TextBgPixel / RenderSprites 等 | VRAM + OAM + Palette |
initFlash() | 从 localStorage 加载存档 | Flash (0x0E000000) |
saveFlash() | 保存存档到 localStorage | Flash (0x0E000000) |
性能考量
把渲染搬进 WASM 后,每帧的像素合成(240×160 = 38,400 像素,逐层 Tile 解码 + 精灵叠加 + 特效)作为编译后的 C 执行,不再走解释执行的 JS。宿主侧唯一剩余的开销是一次 putImageData(搬运约 150KB RGBA)。
make native-bench 提供精确测量:从空白存档回放到大世界 / 菜单 / 战斗三场景,预热后计时,并用渲染前后的 FNV 哈希校验正确性。详见 构建部署概念 的原生构建目标一节。
相关阅读:
- GBA 硬件模拟概念 — 理解各个硬件区域的结构和原理
- WASM 编译管线代码走读 — 了解编译侧如何生成操作这些地址的代码
- 资源转换管线代码走读 — 了解图形资源如何被处理为 GBA 格式