【译制】红白机NTSC制式PPU——Ricoh 2C02技术参考(第二部分)
接《【译制】红白机NTSC制式PPU——Ricoh 2C02技术参考(第一部分)》

扫描线渲染细节
PPU在一条可见扫描线中会从VRAM的命名表、属性表和样式表中读取数据,用来产生屏幕上的图像。这一部分将详细阐述PPU在一条可见扫描线周期内的行为。
正如前文所述,每一次VRAM访问花费两个PPU时钟周期。而每条扫描线长度为341个PPU时钟周期,在此期间PPU能完成170次VRAM访问(并且用尽这170次VRAM访问)。在从VRAM取回170次数据后,PPU会有一个时钟周期的空闲期。别忘了PPU在每一个时钟周期都会渲染一个像素。

第1-128次VRAM访问的每一次
1. 读一个字节的命名表数据
2. 读一个字节的属性表数据
3. 读对应样式表位图的0号字节
4. 读对应样式表位图的1号字节
这一过程会重复32次(每一条扫描线上有32个图块,每个图块每一行有8个像素,而NES/FC的分辨率为256*240)。
在此期间PPU会从VRAM中取回对应的数据并渲染背景。此扫描线中第一个被取回的背景图块被作为屏幕上该行第三个图块显示(屏幕上该行的前两个背景图块的数据是在上一条扫描线的最后被取回的)。
屏幕上所有显示像素的数据都是在这256个PPU时钟周期(128个VRAM访问周期)里被输出到PPU视频输出引脚。从图块位图(一行,8位即1个字节)开始被取回(即上述四次完整内存访问的开始)到此图块位图数据的第一个像素被输出到视频输出引脚,一共经历了(16-n)个PPU时钟周期,其中n代表屏幕水平卷动偏移的低3位(译者注:PPU屏幕水平和垂直卷动偏移各8位,单位为像素,即卷动范围为0-255,而低3位表示的卷动范围为0-7,因为一个图块有水平方向有8个像素,因而0-7表示“图块内的平滑卷动”;而无图块内水平卷动偏移时所经历的16个时钟周期中,2*4个时钟周期用于访问四次VRAM,另外8个用于下文所述的背景渲染流水线移位寄存器的移位操作,这就是(16-n)的由来,16个时钟周期,恰好有16个像素被输出,为2个图块的宽度)。此信息对于理解基于“0号精灵碰撞”标志的扫描线精确计时操作至关重要。
我们注意到PPU每次取回的属性表数据作用于其绘制的8个连续的水平像素(一个图块的一行)。这制约了PPU的同屏发色数(也就是说此区域的像素只能共用1个只有3种颜色的用户调色板项,更准确的说,一个由2*2个图块(Tile)构成的区块(Block)需要共用一个用户调色板项),也就是说8个连续的水平像素位于同一个“颜色区域”中(属性表为该区域选择了某一个用户调色板)。
在此期间PPU还会将下一条扫描线的纵坐标(Y)与OAM中64个精灵的表项逐一比较,来判断是否有精灵需要在下一条扫描线上绘制(优先于/覆盖背景像素),即判断是否有精灵落入了下一条可见扫描线的范围之中(这正是OAM表项中的精灵纵坐标是(其出现的扫描线-1)的原因)。每一次比较耗费四个PPU时钟周期(据猜测),因此64个精灵花费256个时钟周期(这恰好与屏幕上像素的渲染过程同步进行)。

画面内精灵的判定
PPU内部有一个用来计算(当前扫描线-21)和OAM每个精灵表项的(纵坐标-1)差值的8位比较器,结果是一个9位二进制数。如果比较得到的差值在0-7($2000 寄存器的第五位等于0,使用矮精灵)或0-15($2000 寄存器的第五位等于1,使用高精灵)的范围内,则表明精灵在被比较的扫描线内。
(值得注意的是比较结果是一个9位二进制数。也就是说如果将精灵扫描线坐标设置为(-1)-(-15),比较器会将其视为241-255处理,这是8位坐标值和9位结果的符号位的差异造成的。在这种情况下,精灵将不在任何一条可见扫描线的范围之内,也就表明我们无法实现精灵从屏幕顶端平滑地向上卷动出屏幕的效果)
PPU内部存放的该扫描线内精灵的OAM表项中的8位的图块索引、8位的X坐标、4位的属性信息,以及比较得到差值的低四位(精灵内垂直偏移)共同构成了“精灵/OAM临时内存” (不考虑垂直翻转的情况)。如果该精灵在其OAM表项中设置了垂直翻转,先前得到的4位的比较差值会被做逻辑非运算。
因为以上精灵范围的判定过程是顺序遍历整个OAM内存的(从第0个到第63个精灵),所以OAM临时内存会按照精灵优先级从高到低的顺序追加更新(若需要)。PPU中一个4位的“扫描线上精灵”计数器用于记录扫描线上发现的精灵个数(0-8个),同时也被用作OAM临时内存的索引指针,来将新发现的精灵数据放入最多容纳8个元素(精灵)的OAM临时内存。在精灵判定阶段的开始,该计数器被清零,并在每发现一个扫描线上的精灵后自增1。在该计数器的值为8之前,该过程持续进行,此后发现的精灵将被丢弃,同时 $2002 寄存器的第5位被置位(精灵溢出),以指示下一条扫描线上将有精灵不被显示。
OAM临时内存还带有一个额外的比特位,用来指示相应扫描线上是否存在0号精灵(优先级最高的精灵)。这一信息将被用于后续的0号精灵与非0背景像素碰撞的判断。

