Skip to content

渲染管线 — 练习

练习 1:追踪文字背景像素渲染

阅读 src/wasm_display.c 中的 TextBgPixel() 函数。假设当前处于 Mode 1,BG0 层(文字背景),追踪屏幕坐标 (120, 80) 处像素的完整渲染过程。

已知条件:

  • BG0CNT0x04000008)= 0x1FC0(charBase = 基地址 0,screenBase = 基地址 7,16 色模式,size = 0)
  • BG0HOFS0x04000010)= 0x0010(水平偏移 16)
  • BG0VOFS0x04000012)= 0x0008(垂直偏移 8)
  • 对应 Tile Map Entry = 0x0845

回答以下问题:

  1. 实际的地图坐标 (sx, sy) 是多少?
  2. Tile Map 中的 Entry 0x0845 解析出的 Tile 编号、翻转标志和调色板编号各是多少?
  3. 如果 Tile 内像素坐标为 (3, 5),该像素在 VRAM 中的读取地址如何计算?
参考答案
  1. 地图坐标计算(TextBgPixelScanlineGpuReg 读 hofs/vofs,此处无 HBlank DMA 故等于寄存器值):

    • sx = (120 + 16) & (256 - 1) = 136
    • sy = (80 + 8) & (256 - 1) = 88
    • 实际坐标为 (136, 88)
  2. Entry 0x0845 解析(entry & 0x3ff 取 Tile 号等):

    • Tile 编号 = 0x0845 & 0x3FF = 0x045 = 69
    • 水平翻转 = 0x0845 & 0x400 = 0x0400 ≠ 0,即水平翻转
    • 垂直翻转 = 0x0845 & 0x800 = 0,不翻转
    • 调色板编号 = (0x0845 >> 12) & 0xF = 0
  3. 像素地址计算(16 色模式,4bpp 打包):

    • 翻转后像素坐标:因为水平翻转,px = 7 - (136 & 7) = 7 - 0 = 7py = 88 & 7 = 0
    • Tile 在 charBase 中的偏移 = tile * 32 = 69 * 32 = 2208 字节
    • 行内偏移 = py * 4 + (px >> 1) = 0 * 4 + 3 = 3
    • 读取地址 = charBase + 2208 + 3charBase = VRAM + 0
    • *Ptr8(...) 读取一个字节,px & 1 = 1 故取高 4 位(packed >> 4)得到颜色索引
    • 最终颜色 = GbaColor(ReadU16(PLTT + (palette * 16 + colorIndex) * 2))

练习 2:分析精灵渲染与 OAM 属性

阅读 RenderSprites()ObjPixel() 函数,分析以下精灵:

  • a0 = 0x0001(Y=1, 形状=正方形, 普通模式, 16 色)
  • a1 = 0x5008(X=8, 尺寸=1, 水平翻转)
  • a2 = 0x0C02(Tile 基址=2, 优先级=3, 调色板=0)

回答以下问题:

  1. 该精灵在屏幕上的位置、大小和优先级是什么?
  2. 渲染精灵内像素 (4, 6) 时,由于水平翻转,实际从 Tile 中读取的纹理坐标是什么?
  3. 该精灵在 16 色、2D mapping 模式下,像素数据在 VRAM 中如何寻址?mapping1d 标志如何影响寻址?
