我要编写一个Game Boy模拟器(Z80是CPU,以防有人不熟悉它),当我在做研究时,我发现了一些东西我我不太确定。
第一个是C是这里选择的编程语言。这不是一个问题,但我想从今天的观点听取你的意见。甚至不建议使用C ++。
我发现的第二件事是每个操作码都使用一个函数。这似乎是合乎逻辑的,因为它只是一个函数调用,并且可能比为“ADD”指令设置一个函数更好地优化,然后你必须找出这里使用的寄存器。但今天有多必要?这是我应该坚持的东西,还是我应该改写我的模拟器,如果我注意到另一种可能更方便的方法就是不切割它(现在或多或少现代游戏控制台出现在我脑海中)?
此外,一遍又一遍地编写一个“将该寄存器添加到该寄存器”的函数是一种失真。有没有办法从操作码映射或类似的东西自动化?
答案 0 :(得分:6)
我大多同意WingsOfIcarus。我已经写了一些模拟器,所以这是我的见解:
OOP不是问题
是的,成员调用有点慢,但如果你小心,它不会对性能造成太大影响。另一方面,OOP仿真代码更易于管理/阅读/理解。
使用指令数据库而不是固定指令解码。
我正在使用单个文本文件,其中包含所有指令的所有必要信息。模拟器在初始化期间解析它(提供函数指针和操作数的数组......)。在这种架构中,很容易在没有任何代码更改的情况下纠正指令集中的错误。
复杂的指令集文档在某些方面几乎总是有缺陷的。最糟糕的情况是 Z80 (我从未看到100%无错误的指令集)。因此,使用更多指令集,比较它们并创建一个无错误集(如果可以的话)。
为您的模拟添加声音,视频,键盘和鼠标
这通常不是问题。在 Windows 上使用 WaveOut 而不是 DirectSound 。它更稳定,更快(DSound的可用延迟有时甚至> 400 ms)。使用WaveOut,我能够将潜伏期延迟到20-80毫秒,这是可以的。
将限制速度应用于每秒模拟CPU的T周期
我正在使用机器周期校正时间慢得多,但允许我正确实现任何硬件外围仿真(FDC,DMAC,声音芯片,......没有任何黑客攻击)
为模拟平台应用加载/保存文件
例如,这是我的指令集的一部分(直接输入到CPU仿真:
opc T0 T1 MC1 MC2 MC3 MC4 MC5 MC6 MC7 mnemonic
B8 04 00 M1R 4 ... 0 ... 0 ... 0 ... 0 ... 0 ... 0 CP A,B
B9 04 00 M1R 4 ... 0 ... 0 ... 0 ... 0 ... 0 ... 0 CP A,C
BA 04 00 M1R 4 ... 0 ... 0 ... 0 ... 0 ... 0 ... 0 CP A,D
BB 04 00 M1R 4 ... 0 ... 0 ... 0 ... 0 ... 0 ... 0 CP A,E
BC 04 00 M1R 4 ... 0 ... 0 ... 0 ... 0 ... 0 ... 0 CP A,H
BD 04 00 M1R 4 ... 0 ... 0 ... 0 ... 0 ... 0 ... 0 CP A,L
BE 07 00 M1R 4 MRD 3 ... 0 ... 0 ... 0 ... 0 ... 0 CP A,(HL)
BF 04 00 M1R 4 ... 0 ... 0 ... 0 ... 0 ... 0 ... 0 CP A,A
C0 11 05 M1R 5 MRD 3 MRD 3 ... 0 ... 0 ... 0 ... 0 RET NZ
C1 10 00 M1R 4 MRD 3 MRD 3 ... 0 ... 0 ... 0 ... 0 POP BC
C2L2H2 10 10 M1R 4 MRD 3 MRD 3 ... 0 ... 0 ... 0 ... 0 JP NZ,U16
C3L1H1 10 00 M1R 4 MRD 3 MRD 3 ... 0 ... 0 ... 0 ... 0 JP U16
C4L2H2 17 10 M1R 4 MRD 3 MRD 4 MWR 3 MWR 3 ... 0 ... 0 CALL NZ,U16
C5 11 00 M1R 5 MWR 3 MWR 3 ... 0 ... 0 ... 0 ... 0 PUSH BC
C6U2 07 00 M1R 4 MRD 3 ... 0 ... 0 ... 0 ... 0 ... 0 ADD A,U8
C7 11 00 M1R 5 MWR 3 MWR 3 ... 0 ... 0 ... 0 ... 0 RST 00H
C8 11 05 M1R 5 MRD 3 MRD 3 ... 0 ... 0 ... 0 ... 0 RET Z
C9 10 00 M1R 4 MRD 3 MRD 3 ... 0 ... 0 ... 0 ... 0 RET
CAL2H2 10 10 M1R 4 MRD 3 MRD 3 ... 0 ... 0 ... 0 ... 0 JP Z,U16
opc: operation code [hex]
L1,H1,U1,S1 means first operand direct number or address
L2,H2,U2,S2 means second operand direct number or address
L3,H3,U3,S3 means third operand direct number or address
H,L ... U16 high and low byte
U ... U8 unsigned byte
S ... S8 signed byte
T0 normal instruction duration [T] always 2 decimal digits
T1 instruction duration if condition not met [T] always 2 decimal digits
MC1++ Machine cycle first is type,second is duration [T] always 1 decimal digit
... unused
M1R M1 cycle
MRD memory read
MWR memory write
IOR IO read
IOW IO write
NON no external operation (internal computation)
INT interrupt cycle
mnem instruction text (mnemonic)
opc
用于指针数组中的地址mnemonic
用于选择正确的函数指针,操作数类型T0
和T1
用于指示时间(这对于粗略仿真来说已足够)MC1++
用于正确的MC时序(实现正确的硬件仿真和争用时间)以下是my Zilog Z80A complete instruction set with machine cycle timing链接供下载。随意使用(只是在某处提到我的昵称)。移植到此后,我终于能够100%通过 ZEXALL 测试。有关详细信息,请参阅 Writing a graphical Z80 emulator in C or C++ 。
答案 1 :(得分:2)
第一个建议,你不应该使用嵌套的switch语句,你应该使用函数指针数组,更快 - >更好的仿真,更好的代码,嵌套的开关也可能有点凌乱,这里有一些链接,你可以阅读更多有关这些阵列的信息
http://www.newty.de/fpt/fpt.html
http://www.multigesture.net/wp-content/uploads/mirror/zenogais/FunctionPointers.htm
第二个建议,是的,你可以用C#,Java,C ++来实现它,但是因为你想要你的每一个CPU循环所以你可以尽可能接近仿真 - 模拟目标架构的一个CPU周期,最少数量的对于当前架构的CPU周期,在这种情况下,OOP与我从人们听到/读取的内容不太一样。其中一个是性能,第二个是非常明显的,仿真,正如你可能已经注意到的,真正复杂的任务和在OOP中包装它可能是不必要的痛苦。
答案 2 :(得分:1)
这是一个非常酷的实现,使用NES模拟器的一些操作码:
http://bisqwit.iki.fi/jutut/kuvat/programming_examples/nesemu1/
以下是随附的YouTube视频,它们对正在发生的事情有更多解释
http://www.youtube.com/watch?v=y71lli8MS8s
它使用C ++模板和一些额外的C ++ 11功能。至于你选择的C ++或C是否适用于你,但它并不重要。如果你只是模仿一个游戏男孩,我怀疑速度将成为现代处理器的一个问题,所以尽量使用你喜欢的任何东西。