Appearance
渲染管线 — 练习
练习 1:追踪文字背景像素渲染
阅读 src/wasm_display.c 中的 TextBgPixel() 函数。假设当前处于 Mode 1,BG0 层(文字背景),追踪屏幕坐标 (120, 80) 处像素的完整渲染过程。
已知条件:
BG0CNT(0x04000008)=0x1FC0(charBase = 基地址 0,screenBase = 基地址 7,16 色模式,size = 0)BG0HOFS(0x04000010)=0x0010(水平偏移 16)BG0VOFS(0x04000012)=0x0008(垂直偏移 8)- 对应 Tile Map Entry =
0x0845
回答以下问题:
- 实际的地图坐标
(sx, sy)是多少? - Tile Map 中的 Entry
0x0845解析出的 Tile 编号、翻转标志和调色板编号各是多少? - 如果 Tile 内像素坐标为
(3, 5),该像素在 VRAM 中的读取地址如何计算?
参考答案
地图坐标计算(
TextBgPixel用ScanlineGpuReg读 hofs/vofs,此处无 HBlank DMA 故等于寄存器值):sx = (120 + 16) & (256 - 1) = 136sy = (80 + 8) & (256 - 1) = 88- 实际坐标为
(136, 88)
Entry
0x0845解析(entry & 0x3ff取 Tile 号等):- Tile 编号 =
0x0845 & 0x3FF=0x045= 69 - 水平翻转 =
0x0845 & 0x400=0x0400≠ 0,即水平翻转 - 垂直翻转 =
0x0845 & 0x800= 0,不翻转 - 调色板编号 =
(0x0845 >> 12) & 0xF= 0
- Tile 编号 =
像素地址计算(16 色模式,4bpp 打包):
- 翻转后像素坐标:因为水平翻转,
px = 7 - (136 & 7) = 7 - 0 = 7,py = 88 & 7 = 0 - Tile 在 charBase 中的偏移 =
tile * 32 = 69 * 32 = 2208字节 - 行内偏移 =
py * 4 + (px >> 1) = 0 * 4 + 3 = 3 - 读取地址 =
charBase + 2208 + 3(charBase = 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)
回答以下问题:
- 该精灵在屏幕上的位置、大小和优先级是什么?
- 渲染精灵内像素
(4, 6)时,由于水平翻转,实际从 Tile 中读取的纹理坐标是什么? - 该精灵在 16 色、2D mapping 模式下,像素数据在 VRAM 中如何寻址?
mapping1d标志如何影响寻址?
参考答案
属性解析(
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)
- 位置:X =
纹理坐标(普通精灵,
RenderSprites非仿射分支):- 水平翻转标志 =
a1 & 0x1000=0x5008 & 0x1000=0x1000≠ 0,水平翻转生效 - 垂直翻转标志 =
a1 & 0x2000=0x5008 & 0x2000= 0,不翻转 px = w - 1 - x = 16 - 1 - 4 = 11(水平翻转),py = y = 6
- 水平翻转标志 =
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 = 1)packed >> 4- 若
mapping1d(DISPCNT bit6 = 1,1D mapping):tileOffset = tileBase + tileY * (width >> 3) + tileX(本题 tileY=0,结果相同) - 最终颜色 =
GbaColor(ReadU16(OBJ_PLTT + (palette * 16 + colorIndex) * 2))(OBJ 调色板区在背景调色板之后)
- Tile 基地址 =
练习 3:渲染管线性能估算
GBA 分辨率为 240×160 = 38,400 像素。假设当前处于 Mode 1,启用了 3 个文字背景层和 50 个可见精灵。
- 计算一帧渲染中,
TextBgPixel()被调用的次数 - 假设每次
TextBgPixel()调用约需 100ns(包括 Tile Map 查找 + 像素读取 + 调色板查找),3 个 BG 层的纯背景渲染需要多少毫秒? - 如果还要渲染 50 个 16×16 的精灵,额外需要多少像素处理操作?整个渲染管线在浏览器中能否在 16ms(60 FPS)预算内完成?
参考答案
TextBgPixel()调用次数:- 每个 BG 层遍历 240×160 = 38,400 个像素(
RenderBgLayer双重循环) - 3 个 BG 层 = 3 × 38,400 = 115,200 次调用
- 注意:
RenderTiled()用优先级外循环,但每个 BG 层的每个像素最终只调用一次TextBgPixel
- 每个 BG 层遍历 240×160 = 38,400 个像素(
背景渲染时间:
- 115,200 次 × 100ns = 11.52 ms
- 这已接近 16ms 帧预算,还未算精灵渲染与宿主
putImageData
精灵渲染开销:
- 50 个 16×16 精灵 = 50 × 256 = 12,800 像素
- 但很多精灵像素可能超出屏幕或透明(
colorIndex == 0时ObjPixel返回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.c的ActiveBlendColor()Alpha 混合逻辑,在游戏中找到一个实际使用半透明效果的场景(如水面反射),分析BLDCNT/BLDALPHA寄存器的配置 - 研究
RefreshHblankDmaGpuRegs()/ScanlineGpuReg():找一个使用 HBlank DMA 逐行改写BGxHOFS或BLDY的游戏效果,画出「扫描线 y → 寄存器值」的映射关系 - 思考
RenderTiled()为什么要把精灵在每个优先级级别都渲染一次,而不是「全部 BG 画完再画所有精灵」