I created an emulator...
Moderator: Moderators
I created an emulator...
I created a NES emulator.
Target: YouTube.
Umm, what?
My goal was to produce an emulator that has as compact source code as possible, so that it -- the source code -- can be showcased within a 15 minutes of a YouTube video that is not too much sped up that it cannot be read without pausing.
Regular readers of this forum have already seen bits and pieces of it in my posts.
The finished* emulator is 940 lines long, and has the following features:
- Cycle accurate and memory access accurate CPU emulation with unofficial opcodes
- Cycle accurate PPU which passes a satisfying number of tests by Blargg (though not all)
- Accuracy-oriented emulation of the five-channel APU (actual accuracy not verified, but it sounds good)
- Additional FCEUX compatibility by preinitializing RAM in a certain manner
- iNES mappers 0, 1, 2, 3 and 7
- RGB colors synthesized through NTSC modem (modulation + demodulation), with subpixel-precision NTSC artifacts (color/luma leak, marching ants) (window resizing / scaling not implemented)
- Input is read from a Famtasia movie file (joypad/keyboard is ignored completely)
- Sound is produced through an external program / combination of external programs (synchronization may be an issue)
*) Finished, as in the version that I felt is good enough to publish on YouTube given the goals stated earier.
You can see the emulator here. In order to acquire a copy, you will have to type the source code as seen in the video, or look at the movie description and spot the URL.
- http://youtu.be/y71lli8MS8s -- Part 1/2: Programming
- http://youtu.be/XZWw745wPXY -- Part 2/2: Compiling & demonstration (replays a few TASes)
Part 2/2 is still uploading as I type this post, but it should be watchable in about 6 hours (at the turn of December 8th in UTC).
Screenshot (YouTube caps at 30 fps, so I used motion blur for fps reduction):
Target: YouTube.
Umm, what?
My goal was to produce an emulator that has as compact source code as possible, so that it -- the source code -- can be showcased within a 15 minutes of a YouTube video that is not too much sped up that it cannot be read without pausing.
Regular readers of this forum have already seen bits and pieces of it in my posts.
The finished* emulator is 940 lines long, and has the following features:
- Cycle accurate and memory access accurate CPU emulation with unofficial opcodes
- Cycle accurate PPU which passes a satisfying number of tests by Blargg (though not all)
- Accuracy-oriented emulation of the five-channel APU (actual accuracy not verified, but it sounds good)
- Additional FCEUX compatibility by preinitializing RAM in a certain manner
- iNES mappers 0, 1, 2, 3 and 7
- RGB colors synthesized through NTSC modem (modulation + demodulation), with subpixel-precision NTSC artifacts (color/luma leak, marching ants) (window resizing / scaling not implemented)
- Input is read from a Famtasia movie file (joypad/keyboard is ignored completely)
- Sound is produced through an external program / combination of external programs (synchronization may be an issue)
*) Finished, as in the version that I felt is good enough to publish on YouTube given the goals stated earier.
You can see the emulator here. In order to acquire a copy, you will have to type the source code as seen in the video, or look at the movie description and spot the URL.
- http://youtu.be/y71lli8MS8s -- Part 1/2: Programming
- http://youtu.be/XZWw745wPXY -- Part 2/2: Compiling & demonstration (replays a few TASes)
Part 2/2 is still uploading as I type this post, but it should be watchable in about 6 hours (at the turn of December 8th in UTC).
Screenshot (YouTube caps at 30 fps, so I used motion blur for fps reduction):
Last edited by Bisqwit on Wed Dec 07, 2011 10:56 am, edited 2 times in total.
-
- Posts: 36
- Joined: Wed Jul 04, 2007 8:40 am
- infiniteneslives
- Posts: 2104
- Joined: Mon Apr 04, 2011 11:49 am
- Location: WhereverIparkIt, USA
- Contact:
Very cool, sir. BTW what TAS movies/games depend on the RAM initialization state?
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi
I seem to remember that I could not get the Wizards & Warriors TAS to sync before I added the RAM initialization. It could have been caused by something else, but that's what I did that made the difference.thefox wrote:Very cool, sir. BTW what TAS movies/games depend on the RAM initialization state?
http://bisqwit.iki.fi/jutut/kuvat/progr ... nesemu1.cc
All I have to say is this, as I believe it speaks for itself (I hope you get my sarcasm -- and yes, I am well aware that you stated your goal was "to produce an emulator that has as compact source code as possible" -- I'm hearing you on FM):
EDIT: Oh, I guess I'll include another bit.
All I have to say is this, as I believe it speaks for itself (I hope you get my sarcasm -- and yes, I am well aware that you stated your goal was "to produce an emulator that has as compact source code as possible" -- I'm hearing you on FM):
Code: Select all
void Ins() // With template magic, the compiler will literally synthesize >256 different functions.
{
// 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.)
enum { o8 = op/8, o8m = 1 << (op%8) };
// Fetch op'th item from a bitstring encoded in a data-specific variant of base64,
// where each character transmits 8 bits of information rather than 6.
// This peculiar encoding was chosen to reduce the source code size.
// Enum temporaries are used in order to ensure compile-time evaluation.
#define t(s,code) { enum { \
i=o8m & (s[o8]>90 ? (130+" (),-089<>?BCFGHJLSVWZ[^hlmnxy|}"[s[o8]-94]) \
: (s[o8]-" (("[s[o8]/39])) }; if(i) { code; } }
/* Decode address operand */
t(" !", addr = 0xFFFA) // NMI vector location
t(" *", addr = 0xFFFC) // Reset vector location
t("! ,", addr = 0xFFFE) // Interrupt vector location
t("zy}z{y}zzy}zzy}zzy}zzy}zzy}zzy}z ", addr = RB(PC++))
t("2 yy2 yy2 yy2 yy2 XX2 XX2 yy2 yy ", d = X) // register index
t(" 62 62 62 62 om om 62 62 ", d = Y)
t("2 y 2 y 2 y 2 y 2 y 2 y 2 y 2 y ", addr=u8(addr+d); d=0; tick()) // add zeropage-index
t(" y z!y z y z y z y z y z y z y z ", addr=u8(addr); addr+=256*RB(PC++)) // absolute address
t("3 6 2 6 2 6 286 2 6 2 6 2 6 2 6 /", addr=RB(c=addr); addr+=256*RB(wrap(c,c+1)))// indirect w/ page wrap
t(" *Z *Z *Z *Z 6z *Z *Z ", Misfire(addr, addr+d)) // abs. load: extra misread when cross-page
t(" 4k 4k 4k 4k 6z 4k 4k ", RB(wrap(addr, addr+d)))// abs. store: always issue a misread
/* Load source operand */
t("aa__ff__ab__,4 ____ - ____ ", t &= A) // Many operations take A or X as operand. Some try in
t(" knnn 4 99 ", t &= X) // error to take both; the outcome is an AND operation.
t(" 9989 99 ", t &= Y) // sty,dey,iny,tya,cpy
t(" 4 ", t &= S) // tsx, las
t("!!!! !! !! !! ! !! !! !!/", t &= P.raw|pbits; c = t)// php, flag test/set/clear, interrupts
t("_^__dc___^__ ed__98 ", c = t; t = 0xFF) // save as second operand
t("vuwvzywvvuwvvuwv zy|zzywvzywv ", t &= RB(addr+d)) // memory operand
t(",2 ,2 ,2 ,2 -2 -2 -2 -2 ", t &= RB(PC++)) // immediate operand
/* Operations that mogrify memory operands directly */
t(" 88 ", P.V = t & 0x40; P.N = t & 0x80) // bit
t(" nink nnnk ", sb = P.C) // rol,rla, ror,rra,arr
t("nnnknnnk 0 ", P.C = t & 0x80) // rol,rla, asl,slo,[arr,anc]
t(" nnnknink ", P.C = t & 0x01) // lsr,sre, ror,rra,asr
t("ninknink ", t = (t << 1) | (sb * 0x01))
t(" nnnknnnk ", t = (t >> 1) | (sb * 0x80))
t(" ! kink ", t = u8(t - 1)) // dec,dex,dey,dcp
t(" ! khnk ", t = u8(t + 1)) // inc,inx,iny,isb
/* Store modified value (memory) */
t("kgnkkgnkkgnkkgnkzy|J kgnkkgnk ", WB(addr+d, t))
t(" q ", 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 */
t("rpstljstqjstrjst - - - -kjstkjst/", tick()) // nop,flag ops,inc,dec,shifts,stack,transregister,interrupts
/* Stack operations and unconditional jumps */
t(" ! ! ! ", tick(); t = Pop()) // pla,plp,rti
t(" ! ! ", RB(PC++); PC = Pop(); PC |= (Pop() << 8)) // rti,rts
t(" ! ", RB(PC++)) // rts
t("! ! /", d=PC+(op?-1:1); Push(d>>8); Push(d)) // jsr, interrupts
t("! ! 8 8 /", PC = addr) // jmp, jsr, interrupts
t("!! ! /", Push(t)) // pha, php, interrupts
/* Bitmasks */
t("! !! !! !! !! ! !! !! !!/", t = 1)
t(" ! ! !! !! ", t <<= 1)
t("! ! ! !! !! ! ! !/", t <<= 2)
t(" ! ! ! ! ! ", t <<= 4)
t(" ! ! ! !____ ", t = u8(~t)) // sbc, isb, clear flag
t("`^__ ! ! !/", t = c | t) // ora, slo, set flag
t(" !!dc`_ !! ! ! !! !! ! ", t = c & t) // and, bit, rla, clear/test flag
t(" _^__ ", t = c ^ t) // eor, sre
/* Conditional branches */
t(" ! ! ! ! ", if(t) { tick(); Misfire(PC, addr = s8(addr) + PC); PC=addr; })
t(" ! ! ! ! ", if(!t) { tick(); Misfire(PC, addr = s8(addr) + PC); PC=addr; })
/* Addition and subtraction */
t(" _^__ ____ ", c = t; t += A + P.C; P.V = (c^t) & (A^t) & 0x80; P.C = t & 0x100)
t(" ed__98 ", t = c - t; P.C = ~t & 0x100) // cmp,cpx,cpy, dcp, sbx
/* Store modified value (register) */
t("aa__aa__aa__ab__ 4 !____ ____ ", A = t)
t(" nnnn 4 ! ", X = t) // ldx, dex, tax, inx, tsx,lax,las,sbx
t(" ! 9988 ! ", Y = t) // ldy, dey, tay, iny
t(" 4 0 ", S = t) // txs, las, shs
t("! ! ! !! ! ! ! ! !/", P.raw = t & ~0x30) // plp, rti, flag set/clear
/* Generic status flag updates */
t("wwwvwwwvwwwvwxwv 5 !}}||{}wv{{wv ", P.N = t & 0x80)
t("wwwv||wvwwwvwxwv 5 !}}||{}wv{{wv ", P.Z = u8(t) == 0)
t(" 0 ", P.V = (((t >> 5)+1)&2)) // [arr]
/* All implemented opcodes are cycle-accurate and memory-access-accurate.
* [] means that this particular separate rule exists only to provide the indicated unofficial opcode(s).
*/
}
Code: Select all
void Op()
{
/* Check the state of NMI flag */
bool nmi_now = nmi;
unsigned op = RB(PC++);
if(reset) { op=0x101; }
else if(nmi_now && !nmi_edge_detected) { op=0x100; nmi_edge_detected = true; }
else if(intr && !P.I) { op=0x102; }
if(!nmi_now) nmi_edge_detected=false;
// Define function pointers for each opcode (00..FF) and each interrupt (100,101,102)
#define c(n) Ins<0x##n>,Ins<0x##n+1>,
#define o(n) c(n)c(n+2)c(n+4)c(n+6)
static void(*const i[0x108])() =
{
o(00)o(08)o(10)o(18)o(20)o(28)o(30)o(38)
o(40)o(48)o(50)o(58)o(60)o(68)o(70)o(78)
o(80)o(88)o(90)o(98)o(A0)o(A8)o(B0)o(B8)
o(C0)o(C8)o(D0)o(D8)o(E0)o(E8)o(F0)o(F8) o(100)
};
#undef o
#undef c
i[op]();
reset = false;
}
2006 winner entries are finally available? Oh goodie! Thanks for telling.kyuusaku wrote:http://www.ioccc.org/ :P
EDIT: Oh, and in all honesty, I was partially inspired by the 2005 winner that does a 6502 emulator (Commodore PET). But both algorithmically and structurally my emulator is very different. Mr. Sykes's emulator used addressing mode & operation tables and switch-cases, albeit constructed from ?: operators. I studied his submission in great detail some time in 2009, deobfuscating it entirely. I doubt his approach would have worked well with my goals of accuracy and feature-completeness.