続CPUエミュレーションを速くしたい - 静的バイナリ変換のハイブリッドシステム
前回の続きです。
どうやって、デコンパイルで生成したJavaコード実行と、エミュレーション実行を、都合よく切り替えるか、という
お話です。
以下が簡単化した擬似コードです。
public void Execute() { while (clk > 0) { int beforeClk = clk; デコンパイルしたコード列 if (beforeClk == clk) { エミュレーション実行(); } } }
Executeメソッドが、6502コアとなるメソッドです。
「デコンパイルしたコード列」という部分に、6502コードからJavaへ変換したソースコードが
埋め込まれているという前提です。
clkが6502コアを駆動する残りクロック数を表す変数です。
clkが0より大きい間、6502コアを駆動します。
ポイントとなるのが、デコンパイルしたコード列を実行する前に、clkの値を別の変数へ保存しておきます。
それがbeforeClkという変数です。
「デコンパイルしたコード列」を実行した結果、通常はclkの値が減少します。
もし、clkが減少しなかった場合は、それはデコンパイルしたコードでは実行が不可能なパターンだったということになります。
そこで、clkが減少していない場合、つまりbeforeClkがclkと同値だった場合は、エミュレーション実行に切り替えます。
この仕組みによって、自己書換などのパターンでも、うまく実行することができるようになります。
その部分はエミュレーションによって実行されるようになります。
また、デコンパイルしたコード列で、元の6502のコードをすべてカバーしていなくても、エミュレーション実行が
カバーしてくれるようになるため、デコンパイルしたコード列は部分的でも良いというメリットが生まれます。
ファミコンソフトの6502コード中の、メインループとなる何度も繰り返し実行される部分のみ、デコンパイルをかけておき
高速に実行するということが可能になります。
メインループ以外の実行回数が少ない部分は、エミュレーション実行とすることで、コードサイズの増加を防ぐことができます。
長いため一部省略していますが、具体的には以下のようなコードになります。
class NES { protected int PC; private int CLK; private int A; private int T; private int X; private int Y; private int SP; private int C; private int I; private int D; private int B; private int R; private int V; protected final int[] wram; protected final int[] prgRom; private final PPU ppu; private final PAD pad; private final NESApu nesApu; private final void ExecuteEmulation() { while (clk > 0) { switch (LdMemB_PRG(pc++)) { 6502エミュレーションコア } } } public final void Execute() { while (CLK > 0) { int beforeClk = CLK; // デコンパイルしたコード列 switch (pc) { case 0x8000: clk -= 2; I = SFLG.I; case 0x8001: clk -= 2; D = 0; case 0x8002: clk -= 2; a = 16; case 0x8004: clk -= 4; ppu.SetControlRegister1(a); case 0x8007: clk -= 2; X = 255; case 0x8009: clk -= 2; SP = X; case 0x800A: clk -= 4; t = a = ppu.GetStatusRegister(); case 0x800D: clk -= 2; if ((t & 0x80) == 0) { clk--; pc = 0x800A; break; } case 0x800F: clk -= 4; t = a = ppu.GetStatusRegister(); case 0x8012: clk -= 2; if ((t & 0x80) == 0) { clk--; pc = 0x800F; break; } case 0x8014: clk -= 2; Y = 254; case 0x8016: clk -= 2; X = 5; case 0x8018: clk -= 4; a = LdMemB(2007 + X); case 0x801B: clk -= 2; t = a - 10; c = (~t >> 8) & 1; t &= 0xFF; case 0x801D: clk -= 2; if (c != 0) { clk--; pc = 0x802B; break; } case 0x801F: clk -= 2; t = X = (X - 1) & 0xFF; case 0x8020: clk -= 2; if ((t & 0x80) == 0) { clk--; pc = 0x8018; break; } case 0x8022: clk -= 4; a = wram[2047 & 0x7FF]; case 0x8025: clk -= 2; t = a - 165; c = (~t >> 8) & 1; t &= 0xFF; case 0x8027: clk -= 2; if (t > 0) { clk--; pc = 0x802B; break; } case 0x8029: clk -= 2; t = Y = 214; case 0x802B: clk -= 6; PushStackW(0x802B + 2); pc = 0x90CC; break; case 0x802E: clk -= 4; nesApu.writeReg(0x4011 , a); case 0x8031: clk -= 4; wram[1904 & 0x7FF] = a; case 0x8034: clk -= 2; a = 165; case 0x8036: clk -= 4; wram[2047 & 0x7FF] = a; case 0x8039: clk -= 4; wram[1959 & 0x7FF] = a; case 0x803C: clk -= 2; a = 15; case 0x803E: clk -= 4; nesApu.writeReg(0x4015 , a); case 0x8041: clk -= 2; t = a = 6; case 0x8043: clk -= 4; ppu.SetControlRegister2(a); case 0x8046: clk -= 6; PushStackW(0x8046 + 2); pc = 0x8220; break; case 0x8049: clk -= 6; PushStackW(0x8049 + 2); pc = 0x8E19; break; case 0x804C: clk -= 6; t = (wram[1908 & 0x7FF] + 1) & 0xFF; wram[1908 & 0x7FF] = t; case 0x804F: clk -= 4; a = wram[1912 & 0x7FF]; case 0x8052: clk -= 2; t = a |= 128; case 0x8054: clk -= 6; PushStackW(0x8054 + 2); pc = 0x8EED; break; case 0x8057: clk -= 3; pc = 0x8057; break; case 0x805A: clk -= 6; a |= LdMemB(LdWramW((164 + X) & 0xFF)); case 0x805C: clk -= 2; Y = (Y + 1) & 0xFF; case 0x805D: clk -= 4; t = X - wram[16 & 0x7FF]; c = (~t >> 8) & 1; case 0x8060: clk -= 6; t = a ^= LdMemB(LdWramW((65 + X) & 0xFF)); case 0x8062: clk -= 3; pc = 0x3C34; break; /* 中略 */ default: break translation_loop; } if (beforeClk == CLK) { ExecuteEmulation(); } } } }
また、Executeメソッド内のデコンパイルによって作られたコードの脇に、6502の逆アセンブル結果を
くっつけたものを載せておきます。
6502のコードがどのようにJavaに変換されたのか分かると思います。
public final void Execute() { while (CLK > 0) { int beforeClk = CLK; // デコンパイルしたコード列 switch (pc) { case 0x8000: clk -= 2; I = SFLG.I; // 8000 : 78 sei ; case 0x8001: clk -= 2; D = 0; // 8001 : D8 cld ; case 0x8002: clk -= 2; a = 16; // 8002 : A9 10 lda #$10 ; case 0x8004: clk -= 4; ppu.SetControlRegister1(a); // 8004 : 8D 00 20 sta $2000 ;PPU 制御Reg.#1 case 0x8007: clk -= 2; X = 255; // 8007 : A2 FF ldx #$FF ; case 0x8009: clk -= 2; SP = X; // 8009 : 9A txs ; case 0x800A: clk -= 4; t = a = ppu.GetStatusRegister(); // 800A : AD 02 20 lda $2002 ;PPU Status Reg. case 0x800D: clk -= 2; if ((t & 0x80) == 0) { clk--; pc = 0x800A; break; } // 800D : 10 FB bpl $800A ; case 0x800F: clk -= 4; t = a = ppu.GetStatusRegister(); // 800F : AD 02 20 lda $2002 ;PPU Status Reg. case 0x8012: clk -= 2; if ((t & 0x80) == 0) { clk--; pc = 0x800F; break; } // 8012 : 10 FB bpl $800F ; case 0x8014: clk -= 2; Y = 254; // 8014 : A0 FE ldy #$FE ; case 0x8016: clk -= 2; X = 5; // 8016 : A2 05 ldx #$05 ; case 0x8018: clk -= 4; a = LdMemB(2007 + X); // 8018 : BD D7 07 lda $07D7,x ; case 0x801B: clk -= 2; t = a - 10; c = (~t >> 8) & 1; t &= 0xFF; // 801B : C9 0A cmp #$0A ; case 0x801D: clk -= 2; if (c != 0) { clk--; pc = 0x802B; break; } // 801D : B0 0C bcs $802B ; case 0x801F: clk -= 2; t = X = (X - 1) & 0xFF; // 801F : CA dex ; case 0x8020: clk -= 2; if ((t & 0x80) == 0) { clk--; pc = 0x8018; break; } // 8020 : 10 F6 bpl $8018 ; case 0x8022: clk -= 4; a = wram[2047 & 0x7FF]; // 8022 : AD FF 07 lda $07FF ; case 0x8025: clk -= 2; t = a - 165; c = (~t >> 8) & 1; t &= 0xFF; // 8025 : C9 A5 cmp #$A5 ; case 0x8027: clk -= 2; if (t > 0) { clk--; pc = 0x802B; break; } // 8027 : D0 02 bne $802B ; case 0x8029: clk -= 2; t = Y = 214; // 8029 : A0 D6 ldy #$D6 ; case 0x802B: clk -= 6; PushStackW(0x802B + 2); pc = 0x90CC; break; // 802B : 20 CC 90 jsr $90CC ; case 0x802E: clk -= 4; nesApu.writeReg(0x4011 , a); // 802E : 8D 11 40 sta $4011 ;DPCM 音量Reg. case 0x8031: clk -= 4; wram[1904 & 0x7FF] = a; // 8031 : 8D 70 07 sta $0770 ; case 0x8034: clk -= 2; a = 165; // 8034 : A9 A5 lda #$A5 ; case 0x8036: clk -= 4; wram[2047 & 0x7FF] = a; // 8036 : 8D FF 07 sta $07FF ; case 0x8039: clk -= 4; wram[1959 & 0x7FF] = a; // 8039 : 8D A7 07 sta $07A7 ; case 0x803C: clk -= 2; a = 15; // 803C : A9 0F lda #$0F ; case 0x803E: clk -= 4; nesApu.writeReg(0x4015 , a); // 803E : 8D 15 40 sta $4015 ;SoundChannel 制御Reg. case 0x8041: clk -= 2; t = a = 6; // 8041 : A9 06 lda #$06 ; case 0x8043: clk -= 4; ppu.SetControlRegister2(a); // 8043 : 8D 01 20 sta $2001 ;PPU 制御Reg.#2 case 0x8046: clk -= 6; PushStackW(0x8046 + 2); pc = 0x8220; break; // 8046 : 20 20 82 jsr $8220 ; case 0x8049: clk -= 6; PushStackW(0x8049 + 2); pc = 0x8E19; break; // 8049 : 20 19 8E jsr $8E19 ; case 0x804C: clk -= 6; t = (wram[1908 & 0x7FF] + 1) & 0xFF; wram[1908 & 0x7FF] = t; // 804C : EE 74 07 inc $0774 ; case 0x804F: clk -= 4; a = wram[1912 & 0x7FF]; // 804F : AD 78 07 lda $0778 ; case 0x8052: clk -= 2; t = a |= 128; // 8052 : 09 80 ora #$80 ; case 0x8054: clk -= 6; PushStackW(0x8054 + 2); pc = 0x8EED; break; // 8054 : 20 ED 8E jsr $8EED ; case 0x8057: clk -= 3; pc = 0x8057; break; // 8057 : 4C 57 80 jmp $8057 ; case 0x805A: clk -= 6; a |= LdMemB(LdWramW((164 + X) & 0xFF)); // 805A : 01 A4 ora ($A4,x) ; case 0x805C: clk -= 2; Y = (Y + 1) & 0xFF; // 805C : C8 iny ; case 0x805D: clk -= 4; t = X - wram[16 & 0x7FF]; c = (~t >> 8) & 1; // 805D : EC 10 00 cpx $0010 ; case 0x8060: clk -= 6; t = a ^= LdMemB(LdWramW((65 + X) & 0xFF)); // 8060 : 41 41 eor ($41,x) ; case 0x8062: clk -= 3; pc = 0x3C34; break; // 8062 : 4C 34 3C jmp $3C34 ; /* 中略 */ default: break translation_loop; } if (beforeClk == CLK) { ExecuteEmulation(); } } }