街机模拟器工作原理

转自=easyrock(2路转4路)=的原创,很少看见这么深入底层的与性机制详解,牛人啊,膜拜ing进而收藏之!

街机模拟器工作原理         这几天学习了一下finalburn的源代码,有一些心得,惊喜之余,整理出来与大家分享。         我们 常说的芯片,通常都是接受一定的输入,完成特定的计算,并产生结果。输入通常来自相连的外设,输出也传递到外设。例如,最简单的计算器,内部也有一个芯片,我们在计算器的键盘上输入3+5=,就转化成了计算器芯片的输入参数,并启动内部的计算逻辑,算出结果8,在屏幕上显示出这个结果。作为cpu的计算器芯片,在这个过程中已经与两个外部设备进行了交互:键盘和显示屏。         cpu与外围设备交互,通常有特定的方法。像x86系列cpu,提供了io端口,中断,内存映射这几种方式。例如cpu与显卡的交互,通常会使用到以上所有的方式:显卡的帧缓存通过内存映射,使cpu可以寻址到;显卡芯片的寄存器(显卡芯片也是一个cpu,也有寄存器)通过io端口进行读写,通过读写寄存器这种低级的编程方式,显卡开始各种工作(例如bitblt),cpu也并行的进行其他计算工作,当显卡完成某个 功能 调用后,通过x86中断通知cpu所需的请求已经完成,进而cpu可以再请求显卡做下一步工作。通过中断,cpu和显卡处于异步的方式,cpu不必等待显卡完成绘图。         几乎所有的芯片都是可编程的,也就是说,可以定制它们的工作方式。只是一些芯片提供了良好的编程接口。像x86系列,MC68000等这样的芯片,可以运行一段事先写好的程序,来完成某一特定的功能。而像早期的显卡芯片,只能通过读写寄存器来调用显卡的功能。         街机模拟器,最核心的功能就是模拟那些街机用到的芯片。比如《街霸2》这个游戏,用到了MC68000做cpu,Z80做音频处理,还用到了一个yamaha的音频芯片,视频方面应该用到了一个芯片做输出。         游戏的rom,包含音频、视频数据,和游戏代码。代码就是在MC68000上跑的,因此先说一下MC68000的模拟。         MC68000(m68k)的模拟器是用汇编写的,来自于MAME。从代码中可以看出,m68k有一组寄存器、接受中断输入、通过内存映射与外设交互。m68k模拟器的内存接口由客户端(使用该模拟器的代码,在这里就是街机模拟程序)提供,这样一来,客户端就可以接管m68k与外设的交互了。例如,用于音频处理的z80,寄存器组被映射到m68k的某一地址段上。m68k在运行过程中读写z80的寄存器,实际上回调街机模拟程序的相应代码,转发给了z80模拟器。通过这种方式,就可以模拟m68k与其他外设的交互了。         m68k模拟器用本机代码模拟m68k的指令集。每一条m68k指令用一段本机代码来模拟。对于一段m68k代码,结合ip(指令指针)找到对应的m68k指令,通过一个跳转表,跳到对应的那一段模拟代码执行。每一条m68k指令模拟出来的执行周期是原指令周期的数倍到数十倍。因为本机cpu的速度通常是m68k速度的数百倍,所以这样的模拟不会有问题。         那么本机cpu(host)与被模拟的cpu(m68k)是如何联系的呢?首先必须让模拟的m68k在正常的主频下运行。host用指定的频率(例如60Hz)来调用m68k模拟器,指定让m68k运行多少个时钟周期(cycle)。比如想让m68k跑在8MHz,那么每次要让m68k跑8000000/60=133333个cycle。在m68k运行的过程中,自然会通过内存接口与外设交互,这样就让其他外设的模拟器开始相应的模拟工作。         视频部分没有用到模拟器,我推测是使用的帧缓存(framebuffer)。因为2D游戏的画面都是通过贴图(tile)完成的,不需要绘制。为了达到60Hz的刷新率,模拟程序每次在m68k跑完时钟周期后,读取这个帧缓存,转化成符合当前屏幕格式的位图,blt到显卡。这个过程还需要进一步的 学习 。      我的最终目的是想移植finalburn到linux上。国外早就有了这样一个 项目 http://fblinux.emuunlim.com,但是为了学习,一切都要自己去尝试。         finalburn的源代码直接在www.finalburn.com下载。逻辑上分为几块:底层的模拟库burn,上层的ui库burner。其中burn用到了更底层的a68k和doze两个 模块 ,他们分别是MC68000模拟器和Z80模拟器。         街机游戏包含两块板卡:主机板和游戏板。主机板上有cpu,dsp等处理器,游戏板上存放的是游戏rom和加密用的电路。我们玩模拟器,需要解密后的游戏rom,相当于我们手头上有了游戏板,在模拟器中载入游戏rom,就可以开始游戏。所以模拟器还需要模拟主机板( 基板 ),而不是光是单纯的MC68000cpu和Z80cpu。         基板有点像电脑主板,上面不光有cpu,还有很多外设,像声卡、网卡等。比如《街霸2》游戏使用的cps1基板(Capcom   Play   System-1),主控cpu是用的MC68000,音效cpu是用的Z80。为了能控制Z80,肯定是要把Z80的寄存器空间映射到MC68000的地址空间上来。MC68000读写某一段地址的内存,到底是要读写数据,还是访问某个外设的寄存器?不同的基板,各有特定的外设,和映射方式。所以正确的模拟出这些游戏机板,是模拟器的第二个重要工作(第一个当然是模拟各个独立的cpu)。         因此,模拟器的底层模块,分为模拟cpu和模拟基板两类。显然模拟cpu的模块是高度可复用的,比如mc68000的模拟模块,可以用于模拟cps1,cps2和mvs(neogeo)基板,因为他们都用到MC68000cpu。针对新出现的基板,如果用到的某个cpu已经有了模拟的模块,同样可以直接使用。MAME就是不断的模拟各种cpu和基板,以支持更多的游戏。         对于模拟的每一个游戏,也需要一个单独的模块。提供一些必要的信息和特定的处理。比如这个游戏有哪些rom,哪些用于视频,哪些用于音频,哪些是代码。有些游戏也需要特定的前期和后期处理。比如1944这个纵版游戏,在完成每一帧画面的组织后,要翻转90度。这些都是在特定游戏的模块中完成的。         burn库模拟了cps1、cps2和system16基板。这三款基板都是用的MC68000cpu。加入更多的cpu模块,基版模块和游戏模块,就可以模拟更多的游戏了。         对于2D游戏的视频部分,因为不像3D游戏画面那样需要动态计算,2D游戏的所有图像数据都是事先画好的。通常分为几层,比如角色层、背景层等。每一帧的绘制都是按Z序来绘制这几层的。用“贴图”来形容这个过程比用“绘制”更贴切。因为每一层都被分成了NxM个方块,按照索引保存在rom的视频数据区。对于每一帧,计算出出现在屏幕上的那些方块,按照索引,就能找到每一个方块对应的图像。把这些方块贴到framebuffer中,就形成了生动的图像。    

