• 首发公众号: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 × 8 8\times8 8×8,1: 8 × 16 8\times16 8×16
  • 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,则会从下图所示位置开始渲染:

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,求关注支持

童年神机小霸王(四) 滚屏渲染 1相关推荐

  1. 童年神机小霸王(七) Mapper

    首发公号:Rand_cs,求关注支持 Mapper mapper,这个概念来源于 memory mapping,又叫做 Memory Management Circuit,它是解决地址映射的一种电路, ...

  2. 童年神机小霸王(六) 手柄

    首发公号:Rand_cs,求关注支持 Controller&Format Controller 本文讲述 NES 的输入设备,最为常见的就是手柄 joypad: 一般支持两个手柄,手柄 1 和 ...

  3. python爬取今日头条瀑布流_连续动作:滚屏采集瀑布流网页—以头条新闻为例

    常见的网页大多数在页面下方会有翻页的按钮,比如"下一页"."加载更多",这类网页设置翻页就可以搞定,但是瀑布流网页没有这些按钮,而是随着鼠标滚动会不停的加载更多 ...

  4. 聚观早报 | 神舟十四号飞行任务圆满成功;大众ID大面积车机故障

    今日要闻:神舟十四号载人飞行任务圆满成功:大众 ID 系列被曝大面积车机故障:比亚迪:刀片电池将会用于商用车:SpaceX推出新一代"星盾"服务:花椒直播将在12月12日上市 神舟 ...

  5. 同级最强!天玑8200实测成绩放出,iQOO Neo7 SE神机配神U

    联发科的天玑8200芯片于12月8日正式发布,首发搭载天玑8200的性能神机iQOO Neo7 SE也在同天发布开售.作为天玑8000家族的新成员,天玑8200不负"神U二代"的威 ...

  6. 怎么通过media foundation将图像数据写入虚拟摄像头_不知道怎么挑手机?性价比神机绝对适合你...

    阅读本文前,请您先点击上面的蓝色字体,再点击"关注",这样您就可以继续免费收到最新文章了.每天都有分享.完全是免费订阅,请放心关注.注:本文转载自网络,不代表本平台立场,仅供读者参 ...

  7. echart 时间滚动_基于 ECharts 封装甘特图并实现自动滚屏

    项目中需要用到甘特图组件,之前的图表一直基于 EChart 开发,但 EChart 本身没有甘特图组件,需要自行封装 经过一番鏖战,终于完成了... 我在工程中参考 v-chart 封装了一套图表组件 ...

  8. 250鲁大师跑分_我装了一台鲁大师 230W 分的神机,3A 游戏平台装机作业

    原标题:我装了一台鲁大师 230W 分的神机,3A 游戏平台装机作业 今年 AMD 的表现可以说是非常令人震惊,CPU 和 GPU 两开花,拳打 Intel 敬老院,脚踢 NVIDIA 幼儿园,一举成 ...

  9. jQuery实现 自动滚屏操作

    实现自动滚屏思路: 1.滚屏即:文本的往上移动一段距离: 2.那么我们使文本每过一段时间就往上移动一段固定距离,就可实现滚屏: 3.直到文本底部出现在浏览器窗口中,专业点就是 文本移动的距离 + 浏览 ...

  10. OpenGLES2.0渲图步骤:绘几何图形、图片处理、离屏渲染(3)

    OpenGLES2.0是一个图形渲染(图形处理)库. OpenGL ES 2.0渲染过程为:读取顶点数据--执行顶点着色器--组装图元--光栅化图元--执行片元着色器--写入帧缓冲区--显示到屏幕上. ...

最新文章

  1. SpringBoot 2.0 多图片上传加回显
  2. 设置Android设备在睡眠期间始终保持WLAN开启的代码实现
  3. 树莓派:关于linux内核
  4. 智能循迹小车_智能机器人之循迹小车——循迹原理
  5. 英语语法---介词短语详解
  6. win10无法检测java_Javac 在windows10系统不识别
  7. RocketMQ(一)——发展历程及基本概念
  8. php oci_bind_array_by_name查询,PHP - 函数:OCIBindByName()
  9. 金蝶ERP实现产品入库冲减生产现场虚仓毛坯数
  10. 语音识别市场竞争激烈,亚马逊崛起与微软衰落形成反差
  11. java mybatis cms_java cms系统 springmvc mybatis
  12. iperf3 for Linux
  13. 观看影片《硅谷传奇》
  14. 洛谷 P1097 统计数字
  15. 部署 php 项目,使用deployer 来部署PHP项目
  16. 四川川之音文化传媒有限公司:电商物流运行呈加快恢复态势
  17. Lumen (Laravel子框架) 简介及分析
  18. 计算机网络技术协议的三要素,通信网络协议三要素
  19. Matlab 在线版 —— 科研人员的福音!无需下载安装,可计算可作图
  20. 医院临床信息管理系统

热门文章

  1. 北海康成通过聆讯:核心产品处于临床阶段,两年半亏损14亿元
  2. 计算机控制水槽液位控制,电气工程综合设计终极版(储液罐液位计算机控制系统设计)资料.doc...
  3. java输出图形代码大全_分享java打印简单图形的实现代码
  4. 零基础,安装ubuntu17.10,双系统双硬盘双显卡
  5. uniapp IOS移动端 固定定位position:fixed 失效
  6. BZOJ1067: [SCOI2007]降雨量
  7. table固定表头并且固定左边第一列的纯粹css实现
  8. 点云从入门到精通技术详解100篇-基于 CBCT 与口内扫描数据的牙齿点云配准(续)
  9. 动态创建xxl-job任务
  10. 5个问题解释css浮动问题