参考答案
  1. 属性解析(RenderSprites 解析 a0/a1/a2):

    • 位置:X = 0x5008 & 0x1FF = 8,Y = 0x0001 & 0xFF = 1
    • 形状 = (0x0001 >> 14) & 3 = 0(正方形),尺寸 = (0x5008 >> 14) & 3 = 1 → sizes[0][1] = {16, 16},即 16×16
    • 优先级 = (0x0C02 >> 10) & 3 = 3
    • 调色板 = (0x0C02 >> 12) & 0xF = 0,16 色模式(a0 & 0x2000 = 0
  2. 纹理坐标(普通精灵,RenderSprites 非仿射分支):

    • 水平翻转标志 = a1 & 0x1000 = 0x5008 & 0x1000 = 0x1000 ≠ 0,水平翻转生效
    • 垂直翻转标志 = a1 & 0x2000 = 0x5008 & 0x2000 = 0,不翻转
    • px = w - 1 - x = 16 - 1 - 4 = 11(水平翻转),py = y = 6
  3. VRAM 寻址(ObjPixel + ObjTileOffset):

    • Tile 基地址 = a2 & 0x3FF = 2
    • tileOffset = ObjTileOffset(tileBase=2, tileX=11>>3=1, tileY=6>>3=0, width=16, color256=false, mapping1d=false)
    • 2D mapping:tileOffset = tileBase + tileY * 32 + tileX * 1 = 2 + 0 + 1 = 3
    • 像素读取:packed = *Ptr8(VRAM + 0x10000 + tileOffset * 32 + (py & 7) * 4 + ((px & 7) >> 1)) = VRAM + 0x10000 + 3*32 + (6&7)*4 + ((11&7)>>1) = VRAM + 0x10000 + 96 + 24 + 1
    • colorIndex = px & 1 ? packed >> 4 : packed & 15 =(11 & 1 = 1packed >> 4
    • mapping1d(DISPCNT bit6 = 1,1D mapping):tileOffset = tileBase + tileY * (width >> 3) + tileX(本题 tileY=0,结果相同)
    • 最终颜色 = GbaColor(ReadU16(OBJ_PLTT + (palette * 16 + colorIndex) * 2))(OBJ 调色板区在背景调色板之后)

练习 3:渲染管线性能估算

GBA 分辨率为 240×160 = 38,400 像素。假设当前处于 Mode 1,启用了 3 个文字背景层和 50 个可见精灵。

  1. 计算一帧渲染中,TextBgPixel() 被调用的次数
  2. 假设每次 TextBgPixel() 调用约需 100ns(包括 Tile Map 查找 + 像素读取 + 调色板查找),3 个 BG 层的纯背景渲染需要多少毫秒?
  3. 如果还要渲染 50 个 16×16 的精灵,额外需要多少像素处理操作?整个渲染管线在浏览器中能否在 16ms(60 FPS)预算内完成?
参考答案
  1. TextBgPixel() 调用次数:

    • 每个 BG 层遍历 240×160 = 38,400 个像素(RenderBgLayer 双重循环)
    • 3 个 BG 层 = 3 × 38,400 = 115,200 次调用
    • 注意:RenderTiled() 用优先级外循环,但每个 BG 层的每个像素最终只调用一次 TextBgPixel
  2. 背景渲染时间:

    • 115,200 次 × 100ns = 11.52 ms
    • 这已接近 16ms 帧预算,还未算精灵渲染与宿主 putImageData
  3. 精灵渲染开销:

    • 50 个 16×16 精灵 = 50 × 256 = 12,800 像素
    • 但很多精灵像素可能超出屏幕或透明(colorIndex == 0ObjPixel 返回 FALSE 跳过)
    • 假设约 60% 像素可见且不透明 ≈ 7,680 次有效像素操作
    • 额外时间 ≈ 7,680 × 100ns = 0.77 ms

    总计 ≈ 11.52 + 0.77 + putImageData 开销 ≈ 13-14 ms,勉强达到 60 FPS。

    与早期 JS 版相比,现在整条管线是 编译后的 C(WASM,或经 wasm2c 转成原生代码),执行效率显著高于解释执行的 JS。make native-bench 提供了精确测量手段:它从空白存档回放到大世界/菜单/战斗三个场景,预热 10,007 帧后精确计时 1,000,003 次 WasmRunFrame,并用渲染前后的 FNV 哈希做正确性校验(哈希不匹配则分数为 0)。因此上面 100ns 的假设相当保守。

拓展挑战

  • make native-bench 跑一次基准测试,观察大世界/菜单/战斗三个场景的真实 FPS,对比上面手工估算的数量级
  • 阅读 wasm_display.cActiveBlendColor() Alpha 混合逻辑,在游戏中找到一个实际使用半透明效果的场景(如水面反射),分析 BLDCNT/BLDALPHA 寄存器的配置
  • 研究 RefreshHblankDmaGpuRegs() / ScanlineGpuReg():找一个使用 HBlank DMA 逐行改写 BGxHOFSBLDY 的游戏效果,画出「扫描线 y → 寄存器值」的映射关系
  • 思考 RenderTiled() 为什么要把精灵在每个优先级级别都渲染一次,而不是「全部 BG 画完再画所有精灵」