目前 流行的模拟器都有Save/Load   Game的功能,和录像功能,这些是如何实现的呢?         先谈谈游戏中随机事件的产生。比如玩《街霸2》,每次我们选同一个人物,第一关的对手也不一定是相同的。又比如,第一关对手即使相同,电脑每次的进攻策略也不一样。这些都是相当随机的。可以想象成游戏使用了一个类似于rand()的函数,根据函数产生的随机值作出随机的反应。当然电脑对手的动作不是完全依赖于这个随机值,肯定还依赖于游戏者的输入动作,比如游戏者跳重腿,电脑对手通常会发出对应的招式来反击,或者挡,而不是随机的乱动。当然决定是反击还是挡,这本身也存在一个随机的选择。正是因为游戏中处处充满的不可 预测 性,才使得游戏更具有娱乐性。         然而,事实上,真正的随机是不存在的。这些随机都是根据一个种子值,经过各种不相关的运算产生的伪随机值,计算出的值又作为计算下一个“随机”值的种子。也就是,只要初始给的那个种子相同,每次计算出的随机序列都是一样的。这样随机就变成可预见和可再现的了。如果让初始种子与时间相关,那么只有相同的时间计算出的随机序列才相同,这样就大大降低了随机重复的可能性。可以推测,游戏中也用到了时间值来做为随机数的种子。这个时间值要么来自于一个单独的计时器,要么就来自于cpu——不管来自于哪里,每次复位后,这些值都变成初始值了。         打开两个模拟器,载入相同的游戏,开始运行。这两个游戏运行的画面肯定是完全相同的。当然这还不足以说明问题。但是你投币,开始游戏,即使选择相同的人物,相同的剧情,游戏的发展也可能完全不同。这个不同来自于另一种类型的随机——用户输入。因为用户的输入序列不太可能做到每次都相同,即使按键的序列相同,按键时间几乎不可能是一样的(这个时间显然是相对于游戏复位的时间)。如果能够做到在每次复位后,输入的按键和按键的时间都相同,那么游戏就变成了重复的视频和音频序列,也就是——录像回放。模拟器要做到精确到帧的输入是很容易的,所以录像功能的实现,就是记录游戏者每次输入的精确帧号(或时间)和按键。播放录像时,首先复位,然后在适当的帧(或时间)上输入适当的按键。就这么简单了。         这个功能可以进一步发展到 网络 对战的实现。首先,连网的多个模拟器需要复位,然后每一帧都需要同步,这个同步就包括很重要的游戏者输入信息。首先,帧的同步确保了游戏时间的一致,也就确保产生一致的随机序列。再加上用户输入的同步,另一个随机源也达到一致了,所以每一个模拟器上的运行效果就都是相同的“随机”情况了。当然,网络对战实现起来远比这里提到的复杂,需要容忍网络延时,和处理网络数据的高效率传送,同步本身也是一个相当伤脑筋的事情。         游戏的Save/Load功能相对简单,Save只需记录当前游戏的运行数据,以便下次Load时恢复。需要记录的数据首先是内存。把所有的RAM保存下来,这样包括那些映射的外设的寄存器也一并存了,恢复时外设也恢复到了相应的状态。最后还应该保存cpu的寄存器状态。但是我在finalburn的Load/Save代码里没有找到保存68000cpu寄存器的相应代码。推测:开始运行后,寄存器就处于稳定的状态,或者处于一个简单的循环——读取RAM和ROM数据,分派到相应的子程序进行处理。所以只需要把RAM数据替换,游戏就来到了保存时的状态。这个推测不太合理......  

