Appearance
内存模型代码导读 (Memory Model Code Walkthrough)
本节深入 app.js 源码,逐步解析 WASM 内存的创建、GBA 地址映射的实现,以及关键的内存操作函数。
内存创建与初始化
创建 SharedArrayBuffer 与 WebAssembly.Memory
javascript
// 创建 256MB 的共享内存缓冲区
const MEMORY_SIZE = 268435456; // 0x10000000 = 256 * 1024 * 1024
const sharedBuffer = new SharedArrayBuffer(MEMORY_SIZE);
// 用共享缓冲区创建 WebAssembly 内存对象
const memory = new WebAssembly.Memory({
buffer: sharedBuffer,
initial: MEMORY_SIZE / 65536, // WASM 内存以 64KB 页为单位,256MB = 4096 页
maximum: MEMORY_SIZE / 65536, // 不允许增长,固定 256MB
shared: true // 启用 SharedArrayBuffer 共享
});关键参数说明:
| 参数 | 值 | 说明 |
|---|---|---|
initial | 4096 | 初始页数(每页 64KB),4096 * 64KB = 256MB |
maximum | 4096 | 最大页数与初始相同,内存不可增长 |
shared | true | 使用 SharedArrayBuffer,支持线程间共享 |
创建类型化数组视图
javascript
// 创建不同粒度的内存视图
const mem8 = new Uint8Array(sharedBuffer); // 8位视图:逐字节访问
const mem16 = new Uint16Array(sharedBuffer); // 16位视图:用于 GBA 寄存器
const mem32 = new Uint32Array(sharedBuffer); // 32位视图:用于像素操作
// GBA 特定区域的视图
const VRAM_START = 0x06000000;
const PALETTE_START = 0x05000000;
const IO_START = 0x04000000;
const FLASH_START = 0x0E000000;
// 常用区域视图
const vram = new Uint16Array(sharedBuffer, VRAM_START, 96 * 1024 / 2);
const palette = new Uint16Array(sharedBuffer, PALETTE_START, 1024 / 2);
const ioRegisters = new Uint16Array(sharedBuffer, IO_START, 0x400 / 2);ROM 加载
javascript
async function loadROM(romData) {
const romStart = 0x08000000; // GBA ROM 起始地址
const romView = new Uint8Array(sharedBuffer, romStart);
// 将 ROM 数据复制到 WASM 内存的 ROM 区域
romView.set(new Uint8Array(romData));
console.log(`ROM 已加载到 0x${romStart.toString(16)},大小: ${romData.byteLength} 字节`);
}ROM 数据被放置在 0x08000000 起始的地址空间中,对应 GBA 的 Game Pak ROM 区域。C 代码中的指针 0x08000000 到 0x0DFFFFFF 范围的读取将直接访问这块数据。
内存读取与 GBA 硬件模拟
读取 I/O 寄存器
javascript
function readIORegister(address) {
// I/O 寄存器地址在 0x04000000 - 0x040003FF 范围
const offset = (address - 0x04000000) / 2;
return mem16[address / 2]; // 16位对齐读取
}
// 示例:读取显示控制寄存器 (DISPCNT)
function getDISPCNT() {
return readIORegister(0x04000000);
}写入 VRAM 后如何变成画面
架构变更
早期版本由 app.js 每帧读取 VRAM 并在 JS 中做 RGB555→RGBA 转换再渲染。上游已把整条渲染搬进 WASM 模块(src/wasm_display.c),宿主不再读取 VRAM 做渲染——详见 渲染代码走读。
现在「读 VRAM → 合成 RGBA」全部发生在 WASM 内部。wasm_display.c 的 GbaColor() 完成 BGR555→RGB888 转换(等价于下面的位运算),WasmRenderFrame() 每帧把整张 240×160 合成到静态缓冲区 sWasmDisplayRgba,再由 WasmDisplayBuffer() 导出指针:
javascript
// web/app.js — 宿主只负责搬运,不读取 VRAM
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 内部合成整帧(含 RGB555→RGBA)
ctx.putImageData(image, 0, 0); // 搬运到 Canvas
}WASM 模块导入函数
app.js 向 WASM 模块提供了一些与内存相关的导入函数:
javascript
const importObject = {
env: {
memory: memory, // 共享内存对象
// 通知 JavaScript 某个内存区域被写入
jsNotifyVramUpdate: function(offset, length) {
// 标记 VRAM 脏区域,渲染时只更新变化部分
markDirtyRegion(offset, length);
},
// 获取当前时间(用于定时器)
jsGetTimestamp: function() {
return performance.now();
},
}
};关键函数索引
| 函数名 | 所在文件 | 说明 |
|---|---|---|
new SharedArrayBuffer(MEMORY_SIZE) | app.js | 创建 256MB 共享内存缓冲区 |
new WebAssembly.Memory({...}) | app.js | 创建 WASM 内存对象 |
new Uint8Array(sharedBuffer) | app.js | 创建字节粒度内存视图 |
new Uint16Array(sharedBuffer, offset, length) | app.js | 创建 16位 内存视图(用于寄存器/VRAM) |
romView.set(romData) | app.js | 将 ROM 数据加载到内存 |
render() | app.js | 调用 WasmRenderFrame() 并把 RGBA 搬到 Canvas(渲染在 WASM 内) |
readIORegister(address) | app.js | 读取 GBA I/O 寄存器值 |
loadROM(romData) | app.js | 异步加载 ROM 到内存 |