Appearance
渲染管线 — 代码走读
本章逐段解读 src/wasm_display.c——这是迁移进 WASM 模块内部的软件 PPU。它整体被 #if WASM 守卫(非 WASM 构建不会编译),通过 --export-all 把渲染入口暴露给宿主。
模块状态
wasm_display.c 用两块静态缓冲区承载一帧的渲染结果:
c
// src/wasm_display.c
static u8 sWasmDisplayRgba[DISPLAY_PIXELS * RGBA_CHANNELS]; // 240*160*4,输出 RGBA
static u8 sLayerData[DISPLAY_PIXELS]; // 每像素所属层号,供 Alpha 混合判断目标层
static struct HblankDmaGpuReg sHblankDmaGpuRegs[GPU_REG_U16_COUNT]; // 扫描线 DMA 状态宿主不会自己解码像素,只会读 sWasmDisplayRgba(见文末「宿主搬运」)。
渲染入口:WasmRenderFrame()
WasmRenderFrame() 是每帧渲染的导出入口,宿主在调用 WasmRunFrame() 推进游戏逻辑后再调用它:
c
// src/wasm_display.c — WasmRenderFrame() (第 600 行)
void WasmRenderFrame(void)
{
const u16 dispcnt = REG_DISPCNT;
const u8 mode = dispcnt & 7;
WasmRefreshHblankDmaGpuRegs(); // 先采样本帧的扫描线 DMA 状态
if (mode == 3) RenderBitmapMode3();
else if (mode == 4) RenderBitmapMode4(dispcnt);
else RenderTiled(dispcnt); // Mode 0/1/2 走 Tile 渲染路径
if (mode == 3 || mode == 4)
RenderSprites(dispcnt, -1); // 位图模式:精灵全部叠加
}调色板转换:GbaColor()
将 GBA BGR555 颜色转换为 struct Rgb(RGB888):
c
// src/wasm_display.c — GbaColor() (第 99 行)
static inline struct Rgb GbaColor(u16 value)
{
struct Rgb color;
color.r = (u8)((value & 31) * 255 / 31);
color.g = (u8)(((value >> 5) & 31) * 255 / 31);
color.b = (u8)(((value >> 10) & 31) * 255 / 31);
return color;
}关键点:
- GBA 颜色为 15 位(每通道 5 位,范围 0-31),通过
* 255 / 31线性映射到 8 位(0-255) - 作为
static inline,每个调用点被内联展开,避免函数调用开销
像素写入:PutPixel()
所有渲染路径最终都通过 PutPixel() 把像素写入 sWasmDisplayRgba:
c
// src/wasm_display.c — PutPixel() (第 240 行)
static void PutPixel(s32 x, s32 y, struct Rgb color, u8 layer, bool8 forceAlphaBlend)
{
u8 mask;
u32 pixel, p;
if (x < 0 || y < 0 || x >= DISPLAY_WIDTH || y >= DISPLAY_HEIGHT) return; // 边界检查
mask = WindowMask(x, y); // 获取 Window 层遮罩
if (layer != LAYER_BACKDROP && !(mask & layer)) return; // 该层被 Window 屏蔽
pixel = y * DISPLAY_WIDTH + x;
color = ActiveBlendColor(color, layer, pixel, mask & LAYER_BACKDROP, y, forceAlphaBlend);
p = pixel * RGBA_CHANNELS;
sWasmDisplayRgba[p] = color.r; // R
sWasmDisplayRgba[p + 1] = color.g; // G
sWasmDisplayRgba[p + 2] = color.b; // B
sWasmDisplayRgba[p + 3] = 255; // A(始终不透明)
sLayerData[pixel] = layer; // 记录层号(供后续 Alpha 混合判断目标层)
}layer 取 LAYER_BG0(0x01)…LAYER_BG3(0x08)、LAYER_OBJ(0x10)、LAYER_BACKDROP(0x20)。sLayerData[pixel] 记录「这个像素目前是谁画的」,后续 Alpha 混合时用来判断目标层是否匹配。
文字背景:TextBgPixel()
c
// src/wasm_display.c — TextBgPixel() (第 303 行) — 关键逻辑
static bool8 TextBgPixel(u8 bg, u8 x, u8 y, struct Rgb *color)
{
const u16 cnt = ReadU16(REG_BASE + REG_OFFSET_BG0CNT + bg * 2);
const u32 charBase = VRAM + ((cnt >> 2) & 3) * 0x4000; // Tile 数据基址
const u32 screenBase = VRAM + ((cnt >> 8) & 31) * 0x800; // Tile Map 基址
const bool8 color256 = (cnt & 0x80) != 0; // 256色/16色
// ... 滚动偏移(经 ScanlineGpuReg 采样) ...
const u16 sx = (x + hofs) & (width - 1); // 带环绕的地图坐标
const u16 sy = (y + vofs) & (height - 1);
const u16 entry = ReadU16(screenBase + block * 0x800 + (mapY * 32 + mapX) * 2);
const u16 tile = entry & 0x3ff; // Tile 编号
const u8 palette = (entry >> 12) & 15; // 调色板编号
const u8 px = entry & 0x400 ? 7 - (sx & 7) : sx & 7; // 水平翻转
const u8 py = entry & 0x800 ? 7 - (sy & 7) : sy & 7; // 垂直翻转
// 16 色模式:每像素 4 位,两像素打包成一字节
const u8 packed = *Ptr8(charBase + tile * 32 + py * 4 + (px >> 1));
colorIndex = px & 1 ? packed >> 4 : packed & 15;
if (!colorIndex) return FALSE; // 颜色0 = 透明
*color = GbaColor(ReadU16(PLTT + (palette * 16 + colorIndex) * 2));
return TRUE;
}注意 hofs/vofs 通过 ScanlineGpuReg() 读取(而非直接读寄存器),这是为了支持逐行滚屏(见「HBlank DMA」一节)。
仿射背景:AffineBgPixel()
c
// src/wasm_display.c — AffineBgPixel() (第 347 行) — 关键逻辑
static bool8 AffineBgPixel(u8 bg, u8 x, u8 y, struct Rgb *color)
{
// 读取仿射参数(16 位有符号)与参考点(28 位有符号)
const s16 pa = Signed16(ReadU16(REG_BASE + reg));
const s16 pb = Signed16(ReadU16(REG_BASE + reg + 2));
const s16 pc = Signed16(ReadU16(REG_BASE + reg + 4));
const s16 pd = Signed16(ReadU16(REG_BASE + reg + 6));
const s32 refX = Signed28(Word(reg + 8));
const s32 refY = Signed28(Word(reg + 12));
// 反向映射:屏幕坐标 → 纹理坐标
s32 sx = (refX + pa * x + pb * y) >> 8;
s32 sy = (refY + pc * x + pd * y) >> 8;
if (wrap) { sx &= size - 1; sy &= size - 1; } // 环绕
else if (sx < 0 || sy < 0 || sx >= size || sy >= size) return FALSE; // 裁剪
// 仿射背景仅支持 8bpp Tile
const u8 tile = *Ptr8(screenBase + (sy >> 3) * (size >> 3) + (sx >> 3));
const u8 colorIndex = *Ptr8(charBase + tile * 64 + (sy & 7) * 8 + (sx & 7));
if (!colorIndex) return FALSE;
*color = GbaColor(ReadU16(PLTT + colorIndex * 2));
return TRUE;
}Tile 模式渲染调度:RenderTiled()
c
// src/wasm_display.c — RenderTiled() (第 567 行)
static void RenderTiled(u16 dispcnt)
{
struct BgLayer layers[4];
const u8 count = BgLayersForMode(dispcnt, layers); // 当前模式激活的 BG 层
ClearScreen(); // 用背景色填充(LAYER_BACKDROP)
for (s8 priority = 3; priority >= 0; priority--) { // 从远到近
for (u8 i = 0; i < count; i++) {
const u8 bg = layers[i].bg;
if ((ReadU16(REG_BASE + REG_OFFSET_BG0CNT + bg * 2) & 3) == priority)
RenderBgLayer(bg, layers[i].type); // 渲染该优先级的 BG 层
}
RenderSprites(dispcnt, priority); // 渲染该优先级的精灵
}
}这是优先级交替合成的核心:从 Priority 3(最远)到 Priority 0(最近),每个级别先渲染 BG 后渲染精灵,后写入的像素覆盖先写入的。RenderBgLayer() 对每个像素调用 TextBgPixel 或 AffineBgPixel,命中(返回 TRUE)时 PutPixel。
精灵渲染:RenderSprites()
c
// src/wasm_display.c — RenderSprites() (第 470 行) — 简化流程
static void RenderSprites(u16 dispcnt, s8 priority)
{
const bool8 mapping1d = dispcnt & 0x40; // 1D/2D Tile 映射模式
if (!(dispcnt & 0x1000)) return; // OBJ 显示未启用
for (s32 i = 127; i >= 0; i--) { // 倒序(低索引 = 高优先级)
// 读取 OAM 属性
const u16 a0 = ReadU16(base), a1 = ReadU16(base + 2), a2 = ReadU16(base + 4);
// ... 解析形状、大小、位置、优先级、调色板、是否仿射 ...
if (priority >= 0 && spritePriority != priority) continue; // 优先级过滤
if (affine) {
// 仿射精灵:从矩阵区读 PA/PB/PC/PD,反向映射纹理坐标
const s32 px = ((pa * dx + pb * dy) >> 8) + texCx;
const s32 py = ((pc * dx + pd * dy) >> 8) + texCy;
// ... 越界跳过,否则 ObjPixel() ...
} else {
// 普通精灵:处理翻转
const u8 px = a1 & 0x1000 ? w - 1 - x : x; // 水平翻转
const u8 py = a1 & 0x2000 ? h - 1 - y : y; // 垂直翻转
// ... ObjPixel() ...
}
// 命中像素 → PutPixel(ox + x, oy + y, color, LAYER_OBJ, forceAlphaBlend)
}
}精灵尺寸由 sizes[shape][(a1>>14)&3] 查表得到(shape 来自 a0 高 2 位)。半透明精灵(objMode == 1)置 forceAlphaBlend = TRUE,强制走 Alpha 混合。
ObjPixel()(第 432 行)从 VRAM + 0x10000 起的 OBJ Tile 区读取像素,调色板用 OBJ_PLTT(背景调色板之后的 256 色)。
HBlank DMA 扫描线:RefreshHblankDmaGpuRegs() / ScanlineGpuReg()
c
// src/wasm_display.c — RefreshHblankDmaGpuRegs() (第 116 行,静态)
// 扫描 4 个 DMA 通道,挑出 HBlank 触发 + 重复 + 16位 + 计数=1 + 目标为 GPU 寄存器的通道
for (u32 channel = 0; channel < 4; channel++) {
const u16 control = ReadU16(dma + 10);
if (!(control & DMA_ENABLE) || !(control & DMA_REPEAT)
|| (control & DMA_START_MASK) != DMA_START_HBLANK
|| (control & DMA_32BIT) || ReadU16(dma + 8) != 1) continue;
// ... 校验目标地址落在 GPU 寄存器区 ...
sHblankDmaGpuRegs[offset >> 1].src = ReadU32(dma); // DMA 源地址
sHblankDmaGpuRegs[offset >> 1].stride = /* FIXED?0 : DEC?-2 : 2 */;
sHblankDmaGpuRegs[offset >> 1].active = TRUE;
}
// src/wasm_display.c — ScanlineGpuReg() (第 157 行)
static u16 ScanlineGpuReg(u32 offset, u8 y)
{
if (offset < REG_OFFSET_DMA0) {
struct HblankDmaGpuReg *dma = &sHblankDmaGpuRegs[offset >> 1];
if (dma->active && y > 0)
return ReadU16(dma->src + dma->stride * (y - 1)); // 第 y 行看到的寄存器值
}
return ReadU16(REG_BASE + offset); // 无 DMA,返回当前值
}这样 TextBgPixel 读 hofs、WindowMask 读 WINxH、ActiveBlendColor 读 BLDY 时,都能拿到逐扫描线变化的值。
导出与宿主搬运
c
// src/wasm_display.c — 导出给宿主 (第 617/622 行)
u8 *WasmDisplayBuffer(void) { return sWasmDisplayRgba; }
u32 WasmDisplayBufferSize(void) { return sizeof(sWasmDisplayRgba); }浏览器端的 app.js 现在极简——把 WASM 缓冲区直接包成 ImageData 贴上 Canvas:
javascript
// web/app.js — 渲染相关 (第 603-610 行)
const ptr = instance.exports.WasmDisplayBuffer(); // RGBA 缓冲区指针
const size = instance.exports.WasmDisplayBufferSize();
image = new ImageData(new Uint8ClampedArray(memory.buffer, ptr, size), WIDTH, HEIGHT);
function render() {
instance.exports.WasmRenderFrame(); // WASM 内部合成整帧
ctx.putImageData(image, 0, 0); // 搬运到 Canvas
}render() 由主循环 tick() 在每帧 WasmRunFrame() 之后调用一次。原生前端同理:native-raylib 把同一缓冲区上传为纹理,native-kindle 转灰度写 /dev/fb0,native-bench 对它做 FNV 哈希做正确性校验。
关键函数索引
| 函数 | 行号 | 职责 |
|---|---|---|
WasmRenderFrame() | 600 | 渲染导出入口,按模式分发到 Tile/位图渲染 |
WasmDisplayBuffer() / WasmDisplayBufferSize() | 617 / 622 | 导出 RGBA 缓冲区给宿主 |
GbaColor() | 99 | BGR555 → RGB888 调色板转换 |
PutPixel() | 240 | 单像素写入,含 Window 遮罩和 Alpha 混合 |
ClearScreen() | 263 | 用背景色(BG_PLTT[0])填充整帧 |
RenderBitmapMode3() | 272 | Mode 3 位图渲染(16bit 直色) |
RenderBitmapMode4() | 286 | Mode 4 位图渲染(8bit 索引 + 双缓冲) |
TextBgPixel() | 303 | 文字背景单像素渲染(Tile Map 查找 + 翻转) |
AffineBgPixel() | 347 | 仿射背景单像素渲染(仿射矩阵变换) |
BgLayersForMode() | 386 | 根据模式返回 BG 层列表及类型 |
ObjTileOffset() | 425 | 计算精灵 Tile 偏移(1D/2D mapping) |
ObjPixel() | 432 | 精灵单像素读取(VRAM OBJ 区域) |
RenderBgLayer() | 454 | 渲染单个 BG 层所有像素 |
RenderSprites() | 470 | 精灵渲染(普通 + 仿射,按优先级过滤) |
RenderTiled() | 567 | Tile 模式渲染(优先级交替合成) |
WindowMask() | 174 | Window 遮罩计算 |
ActiveBlendColor() | 195 | Alpha 混合 / 亮度增减特效 |
RefreshHblankDmaGpuRegs() / ScanlineGpuReg() | 116 / 157 | HBlank DMA 扫描线寄存器采样 |
WasmRefreshHblankDmaGpuRegs() / WasmWindowMask() | 585 / 590 | 供宿主/自动化探测的导出包装 |
宿主侧(
web/app.js)相关:render()(第 608 行)、ImageData绑定(第 603 行)、主循环tick()。行号随上游版本可能变化,请以实际源码为准。