2D街机游戏都是以恒定的帧率运行的。比如《街霸2》这款格斗游戏,帧率为60。显然游戏运行过程中,CPU并不是一直处于忙碌的状态,而是等待一个60Hz的时钟,每次到了时间点上,才运行一帧。这里的运行一帧,包括检测玩家输入,运行游戏逻辑,产生一帧图像,和产生时间长度为1/60秒的音频数据。这个60Hz的时钟,来自一个外部设备的中断。通常这个外部设备就是街机的显示器。该显示器刷新率为60Hz,每秒产生60次VBlank信号,中断到CPU,驱动游戏运行。         模拟器的工作循环的伪代码类似于这样:     while   (1)   {     休眠直到时间点();             扫描输入();             设置虚拟机VBlank中断();             运行虚拟机n个周期(8000000/60);             播放音频采样();             更新视频画面();     }         但是有几个问题需要考虑。首先,音频的播放比较麻烦。声卡以一定的速率,从一个缓冲区中循环读取PCM采样,然后播放。这个恒定的读取速率由采样率决定。比如,采样率为44100Hz,速率就是每秒44100个采样。反过来也决定了,声卡每读取44100个采样,时间就是1秒。1/60秒的时间,就是声卡读取44100/60=735个采样的时间。所以,“休眠直到时间点”这个过程,就是等待声卡读取完735个采样的时间,而不是系统时钟的1/60秒。同时使用两个时钟源的结果将是视频和音频不同步。         一方面要以声卡作为时钟源,另一方面还要确保声卡的缓冲区总是有正确的数据可供读取。不能等到声卡播放完当前一帧的数据,才去准备下一帧数据。需要提前准备好即将播放的数据,也就是缓冲一定量的数据。但是缓冲的时间应该尽可能小,以保持模拟器的低延迟性。         通过 查询 当前声卡读取缓冲区的位置,可以计算大概的时间,以确定是休眠等待还是运行下一帧。但是这个时间只能是大概的,而声卡的播放速度决定的时间是精确的。每运行一帧,把产生的音频数据写到缓冲区,这个过程重复的速度只有跟声卡读取数据的速度一模一样,才不会导致缓冲区溢出。速度不匹配,就需要控制了,如果产生数据的速度太快,就需要休眠一些时间,等待声卡播放到合适的位置,反之,则要快速运行几帧,准备好需要的音频数据,同时只更新最后一帧的视频画面。这种情况下,视频就丢帧了,但是几帧的丢失不会影响视频的流畅性。另外值得注意的是,即使视频的帧率因为丢帧而达不到60,虚拟机的运行还是要保持每秒运行60帧的,这样确保音频,输入和其他逻辑的正常运行。         如果加入网络对战的支持,“扫描输入”这个过程,需要通过网络通信来得到对方的输入,并同步各方的运行。网络的延迟是不可预测的,所以在延迟较大的情况下,很难保持音频的流畅。如果通过缓冲数据来缓解这个问题,又将带来游戏的延迟。在这种限制下实现的最好效果,因该是不超过100ms的延迟,和偶尔的停顿。 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值