Appearance
渲染管线 — 概念
渲染管线总览
GBA 的图形输出通过一条固定的像素处理管线生成。在 pokeemerald-wasm 中,这条管线由 WASM 模块内的 WasmRenderFrame()(src/wasm_display.c)驱动,逐像素地从内存数据合成最终的 RGBA 帧。宿主端随后把这个帧缓冲区搬到屏幕(浏览器用 putImageData,原生前端用纹理或帧缓冲)。
渲染器维护两块静态缓冲区:
sWasmDisplayRgba[240×160×4]:最终输出的 RGBA 像素,宿主通过WasmDisplayBuffer()直接读取。sLayerData[240×160]:每个像素当前所属的层号(LAYER_BG0..LAYER_BG3、LAYER_OBJ、LAYER_BACKDROP),供 Alpha 混合判断「目标层」使用。
BG 显示模式
GBA 的 DISPCNT 寄存器(0x04000000)的低 3 位决定了当前的显示模式,不同模式决定了背景层的类型和数量。WasmRenderFrame() 据此把 Mode 3/4 分发到位图路径,Mode 0/1/2 分发到 Tile 路径:
| 模式 | 背景层 | 类型 | 用途 |
|---|---|---|---|
| Mode 0 | BG0-BG3 | 文字 | 菜单、对话框等静态界面 |
| Mode 1 | BG0-BG1 文字, BG2 仿射 | 混合 | 地图场景(最常用) |
| Mode 2 | BG2-BG3 仿射 | 仿射 | 旋转/缩放场景 |
| Mode 3 | 无 | 位图 16bit | 全帧直接像素写入 |
| Mode 4 | 无 | 位图 8bit | 索引色全帧(支持双缓冲) |
BgLayersForMode() 根据 DISPCNT 的「层启用位」(0x100 << bg)和模式,返回当前激活的 BG 层列表,每层标记 type(0 = 文字,1 = 仿射)。
Tile 渲染详解
文字背景(Text BG)
文字背景是最常用的渲染模式。每个背景层由一个 Tile Map(屏幕块矩阵)和一组 Tile(8×8 像素图块)组成:
Tile Map 中每个 Entry(2 字节)的结构:
| 位 | 含义 |
|---|---|
| [9:0] | Tile 编号(0-1023) |
| [11:10] | 水平/垂直翻转 |
| [15:12] | 调色板编号(16 色模式下) |
TextBgPixel(bg, x, y, &color) 关键步骤:
- 从
BGxCNT读取 charBase(Tile 数据基址)、screenBase(Tile Map 基址)、color256标志、尺寸 - 用扫描线采样到的
hofs/vofs(见下文 HBlank DMA)计算屏幕坐标对应的地图坐标,并按尺寸环绕 - 从 Tile Map 读取 Entry,获取 Tile 编号、翻转标志、调色板编号
- 计算 Tile 内的像素坐标(考虑翻转)
- 从 VRAM 读取像素的颜色索引(4bpp 打包或 8bpp)
- 通过
GbaColor()将 BGR555 颜色值转换为 RGB;索引 0 视为透明,返回FALSE
仿射背景(Affine BG)
仿射背景支持旋转和缩放变换,通过一个 2×2 仿射矩阵(PA/PB/PC/PD)实现。AffineBgPixel() 对每个屏幕像素反向映射回纹理坐标:
仿射参考点 refX/refY 是 28 位有符号定点数(Signed28),矩阵元素是 16 位有符号(Signed16),右移 8 位完成定点运算。
精灵渲染
Sprite 渲染从 OAM 读取精灵属性,将其像素叠加到画面上。RenderSprites(dispcnt, priority) 只渲染指定优先级的精灵(位图模式下 priority = -1 表示全部):
OAM 属性详解
每个精灵在 OAM 中占用 4 个半字(8 字节,最后 2 字节对普通精灵未用),渲染读前 3 个:
Attribute 0(a0):
| 位 | 含义 |
|---|---|
| [7:0] | Y 坐标 |
| [9:8] | 仿射/普通模式(affineMode) |
| [10:11] | 对象模式(objMode,0=正常,1=半透明,2=窗口) |
| [13] | 256 色 / 16 色模式 |
| [15:14] | 形状(正方形/横向/纵向) |
Attribute 1(a1):
| 位 | 含义 |
|---|---|
| [8:0] | X 坐标(9 位,超出 240 取负) |
| [12] | 水平翻转(普通精灵) |
| [13] | 垂直翻转(普通精灵) |
| [8:4] | 仿射矩阵索引(仿射精灵,(a1>>9)&31) |
| [15:14] | 尺寸 |
Attribute 2(a2):
| 位 | 含义 |
|---|---|
| [9:0] | Tile 基地址 |
| [11:10] | 优先级(0-3) |
| [15:12] | 调色板编号(16 色模式) |
仿射精灵
仿射精灵从 OAM 的矩阵区(OAM + matrix*32)读取 4 个矩阵参数(PA/PB/PC/PD),对每个输出像素反向映射回纹理坐标:
dx = x - drawCx, dy = y - drawCy // 相对绘制中心的偏移
px = (PA * dx + PB * dy) >> 8 + texCx
py = (PC * dx + PD * dy) >> 8 + texCy其中 drawCx/drawCy 是绘制中心,texCx/texCy 是纹理中心。affineMode == 3(双倍尺寸)时绘制区域放大一倍。
优先级系统
Tile 模式下的渲染采用 优先级交替合成 策略,在 RenderTiled() 中实现,确保 BG 层和精灵按正确顺序叠加:
外层循环从 Priority 3 到 Priority 0,每个级别先画该优先级的 BG 层、再画该优先级的精灵;后写入的像素覆盖先写入的(PutPixel 直接写 RGBA 并更新 sLayerData)。这意味着一个 Priority 0 的精灵会显示在所有 Priority 1-3 的 BG 层之上,但可能与 Priority 0 的 BG 层交错。
调色板转换
GBA 使用 BGR555 格式存储颜色(15 位,1 位未用),需要转换为 RGBA8888。GbaColor() 完成此转换,将每个 5 位色阶(0-31)线性映射到 8 位色阶(0-255):
c
// src/wasm_display.c — GbaColor()
static inline struct Rgb GbaColor(u16 value)
{
struct Rgb color;
color.r = (u8)((value & 31) * 255 / 31); // bit[4:0] → R
color.g = (u8)(((value >> 5) & 31) * 255 / 31); // bit[9:5] → G
color.b = (u8)(((value >> 10) & 31) * 255 / 31);// bit[14:10]→ B
return color;
}Alpha 通道固定为 255(不透明),在 PutPixel() 写入时填充。
特效系统
Window 遮罩
GBA 支持 Window 0、Window 1 和 Object Window 三种窗口,可以对不同区域启用/禁用 BG 层和精灵的渲染。WindowMask(x, y) 根据当前坐标判断所在窗口并返回一个 6 位的层遮罩(哪些层允许绘制)。PutPixel() 在写像素前先检查 mask & layer,被屏蔽的层直接跳过。
Alpha 混合与亮度
ActiveBlendColor() 实现了三种半透明特效,由 BLDCNT 的高 2 位(effect)选择:
| 效果 | effect | 计算方式 |
|---|---|---|
| Alpha 混合 | 1 | color = src * EVA + dst(已绘) * EVB(>> 4) |
| 亮度增加 | 2 | color = color + (255 - color) * EVY |
| 亮度减少 | 3 | color = color - color * EVY |
Alpha 混合时,源层必须是 BLDCNT 的目标层之一,且当前像素的「已绘制层」(来自 sLayerData)必须是混合目标层,才会把新颜色与缓冲区里的旧颜色按 EVA/EVB 加权混合。半透明精灵(objMode == 1)会强制触发 Alpha 混合。
HBlank DMA 扫描线寄存器
上游新增能力
真实 GBA 上,游戏常用 HBlank DMA 在每条扫描线开始时改写 GPU 寄存器(如逐行滚屏 BGxHOFS、逐行变色 BLDY),实现水面波动、扭曲、分屏等扫描线特效。wasm_display.c 还原了这一机制。
RefreshHblankDmaGpuRegs() 在每帧渲染前扫描 4 个 DMA 通道,挑出「HBlank 触发 + 重复 + 16 位 + 计数=1 + 目标为 GPU 寄存器」的通道,记录其源地址和步进到 sHblankDmaGpuRegs[]。随后所有读取 GPU 寄存器的地方都改用 ScanlineGpuReg(offset, y):
- 若该寄存器有活跃的 HBlank DMA,返回
src + stride * (y - 1)——即第y条扫描线应该看到的寄存器值; - 否则返回寄存器当前值。
因此 TextBgPixel 读 hofs/vofs、WindowMask 读 WIN0H/WIN1H、ActiveBlendColor 读 BLDY 时,拿到的都是逐扫描线变化的值,从而正确重现扫描线特效。宿主端(app.js)在自动化回放时还会调用导出的 WasmRefreshHblankDmaGpuRegs() / WasmWindowMask() 来探测 DMA 状态。