NES基本原理(四)滚屏渲染

简介: 滚屏渲染

首发公众号:Rand_cs

滚屏渲染(基础部分)

本文继续 PPU 的话题来讲述滚屏,从我们小时候玩游戏的经验知道 NES 是支持像素级滚屏的,这在当时那个年代是个创举,这也是为什么 FC/NES 那么火热的原因之一

那 PPU 是如何支持像素级的滚屏?这就要先来看看 PPU 的一些硬件部分。

内存映射寄存器

首先来看看映射到 CPU 地址空间的一些寄存器,也是 CPU 与 PPU 通信的端口。从本文开始十六进制数我还是用 0x 表示,用 \$ 有太多的格式问题,前文我每个 \$ 前面加上了 \ 转义,然后使用 mdnice 导格式始终有问题,所以干脆直接就使用 0x 算了,大家也还熟悉些,只是如果要对 nes 汇编开发的话十六进制数得使用 \$,不过估计几乎也没人干这事。不多说了,来看 PPU 的寄存器:

PPUCTRL

控制寄存器(0x2000):

  • bit0-1,选取 NameTable,00-0x2000, 01-0x2400, 10-0x2800, 11-0x2C00
  • bit2,有个专门的寄存器记录访问 VRAM 时的地址,每次访问现存 VRAM,这个值都要增长,0:增长 1 即水平移动,1:增长 32 即纵向移动
  • bit3,精灵使用哪个 PatternTable,0:0x0000, 1:0x1000
  • bit4,背景使用哪个 PatternTable,0:0x0000, 1:0x1000
  • bit5,精灵大小,0:$8\times8$,1:$8\times16$
  • bit7,在 V_Blank 开始的时候产生 NMI

基本上前文都说过,详细的情况后面也还会说到。

PPUMASK

mask 都知道啥意思,要屏蔽什么东西,所以这个寄存器就是来控制哪些渲染哪些不渲染:

  • bit0,0:显示正常颜色,1:显示黑白图像
  • bit1,1:渲染背景最左侧 8 列像素,0:不渲染
  • bit2,当精灵位于屏幕最左侧时,1:渲染精灵左侧 8 列像素,0:不渲染
  • bit3,1:渲染背景,0:不渲染
  • bit4,1:渲染精灵,0:不渲染

PPUSTATUS

状态寄存器,主要记录 3 个状态:

bit5:精灵是否溢出,精灵溢出是只当前扫描行有没有超过 8 个精灵,超过则该位置 1,表溢出

bit6:sprite 0 hit,当 sprite 0 的不透明像素与背景不透明像素重叠时该位置 1,这个主要用于屏幕分割,就是制造那大片级的效果

bit7:是否处于 V_Blank,是的话置 1

OAMADDR & OAMDATA & DMA

看到像是 addr 和 data 寄存器,大概就知道是是用 addr 来选取一个地址,然后从 data 寄存器对该地址上的内容进行读写。的确如此,这两个端口就是用来操作 OAM 这片空间的。这里要注意因为地址总线有 16 位,而数据只有 8 位,所以每次对地址相关信息读写时要连续操作 2 次

However,一般不这样使用,因为每次传输数据经过 CPU,太慢,通常是 OAMADDR、OAMDMA(0x4014) 两个端口配合使用。DMA 大家应该很熟悉,这里一样的道理,只要将 CPU 地址空间中的精灵信息首地址(通常是 0x200)的高低 8 位 分别填入 ADDR 和 DMA 中,DMA 就会自动将 CPU 地址空间中的精灵信息加载到 OAM,不用每次经过 CPU 中转速度大大加快

而且大多数时间都不应该更改 OAM 里面的内容,通常情况下 OAM 里面的内容只应在 V_Blank 期间更改,因为其他时间段都处于渲染阶段,比如说当前帧渲染刚开始时精灵在地上,当前帧渲染要结束时精灵跑天上去了,这明显不合理是吧。

