bookmark_bordernes模拟器开发记录(三)

1. 解决了 js/ppu.js 被adblock误伤导致游戏无法执行的bug。方法是把所有js打包在一个文件里,靠sourceMappingURL度日。

2. 用户输入实现了,玩起来意外的顺利。
其实我ppu还有些功能没实现呢,8×16精灵什么的。我本来是想等遇到了显示bug再去实现,也好检查自己是不是实现对了,可就是没遇到这种场景,算了不测了,下次盲写吧。

3. 下一步是实现声音处理。
先读文档吧,一边看文档一边参考别人的实现。因为基本能玩了,我的“业余项目完成75%就失去兴趣”的症状开始发作了。这时候更要坚持!2019年将是见证奇迹的一年。

bookmark_bordernes模拟器开发记录(二)

滚屏实现完了,效果如下:

 

期间修复了一处笔误,当时是把0xff3f算错了,写成了0xffcf, 两个0bit歪了两位,害我排查了老半天,惨。

不过这种bug很难避免,只能继续提高debug能力了。幸好我有正确答案可以参考,修起来比较容易。

有这么优越的条件,这次一定要坚持做完这个项目呀!

后面还差手柄控制和音频输出和执行效率优化了。加油!

bookmark_bordernes模拟器开发记录(一)

nes

如图,我正在开发nes模拟器,也就是2018年5月份这篇日志里说的黄卡游戏机的模拟器。
本来想等到做完之后再突然贴出来的,不过这个个人项目实在是拖的太久了,其中有很多感想我担心到最后写日志的时候已经忘记了,所以还是以定期总结记录的形式发出来吧。

关于nes模拟器的开发难度。
开发nes模拟器对技术深度没有要求,可以说会写程序,会显示图像,会播放声音的人就能开发。只是模拟器的细节比较多,所以做起来耗时比较长,有点累而已。和自己设计一个应用程序呀游戏什么的比起来,制作模拟器的好处在于完全省略了设计部分,所有的设计细节都是现成的,你只需要充分施展你的工程能力即可。对于我这种没有产品能力没有营销能力唯独喜欢搞工程的人来说是最适合的业余项目了。说起来自己重新实现一些rfc协议也有类似的乐趣,还是我那个提了一万回的自制http服务器项目,就是重新实现rfc2616,也是乐趣无穷。

再介绍一下我现在的进度。
如图所示,我现在做到了正确显示游戏内的背景图像了。具体来说就是实现了全部的cpu指令,实现了显示背景nametable中的内容。
没实现的呢,主要是音频完全没做,手柄操作完全没做,屏幕滚动还没做,甚至按固定帧率执行游戏都还没做。上面的截图是我顺序执行了大约350000条汇编指令后的效果。不过这个固定帧率不难,我因为一直在修一些低级bug,都是跑几帧就出问题的地方,所以就没着急做按帧率执行。

关于这个项目的预期时间投入。
目前看来总时间投入要在200小时甚至以上了。当初觉得自己两个礼拜就能搞定,真是太可笑了。
时间分配上,阅读和查阅文档和参考项目要占70%的时间,代码实现占10%的时间,修bug占剩余的20%的时间,大概是这样。因为nes模拟器是一个比较常见的个人项目选题,github上有很多别人实现好的项目可以参考,我甚至可以实际运行别人的模拟器,逐命令比较我和他执行结果的不同,所以调试方面已经比早期那些第一代做模拟器的人们简单很多了。但越做到后面遇到的硬件细节越多,而且错误越来越隐蔽,还是花掉了我两倍于开发的时间来调试bug。如果这个趋势继续恶化的话,我接下来要考虑先做一个高效率的模拟器调试器了。

最后记录几个我踩过的坑,留给未来的自己回顾时回忆。
1. cpu读取当前显存地址的内容时,得到的永远是自己上一次读取显存时的缓存值! (https://wiki.nesdev.com/w/index.php/PPU_registers#The_PPUDATA_read_buffer_.28post-fetch.29
这个太反直觉了,一定是当时的硬件限制导致的。没有正确实现这个细节导致我的地图贴图序号部分出现了错误,甚至我的贴图图案都被污染了一部分。

broken-nes

比如下图的上边两张是原始的贴图,下边是被污染后的贴图。为了清楚分辨,我给他们上了比较艳丽的颜色。可以看到右下的©中间被污染了,显示在屏幕上就是中间出现了一些多余的黑色的像素。

broken-texture

调查这个bug花了我大概一整个上午的时间,期间发现用chrome调试面板在高频执行处下断点会极大的影响程序执行速度,甚至有时候会内存使用量过大导致页面崩溃。就是那时候我开始考虑是不是应该做一个模拟器专用调试器了。

2. 2d canvas 的绘制效率。
html5的canvas我工作中经常接触,但大多是在使用现有绘制库的基础上。徒手操作canvas绘制时的绘制效率方面我之前并没有研究。
最开始我按照我知道的方法, ctx.fillStyle=”rgb(r,g,b)”; ctx.fillRect(x, y, 1, 1); 来绘制每一个像素。一帧两帧还好,当我执行到一口气绘制30多帧的时候,绘制部分的耗时陡增到5秒多。当时我就意识到方法错了。毕竟有现成的jsnes模拟器,每秒跑60帧都没问题的,所以肯定有更高效的绘制方法。
稍微搜了一下,找到了创建 imagedata,然后直接修改 imagedata.data 数组里的数字来设置每个像素的颜色,最后把imagedata整个画到 canvas 上的方法。换这个方法以后绘制部分耗时只有原来的1/10了。虽然离60帧每秒还有一点距离,但至少不影响我调试了,暂时先这样吧。