背景渲染流水线的细节
在样式表数据和用户调色板选择数据(使用一个两位信号控制的四选一选择器从取回的一个字节属性表数据中选出两位作为用户调色板选择数据,如何选择与当前图块在2*2图块构成的区块中的位置有关)被取回后,它们将被载入PPU内部的锁存器。
(译者注:一个图块某一行8个像素的用户调色板的颜色选择信号由被命名表数据选出的样式表数据连续两个字节拼凑而成,单个像素的用户调色板颜色选择信号有两位,通过一个四选一选择器选中上述“用户调色板选择数据”选出的某个用户调色板中四种系统调色板索引的某一个,作为硬编码的系统调色板的选择信号(8位,实际6位有效),该信号编码了最终的颜色,详见“视频信号的产生”)。
在新图块数据取回阶段(8个PPU时钟周期)的开始,上一次取回并锁存的2个字节图块样式表位图被分别载入2个16位移位寄存器的高八位(这两个移位寄存器会在每个时钟周期右移一次)。在此期间,2位用户调色板选择数据也会被传送至另一个锁存器(这个2位的锁存器的2个比特位将分别作为另外2个在每个时钟周期右移一次的8位右移寄存器的串行输入)。4位的像素数据(2位用户调色板选择数据和2位用户调色板颜色选择数据,即唯一的4位用户调色板颜色索引)被拆分灌入这4个额外的移位寄存器,目的是在PPU固定的访问VRAM按字节取回图块数据的工作方式下实现平滑的水平卷动。
最终将从每个移位寄存器中各选出一个比特位,形成当前时钟周期有效的4位背景像素值(用户调色板的颜色索引)。从4个移位寄存器中选出比特位时使用的偏移/索引是基于用户设置的水平屏幕卷动偏移产生的(正是8位水平偏移的低3为,表示图块内偏移,此索引范围为0-7,即选择4个移位寄存器的0-7位)。选出的4位像素数据会被送入多路复用器(下文描述),与精灵数据进行混合(选择)后输出。

第129-160次VRAM访问的每一次
1. 读一个字节的废弃命名表数据
2. 读一个字节的废弃属性表数据
3. 读下一条扫描线上相应精灵(如果有,对应OAM临时内存索引0-7)样式表位图的0号字节
4. 读下一条扫描线上相应精灵(如果有,对应OAM临时内存索引0-7)样式表位图的1号字节
以上过程重复8次(因为一条扫描线上最多能显示8个精灵)。
这个阶段留给PPU取回下一条扫描线上需要显示精灵的样式表数据。当(“扫描线上精灵”计数器所指示的)下一条扫描线上不足8个精灵时,取回的样式表数据将来自“哑样式表”而不是实际有用的样式表。此时在PPU内部,取回的“哑数据”将会被丢弃,并使用完全透明的样式表位图代替。
虽然取回的命名表数据最终被丢弃,且使用到的命名表地址某种程度上是不可预知的,但是这个命名表地址似乎与下一条扫描线上的第一个图块的命名表有关。这似乎暗示着在扫描线第256个时钟周期(第128次VRAM访问结束时),PPU的卷动/地址计数器的值将被自动重装载为PPU水平屏幕卷动寄存器的值(译者注:也就是说一条扫描线中的水平卷动偏移保持一致,依照水平卷动计数器的值来设置水平方向的偏移,在此期间若CPU执行的程序改变了PPU水平屏幕卷动寄存器的值,该水平卷动的变化将反映在下一条扫描线上)。
同样值得关注的是,因为下一条扫描线的精灵样式表需要在本条扫描线取回,所以第一条可见扫描线前的“哑扫描线”是必要的,只有这样,第一条可见扫描线上的精灵才能被判定,OAM临时内存的条目才可以被初始化,第一条扫描线上精灵的样式位图信息才可以被取出。
至于为什么要在取回精灵样式前取回废弃的命名表和属性表数据,大概是因为任天堂想在精灵渲染的硬件上复用背景样式表取回的硬件才选择这么做的吧。

