« 任天堂娱乐系统(NES)架构 (index)

任天堂娱乐系统(NES)架构

Chapter 5: 图形


目录

  1. 硬件组织
  2. 构造帧
    1. 图块
    2. 背景图层
    3. 精灵层
    4. 背景分屏
    5. 结果
  3. 秘密和限制
    1. 多重卷轴
    2. 图块转换
    3. 奇怪的行为

图形是由一个名为图像处理单元(Picture Processing Unit,PPU)的专有芯片生成的。 这是 NES 的标志性芯片 换句话说,所有人都可以从硬件商店购买 6502 CPU,那为什么说 NES 与 Apple 2 或 Commodore 64 不同呢? 嗯,NES 与其他机器的区别就在于 CPU 外围的芯片:PPU 和 APU。 这些分别造就了 NES 独特的图形和音频功能。

就是说,PPU 渲染被称为精灵图(sprite)背景(Background)的 2D 图形,将渲染结果输出为视频信号。

硬件组织

Image
PPU的内存架构

为了在屏幕上渲染图形,PPU 必须知道要绘制什么图形,将它们放置在屏幕上的哪个位置;以及如何绘制它们(即,使用哪个调色板)。

为了回答这些问题,PPU 被预编程为使用不同内存映射,查找下列类型的数据:

不用担心新术语,这些数据结构的含义将在下面的段落中逐步讨论。

构造帧

与其同时代产品一样,该芯片专为 CRT 显示器特性而设计。 没有帧缓冲:PPU 与 CRT 的电子枪同步渲染,即时构建图像。

PPU 绘制尺寸固定为 256x240 像素的帧。 哎呀,因为世界各地模拟电视标准不同,图像会因为显示设备的制式(NTSC 或 PAL)而存在差异。 简而言之,NTSC 电视会裁剪顶部和底部边缘以适应过扫描(只有约 244 行扫描线可见),所以开发人员在游戏中放置元素的时候将这些边缘视为"危险区域。" 另一方面,PAL 电视不会裁剪边缘,但是会用黑边来填充比 NTSC 多出来的扫描线(PAL使用 288 行扫描线)。

在幕后,PPU 输出的帧由两层组成。 出于演示目的,让我们用 《超级马力欧兄弟》 来展示它是如何工作的:

图块

Image
两个由很多图块组成的 pattern table 上下拼接在一起。

Image
一个图块。

在 Character ROM 中找到的图块。(为了演示目的,使用了默认调色板)。

首先,PPU 使用图块(tile)作为精灵图(sprite)和背景(background)的构成。

NES 将图块定义为基本的 8x8 像素图,它们存储在 Character Memory(位于游戏卡带中)并组织成被为 Pattern Table 的大型数据结构。 每个图块占用 16 B,一个 Pattern Table 包含 256 个图块 。 由于 PPU 最大可寻址 8 KB 的 Character Memory,因此它最多可以同时访问两个 Pattern Table。

在图块内部,每个像素使用 2-bits 编码,每个像素可以引用调色板 4 种颜色中的一种。 程序员最多可以定义 8 个调色板(4 个用于背景,其它的用于精灵图)。 每个调色板上引用的颜色指向由 64 种颜色 组成的"主调色板",主调色板代表该主机可以生成的所有颜色。 调色板由 4 种颜色组成,其中一个被保留用来 表示透明

要开始在屏幕上绘制一些东西,游戏会设置一些表格,其中引用了 Character Memory 中的图块。 每个表负责每一帧中的一层(精灵图或者背景)。 然后,PPU 读取这些表,将其组装成 CRT 电子枪生成的扫描线。

我现在将解释每个层/表是如何工作的,以及它们在功能方面有何不同。

背景图层

Image
填充好的背景图。

Image
填充好的背景图,选定区域被标记。

设置有垂直镜像的背景图,可实现平滑的水平卷动。 但是,只有一半可用。

背景层是一个静态图块组成的 512x480 的像素图 。 你可能还记得可见帧要小得多,所以由游戏来决定显示哪一部分图层。 游戏还可以在运行过程中移动可见区域:卷轴效果就是这样实现的。