Scroll

滚屏寄存器,只写,连续写两次来决定哪一个像素位于屏幕左上角。举个例子直观了解,这是马里奥的两个 NameTable:

上图一个小格就是 8 个像素,如果我向 Scroll 先后写入 24,16,则会从下图所示位置开始渲染:

scroll1

ADDRESS&DATA

PPUADDR 寄存器地址 0x2006,PPUDATA 寄存器地址 0x2007,同样的还是 addr/data 的方式读写内存。只不过这里的内存是 PPU 地址空间的内存,也就是说可以通过这两个寄存器访存 PPU RAM,PatternTable,Pallete,其他的没啥说的,基本一样。

内部寄存器

这一部分讲述 PPU 内部不可见的寄存器,前面那几个 内存映射的寄存器 我们是可以操作访问的,但是下面这几个寄存器是不能直接访问的,来看:

v

Currrent VRAM address,15 bit,即 v 里面存放的是当前访问要 VRAM 的地址

t

Temporary VRAM address,15 bit,临时存放要访问的 VRAM 地址,或者存放滚屏地址,关于这后面详细解释。

x

fine X Scroll,3bit 存放滚屏时 x 轴方向的细致地址,关于滚屏后面详细说明。

w

toggle,1bit,一个开关,因为地址有 16bit,数据总线只有 8bit,所以写地址需要连续写两次,因此需要一个 toggle 来记录是第一次写还是第二次写。

滚屏简析

滚屏前面在 Scroll 寄存器的地方说过一点,这里稍微详细地解释一下,也是解释内存映射寄存器和其内部的寄存器的关系。

前面我们说过向 Scroll 寄存器连续写两次(X 地址和 Y 地址)就可以设定哪一个 NameTable 的哪一个像素位于屏幕的左上角。虽然 NameTable 实际上是存放着一屏 tile 的索引,但是我们从逻辑上可以看作就是一屏 tile。

其中设定哪一个 NameTable 是通过写 0x2000 PPUCTRL 寄存器的低 2bit

而 X 地址可以分为 coarse X 和 fine X,简单的翻一下就是粗糙的 X 地址和细致的 X 地址(有啥好的翻译??),Y 地址同样也是如此,可以分为 coarse Y 和 fine Y,什么意思呢,直接来看图:

还是很好理解吧,coarse 表示某个 tile 的坐标,fine 表示这个 tile 内某个像素的精确位置

而这与 t v x t 啥关系呢?

如果 t,v 表示滚屏地址的话,它们有如下的结构:

图示很清晰不再解释,只是这里少了 fine X scroll,fine X 单独存放在 x 寄存器里面。

向 0x2000 写的数据低 2 bit 写进 t 的相应位置,表示使用哪个 NameTable

当 w = 0 即第一次向 Scroll 寄存器写时,X 地址的高 5 位写进 t 的低 5 位,数据低 3 位写进 x,写后将 w 置 1 表示下一次写将是第二次写。

当 w = 1 即第二次向 Scroll 寄存器写时,Y 地址直接写进 t 的相应位置,写后将 w 清 0.

上述操作就可以设置 某个 NameTable 的某像素位于屏幕左上角,一般情况是在 V_Blank 期间也就是 CPU 处理 NMI 的时候设置,每次使其加 1,就可以实现横向滚屏

从编程人员的角度来说,这就是滚屏,再来总结一番:向 0x2000 低 2 位写入 NameTable,连续向 0x2005 写两次 X、Y 选取某个像素位于左上角,每次 V_Blank 期间设置一次就可以实现滚屏

这只是一般情况下的简单滚屏方式,有一些高级玩法屏幕分割技术后面再说,另外这也只是从编程人员的角度理解,硬件怎么做的渲染部分详述。

硬件抠门部分

前面说过 NES 很多抠门的地方,不过都是软件部分,这里来说说硬件部分抠门的部分。