精灵样式的取出和渲染细节
PPU从VRAM的何处取回单个精灵的样式表数据受到OAM临时内存表项和 $2000 寄存器的第五位(精灵高度选择)的控制。如果 $2000 寄存器的第五位为0,PPU使用一般的图块索引,使用到的样式表由$2000 寄存器的第三位选择。而如果 $2000 寄存器的第五位为1(使用16线高精灵),由地址计算电路产生的样式表内偏移的高位比特将被作为图块索引号的低位比特,而OAM中存放的精灵图块索引的最低位将被作为VRAM低端两个样式表(也称图案表)的选择位。地址计算电路产生VRAM地址的最低三位则永远会被作为精灵样式位图内部的垂直偏移量。
若相应的OAM临时内存表项指示当前精灵设置了水平翻转,则取回的一字节样式表位图数据的比特位将会依次发生交换以实现精灵的水平翻转。
取回的两个字节的样式表数据,外加OAM中的3位精灵属性信息(两位调色板选择位和一位精灵相对于背景像素的优先级位)和一个字节的精灵横坐标被载入PPU中一个叫作“精灵缓冲内存”的部分(0号精灵指示位也一同被拷贝)。这一片存储区同样能存放8个精灵的数据。
精灵缓冲内存的每一个元素都是由以下部分组成的:2个8位移位寄存器(取回的两个字节样式位图数据将分别载入进两个移位寄存器中,并在合适的时间里移位并移出)、1个3位锁存器(存放精灵的用户调色板选择和相对于背景的优先级信息)和1个8位减1计数器(精灵的横坐标被载入到这里)。
上述的8位计数器会在PPU每渲染一个像素时自减1(在扫描线的前256个时钟周期内进行,详见上文的“第1-128次VRAM访问的每一次”)。当计数器的值等于0时,上面提到的两个存放样式表数据的移位寄存器会开始移位,并在每个时钟周期移位一次。在计数器自减至0之前及8次移位(有效数据被全部移出)之后,移位寄存器的串行输出端会始终输出0(代表透明)。
8个精灵样式位图的串行移位输出是需要经过优先级裁定的,最终只会有一条串行移位输出(连同该精灵的用户调色板选择和相对于背景的优先级位)被选出并输出到多路复用器(在这里进行精灵和背景像素的优先级裁定)。
如果从精灵缓冲内存第一个条目(元素)移出的像素(样式表位图比特)是非透明(非0)的,则其中的数据(包括0号精灵标志)则会被最优先地送入多路复用器并停止精灵优先级的裁决过程。否则优先权将被交给精灵缓冲内存的下一个元素,重新测试其输出的精灵像素透明与否(而当前的0号精灵标志将会被一直传递下去,当前是“无0号精灵”的状态)。若前7个精灵均输出透明像素,则第8个精灵的数据将被无条件地传递给多路复用器。注意,这个过程在每个时钟周期都会发生(硬件会以电信号在逻辑电路中传播的速度迅速裁决出优先级,此时间短于一个时钟周期)。

多路复用器的行为
多路复用器有两件事要做:判断0号精灵与非透明(非0)背景是否发生了碰撞,并在裁决背景像素和最高优先级的非透明精灵像素的优先级后将优先级高者的像素数据传递给用户调色板。
“0号精灵碰撞”事件在非透明背景像素和非透明精灵像素在多路复用器相遇,且当前时钟周期精灵优先级裁定电路传给多路复用器的0号精灵标志被置位的条件下产生。这将导致 $2002 寄存器的第六位对应的触发器被置位,这个标志将一直保持,并在下一帧开始渲染时被清除。
选出需要传递给用户调色板的像素的过程非常简单。使用精灵数据而不是背景数据来产生最终输出到屏幕上的像素的充要条件是:
(精灵的优先级比背景高 或 背景像素是透明的) 且 精灵像素不是透明的
PPU有两个用户调色板:一个精灵调色板和一个背景调色板。因此,最终的调色板索引数据(即上文所谓的“像素”)被传递给哪个调色板以索引得到硬编码系统调色板的颜色,取决于最终通过多路选择器的是精灵像素还是背景像素。
再经过用户调色板的索引后,PPU便会以上文“视频信号的产生”一节中描述的过程输出视频信号。

第161-168次VRAM访问的每一次
1. 读一个字节的命名表数据(对应下一条扫描线上的连续8个像素)
2. 读一个字节的属性表数据(对应下一条扫描线上的连续8个像素)
3. 读对应样式表位图的0号字节
4. 读对应样式表位图的1号字节
这一过程将重复2次。
在此期间,PPU从VRAM取回下一条扫描线上第1和第2个背景图块数据。PPU会用取回的数据有效位图数据初始化内部背景像素流水线(准确地说是那2个16位的移位寄存器)。下一条扫描线上剩下的背景图块(第3至32个背景图块)将会在下一条扫描线的开始被取回并渲染。

第169-170次VRAM访问的每一次
1. 读一个字节的命名表数据
2. 读一个字节的命名表数据
我不太清楚这两次VRAM访问的目的何在。这里两次显存中命名表的访问所使用到的地址,均指向屏幕上要渲染的第3个图块(即下一条扫描时间中渲染的第1个图块或下一条扫描线上实际显示的第3个图块的命名表地址)。

第170次VRAM访问之后
PPU在重复以上步骤进行下一条扫描线的计时之前会空闲1个时钟周期(即半个VRAM访问周期)。

(未完待续)