【译文】NTSC 2C02 技术参考

NTSC 2C02 技术参考

Brad Taylor ([email protected])

第五版:2004 年 4 月 23

感谢 NES 社区。 http://nesdev.parodius.com。

特别感谢 Neal Tew。

推荐文献:任天堂 PPU 专利文档 (U.S.#4,824,106)。

目录

  • 2C02 集成组件列表
  • 2C02 引脚及信号说明
  • 2C02 编程模型
  • 视频信号生成
  • 基础时序
  • 其它 PPU 信息
  • PPU 内存读写周期
  • 视频帧渲染细节
  • 扫描线渲染细节
  • 待渲染对象评估
  • 背景图渲染管线细节
  • 精灵 Pattern Table 读取与渲染细节
  • 变时长帧
  • MMC3 扫描线计数器
  • PPU 像素优先级怪事
  • PPU 滚动与寻址概述

2C02 集成组件列表

  • 控制寄存器、其它标志寄存器
  • 行、列计数器
  • 色彩同步相位生成器
  • VRAM 地址锁存器、计数器
  • 地址缓冲、图块索引字节
  • VRAM 读缓冲
  • 对象属性存储区 (OAM)
  • OAM 索引寄存器、计数器
  • OAM 临时存储区、扫描线大小比较器
  • 水平反相器、垂直反相器
  • OAM 像素缓冲区
  • 背景像素缓冲区
  • 多路复用器
  • 调色板存储区
  • 电平解码器/相位选择器/DAC
  • 写计数触发器

2C02 引脚及信号说明

          ___  ___
         |*  \/   |
 R/W  >01]        [40<  VCC
  D0  [02]        [39>  ALE
  D1  [03]        [38]  AD0
  D2  [04]        [37]  AD1
  D3  [05]        [36]  AD2
  D4  [06]        [35]  AD3
  D5  [07]        [34]  AD4
  D6  [08]        [33]  AD5
  D7  [09]        [32]  AD6
  A2  >10]  2C02  [31]  AD7
  A1  >11]        [30>  A8
  A0  >12]        [29>  A9
 /CS  >13]        [28>  A10
EXT0  [14]        [27>  A11
EXT1  [15]        [26>  A12
EXT2  [16]        [25>  A13
EXT3  [17]        [24>  /R
 CLK  >18]        [23>  /W
/VBL  <19]        [22<  /SYNC
 VEE  >20]        [21>  VOUT
         |________|

R/WD0-D7A2-A0/CS:这些是 PPU 的控制总线信号,用于对 2C02 的内部寄存器编程。R/W 控制数据方向 (0 表示写入 PPU 寄存器),A0-A2 用于选择要读写的 PPU 寄存器,当 /CS 被设置为 0 时,可通过 D0-D7 和所选的寄存器交换数据(如果 /CS=1,D0-D7 浮空)。下一节将讲述对寄存器的操作。

EXT0-EXT3:依据对 2C02 的编程方式不同,该总线可以作为像素输入(用于将外部生成的图形与 2C02 的混合),或输出(用于驱动另一个图形处理器)。通常情况下,该总线被编程为接受输入,因为 NES/FC 主板总是将这四个引脚接地。

CLK: 此为 2C02 的 21.48 MHz 时钟信号输入线。

/VBL:当 PPU 进入 VBLANK 阶段时,这个信号会发出一个表示逻辑 0 的电平,并且保持逻辑 0 长达 20 个扫描线。该信号通常连接到 2A03 的 /NMI 引脚,这样每一帧都能生成一个不可屏蔽的中断。基于 /VBL 中断的软件通常通过读写某个寄存器快速地清除和重新设置 /VBL 位,从而使得 /VBL 的激活时间少于一条扫描线。该输出也是集电极开路的。

VEEVCC: 接地和 +5V DC 电源线信号线。

VOUT:2C02 的无缓冲复合视频输出。为了让视频驱动器在峰峰值为 1V 时支持 75 欧姆的负载,该信号通常传送到两级共集电极晶体管放大器。

/SYNC:此信号为 0 时,将强行把色彩同步控制器状态、扫描线和 PPU 内部使用的像素计数器/触发器设置为确定状态。 一般,两个以主从配置(通过 EXT 总线)连接在一起的 2C02 会采用此方法相互同步; 主 PPU 使用 /VBL 线将 vblank 信息作为输入提供给从属 PPU 的 /SYNC 线。 在 Famicom 主机上,此引脚始被设置为逻辑 1 。 然而,在 NES 上,此引脚与 2A03 的复位输入相连。这就是在 NES 上按住复位开关时,PPU 不会渲染图像的原因。

/R/WALEAD0-AD7A8-A13:这些信号控制 PPU 相关的数据总线。 当 PPU 将地址位 0 到 7 放在 AD 总线上时,会激活 ALE( 逻辑 1)(通常使用 74LS373 来存储低地址总线内容)。 当 /R 或 /W 信号有效的时(逻辑 0 时),连接到 PPU AD 总线的存储设备就可以解码 PPU 产生的 14 位地址(由外部 A0-A7 锁存器和 A8-A13 线组合而成)了,并且可以按指示的方向传输数据(/R 表示将数据发送到 2C02,/W 反之)。/R、/W 或 ALE 信号同时间只会有一个有效。

2C02 编程模型

本节阐述了 2C02 IO 端口和可编程内部存储器的组织方式。 在整个文档中,端口名称就是在数字前面添加 $200(例如 $2002)。 此处未解释的内容将在后文解释。

2C02 可写寄存器

reg	bit	desc
---	---	----
0	0	左右 Name Table 选择
	1	上下 Name Table 选择
	2	读写 7 号端口后,PPU 的地址增量(0 表示增量为 1,1 表示增量为 32)
	3	精灵 Pattern Table 选择 (仅在 5 号比特位为 0 时起作用)
	4	背景 Pattern Table 选择
	5	精灵大小(0 表示高度为 8,1 表示高度为 16)
	6	EXT 总线方向标志 (0 表示输入,1 表示输出)
	7	/VBL 禁用标志 (0 表示禁用)

1	0	是否禁用色彩同步 (1 表示禁用)。禁用后,PPU 会渲染出灰阶图片
	1	屏幕左侧 8 像素是否渲染背景
	2	屏幕左侧 8 像素是否渲染精灵
	3	启用背景渲染 (1 表示启用)
	4	启用精灵渲染 (1 表示启用)
	5	R (待补充文档)
	6	G (待补充文档)
	7	B (待补充文档)

3	-	OAM (64 个属性, 每个 32 比特,访存粒度为字节) 地址控制端口。 该端口中存储的数据在访问 4 号端口后自增

4	-	OAM 数据端口,返回 OAM 中位于 3 号端口给出的地址中的内容。 访问后 3 号端口数据将自增

5	-	用于控制滚动偏移量的端口

6	-	PPU 地址控制端口(与 7 号端口搭配使用)

7	-	PPU 数据端口

2C02 可读寄存器

reg	bit	desc
---	---	----
2	5	一条扫描线上是否有 8 个以上的可渲染对象
	6	主对象与背景是否存在像素冲突
	7	vblank 标志

4	-	OAM 数据端口 (访问后 3 号端口数据将自增)

7	-	PPU 数据端口

对象属性结构 (8 比特 x 4)

ofs	bit	desc
---	---	----
0	-	上边缘 Y 坐标

1	-	图块索引。若 0 号寄存器 5 号比特位为 1(译注:即 $2000.5 为 1,精灵高度为 16 时), 则 0 号比特位控制 Pattern Table 选择

2	0	调色板序号第 1 个比特位
	1	调色板序号第 2 个比特位
	5	优先级 (0 表示高于背景; 1 表示低于背景)
	6	是否要对读取到的 Pattern Table 数据应用比特序反转(译注:水平镜像)
	7	在读取精灵图块数据时,反转扫描线号的 3 号比特位 (若精灵高度为 16 则 4 号比特位) (译注:垂直镜像)

3	-	左边缘的 X 坐标

视频信号生成

NES 为 2C02 接入了一个 21.48 MHz 的时钟信号,此信号也接入了 2A03。

在 PPU 内部,这个 21.48 MHz 信号用于给一个三级约翰逊计数器提供时钟。每级主从部分的互补输出可以生成 12 个互斥的输出相位,每个相位频率 3.58 MHz(NTSC 色同步)。 这 12 个不同的相位就是 PPU 复合视频输出颜色生成的基础。

所以,当用户设定好调色板寄存器的低 4 位后,从调色板中使用某个颜色实际上是从 12 个相位中选择一个相位给视频输出引脚使用(亮度或明度同理)。色度的其它组合(0 和 13)则是简单地连接到 1 或 0,以便生成灰度像素。

调色板条目的 4 号和 5 号比特位用于在 4 个线性直流电压偏移中选一个,此偏移会应用到像素的色度信号上(亮度或明度也一样)。

不论亮度值设置为什么值,色度值 14 和 15 总是表示黑色。

亮度值 0 与色度值 13 能够组合出一种 “比黑色还黑” 的颜色。这种超级黑的输出电压非常接近于水平或垂直同步信号的脉冲电压。正因为如此,如果游戏用到了这种颜色,在一些显示器上会看到扭曲的游戏画面(Game Genie 就是例子)。本质上是显示器把这种超级黑误认作了水平或垂直同步信号,导致时序混乱。虽然这种行为不会损坏受影响的显示器,但由于会带来画面扭曲,所以还是不要使用这种超级黑为好。

不论 4、5 号比特位的值是什么,选定的色度信号的振幅都将保持不变。也就是说,颜色的饱和度是不能调整的。

译注:

译者几乎不了解 NTSC 制式的细节及电气原理,只是按自己理解尽量翻译,如有错漏,欢迎指出。

PPU 基础时序

除三级约翰逊计数器外,其它 PPU 硬件并不直接使用 21.48 MHz 的时钟信号。取而代之的是,此信号除以 4 后得到的 5.37 MHz 新时钟信号,并以此作为 PPU 的最小计时单位。 除非另有说明,否则本文档中所有引用的 PPU 时钟周期(缩写为“cc”)都以此为准。

  • 像素渲染速率与 PPU 时钟相同,即一个时钟周期渲染一个像素。
  • 一条扫描线时长 341 个 PPU 时钟周期(或者 341/3 个 CPU 时钟周期)。
  • 一个视频帧由 262 条扫描线组成。 即每帧 341x262 个 PPU 时钟周期(除以 3 可得到 CPU 时钟周期数)。

PPU 内存读写周期

PPU 内存访问需要 2 个时钟周期,并且可以连续进行(通常在渲染期间完成),以下是访存细节。

在访存周期开始时,PPU 地址线 A8…A13 更新为目标地址。此数据一直保留到下一个访存周期。

为减少 PPU 的引脚数,PPU 地址线的低 8 位与数据总线是复用的。在访存的第一个时钟周期,A0…A7 被置于 PPU 数据总线上(AD0…AD7),并且在该周期的前半周期激活 ALE(地址锁存使能)线。 这样,就可以用 ALE 选通一个外部 8 位锁存器(74LS373)来保存低 8 位地址。

在访存的第二个时钟周期,激活 /RD(或 /WR)线并保持一个时钟周期。在此期间,数据写入到 PPU 数据总线上(或从总线传输到 PPU)。

其它 PPU 信息

  • 通过向 PPU 地址端口 $2006 写入 $3Fxx 范围的地址,可以访问内部 25 元素调色板 RAM。地址位 4 号比特位表示要选择背景图(0)还是精灵(1)的调色板;地址位 3…2 号比特位表示调色板索引(1…3)。地址位 3…0 号比特位全部为 0 时,可以访问调色板中表示透明色的元素。
  • 读取 $2002 将清除 vblank 标志(7 号比特位),并且重置 $2005/6 内部的写计数触发器。对 $2002 进行写入,不会有任何作用(译注:$2002 是只读寄存器)。
  • 2C02 /VBL 引脚的输出值是 $2002.7 与 $2000.7 的与非值。
  • 设置 $2002.5 和 $2002.6 后,它们的值会一直保留到新一帧的头 20 条扫描线(相对于 VINT 计算)。(译注:这些值会在 20 号扫描线开始时清除)
  • 在背景图渲染期间,调色板 RAM 访问将在内部直接进行(即,调色板 RAM 的地址和数据不会出现在 PPU 总线上)。另外,通过 $2006/7 访问调色板 RAM 时,虽然调色板 RAM 的地址确实会出现在 PPU 地址总线上,但是 PPU 不会激活 /RD 或 /WR 引脚。这样做很有必要,可以防止位于镜像区的 Name Table 被覆写。
  • 当有读取请求时,因为 PPU 不能立即地从 PPU 内存中读取数据,所以内部设有一个充当数据管线的缓冲区。当有读取请求时,将先返回缓冲区中的内容。然后,在 PPU 最早的那趟数据读取便车上,PPU 才会真正读取数据,然后写入到读缓冲。通过 $2007 写入 PPU 内存也同理,但目前我不确定 PPU 是否使用了同一个缓冲(译注:就是同一个)。

视频帧渲染细节

下文描述了一个视频帧里 262 条扫描线期间 PPU 的状态。扫描线的工作细节将在下一节中描述。

0…19: 从拉低 VINT 标志的那一刻开始(即生成 NMI 后),共计 20 条扫描线的时间,构成了我称之为 VINT 的时间段。 在此期间,PPU 不会访问其外部存储器(如,Name Table、Pattern Table 等)。

20: 经过 20 条扫描线后(自 VINT 标志设置后),PPU 开始扫描线渲染。 第一条扫描线是虚拟的,尽管这条线上访问其外部存储器的顺序与正常扫描线相同,但这期间并不会在屏幕上渲染任何像素,获取到的背景数据也不会用到。 在此扫描线的时钟周期偏移量 256 处,(可能)会更新水平垂直滚动计数器。 除此之外,此扫描线与其它扫描线并无不同。设立此扫描线的主要目的是启动对象渲染管线,因为对于所有要渲染的扫描线,都需要 256 个时钟周期的时间来确定哪些对象在扫描线的范围内。(译注:此扫描线即预渲染扫描线)

21…260: 虚拟扫描线渲染完毕后,PPU 开始渲染要实际显示的数据。显然,共需要 240 条扫描线才能完成。

261: 在最后一条扫描线渲染完后,PPU 将空闲 1 条扫描线的时间。 当这条扫描线结束时,会设置 VINT 标志,整个绘制扫描线的过程也会重新开始。

扫描线渲染细节

为了在屏幕上生成图像,PPU 将在一条扫描线时间内完成从 Name Table、Attribute Table 和 Pattern Table 中读取数据。本节详细介绍 PPU 在此期间的工作。

如前所述,PPU 每 2 个时钟周期访问一次外部存储。 每条扫描线有 341 个时钟周期,使得 PPU 在每条扫描线上都有足够的时间进行 170 次内存访问(PPU 全部都用掉了!)。 在第 170 次读取之后,PPU 会空闲 1 个时钟周期。记住,每个时钟周期都会渲染一个像素。

内存读取阶段 1 … 128

  1. 读取 Name Table 字节
  2. 读取 Attribute Table 字节
  3. 读取 Pattern Table #0 号位图
  4. 读取 Pattern Table #1 号位图

此过程将重复 32 次(译注:一条扫描线包含 32 个图块)。

这是 PPU 从 PPU 内存中读取相应数据来渲染背景图像的时机。此处读取的第一个背景图块实际上被用于绘制屏幕上的第 3 个图块(要在此扫描线渲染的前 2 个背景图块的数据是在上一条扫描线末尾处读取的)。

在此期间(256 个时钟周期),所有有效的屏幕像素数据都会到达 PPU 的视频输出引脚。 要确定图块的位图读取阶段开始(完整的 4 次内存读取)与该图块的位图数据的第一个像素到达视频输出引脚之间的精确延迟,可以按公式 (16-n) 计算时钟周期,其中 n 是水平滚动偏移量(0…7 像素)。 此信息与理解 “主对象碰撞” 标志的准确时序有关。

注意,PPU 每横向绘制 8 个像素就读取一次 Attribute Table 字节。 这实质上将 PPU 的颜色区域(被迫使用相同子调色板的像素区域)限制为仅 8 个水平连续像素。

也是在这段时间内,PPU 评估对象属性 RAM (OAM) 中全部 64 个对象的 “Y 坐标”,检查对象是否位于下一条扫描线的渲染范围内(将在屏幕上绘制)(这就是为什么必须将 OAM 中的 Y 坐标字段设置为对象预期要出现的扫描线减 1)。 每次评估(大概)需要 4 个时钟周期,总共 256 个时钟周期(这就是它在屏幕像素渲染期间完成的原因)。

待渲染对象评估

为计算当前扫描线(减去 21 后)与 OAM 中每个对象条目的 Y 坐标(加上 1)之间的 9 位差值,PPU 用到了一个 8 位比较器。 如果比较器产生的结果为于 0…7(当前 $2000.5 为 0 时)或 0…15(当前 $2000.5 为 1 时)区间内,则认为对象是待渲染的。

(注意这个 9 位的比较结果。当对象 Y 坐标位于 -1…-15 区间时,实际上会被解释为位于 241…255 区间。因此,在此区间的对象将永远不会被视为待渲染目标,并且超出屏幕顶部的对象也不会支持平滑滚动。)

待渲染对象的图块索引(8位)、X 坐标(8位)、及属性信息(4 位;不包含垂直反转位),加上范围比较的 4 位结果将存储在 PPU 中一块称为 “临时精灵内存”(译注:即 OAM2)的地方。 如果设置了对象的垂直反转属性位,则加载到的 4 位范围比较结果会做一次位反转。

因为待渲染对象检测是按 OAM 内顺序进行的(从 0 号条目到 63 号),所以待渲染对象会按优先级从高到低的顺序进入临时精灵内存。为了统计一条扫描线上找到的待渲染对象的数量(从 0 到 8),PPU 用到了一个 4 位 “待渲染” 计数器。此计数器也用作索引指针,指向 8 元素临时精灵内存中的位置,找到的对象的数据将存放到指针所指的位置。此计数器在待渲染对象检测阶段开始时重置,每次找到待渲染对象时递增。计数器一直递增到等于 8 时停止,此后找到的对象数据会被丢弃,并且会设置一个标志($2002 的 5 号比特位),表示下一条扫描线该丢弃一些对象了。

临时精灵内存还关联了一个额外的比特位用于指示找到了主对象(0 号对象)。 稍后将看到此比特位可用于检测主对象与背景图层像素级别的碰撞。

背景图渲染管线细节

Pattern Table 及子调色板号取到后,数据将被加载到内部锁存器中(子调色板号是将读取到的数据丢进 2 比特 4 选 1 选择器后选出来的)。

在开始读取(每 8 个 cc)新图块时,已锁存的 Pattern Table 位图数据将被加载到 16 位移位寄存器(共两个,它们每个时钟周期右移一位)的高 8 位中。同时,子调色板号也会被加载到另外一个锁存器中,此锁存器用于给两个 8 位移位寄存器(也是每个时钟周期右移一位)提供串行输入。之所以要把像素数据加载到这些移位寄存器中,是为了实现水平精细滚动。

为了合成出当前时钟周期内背景图层的 4 比特像素数据,PPU 会从每个移位寄存器中取一个比特位。具体取哪个比特位,取决于水平精细滚动的值( 0…7,适用于全部 4 个移位寄存器)。合成的像素数据将输出给多路复用器(见下文),在那里与精灵数据混合形成最终数据。

内存读取阶段 129 … 160

  1. 空读 Name Table 字节
  2. 空读 Name Table 字节
  3. 读取 Pattern Table #0 号位图 (下条扫描线用)
  4. 读取 Pattern Table #1 号位图 (下条扫描线用)

以上过程将重复 8 次。

这是 PPU 为下一条扫描线上要渲染的精灵读取 Pattern Table 数据的时机。如果下一条扫描线上可渲染对象的数量少于 8 个,则剩下的数据读取会被空读取替代,并不会停止读取。在 PPU 内部,空读取读到的内容会被直接丢弃,且被替换为表示全透明的数据(译注:$ff)。

虽然读取到的 Name Table 数据会被直接丢弃,且 Name Table 地址看似无法预测,但是该地址似乎与下一条扫描线第一个要读取的 Name Table 图块有关。此行为表明,第 256 个时钟周期很可能是 PPU 的滚动或地址计数器更新水平滚动值的周期(译注:就是。nesdev 上每条扫描线的第一个周期是空闲的,所以在第 257 个周期时会重新加载水平滚动计数器)。

值得注意的是,由于这些读取都是为下一条扫描线上的对象准备的,所以在渲染真正的第一条扫描线前需要一条额外的扫描线(译注:即预渲染扫描线),以便为第一条扫描线评估对象属性 RAM 中的条目以及加载相应数据。

至于为什么有一些无用的读取,是因为任天堂想复用背景图 Pattern Table 数据读取的硬件。

精灵 Pattern Table 读取与渲染细节

对于某个精灵来说,PPU 要去哪里读取 Pattern Table 数据,由临时精灵内存中的内容和 $2000.5 的值决定。如果 $2000.5 为 0,直接使用图块索引,而 $2000.3 则用于选择 Pattern Table。若 $2000.5 为 1,则将范围比较结果(译注:4 位,见上文)的最高位用作新图块索引的最低位,而原图块索引的最低位用于选择 Pattern Table。范围比较结果的低 3 位将被用作垂直精细偏移量。

如果临时精灵内存中的精灵需要水平镜像,则对读取到的 Pattern Table 数据应用一次比特顺序反转。

读取到的 Pattern Table 数据(2 个 1 字节),加上临时精灵内存中对应精灵的 3 个属性位(子调色板号与优先级)、X 坐标字节,将一起被复制到一个叫做 “精灵缓冲”(译注:一个 FIFO 队列)的地方(主对象找到标志也会一起复制过去)。这块内存区域,能够容纳 8 个精灵的数据。

精灵缓冲中的元素构成如下:2 个 8 位移位寄存器(读取到的 Pattern Table 数据会放到这里),1 个 3 位锁存器,以及 1 个 8 位倒数计数器(当前 x 坐标会存到这里)。

每次 PPU 渲染一个像素后(即扫描线的前 256 个周期,见上文 “内存读取阶段 1 … 128”),上述倒数计数器就会减 1。当计数器计到 0 时,移位寄存器中的 Pattern Table 数据将开始串行输出(每个时钟周期移位一次)。计到 0 之前,或者计到 0 之后的 8 个时钟周期之后,可把移位寄存器的输出视为 0(透明)。

8 个精灵的 Pattern Table 串行输出是有优先级的,且只有一个精灵的数据(包含调色板和优先级)会最终进入多路复用器(在那里,精灵的像素与背景的像素会做优先级比较并得到最终像素)。

如果第一个精灵的像素是不透明的(非 0),则它的数据(包含主对象找到标志)会第一个进入多路复用器。否则,优先级传递到精灵缓冲中的下一个精灵,颜色可见性检查也会再执行一次(主对象找到标志也会传入多路复用器,但此情形下值为“假”)。此过程一直进行到第 8 轮,此时精灵数据将被无条件传入多路复用器。需注意的是,每个时钟周期都会进行一次这一整个过程。

多路复用器

多路复用器负责两件事:检查主对象冲突,和决定背景和精灵谁的像素可以作为最终像素。

当背景图像有非透明像素与精灵的非透明像素重叠时,且当前时钟周期内进入多路复用器的主要对象找到标志为“真”,则主对象冲突发生。冲突发生后,会设置一个触发器($2002.6)保存此状态,当下一帧渲染开始时,此触发器会被重置。

决定最终像素的算法非常简单。使用精灵像素作为最终像素的条件如下(反之使用背景像素):

(OBJpri=foreground OR PFpixel=xparent) AND OBJpixel<>xparent

由于 PPU 有两套调色板,一套给精灵用,另一套给背景用,所以用哪套调色板取决于谁的像素是最终像素。

最终像素确定后,将在调色板里查找对应颜色,颜色查找结束后发生的事情可参考前文 “视频信号生成” 一节。

内存读取阶段 161 … 168

  1. 读取 Name Table 字节
  2. 读取 Attribute Table 字节
  3. 读取 Pattern Table #0 号位图 (下条扫描线用)
  4. 读取 Pattern Table #1 号位图 (下条扫描线用)

以上过程将重复 2 次。

这是 PPU 为下一条扫描线上要渲染的前两个背景图块读取数据的时机。这些读取将为内部的背景图像素管线(两个 16 位移位寄存器)初始化有效数据。剩下的图块(3…32)的数据将在接下来的扫描线中读取。

内存读取阶段 169 … 170

  1. 读取 Name Table 字节
  2. 读取 Name Table 字节

我不是很清楚这里为什么要进行内存访问。这里的 Name Table 地址,也是要渲染的第三个图块的地址(或者说,下条扫描线背景图渲染时,读取的第一个 Name Table 图块地址)。

内存读取阶段 170 之后

PPU 将休息 1 个时钟周期(或者半个访存周期),然后将重复整个像素或扫描线渲染过程。

变时长帧

20 号扫描线是唯一一条变长扫描线。奇数帧时,这条扫描线只有 340 个时钟周期(移除了最后一个时钟周期)。这导致 NTSC 色彩同步时有一定位移。

为了在 NTSC 显示器上生成颜色,需要将 3.58 MHz 的信号调制成亮度承载信号,这就是所谓 NTSC 色彩同步。由于 PPU 的视频输出基本上由方波组成,NTSC 显示器需要一整个色彩同步周期(1/3.58 MHz)才能准确识别 PPU 生成的像素的颜色。

还记得之前讲过 PPU 按 5.37 MHz 的频率渲染像素,这个频率是 NTSC 色彩同步的 1.5 倍。这导致一个有趣的现象,如果一条扫描线上某个像素的颜色与周围像素的颜色不同,则实际显示出来的颜色可能会略有差别。

所以,可能是为了稍稍修复这个问题,他们在每个奇数视频帧中添加了一个额外像素(稍稍平移了一下色彩同步的相位),这就改变了显示器识别有色像素的方式。这就是玩一些背景图细节特别丰富的游戏时,背景看起来有点闪烁的原因。如果滚动屏幕,可以注意到有些像素似乎消失了。如果不在奇数帧中移除一个时钟周期,即使静止的图像也会出现这种像素消失的现象。

不论是否开启相位平移,某些特定的滚动频率也会暴露 NTSC PPU 颜色问题。塞尔达 2 的某些地牢的背景就是比较好的例子,可以看到这种现象。

MMC3 扫描线计数器

众所周知,MMC3 依靠 PPU 地址线 A13 进行扫描线计数(这就是什么通过写入 $2006 来翻转 A13 可以手动触发 IRQ)。但鲜为人知的是,一条扫描线内,理论上 A13 应该要翻转多少次(如果你已经仔细阅读了上文,你应该已经有答案了)。

A13 很可能可用于 IRQ 计数,因为这条地址线需要连接到 MMC 用于内存换页(bankswitching)(换句话说,可以节省 MMC3 一个引脚)。他们也许已经用到了这种计数方式,因为一条扫描线上 A13 重复(0 -> 1)恰好 42 次,而一条扫描线上 CPU 的时钟周期数不是整数(113.67)。我猜任天堂是想为实现一些特别的图像效果提供一种 “简单易用” 的机制,这样程序员们就不用费尽心思去数 CPU 时钟周期再配置 IRQ 计数器(不提供与 CPU 时钟同等时钟精度的 IRQ 计数器是真的很不方便)。

在图像渲染期间,不管 PPU 寄存器中写入的是何值,A13 的行为总是可预测的(如果你理解 PPU 的寻址机制,你就会明白,图像渲染期间,A13 是唯一一条有固定行为的地址线)。

PPU 像素优先级怪事

精灵之间及精灵与背景图层之间的数据是有优先级之分的。这会导致一些奇怪的副作用。比如,设想为一个低优先级的精灵却有着前景像素的优先级,一个高优先级的精灵却有着背景像素的优先级,以及一个全是不透明像素的背景图。

理论上,背景图应该介于拥有背景像素优先级和前景像素优先级的精灵之间。也就是说,背景图的像素应该覆盖拥有背景像素优先级的精灵所产生的像素(不管精灵之间的优先级),而拥有前景像素优先级的精灵产生的像素又应该覆盖背景图像素。

然而,由 PPU 的渲染方式可知,精灵的优先级评估是先进行的,因此背景图的像素总是会赢。

此行为的最佳示例是洛克人 2(Mega Man 2)。进入 “空气侠”(Airman)关卡。首先,往能量条在的位置跳,可以看到主角的精灵图层比能量条有更高的优先级。然后,去到这关的后面部分,找到云朵可以包住能量条的地方。现在可以看到是渲染顺序是 主角 -> 能量条 -> 云朵。注意看此时如果往能量条里面跳会发生什么,你会发现主角所在位置的云朵覆盖了能量条。

PPU 滚动与寻址概述

下方的图表是一个三行两列表格,展示了地址、数据是如何进入、离开 PPU 内的计数器与寄存器的。

图表最上面的行表示进入 PPU 的数据,以及数据是如何写入 PPU 内部寄存器的。左列表示数据是在什么情况下(通过对 PPU 寄存器编程,还是通过从 VRAM 数据总线读取)进入 PPU 的,而右列表示写入的数据比特位与寄存器比特位之间的映射关系(译注:这里要结合第二行的内容来看。例如,$2005 的第二次写入,这行表达的意思是比特位 2…0 将写入 FV,而比特位 7…3 则写入 VT。FV 共 3 比特,右边为最低位;VT 共 5 比特,右边为最低位)。数字 0…7 表示写入数据的比特位(若图表中没有出现某个数字,则代表没有用到相应比特位),“-” 则表示写入常数 0。

中间行表示与滚动和寻址直接相关的计数器与锁存器。该行右列的每个方框的上半部分表示锁存器、寄存器。如果右列的方框有下半部分,则表示有对应的计数器。计数器需要加载数据时,就会从对应的锁存器、寄存器加载。计数器的行为将在后文描述。

最后一行表示 PPU 内部寄存器、计数器是如何影响 PPU 地址线的。该行的左列与第一行左列意义相同(译注:即在什么情形下的地址线)。不同的是,右列中出现的数字(此时用的是 16 进制)现在表示 PPU 地址线编号。若某些地址线的编号没有出现在右列中,则它们的值会在图表下方的备注中说明,而所引用的备注编号会出现在左列的括号内。若存在计数器,则地址线的内容来自计数器而非寄存器。

译注:

以下图表对于理解寄存器读写、计数器更新、地址生成有巨大帮助,值得好好品读。

图表从上到下表达的意思是,哪些情况有数据进入 PPU,进入的数据的每个比特是写入到哪些寄存器去的,地址又是从哪些计数器、寄存器的哪些比特按什么顺序生成的,以及生成地址用于什么情形。

寄存器/计数器命名

NT: Name Table

AT: Attribute/color Table

PT: Pattern Table

FV: 垂直精细偏移锁存器/计数器

FH: 水平精细偏移锁存器/计数器

VT: 垂直图块索引锁存器/计数器

HT: 水平图块索引锁存器/计数器

V: 垂直 Name Table 选择锁存器/计数器

H: 水平 Name Table 选择锁存器/计数器

S: 背景 Pattern Table 选择锁存器

PAR: 地址寄存器 (名字来自专利文档)

AR: 图块属性数据 (子调色板号) 锁存器

/1: 自读取 $2002 后第一次写入 $2005 或 $2006

/2: 自读取 $2002 后第二次写入 $2005 或 $2006

╔═══════════════╤═══════════════════════════════════════════════╗
║2000           │      1  0                     4               ║
║2005/1         │                   76543  210                  ║
║2005/2         │ 210        76543                              ║
║2006/1         │ -54  3  2  10                                 ║
║2006/2         │              765  43210                       ║
║NT read        │                                  76543210     ║
║AT read (4)    │                                            10 ║
╟───────────────┼───────────────────────────────────────────────╢
║               │╔═══╗╔═╗╔═╗╔═════╗╔═════╗╔═══╗╔═╗╔════════╗╔══╗║
║PPU registers  │║ FV║║V║║H║║   VT║║   HT║║ FH║║S║║     PAR║║AR║║
║PPU counters   │╟───╢╟─╢╟─╢╟─────╢╟─────╢╚═══╝╚═╝╚════════╝╚══╝║
║               │╚═══╝╚═╝╚═╝╚═════╝╚═════╝                      ║
╟───────────────┼───────────────────────────────────────────────╢
║2007 access    │  DC  B  A  98765  43210                       ║
║NT read (1)    │      B  A  98765  43210                       ║
║AT read (1,2,4)│      B  A  543c   210b                        ║
║PT read (3)    │ 210                           C  BA987654     ║
╚═══════════════╧═══════════════════════════════════════════════╝

说明:

  1. 地址线 DC = 10
  2. 地址线 9876 = 1111.
  3. 地址线 D = 0。 地址线 3 的值与 Pattern Table 的读取有关。(译注:第二次读取时为 1,即第一次读取时的地址加 8)
  4. PPU 内有一个 2 比特 4 元素移位寄存器(译注:也可看作 2 个 4 比特移位寄存器),在 Attribute Table 的读取阶段,用于生成 2 比特的子调色板号。为了在图中表示此数据是如何位移的,字母 c…a 表示要应用到属性数据的右移位移量(a 必为 0)(译注:这里比较难以理解,要对照第二行看。就是 VT 与 HT 的第 2 位,分别形成了位移量的第 3 位和第 2 位。由于第 1 位为 0,所以位移量是 2 的倍数,即一次位移 2 个比特。将读取到的 Attribute Table 数据,放入移位寄存器,按位移量右移后,头两位即是子调色板号)。这就是为什么图中 “AT read” 一行中只有 b 和 c 。

计数器行为

在渲染期间,或者通过 $2007 访问 VRAM 时,计数器们(FV、V、H、VT 和 HT)会递增。它们递增的方式由 PPU 访问 VRAM 的方式决定。

通过 $2007 访问 VRAM

如果 VRAM 地址递增比特位($2000.2)为 0(即增量为 1),所有的计数器按 HT -> VT -> H -> V -> FV 的顺序组成菊花链(daisy-chain),每个计数器的进位即是下级(右边的)计数器的时钟。这样的结果相当于这 5 个计数器组成一个 15 位的计数器。访问 $2007 将给 HT 计数器一个时钟信号,使其递增。

如果 VRAM 地址递增比特位($2000.2)为 1(即增量为 32),与上述行为相比,唯一的差别是给 VT 计数器时钟信号,而不再是 HT 计数器。

渲染期间访问 VRAM

根据 Name Table 的组织方式,计数器行为与访问 $2007 时有所不同。在渲染期间($2001.3 或 $2001.4 为 1),且扫描线编号比特位于 20…260 (相对于 VINT) 区间时,PPU 建立了两个计数器(为了读取 Name Table、Attribute Table 和 Pattern Table 数据),它们的行为将在下文描述。

第一个计数器,即水平滚动偏移,共 6 位,是 HT -> H 构成的菊花链。HT 每 8 个像素增加一次(或者每 8/3 个 CPU 时钟周期)。

第二个计数器,即垂直滚动偏移,共 9 位,是 FV -> VT -> V 构成的菊花链。FV 由 PPU 水平消隐(horizontal blanking)脉冲驱动,因此它每条扫描线增加一次。VT 相当于一个 30 分频的计数器,只在 29 增加到 30 时才进位(同时,计数器也清零)。这样做是防止误把 Attribute Table 中的内容用作图块索引(译注:垂直方向上只有 30 个图块)。

计数器加载/更新

5 个计数器加载对应的锁存器的值的情况共有两种。第一种是在写入 $2006/2 后(译注:$2006 的第二次写入)。第二种,是在 20 号扫描线的开始处,当 PPU 第一次为当前帧渲染数据时(若使用 $2001.3 和 $2001.4 禁用渲染后,则不会发生计数器更新)。

还有一种只更新 H 和 HT 计数器的情形,就是在扫描线的水平消隐(horizontal blanking)结束的时候。同样,必须打开图像渲染才会发生更新。

帧中分屏滚动实现

虽然在帧中只通用 $2006 把 FV 更新为想要的值是不可能的,但是交替写入 $2005 和 $2006 却是可行的。在重置(读 $2002) $2005、$2006 的写计数触发器后,按以下顺序对寄存器进行写入,就可以把全部计数器(包括 FV)更新为任何想要的值。注意,因为寄存器中的值会被覆写多次,所以下面只列出了有关联的更改。

reg 	update
-----	------
2006:	Name Table 选择位 (V, H)
2005:	FV 和 VT 的 3、4 号比特位
2005:	FH。 立即生效
2006:	HT 和 VT 的 0、1、2 号比特位

在 $2006 的最后一次写入时,此前写入的数据会加载到计数器中。

猜你喜欢

转载自blog.csdn.net/ZML086/article/details/129629051