Skip to content

渲染管线 — 代码走读

本章逐段解读 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 混合判断目标层)
}

layerLAYER_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() 对每个像素调用 TextBgPixelAffineBgPixel,命中(返回 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,返回当前值
}

这样 TextBgPixelhofsWindowMaskWINxHActiveBlendColorBLDY 时,都能拿到逐扫描线变化的值。

导出与宿主搬运

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/fb0native-bench 对它做 FNV 哈希做正确性校验。

关键函数索引

函数行号职责
WasmRenderFrame()600渲染导出入口,按模式分发到 Tile/位图渲染
WasmDisplayBuffer() / WasmDisplayBufferSize()617 / 622导出 RGBA 缓冲区给宿主
GbaColor()99BGR555 → RGB888 调色板转换
PutPixel()240单像素写入,含 Window 遮罩和 Alpha 混合
ClearScreen()263用背景色(BG_PLTT[0])填充整帧
RenderBitmapMode3()272Mode 3 位图渲染(16bit 直色)
RenderBitmapMode4()286Mode 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()567Tile 模式渲染(优先级交替合成)
WindowMask()174Window 遮罩计算
ActiveBlendColor()195Alpha 混合 / 亮度增减特效
RefreshHblankDmaGpuRegs() / ScanlineGpuReg()116 / 157HBlank DMA 扫描线寄存器采样
WasmRefreshHblankDmaGpuRegs() / WasmWindowMask()585 / 590供宿主/自动化探测的导出包装

宿主侧(web/app.js)相关:render()(第 608 行)、ImageData 绑定(第 603 行)、主循环 tick()。行号随上游版本可能变化,请以实际源码为准。