梦开始的地方——FC游戏开发指南(7)高性能的秘密:DMA
(本系列是一个回归电子游戏原点的特别系列,作者 @goodorc_gamedev)
上期链接:https://www.bilibili.com/read/cv13722007
上次文章介绍了精灵的概念。按照我们玩FC游戏的的经验,FC同屏处理几十个精灵应该不是难事。比如《沙罗曼蛇》,在模拟器里将背景层关闭,就可以看到同屏有几十个精灵同时在屏幕上运动:(一个敌人是由4个左右的精灵组成的,所以下图至少有30个以上的精灵)

但是当我用上次视频的方法,将精灵拷贝到显卡,发现只要超过10个精灵,就无法正常显示了。
这是为什么呢?
还是因为FC的CPU太慢了,速度不到PPU的三分之一,如果每帧通过0x2004端口循环拷贝几十个精灵,那么CPU会死给你看。
所以FC高性能的秘密,就在于——精灵数据能够以极高的速度传输给PPU,每秒轻松传递几十次,实现数十个精灵每秒几十次的移动、变换。
1、DMA(Direct Memory Access)
百度百科上DMA的介绍很直截了当:

DMA(Direct Memory Access,直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU的大量中断负载。
简单来说,就是有一个专用硬件,它受CPU控制。只要CPU告诉它,把内存里的哪一段地址的数据搬到PPU,它就会以极高的速度将内存数据搬进PPU。而且它走的还是一条“高速公路”,叫做DMA总线。
在DMA传输阶段,CPU还可以腾出手做其它事情。
2、基于DMA的渲染流程
我按照参考资料,基于DMA改善了FC渲染流程,如图:

解释:
很多精灵对象,用struct数组表示,数组做成固定长度的即可。
★ 注意精灵数组的起始地址是专门指定的,下面会提到。
每一帧更新每个精灵对象的tile、位置等等。不需要的精灵填0即可。
在原来的程序中,每一帧要通过load_SP函数将精灵拷贝到PPU。但这次改为直接控制DMA,作用一样是把很多精灵对象更新到PPU。
一旦想清楚了,就是这么简单。
DMA传输的C语言写法:
参数page=2,start=0是怎么回事呢?
任天堂文档里是这么说的:
$4014 写
DMA 访问精灵 RAM:通过写一个值 xx 到这个端口,引起 CPU 内存地址为$xx00-$xxFF 的区域传送到精灵内存
简单解释:端口0x4014是一个DMA控制端口,往里写一个0~255的数字,就会启动DMA,将内存中0xXX00 ~ 0xXXFF的数据传输到PPU的精灵区。比如写上2,就会自动拷贝0x0200 ~ 0x02FF这一段精灵数据。
内存中一整块256字节的区域,比如从0x1A00 ~ 0x1AFF ,称之为一页(page)。
另一个参数start应该是偏移量,暂时填0即可。
3、C语言特性——直接指定变量的内存地址
我们一般学习C语言都是用PC机学习,内存足够用,内存地址也是由操作系统分配。但到了FC这种单片机系统,就要借用更高级的C技巧了。
既然DMA要求必须事先说定精灵数据的地址,而且说好是传一整页。这就要求必须把精灵放在指定的地方,比如从0x0200开始,一直放到0x02ff,共256字节。每个精灵对象4字节(tile, x, y, attr),一页可以放64个精灵。
写法像这样:
如果这样定义,初始化的写法:
只要用到精灵数据的地方,都要用类似的写法修改。也许有更好的写法。
至此,大家既可以参考第一篇文章放出的《spitfire》射击游戏项目,也可以在《移动》简单项目中修改。
4、实例
《移动》的测试项目,加入DMA后代码修改如下:
上面这段代码与之前的效果相同。读者可以给它加上发射子弹的功能,看看最多能同时发射多少子弹。
5、将DMA指令写成汇编
像DMA这种每帧调用的必要操作,写在C语言循环里仍然不是最佳选择。最好的办法是直接写成汇编,连函数调用也省了。
修改之前看过的crt0.s文件,找到中间的标签nmi:
nmi是一个特殊的中断,在电视机的每帧渲染完毕后触发,在这里做一些每帧都需要做的DMA工作再合适不过了。
在inc tickcount那句代码之前,插入DMA操作代码:
编写完毕后,应删除C语言中的SP_DMA(2,0)函数调用,显示效果不变。
至此关于精灵Sprite的讨论告一段落,下一期将介绍FC的背景地图与卷轴功能。
(本文作者 @goodorc_gamedev。欢迎加入游戏开发群欢乐搅基:1082025059
对游戏开发感兴趣的童鞋可戳这里进一步了解:http://www.levelpp.com/)