为了节约内存,4 个图块被组合成称为 块(Block)的 16x16 像素图,其中所有图块共享同一个调色板。

Nametable(保存在 VRAM中)指定在背景层中显示哪些图块。 PPU 查找 4 个 1024 Bytes 大小的 Nametable,每个 Nametables 对应背景层的一个象限。 但是,只有 2 KB 的 VRAM 可用! 因此,如果卡带中没有额外的 VRAM,则只能存储 2 个 Nametable。 然而,还需要处理另外两个 Nametable 的地址:大多数游戏只是简单的让后两个 Nametable 与前两个 Nametable 使用相同的地址(这被称之为镜像)。

这种架构初看可能存在缺陷,但是它旨在降低成本,同时提供简单的 可扩展性:如果游戏需要更宽的背景,可以在卡带中包含额外的 VRAM。

每个 Nametable 的最后 64 字节用来存储属性表(Attribute table),该表用来指定每个块(Block,由 4 个图块组成)的调色板 。

精灵层

Image
渲染精灵图层.

精灵图(Sprite)是可以在屏幕上移动的图块(Tile)。 它们可以相互重叠,或者出现在背景后面。 可见性由它自身优先级数值决定(与传统图形设计软件中的"图层"概念相同)。

Object Attribute Memory(OAM)表指定了哪些图块被用作精灵图 。 除了图块索引外,每个条目还包含了 (x, y) 坐标以及多个属性(调色盘,优先级和翻转标志)。 该表保存在 PPU 芯片的 256 Bytes DRAM 中。

OAM 表可以由 CPU 填充。 但是实践中这样做会很慢(并且如果没有在正确的实时间点完成,会有破坏帧图像的风险),因此,PPU 包含了一个名为直接内存访问(Direct Memory Access)或"DMA"的小组件,可以通过对它编程(方法是修改 PPU 的寄存器)来实现从 WRAM 复制数据到 OAM。 使用 DMA,可以保证数据表会在下一帧绘制开始前传输结束,但请记住,CPU 会在传输过程中暂停工作!

PPU 被限制为每条扫描线 八个精灵图,每帧最多 64 个精灵图。 由于一种名为"OAM 轮替顺序"的技术,可以突破扫描线限制,游戏手动改变OAM中条目的顺序。 这使得PPU在每一帧渲染不同的精灵集,同时CRT光束的速度会欺骗人眼,使其看到超出限制的精灵图。 然而,它们也会在屏幕上出现闪烁。

背景分屏

Image
渲染结束的背景层,注意着重标记两个部分,这两个部分使用了不同的卷轴坐标值。 只有第二个部分会随着马力欧的移动而卷动。

在继续之前,有一件事我还没告诉你。 如果你玩《超级马力欧兄弟》,你会注意到马力欧移动的时候,场景滚动很流畅。 但是,你会注意到顶部区域(统计数据所在的位置)保持静止,即使这两个部分都是同一个背景层!那么,这里发生了什么? 好吧,游戏在帧生成的过程中(即所谓的 mid-frame)改变了卷轴滚动值,这样就可以显示当前世界名和统计数据(位于背景顶部的固定部分)。 NES 本身不提供这个功能,但是游戏可以通过观察 PPU 状态(主要是通过其状态寄存器)来推断时机。

为了实现这一点,游戏会使用一种叫 Sprite 0 Hit 的技术。 超级马力欧兄弟控制 PPU 在顶部状态栏的硬币后面渲染一个隐藏的精灵图,这恰好是一帧内绘制的第一个精灵图。 在 PPU 控制的电子束绘制完成第一个精灵图后,PPU 会在它的状态寄存器更新一个标志,该标志表示第一个精灵图(又名"Sprite 0")绘制完成。 同时,游戏会在帧生成过程中不停检测 PPU 是否标记了"Sprite 0"状态(又被称为"Hit",命中),如果命中,游戏会更新背景表的卷轴滚动属性,将可见区域移动到马力欧所在的位置。