向 0x2005 写入数据实际上就是写入 t,向 0x2006 写入地址实际上也是写入 t,只不过最后再从 t 复制到 v。地址 16 位同样需要写 2 次,所以需要一个 toggle 来记录到底是第几次写,而这个 toggle 也是共用上面提到的 w

也就是说可以认为向 0x2005 和 0x2006 写入数据时,实际上共用两个寄存器 t 和 w,下面详细说说:

向 0x2006 第一次写入高地址时,只有数据的低 6 位有效,t 的最高位是清 0 的,另外 w 置 1。

向 0x2006 第二次写入低地址,数据的 8 位全都有效,将其写到 t 的低 8 位,写完立即将 t 复制一份 到 v,这就是写 0x2005 和 写 0x2006 的区别。写完 0x2005 后不会从 t 复制到 v,而写 0x2006 需要。另外写完之后都是需要将 w 清 0 的

另外不论是读还是写 VRAM,都会使得 v 中的值自动加 1 或 32,这由 PPUCTRL 寄存器 bit2 控制,加 1 表示横向下一个 tile,加 32 表示纵向下一个 tile

这部分的最后好好捋捋两个地址,一是向 0x2005 写入的滚屏地址,二是向 0x2006 写入的普通地址。

普通地址就没什么说的,它是 PPU 地址空间的地址,但是 PPU 地址空间有 64KB,但是有用的只有 8KB,所以其实 14 位就足够了,因此第一次写 0x2006 高位字节时只有 低 6 位有效

而向写 0x2005 写的滚屏地址,严格意义上来说不能算是地址,t 与 x 加起来算是某个像素的位置。

明显的看这个图,怎么都不想一个地址的格式,一个地址也不可能这么分割。但是,t 的低 12 位,也就是 NNYYYYYXXXXX 确实可以看作一个地址。

12bit 可以索引 4KB,刚好是 4 个 NameTable & AttributeTable 的大小,而地址划分的格式刚好就是 NNYYYYYXXXXX,NN 选取 NameTable,YYYYY 表示 tile 的 Y 坐标,XXXXX 表示 tile X 坐标

当然这里的 12 位地址不是绝对地址,而是相对于 0x2000 的相对地址

渲染

渲染就分两部分,背景渲染和精灵渲染,以像素为单位渲染。PPU 的 “每个时钟周期” 获取背景的颜色信息和精灵的颜色信息,两者优先级竞争决定输出哪个

很粗浅的解释,要弄清楚还是得来了解 PPU 内部的一些硬件:

背景

  • 首先是前面提到过的 VRAM address,temporary VRAM address,Scroll,toggle

  • 2 个 16bit 移位寄存器,后面我称作 pattern_shifter,这 2 个寄存器存放将要渲染的 2 个 tile,这里要清楚 tile 是高低位分开存放的,所以一个寄存器存放 2 个 tile 的高位,一个寄存器存放 2 个 tile 的低位。一个 tile 图案是 64 个像素,128 位信息,因为是一行一行的渲染,只用存放一行的 tile 信息,所以 shifter 16 位就足够了

  • 2 个 8bit 移位寄存器,后面我称作 attribute_shifter,这 2 个寄存器存放相应的 Atrribute 信息。

下面来详细说明这些硬件在渲染期间的作用:

前面说过,渲染的方式是一个像素一个像素的渲染,且走的是 Z 字型。对于一般的 NTSC 系统来说有 262 条 Scanline,其中有 240 条 Scanline 可见,每条 Scanline 持续 341 个时钟周期,这期间就是不停的取数据然后输出渲染。这里我们先不说明每条 Scanline,每个时钟周期干什么,先来了解背景总体的渲染过程。

渲染一个背景像素需要 4bit 的颜色信息,渲染过程其实就是取得这 4bit 颜色信息。如何取得呢?

