/* NES V-Blank / NMI / CPU timing skeleton, platform-independent C++11 code * Written by and copyright 2011 Joel Yliluoma - http://iki.fi/bisqwit/ */ #include #include #include #include #include #ifndef NMI_BUFFER_LENGTH #define NMI_BUFFER_LENGTH 3 #endif #ifndef RB_TICKS_BEFORE #define RB_TICKS_BEFORE 3 #endif #ifndef WB_TICKS_BEFORE #define WB_TICKS_BEFORE 3 #endif #ifndef VBL_CLEAR_DELAY #define VBL_CLEAR_DELAY 1 #endif #ifndef VBL_SET_DELAY #define VBL_SET_DELAY 1 #endif // Integer types typedef unsigned char u8; typedef signed char s8; typedef unsigned short u16; typedef unsigned int u32; // Coroutine utilities #define cobegin static unsigned state=0; switch(state) { default: #define coyield do { state=__LINE__;return;case __LINE__:; } while(0) #define coend } state=0 // Bitfield utilities template struct RegBit { T data; RegBit& operator=(const RegBit& v) { return *this = (unsigned)v; } template RegBit& operator=(T2 val) { if(nbits == 1) data = (data & ~mask()) | (!!val << bitno); else data = (data & ~mask()) | ((val & mask(0)) << bitno); return *this; } operator unsigned() const { return (data >> bitno) & mask(0); } static inline constexpr unsigned mask(unsigned shift=bitno) { return ((1 << nbits)-1) << shift; } RegBit& operator++ () { return *this = *this + 1; } RegBit& operator^= (unsigned v) { return *this = *this ^ v; } }; namespace Pak { std::vector ROM; unsigned char* banks[4] = { 0,0,0,0 }; u8 Read(unsigned addr) { return banks[ (addr >> 14) & 3] [addr & 0x3FFF]; } void Write(unsigned addr, u8 value) { // Treat data from Blargg's test ROMs if(addr >= 0x6004 && addr < 0x6200 && value) { fputc(value, stderr); } if(addr == 0x6000) { fprintf(stderr, "[6000] <- 0x%02X\n", value); if(value < 0x80) exit(value); } if(addr == 0x6001) fprintf(stderr, "[6001] <- 0x%02X\n", value); if(addr == 0x6002) fprintf(stderr, "[6002] <- 0x%02X\n", value); if(addr == 0x6003) fprintf(stderr, "[6003] <- 0x%02X\n", value); } void Init() { for(unsigned v=0; v<4; ++v) banks[v] = &ROM[0]; banks[3] = &ROM[ROM.size()-0x4000]; } } namespace CPU { bool reset=true, nmi[NMI_BUFFER_LENGTH]={0}, nmi_edge_detected=false; // IRQ functions ignored } namespace PPU { union // Register file { u32 all; // Reg0 RegBit< 0,8,u32> reg0; RegBit< 0,2,u32> BaseNTA; RegBit< 2,1,u32> Inc; RegBit< 3,1,u32> SPaddr; RegBit< 4,1,u32> BGaddr; RegBit< 5,1,u32> SPsize; RegBit< 6,1,u32> SlaveFlag; RegBit< 7,1,u32> NMIenabled; // Reg1 RegBit< 8,8,u32> reg1; RegBit< 8,1,u32> Grayscale; RegBit< 9,1,u32> ShowBG8; RegBit<10,1,u32> ShowSP8; RegBit<11,1,u32> ShowBG; RegBit<12,1,u32> ShowSP; RegBit<13,3,u32> EmpRGB; // Reg2 RegBit<16,8,u32> reg2; RegBit<21,1,u32> SPoverflow; RegBit<22,1,u32> SP0hit; RegBit<23,1,u32> InVBlank; } reg; // Memory functions ignored int scanline=-2, x, slend, VBlankState=0; bool even_odd_toggle=false; static void (*Write[8])(u8) = { [] (u8 v) { reg.reg0 = v; }, [] (u8 v) { reg.reg1 = v; }, [] (u8 v) { }, [] (u8 v) { }, // OAM function ignored [] (u8 v) { }, // OAM function ignored [] (u8 v) { }, // Video function ignored [] (u8 v) { }, // Video function ignored [] (u8 v) { } // Memory function ignored // Latch/open bus functions ignored }; static u8 (*Read[8])() = { [] () -> u8 { return 0; }, [] () -> u8 { return 0; }, [] () -> u8 { u8 retval = reg.reg2; if(VBlankState > 0) VBlankState = 0; // Cancel the oncoming setting of VBlank flag reg.reg2 = reg.reg2 & 0x7F; //printf("Reading back 2002 as %02X, at scanline=%d, x=%u\n", retval, scanline,x); return retval; }, [] () -> u8 { return 0; }, [] () -> u8 { return 0; }, // OAM function ignored [] () -> u8 { return 0; }, [] () -> u8 { return 0; }, [] () -> u8 { return 0; } // Memory function ignored }; void tick() { // Push NMI state CPU::nmi[0] = reg.InVBlank && reg.NMIenabled; for(unsigned a=sizeof(CPU::nmi); a-- > 1; ) CPU::nmi[a] = CPU::nmi[a-1]; // Delayed-update VBlank flag if(VBlankState == -VBL_CLEAR_DELAY) { reg.reg2 = 0; VBlankState = 0; }// clear vblank if(VBlankState == VBL_SET_DELAY) { reg.InVBlank = 1; VBlankState = 0; } if(VBlankState > 0) ++VBlankState; if(VBlankState < 0) --VBlankState; cobegin; // Scanline/Cycle loop for(scanline=241; ; scanline = scanline<260 ? scanline+1 : -1) { slend = 341; for(x = 0; x < slend; ++x) { if(x == 0 && scanline == 241) { VBlankState = 1; // Enter vblank at the beginning of first vblank scanline } if(x == 340 && scanline == 260) { reg.SP0hit = 0; reg.SPoverflow = 0; // Reset OAM flags } if(x == 0 && scanline == -1) { // Exit vblank at the beginning of the pre-render scanline VBlankState = -1; even_odd_toggle = !even_odd_toggle; // Begin rendering. } // The length of the pre-render scanline is decided at cycle 338 of that scanline. if(reg.ShowBG && x == 338 && scanline == -1 && even_odd_toggle) slend = 340; // Memory functions ignored // Rendering functions ignored // OAM functions ignored coyield; } } coend; } } namespace CPU { u16 PC=0xC000; u8 RAM[0x800], A=0,X=0,Y=0,S=0x00; union { u8 raw; RegBit<0> C; // carry RegBit<1> Z; // zero RegBit<2> I; // interrupt enable/disable RegBit<3> D; // decimal mode (unsupported on NES) // 4,5 (0x10,0x20) don't exist RegBit<6> V; // overflow RegBit<7> N; // negative } P; void tick() { // PPU will not receive clock while reset is being signalled if(reset) return; // PPU clock: 3 times the CPU rate PPU::tick(); PPU::tick(); PPU::tick(); } u8 RB(u16 addr) { if(RB_TICKS_BEFORE >= 1) PPU::tick(); if(RB_TICKS_BEFORE >= 2) PPU::tick(); if(RB_TICKS_BEFORE >= 3) PPU::tick(); u8 r; /**/ if(addr < 0x2000) r = RAM[addr & 0x7FF]; else if(addr < 0x4000) r = PPU::Read[addr & 7](); else if(addr < 0x4018) r = 0; // I/O function ignored else r = Pak::Read(addr); if(RB_TICKS_BEFORE < 3) PPU::tick(); if(RB_TICKS_BEFORE < 2) PPU::tick(); if(RB_TICKS_BEFORE < 1) PPU::tick(); return r; } void WB(u16 addr, u8 v) { if(WB_TICKS_BEFORE >= 1) PPU::tick(); if(WB_TICKS_BEFORE >= 2) PPU::tick(); if(WB_TICKS_BEFORE >= 3) PPU::tick(); // Memory writes are turned into reads while reset is being signalled if(reset) { /* Read ignored */ } else if(addr < 0x2000) RAM[addr & 0x7FF] = v; else if(addr < 0x4000) PPU::Write[addr & 7](v); else if(addr == 0x4014) // OAM DMA, required for timing for(unsigned b=0; b<256; ++b) WB(0x2004, RB((v&7)*0x0100+b)); else if(addr < 0x4018) {} // I/O function ignored else Pak::Write(addr, v); if(WB_TICKS_BEFORE < 3) PPU::tick(); if(WB_TICKS_BEFORE < 2) PPU::tick(); if(WB_TICKS_BEFORE < 1) PPU::tick(); } u16 wrap(u16 oldaddr, u16 newaddr) { return (oldaddr & 0xFF00) + u8(newaddr); } u16 RW(u16 addr) { unsigned q=RB(addr); return q + 256*RB(wrap(addr,addr+1)); } void Misfire(u16 old, u16 addr) { u16 q = wrap(old, addr); if(q != addr) RB(q); } u8 PB() { return RB(PC++); } u8 Pop() { return RB(0x100 | u8(++S)); } void Push(u8 v) { WB(0x100 | u8(S--), v); } void PushW(u16 v) { Push(v>>8); Push(v); } template void ins() { // Note: op 0x100 means "NMI", 0x101 means "Reset", 0x102 means "IRQ". They are implemented in terms of "BRK". // User is responsible for ensuring that WB() will not store into memory while Reset is being processed. unsigned addr=0, d=0, t=0xFF, c=0, sb=0, pbits = op<0x100 ? 0x30 : 0x20; // Define the opcode decoding matrix, which decides which micro-operations constitute // any particular opcode. (Note: The PLA of 6502 works on a slightly different principle.) #define i(str,code) { enum { i=str[op/6], b=i-":) .55;;"[i/16] }; if(b & (1<<(op%6))) { code; } } /* Decode address operand */ i(" E ", addr = 0xFFFA); // NMI vector location i(" U ", addr = 0xFFFC); // Reset vector location i("! !", addr = 0xFFFE); // Interrupt vector location i("u1zzujDwzfjykzjyu1zzufDwzfjykzjyu1zzufDwzfD ", addr = PB()); i("8 wkf k1j0 Dw8 wkf k X0 1A8 wkf k1D ", d = X); // register index i(" U18 Cc sU0 U18 CfA sgm U18 Cc ", d = Y); i("8 w c k1U0 D 8 w c k1U0 D 8 w c k1 ", tick(); addr = u8(addr+d); d=0; );// add zeropage-index i(" D u5 w cD k1Uy D u1 w cD k1Uy D u1 w cD ", addr = u8(addr) + 256 * PB()); // absolute address i("9 U1 c C U0 s 8 V1 c C U0 s 8 U1 c C k!", addr = RW(addr)); // indirect, with page wrap i(" U m 061 6UA U m sUy U m 061 ", Misfire(addr, addr+d)); // read:misread when cross-page i(" 161 AUA k m 161 CcD 161 AUA ", RB(wrap(addr, addr+d)));// store:misread always /* Load source operand */ i("iueeeufjeeefjeee2k ceeee8 3 eeeee0 ", t &= A); // Many operations take A or X as operand. Some try in i(" UAnAnA k 2F2 ", t &= X); // error to take both; the outcome is an AND operation. i(" 2F2E2! F2! ", t &= Y); // sty,dey,iny,tya,cpy i(" 1 ", t &= S); // tsx, las i("!2E ! !2 2E E ! ! 2E E ! !2k!", t &= P.raw|pbits; c = t);// brk, php, flag test/set/clear i("e8eeeefgeeeeWeee vefee4!2 ", c = t; t = 0xFF); // save as second operand i("e1ivefDwifiuUviue1ive1 Uykvjyu1jvefDwifC ", t &= RB(addr+d)); // memory operand i("2c EU0 !8 2c IU0 E!8 3c IU0 ", t &= PB()); // immediate operand /* Operations that mogrify memory operands directly */ i(" !2 ", P.V = t & 0x40; P.N = t & 0x80); // bit i(" kAlAXA AnAn61 ", sb = P.C); // rol,rla, ror,rra,arr i("AnAn6nAnAXA U ", P.C = t & 0x80); // rol,rla, asl,slo,[arr,anc] i(" nAnAmAHAn61 ", P.C = t & 0x01); // lsr,sre, ror,rra,asr i("AHAn6nAlAXA ", t = (t << 1) | (sb << 0)); i(" nAnAmAnAn61 ", t = (t >> 1) | (sb << 7)); i(" E 6HAn61 ", t = u8(t - 1)); // dec,dex,dey,dcp i(" 2 UQkAXA ", t = u8(t + 1)); // inc,inx,iny,isb /* Store modified value (memory) */ i("61An6XAkAXAm nAm61An6fDwy90 61An6XAkAXA ", WB(addr+d, t)); i(" UB ", WB(wrap(addr, addr+d), t &= ((addr+d) >> 8))); // [shx,shy,shs,sha?] /* Some operations used up one clock cycle that we did not account for yet */ i("NLBrRbQlQrBq3nRrNLArR1E! I 3 E!6LArRXQlQrx!", tick()); // nop,flag ops,inc,dec,shifts,stack,transregister,interrupts /* Stack operations and unconditional jumps */ i(" E E 2 ", tick(); t = Pop()); // pla,plp,rti i(" E ! ", PB(); PC = Pop(); PC |= (Pop() << 8)); // rti,rts i(" ! ", PB()); // rts i("! 2 k!", PushW(PC + (op?-1:1)));// jsr, interrupts (brk,reset,intr,nmi) i("! 2 E ! k!", PC = addr); // jmp, jsr, interrupts i("!2 ! k!", Push(t)); // pha, php, interrupts /* Bitmasks */ i("! E ! !2 2E E ! ! 2E E ! !2k!", t = 1); i(" E ! E ! !2 ", t <<= 1); i("! E ! 2E E ! E ! 2k!", t <<= 2); i(" E ! 2 E E ", t <<= 4); i(" ! E E !ceeee8 ", t = u8(~t)); // sbc, isb, clear flag i("f8eee0 2 ! 2k!", t = c | t); // ora, slo, set flag i(" E !cfgfe8 2E E ! 2E E ! ! ", t = c & t); // and, bit, rla, clear/test flag i(" UeWeee ", t = c ^ t); // eor, sre /* Conditional branches */ i(" ! E 2 ! ", if(t) { tick(); Misfire(PC, addr = s8(addr) + PC); PC=addr; }); i(" E 2 ! E ", if(!t) { tick(); Misfire(PC, addr = s8(addr) + PC); PC=addr; }); /* Addition and subtraction */ i(" e8eee0 ceeee8 ", c = t; t += A + P.C; P.V = (c^t) & (A^t) & 0x80; P.C = t & 0x100); i(" vefee4!2 ", t = c - t; P.C = ~t & 0x100); // cmp,cpx,cpy, dcp, sbx /* Store modified value (register) */ i("iueeeuefeeefieeeiyeee0 1 2Ueeeee ceeee8 ", A = t); i(" nAnAn k E ", X = t); // ldx, dex, tax, inx, tsx,lax,las,sbx i(" E E2F !2 2 ", Y = t); // ldy, dey, tay, iny i(" k 0 ", S = t); // txs, las, shs i("! ! E 2E E ! E ! 2k!", P.raw = t & ~0x30); // plp, rti, flag set/clear /* Generic status flag updates */ i("ivivevivifiviviuizive1E1 2kzzvjzvzjvejzyifC ", P.N = t & 0x80); i("ivivevjzifiviviuizive1E1 2kzzvjzvzjvejzyifC ", P.Z = u8(t) == 0); i(" U ", P.V = (((t >> 5)+1)&2)); // [arr] #undef i /* All implemented opcodes are cycle-accurate and memory-access-accurate. * Cycle-accuracy depends on memory access functions RB and WB executing tick(). * [] in the comments indicates that this particular separate rule * exists only because of the indicated unofficial opcode(s). * Note: Unofficial NOP opcodes may do memory accesses that * they are not supposed to do. There was no comprehensive test. * Note: All KIL/JAM opcodes do (some) actual work instead of halting. * Note: Reset,Interrupt,NMI are implemented as instructions 101,102,100 respectively. * They are variants of BRK. This is the same as what real 6502 does. * Note that for Reset to work properly, you will have to disable RAM writes * while the instruction is in progress. */ } void Op() { // Pull NMI state bool nmi_now = nmi[ sizeof(nmi)-1 ]; // Read opcode unsigned op = PB(); if(reset) { op=0x101; } else if(nmi_now && !nmi_edge_detected) { op=0x100; nmi_edge_detected = true; } // IRQ functions ignored if(!nmi_now) nmi_edge_detected=false; #define c(o) case 0x##o: ins<0x##o>(); break; #define i(o) c(o+0) c(o+1) c(o+2) c(o+3) c(o+4) c(o+5) c(o+6) c(o+7) switch(op) { i(00)i(08)i(10)i(18) i(20)i(28)i(30)i(38) i(40)i(48)i(50)i(58) i(60)i(68)i(70)i(78) i(80)i(88)i(90)i(98) i(A0)i(A8)i(B0)i(B8) i(C0)i(C8)i(D0)i(D8) i(E0)i(E8)i(F0)i(F8) i(100) } #undef i #undef c reset = false; } } int main(int argc, char** argv) { FILE* fp = fopen(argv[1], "rb"); assert(fgetc(fp)=='N' && fgetc(fp)=='E' && fgetc(fp)=='S' && fgetc(fp)=='\32'); u8 rom16count = fgetc(fp); fgetc(fp);fgetc(fp);fgetc(fp); // VROM, mirroring, mapper data ignored fgetc(fp);fgetc(fp);fgetc(fp);fgetc(fp);fgetc(fp);fgetc(fp);fgetc(fp);fgetc(fp); Pak::ROM.resize(rom16count * 0x4000); fread(&Pak::ROM[0], rom16count, 0x4000, fp); // VROM ignored // Mirroring ignored fclose(fp); PPU::reg.all = 0; Pak::Init(); for(;;) CPU::Op(); }