总的来说,"Sprite 0 Hit"是一个非常微妙的操作,因为它很容易错过时机(Sprite 0 标志如果没有被及时清除,会导致重复的错误操作)。 此外,由于检测 PPU 状态需要循环执行,执行的成本会很昂贵(就 CPU 周期而言)。 从好的方面说,后续的 Mapper 通过使用自动中断接管了这一操作,当任意扫描线命中时会触发中断(这是更高效的技术)。例如,《超级马力欧兄弟 3》使用这一技术显著提高了图像表现。

结果

Image
嗒哒——!

完成了一帧,是时候继续下一帧了!

但是,CPU 不能修改 PPU 正在使用的任何表的内容,否则屏幕上会出现伪影。 所以,当所有扫描线完成后,PPU 会在 CPU 上触发垂直消隐(Vertical Blank,V-Blank)中断 。 这会通知游戏,现在可以开始更新表而不会破坏当前显示的图像了。 此时,CRT 电子束指向屏幕的可见区域下方,进入了过扫描区域(或底部边框区域)。

只有少数 PPU 寄存器可以在垂直消隐期间以外 被修改,这解释了为什么在帧生成过程中修改背景卷轴位置不会破坏图像。

秘密和限制

如果你认为分配内存来存储完整帧缓冲的设计更可取:这会导致非常高的内存消耗,而游戏主机的设计目标是合理的价格。 让我展示给你看为什么 NES 的设计是非常有效且灵活的。

多重卷轴

Image
超级马里奥2。 为垂直卷轴设置的 Nametable(水平镜像)。

Image
超级马里奥3。 马里奥可以跑和飞,所以需要 PPU 能沿对角线方向卷动。 注意右侧边缘显示了错误的调色板! 左侧边缘的错误被遮住了。

有些游戏需要主角能垂直移动——因此 Nametable 被设置为水平镜像。 另一些游戏需要主角左右移动,所以使用垂直镜像

任何一种镜像方式都允许 PPU 在不引起用户注意的情况下更新背景图块:在可见区域外的一定距离上更新图块。

但是如果角色想沿对角线方向移动怎么办? PPU 可以向任何方向滚动,但是如果没有额外的 VRAM,边缘会被迫共享同样的调色板(记住,图块被组织成块 )。

这就是为什么类似《超级马力欧兄弟 3》这样的游戏在马力欧移动的时候会在屏幕边缘显示奇怪的图形(游戏被配置为垂直卷轴)。 这可能是因为他们需要最小化每个卡带的硬件成本(因为这个游戏已经使用了功能非常强大的 Mapper)。

一个有趣的修复方案:PPU 允许开发者在图块上应用一个垂直蒙版,可以有效地隐藏部分错误渲染的区域。

图块转换

Image
初始扫描线。

Image
后期扫描线。

Image
用户实际看到的帧图像。

假设帧渲染使用特定扫描线期间可用的图块。

《超级马力欧兄弟 3》的另外一个特点是它能显示的图形数量。

这个游戏显示的背景图块种类数量超出了 NES 的严格限制。 那么它是怎么做到的? 如果能在图像生成时捕获两张截图,我们就可以看到最终显示的帧实际上是由两个不同的帧合成的。

这是 MMC3 Mapper 的另外一个神奇之处,它不仅提供了额外的程序 ROM 空间,还通过连接两个 Character ROM 芯片扩展了 Character ROM 空间。 通过检查 PPU 正在请求屏幕的哪一个部分,Mapper 会重定向 PPU 到两个芯片的访问,比起原始设计,屏幕上现在能同时显示更多的图块种类 。

奇怪的行为

在我的研究中,我遇到了许多描述 PPU 不常见行为的有趣文章,所以我想在这里提及其中的一些:


Previous: 4. 中央处理器 (CPU)

Next: 6. 音频


Rodrigo Copetti © 2026 RSS Feed

切换至现代版

主页 · 文章 · 支持 · 关于作者 · 关于此网站