SNES: 精灵与背景渲染
摘要
本文解释了SNES的PPU如何在紧张的VRAM带宽限制下渲染精灵和背景,描述了不同视频模式中的硬件权衡。
暂无内容
查看缓存全文
缓存时间: 2026/05/16 03:35
# SNES:精灵和背景的渲染
来源:https://fabiensanglard.net/snes_ppus_why/index.html
2024年8月9日
SNES:精灵和背景的渲染
---
上一篇文章讨论了超级任天堂图形系统的硬件(https://fabiensanglard.net/snes_ppus_how)。本文描述这些组件如何协同工作以渲染精灵和背景。
在研究这个主题时,我学到的最有趣的事情是,诸如模式中的图层数量、每个图层的深度(位每像素)以及一行中精灵的最大数量等限制,都源于单个组件的延迟。
要解决的问题:每 186.2ns 一个像素
---
SNES 属于一个没有帧缓冲区的时代。一场画面逐行渲染,每个像素的颜色在 CRT 需要它的那一刻即时生成。在视频系统文章(https://fabiensanglard.net/snes_video)中,我们看到 SNES 视频点时钟为 5,369,317Hz。这意味着它必须每 1,000,000,000ns / 5,369,317 = 186.2ns 产生一个颜色并发送到 CRT,无论发生什么。
为了生成一个像素,SNES 需要检索背景和精灵描述符,然后获取实际的调色板索引。问题在于访问 VRAM 需要时间。非常长的时间。如果我们查看 SNES 中使用的 VRAM 芯片数据手册(https://fabiensanglard.net/snes_ppus_how/LH5P832.pdf),我们看到访问时间为 100ns。这意味着 PPU 每像素只能发出**一次 VRAM 读取**。
按此描述,听起来不可能。如果它们只能读取一次,PPU 如何处理四个图层?它们又如何能在图层之上检索精灵像素?
SNES 图块地图渲染的工作原理
---
图块地图(即背景、图层)由图块地图条目[\[1\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_1)描述,存储在 VRAM 中。一个条目为 16 位,提供 8x8 图块[\[2\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_2)的地址,以及调色板、翻转位和优先级等属性。根据模式不同,图块具有不同的位每像素(bpp),从 2bpp 一直到 8bpp。
为了生成每个像素,PPU2 需要检索所有图层的调色板索引,根据透明度和优先级进行合成(选择一个),通过调色板从索引获取颜色,然后向 CRT 发出颜色。
下表列出了 SNES 支持的所有模式[\[3\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_3)以及每个图层的调色板索引深度。注意,支持的图层越多,bpp 越低。这个限制完全源于限制了带宽的 VRAM 延迟。
视频模式BG 0BG 1BG 2BG 302bpp2bpp2bpp2bpp14bpp4bpp2bpp24bpp4bpp38bpp4bpp48bpp2bpp54bpp2bpp64bpp78bppEXTBG模式 7 中的 EXTBG 是什么?它是 BG0 图层的副本,但优先级不同。它允许将精灵夹在中间。在《魂斗罗 3》中,这就是玩家精灵在第二关能够走到桥下的原因![\[4\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_4)
解决方案
---
每像素一次读取不可行,但 PPU 有内部存储器来分摊成本。PPU2 在绘制一行第一个像素之前就开始接收 VRAM 数据并将其记忆。
让我们详细看看这是如何工作的,并进行延迟计算以确保其成立。
模式 0
---
模式 0 有四个图层。这意味着每像素需要访问四个 16 位的图块地图条目。由于总线是 16 位宽,PPU 可以用四次读取完成。VRAM 访问时间预算已经超支(4 x 100ns = 400ns)。
对于每个图块地图条目,必须从引用的图块中获取调色板索引。一行图块有 8 像素宽,在模式 0 中图块使用 2bpp。一次 16 位读取检索 8 个像素(一整行图块)。让我们计算一下。
``
1 次读取 -> BG 0 图块地图条目
1 次读取 -> BG 1 图块地图条目
1 次读取 -> BG 2 图块地图条目
1 次读取 -> BG 3 图块地图条目
1 次读取 -> BG0 图块的 8 个像素
1 次读取 -> BG1 图块的 8 个像素
1 次读取 -> BG2 图块的 8 个像素
1 次读取 -> BG3 图块的 8 个像素
===============================
8 次读取 -> 可以生成 8 个像素
``
它勉强在时间预算内,但可行!通过提前 8 个像素开始,PPU 可以发出 8 次读取,记住每个结果,然后在接下来的 8 个点时钟内生成 8 个像素,无需访问 VRAM。这个过程可以重复,直到一整行绘制完成。
为了在一次 16 位读取中检索 8 个像素(对于 2 bpp 图层),SNES 要求背景图块以有利的方式布局。这称为平面模式[\[5\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_5),其中一个芯片保存高位字节,而另一个保存低位字节。
同样,图块地图条目根据我们在上一篇文章中看到的 VRAM 交错方式存储,以保证使用两个地址总线上的相同地址,在一次读取中检索完整条目。
模式 1
---
模式 1(游戏中最常用的模式)由三个图层组成。两个使用 4bpp,第三个使用 2bpp。再次进行计算。
``
1 次读取 -> BG 0 图块地图条目
1 次读取 -> BG 1 图块地图条目
1 次读取 -> BG 2 图块地图条目
1 次读取 -> BG0 图块的 4 个像素
1 次读取 -> BG0 图块的 4 个像素
1 次读取 -> BG1 图块的 4 个像素
1 次读取 -> BG1 图块的 4 个像素
1 次读取 -> BG2 图块的 8 个像素
===============================
8 次读取 -> 可以生成 8 个像素
``
再次,它勉强在预算内,但延迟成立!
为了在一次 16 位读取中检索 4 个像素,SNES 要求 4bpp 背景图块以另一种平面模式布局,其中两个平面存储在一个字节中。每个 VRAM 芯片中存储一个字节[\[6\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_6)。
模式 3
---
在模式 3 中,一个图层使用高达 8bpp,另一个图层使用 2bpp。
``
1 次读取 -> BG 0 图块地图条目
1 次读取 -> BG 1 图块地图条目
1 次读取 -> BG0 图块的 2 个像素
1 次读取 -> BG0 图块的 2 个像素
1 次读取 -> BG0 图块的 2 个像素
1 次读取 -> BG0 图块的 2 个像素
1 次读取 -> BG1 图块的 8 个像素
===============================
7 次读取 -> 可以生成 8 个像素
``
模式 2、4、5 和 6
---
我相信你已经明白了。
模式 7
---
模式 7 很特殊。尽管它只使用一个 8bpp 图层,但却是最难实现的。
在之前的所有例子中,计算之所以有效,是因为 16 位总线允许同时检索几个相邻的像素。但在模式 7 中,这个技巧不再可用,因为它的图层可以旋转和缩放。例如:如果图层旋转了 90°,批量检索水平连续的像素就没有什么意义了。
这个问题,任天堂用我们在硬件文章(https://fabiensanglard.net/snes_ppus_how)中看到的奇怪地址线解决了。在模式 7 中,VRAM 的设置不同。每个芯片只使用 16 KiB。一个芯片包含图块地图条目,而另一个包含图块[\[7\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_7)[\[8\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_8)。
凭借单独寻址每个芯片的能力,PPU 可以每个周期发射 1 个像素。
``
时间 -2:获取像素 0 的图块地图(VRAM 芯片 1)
时间 -1:获取像素 1 的图块地图(VRAM 芯片 1)
时间 -1:获取像素 0 的图块(VRAM 芯片 2)
时间 0:发射像素 0
时间 0:获取像素 2 的图块地图(VRAM 芯片 1)
时间 0:获取像素 1 的图块(VRAM 芯片 2)
时间 1:发射像素 1
时间 1:获取像素 3 的图块地图(VRAM 芯片 1)
时间 1:获取像素 2 的图块(VRAM 芯片 2)
``
两次读取得到两个像素,即每像素一次访问。再次,时序成立。
SNES 精灵渲染的工作原理
---
SNES 支持屏幕上最多 128 个精灵(使用精灵复用可以更多)。它们由 15 种颜色 + 透明度(4bpp)组成,宽度为 8、16、32 或 64 像素。
精灵属性存储在 OAM(PPU1 内部),但精灵颜色仍然需要从 VRAM 中检索。这是一个问题,因为整个 VRAM 带宽都被背景渲染消耗了。
> 在活动屏幕渲染期间,PPU 的视频 RAM 对 SNES CPU 完全不可访问:当 PPU 访问该内存时,你甚至无法读取正在获取的内容。—— byuu[\[9\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_9)
SNES 一行由 341 个点组成。其中 256 个有颜色,而 85 个是消隐的。
PPU1 利用 HBLANK 时间(VRAM 不被使用)尽可能多地获取精灵数据。它构建一行精灵图层,并将其缓存到一个称为精灵行缓冲区的结构中。
行缓冲区由 8 像素组组成,称为片(slivers)(即 4 字节,也就是两次 VRAM 访问)。PPU1 行缓冲区有空间容纳 34 个片。这意味着一条扫描线最多可以包含 34 个精灵或 34 * 8 = 272 个精灵像素,以先达到的限制为准[\[10\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_10)。
更多关于精灵系统
---
精灵渲染的主题非常迷人。值得作为支线任务探索 90 年代初其他机器是如何做到的。
精灵技术机器详情软件Amstrad CPC血汗泪Atari ST泪精灵单元C648个精灵单元,每行不可复用[\[11\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_11)[\[12\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_12)Amiga8个精灵单元。每行可复用[\[13\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_13)[\[14\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_14)[\[15\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_15)行缓冲区SNES128个精灵,每行最多:32个精灵 / 272像素。Sega Genesis80个精灵,每行最多:20个精灵 / 320像素[\[16\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_16)[\[17\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_17)双行缓冲区Neo-Geo381个精灵,每行96个[\[18\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_18)精灵帧缓冲区CPS-1256个精灵。无每行限制。软件渲染
作为曾经拥有 Amstrad CPC、Atari ST、PC 的用户,我对此没有太多美好的回忆。也许读者会理解为什么我如此喜爱 SNES。
精灵单元
---
在 SNES 和 Genesis 之前推出的机器使用“精灵单元”,它拦截光栅线以在背景之上叠加精灵。这种方法使得精灵获取与背景获取在一行渲染时竞争。
一种很酷的技术允许通过称为复用(multiplexing)的方式绘制比精灵单元数量更多的精灵。其思想是在屏幕下方不再需要某个单元(所有精灵高度已渲染)时重新使用它。
Amiga 甚至更强大,因为它能够在一行上复用精灵单元[\[19\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_19),并完全用精灵图层覆盖屏幕[\[20\]](https://fabiensanglard.net/snes_ppus_why/index.html#footnote_20)。
行缓冲区
---
行缓冲区不仅被超级任天堂使用。Sega Genesis 也用它来渲染精灵。
注意,SNES 并没有用精灵检索充满 HBLANK。获取 34 个片需要 100ns * 2 * 34 = 6800ns。HBLANK 持续 85 * 186.2ns = 15,827ns。这留下了 15,827 - 6,800 = 9,027ns 用于特殊效果。例如:在 HBLANK 期间每行图层分别移动的华丽光栅效果,如下面《魂斗罗 3》所示。
*《魂斗罗 III》(1992)*双行缓冲区
---
进一步提升的方法是加倍精灵行缓冲区。
使用双行缓冲区,机器始终在渲染精灵。一个缓冲区提供给 CRT,另一个正在生成。这就是 Neo Geo 的工作方式。
相对于单个行缓冲区,其巨大优势在于 PPU 不仅可以在 HBLANK 期间检索精灵,还可以在一整行的整个持续时间内检索精灵。
这种设计,加上直接连接到 MiB GFX-ROM 的大规模 PPU 数据线,使 Neo-Geo 每行最多可支持 96 个精灵(是 SNES 的 3 倍)。
*来源 (superadventuresingaming.blogspot.com) (https://superadventuresingaming.blogspot.com/2020/09/metal-slug-arcade.html) 。《合金弹头》*如果 PPU 为下一行获取精灵,那么如何获取当前行的背景?答案:它不获取。Neo-Geo 没有背景,一切都是精灵。
双精灵帧缓冲区
---
比双行缓冲区更好的是什么?一个全屏精灵缓冲区,我称之为精灵帧缓冲区。
这是一种极其昂贵的架构,因为它需要足够的 VRAM 来覆盖整个屏幕(由于双缓冲,需要两倍)。这在 90 年代是一个非常昂贵的选择。不仅因为 RAM 昂贵,而且 VRAM 由 SRAM(相对于 DRAM)制成。
Capcom 在打造其街机 CPS1 机箱(https://fabiensanglard.net/cpsb_paper)时不惜工本。《街头霸王 II》和《快打旋风》中巨大的街机精灵本身就说明了问题。
*《街头霸王 II》(1991)*精灵帧缓冲区的优点有两个:有了精灵帧缓冲区,系统有一整帧的时间来获取所有精灵,从而允许更多的精灵。因此,CPS-1 支持 256 个精灵,由 16x16 图块(4bpp)组成。更重要的是,没有每行限制。
整合在一起
---
此时,我们完整了解了 PPU 内部发生的事情。为了绘制一行,PPU1 通过设置 VRAM 地址线驱动 PPU2,PPU2 在 VRAM 数据线上消耗 BG 图层。通过直接连接到 PPU2 的线路,PPU1 流式传输在上一个 HBLANK 期间构建的精灵图层。
所有五个图层的合成发生在 PPU2 上。生成的调色板索引与调色板对照,并向 CRT 发送 RGB 颜色。
再次,该架构与 CPS1 PPU 非常相似。在 Capcom 的机器中,CPS-A 驱动 CPS-B 绘制一个精灵帧缓冲区,同时向 CPS-B 填充另一个精灵帧缓冲区。CPS-B 完成所有合成工作。
*Capcom PPU,CPS-A 和 CPS-B(来自《CP-System 书》(https://fabiensanglard.net/cpsb_paper)》*图形流水线依赖于一个巨大的 32 位数据总线,由双通道 GFX ROM 供电。与 SNES 相比,它确实是压倒性的。
进一步探索
---
本文不涉及 PPU 编程主题,因为 snes.nesdev.org(https://snes.nesdev.org/)上有广泛介绍。还有一系列精彩的 YouTube 视频(https://www.youtube.com/playlist?list=PLHQ0utQyFw5KCcj1ljIhExH_lvGwfn6GV),由“Retro Game Mechanics Explained”制作,你不应错过。
参考文献
---
^ (https://fabiensanglard.net/snes_ppus_why/index.html#back_1)[ 1]snes.nesdev.org: Tilemaps (https://snes.nesdev.org/wiki/Tilemaps)
^ (https://fabiensanglard.net/snes_ppus_why/index.html#back_2)[ 2]snes.nesdev.org: Tiles (https://snes.nesdev.org/wiki/Tiles)
^ (https://fabiensanglard.net/snes_ppus_why/index.html#back_3)[ 3]snes.nesdev.org: Backgrounds (https://snes.nesdev.org/wiki/Backgrounds#EXTBG)
^ (https://fabiensanglard.net/snes_ppus_why/index.html#back_4)[ 4]snes.nesdev.org: Uncommon graphics mode games (https://snes.nesdev.org/wiki/Uncommon_graphics_mode_games)
^ (https://fabiensanglard.net/snes_ppus_why/index.html#back_5)[ 5]Tiles: 2bpp layout (https://snes.nesdev.org/wiki/Tiles#:~:text=7%20Direct%20Color-,2bpp,-Used%20in%20all)
^ (https://fabiensanglard.net/snes_ppus_why/index.html#back_6)[ 6]Tiles: 4bpp layout (https://snes.nesdev.org/wiki/Tiles#:~:text=in%20CGRAM.-,4bpp,-The%20most%20common)
^ (https://fabiensanglard.net/snes_ppus_why/index.html#back_7)[ 7]Tilemap in Mode 7 (https://snes.nesdev.org/wiki/Tilemaps#:~:text=vertically%20(%2B16%2C%20%2B17).-,Mode%207,-Mode%207%20has)
^ (https://fabiensanglard.net/snes_ppus_why/index.html#back_8)[ 8]T
相似文章
SNES 图形系统工作原理
基于 Jonathon Donaldson 的原理图,对超级任天堂图形硬件的详细技术解释,包括 PPU1 和 PPU2 芯片、VRAM、OAM 和 CGRAM。
超级任天堂卡带内部揭秘
对超级任天堂卡带的详细技术分析,涵盖 CIC 复制保护、ROM 容量分布、带电池备份的 SRAM 以及 Super FX 等增强处理器。
剖析超级任天堂视频系统
从任天堂工程师的视角,详细探讨超级任天堂的视频系统设计,解释CRT技术与工程选择。
使用稀疏条带在CPU上进行高性能2D图形渲染
研究采用稀疏条带技术在CPU上优化2D图形渲染,以提升性能并降低内存开销。
Nintendo 64 上的加法混合(Additive Blending)
文章介绍了在 Nintendo 64 上实现加法混合的技术挑战,由于该平台缺乏颜色钳制功能,作者提出了一种使用 Libdragon 的解决方案,通过管理 32 位缓冲区并进行颜色转换来实现更出色的视觉效果。