I created an emulator...

Discuss emulation of the Nintendo Entertainment System and Famicom.

Moderator: Moderators

Bisqwit
Posts: 248
Joined: Fri Oct 14, 2011 1:09 am

I created an emulator...

Post by Bisqwit » Wed Dec 07, 2011 9:40 am

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):

Image
Last edited by Bisqwit on Wed Dec 07, 2011 10:56 am, edited 2 times in total.

User avatar
MottZilla
Posts: 2835
Joined: Wed Dec 06, 2006 8:18 pm

Post by MottZilla » Wed Dec 07, 2011 10:24 am

Interesting. I will have to see Part 2 when it's uploaded.

Hamburgler
Posts: 36
Joined: Wed Jul 04, 2007 8:40 am

Post by Hamburgler » Wed Dec 07, 2011 11:51 am

That's a very entertaining video! Much better than the source code credit scroll I was expecting. The on-screen explanations are a very nice touch.

User avatar
infiniteneslives
Posts: 2102
Joined: Mon Apr 04, 2011 11:49 am
Location: WhereverIparkIt, USA
Contact:

Post by infiniteneslives » Wed Dec 07, 2011 3:26 pm

This is VERY impressive. I would have never imagined it could be done in under 1000 lines of code. It's pretty memorizing to watch makes it LOOK easy and fun.

Nice work!

User avatar
miker00lz
Posts: 235
Joined: Thu Sep 23, 2010 7:28 pm

Post by miker00lz » Wed Dec 07, 2011 5:07 pm

bad ass, Bisqwit! this is pro.

User avatar
thefox
Posts: 3141
Joined: Mon Jan 03, 2005 10:36 am
Location: Tampere, Finland
Contact:

Post by thefox » Thu Dec 08, 2011 12:50 am

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

Bisqwit
Posts: 248
Joined: Fri Oct 14, 2011 1:09 am

Post by Bisqwit » Thu Dec 08, 2011 12:59 am

thefox wrote:Very cool, sir. BTW what TAS movies/games depend on the RAM initialization state?
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.

User avatar
Dwedit
Posts: 4408
Joined: Fri Nov 19, 2004 7:35 pm
Contact:

Post by Dwedit » Thu Dec 08, 2011 1:13 am

Metroid refuses to boot if the CHR-RAM is really dirty. For a while, FCEUX was not initializing CHR-RAM to anything, so Metroid would often refuse to boot.
Here come the fortune cookies! Here come the fortune cookies! They're wearing paper hats!

User avatar
koitsu
Posts: 4218
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Post by koitsu » Thu Dec 08, 2011 1:20 am

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):

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).
         */
    }
EDIT: Oh, I guess I'll include another bit.

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;
    }

User avatar
Zepper
Formerly Fx3
Posts: 3223
Joined: Fri Nov 12, 2004 4:59 pm
Location: Brazil
Contact:

Post by Zepper » Thu Dec 08, 2011 3:14 am

Image
I know some of you don't like such images, but it's just a bit of humor, needless to say, eh?

User avatar
kyuusaku
Posts: 1665
Joined: Mon Sep 27, 2004 2:13 pm

Post by kyuusaku » Thu Dec 08, 2011 1:47 pm


Bisqwit
Posts: 248
Joined: Fri Oct 14, 2011 1:09 am

Post by Bisqwit » Thu Dec 08, 2011 2:17 pm

kyuusaku wrote:http://www.ioccc.org/ :P
2006 winner entries are finally available? Oh goodie! Thanks for telling.

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.

sleepy9090
Posts: 81
Joined: Fri Aug 22, 2008 10:04 am

Post by sleepy9090 » Fri Dec 09, 2011 12:26 am

cry

User avatar
Zepper
Formerly Fx3
Posts: 3223
Joined: Fri Nov 12, 2004 4:59 pm
Location: Brazil
Contact:

Post by Zepper » Fri Dec 09, 2011 4:58 am

sleepy9090 wrote:cry
I AM ERROR.

User avatar
jwdonal
Posts: 719
Joined: Sat Jun 27, 2009 11:05 pm
Location: New Mexico, USA
Contact:

Post by jwdonal » Tue Dec 13, 2011 9:14 pm

This is incredibly cool - and very impressive.

Post Reply