PPU 会从 v 中获取该像素所在的 tile 索引的地址信息,将这个 tile 取过来分高低位存放到 pattern_shifter 寄存器当中。然后取该 tile 的 attribute 信息分高低位存放到 attribute_shifter 寄存器当中,如此一个像素的 4 bit 颜色信息就齐了

在每条 Scanline 的前 256 个周期,每个周期 shifter 寄存器左移 1 位,每 8 个周期就加载下一个 tile 信息到 shifter 寄存器,之后根据 fine_x 选出当前要渲染的像素,举个例子说明:

上图将 0x2005 设置滚屏地址,shifter 寄存器联系起来了,像是 attribute_shifter 也是类似的操作,图上有说明,应该能看懂什么意思的,我就不详细解释了,另外这些细节 wiki 上其实并没有明说,这是我根据模拟器的源码推出来的,这方面似乎应该也没什么详细的手册资料吧,如果有错还请指出。

可能有朋友有疑问,为什么 v 中存放着该像素所在的 tile 地址信息,这个问题其实与为什么向 0x2005 连续写两次就可以选取某个 NameTable 的某个像素位于屏幕左上角相似。

当我们向 0x2005 写两次,其实就是将某个 NameTable 的某个像素地址写入了 t,在渲染期间 t 会被复制到 v(这里我们再后文会讲述),所以写 0x2005 后第一次用 v 中的地址信息取得的 tile 就是我们所设定的,那么就使其位于屏幕左上角之后每次使用 v 中的地址读取 tile 索引的地址信息都会自动加 1 指向下一个 tile,如此循环往复渲染 960 个 tile,一帧背景

背景的渲染总过程就先说到这儿,一句话总结,根据 v 中记录的 tile 地址从 PatternTable 中取得 2bit 颜色信息到 pattern_shifter 寄存器,然后从 AttributeTable 中又取得 2bit 颜色信息到 attribute_shifter 寄存器,最后根据 fine_x 从 shifter 寄存器中选取要渲染的像素颜色信息

精灵

对于精灵来说,有这些相关硬件

  • Primary OAM,前文说过,256 字节,每一帧支持 64 个精灵
  • Secondary OAM,当前正渲染的扫描行支持的 8 个精灵
  • 8 对 8bit 移位寄存器,存放当前正渲染的扫描行上的精灵 tile
  • 8 个 锁存器,存放 8 个精灵相应的 Attribute
  • 8 个 计数器,记录 8 个精灵的 X 坐标值

存放 tile 图案信息到 pattern_shifter 和 attribute 信息到锁存器道理同背景,只是换了个名字锁存器其他的基本没啥不同,也不需要了解那么深入,有兴趣的可以在我后台回复 NES 获取 PPU 的手册。

这里主要说说计数器有什么作用,渲染是一行一行的渲染,每行像素的 x 坐标值范围为 [0, 255],存放在计数器中的 X 坐标每个周期是会减 1 的,所以说,当某个计数器减到 0 时说明渲染到该精灵了。

而对于精灵渲染总体过程与背景大致相同,主要是取得一个像素的 4bit 颜色信息,只是 shifter 寄存器只有等到计数器为 0 的时候才会活动(每个周期左移)。

取数据到 shifter 需要地址,这个地址就不是在 v 里面了,而是在精灵条目 OAM 中(正渲染的时候是在 Primary OAM 当中),从这里面取得 tile 索引的地址之后就去获取 tile 图案信息存放到 pattern_shifter 寄存器当中,然后获取 attribute 信息就简单了,直接从 OAM 当中获取。

好了现在我们精灵的 4bit 颜色信息和背景的 4bit 颜色信息都有了,然后就竞争到底输出哪个,当然只有背景和精灵重合的时候会有竞争,方式如下:

如果只有背景,输出背景

如果背景像素和精灵像素重合:

数字表示使用的 Pallete 中的哪个颜色,0 号颜色不管背景还是精灵都是相同的,对于背景来说可以看作是通用的背景色,对于精灵来说就是透明色。而 priority 是精灵条目中的一个属性位

