Skip to content

内存模型练习 (Memory Model Exercises)

练习 1:计算 GBA 关键内存区域的使用量

题目: GBA 有多个硬件内存区域,它们的实际物理大小远小于 256MB 地址空间中为它们预留的范围。请根据以下信息完成计算:

区域起始地址实际物理大小
EWRAM0x02000000256 KB
IWRAM0x0300000032 KB
调色板 RAM0x050000001 KB
VRAM0x0600000096 KB
OAM0x070000001 KB
Flash0x0E000000128 KB
  1. 所有实际物理内存的总和是多少 KB?换算为 MB 是多少?
  2. 256MB 地址空间中,这些实际物理内存占用了百分之几?
  3. VRAM 在地址空间中占用多大的地址范围(从 0x06000000 到哪里)?为什么说地址空间远大于实际物理大小?
参考答案
  1. 总和 = 256 + 32 + 1 + 96 + 1 + 128 = 514 KB,约等于 0.502 MB
  2. 0.502 MB / 256 MB ≈ 0.196%。GBA 的实际硬件内存非常小,256MB 地址空间中的绝大部分是未使用或镜像(mirror)的。
  3. VRAM 从 0x060000000x06FFFFFF,地址空间大小为 16MB,但实际物理 VRAM 只有 96KB。这是因为 GBA 使用地址镜像 (mirroring)——对 0x06018000 的访问会自动映射到 0x06000000 + (0x018000 % 0x18000)0x06000000。地址空间中同一块物理内存被重复映射多次。

练习 2:分析为什么需要 256MB 内存

题目: WebAssembly 模块初始化时设置了 initial-memory=268435456(即 256MB)。有人认为 GBA 实际物理内存才几百 KB,分配 256MB 太浪费了。

  1. 请解释为什么不能用更小的内存(例如 32MB)来满足需求。
  2. 假设存档地址 0x0E000000 改为 0x08000000 附近的一个偏移,是否可以减少 WASM 内存大小?这样做会有什么问题?
  3. 在实际的浏览器环境中,分配 256MB 的 SharedArrayBuffer 意味着什么?是否真的占用了 256MB 物理内存?
参考答案
  1. 不能减少到 32MB 的原因是:Flash 存档的地址是 0x0E000000(即 224MB 处),如果 WASM 内存只有 32MB(0x02000000),则 0x0E000000 超出范围,访问该地址会触发 WASM 的内存越界陷阱 (out-of-bounds trap),导致模块崩溃。ROM 区域也位于 0x08000000(128MB 处),所以至少需要超过 128MB。必须用 256MB 才能覆盖 0x0E000000

  2. 理论上可以,但会引入严重问题:GBA 的 C 源码中硬编码了大量地址常量(如 #define VRAM ((u8*)0x06000000)#define FLASH_BASE ((u8*)0x0E000000)),修改地址意味着要改动数千处源码,还可能引入指针运算错误。更重要的是,这会破坏与原始 GBA 硬件的兼容性设计。

  3. 现代浏览器对 SharedArrayBuffer 使用虚拟内存——分配 256MB 地址空间并不意味着立即占用 256MB 物理内存。只有实际被访问(写入)的页面才会消耗物理 RAM。由于 GBA 实际只使用了地址空间中极小的一部分,物理内存占用通常远低于 256MB(通常在几 MB 到几十 MB 之间)。

练习 3:追踪一次从 C 代码到 Canvas 像素的内存写入

题目: 当游戏中需要将一个像素设为红色(RGB15 格式:0x001F)时,C 代码会写入 VRAM。请追踪这个写入操作从 C 源码到最终在 Canvas 上显示的完整路径。

提示:

  • C 代码:*(volatile u16*)0x06000000 = 0x001F;
  • GBA 使用 RGB555 颜色格式(15位)
  • JavaScript 通过类型化数组视图读取 VRAM

请描述以下每一步发生了什么:

  1. C 代码执行指针写入时,编译器生成了什么 WASM 指令?
  2. 这条 WASM 指令执行后,SharedArrayBuffer 中哪个字节发生了变化?
  3. JavaScript 如何检测到这个变化?
  4. JavaScript 如何将 RGB555 的 0x001F 转换为 Canvas 使用的 RGBA 格式?
参考答案
  1. C 代码 *(volatile u16*)0x06000000 = 0x001F; 编译后大致生成如下 WASM 指令:

    wasm
    i32.const 0x06000000   ;; 将地址压入栈
    i32.const 0x001F       ;; 将值压入栈
    i32.store16 align=1    ;; 以 16 位宽度写入

    volatile 关键字确保编译器不会优化掉这次写入。

  2. WASM 的 i32.store16 将值 0x001F 以小端序 (little-endian) 写入线性内存偏移 0x06000000 处。由于 GBA 是小端架构,SharedArrayBuffer 中:

    • buffer[0x06000000] = 0x1F(低字节)
    • buffer[0x06000001] = 0x00(高字节)
  3. 渲染发生在 WASM 模块内部src/wasm_display.c),而非 app.jsWasmRenderFrame() 每帧直接从线性内存读取 VRAM、OAM、调色板,合成一张 240×160 的 RGBA 帧并通过 WasmDisplayBuffer() 导出指针;宿主 app.js 只做一次 putImageData 搬运。也就是说,VRAM 的消费方是 WASM 内的软件 PPU,不是 JavaScript——没有跨语言轮询开销。

  4. RGB555 到 RGBA 的转换过程:

    • 0x001F 的二进制为 00000 00000 11111(B=0, G=0, R=31)
    • 提取各通道:R = (color & 0x1F) = 31G = (color >> 5) & 0x1F = 0B = (color >> 10) & 0x1F = 0
    • GBA 5位颜色扩展到 8位:R8 = (R * 255 + 15) / 31 ≈ 255(公式等价于 (R << 3) | (R >> 2)
    • 最终 RGBA:rgba(255, 0, 0, 255),即纯红色。
    javascript
    // 典型的转换代码
    const r = (color & 0x1F);
    const g = (color >> 5) & 0x1F;
    const b = (color >> 10) & 0x1F;
    const r8 = (r << 3) | (r >> 2);
    const g8 = (g << 3) | (g >> 2);
    const b8 = (b << 3) | (b >> 2);
    // ImageData: [r8, g8, b8, 255]

拓展挑战

  • 挑战 1:修改 app.js,在控制台打印每帧 VRAM 的写入热区(被修改的字节范围),分析哪些区域最频繁被更新。
  • 挑战 2:实现一个简易的 VRAM 查看器——将 VRAM 内容实时可视化为像素网格,可以在另一个 Canvas 上显示原始图块数据。
  • 挑战 3:研究 WebAssembly.Memorygrow() 方法。如果 GBA 游戏需要访问超出 256MB 的地址(理论上不会发生),会发生什么?WASM 的内存增长机制如何工作?
  • 挑战 4:尝试使用 Atomics.wait / Atomics.notify 在 SharedArrayBuffer 上实现 WASM 与 JavaScript 之间的事件驱动通信,替代轮询方式。