Skip to content

内存模型代码导读 (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 共享
});

关键参数说明:

参数说明
initial4096初始页数(每页 64KB),4096 * 64KB = 256MB
maximum4096最大页数与初始相同,内存不可增长
sharedtrue使用 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 代码中的指针 0x080000000x0DFFFFFF 范围的读取将直接访问这块数据。

内存读取与 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.cGbaColor() 完成 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 到内存

相关阅读