好了本文就先说这么多,本文主要讲述了内存映射的几个寄存器和内部的几个寄存器,另外简析了滚屏和渲染,后文讲述渲染每个周期的细节,以及一些关于滚屏的高级玩法。
首发公众号:Rand_cs

目录
相关文章
|
6月前
|
编解码 并行计算 算法
MPI分形图像高精度绘制程序和PC端Mandelbrot-Julia分形集预览程序
这篇文章描述了一个使用2010年技术的集群程序,该程序基于Linux + MPI + C++或Windows + .NET + C#,用于并行计算生成高分辨率BMP图像,特别是Mandelbrot和Julia集。在8台节点上,程序实现了7.31的稳定加速比,并在更大规模任务中有望提升。它支持MPI并行计算、任务日志、不同阶数的分形集生成、批处理、多线程以及优化的颜色处理等功能。创新点包括颜色表的正弦控制、动态调整运算精度、复杂颜色生成、优化的颜色更新和并发机制等。程序产生的图像样本显示了其多样性和质量。作者提供源代码,并提到设计思路可应用于类似图像生成任务。
|
7月前
|
存储 索引
|
开发者 Kotlin
变“鼠”为“鸭”——为SVG Path制作FIFO路径变换动画,效果丝滑
曾撰文《使用batik在kotlin中将TTF字体转换为SVG图像》介绍了如何将汉字转为SVG Path路径进行展示和变换,以此为基础用动画将一个汉字变为另一个汉字,感官上很好玩
327 0
|
计算机视觉
Qt实用技巧:Qt设计器中QIcon的缩放(qss的放大和QIcon自动缩小(无法自动放大))
Qt实用技巧:Qt设计器中QIcon的缩放(qss的放大和QIcon自动缩小(无法自动放大))
Qt实用技巧:Qt设计器中QIcon的缩放(qss的放大和QIcon自动缩小(无法自动放大))
SwiftUI—如何将Picker转换为分段拾取器
SwiftUI—如何将Picker转换为分段拾取器
209 0
SwiftUI—如何将Picker转换为分段拾取器
|
vr&ar 图形学
【Unity3D 灵巧小知识点】 ☀️ | 使用代码控制 Image图片层级渲染 顺序
Unity 小科普 老规矩,先介绍一下 Unity 的科普小知识: Unity是 实时3D互动内容创作和运营平台 。 包括游戏开发、r美术、建筑、汽车设计、影视在内的所有创作者,借助 Unity 将创意变成现实。 Unity 平台提供一整套完善的软件解决方案,可用于创作、运营和变现任何实时互动的2D和3D内容,支持平台包括手机、平板电脑、PC、游戏主机、增强现实和虚拟现实设备。
【Unity3D 灵巧小知识点】 ☀️ | 使用代码控制 Image图片层级渲染 顺序
|
C# Windows
WPF 定时器DispatcherTimer+GetCursorPos 的使用,动态查看屏幕上任一点坐标
原文:WPF 定时器DispatcherTimer+GetCursorPos 的使用,动态查看屏幕上任一点坐标 using System;using System.Collections.Generic;using System.
1054 0
|
编解码 缓存 编译器
在屏幕上显示字符的原理
只描述在IA-32e模式下的字符显示 首先要有一个字符库(包含这每一个字符的像素信息, 空白的地方时0x00, 一个字符一个8x16的矩阵) 每一个像素点就是一个int类型4bytes大小的整数, 该整数的每一个字节都有特定的属性用来配置显示出来的字符的样式 要想实现, 需要在定义一个二维数组, ...
1039 0
|
机器学习/深度学习 存储 前端开发
WEBGL学习【十三】鼠标点击立方体改变颜色的原理与实现
版权声明:本文为博主原创文章,未经博主允许不得转载。更多学习资料请访问我爱科技论坛:www.52tech.tech https://blog.csdn.net/m0_37981569/article/details/79035437 // PickFace.
1199 0