Appearance
内存模型练习 (Memory Model Exercises)
练习 1:计算 GBA 关键内存区域的使用量
题目: GBA 有多个硬件内存区域,它们的实际物理大小远小于 256MB 地址空间中为它们预留的范围。请根据以下信息完成计算:
| 区域 | 起始地址 | 实际物理大小 |
|---|---|---|
| EWRAM | 0x02000000 | 256 KB |
| IWRAM | 0x03000000 | 32 KB |
| 调色板 RAM | 0x05000000 | 1 KB |
| VRAM | 0x06000000 | 96 KB |
| OAM | 0x07000000 | 1 KB |
| Flash | 0x0E000000 | 128 KB |
- 所有实际物理内存的总和是多少 KB?换算为 MB 是多少?
- 256MB 地址空间中,这些实际物理内存占用了百分之几?
- VRAM 在地址空间中占用多大的地址范围(从
0x06000000到哪里)?为什么说地址空间远大于实际物理大小?
参考答案
- 总和 = 256 + 32 + 1 + 96 + 1 + 128 = 514 KB,约等于 0.502 MB。
- 0.502 MB / 256 MB ≈ 0.196%。GBA 的实际硬件内存非常小,256MB 地址空间中的绝大部分是未使用或镜像(mirror)的。
- VRAM 从
0x06000000到0x06FFFFFF,地址空间大小为 16MB,但实际物理 VRAM 只有 96KB。这是因为 GBA 使用地址镜像 (mirroring)——对0x06018000的访问会自动映射到0x06000000 + (0x018000 % 0x18000)即0x06000000。地址空间中同一块物理内存被重复映射多次。
练习 2:分析为什么需要 256MB 内存
题目: WebAssembly 模块初始化时设置了 initial-memory=268435456(即 256MB)。有人认为 GBA 实际物理内存才几百 KB,分配 256MB 太浪费了。
- 请解释为什么不能用更小的内存(例如 32MB)来满足需求。
- 假设存档地址
0x0E000000改为0x08000000附近的一个偏移,是否可以减少 WASM 内存大小?这样做会有什么问题? - 在实际的浏览器环境中,分配 256MB 的
SharedArrayBuffer意味着什么?是否真的占用了 256MB 物理内存?
参考答案
不能减少到 32MB 的原因是:Flash 存档的地址是
0x0E000000(即 224MB 处),如果 WASM 内存只有 32MB(0x02000000),则0x0E000000超出范围,访问该地址会触发 WASM 的内存越界陷阱 (out-of-bounds trap),导致模块崩溃。ROM 区域也位于0x08000000(128MB 处),所以至少需要超过 128MB。必须用 256MB 才能覆盖0x0E000000。理论上可以,但会引入严重问题:GBA 的 C 源码中硬编码了大量地址常量(如
#define VRAM ((u8*)0x06000000)、#define FLASH_BASE ((u8*)0x0E000000)),修改地址意味着要改动数千处源码,还可能引入指针运算错误。更重要的是,这会破坏与原始 GBA 硬件的兼容性设计。现代浏览器对
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
请描述以下每一步发生了什么:
- C 代码执行指针写入时,编译器生成了什么 WASM 指令?
- 这条 WASM 指令执行后,
SharedArrayBuffer中哪个字节发生了变化? - JavaScript 如何检测到这个变化?
- JavaScript 如何将 RGB555 的
0x001F转换为 Canvas 使用的 RGBA 格式?
参考答案
C 代码
*(volatile u16*)0x06000000 = 0x001F;编译后大致生成如下 WASM 指令:wasmi32.const 0x06000000 ;; 将地址压入栈 i32.const 0x001F ;; 将值压入栈 i32.store16 align=1 ;; 以 16 位宽度写入volatile关键字确保编译器不会优化掉这次写入。WASM 的
i32.store16将值0x001F以小端序 (little-endian) 写入线性内存偏移0x06000000处。由于 GBA 是小端架构,SharedArrayBuffer中:buffer[0x06000000]=0x1F(低字节)buffer[0x06000001]=0x00(高字节)
渲染发生在 WASM 模块内部(
src/wasm_display.c),而非app.js。WasmRenderFrame()每帧直接从线性内存读取 VRAM、OAM、调色板,合成一张 240×160 的 RGBA 帧并通过WasmDisplayBuffer()导出指针;宿主app.js只做一次putImageData搬运。也就是说,VRAM 的消费方是 WASM 内的软件 PPU,不是 JavaScript——没有跨语言轮询开销。RGB555 到 RGBA 的转换过程:
0x001F的二进制为00000 00000 11111(B=0, G=0, R=31)- 提取各通道:
R = (color & 0x1F) = 31,G = (color >> 5) & 0x1F = 0,B = (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.Memory的grow()方法。如果 GBA 游戏需要访问超出 256MB 的地址(理论上不会发生),会发生什么?WASM 的内存增长机制如何工作? - 挑战 4:尝试使用
Atomics.wait/Atomics.notify在 SharedArrayBuffer 上实现 WASM 与 JavaScript 之间的事件驱动通信,替代轮询方式。