Not counting the OAMDMA write tick, the above procedure takes 513 CPU cycles (+1 on odd CPU cycles): first one (or two) idle cycles, and then 256 pairs of alternating read/write cycles. (For comparison, an unrolled LDA/STA loop would usually take four times as long.)
中精度同步
之前用的是非常暴力(?)的同步方法, 同步精度大致在帧, 自己称之为"低精度同步". 现在就要使用更加精确的同步了, 同步精度大致在扫描行, 自己称为"中精度同步". 还有最高的基于各个部件周期, 精度大致为次像素, 称为"高精度同步". 不过高精度同步会消耗较大的计算资源, 不在本博客范围内, 有兴趣的读者可以自行实现.
CPU周期
之前提到了每个指令需要消耗多少多少周期, 现在就需要用了. 固定周期的直接加就行:
至于浮动的, 条件转移:
让条件转移语句判断后调用即可.
对于像"绝对X变址", "绝对Y变址", "间接Y变址"这三个有些没有额外的一周期这里用大小写区分:
(绝对X/Y变址也是同样的)
截图中可以看到有
SFC_READ_PC
和SFC_READ
, 为什么这么区分, 原因在后面还有像精灵DMA会消耗相当的周期:
CHR-RAM
之前提到有些ROM没有CHA-ROM, 只有CHR-ROM, 解决方法很简单:
在CHR-ROM的长度为0时多分配8KB即可, 这里偷懒直接'或'了一下.
主要是因为C没有
std::max
, 又太懒╮( ̄▽ ̄)╭不过注意的是这部分是会被程序修改的...反正是
malloc
的无所谓了.分割
在实现之前, 先看一下之前使用的一张图片:
之前提到了红框是当前的屏幕偏移量, 那有没有什么奇怪的地方?
就是左上角的分数显示! 因为直接显示这样分数会错位的. 这就是FC程序的小技巧, 屏幕刷新时写入VRAM指针. 大致时机可以分成两部分:
其中VBlank期间访问VRAM是合法, 而刷新时访问是不合法的. 但是可以利用写入\$2006(VRAM指针)这个制作出一些‘滚动’效果(或者\$2005, 但是PPU内部实现表明其实是一回事), 而真正的访问VRAM - \$2007(VRAM数据) 就太非法了, 模拟难度较高——需要高精度同步.
一般利用这个实现之前提到的滚动方式, 被称为'分割滚动'(split-scrolling)
精灵命中与溢出
有这么两个标志位, 会在渲染中途才会被设置, VBlank结束后会清空. 这两个标志位一般来作为'分割滚动'的动作的转折点.(不过实际上溢出很少有游戏会使用). 所以一些游戏(比如超级马里奥):
超马1的那个金币下边就是精灵0, 等到触发#0命中, 然后程序猿掐到大概状态栏显示完了(还有一行), 就恢复滚动.
除了这两个还有通过硬件的IRQ(比如超级马里奥3利用Mapper触发IRQ, 状态栏在下面; 甚至还有的ROM是通过APU的IRQ), 后面再说
精灵0命中
简单地说就是精灵#0, 如果某像素不是透明的, 然后背景也不是全局背景色(即低2位都有效时)会触发, 详细的说明(细节)还是请到引用连接查看. 目前的实现, 很多细节是通过不了全部测试的.
不过一般游戏, 要么就是用来分割屏幕, 要么就不用, 实际上就懒得完全实现.
精灵溢出
简单地说一条扫描线需要渲染超过8个精灵时, 触发溢出. 详细的说明(细节)还是请到引用连接查看. 目前的实现, 很多细节是通过不了全部测试的
不过一般游戏, 基本就不用(就几个游戏在用), 实际上就懒得完全实现.
相关内部寄存器
VRAM指针(\$2006)在渲染时可被描述为:
PPU scrolling中提到的这几个内部寄存器, 应该是PPU本身的内部实现, 但是我们不用太拘束, 只要理解每个位表示什么就行:
修改点
同步
我们先实现一个简单的模式来模拟一帧(场), 也就是"中精度同步". 并且基于'同时只能显示25色'的假设(虽然可以在渲染时修改调色板以达到超过25色的可能, 也就是说这种情况是不支持的), 也就是, 和之前的实现差不多.
根据说明, 我们可以这么实现:
261-262 空行 x2 (或者1.5)
'执行CPU一段时间'是多久呢, 根据文档Clock rate, NTSC的Master Clock是'21.477272 MHz', 除以60, 再除以 262.5大概是1364周期. CPU频率是Master的12分之一, 113.5. 为了避免小数, 我们用 Master Clock作为基准就行.
现在刷新的频率是和模拟器环境, 也就显示器, 的刷新频率是一致的. 好在自己电脑就是60Hz无所谓, 如果是144Hz显示器的话速度就会很快. 这个由于自己显示器就是60Hz所以一直没在意, 一直没改, 后面再说吧.
合在一起
现在就是把背景和精灵合在一起了, 如果我们把全局背景色理解为清除色的话, 就很简单地理解了:
合成:
看起来很简单, 但是花的时间非常多, 大概花了一个星期. 完成倒是很早就完成了, 完成后就是去通过ROM测试, 发现通过不了就再修改, 反反复复, 直到测试通过到一定程度(没有完全通过), 非常花时间.
当然, 实现了精灵的8x16, 两个方向翻转, 但是在处理P精灵时存在小BUG, 这个因为打算后面用顶点去渲染, 有Z坐标的话就很方便了.
细节
有些细节依然没有实现:
SSE指令
实现了之后, 自己用SSE处理了背景渲染的部分, 因为背景相对来说是对齐的, 就像数组那样, CPU使用明显降了不少.
不过精灵部分没有用SSE实现, 因为没有对齐. 通过对雪碧拉罐(spritecan)的ROM测试:
再使用CPU剖析:
可以看出雪碧(sprite), 哦不, 精灵(sprite)的渲染占了整个核心的6%(1.28 / 20.37), 换句话说就算精灵渲染优化到0(效率提升无限倍), 也只有6%的核心提升, 全局甚至只有1%. 所以懒得优化了.
是的, 这次的测试ROM是spritecans, 这个ROM使用的是8x16精灵模式.
项目地址Github-StepFC-Step7
F-1 Race
被誉为天才程序猿的岩田聪在FC上开发了一款伪3D赛车游戏 - F-1 Race. 目前模拟效果如下:
可以看出这个游戏是算准了CPU周期然后执行水平偏移实现的伪3D(大神就是能在处理游戏逻辑的同时掐准CPU周期写入偏移量). 也可以看出目前的同步率不够高(基于行, 一旦错过则该行错位), 看看以后能不能解决(比如一旦写入偏移则通知渲染层).
顺带一提这个游戏是分奇数帧和偶数帧的, 处理不当会导致画面闪烁.
作业
REF