Compare commits

..

54 Commits
v100 ... v102

Author SHA1 Message Date
Tim Allen
ae5968cfeb Update to v102 release.
byuu says (in the public announcement):

This release adds very preliminary emulation of the Sega Master System
(Mark III), Sega Game Gear, Sega Mega Drive (Genesis), and NEC PC Engine
(Turbografx-16). These cores do not yet offer sound emulation, save
states or cheat codes.

I'm always very hesitant to release a new emulation core in its alpha
stages, as in the past this has resulted in lasting bad impressions
of cores that have since improved greatly. For instance, the Game Boy
Advance emulation offered today is easily the second most accurate around,
yet it is still widely judged by its much older alpha implementation.

However, it's always been tradition with higan to not hold onto code
in secret. Rather than delay future releases for another year or two,
I'll put my faith in you all to understand that the emulation of these
systems will improve over time.

I hope that by releasing things as they are now, I might be able to
receive some much needed assistance in improving these cores, as the
documentation for these new systems is very much less than ideal.

byuu says (in the WIP forum):

Changelog:

  - PCE: latch background scroll registers (fixes Neutopia scrolling)
  - PCE: clip background attribute table scrolling (fixes Blazing Lazers
    scrolling)
  - PCE: support background/sprite enable/disable bits
  - PCE: fix large sprite indexing (fixes Blazing Lazers title screen
    sprites)
  - HuC6280: wrap zeropage accesses to never go beyond $20xx
  - HuC6280: fix alternating addresses for block move instructions
    (fixes Neutopia II)
  - HuC6280: block move instructions save and restore A,X,Y registers
  - HuC6280: emulate BCD mode (may not be 100% correct, based on SNES
    BCD) (fixes Blazing Lazers scoring)
2017-01-20 08:01:15 +11:00
Tim Allen
b03563426f Update to v101r35 release.
byuu says:

Changelog:
  - PCE: added 384KB HuCard ROM mirroring mode
  - PCE: corrected D-pad polling order
  - PCE: corrected palette color ordering (GRB, not RGB -- yes,
    seriously)
  - PCE: corrected SATB DMA -- should write to SATB, not to VRAM
  - PCE: broke out Background, Sprite VDC settings to separate
    subclasses
  - PCE: emulated VDC backgrounds
  - PCE: emulated VDC sprites
  - PCE: emulated VDC sprite overflow, collision interrupts
  - HuC6280: fixed disassembler output for STi instructions
  - HuC6280: added missing LastCycle check to interrupt()
  - HuC6280: fixed BIT, CMP, CPX, CPY, TRB, TSB, TST flag testing and
    result
  - HuC6280: added extra cycle delays to the block move instructions
  - HuC6280: fixed ordering for flag set/clear instructions (happens
    after LastCycle check)
  - HuC6280: removed extra cycle from immediate instructions
  - HuC6280: fixed indirectLoad, indirectYStore absolute addressing
  - HuC6280: fixed BBR, BBS zeropage value testing
  - HuC6280: fixed stack push/pull direction

Neutopia looks okay until the main title screen, then there's some
gibberish on the bottom. The game also locks up with some gibberish once
you actually start a new game. So, still not playable just yet =(
2017-01-19 19:38:57 +11:00
Tim Allen
f500426158 Update to v101r34 release.
byuu says:

Changelog:

  - PCE: emulated gamepad polling
  - PCE: emulated CPU interrupt sources
  - PCE: emulated timer
  - PCE: smarter emulation of ST0,ST1,ST2 instructions
  - PCE: better structuring of CPU, VDP IO registers
  - PCE: connected palette generation to the interface
  - PCE: emulated basic VDC timing
  - PCE: emulated VDC Vblank, Coincidence, and DMA completion IRQs
  - PCE: emulated VRAM, SATB DMA transfers
  - PCE: emulated VDC I/O registers

Everything I've implemented today likely has lots of bugs, and is
untested for obvious reasons.

So basically, after I fix many horrendous bugs, it should now be
possible to implement the VDC and start getting graphical output.
2017-01-17 08:02:56 +11:00
Tim Allen
8499c64756 Update to v101r33 release.
byuu says:

Changelog:

  - PCE: HuC6280 core completed

There's bound to be a countless stream of bugs, and the cycle counts are
almost certainly not exact yet, but ... all instructions are implemented.

So at this point, I can start comparing trace logs against Mednafen's
debugger output.

Of course, we're very likely to immediately slam into a wall of needing
I/O registers implemented for the VDC in order to proceed further.
2017-01-15 11:58:47 +11:00
Tim Allen
26bd7590ad Update to v101r32 release.
byuu says:

Changelog:

  - SMS: fixed controller connection bug
  - SMS: fixed Z80 reset bug
  - PCE: emulated HuC6280 MMU
  - PCE: emulated HuC6280 RAM
  - PCE: emulated HuCard ROM reading
  - PCE: implemented 178 instructions
  - tomoko: removed "soft reset" functionality
  - tomoko: moved "power cycle" to just above "unload" option

I'm not sure of the exact number of HuC6280 instructions, but it's less
than 260.

Many of the ones I skipped are HuC6280-originals that I don't know how
to emulate just yet.

I'm also really unsure about the zero page stuff. I believe we should be
adding 0x2000 to the addresses to hit page 1, which is supposed to be
mapped to the zero page (RAM). But when I look at turboEMU's source, I
have no clue how the hell it could possibly be doing that. It looks to
be reading from page 0, which is almost always ROM, which would be ...
really weird.

I also don't know if I've emulated the T mode opcodes correctly or not.
The documentation on them is really confusing.
2017-01-14 10:59:38 +11:00
Tim Allen
21ee597aae Add a .gitlab-ci.yml to automate WIP builds. 2017-01-13 12:18:25 +11:00
Tim Allen
bf90bdfcc8 Update to v101r31 release.
byuu says:

Changelog:

  - converted Emulator::Interface::Bind to Emulator::Platform
  - temporarily disabled SGB hooks
  - SMS: emulated Game Gear palette (latching word-write behavior not
    implemented yet)
  - SMS: emulated Master System 'Reset' button, Game Gear 'Start' button
  - SMS: removed reset() functionality, driven by the mappable input now
    instead
  - SMS: split interface class in two: one for Master System, one for
    Game Gear
  - SMS: emulated Game Gear video cropping to 160x144
  - PCE: started on HuC6280 CPU core—so far only registers, NOP
    instruction has been implemented

Errata:

  - Super Game Boy support is broken and thus disabled
  - if you switch between Master System and Game Gear without
    restarting, bad things happen:
      - SMS→GG, no video output on the GG
      - GG→SMS, no input on the SMS

I'm not sure what's causing the SMS\<-\>GG switch bug, having a hard
time debugging it. Help would be very much appreciated, if anyone's up
for it. Otherwise I'll keep trying to track it down on my end.
2017-01-13 12:15:45 +11:00
Tim Allen
0ad70a30f8 Update to v101r30 release.
byuu says:

Changelog:

  - SMS: added cartridge ROM/RAM mirroring (fixes Alex Kidd)
  - SMS: fixed 8x16 sprite mode (fixes Wonder Boy, Ys graphics)
  - Z80: emulated "ex (sp),hl" instruction
  - Z80: fixed INx NF (should be set instead of cleared)
  - Z80: fixed loop condition check for CPxR, INxR, LDxR, OTxR (fixes
    walking in Wonder Boy)
  - SFC: removed Debugger and sfc/debugger.hpp
  - icarus: connected MS, GG, MD importing to the scan dialog
  - PCE: added emulation skeleton to higan and icarus

At this point, Master System games are fairly highly compatible, sans
audio. Game Gear games are running, but I need to crop the resolution
and support the higher color palette that they can utilize. It's really
something else the way they handled the resolution shrink on that thing.

The last change is obviously going to be the biggest news.

I'm very well aware it's not an ideal time to start on a new emulation
core, with the MS and MD cores only just now coming to life with no
audio support.

But, for whatever reason, my heart's really set on working on the PC
Engine. I wanted to write the final higan skeleton core, and get things
ready so that whenever I'm in the mood to work on the PCE, I can do so.

The skeleton is far and away the most tedious and obnoxious part of the
emulator development, because it's basically all just lots of
boilerplate templated code, lots of new files to create, etc.

I really don't know how things are going to proceed ... but I can say
with 99.9% certainty that this will be the final brand new core ever
added to higan -- at least one written by me, that is. This was
basically the last system from my childhood that I ever cared about.
It's the last 2D system with games that I really enjoy playing. No other
system is worth dividing my efforts and reducing the quality and amount
of time to work on the systems I have.

In the future, there will be potential for FDS, Mega CD and PCE-CD
support. But those will all be add-ons, and they'll all be really
difficult and challenge the entire design of higan's UI (it's entirely
cartridge-driven at this time.) None of them will be entirely new cores
like this one.
2017-01-12 07:27:30 +11:00
Tim Allen
79c83ade70 Update to v101r29 release.
byuu says:

Changelog:

  - SMS: background VDP clips partial tiles on the left (math may not be
    right ... it's hard to reason about)
  - SMS: fix background VDP scroll locks
  - SMS: fix VDP sprite coordinates
  - SMS: paint black after the end of the visible display
      - todo: shouldn't be a brute force at the end of the main VDP
        loop, should happen in each rendering unit
  - higan: removed emulator/debugger.hpp
  - higan: removed privileged: access specifier
  - SFC: removed debugger hooks
      - todo: remove sfc/debugger.hpp
  - Z80: fixed disassembly of (fd,dd) cb (displacement) (opcode)
    instructions
  - Z80: fix to prevent interrupts from firing between ix/iy prefixes
    and opcodes
      - todo: this is a rather hacky fix that could, if exploited, crash
        the stack frame
  - Z80: fix BIT flags
  - Z80: fix ADD hl,reg flags
  - Z80: fix CPD, CPI flags
  - Z80: fix IND, INI flags
  - Z80: fix INDR, INIT loop flag check
  - Z80: fix OUTD, OUTI flags
  - Z80: fix OTDR, OTIR loop flag check
2017-01-10 08:27:13 +11:00
Tim Allen
a3aea95e6b Update to v101r28 release.
byuu says:

Changelog:

  - SMS: emulated the remaining 240 instructions in the (0xfd, 0xdd)
    0xcb (displacement) (opcode) set
      - 1/8th of these were "legal" instructions, and apparently games
        use them a lot
  - SMS: emulated the standard gamepad controllers
      - reset button not emulated yet

The reset button is tricky. In every other case, reset is a hardware
thing that instantly reboots the entire machine.

But on the SMS, it's more like a gamepad button that's attached to the
front of the device. When you press it, it fires off a reset vector
interrupt and the gamepad polling routine lets you query the status of
the button.

Just having a reset option in the "Master System" hardware menu is not
sufficient to fully emulate the behavior. Even more annoying is that the
Game Gear doesn't have such a button, yet the core information structs
aren't flexible enough for the Master System to have it, and the Game
Gear to not have it, in the main menu. But that doesn't matter anyway,
since it won't work having it in the menu for the Master System.

So as a result, I'm going to have to have a new "input device" called
"Hardware" that has the "Reset" button listed under there. And for the
sake of consistency, I'm not sure if we should treat the other systems
the same way or not :/
2017-01-09 07:55:02 +11:00
Tim Allen
569f5abc28 Update to v101r27 release.
byuu says:

Changelog:

  - SMS: emulated the generic Sega memory mapper (none of the more
    limited forms of it yet)
      - (missing ROM shift, ROM write enable emulation -- no commercial
        games use either, though)
  - SMS: bus I/O returns 0xff instead of 0x00 so games don't think every
    key is being pressed at once
      - (this is a hack until I implement proper controller pad reading)
  - SMS: very limited protection against reading/writing past the end of
    ROM/RAM (todo: should mirror)
  - SMS: VDP background HSCROLL subtracts, rather than adds, to the
    offset (unlike VSCROLL)
  - SMS: VDP VSCROLL is 9-bit, modulates voffset+vscroll to 224 in
    192-line mode (32x28 tilemap)
  - SMS: VDP tiledata for backgrounds and sprites use `7-(x&7)` rather
    than `(x&7)`
  - SMS: fix output color to be 6-bit rather than 5-bit
  - SMS: left clip uses register `#7`, not palette color `#7`
      - (todo: do we want `color[reg7]` or `color[16 + reg7]`?)
  - SMS: refined handling of 0xcb, 0xed prefixes in the Z80 core and its
    disassembler
  - SMS: emulated (0xfd, 0xdd) 0xcb opcodes 0x00-0x0f (still missing
    0x10-0xff)
  - SMS: fixed 0xcb 0b-----110 opcodes to use direct HL and never allow
    (IX,IY)+d
  - SMS: fixed major logic bug in (IX,IY)+d displacement
      - (was using `read(x)` instead of `operand()` for the displacement
        byte fetch before)
  - icarus: fake there always being 32KiB of RAM in all SMS cartridges
    for the time being
      - (not sure how to detect this stuff yet; although I've read it's
        not even really possible `>_>`)

TODO: remove processor/z80/dissassembler.cpp code block at line 396 (as it's unnecessary.)

Lots of commercial games are starting to show trashed graphical output now.
2017-01-06 19:11:38 +11:00
Tim Allen
5bdf55f08f Update to v101r25 release.
byuu says:

Changelog:

  - SMS: emulated VDP mode 4 graphical output (background, sprites)
  - added $(windres) to icarus as well

I'm sure the VDP emulation is still really, really buggy, but
essentially I handle:

  - mode 4 rendering
  - background scrolling
  - background hscroll lock
  - background vscroll lock
  - background nametable relocation
  - sprite nametable relocation
  - sprite tiledata relocation
  - sprite 192-line y=0xd0 edge case (end sprite rendering)
  - sprite 8-pixel x-coordinate displacement
  - sprite extended size (height only in mode 4)
  - sprite overflow
  - sprite collision
  - left column masking
  - display disable
  - backdrop color
  - 192, 224, 240 height

I do not support:

  - mode 2 rendering
  - sprite zoom
  - disallowing 240 height in NTSC mode
  - PAL mode
  - probably lots more
2016-12-30 18:24:35 +11:00
Tim Allen
e30780bb72 Update to v101r25 release.
byuu says:

Changelog:

  - Makefile: added $(windres), -lpthread to Windows port
  - GBA: WAITCNT.prefetch is not writable (should fix Donkey Kong: King
    of Swing) \[endrift\]
  - SMS: fixed hcounter shift value \[hex\_usr\]
  - SMS: emulated interrupts (reset button isn't hooked up anywhere, not
    sure where to put it yet)

This WIP actually took a really long time because the documentation on
SMS interrupts was all over the place. I'm hoping I've emulated them
correctly, but I honestly have no idea. It's based off my best
understanding from four or five different sources. So it's probably
quite buggy.

However, a few interrupts fire in Sonic the Hedgehog, so that's
something to start with. Now I just have to hope I've gotten some games
far enough in that I can start seeing some data in the VDP VRAM. I need
that before I can start emulating graphics mode 4 to get some actual
screen output.

Or I can just say to hell with it and use a "Hello World" test ROM.
That'd probably be smarter.
2016-12-26 23:11:08 +11:00
Tim Allen
bab2ac812a Update to v101r24 release.
byuu says:

Changelog:

  - SMS: extended bus mapping of in/out ports: now decoding them fully
    inside ms/bus
  - SMS: moved Z80 disassembly code from processor/z80 to ms/cpu
    (cosmetic)
  - SMS: hooked up non-functional silent PSG sample generation, so I can
    cap the framerate at 60fps
  - SMS: hooked up the VDP main loop: 684 clocks/scanline, 262
    scanlines/frame (no PAL support yet)
  - SMS: emulated the VDP Vcounter and Hcounter polling ... hopefully
    it's right, as it's very bizarre
  - SMS: emulated VDP in/out ports (data read, data write, status read,
    control write, register write)
  - SMS: decoding and caching all VDP register flags (variable names
    will probably change)
  - nall: \#undef IN on Windows port (prevent compilation warning on
    processor/z80)

Watching Sonic the Hedgehog, I can definitely see some VDP register
writes going through, which is a good sign.

Probably the big thing that's needed before I can get enough into the
VDP to start showing graphics is interrupt support. And interrupts are
never fun to figure out :/

What really sucks on this front is I'm flying blind on the Z80 CPU core.
Without a working VDP, I can't run any Z80 test ROMs to look for CPU
bugs. And the CPU is certainly too buggy still to run said test ROM
anyway. I can't find any SMS emulators with trace logging from reset.
Such logs vastly accelerate tracking down CPU logic bugs, so without
them, it's going to take a lot longer.
2016-12-17 22:31:34 +11:00
Tim Allen
1d7b674dd4 Update to v101r23 release.
byuu says:

This is a really tiny WIP. Just wanted to add the known fixes before I start debugging it against Mednafen in a fork.

Changelog:

  - Z80: fixed flag calculations on 8-bit ADC, ADD, SBC, SUB
  - Z80: fixed flag calculations on 16-bit ADD
  - Z80: simplified DAA logic \[AWJ\]
  - Z80: RETI sets IFF1=IFF2 (same as RETN)
2016-11-15 18:20:42 +11:00
Tim Allen
c2c957a9da Update to v101r22 release.
byuu says:

Changelog:
- Z80: all 25 remaining instructions implemented

Now onto the debugging ... :/
2016-11-01 22:42:25 +11:00
Tim Allen
8cf20dabbf Update to v101r21 release.
byuu says:

Changelog:

- Z80: emulated 83 new instructions
- Z80: timing improvements

DAA is a skeleton implementation to complete the normal opcode set. Also
worth noting that I don't know exactly what the hell RETI is doing,
so for now it acts like RET. RETN probably needs some special handling
besides just setting IFF1=IFF2 as well.

I'm now missing 24 ED-prefix instructions, plus DAA, for a total of 25
opcodes remaining. And then, of course, several weeks worth of debugging
all of the inevitable bugs in the core.
2016-11-01 08:10:33 +11:00
Tim Allen
2707c5316d Update to v101r20 release.
byuu says:

Changelog:
- Z80: emulated 272 new instructions
- hiro/GTK: fixed v101r19 Linux regression [thanks, SuperMikeMan!]
2016-10-29 11:33:30 +11:00
Tim Allen
f3e67da937 Update to v101r19 release.
byuu says:

Changelog:

-   added \~130 new PAL games to icarus (courtesy of Smarthuman
    and aquaman)
-   added all three Korean-localized games to icarus
-   sfc: removed SuperDisc emulation (it was going nowhere)
-   sfc: fixed MSU1 regression where the play/repeat flags were not
    being cleared on track select
-   nall: cryptography support added; will be used to sign future
    databases (validation will always be optional)
-   minor shims to fix compilation issues due to nall changes

The real magic is that we now have 25-30% of the PAL SNES library in
icarus!

Signing will be tricky. Obviously if I put the public key inside the
higan archive, then all anyone has to do is change that public key for
their own releases. And if you download from my site (which is now over
HTTPS), then you don't need the signing to verify integrity. I may just
put the public key on my site on my site and leave it at that, we'll
see.
2016-10-28 08:16:58 +11:00
Tim Allen
c6fc15f8d2 Update to v101r18 release.
byuu says:

Changelog:

  - added 30 new PAL games to icarus (courtesy of Mikerochip)
  - new version of libco no longer requires mprotect nor W|X permissions
  - nall: default C compiler to -std=c11 instead of -std=c99
  - nall: use `-fno-strict-aliasing` during compilation
  - updated nall/certificates (hopefully for the last time)
  - updated nall/http to newer coding conventions
  - nall: improve handling of range() function

I didn't really work on higan at all, this is mostly just a release
because lots of other things have changed.

The most interesting is `-fno-strict-aliasing` ... basically, it joins
`-fwrapv` as being "stop the GCC developers from doing *really* evil
shit that could lead to security vulnerabilities or instabilities."

For the most part, it's a ~2% speed penalty for higan. Except for the
Sega Genesis, where it's a ~10% speedup. I have no idea how that's
possible, but clearly something's going very wrong with strict aliasing
on the Genesis core.

So ... it is what it is. If you need the performance for the non-Genesis
cores, you can turn it off in your builds. But I'm getting quite sick of
C++'s "surprises" and clever compiler developers, so I'm keeping it on
in all of my software going forward.
2016-09-14 21:55:53 +10:00
Tim Allen
d6e9d94ec3 Update to v101r17 release.
byuu says:

Changelog:

  - Z80: added most opcodes between 0x00 and 0x3f (two or three hard
    ones missing still)
  - Z80: redid register declaration *again* to handle AF', BC', DE',
    HL' (ugggggh, the fuck? Alternate registers??)
      - basically, using `#define <register name>` values to get around
        horrendously awful naming syntax
  - Z80: improved handling of displace() so that it won't ever trigger
    on (BC) or (DE)
2016-09-06 23:53:14 +10:00
Tim Allen
2fbbccf985 Update to v101r16 release.
byuu says:

Changelog:

  - Z80: implemented 113 new instructions (all the easy
    LD/ADC/ADD/AND/OR/SBC/SUB/XOR ones)
  - Z80: used alternative to castable<To, With> type (manual cast inside
    instruction() register macros)
  - Z80: debugger: used register macros to reduce typing and increase
    readability
  - Z80: debugger: smarter way of handling multiple DD/FD prefixes
    (using gotos, yay!)
  - ruby: fixed crash with Windows input driver on exit (from SuperMikeMan)

I have no idea how the P/V flag is supposed to work on AND/OR/XOR, so
that's probably wrong for now. HALT is also mostly a dummy function for
now. But I typically implement those inside instruction(), so it
probably won't need to be changed? We'll see.
2016-09-06 10:09:33 +10:00
Tim Allen
4c3f58150c Update to v101r15 release.
byuu says:

Changelog:

  - added (poorly-named) castable<To, With> template
  - Z80 debugger rewritten to make declaring instructions much simpler
  - Z80 has more instructions implemented; supports displacement on
    (IX), (IY) now
  - added `Processor::M68K::Bus` to mirror `Processor::Z80::Bus`
      - it does add a pointer indirection; so I'm not sure if I want to
        do this for all of my emulator cores ...
2016-09-04 23:51:27 +10:00
Tim Allen
d91f3999cc Update to v101r14 release.
byuu says:
Changelog:

  - rewrote the Z80 core to properly handle 0xDD (IX0 and 0xFD (IY)
    prefixes
  - added Processor::Z80::Bus as a new type of abstraction
  - all of the instructions implemented have their proper T-cycle counts
    now
  - added nall/certificates for my public keys

The goal of `Processor::Z80::Bus` is to simulate the opcode fetches being
2-read + 2-wait states; operand+regular reads/writes being 3-read. For
now, this puts the cycle counts inside the CPU core. At the moment, I
can't think of any CPU core where this wouldn't be appropriate. But it's
certainly possible that such a case exists. So this may not be the
perfect solution.

The reason for having it be a subclass of Processor::Z80 instead of
virtual functions for the MasterSystem::CPU core to define is due to
naming conflicts. I wanted the core to say `in(addr)` and have it take
the four clocks. But I also wanted a version of the function that didn't
consume time when called. One way to do that would be for the core to
call `Z80::in(addr)`, which then calls the regular `in(addr)` that goes to
`MasterSystem::CPU::in(addr)`. But I don't want to put the `Z80::`
prefix on all of the opcodes. Very easy to forget it, and then end up not
consuming any time. Another is to use uglier names in the
`MasterSystem::CPU` core, like `read_`, `write_`, `in_`, `out_`, etc. But,
yuck.

So ... yeah, this is an experiment. We'll see how it goes.
2016-09-03 21:26:04 +10:00
Tim Allen
7c96826eb0 Update to v101r13 release.
byuu says:

Changelog:

  - MS: added ms/bus
  - Z80: implemented JP/JR/CP/DI/IM/IN instructions
  - MD/VDP: added window layer emulation
  - MD/controller/gamepad: fixed d2,d3 bits (Altered Beast requires
    this)

The Z80 is definitely a lot nastier than the LR35902. There's a lot of
table duplication with HL→IX→IY; and two of them nest two levels deep
(eg FD CB xx xx), so the design may change as I implement more.
2016-08-27 14:48:21 +10:00
Tim Allen
5df717ff2a Update to v101r12 release.
byuu says:

Changelog:

  - new md/bus/ module for bus reads/writes
      - abstracts byte/word accesses wherever possible (everything but
        RAM; forces all but I/O to word, I/O to byte)
      - holds the system RAM since that's technically not part of the
        CPU anyway
  - added md/controller and md/system/peripherals
  - added emulation of gamepads
  - added stub PSG audio output (silent) to cap the framerate at 60fps
    with audio sync enabled
  - fixed VSRAM reads for plane vertical scrolling (two bugs here: add
    instead of sub; interlave plane A/B)
  - mask nametable read offsets (can't exceed 8192-byte nametables
    apparently)
  - emulated VRAM/VSRAM/CRAM reads from VDP data port
  - fixed sprite width/height size calculations
  - added partial emulation of 40-tile per scanline limitation (enough
    to fix Sonic's title screen)
  - fixed off-by-one sprite range testing
  - fixed sprite tile indexing
  - Vblank happens at Y=224 with overscan disabled
      - unsure what happens when you toggle it between Y=224 and Y=240
        ... probably bad things
  - fixed reading of address register for ADDA, CMPA, SUBA
  - fixed sign extension for MOVEA effect address reads
  - updated MOVEM to increment the read addresses (but not writeback)
    for (aN) mode

With all of that out of the way, we finally have Sonic the Hedgehog
(fully?) playable. I played to stage 1-2 and through the special stage,
at least. EDIT: yeah, we probably need HIRQs for Labyrinth Zone.

Not much else works, of course. Most games hang waiting on the Z80, and
those that don't (like Altered Beast) are still royally screwed. Tons of
features still missing; including all of the Z80/PSG/YM2612.

A note on the perihperals this time around: the Mega Drive EXT port is
basically identical to the regular controller ports. So unlike with the
Famicom and Super Famicom, I'm inheriting the exension port from the
controller class.
2016-08-22 08:11:24 +10:00
Tim Allen
f7ddbfc462 Update to v101r11 release.
byuu says:

Changelog:

  - 68K: fixed NEG/NEGX operand order
  - 68K: fixed bug in disassembler that was breaking trace logging
  - VDP: improved sprite rendering (still 100% broken)
  - VDP: added horizontal/vertical scrolling (90% broken)

Forgot:

  - 68K: fix extension word sign bit on indexed modes for disassembler
    as well
  - 68K: emulate STOP properly (use r.stop flag; clear on IRQs firing)

I'm really wearing out fast here. The Genesis documentation is somehow
even worse than Game Boy documentation, but this is a far more complex
system.

It's a massive time sink to sit here banging away at every possible
combination of how things could work, only to see no positive
improvements. Nothing I do seems to get sprites to do a goddamn thing.

squee says the sprite Y field is 10-bits, X field is 9-bits. genvdp says
they're both 10-bits. BlastEm treats them like they're both 10-bits,
then masks off the upper bit so it's effectively 9-bits anyway.

Nothing ever bothers to tell you whether the horizontal scroll values
are supposed to add or subtract from the current X position. Probably
the most basic detail you could imagine for explaining horizontal
scrolling and yet ... nope. Nothing.

I can't even begin to understand how the VDP FIFO functionality works,
or what the fuck is meant by "slots".

I'm completely at a loss as how how in the holy hell the 68K works with
8-bit accesses. I don't know whether I need byte/word handlers for every
device, or if I can just hook it right into the 68K core itself. This
one's probably the most major design detail. I need to know this before
I go and implement the PSG/YM2612/IO ports-\>gamepads/Z80/etc.

Trying to debug the 68K is murder because basically every game likes to
start with a 20,000,000-instruction reset phase of checksumming entire
games, and clearing out the memory as agonizingly slowly as humanly
possible. And like the ARM, there's too many registers so I'd need three
widescreen monitors to comfortably view the entire debugger output lines
onscreen.

I can't get any test ROMs to debug functionality outside of full games
because every **goddamned** test ROM coder thinks it's acceptable to tell
people to go fetch some toolchain from a link that died in the late '90s
and only works on MS-DOS 6.22 to build their fucking shit, because god
forbid you include a 32KiB assembled ROM image in your fucking archives.

... I may have to take a break for a while. We'll see.
2016-08-21 12:50:05 +10:00
Tim Allen
0b70a01b47 Update to v101r10 release.
byuu says:
Changelog:

  - 68K: MOVEQ is 8-bit signed
  - 68K: disassembler was print EOR for OR instructions
  - 68K: address/program-counter indexed mode had the signed-word/long
    bit backward
  - 68K: ADDQ/SUBQ #n,aN always works in long mode; regardless of size
  - 68K→VDP DMA needs to use `mode.bit(0)<<22|dmaSource`; increment by
    one instead of two
  - Z80: added registers and initial two instructions
  - MS: hooked up enough to load and start running games
      - Sonic the Hedgehog can execute exactly one instruction... whoo.
2016-08-20 00:11:26 +10:00
Tim Allen
4d2e17f9c0 Update to v101r09 release.
byuu says:

Sorry, two WIPs in one day. Got excited and couldn't wait.

Changelog:

  - ADDQ, SUBQ shouldn't update flags when targeting an address register
  - ADDA should sign extend effective address reads
  - JSR was pushing the PC too early
  - some improvements to 8-bit register reads on the VDP (still needs
    work)
  - added H/V counter reads to the VDP IO port region
  - icarus: added support for importing Master System and Game Gear ROMs
  - tomoko: added library sub-menus for each manufacturer
      - still need to sort Game Gear after Mega Drive somehow ...

The sub-menu system actually isn't all that bad. It is indeed a bit more
annoying, but not as annoying as I thought it was going to be. However,
it looks a hell of a lot nicer now.
2016-08-18 08:05:50 +10:00
Tim Allen
043f6a8b33 Update to v101r08 release.
byuu says:

Changelog:

  - 68K: fixed read-modify-write instructions
  - 68K: fixed ADDX bug (using wrong target)
  - 68K: fixed major bug with SUB using wrong argument ordering
  - 68K: fixed sign extension when reading address registers from
    effective addressing
  - 68K: fixed sign extension on CMPA, SUBA instructions
  - VDP: improved OAM sprite attribute table caching behavior
  - VDP: improved DMA fill operation behavior
  - added Master System / Game Gear stubs (needed for developing the Z80
    core)
2016-08-17 22:31:22 +10:00
Tim Allen
ffd150735b Update to v101r07 release.
byuu says:

Added VDP sprite rendering. Can't get any games far enough in to see if
it actually works. So in other words, it doesn't work at all and is 100%
completely broken.

Also added 68K exceptions and interrupts. So far only the VDP interrupt
is present. It definitely seems to be firing in commercial games, so
that's promising. But the implementation is almost certainly completely
wrong. There is fuck all of nothing for documentation on how interrupts
actually work. I had to find out the interrupt vector numbers from
reading the comments from the Sonic the Hedgehog disassembly. I have
literally no fucking clue what I0-I2 (3-bit integer priority value in
the status register) is supposed to do. I know that Vblank=6, Hblank=4,
Ext(gamepad)=2. I know that at reset, SR.I=7. I don't know if I'm
supposed to block interrupts when I is >, >=, <, <= to the interrupt
level. I don't know what level CPU exceptions are supposed to be.

Also implemented VDP regular DMA. No idea if it works correctly since
none of the commercial games run far enough to use it. So again, it's
horribly broken for usre.

Also improved VDP fill mode. But I don't understand how it takes
byte-lengths when the bus is 16-bit. The transfer times indicate it's
actually transferring at the same speed as the 68K->VDP copy, strongly
suggesting it's actually doing 16-bit transfers at a time. In which case,
what happens when you set an odd transfer length?

Also, both DMA modes can now target VRAM, VSRAM, CRAM. Supposedly there's
all kinds of weird shit going on when you target VSRAM, CRAM with VDP
fill/copy modes, but whatever. Get to that later.

Also implemented a very lazy preliminary wait mechanism to to stall out
a processor while another processor exerts control over the bus. This
one's going to be a major work in progress. For one, it totally breaks
the model I use to do save states with libco. For another, I don't
know if a 68K->VDP DMA instantly locks the CPU, or if it the CPU could
actually keep running if it was executing out of RAM when it started
the DMA transfer from ROM (eg it's a bus busy stall, not a hard chip
stall.) That'll greatly change how I handle the waiting.

Also, the OSS driver now supports Audio::Latency. Sound should be
even lower latency now. On FreeBSD when set to 0ms, it's absolutely
incredible. Cannot detect latency whatsoever. The Mario jump sound seems
to happen at the very instant I hear my cherry blue keyswitch activate.
2016-08-15 14:56:38 +10:00
Tim Allen
427bac3011 Update to v101r06 release.
byuu says:

I reworked the video sizing code. Ended up wasting five fucking hours
fighting GTK. When you call `gtk_widget_set_size_request`, it doesn't
actually happen then. This is kind of a big deal because when I then go
to draw onto the viewport, the actual viewport child window is still the
old size, so the image gets distorted. It recovers in a frame or so with
emulation, but if we were to put a still image on there, it would stay
distorted.

The first thought is, `while(gtk_events_pending())
gtk_main_iteration_do(false);` right after the `set_size_request`. But
nope, it tells you there's no events pending. So then you think, go
deeper, use `XPending()` instead. Same thing, GTK hasn't actually issued
the command to Xlib yet. So then you think, if the widget is realized,
just call a blocking `gtk_main_iteration`. One call does nothing, two
calls results in a deadlock on the second one ... do it before program
startup, and the main window will never appear. Great.

Oh, and it's not just the viewport. It's also the widget container area
of the windows, as well as the window itself, as well as the fullscreen
mode toggle effect. They all do this.

For the latter three, I couldn't find anything that worked, so I just
added 20ms loops of constantly calling `gtk_main_iteration_do(false)`
after each one of those things. The downside here is toggling the status
bar takes 40ms, so you'll see it and it'll feel a tiny bit sluggish.

But I can't have a 20ms wait on each widget resize, that would be
catastrophic to performance on windows with lots of widgets.

I tried hooking configure-event and size-allocate, but they were very
unreliable. So instead I ended up with a loop that waits up to a maximm
of 20ms that inspects the `widget->allocation.(width,height)` values
directly and waits for them to be what we asked for with
`set_size_request`.

There was some extreme ugliness in GTK with calling
`gtk_main_iteration_do` recursively (`hiro::Widget::setGeometry` is
called recursively), so I had to lock it to only happen on the top level
widgets (the child ones should get resized while waiting on the
top-level ones, so it should be fine in practice), and also only run it
on realized widgets.

Even still, I'm getting ~3 timeouts when opening the settings dialog in
higan, but no other windows. But, this is the best I can do for now.

And the reason for all of this pain? Yeah, updated the video code.

So the Emulator::Interface now has this:

    struct VideoSize { uint width, height; };  //or requiem for a tuple
    auto videoSize() -> VideoSize;
    auto videoSize(uint width, uint height, bool arc) -> VideoSize;

The first function, for now, is just returning the literal surface size.
I may remove this ... one thing I want to allow for is cores that send
different texture sizes based on interlace/hires/overscan/etc settings.

The second function is more interesting. Instead of having the UI trying
to figure out sizing, I figure the emulation cores can do a better job
and we can customize it per-core now. So it gets the window's width and
height, and whether the user asked for aspect correction, and then
computes the best width/height ratio possible. For now they're all just
doing multiples of a 1x scale to the UI 2x,3x,4x modes.

We still need a third function, which will probably be what I repurpose
videoSize() for: to return the 'effective' size for pixel shaders, to
then feed into ruby, to then feed into quark, to then feed into our
shaders. Since shaders use normalized coordinates for pixel fetching,
this should work out just fine. The real texture size will be exposed to
quark shaders as well, of course.

Now for the main window ... it's just hard-coded to be 640x480, 960x720,
1280x960 for now. It works nicely for some cores on some modes, not so
much for others. Work in progress I guess.

I also took the opportunity to draw the about dialog box logo on the
main window. Got a bit fancy and used the old spherical gradient and
impose functionality of nall/image on it. Very minor highlight, nothing
garish. Just something nicer than a solid black window.

If you guys want to mess around with sizes, placements, and gradient
styles/colors/shapes ... feel free. If you come up with something nicer,
do share.

That's what led to all the GTK hell ... the logo wasn't drawing right as
you resized the window. But now it is, though I am not at all happy with
the hacking I had to do.

I also had to improve the video update code as a result of this:

  - when you unload a game, it blacks out the screen
      - if you are not quitting the emulator, it'll draw the logo; if
        you are, it won't
  - when you load a game, it black out the logo

These options prevent any unsightliness from resizing the viewport with
image data on it already

I need to redraw the logo when toggling fullscreen with no game loaded
as well for Windows, it seems.
2016-08-15 14:52:05 +10:00
Tim Allen
ac2d0ba1cf Update to v101r05 release.
byuu says:

Changelog:

  - 68K: fixed bug that affected BSR return address
  - VDP: added very preliminary emulation of planes A, B, W (W is
    entirely broken though)
  - VDP: added command/address stuff so you can write to VRAM, CRAM,
    VSRAM
  - VDP: added VRAM fill DMA

I would be really surprised if any commercial games showed anything at
all, so I'd probably recommend against wasting your time trying, unless
you're really bored :P

Also, I wanted to add: I am accepting patches\! So if anyone wants to
look over the 68K core for bugs, that would save me untold amounts of
time in the near future :D
2016-08-13 09:47:30 +10:00
Tim Allen
1df2549d18 Update to v101r04 release.
byuu says:

Changelog:

  - pulled the (u)intN type aliases into higan instead of leaving them
    in nall
  - added 68K LINEA, LINEF hooks for illegal instructions
  - filled the rest of the 68K lambda table with generic instance of
    ILLEGAL
  - completed the 68K disassembler effective addressing modes
      - still unsure whether I should use An to decode absolute
        addresses or not
      - pro: way easier to read where accesses are taking place
      - con: requires An to be valid; so as a disassembler it does a
        poor job
      - making it optional: too much work; ick
  - added I/O decoding for the VDP command-port registers
  - added skeleton timing to all five processor cores
  - output at 1280x480 (needed for mixed 256/320 widths; and to handle
    interlace modes)

The VDP, PSG, Z80, YM2612 are all stepping one clock at a time and
syncing; which is the pathological worst case for libco. But they also
have no logic inside of them. With all the above, I'm averaging around
250fps with just the 68K core actually functional, and the VDP doing a
dumb "draw white pixels" loop. Still way too early to tell how this
emulator is going to perform.

Also, the 320x240 mode of the Genesis means that we don't need an aspect
correction ratio. But we do need to ensure the output window is a
multiple 320x240 so that the scale values work correctly. I was
hard-coding aspect correction to stretch the window an additional \*8/7.
But that won't work anymore so ... the main higan window is now 640x480,
960x720, or 1280x960. Toggling aspect correction only changes the video
width inside the window.

It's a bit jarring ... the window is a lot wider, more black space now
for most modes. But for now, it is what it is.
2016-08-12 11:07:04 +10:00
Tim Allen
9b8c3ff8c0 Update to v101r03 release.
byuu says:

The 68K core now implements all 88 instructions. It ended up being 111
instructions in my core due to splitting up opcodes with the same name
but different addressing modes or directions (removes conditions at the
expense of more code.)

Technically, I don't have exceptions actually implemented yet, and
RESET/STOP don't do anything but set flags. So there's still more to
go. But ... close enough for statistics time!

The M68K core source code is 124,712 bytes in size. The next largest
core is the ARM7 core at 70,203 bytes in size.

The M68K object size is 942KiB; with the next largest being the V30MZ
core at 173KiB.

There are a total of 19,656 invalid opcodes in the 68000 revision (unless
of course I've made mistakes in my mappings, which is very probably.)

Now the fun part ... figuring out how to fix bugs in this core without
VDP emulation :/
2016-08-11 08:02:02 +10:00
Tim Allen
0a57cac70c Update to v101r02 release.
byuu says:

Changelog:

  - Emulator: use `(uintmax)-1 >> 1` for the units of time
  - MD: implemented 13 new 68K instructions (basically all of the
    remaining easy ones); 21 remain
  - nall: replaced `(u)intmax_t` (64-bit) with *actual* `(u)intmax` type
    (128-bit where available)
      - this extends to everything: atoi, string, etc. You can even
        print 128-bit variables if you like

22,552 opcodes still don't exist in the 68K map. Looking like quite a
few entries will be blank once I finish.
2016-08-09 21:07:18 +10:00
Tim Allen
8bdf8f2a55 Update to v101r01 release.
byuu says:

Changelog:

  - added eight more 68K instructions
  - split ADD(direction) into two separate ADD functions

I now have 54 out of 88 instructions implemented (thus, 34 remaining.)
The map is missing 25,182 entries out of 65,536. Down from 32,680 for
v101.00

Aside: this version number feels really silly. r10 and r11 surely will
as well ...
2016-08-08 20:12:03 +10:00
Tim Allen
e39987a3e3 Update to v101 release.
byuu says (in the public announcement):

Not a large changelog this time, sorry. This release is mostly to fix
the SA-1 issue, and to get some real-world testing of the new scheduler
model. Most of the work in the past month has gone into writing a 68000
CPU core; yet it's still only about half-way finished.

Changelog (since the previous release):

  - fixed SNES SA-1 IRQ regression (fixes Super Mario RPG level-up
    screen)
  - new scheduler for all emulator cores (precision of 2^-127)
  - icarus database adds nine new SNES games
  - added Input/Frequency to settings file (allows simulation of
    latency)

byuu says (in the WIP forum):

Changelog:

  - in 32-bit mode, Thread uses uint64\_t with 2^-63 time units (10^-7
    precision in the worst case)
      - nearly ten times the precision of an attosecond
  - in 64-bit mode, Thread uses uint128\_t with 2^-127 time units
    (10^-26 precision in the worst case)
      - far more accurate than yoctoseconds; almost closing in on planck
        time

Note: a quartz crystal is accurate to 10^-4 or 10^-5. A cesium fountain
atomic clock is accurate to 10^-15. So ... yeah. 2^-63 was perfectly
fine; but there was no speed penalty whatsoever for using uint128\_t in
64-bit mode, so why not?
2016-08-08 20:04:15 +10:00
Tim Allen
f5e5bf1772 Update to v100r16 release.
byuu says:

(Windows users may need to include <sys/time.h> at the top of
nall/chrono.hpp, not sure.)

Unchangelog:
- forgot to add the Scheduler clock=0 fix because I have the memory of
  a goldfish

Changelog:
- new icarus database with nine additional games
- hiro(GTK,Qt) won't constantly write its settings.bml file to disk
  anymore
- added latency simulator for fun (settings.bml => Input/Latency in
  milliseconds)

So the last one ... I wanted to test out nall::chrono, and I was also
thinking that by polling every emulated frame, it's pretty wasteful when
you are using Fast Forward and hitting 200+fps. As I've said before,
calls to ruby::input::poll are not cheap.

So to get around this, I added a limiter so that if you called the
hardware poll function within N milliseconds, it'll return without
doing any actual work. And indeed, that increases my framerate of Zelda
3 uncapped from 133fps to 142fps. Yay. But it's not a "real" speedup,
as it only helps you when you exceed 100% speed (theoretically, you'd
need to crack 300% speed since the game itself will poll at 16ms at 100%
speed, but yet it sped up Zelda 3, so who am I to complain?)

I threw the latency value into the settings file. It should be 16,
but I set it to 5 since that was the lowest before it started negatively
impacting uncapped speeds. You're wasting your time and CPU cycles setting
it lower than 5, but if people like placebo effects it might work. Maybe
I should let it be a signed integer so people can set it to -16 and think
it's actually faster :P (I'm only joking. I took out the 96000hz audio
placebo effect as well. Not really into psychological tricks anymore.)

But yeah seriously, I didn't do this to start this discussion again for
the billionth time. Please don't go there. And please don't tell me this
WIP has higher/lower latency than before. I don't want to hear it.

The only reason I bring it up is for the fun part that is worth
discussing: put up or shut up time on how sensitive you are to
latency! You can set the value above 5 to see how games feel.

I personally can't really tell a difference until about 50. And I can't
be 100% confident it's worse until about 75. But ... when I set it to
150, games become "extra difficult" ... the higher it goes, the worse
it gets :D

For this WIP, I've left no upper limit cap. I'll probably set a cap of
something like 500ms or 1000ms for the official release. Need to balance
user error/trolling with enjoyability. I'll think about it.

[...]

Now, what I worry about is stupid people seeing it and thinking it's an
"added latency" setting, as if anyone would intentionally make things
worse by default. This is a limiter. So if 5ms have passed since the
game last polled, and that will be the case 99.9% of the time in games,
the next poll will happen just in time, immediately when the game polls
the inputs. Thus, a value below 1/<framerate>ms is not only pointless,
if you go too low it will ruin your fast forward max speeds.

I did say I didn't want to resort to placebo tricks, but I also don't
want to spark up public discussion on this again either. So it might
be best to default Input/Latency to 0ms, and internally have a max(5,
latency) wrapper around the value.
2016-08-03 22:32:40 +10:00
Tim Allen
c50723ef61 Update to v100r15 release.
byuu wrote:

Aforementioned scheduler changes added. Longer explanation of why here:
http://hastebin.com/raw/toxedenece

Again, we really need to test this as thoroughly as possible for
regressions :/
This is a really major change that affects absolutely everything: all
emulation cores, all coprocessors, etc.

Also added ADDX and SUB to the 68K core, which brings us just barely
above 50% of the instruction encoding space completed.

[Editor's note: The "aformentioned scheduler changes" were described in
a previous forum post:

    Unfortunately, 64-bits just wasn't enough precision (we were
    getting misalignments ~230 times a second on 21/24MHz clocks), so
    I had to move to 128-bit counters. This of course doesn't exist on
    32-bit architectures (and probably not on all 64-bit ones either),
    so for now ... higan's only going to compile on 64-bit machines
    until we figure something out. Maybe we offer a "lower precision"
    fallback for machines that lack uint128_t or something. Using the
    booth algorithm would be way too slow.

    Anyway, the precision is now 2^-96, which is roughly 10^-29. That
    puts us far beyond the yoctosecond. Suck it, MAME :P I'm jokingly
    referring to it as the byuusecond. The other 32-bits of precision
    allows a 1Hz clock to run up to one full second before all clocks
    need to be normalized to prevent overflow.

    I fixed a serious wobbling issue where I was using clock > other.clock
    for synchronization instead of clock >= other.clock; and also another
    aliasing issue when two threads share a common frequency, but don't
    run in lock-step. The latter I don't even fully understand, but I
    did observe it in testing.

    nall/serialization.hpp has been extended to support 128-bit integers,
    but without explicitly naming them (yay generic code), so nall will
    still compile on 32-bit platforms for all other applications.

    Speed is basically a wash now. FC's a bit slower, SFC's a bit faster.

The "longer explanation" in the linked hastebin is:

    Okay, so the idea is that we can have an arbitrary number of
    oscillators. Take the SNES:

    - CPU/PPU clock = 21477272.727272hz
    - SMP/DSP clock = 24576000hz
    - Cartridge DSP1 clock = 8000000hz
    - Cartridge MSU1 clock = 44100hz
    - Controller Port 1 modem controller clock = 57600hz
    - Controller Port 2 barcode battler clock = 115200hz
    - Expansion Port exercise bike clock = 192000hz

    Is this a pathological case? Of course it is, but it's possible. The
    first four do exist in the wild already: see Rockman X2 MSU1
    patch. Manifest files with higan let you specify any frequency you
    want for any component.

    The old trick higan used was to hold an int64 counter for each
    thread:thread synchronization, and adjust it like so:

    - if thread A steps X clocks; then clock += X * threadB.frequency
      - if clock >= 0; switch to threadB
    - if thread B steps X clocks; then clock -= X * threadA.frequency
      - if clock <  0; switch to threadA

    But there are also system configurations where one processor has to
    synchronize with more than one other processor. Take the Genesis:

    - the 68K has to sync with the Z80 and PSG and YM2612 and VDP
    - the Z80 has to sync with the 68K and PSG and YM2612
    - the PSG has to sync with the 68K and Z80 and YM2612

    Now I could do this by having an int64 clock value for every
    association. But these clock values would have to be outside the
    individual Thread class objects, and we would have to update every
    relationship's clock value. So the 68K would have to update the Z80,
    PSG, YM2612 and VDP clocks. That's four expensive 64-bit multiply-adds
    per clock step event instead of one.

    As such, we have to account for both possibilities. The only way to
    do this is with a single time base. We do this like so:

    - setup: scalar = timeBase / frequency
    - step: clock += scalar * clocks

    Once per second, we look at every thread, find the smallest clock
    value. Then subtract that value from all threads. This prevents the
    clock counters from overflowing.

    Unfortunately, these oscillator values are psychotic, unpredictable,
    and often times repeating fractions. Even with a timeBase of
    1,000,000,000,000,000,000 (one attosecond); we get rounding errors
    every ~16,300 synchronizations. Specifically, this happens with a CPU
    running at 21477273hz (rounded) and SMP running at 24576000hz. That
    may be good enough for most emulators, but ... you know how I am.

    Plus, even at the attosecond level, we're really pushing against the
    limits of 64-bit integers. Given the reciprocal inverse, a frequency
    of 1Hz (which does exist in higan!) would have a scalar that consumes
    1/18th of the entire range of a uint64 on every single step. Yes, I
    could raise the frequency, and then step by that amount, I know. But
    I don't want to have weird gotchas like that in the scheduler core.

    Until I increase the accuracy to about 100 times greater than a
    yoctosecond, the rounding errors are too great. And since the only
    choice above 64-bit values is 128-bit values; we might as well use
    all the extra headroom. 2^-96 as a timebase gives me the ability to
    have both a 1Hz and 4GHz clock; and run them both for a full second;
    before an overflow event would occur.

Another hastebin includes demonstration code:

    #include <libco/libco.h>

    #include <nall/nall.hpp>
    using namespace nall;

    //

    cothread_t mainThread = nullptr;
    const uint iterations = 100'000'000;
    const uint cpuFreq = 21477272.727272 + 0.5;
    const uint smpFreq = 24576000.000000 + 0.5;
    const uint cpuStep = 4;
    const uint smpStep = 5;

    //

    struct ThreadA {
      cothread_t handle = nullptr;
      uint64 frequency = 0;
      int64 clock = 0;

      auto create(auto (*entrypoint)() -> void, uint frequency) {
        this->handle = co_create(65536, entrypoint);
        this->frequency = frequency;
        this->clock = 0;
      }
    };

    struct CPUA : ThreadA {
      static auto Enter() -> void;
      auto main() -> void;
      CPUA() { create(&CPUA::Enter, cpuFreq); }
    } cpuA;

    struct SMPA : ThreadA {
      static auto Enter() -> void;
      auto main() -> void;
      SMPA() { create(&SMPA::Enter, smpFreq); }
    } smpA;

    uint8 queueA[iterations];
    uint offsetA;
    cothread_t resumeA = cpuA.handle;

    auto EnterA() -> void {
      offsetA = 0;
      co_switch(resumeA);
    }

    auto QueueA(uint value) -> void {
      queueA[offsetA++] = value;
      if(offsetA >= iterations) {
        resumeA = co_active();
        co_switch(mainThread);
      }
    }

    auto CPUA::Enter() -> void { while(true) cpuA.main(); }

    auto CPUA::main() -> void {
      QueueA(1);
      smpA.clock -= cpuStep * smpA.frequency;
      if(smpA.clock < 0) co_switch(smpA.handle);
    }

    auto SMPA::Enter() -> void { while(true) smpA.main(); }

    auto SMPA::main() -> void {
      QueueA(2);
      smpA.clock += smpStep * cpuA.frequency;
      if(smpA.clock >= 0) co_switch(cpuA.handle);
    }

    //

    struct ThreadB {
      cothread_t handle = nullptr;
      uint128_t scalar = 0;
      uint128_t clock = 0;

      auto print128(uint128_t value) {
        string s;
        while(value) {
          s.append((char)('0' + value % 10));
          value /= 10;
        }
        s.reverse();
        print(s, "\n");
      }

      //femtosecond (10^15) =    16306
      //attosecond  (10^18) =   688838
      //zeptosecond (10^21) = 13712691
      //yoctosecond (10^24) = 13712691 (hitting a dead-end on a rounding error causing a wobble)
      //byuusecond? ( 2^96) = (perfect? 79,228 times more precise than a yoctosecond)

      auto create(auto (*entrypoint)() -> void, uint128_t frequency) {
        this->handle = co_create(65536, entrypoint);

        uint128_t unitOfTime = 1;
      //for(uint n : range(29)) unitOfTime *= 10;
        unitOfTime <<= 96;  //2^96 time units ...

        this->scalar = unitOfTime / frequency;
        print128(this->scalar);
        this->clock = 0;
      }

      auto step(uint128_t clocks) -> void { clock += clocks * scalar; }
      auto synchronize(ThreadB& thread) -> void { if(clock >= thread.clock) co_switch(thread.handle); }
    };

    struct CPUB : ThreadB {
      static auto Enter() -> void;
      auto main() -> void;
      CPUB() { create(&CPUB::Enter, cpuFreq); }
    } cpuB;

    struct SMPB : ThreadB {
      static auto Enter() -> void;
      auto main() -> void;
      SMPB() { create(&SMPB::Enter, smpFreq); clock = 1; }
    } smpB;

    auto correct() -> void {
      auto minimum = min(cpuB.clock, smpB.clock);
      cpuB.clock -= minimum;
      smpB.clock -= minimum;
    }

    uint8 queueB[iterations];
    uint offsetB;
    cothread_t resumeB = cpuB.handle;

    auto EnterB() -> void {
      correct();
      offsetB = 0;
      co_switch(resumeB);
    }

    auto QueueB(uint value) -> void {
      queueB[offsetB++] = value;
      if(offsetB >= iterations) {
        resumeB = co_active();
        co_switch(mainThread);
      }
    }

    auto CPUB::Enter() -> void { while(true) cpuB.main(); }

    auto CPUB::main() -> void {
      QueueB(1);
      step(cpuStep);
      synchronize(smpB);
    }

    auto SMPB::Enter() -> void { while(true) smpB.main(); }

    auto SMPB::main() -> void {
      QueueB(2);
      step(smpStep);
      synchronize(cpuB);
    }

    //

    #include <nall/main.hpp>
    auto nall::main(string_vector) -> void {
      mainThread = co_active();

      uint masterCounter = 0;
      while(true) {
        print(masterCounter++, " ...\n");

        auto A = clock();
        EnterA();
        auto B = clock();
        print((double)(B - A) / CLOCKS_PER_SEC, "s\n");

        auto C = clock();
        EnterB();
        auto D = clock();
        print((double)(D - C) / CLOCKS_PER_SEC, "s\n");

        for(uint n : range(iterations)) {
          if(queueA[n] != queueB[n]) return print("fail at ", n, "\n");
        }
      }
    }

...and that's everything.]
2016-07-31 12:11:20 +10:00
Tim Allen
ca277cd5e8 Update to v100r14 release.
byuu says:

(Windows: compile with -fpermissive to silence an annoying error. I'll
fix it in the next WIP.)

I completely replaced the time management system in higan and overhauled
the scheduler.

Before, processor threads would have "int64 clock"; and there would
be a 1:1 relationship between two threads. When thread A ran for X
cycles, it'd subtract X * B.Frequency from clock; and when thread B ran
for Y cycles, it'd add Y * A.Frequency from clock. This worked well
and allowed perfect precision; but it doesn't work when you have more
complicated relationships: eg the 68K can sync to the Z80 and PSG; the
Z80 to the 68K and PSG; so the PSG needs two counters.

The new system instead uses a "uint64 clock" variable that represents
time in attoseconds. Every time the scheduler exits, it subtracts
the smallest clock count from all threads, to prevent an overflow
scenario. The only real downside is that rounding errors mean that
roughly every 20 minutes, we have a rounding error of one clock cycle
(one 20,000,000th of a second.) However, this only applies to systems
with multiple oscillators, like the SNES. And when you're in that
situation ... there's no such thing as a perfect oscillator anyway. A
real SNES will be thousands of times less out of spec than 1hz per 20
minutes.

The advantages are pretty immense. First, we obviously can now support
more complex relationships between threads. Second, we can build a
much more abstracted scheduler. All of libco is now abstracted away
completely, which may permit a state-machine / coroutine version of
Thread in the future. We've basically gone from this:

    auto SMP::step(uint clocks) -> void {
      clock += clocks * (uint64)cpu.frequency;
      dsp.clock -= clocks;
      if(dsp.clock < 0 && !scheduler.synchronizing()) co_switch(dsp.thread);
      if(clock >= 0 && !scheduler.synchronizing()) co_switch(cpu.thread);
    }

To this:

    auto SMP::step(uint clocks) -> void {
      Thread::step(clocks);
      synchronize(dsp);
      synchronize(cpu);
    }

As you can see, we don't have to do multiple clock adjustments anymore.
This is a huge win for the SNES CPU that had to update the SMP, DSP, all
peripherals and all coprocessors. Likewise, we don't have to synchronize
all coprocessors when one runs, now we can just synchronize the active
one to the CPU.

Third, when changing the frequencies of threads (think SGB speed setting
modes, GBC double-speed mode, etc), it no longer causes the "int64
clock" value to be erroneous.

Fourth, this results in a fairly decent speedup, mostly across the
board. Aside from the GBA being mostly a wash (for unknown reasons),
it's about an 8% - 12% speedup in every other emulation core.

Now, all of this said ... this was an unbelievably massive change, so
... you know what that means >_> If anyone can help test all types of
SNES coprocessors, and some other system games, it'd be appreciated.

----

Lastly, we have a bitchin' new about screen. It unfortunately adds
~200KiB onto the binary size, because the PNG->C++ header file
transformation doesn't compress very well, and I want to keep the
original resource files in with the higan archive. I might try some
things to work around this file size increase in the future, but for now
... yeah, slightly larger archive sizes, sorry.

The logo's a bit busted on Windows (the Label control's background
transparency and alignment settings aren't working), but works well on
GTK. I'll have to fix Windows before the next official release. For now,
look on my Twitter feed if you want to see what it's supposed to look
like.

----

EDIT: forgot about ICD2::Enter. It's doing some weird inverse
run-to-save thing that I need to implement support for somehow. So, save
states on the SGB core probably won't work with this WIP.
2016-07-30 13:56:12 +10:00
Tim Allen
306cac2b54 Update to v100r13 release.
byuu says:

Changelog: M68K improvements, new instructions added.
2016-07-26 20:46:43 +10:00
Tim Allen
f230d144b5 Update to v100r12 release.
byuu says:

All of the above fixes, plus I added all 24 variations on the shift
opcodes, plus SUBQ, plus fixes to the BCC instruction.

I can now run 851,767 instructions into Sonic the Hedgehog before hitting
an unimplemented instruction (SUB).

The 68K core is probably only ~35% complete, and yet it's already within
4KiB of being the largest CPU core, code size wise, in all of higan. Fuck
this chip.
2016-07-25 23:15:54 +10:00
Tim Allen
7ccfbe0206 Update to v100r11 release.
byuu says:

I split the Register class and read/write handlers into DataRegister and
AddressRegister, given that they have different behaviors on byte/word
accesses (data tends to preserve the upper bits; address tends to
sign-extend things.)

I expanded EA to EffectiveAddress. No sense in abbreviating things
to death.

I've now implemented 26 instructions. But the new ones are just all the
stupid from/to ccr/sr instructions.

Ryphecha confirmed that you can't set the undefined bits, so I don't
think the BitField concept is appropriate for the CCR/SR. Instead, I'm
just storing direct flags and have (read,write)(CCR,SR) instead. This
isn't like the 65816 where you have subroutines that push and pop the
flag register. It's much more common to access individual flags. Doesn't
match the consistency angle of the other CPU cores, but ... I think this
is the right thing to for the 68K specifically.
2016-07-23 12:32:35 +10:00
Tim Allen
4b897ba791 Update to v100r10 release.
byuu says:

Redesigned the handling of reading/writing registers to be about eight
times faster than the old system. More work may be needed ... it seems
data registers tend to preserve their upper bits upon assignment; whereas
address registers tend to sign-extend values into them. It may make
sense to have DataRegister and AddressRegister classes with separate
read/write handlers. I'd have to hold two Register objects inside the
EffectiveAddress (EA) class if we do that.

Implemented 19 opcodes now (out of somewhere between 60 and 90.) That gets
the first ~530,000 instructions in Sonic the Hedgehog running (though
probably wrong. But we can run a lot thanks to large initialization
loops.)

If I force the core to loop back to the reset vector on an invalid opcode,
I'm getting about 1500fps with a dumb 320x240 blit 60 times a second and
just the 68K running alone (no Z80, PSG, VDP, YM2612.) I don't know if
that's good or not. I guess we'll find out.

I had to stop tonight because the final opcode I execute is an RTS
(return from subroutine) that's branching back to address 0; which is
invalid ... meaning something went terribly wrong and the system crashed.
2016-07-22 22:03:25 +10:00
Tim Allen
be3f6ac0d5 Update to v100r09 release.
byuu says:

Another six hours in ...

I have all of the opcodes, memory access functions, disassembler mnemonics
and table building converted over to the new template<uint Size> format.

Certainly, it would be quite easy for this nightmare chip to throw me
another curveball, but so far I can handle:

- MOVE (EA to, EA from) case
  - read(from) has to update register index for +/-(aN) mode
- MOVEM (EA from) case
  - when using +/-(aN), RA can't actually be updated until the transfer
    is completed
- LEA (EA from) case
  - doesn't actually perform the final read; just returns the address
    to be read from
- ANDI (EA from-and-to) case
  - same EA has to be read from and written to
  - for -(aN), the read has to come from aN-2, but can't update aN yet;
    so that the write also goes to aN-2
- no opcode can ever fetch the extension words more than once
- manually control the order of extension word fetching order for proper
  opcode decoding

To do all of that without a whole lot of duplicated code (or really
bloating out every single instruction with red tape), I had to bring
back the "bool valid / uint32 address" variables inside the EA struct =(

If weird exceptions creep in like timing constraints only on certain
opcodes, I can use template flags to the EA read/write functions to
handle that.
2016-07-19 19:12:05 +10:00
Tim Allen
92fe5b0813 Update to v100r08 release.
byuu says:

Six and a half hours this time ... one new opcode, and all old opcodes
now in a deprecated format. Hooray, progress!

For building the table, I've decided to move from:

    for(uint opcode : range(65536)) {
      if(match(...)) bind(opNAME, ...);
    }

To instead having separate for loops for each supported opcode. This
lets me specialize parts I want with templates.

And to this aim, I'm moving to replace all of the
(read,write)(size, ...) functions with (read,write)<Size>(...) functions.

This will amount to the ~70ish instructions being triplicated ot ~210ish
instructions; but I think this is really important.

When I was getting into flag calculations, a ton of conditionals
were needed to mask sizes to byte/word/long. There was also lots of
conditionals in all the memory access handlers.

The template code is ugly, but we eliminate a huge amount of branch
conditions this way.
2016-07-18 08:11:29 +10:00
Tim Allen
059347e575 Update to v100r07 release.
byuu says:

Four and a half hours of work and ... zero new opcodes implemented.

This was the best job I could do refining the effective address
computations. Should have all twelve 68000 modes implemented now. Still
have a billion questions about when and how I'm supposed to perform
certain edge case operations, though.
2016-07-17 13:24:28 +10:00
Tim Allen
0d6a09f9f8 Update to v100r06 release.
byuu says:

Up to ten 68K instructions out of somewhere between 61 and 88, depending
upon which PDF you look at. Of course, some of them aren't 100% completed
yet, either. Lots of craziness with MOVEM, and BCC has a BSR variant
that needs stack push/pop functions.

This WIP actually took over eight hours to make, going through every
possible permutation on how to design the core itself. The updated design
now builds both the instruction decoder+dispatcher and the disassembler
decoder into the same main loop during M68K's constructor.

The special cases are also really psychotic on this processor, and
I'm afraid of missing something via the fallthrough cases. So instead,
I'm ordering the instructions alphabetically, and including exclusion
cases to ignore binding invalid cases. If I end up remapping an existing
register, then it'll throw a run-time assertion at program startup.

I wanted very much to get rid of struct EA (EffectiveAddress), but
it's too difficult to keep track of the internal effective address
without it. So I split out the size to a separate parameter, since
every opcode only has one size parameter, and otherwise it was getting
duplicated in opcodes that take two EAs, and was also awkward with the
flag testing. It's a bit more typing, but I feel it's more clean this way.

Overall, I'm really worried this is going to be too slow. I don't want
to turn the EA stuff into templates, because that will massively bloat
out compilation times and object sizes, and will also need a special DSL
preprocessor since C++ doesn't have a static for loop. I can definitely
optimize a lot of EA's address/read/write functions away once the core
is completed, but it's never going to hold a candle to a templatized
68K core.

----

Forgot to include the SA-1 regression fix. I always remember immediately
after I upload and archive the WIP. Will try to get that in next time,
I guess.
2016-07-16 18:39:44 +10:00
Tim Allen
b72f35a13e Update to v100r05 release.
byuu says:

Alright, I'm definitely going to need to find some people willing to
tolerate my questions on this chip, so I'm going to go ahead and announce
I'm working on this I guess.

This core is way too big for a surprise like the NES and WS cores
were. It'll probably even span multiple v10x releases before it's
even ready.
2016-07-13 08:47:04 +10:00
Tim Allen
1c0ef793fe Update to v100r04 release.
byuu says:

I now have enough of three instructions implemented to get through the
first four instructions in Sonic the Hedgehog.

But they're far from complete. The very first instruction uses EA
addressing, which is similar to x86's ModRM in terms of how disgustingly
complex it is. And it also accesses Z80 control registers, which obviously
isn't going to do anything yet.

The slow speed was me being stupid again. It's not 7.6MHz per frame,
it's 7.67MHz per second. So yeah, speed is so far acceptable again. But
we'll see how things go as I keep emulating more. The 68K decode is not
pretty at all.
2016-07-12 20:19:31 +10:00
Tim Allen
76a8ecd32a Update to v100r03 release.
byuu says:

Changelog:
- moved Thread, Scheduler, Cheat functionality into emulator/ for
  all cores
- start of actual Mega Drive emulation (two 68K instructions)

I'm going to be rather terse on MD emulation, as it's too early for any
meaningful dialogue here.
2016-07-10 15:28:26 +10:00
Tim Allen
3dd1aa9c1b Update to v100r02 release.
byuu says:

Sigh ... I'm really not a good person. I'm inherently selfish.

My responsibility and obligation right now is to work on loki, and
then on the Tengai Makyou Zero translation, and then on improving the
Famicom emulation.

And yet ... it's not what I really want to do. That shouldn't matter;
I should work on my responsibilities first.

Instead, I'm going to be a greedy, self-centered asshole, and work on
what I really want to instead.

I'm really sorry, guys. I'm sure this will make a few people happy,
and probably upset even more people.

I'm also making zero guarantees that this ever gets finished. As always,
I wish I could keep these things secret, so if I fail / give up, I could
just drop it with no shame. But I would have to cut everyone out of the
WIP process completely to make it happen. So, here goes ...

This WIP adds the initial skeleton for Sega Mega Drive / Genesis
emulation. God help us.

(minor note: apparently the new extension for Mega Drive games is .md,
neat. That's what I chose for the folders too. I thought it was .smd,
so that'll be fixed in icarus for the next WIP.)

(aside: this is why I wanted to get v100 out. I didn't want this code in
a skeleton state in v100's source. Nor did I want really broken emulation,
which the first release is sure to be, tarring said release.)

...

So, basically, I've been ruminating on the legacy I want to leave behind
with higan. 3D systems are just plain out. I'm never going to support
them. They're too complex for my abilities, and they would run too slowly
with my design style. I'm not willing to compromise my design ideals. And
I would never want to play a 3D game system at native 240p/480i resolution
... but 1080p+ upscaling is not accurate, so that's a conflict I want
to avoid entirely. It's also never going to emulate computer systems
(X68K, PC-98, FM-Towns, etc) because holy shit that would completely
destroy me. It's also never going emulate arcade machines.

So I think of higan as a collection of 2D emulators for consoles
and handhelds. I've gone over every major 2D gaming system there is,
looking for ones with games I actually care about and enjoy. And I
basically have five of those systems supported already. Looking at the
remaining list, I see only three systems left that I have any interest
in whatsoever: PC-Engine, Master System, Mega Drive. Again, I'm not in
any way committing to emulating any of these, but ... if I had all of
those in higan, I think I'd be content to really, truly, finally stop
writing more emulators for the rest of my life.

And so I decided to tackle the most difficult system first. If I'm
successful, the Z80 core should cover a lot of the work on the SMS. And
the HuC6280 should land somewhere between the NES and SNES in terms of
difficulty ... closer to the NES.

The systems that just don't appeal to me at all, which I will never touch,
include, but are not limited to:
* Atari 2600/5200/7800
* Lynx
* Jaguar
* Vectrex
* Colecovision
* Commodore 64
* Neo-Geo
* Neo-Geo Pocket / Color
* Virtual Boy
* Super A'can
* 32X
* CD-i
* etc, etc, etc.

And really, even if something were mildly interesting in there ... we
have to stop. I can't scale infinitely. I'm already way past my limit,
but I'm doing this anyway. Too many cores bloats everything and kills
quality on everything. I don't want higan to become MESS v2.

I don't know what I'll do about the Famicom Disk System, PC-Engine CD,
and Mega CD. I don't think I'll be able to achieve 60fps emulating the
Mega CD, even if I tried to.

I don't know what's going to happen here with even the Mega Drive. Maybe
I'll get driven crazy with the documentation and quit. Maybe it'll end
up being too complicated and I'll quit. Maybe the emulation will end up
way too slow and I'll give up. Maybe it'll take me seven years to get
any games playable at all. Maybe Steve Snake, AamirM and Mike Pavone
will pool money to hire a hitman to come after me. Who knows.

But this is what I want to do, so ... here goes nothing.
2016-07-09 14:21:37 +10:00
Tim Allen
88c79e56a0 Update to v100r01 release.
[This version, with the internal version number changed back to "v100",
replaced the original v100 source archive on byuu.org soon after v100's
release, because it fixes important bugs in that version. --Ed]

byuu says:

Changelog:
- fixed default paths for Sufami Turbo slotted games
- moved WonderSwan orientation controls to the port rather than the device
  - I do like hex_usr's idea here; but that'll need more consideration;
    so this is a temporary fix
- added new debugger interface (see the public topic for more on that)
2016-07-08 22:31:35 +10:00
415 changed files with 24035 additions and 4128 deletions

47
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,47 @@
# NOTE: This file is not part of the official higan source, it's been added
# to help build WIP binaries with minimal fuss.
image: debian:stable
linux-x86_64-binaries:
script:
- apt-get update && apt-get -y install build-essential libgtk2.0-dev libpulse-dev mesa-common-dev libgtksourceview2.0-dev libcairo2-dev libsdl1.2-dev libxv-dev libao-dev libopenal-dev libudev-dev
- make -C icarus compiler=g++
- make -C higan compiler=g++
- mkdir higan-nightly
- cp -a icarus/out/icarus higan-nightly/icarus
- cp -a icarus/Database higan-nightly/
- cp -a higan/out/higan higan-nightly/higan
- cp -a higan/data/cheats.bml higan-nightly/
- cp -a higan/systems/* higan-nightly/
artifacts:
paths:
- higan-nightly/*
cache:
paths:
- icarus/obj/*.o
- higan/obj/*.o
windows-x86_64-binaries:
# This is a normal Windows cross-compile process, except that
# nall::chrono tries to use clock_gettime on Windows even
# though it's a POSIX function, and for some weird reason mingw has
# clock_gettime() in the pthread library.
script:
- apt-get update && apt-get -y install build-essential mingw-w64
- sed -i -e 's/-lole32/& -static -lpthread/' nall/GNUmakefile
- make -C icarus platform=windows compiler="x86_64-w64-mingw32-g++ -static-libgcc -static-libstdc++" windres="x86_64-w64-mingw32-windres"
- make -C higan platform=windows compiler="x86_64-w64-mingw32-g++ -static-libgcc -static-libstdc++" windres="x86_64-w64-mingw32-windres"
- mkdir higan-nightly
- cp -a icarus/out/icarus higan-nightly/icarus.exe
- cp -a icarus/Database higan-nightly/
- cp -a higan/out/higan higan-nightly/higan.exe
- cp -a higan/data/cheats.bml higan-nightly/
- cp -a higan/systems/* higan-nightly/
artifacts:
paths:
- higan-nightly/*
cache:
paths:
- icarus/obj/*.o
- higan/obj/*.o

View File

@@ -4,7 +4,7 @@ target := tomoko
# console := true
flags += -I. -I.. -O3
objects := libco audio video resource
objects := libco emulator audio video resource
# profile-guided optimization mode
# pgo := instrument
@@ -24,7 +24,7 @@ ifeq ($(platform),windows)
else
link += -mwindows
endif
link += -mthreads -luuid -lkernel32 -luser32 -lgdi32 -lcomctl32 -lcomdlg32 -lshell32
link += -mthreads -lpthread -luuid -lkernel32 -luser32 -lgdi32 -lcomctl32 -lcomdlg32 -lshell32
link += -Wl,-enable-auto-import
link += -Wl,-enable-runtime-pseudo-reloc
else ifeq ($(platform),macosx)
@@ -52,10 +52,11 @@ compile = \
all: build;
obj/libco.o: ../libco/libco.c $(call rwildcard,../libco/)
obj/audio.o: audio/audio.cpp $(call rwildcard,audio/)
obj/video.o: video/video.cpp $(call rwildcard,video/)
obj/resource.o: resource/resource.cpp $(call rwildcard,resource/)
obj/libco.o: ../libco/libco.c $(call rwildcard,../libco)
obj/emulator.o: emulator/emulator.cpp $(call rwildcard,emulator)
obj/audio.o: audio/audio.cpp $(call rwildcard,audio)
obj/video.o: video/video.cpp $(call rwildcard,video)
obj/resource.o: resource/resource.cpp $(call rwildcard,resource)
ui := target-$(target)
include $(ui)/GNUmakefile

View File

@@ -6,6 +6,8 @@ namespace Emulator {
Audio audio;
auto Audio::reset(maybe<uint> channels_, maybe<double> frequency_) -> void {
interface = nullptr;
if(channels_) channels = channels_();
if(frequency_) frequency = frequency_();
@@ -81,7 +83,7 @@ auto Audio::process() -> void {
if(balance > 0.0) samples[0] *= 1.0 - balance;
}
interface->audioSample(samples, channels);
platform->audioSample(samples, channels);
}
}

View File

@@ -11,7 +11,7 @@ struct Stream;
struct Audio {
auto reset(maybe<uint> channels = nothing, maybe<double> frequency = nothing) -> void;
auto setInterface(Interface*) -> void;
auto setInterface(Interface* interface) -> void;
auto setVolume(double volume) -> void;
auto setBalance(double balance) -> void;

48
higan/emulator/cheat.hpp Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
namespace Emulator {
struct Cheat {
struct Code {
uint addr;
uint data;
maybe<uint> comp;
};
explicit operator bool() const {
return codes.size() > 0;
}
auto reset() -> void {
codes.reset();
}
auto append(uint addr, uint data, maybe<uint> comp = nothing) -> void {
codes.append({addr, data, comp});
}
auto assign(const string_vector& list) -> void {
reset();
for(auto& entry : list) {
for(auto code : entry.split("+")) {
auto part = code.split("/");
if(part.size() == 2) append(part[0].hex(), part[1].hex());
if(part.size() == 3) append(part[0].hex(), part[2].hex(), part[1].hex());
}
}
}
auto find(uint addr, uint comp) -> maybe<uint> {
for(auto& code : codes) {
if(code.addr == addr && (!code.comp || code.comp() == comp)) {
return code.data;
}
}
return nothing;
}
private:
vector<Code> codes;
};
}

View File

@@ -0,0 +1,7 @@
#include <emulator/emulator.hpp>
namespace Emulator {
Platform* platform = nullptr;
}

View File

@@ -4,6 +4,7 @@
#include <nall/vfs.hpp>
using namespace nall;
#include "types.hpp"
#include <libco/libco.h>
#include <audio/audio.hpp>
#include <video/video.hpp>
@@ -11,19 +12,20 @@ using namespace nall;
namespace Emulator {
static const string Name = "higan";
static const string Version = "100";
static const string Version = "102";
static const string Author = "byuu";
static const string License = "GPLv3";
static const string Website = "http://byuu.org/";
//incremented only when serialization format changes
static const string SerializerVersion = "100";
static const string SerializerVersion = "101";
namespace Constants {
namespace Colorburst {
static constexpr double NTSC = 315.0 / 88.0 * 1'000'000.0;
static constexpr double PAL = 283.75 * 15'625.0 + 25.0;
}
}
}
#include "interface.hpp"
#if defined(DEBUGGER)
#define privileged public
#else
#define privileged private
#endif

View File

@@ -2,14 +2,23 @@
namespace Emulator {
struct Platform {
virtual auto path(uint id) -> string { return ""; }
virtual auto open(uint id, string name, vfs::file::mode mode, bool required = false) -> vfs::shared::file { return {}; }
virtual auto load(uint id, string name, string type) -> maybe<uint> { return nothing; }
virtual auto videoRefresh(const uint32* data, uint pitch, uint width, uint height) -> void {}
virtual auto audioSample(const double* samples, uint channels) -> void {}
virtual auto inputPoll(uint port, uint device, uint input) -> int16 { return 0; }
virtual auto inputRumble(uint port, uint device, uint input, bool enable) -> void {}
virtual auto dipSettings(Markup::Node node) -> uint { return 0; }
virtual auto notify(string text) -> void { print(text, "\n"); }
};
struct Interface {
struct Information {
string manufacturer;
string name;
uint width;
uint height;
bool overscan;
double aspectRatio;
bool resettable;
struct Capability {
bool states;
@@ -41,35 +50,14 @@ struct Interface {
};
vector<Port> ports;
struct Bind {
virtual auto path(uint) -> string { return ""; }
virtual auto open(uint, string, vfs::file::mode, bool) -> vfs::shared::file { return {}; }
virtual auto load(uint, string, string) -> maybe<uint> { return nothing; }
virtual auto videoRefresh(const uint32*, uint, uint, uint) -> void {}
virtual auto audioSample(const double*, uint) -> void {}
virtual auto inputPoll(uint, uint, uint) -> int16 { return 0; }
virtual auto inputRumble(uint, uint, uint, bool) -> void {}
virtual auto dipSettings(Markup::Node) -> uint { return 0; }
virtual auto notify(string text) -> void { print(text, "\n"); }
};
Bind* bind = nullptr;
//callback bindings (provided by user interface)
auto path(uint id) -> string { return bind->path(id); }
auto open(uint id, string name, vfs::file::mode mode, bool required = false) -> vfs::shared::file { return bind->open(id, name, mode, required); }
auto load(uint id, string name, string type) -> maybe<uint> { return bind->load(id, name, type); }
auto videoRefresh(const uint32* data, uint pitch, uint width, uint height) -> void { return bind->videoRefresh(data, pitch, width, height); }
auto audioSample(const double* samples, uint channels) -> void { return bind->audioSample(samples, channels); }
auto inputPoll(uint port, uint device, uint input) -> int16 { return bind->inputPoll(port, device, input); }
auto inputRumble(uint port, uint device, uint input, bool enable) -> void { return bind->inputRumble(port, device, input, enable); }
auto dipSettings(Markup::Node node) -> uint { return bind->dipSettings(node); }
template<typename... P> auto notify(P&&... p) -> void { return bind->notify({forward<P>(p)...}); }
//information
virtual auto manifest() -> string = 0;
virtual auto title() -> string = 0;
//video information
struct VideoSize { uint width, height; };
virtual auto videoSize() -> VideoSize = 0;
virtual auto videoSize(uint width, uint height, bool arc) -> VideoSize = 0;
virtual auto videoFrequency() -> double = 0;
virtual auto videoColors() -> uint32 = 0;
virtual auto videoColor(uint32 color) -> uint64 = 0;
@@ -118,4 +106,6 @@ struct File {
static const auto Required = true;
};
extern Platform* platform;
}

View File

@@ -0,0 +1,91 @@
#pragma once
namespace Emulator {
struct Scheduler {
enum class Mode : uint {
Run,
SynchronizeMaster,
SynchronizeSlave,
};
enum class Event : uint {
Step,
Frame,
Synchronize,
};
inline auto synchronizing() const -> bool { return _mode == Mode::SynchronizeSlave; }
auto reset() -> void {
_host = co_active();
_threads.reset();
}
auto primary(Thread& thread) -> void {
_master = _resume = thread.handle();
}
auto append(Thread& thread) -> bool {
if(_threads.find(&thread)) return false;
thread._clock += _threads.size(); //this bias prioritizes threads appended earlier first
return _threads.append(&thread), true;
}
auto remove(Thread& thread) -> bool {
if(auto offset = _threads.find(&thread)) return _threads.remove(*offset), true;
return false;
}
auto enter(Mode mode = Mode::Run) -> Event {
_mode = mode;
_host = co_active();
co_switch(_resume);
return _event;
}
inline auto resume(Thread& thread) -> void {
if(_mode != Mode::SynchronizeSlave) co_switch(thread.handle());
}
auto exit(Event event) -> void {
uintmax minimum = -1;
for(auto thread : _threads) {
if(thread->_clock < minimum) minimum = thread->_clock;
}
for(auto thread : _threads) {
thread->_clock -= minimum;
}
_event = event;
_resume = co_active();
co_switch(_host);
}
inline auto synchronize(Thread& thread) -> void {
if(thread.handle() == _master) {
while(enter(Mode::SynchronizeMaster) != Event::Synchronize);
} else {
_resume = thread.handle();
while(enter(Mode::SynchronizeSlave) != Event::Synchronize);
}
}
inline auto synchronize() -> void {
if(co_active() == _master) {
if(_mode == Mode::SynchronizeMaster) return exit(Event::Synchronize);
} else {
if(_mode == Mode::SynchronizeSlave) return exit(Event::Synchronize);
}
}
private:
cothread_t _host = nullptr; //program thread (used to exit scheduler)
cothread_t _resume = nullptr; //resume thread (used to enter scheduler)
cothread_t _master = nullptr; //primary thread (used to synchronize components)
Mode _mode = Mode::Run;
Event _event = Event::Step;
vector<Thread*> _threads;
};
}

57
higan/emulator/thread.hpp Normal file
View File

@@ -0,0 +1,57 @@
#pragma once
namespace Emulator {
struct Thread {
enum : uintmax { Second = (uintmax)-1 >> 1 };
virtual ~Thread() {
if(_handle) co_delete(_handle);
}
inline auto active() const { return co_active() == _handle; }
inline auto handle() const { return _handle; }
inline auto frequency() const { return _frequency; }
inline auto scalar() const { return _scalar; }
inline auto clock() const { return _clock; }
auto setFrequency(double frequency) -> void {
_frequency = frequency + 0.5;
_scalar = Second / _frequency;
}
auto setScalar(uintmax scalar) -> void {
_scalar = scalar;
}
auto setClock(uintmax clock) -> void {
_clock = clock;
}
auto create(auto (*entrypoint)() -> void, double frequency) -> void {
if(_handle) co_delete(_handle);
_handle = co_create(64 * 1024 * sizeof(void*), entrypoint);
setFrequency(frequency);
setClock(0);
}
inline auto step(uint clocks) -> void {
_clock += _scalar * clocks;
}
auto serialize(serializer& s) -> void {
s.integer(_frequency);
s.integer(_scalar);
s.integer(_clock);
}
protected:
cothread_t _handle = nullptr;
uintmax _frequency = 0;
uintmax _scalar = 0;
uintmax _clock = 0;
friend class Scheduler;
};
}

129
higan/emulator/types.hpp Normal file
View File

@@ -0,0 +1,129 @@
using int1 = nall::Integer< 1>;
using int2 = nall::Integer< 2>;
using int3 = nall::Integer< 3>;
using int4 = nall::Integer< 4>;
using int5 = nall::Integer< 5>;
using int6 = nall::Integer< 6>;
using int7 = nall::Integer< 7>;
using int8 = nall::Integer< 8>;
using int9 = nall::Integer< 9>;
using int10 = nall::Integer<10>;
using int11 = nall::Integer<11>;
using int12 = nall::Integer<12>;
using int13 = nall::Integer<13>;
using int14 = nall::Integer<14>;
using int15 = nall::Integer<15>;
using int16 = nall::Integer<16>;
using int17 = nall::Integer<17>;
using int18 = nall::Integer<18>;
using int19 = nall::Integer<19>;
using int20 = nall::Integer<20>;
using int21 = nall::Integer<21>;
using int22 = nall::Integer<22>;
using int23 = nall::Integer<23>;
using int24 = nall::Integer<24>;
using int25 = nall::Integer<25>;
using int26 = nall::Integer<26>;
using int27 = nall::Integer<27>;
using int28 = nall::Integer<28>;
using int29 = nall::Integer<29>;
using int30 = nall::Integer<30>;
using int31 = nall::Integer<31>;
using int32 = nall::Integer<32>;
using int33 = nall::Integer<33>;
using int34 = nall::Integer<34>;
using int35 = nall::Integer<35>;
using int36 = nall::Integer<36>;
using int37 = nall::Integer<37>;
using int38 = nall::Integer<38>;
using int39 = nall::Integer<39>;
using int40 = nall::Integer<40>;
using int41 = nall::Integer<41>;
using int42 = nall::Integer<42>;
using int43 = nall::Integer<43>;
using int44 = nall::Integer<44>;
using int45 = nall::Integer<45>;
using int46 = nall::Integer<46>;
using int47 = nall::Integer<47>;
using int48 = nall::Integer<48>;
using int49 = nall::Integer<49>;
using int50 = nall::Integer<50>;
using int51 = nall::Integer<51>;
using int52 = nall::Integer<52>;
using int53 = nall::Integer<53>;
using int54 = nall::Integer<54>;
using int55 = nall::Integer<55>;
using int56 = nall::Integer<56>;
using int57 = nall::Integer<57>;
using int58 = nall::Integer<58>;
using int59 = nall::Integer<59>;
using int60 = nall::Integer<60>;
using int61 = nall::Integer<61>;
using int62 = nall::Integer<62>;
using int63 = nall::Integer<63>;
using int64 = nall::Integer<64>;
using uint1 = nall::Natural< 1>;
using uint2 = nall::Natural< 2>;
using uint3 = nall::Natural< 3>;
using uint4 = nall::Natural< 4>;
using uint5 = nall::Natural< 5>;
using uint6 = nall::Natural< 6>;
using uint7 = nall::Natural< 7>;
using uint8 = nall::Natural< 8>;
using uint9 = nall::Natural< 9>;
using uint10 = nall::Natural<10>;
using uint11 = nall::Natural<11>;
using uint12 = nall::Natural<12>;
using uint13 = nall::Natural<13>;
using uint14 = nall::Natural<14>;
using uint15 = nall::Natural<15>;
using uint16 = nall::Natural<16>;
using uint17 = nall::Natural<17>;
using uint18 = nall::Natural<18>;
using uint19 = nall::Natural<19>;
using uint20 = nall::Natural<20>;
using uint21 = nall::Natural<21>;
using uint22 = nall::Natural<22>;
using uint23 = nall::Natural<23>;
using uint24 = nall::Natural<24>;
using uint25 = nall::Natural<25>;
using uint26 = nall::Natural<26>;
using uint27 = nall::Natural<27>;
using uint28 = nall::Natural<28>;
using uint29 = nall::Natural<29>;
using uint30 = nall::Natural<30>;
using uint31 = nall::Natural<31>;
using uint32 = nall::Natural<32>;
using uint33 = nall::Natural<33>;
using uint34 = nall::Natural<34>;
using uint35 = nall::Natural<35>;
using uint36 = nall::Natural<36>;
using uint37 = nall::Natural<37>;
using uint38 = nall::Natural<38>;
using uint39 = nall::Natural<39>;
using uint40 = nall::Natural<40>;
using uint41 = nall::Natural<41>;
using uint42 = nall::Natural<42>;
using uint43 = nall::Natural<43>;
using uint44 = nall::Natural<44>;
using uint45 = nall::Natural<45>;
using uint46 = nall::Natural<46>;
using uint47 = nall::Natural<47>;
using uint48 = nall::Natural<48>;
using uint49 = nall::Natural<49>;
using uint50 = nall::Natural<50>;
using uint51 = nall::Natural<51>;
using uint52 = nall::Natural<52>;
using uint53 = nall::Natural<53>;
using uint54 = nall::Natural<54>;
using uint55 = nall::Natural<55>;
using uint56 = nall::Natural<56>;
using uint57 = nall::Natural<57>;
using uint58 = nall::Natural<58>;
using uint59 = nall::Natural<59>;
using uint60 = nall::Natural<60>;
using uint61 = nall::Natural<61>;
using uint62 = nall::Natural<62>;
using uint63 = nall::Natural<63>;
using uint64 = nall::Natural<64>;

View File

@@ -1,16 +1,13 @@
processors += r6502
objects += fc-interface fc-system fc-scheduler fc-controller
objects += fc-interface fc-system fc-controller
objects += fc-memory fc-cartridge fc-cpu fc-apu fc-ppu
objects += fc-cheat
obj/fc-interface.o: fc/interface/interface.cpp $(call rwildcard,fc/interface/)
obj/fc-system.o: fc/system/system.cpp $(call rwildcard,fc/system/)
obj/fc-scheduler.o: fc/scheduler/scheduler.cpp $(call rwildcard,fc/scheduler/)
obj/fc-controller.o: fc/controller/controller.cpp $(call rwildcard,fc/controller/)
obj/fc-memory.o: fc/memory/memory.cpp $(call rwildcard,fc/memory/)
obj/fc-cartridge.o: fc/cartridge/cartridge.cpp $(call rwildcard,fc/cartridge/)
obj/fc-cpu.o: fc/cpu/cpu.cpp $(call rwildcard,fc/cpu/)
obj/fc-apu.o: fc/apu/apu.cpp $(call rwildcard,fc/apu/)
obj/fc-ppu.o: fc/ppu/ppu.cpp $(call rwildcard,fc/ppu/)
obj/fc-cheat.o: fc/cheat/cheat.cpp $(call rwildcard,fc/cheat/)

View File

@@ -63,8 +63,8 @@ auto APU::main() -> void {
}
auto APU::tick() -> void {
clock += 12;
if(clock >= 0 && !scheduler.synchronizing()) co_switch(cpu.thread);
Thread::step(12);
synchronize(cpu);
}
auto APU::setIRQ() -> void {
@@ -88,8 +88,8 @@ auto APU::power() -> void {
}
auto APU::reset() -> void {
create(APU::Enter, 21'477'272);
stream = Emulator::audio.createStream(1, 21'477'272.0 / 12.0);
create(APU::Enter, system.colorburst() * 6.0);
stream = Emulator::audio.createStream(1, system.colorburst() / 2.0);
pulse[0].reset();
pulse[1].reset();

View File

@@ -42,22 +42,22 @@ Board::Board(Markup::Node& document) {
if(chrram.size) chrram.data = new uint8_t[chrram.size]();
if(prgrom.name = prom["name"].text()) {
if(auto fp = interface->open(cartridge.pathID(), prgrom.name, File::Read, File::Required)) {
if(auto fp = platform->open(cartridge.pathID(), prgrom.name, File::Read, File::Required)) {
fp->read(prgrom.data, min(prgrom.size, fp->size()));
}
}
if(prgram.name = pram["name"].text()) {
if(auto fp = interface->open(cartridge.pathID(), prgram.name, File::Read)) {
if(auto fp = platform->open(cartridge.pathID(), prgram.name, File::Read)) {
fp->read(prgram.data, min(prgram.size, fp->size()));
}
}
if(chrrom.name = crom["name"].text()) {
if(auto fp = interface->open(cartridge.pathID(), chrrom.name, File::Read, File::Required)) {
if(auto fp = platform->open(cartridge.pathID(), chrrom.name, File::Read, File::Required)) {
fp->read(chrrom.data, min(chrrom.size, fp->size()));
}
}
if(chrram.name = cram["name"].text()) {
if(auto fp = interface->open(cartridge.pathID(), chrram.name, File::Read)) {
if(auto fp = platform->open(cartridge.pathID(), chrram.name, File::Read)) {
fp->read(chrram.data, min(chrram.size, fp->size()));
}
}
@@ -70,13 +70,13 @@ auto Board::save() -> void {
auto document = BML::unserialize(cartridge.manifest());
if(auto name = document["board/prg/ram/name"].text()) {
if(auto fp = interface->open(cartridge.pathID(), name, File::Write)) {
if(auto fp = platform->open(cartridge.pathID(), name, File::Write)) {
fp->write(prgram.data, prgram.size);
}
}
if(auto name = document["board/chr/ram/name"].text()) {
if(auto fp = interface->open(cartridge.pathID(), name, File::Write)) {
if(auto fp = platform->open(cartridge.pathID(), name, File::Write)) {
fp->write(chrram.data, chrram.size);
}
}
@@ -109,13 +109,13 @@ auto Board::mirror(uint addr, uint size) -> uint {
}
auto Board::main() -> void {
cartridge.clock += 12 * 4095;
cartridge.step(12 * 4095);
tick();
}
auto Board::tick() -> void {
cartridge.clock += 12;
if(cartridge.clock >= 0 && !scheduler.synchronizing()) co_switch(cpu.thread);
cartridge.step(12);
cartridge.synchronize(cpu);
}
auto Board::readCHR(uint addr) -> uint8 {

View File

@@ -15,11 +15,11 @@ auto Cartridge::main() -> void {
}
auto Cartridge::load() -> bool {
if(auto pathID = interface->load(ID::Famicom, "Famicom", "fc")) {
if(auto pathID = platform->load(ID::Famicom, "Famicom", "fc")) {
information.pathID = pathID();
} else return false;
if(auto fp = interface->open(pathID(), "manifest.bml", File::Read, File::Required)) {
if(auto fp = platform->open(pathID(), "manifest.bml", File::Read, File::Required)) {
information.manifest = fp->reads();
} else {
return false;
@@ -29,8 +29,8 @@ auto Cartridge::load() -> bool {
if(!board) return false;
Hash::SHA256 sha;
sha.data(board->prgrom.data, board->prgrom.size);
sha.data(board->chrrom.data, board->chrrom.size);
sha.input(board->prgrom.data, board->prgrom.size);
sha.input(board->chrrom.data, board->chrrom.size);
information.sha256 = sha.digest();
return true;
}
@@ -49,7 +49,7 @@ auto Cartridge::power() -> void {
}
auto Cartridge::reset() -> void {
create(Cartridge::Enter, 21'477'272);
create(Cartridge::Enter, system.colorburst() * 6.0);
board->reset();
}

View File

@@ -1,28 +0,0 @@
#include <fc/fc.hpp>
namespace Famicom {
Cheat cheat;
auto Cheat::reset() -> void {
codes.reset();
}
auto Cheat::append(uint addr, uint data) -> void {
codes.append({addr, Unused, data});
}
auto Cheat::append(uint addr, uint comp, uint data) -> void {
codes.append({addr, comp, data});
}
auto Cheat::find(uint addr, uint comp) -> maybe<uint> {
for(auto& code : codes) {
if(code.addr == addr && (code.comp == Unused || code.comp == comp)) {
return code.data;
}
}
return nothing;
}
}

View File

@@ -1,17 +0,0 @@
struct Cheat {
struct Code {
uint addr;
uint comp;
uint data;
};
vector<Code> codes;
enum : uint { Unused = ~0u };
alwaysinline auto enable() const -> bool { return codes.size() > 0; }
auto reset() -> void;
auto append(uint addr, uint data) -> void;
auto append(uint addr, uint comp, uint data) -> void;
auto find(uint addr, uint comp) -> maybe<uint>;
};
extern Cheat cheat;

View File

@@ -5,22 +5,24 @@ namespace Famicom {
#include "gamepad/gamepad.cpp"
Controller::Controller(bool port) : port(port) {
if(!thread) create(Controller::Enter, 1);
if(!handle()) create(Controller::Enter, 1);
}
Controller::~Controller() {
scheduler.remove(*this);
}
auto Controller::Enter() -> void {
while(true) {
scheduler.synchronize();
if(co_active() == peripherals.controllerPort1->thread) peripherals.controllerPort1->main();
if(co_active() == peripherals.controllerPort2->thread) peripherals.controllerPort2->main();
if(peripherals.controllerPort1->active()) peripherals.controllerPort1->main();
if(peripherals.controllerPort2->active()) peripherals.controllerPort2->main();
}
}
auto Controller::main() -> void {
step(1);
synchronize(cpu);
}
}

View File

@@ -16,7 +16,7 @@
// 6: data4 $4016.d4 read $4017.d4 read
// 7: gnd
struct Controller : Cothread {
struct Controller : Thread {
enum : bool { Port1 = 0, Port2 = 1 };
Controller(bool port);

View File

@@ -3,7 +3,7 @@ Gamepad::Gamepad(bool port) : Controller(port) {
auto Gamepad::data() -> uint3 {
if(counter >= 8) return 1;
if(latched == 1) return interface->inputPoll(port, ID::Device::Gamepad, A);
if(latched == 1) return platform->inputPoll(port, ID::Device::Gamepad, A);
switch(counter++) {
case 0: return a;
@@ -24,13 +24,13 @@ auto Gamepad::latch(bool data) -> void {
counter = 0;
if(latched == 0) {
a = interface->inputPoll(port, ID::Device::Gamepad, A);
b = interface->inputPoll(port, ID::Device::Gamepad, B);
select = interface->inputPoll(port, ID::Device::Gamepad, Select);
start = interface->inputPoll(port, ID::Device::Gamepad, Start);
up = interface->inputPoll(port, ID::Device::Gamepad, Up);
down = interface->inputPoll(port, ID::Device::Gamepad, Down);
left = interface->inputPoll(port, ID::Device::Gamepad, Left);
right = interface->inputPoll(port, ID::Device::Gamepad, Right);
a = platform->inputPoll(port, ID::Device::Gamepad, A);
b = platform->inputPoll(port, ID::Device::Gamepad, B);
select = platform->inputPoll(port, ID::Device::Gamepad, Select);
start = platform->inputPoll(port, ID::Device::Gamepad, Start);
up = platform->inputPoll(port, ID::Device::Gamepad, Up);
down = platform->inputPoll(port, ID::Device::Gamepad, Down);
left = platform->inputPoll(port, ID::Device::Gamepad, Left);
right = platform->inputPoll(port, ID::Device::Gamepad, Right);
}
}

View File

@@ -17,19 +17,11 @@ auto CPU::main() -> void {
}
auto CPU::step(uint clocks) -> void {
apu.clock -= clocks;
if(apu.clock < 0) co_switch(apu.thread);
ppu.clock -= clocks;
if(ppu.clock < 0) co_switch(ppu.thread);
cartridge.clock -= clocks;
if(cartridge.clock < 0) co_switch(cartridge.thread);
for(auto peripheral : peripherals) {
peripheral->clock -= clocks * (uint64)peripheral->frequency;
if(peripheral->clock < 0) co_switch(peripheral->thread);
}
Thread::step(clocks);
synchronize(apu);
synchronize(ppu);
synchronize(cartridge);
for(auto peripheral : peripherals) synchronize(*peripheral);
}
auto CPU::power() -> void {
@@ -44,7 +36,7 @@ auto CPU::power() -> void {
auto CPU::reset() -> void {
R6502::reset();
create(CPU::Enter, 21'477'272);
create(CPU::Enter, system.colorburst() * 6.0);
regs.pc = bus.read(0xfffc) << 0;
regs.pc |= bus.read(0xfffd) << 8;

View File

@@ -4,39 +4,31 @@
//started: 2011-09-05
#include <emulator/emulator.hpp>
#include <emulator/thread.hpp>
#include <emulator/scheduler.hpp>
#include <emulator/cheat.hpp>
#include <processor/r6502/r6502.hpp>
namespace Famicom {
#define platform Emulator::platform
using File = Emulator::File;
using Scheduler = Emulator::Scheduler;
using Cheat = Emulator::Cheat;
extern Scheduler scheduler;
extern Cheat cheat;
struct Thread {
~Thread() {
if(thread) co_delete(thread);
struct Thread : Emulator::Thread {
auto create(auto (*entrypoint)() -> void, double frequency) -> void {
Emulator::Thread::create(entrypoint, frequency);
scheduler.append(*this);
}
auto create(auto (*entrypoint)() -> void, uint frequency) -> void {
if(thread) co_delete(thread);
thread = co_create(65'536 * sizeof(void*), entrypoint);
this->frequency = frequency;
clock = 0;
inline auto synchronize(Thread& thread) -> void {
if(clock() >= thread.clock()) scheduler.resume(thread);
}
auto serialize(serializer& s) -> void {
s.integer(frequency);
s.integer(clock);
}
cothread_t thread = nullptr;
uint frequency = 0;
int64 clock = 0;
};
struct Cothread : Thread {
auto step(uint clocks) -> void;
auto synchronizeCPU() -> void;
};
#include <fc/scheduler/scheduler.hpp>
#include <fc/controller/controller.hpp>
#include <fc/system/system.hpp>
#include <fc/memory/memory.hpp>
@@ -44,16 +36,6 @@ namespace Famicom {
#include <fc/cpu/cpu.hpp>
#include <fc/apu/apu.hpp>
#include <fc/ppu/ppu.hpp>
#include <fc/cheat/cheat.hpp>
inline auto Cothread::step(uint clocks) -> void {
clock += clocks * (uint64)cpu.frequency;
synchronizeCPU();
}
inline auto Cothread::synchronizeCPU() -> void {
if(clock >= 0 && !scheduler.synchronizing()) co_switch(cpu.thread);
}
}
#include <fc/interface/interface.hpp>

View File

@@ -2,18 +2,12 @@
namespace Famicom {
Interface* interface = nullptr;
Settings settings;
Interface::Interface() {
interface = this;
information.manufacturer = "Nintendo";
information.name = "Famicom";
information.width = 256;
information.height = 240;
information.overscan = true;
information.aspectRatio = 8.0 / 7.0;
information.resettable = true;
information.capability.states = true;
@@ -54,6 +48,17 @@ auto Interface::title() -> string {
return cartridge.title();
}
auto Interface::videoSize() -> VideoSize {
return {256, 240};
}
auto Interface::videoSize(uint width, uint height, bool arc) -> VideoSize {
uint w = 256 * (arc ? 8.0 / 7.0 : 1.0);
uint h = 240;
uint m = min(width / w, height / h);
return {w * m, h * m};
}
auto Interface::videoFrequency() -> double {
return 21477272.0 / (262.0 * 1364.0 - 4.0);
}
@@ -126,7 +131,7 @@ auto Interface::sha256() -> string {
}
auto Interface::load(uint id) -> bool {
return system.load();
return system.load(this);
}
auto Interface::save() -> void {
@@ -139,7 +144,7 @@ auto Interface::unload() -> void {
}
auto Interface::connect(uint port, uint device) -> void {
Famicom::peripherals.connect(port, device);
peripherals.connect(port, device);
}
auto Interface::power() -> void {
@@ -164,15 +169,7 @@ auto Interface::unserialize(serializer& s) -> bool {
}
auto Interface::cheatSet(const string_vector& list) -> void {
cheat.reset();
for(auto& codeset : list) {
auto codes = codeset.split("+");
for(auto& code : codes) {
auto part = code.split("/");
if(part.size() == 2) cheat.append(part[0].hex(), part[1].hex());
if(part.size() == 3) cheat.append(part[0].hex(), part[1].hex(), part[2].hex());
}
}
cheat.assign(list);
}
auto Interface::cap(const string& name) -> bool {

View File

@@ -25,9 +25,13 @@ struct Interface : Emulator::Interface {
auto manifest() -> string override;
auto title() -> string override;
auto videoSize() -> VideoSize override;
auto videoSize(uint width, uint height, bool arc) -> VideoSize override;
auto videoFrequency() -> double override;
auto videoColors() -> uint32 override;
auto videoColor(uint32 color) -> uint64 override;
auto audioFrequency() -> double override;
auto loaded() -> bool override;
@@ -60,7 +64,6 @@ struct Settings {
uint expansionPort = 0;
};
extern Interface* interface;
extern Settings settings;
}

View File

@@ -17,7 +17,7 @@ auto Bus::read(uint16 addr) -> uint8 {
else if(addr <= 0x3fff) data = ppu.readIO(addr);
else if(addr <= 0x4017) data = cpu.readIO(addr);
if(cheat.enable()) {
if(cheat) {
if(auto result = cheat.find(addr, data)) return result();
}

View File

@@ -27,8 +27,8 @@ auto PPU::step(uint clocks) -> void {
if(io.ly == 261 && io.lx == 0) io.nmiFlag = io.nmiHold;
if(io.ly == 261 && io.lx == 2) cpu.nmiLine(io.nmiEnable && io.nmiFlag);
clock += 4;
if(clock >= 0 && !scheduler.synchronizing()) co_switch(cpu.thread);
Thread::step(4);
synchronize(cpu);
io.lx++;
}
@@ -56,7 +56,7 @@ auto PPU::power() -> void {
}
auto PPU::reset() -> void {
create(PPU::Enter, 21'477'272);
create(PPU::Enter, system.colorburst() * 6.0);
memory::fill(&io, sizeof(IO));
memory::fill(&latch, sizeof(Latches));

View File

@@ -1,44 +0,0 @@
#include <fc/fc.hpp>
namespace Famicom {
Scheduler scheduler;
auto Scheduler::reset() -> void {
host = co_active();
resume = cpu.thread;
}
auto Scheduler::enter(Mode mode_) -> Event {
mode = mode_;
host = co_active();
co_switch(resume);
if(event == Event::Frame) ppu.refresh();
return event;
}
auto Scheduler::exit(Event event_) -> void {
event = event_;
resume = co_active();
co_switch(host);
}
auto Scheduler::synchronize(cothread_t thread) -> void {
if(thread == cpu.thread) {
while(enter(Mode::SynchronizeCPU) != Event::Synchronize);
} else {
resume = thread;
while(enter(Mode::SynchronizeAll) != Event::Synchronize);
}
}
auto Scheduler::synchronize() -> void {
if(co_active() == cpu.thread && mode == Mode::SynchronizeCPU) return exit(Event::Synchronize);
if(co_active() != cpu.thread && mode == Mode::SynchronizeAll) return exit(Event::Synchronize);
}
auto Scheduler::synchronizing() const -> bool {
return mode == Mode::SynchronizeAll;
}
}

View File

@@ -1,28 +0,0 @@
struct Scheduler {
enum class Mode : uint {
Run,
SynchronizeCPU,
SynchronizeAll,
};
enum class Event : uint {
Unknown,
Frame,
Synchronize,
};
auto reset() -> void;
auto enter(Mode = Mode::Run) -> Event;
auto exit(Event) -> void;
auto synchronize(cothread_t) -> void;
auto synchronize() -> void;
auto synchronizing() const -> bool;
private:
cothread_t host = nullptr;
cothread_t resume = nullptr;
Mode mode = Mode::Run;
Event event = Event::Unknown;
};
extern Scheduler scheduler;

View File

@@ -6,30 +6,33 @@ namespace Famicom {
#include "video.cpp"
#include "serialization.cpp"
System system;
Scheduler scheduler;
Cheat cheat;
auto System::run() -> void {
scheduler.enter();
if(scheduler.enter() == Scheduler::Event::Frame) ppu.refresh();
}
auto System::runToSave() -> void {
scheduler.synchronize(cpu.thread);
scheduler.synchronize(apu.thread);
scheduler.synchronize(ppu.thread);
scheduler.synchronize(cartridge.thread);
for(auto peripheral : cpu.peripherals) {
scheduler.synchronize(peripheral->thread);
}
scheduler.synchronize(cpu);
scheduler.synchronize(apu);
scheduler.synchronize(ppu);
scheduler.synchronize(cartridge);
for(auto peripheral : cpu.peripherals) scheduler.synchronize(*peripheral);
}
auto System::load() -> bool {
auto System::load(Emulator::Interface* interface) -> bool {
information = Information();
if(auto fp = interface->open(ID::System, "manifest.bml", File::Read, File::Required)) {
if(auto fp = platform->open(ID::System, "manifest.bml", File::Read, File::Required)) {
information.manifest = fp->reads();
} else {
return false;
}
auto document = BML::unserialize(information.manifest);
if(!cartridge.load()) return false;
this->interface = interface;
information.colorburst = Emulator::Constants::Colorburst::NTSC;
serializeInit();
return information.loaded = true;
}
@@ -62,11 +65,12 @@ auto System::reset() -> void {
Emulator::audio.reset();
Emulator::audio.setInterface(interface);
scheduler.reset();
cartridge.reset();
cpu.reset();
apu.reset();
ppu.reset();
scheduler.reset();
scheduler.primary(cpu);
peripherals.reset();
}

View File

@@ -1,10 +1,11 @@
struct System {
auto loaded() const -> bool { return information.loaded; }
auto colorburst() const -> double { return information.colorburst; }
auto run() -> void;
auto runToSave() -> void;
auto load() -> bool;
auto load(Emulator::Interface*) -> bool;
auto save() -> void;
auto unload() -> void;
auto power() -> void;
@@ -25,12 +26,15 @@ struct System {
auto serializeAll(serializer&) -> void;
auto serializeInit() -> void;
private:
Emulator::Interface* interface = nullptr;
struct Information {
bool loaded = false;
double colorburst = 0.0;
string manifest;
} information;
private:
uint _serializeSize = 0;
};

View File

@@ -1,16 +1,13 @@
processors += lr35902
objects += gb-interface gb-system gb-scheduler
objects += gb-interface gb-system
objects += gb-memory gb-cartridge
objects += gb-cpu gb-ppu gb-apu
objects += gb-cheat
obj/gb-interface.o: gb/interface/interface.cpp $(call rwildcard,gb/interface/)
obj/gb-system.o: gb/system/system.cpp $(call rwildcard,gb/system/)
obj/gb-scheduler.o: gb/scheduler/scheduler.cpp $(call rwildcard,gb/scheduler/)
obj/gb-cartridge.o: gb/cartridge/cartridge.cpp $(call rwildcard,gb/cartridge/)
obj/gb-memory.o: gb/memory/memory.cpp $(call rwildcard,gb/memory/)
obj/gb-cpu.o: gb/cpu/cpu.cpp $(call rwildcard,gb/cpu/)
obj/gb-ppu.o: gb/ppu/ppu.cpp $(call rwildcard,gb/ppu/)
obj/gb-apu.o: gb/apu/apu.cpp $(call rwildcard,gb/apu/)
obj/gb-cheat.o: gb/cheat/cheat.cpp $(call rwildcard,gb/cheat/)

View File

@@ -29,7 +29,7 @@ auto APU::main() -> void {
stream->sample(sequencer.left / 32768.0, sequencer.right / 32768.0);
} else {
double samples[] = {sequencer.left / 32768.0, sequencer.right / 32768.0};
interface->audioSample(samples, 2);
//interface->audioSample(samples, 2);
}
if(cycle == 0) { //512hz
@@ -51,8 +51,8 @@ auto APU::main() -> void {
}
cycle++;
clock += cpu.frequency;
if(clock >= 0 && !scheduler.synchronizing()) co_switch(cpu.thread);
Thread::step(1);
synchronize(cpu);
}
//filter to remove DC bias

View File

@@ -19,23 +19,23 @@ auto Cartridge::load(System::Revision revision) -> bool {
switch(revision) {
case System::Revision::GameBoy:
if(auto pathID = interface->load(ID::GameBoy, "Game Boy", "gb")) {
if(auto pathID = platform->load(ID::GameBoy, "Game Boy", "gb")) {
information.pathID = pathID();
} else return false;
break;
case System::Revision::SuperGameBoy:
if(auto pathID = interface->load(ID::SuperGameBoy, "Game Boy", "gb")) {
if(auto pathID = platform->load(ID::SuperGameBoy, "Game Boy", "gb")) {
information.pathID = pathID();
} else return false;
break;
case System::Revision::GameBoyColor:
if(auto pathID = interface->load(ID::GameBoyColor, "Game Boy Color", "gbc")) {
if(auto pathID = platform->load(ID::GameBoyColor, "Game Boy Color", "gbc")) {
information.pathID = pathID();
} else return false;
break;
}
if(auto fp = interface->open(pathID(), "manifest.bml", File::Read, File::Required)) {
if(auto fp = platform->open(pathID(), "manifest.bml", File::Read, File::Required)) {
information.manifest = fp->reads();
} else return false;
@@ -64,12 +64,12 @@ auto Cartridge::load(System::Revision revision) -> bool {
ram.data = (uint8*)memory::allocate(ram.size, 0xff);
if(auto name = board["rom/name"].text()) {
if(auto fp = interface->open(pathID(), name, File::Read, File::Required)) {
if(auto fp = platform->open(pathID(), name, File::Read, File::Required)) {
fp->read(rom.data, min(rom.size, fp->size()));
}
}
if(auto name = board["ram/name"].text()) {
if(auto fp = interface->open(pathID(), name, File::Read, File::Optional)) {
if(auto fp = platform->open(pathID(), name, File::Read, File::Optional)) {
fp->read(ram.data, min(ram.size, fp->size()));
}
}
@@ -96,7 +96,7 @@ auto Cartridge::save() -> void {
auto document = BML::unserialize(information.manifest);
if(auto name = document["board/ram/name"].text()) {
if(auto fp = interface->open(pathID(), name, File::Write)) {
if(auto fp = platform->open(pathID(), name, File::Write)) {
fp->write(ram.data, ram.size);
}
}

View File

@@ -1,28 +0,0 @@
#include <gb/gb.hpp>
namespace GameBoy {
Cheat cheat;
auto Cheat::reset() -> void {
codes.reset();
}
auto Cheat::append(uint addr, uint data) -> void {
codes.append({addr, Unused, data});
}
auto Cheat::append(uint addr, uint comp, uint data) -> void {
codes.append({addr, comp, data});
}
auto Cheat::find(uint addr, uint comp) -> maybe<uint> {
for(auto& code : codes) {
if(code.addr == addr && (code.comp == Unused || code.comp == comp)) {
return code.data;
}
}
return nothing;
}
}

View File

@@ -1,18 +0,0 @@
struct Cheat {
struct Code {
uint addr;
uint comp;
uint data;
};
vector<Code> codes;
enum : uint { Unused = ~0u };
alwaysinline auto enable() const -> bool { return codes.size() > 0; }
auto reset() -> void;
auto append(uint addr, uint data) -> void;
auto append(uint addr, uint comp, uint data) -> void;
auto find(uint addr, uint comp) -> maybe<uint>;
};
extern Cheat cheat;

View File

@@ -77,8 +77,8 @@ auto CPU::stop() -> bool {
if(status.speedSwitch) {
status.speedSwitch = 0;
status.speedDouble ^= 1;
if(status.speedDouble == 0) frequency = 4 * 1024 * 1024;
if(status.speedDouble == 1) frequency = 8 * 1024 * 1024;
if(status.speedDouble == 0) setFrequency(4 * 1024 * 1024);
if(status.speedDouble == 1) setFrequency(8 * 1024 * 1024);
return true;
}
return false;

View File

@@ -8,15 +8,15 @@ auto CPU::wramAddress(uint16 addr) const -> uint {
auto CPU::joypPoll() -> void {
uint button = 0, dpad = 0;
button |= interface->inputPoll(0, 0, (uint)Input::Start) << 3;
button |= interface->inputPoll(0, 0, (uint)Input::Select) << 2;
button |= interface->inputPoll(0, 0, (uint)Input::B) << 1;
button |= interface->inputPoll(0, 0, (uint)Input::A) << 0;
button |= platform->inputPoll(0, 0, (uint)Input::Start) << 3;
button |= platform->inputPoll(0, 0, (uint)Input::Select) << 2;
button |= platform->inputPoll(0, 0, (uint)Input::B) << 1;
button |= platform->inputPoll(0, 0, (uint)Input::A) << 0;
dpad |= interface->inputPoll(0, 0, (uint)Input::Down) << 3;
dpad |= interface->inputPoll(0, 0, (uint)Input::Up) << 2;
dpad |= interface->inputPoll(0, 0, (uint)Input::Left) << 1;
dpad |= interface->inputPoll(0, 0, (uint)Input::Right) << 0;
dpad |= platform->inputPoll(0, 0, (uint)Input::Down) << 3;
dpad |= platform->inputPoll(0, 0, (uint)Input::Up) << 2;
dpad |= platform->inputPoll(0, 0, (uint)Input::Left) << 1;
dpad |= platform->inputPoll(0, 0, (uint)Input::Right) << 0;
if(system.revision() != System::Revision::SuperGameBoy) {
//D-pad pivot makes it impossible to press opposing directions at the same time
@@ -145,7 +145,7 @@ auto CPU::writeIO(uint16 addr, uint8 data) -> void {
if(addr == 0xff00) { //JOYP
status.p15 = data & 0x20;
status.p14 = data & 0x10;
interface->joypWrite(status.p15, status.p14);
//interface->joypWrite(status.p15, status.p14);
return;
}

View File

@@ -16,11 +16,9 @@ auto CPU::step(uint clocks) -> void {
if((status.div & 511) == 0) timer8192hz();
if((status.div & 1023) == 0) timer4096hz();
ppu.clock -= ppu.frequency;
if(ppu.clock < 0) co_switch(ppu.thread);
apu.clock -= apu.frequency;
if(apu.clock < 0) co_switch(apu.thread);
Thread::step(1);
synchronize(ppu);
synchronize(apu);
}
if(system.sgb()) {

View File

@@ -4,41 +4,37 @@
//started: 2010-12-27
#include <emulator/emulator.hpp>
#include <emulator/thread.hpp>
#include <emulator/scheduler.hpp>
#include <emulator/cheat.hpp>
#include <processor/lr35902/lr35902.hpp>
namespace GameBoy {
#define platform Emulator::platform
using File = Emulator::File;
using Scheduler = Emulator::Scheduler;
using Cheat = Emulator::Cheat;
extern Scheduler scheduler;
extern Cheat cheat;
struct Thread {
~Thread() {
if(thread) co_delete(thread);
struct Thread : Emulator::Thread {
auto create(auto (*entrypoint)() -> void, double frequency) -> void {
Emulator::Thread::create(entrypoint, frequency);
scheduler.append(*this);
}
auto create(auto (*entrypoint)() -> void, uint frequency) -> void {
if(thread) co_delete(thread);
thread = co_create(65'536 * sizeof(void*), entrypoint);
this->frequency = frequency;
clock = 0;
inline auto synchronize(Thread& thread) -> void {
if(clock() >= thread.clock()) scheduler.resume(thread);
}
auto serialize(serializer& s) -> void {
s.integer(frequency);
s.integer(clock);
}
cothread_t thread = nullptr;
uint frequency = 0;
int64 clock = 0;
};
#include <gb/memory/memory.hpp>
#include <gb/system/system.hpp>
#include <gb/scheduler/scheduler.hpp>
#include <gb/cartridge/cartridge.hpp>
#include <gb/cpu/cpu.hpp>
#include <gb/ppu/ppu.hpp>
#include <gb/apu/apu.hpp>
#include <gb/cheat/cheat.hpp>
}
#include <gb/interface/interface.hpp>

View File

@@ -2,19 +2,14 @@
namespace GameBoy {
Interface* interface = nullptr;
Settings settings;
Interface::Interface() {
interface = this;
hook = nullptr;
information.manufacturer = "Nintendo";
information.name = "Game Boy";
information.width = 160;
information.height = 144;
information.overscan = false;
information.aspectRatio = 1.0;
information.resettable = false;
information.capability.states = true;
@@ -48,6 +43,17 @@ auto Interface::title() -> string {
return cartridge.title();
}
auto Interface::videoSize() -> VideoSize {
return {160, 144};
}
auto Interface::videoSize(uint width, uint height, bool arc) -> VideoSize {
uint w = 160;
uint h = 144;
uint m = min(width / w, height / h);
return {w * m, h * m};
}
auto Interface::videoFrequency() -> double {
return 4194304.0 / (154.0 * 456.0);
}
@@ -126,9 +132,9 @@ auto Interface::sha256() -> string {
}
auto Interface::load(uint id) -> bool {
if(id == ID::GameBoy) return system.load(System::Revision::GameBoy);
if(id == ID::SuperGameBoy) return system.load(System::Revision::SuperGameBoy);
if(id == ID::GameBoyColor) return system.load(System::Revision::GameBoyColor);
if(id == ID::GameBoy) return system.load(this, System::Revision::GameBoy);
if(id == ID::SuperGameBoy) return system.load(this, System::Revision::SuperGameBoy);
if(id == ID::GameBoyColor) return system.load(this, System::Revision::GameBoyColor);
return false;
}
@@ -163,15 +169,7 @@ auto Interface::unserialize(serializer& s) -> bool {
}
auto Interface::cheatSet(const string_vector& list) -> void {
cheat.reset();
for(auto& codeset : list) {
auto codes = codeset.split("+");
for(auto& code : codes) {
auto part = code.split("/");
if(part.size() == 2) cheat.append(part[0].hex(), part[1].hex());
if(part.size() == 3) cheat.append(part[0].hex(), part[1].hex(), part[2].hex());
}
}
cheat.assign(list);
}
auto Interface::lcdScanline() -> void {

View File

@@ -24,9 +24,13 @@ struct Interface : Emulator::Interface {
auto manifest() -> string override;
auto title() -> string override;
auto videoSize() -> VideoSize override;
auto videoSize(uint width, uint height, bool arc) -> VideoSize override;
auto videoFrequency() -> double override;
auto videoColors() -> uint32 override;
auto videoColor(uint32 color) -> uint64 override;
auto audioFrequency() -> double override;
auto loaded() -> bool override;
@@ -66,7 +70,6 @@ struct Settings {
bool colorEmulation = true;
};
extern Interface* interface;
extern Settings settings;
}

View File

@@ -38,7 +38,7 @@ auto Memory::free() -> void {
auto Bus::read(uint16 addr) -> uint8 {
uint8 data = mmio[addr]->readIO(addr);
if(cheat.enable()) {
if(cheat) {
if(auto result = cheat.find(addr, data)) return result();
}

View File

@@ -78,7 +78,7 @@ auto PPU::runDMG() -> void {
uint32* output = screen + status.ly * 160 + px++;
*output = color;
interface->lcdOutput(color); //Super Game Boy notification
//interface->lcdOutput(color); //Super Game Boy notification
}
auto PPU::runBackgroundDMG() -> void {

View File

@@ -118,9 +118,9 @@ auto PPU::writeIO(uint16 addr, uint8 data) -> void {
status.lx = 0;
//restart cothread to begin new frame
auto clock = this->clock;
auto clock = Thread::clock();
create(Enter, 4 * 1024 * 1024);
this->clock = clock;
Thread::setClock(clock);
}
status.displayEnable = data & 0x80;

View File

@@ -16,7 +16,7 @@ auto PPU::Enter() -> void {
auto PPU::main() -> void {
status.lx = 0;
interface->lcdScanline(); //Super Game Boy notification
//interface->lcdScanline(); //Super Game Boy notification
if(status.ly <= 143) {
mode(2);
@@ -94,8 +94,8 @@ auto PPU::step(uint clocks) -> void {
}
status.lx++;
clock += cpu.frequency;
if(clock >= 0 && !scheduler.synchronizing()) co_switch(cpu.thread);
Thread::step(1);
synchronize(cpu);
}
}

View File

@@ -1,44 +0,0 @@
#include <gb/gb.hpp>
namespace GameBoy {
Scheduler scheduler;
auto Scheduler::power() -> void {
host = co_active();
resume = cpu.thread;
}
auto Scheduler::enter(Mode mode_) -> Event {
mode = mode_;
host = co_active();
co_switch(resume);
if(event == Event::Frame) ppu.refresh();
return event;
}
auto Scheduler::exit(Event event_) -> void {
event = event_;
resume = co_active();
co_switch(host);
}
auto Scheduler::synchronize(cothread_t thread) -> void {
if(thread == cpu.thread) {
while(enter(Mode::SynchronizeCPU) != Event::Synchronize);
} else {
resume = thread;
while(enter(Mode::SynchronizeAll) != Event::Synchronize);
}
}
auto Scheduler::synchronize() -> void {
if(co_active() == cpu.thread && mode == Mode::SynchronizeCPU) return exit(Event::Synchronize);
if(co_active() != cpu.thread && mode == Mode::SynchronizeAll) return exit(Event::Synchronize);
}
auto Scheduler::synchronizing() const -> bool {
return mode == Mode::SynchronizeAll;
}
}

View File

@@ -1,29 +0,0 @@
struct Scheduler {
enum class Mode : uint {
Run,
SynchronizeCPU,
SynchronizeAll,
};
enum class Event : uint {
Unknown,
Step,
Frame,
Synchronize,
};
auto power() -> void;
auto enter(Mode = Mode::Run) -> Event;
auto exit(Event) -> void;
auto synchronize(cothread_t) -> void;
auto synchronize() -> void;
auto synchronizing() const -> bool;
private:
cothread_t host = nullptr;
cothread_t resume = nullptr;
Mode mode = Mode::Run;
Event event = Event::Unknown;
};
extern Scheduler scheduler;

View File

@@ -5,25 +5,27 @@ namespace GameBoy {
#include "video.cpp"
#include "serialization.cpp"
System system;
Scheduler scheduler;
Cheat cheat;
auto System::run() -> void {
scheduler.enter();
if(scheduler.enter() == Scheduler::Event::Frame) ppu.refresh();
}
auto System::runToSave() -> void {
scheduler.synchronize(cpu.thread);
scheduler.synchronize(ppu.thread);
scheduler.synchronize(apu.thread);
scheduler.synchronize(cpu);
scheduler.synchronize(ppu);
scheduler.synchronize(apu);
}
auto System::init() -> void {
assert(interface != nullptr);
}
auto System::load(Revision revision) -> bool {
auto System::load(Emulator::Interface* interface, Revision revision) -> bool {
_revision = revision;
if(auto fp = interface->open(ID::System, "manifest.bml", File::Read, File::Required)) {
if(auto fp = platform->open(ID::System, "manifest.bml", File::Read, File::Required)) {
information.manifest = fp->reads();
} else return false;
@@ -32,7 +34,7 @@ auto System::load(Revision revision) -> bool {
if(revision == Revision::SuperGameBoy) path = "board/icd2/rom/name";
if(auto name = document[path].text()) {
if(auto fp = interface->open(ID::System, name, File::Read, File::Required)) {
if(auto fp = platform->open(ID::System, name, File::Read, File::Required)) {
if(revision == Revision::GameBoy) fp->read(bootROM.dmg, 256);
if(revision == Revision::SuperGameBoy) fp->read(bootROM.sgb, 256);
if(revision == Revision::GameBoyColor) fp->read(bootROM.cgb, 2048);
@@ -41,6 +43,7 @@ auto System::load(Revision revision) -> bool {
if(!cartridge.load(revision)) return false;
serializeInit();
this->interface = interface;
return _loaded = true;
}
@@ -66,12 +69,13 @@ auto System::power() -> void {
Emulator::audio.setInterface(interface);
}
scheduler.reset();
bus.power();
cartridge.power();
cpu.power();
ppu.power();
apu.power();
scheduler.power();
scheduler.primary(cpu);
_clocksExecuted = 0;
}

View File

@@ -1,5 +1,3 @@
struct Interface;
enum class Input : uint {
Up, Down, Left, Right, B, A, Select, Start,
};
@@ -23,7 +21,7 @@ struct System {
auto runToSave() -> void;
auto init() -> void;
auto load(Revision) -> bool;
auto load(Emulator::Interface*, Revision) -> bool;
auto save() -> void;
auto unload() -> void;
auto power() -> void;
@@ -40,6 +38,8 @@ struct System {
auto serializeAll(serializer&) -> void;
auto serializeInit() -> void;
Emulator::Interface* interface = nullptr;
struct BootROM {
uint8 dmg[ 256];
uint8 sgb[ 256];

View File

@@ -1,12 +1,11 @@
processors += arm
objects += gba-memory gba-interface gba-scheduler gba-system
objects += gba-memory gba-interface gba-system
objects += gba-cartridge gba-player
objects += gba-cpu gba-ppu gba-apu
obj/gba-memory.o: gba/memory/memory.cpp $(call rwildcard,gba/memory)
obj/gba-interface.o: gba/interface/interface.cpp $(call rwildcard,gba/interface)
obj/gba-scheduler.o: gba/scheduler/scheduler.cpp $(call rwildcard,gba/scheduler)
obj/gba-system.o: gba/system/system.cpp $(call rwildcard,gba/system)
obj/gba-cartridge.o: gba/cartridge/cartridge.cpp $(call rwildcard,gba/cartridge)
obj/gba-player.o: gba/player/player.cpp $(call rwildcard,gba/player)

View File

@@ -68,8 +68,8 @@ auto APU::main() -> void {
}
auto APU::step(uint clocks) -> void {
clock += clocks;
if(clock >= 0 && !scheduler.synchronizing()) co_switch(cpu.thread);
Thread::step(clocks);
synchronize(cpu);
}
auto APU::power() -> void {

View File

@@ -26,11 +26,11 @@ Cartridge::~Cartridge() {
auto Cartridge::load() -> bool {
information = Information();
if(auto pathID = interface->load(ID::GameBoyAdvance, "Game Boy Advance", "gba")) {
if(auto pathID = platform->load(ID::GameBoyAdvance, "Game Boy Advance", "gba")) {
information.pathID = pathID();
} else return false;
if(auto fp = interface->open(pathID(), "manifest.bml", File::Read, File::Required)) {
if(auto fp = platform->open(pathID(), "manifest.bml", File::Read, File::Required)) {
information.manifest = fp->reads();
} else return false;
@@ -43,7 +43,7 @@ auto Cartridge::load() -> bool {
if(auto node = document["board/rom"]) {
mrom.size = min(32 * 1024 * 1024, node["size"].natural());
if(auto fp = interface->open(pathID(), node["name"].text(), File::Read, File::Required)) {
if(auto fp = platform->open(pathID(), node["name"].text(), File::Read, File::Required)) {
fp->read(mrom.data, mrom.size);
}
}
@@ -55,7 +55,7 @@ auto Cartridge::load() -> bool {
sram.mask = sram.size - 1;
for(auto n : range(sram.size)) sram.data[n] = 0xff;
if(auto fp = interface->open(pathID(), node["name"].text(), File::Read)) {
if(auto fp = platform->open(pathID(), node["name"].text(), File::Read)) {
fp->read(sram.data, sram.size);
}
}
@@ -69,7 +69,7 @@ auto Cartridge::load() -> bool {
eeprom.test = mrom.size > 16 * 1024 * 1024 ? 0x0dffff00 : 0x0d000000;
for(auto n : range(eeprom.size)) eeprom.data[n] = 0xff;
if(auto fp = interface->open(pathID(), node["name"].text(), File::Read)) {
if(auto fp = platform->open(pathID(), node["name"].text(), File::Read)) {
fp->read(eeprom.data, eeprom.size);
}
}
@@ -85,7 +85,7 @@ auto Cartridge::load() -> bool {
if(!flash.id && flash.size == 64 * 1024) flash.id = 0x1cc2;
if(!flash.id && flash.size == 128 * 1024) flash.id = 0x09c2;
if(auto fp = interface->open(pathID(), node["name"].text(), File::Read)) {
if(auto fp = platform->open(pathID(), node["name"].text(), File::Read)) {
fp->read(flash.data, flash.size);
}
}
@@ -98,7 +98,7 @@ auto Cartridge::load() -> bool {
auto Cartridge::save() -> void {
auto document = BML::unserialize(information.manifest);
if(auto node = document["board/ram"]) {
if(auto fp = interface->open(pathID(), node["name"].text(), File::Write)) {
if(auto fp = platform->open(pathID(), node["name"].text(), File::Write)) {
if(node["type"].text() == "sram") fp->write(sram.data, sram.size);
if(node["type"].text() == "eeprom") fp->write(eeprom.data, eeprom.size);
if(node["type"].text() == "flash") fp->write(flash.data, flash.size);

View File

@@ -83,11 +83,9 @@ auto CPU::step(uint clocks) -> void {
}
auto CPU::syncStep(uint clocks) -> void {
ppu.clock -= clocks;
if(ppu.clock < 0) co_switch(ppu.thread);
apu.clock -= clocks;
if(apu.clock < 0) co_switch(apu.thread);
Thread::step(clocks);
synchronize(ppu);
synchronize(apu);
}
auto CPU::keypadRun() -> void {
@@ -99,7 +97,7 @@ auto CPU::keypadRun() -> void {
bool test = regs.keypad.control.condition; //0 = OR, 1 = AND
for(auto n : range(10)) {
if(!regs.keypad.control.flag[n]) continue;
bool input = interface->inputPoll(0, 0, lookup[n]);
bool input = platform->inputPoll(0, 0, lookup[n]);
if(regs.keypad.control.condition == 0) test |= input;
if(regs.keypad.control.condition == 1) test &= input;
}
@@ -150,7 +148,7 @@ auto CPU::power() -> void {
for(auto& swait : regs.wait.control.swait) swait = 0;
regs.wait.control.phi = 0;
regs.wait.control.prefetch = 0;
regs.wait.control.gametype = 0;
regs.wait.control.gametype = 0; //0 = GBA, 1 = GBC
regs.postboot = 0;
regs.mode = Registers::Mode::Normal;
regs.clock = 0;

View File

@@ -64,7 +64,7 @@ auto CPU::readIO(uint32 addr) -> uint8 {
static const uint lookup[] = {5, 4, 8, 9, 3, 2, 0, 1};
if(auto result = player.keyinput()) return result() >> 0;
uint8 result = 0;
for(uint n = 0; n < 8; n++) result |= interface->inputPoll(0, 0, lookup[n]) << n;
for(uint n = 0; n < 8; n++) result |= platform->inputPoll(0, 0, lookup[n]) << n;
if((result & 0xc0) == 0xc0) result &= (uint8)~0xc0; //up+down cannot be pressed simultaneously
if((result & 0x30) == 0x30) result &= (uint8)~0x30; //left+right cannot be pressed simultaneously
return result ^ 0xff;
@@ -72,8 +72,8 @@ auto CPU::readIO(uint32 addr) -> uint8 {
case 0x04000131: {
if(auto result = player.keyinput()) return result() >> 8;
uint8 result = 0;
result |= interface->inputPoll(0, 0, 7) << 0;
result |= interface->inputPoll(0, 0, 6) << 1;
result |= platform->inputPoll(0, 0, 7) << 0;
result |= platform->inputPoll(0, 0, 6) << 1;
return result ^ 0x03;
}
@@ -383,7 +383,7 @@ auto CPU::writeIO(uint32 addr, uint8 data) -> void {
regs.wait.control.swait[2] = data.bit (2);
regs.wait.control.phi = data.bit (3);
regs.wait.control.prefetch = data.bit (6);
regs.wait.control.gametype = data.bit (7);
//regs.wait.control.gametype is read-only
return;
//IME

View File

@@ -4,10 +4,16 @@
//started: 2012-03-19
#include <emulator/emulator.hpp>
#include <emulator/thread.hpp>
#include <emulator/scheduler.hpp>
#include <processor/arm/arm.hpp>
namespace GameBoyAdvance {
#define platform Emulator::platform
using File = Emulator::File;
using Scheduler = Emulator::Scheduler;
extern Scheduler scheduler;
enum : uint { //mode flags for bus read, write:
Nonsequential = 1, //N cycle
@@ -21,30 +27,22 @@ namespace GameBoyAdvance {
Signed = 256, //sign extended
};
struct Thread {
~Thread() {
if(thread) co_delete(thread);
struct Thread : Emulator::Thread {
auto create(auto (*entrypoint)() -> void, double frequency) -> void {
Emulator::Thread::create(entrypoint, frequency);
scheduler.append(*this);
}
auto create(auto (*entrypoint)() -> void, uint frequency) -> void {
if(thread) co_delete(thread);
thread = co_create(65'536 * sizeof(void*), entrypoint);
this->frequency = frequency;
clock = 0;
inline auto synchronize(Thread& thread) -> void {
if(clock() >= thread.clock()) scheduler.resume(thread);
}
auto serialize(serializer& s) -> void {
s.integer(frequency);
s.integer(clock);
inline auto step(uint clocks) -> void {
_clock += clocks;
}
cothread_t thread = nullptr;
uint frequency = 0;
int clock = 0;
};
#include <gba/memory/memory.hpp>
#include <gba/scheduler/scheduler.hpp>
#include <gba/system/system.hpp>
#include <gba/cartridge/cartridge.hpp>
#include <gba/player/player.hpp>

View File

@@ -2,18 +2,12 @@
namespace GameBoyAdvance {
Interface* interface = nullptr;
Settings settings;
Interface::Interface() {
interface = this;
information.manufacturer = "Nintendo";
information.name = "Game Boy Advance";
information.width = 240;
information.height = 160;
information.overscan = false;
information.aspectRatio = 1.0;
information.resettable = false;
information.capability.states = true;
@@ -49,6 +43,17 @@ auto Interface::title() -> string {
return cartridge.title();
}
auto Interface::videoSize() -> VideoSize {
return {240, 160};
}
auto Interface::videoSize(uint width, uint height, bool arc) -> VideoSize {
uint w = 240;
uint h = 160;
uint m = min(width / w, height / h);
return {w * m, h * m};
}
auto Interface::videoFrequency() -> double {
return 16777216.0 / (228.0 * 1232.0);
}
@@ -88,7 +93,7 @@ auto Interface::loaded() -> bool {
}
auto Interface::load(uint id) -> bool {
return system.load();
return system.load(this);
}
auto Interface::save() -> void {

View File

@@ -22,9 +22,13 @@ struct Interface : Emulator::Interface {
auto manifest() -> string override;
auto title() -> string override;
auto videoSize() -> VideoSize override;
auto videoSize(uint width, uint height, bool arc) -> VideoSize override;
auto videoFrequency() -> double override;
auto videoColors() -> uint32 override;
auto videoColor(uint32 color) -> uint64 override;
auto audioFrequency() -> double override;
auto loaded() -> bool override;
@@ -49,7 +53,6 @@ struct Settings {
bool colorEmulation = true;
};
extern Interface* interface;
extern Settings settings;
}

View File

@@ -100,7 +100,7 @@ auto Player::write(uint2 addr, uint8 byte) -> void {
if(addr == 3 && status.packet == 15) {
status.rumble = (status.recv & 0xff) == 0x26; //on = 0x26, off = 0x04
interface->inputRumble(0, 0, 10, status.rumble);
platform->inputRumble(0, 0, 10, status.rumble);
}
}

View File

@@ -43,8 +43,8 @@ auto PPU::main() -> void {
}
auto PPU::step(uint clocks) -> void {
clock += clocks;
if(clock >= 0 && !scheduler.synchronizing()) co_switch(cpu.thread);
Thread::step(clocks);
synchronize(cpu);
}
auto PPU::power() -> void {

View File

@@ -1,44 +0,0 @@
#include <gba/gba.hpp>
namespace GameBoyAdvance {
Scheduler scheduler;
auto Scheduler::power() -> void {
host = co_active();
resume = cpu.thread;
}
auto Scheduler::enter(Mode mode_) -> Event {
mode = mode_;
host = co_active();
co_switch(resume);
if(event == Event::Frame) ppu.refresh();
return event;
}
auto Scheduler::exit(Event event_) -> void {
event = event_;
resume = co_active();
co_switch(host);
}
auto Scheduler::synchronize(cothread_t thread) -> void {
if(thread == cpu.thread) {
while(enter(Mode::SynchronizeCPU) != Event::Synchronize);
} else {
resume = thread;
while(enter(Mode::SynchronizeAll) != Event::Synchronize);
}
}
auto Scheduler::synchronize() -> void {
if(co_active() == cpu.thread && mode == Mode::SynchronizeCPU) return exit(Event::Synchronize);
if(co_active() != cpu.thread && mode == Mode::SynchronizeAll) return exit(Event::Synchronize);
}
auto Scheduler::synchronizing() const -> bool {
return mode == Mode::SynchronizeAll;
}
}

View File

@@ -1,28 +0,0 @@
struct Scheduler {
enum class Mode : uint {
Run,
SynchronizeCPU,
SynchronizeAll,
};
enum class Event : uint {
Unknown,
Frame,
Synchronize,
};
auto power() -> void;
auto enter(Mode = Mode::Run) -> Event;
auto exit(Event) -> void;
auto synchronize(cothread_t) -> void;
auto synchronize() -> void;
auto synchronizing() const -> bool;
private:
cothread_t host = nullptr;
cothread_t resume = nullptr;
Mode mode = Mode::Run;
Event event = Event::Unknown;
};
extern Scheduler scheduler;

View File

@@ -7,6 +7,7 @@ namespace GameBoyAdvance {
#include "serialization.cpp"
BIOS bios;
System system;
Scheduler scheduler;
auto System::init() -> void {
}
@@ -23,29 +24,33 @@ auto System::power() -> void {
Emulator::audio.reset();
Emulator::audio.setInterface(interface);
scheduler.reset();
bus.power();
player.power();
cpu.power();
ppu.power();
apu.power();
cartridge.power();
scheduler.power();
scheduler.primary(cpu);
}
auto System::load() -> bool {
if(auto fp = interface->open(ID::System, "manifest.bml", File::Read, File::Required)) {
auto System::load(Emulator::Interface* interface) -> bool {
if(auto fp = platform->open(ID::System, "manifest.bml", File::Read, File::Required)) {
information.manifest = fp->reads();
} else return false;
auto document = BML::unserialize(information.manifest);
if(auto name = document["system/cpu/rom/name"].text()) {
if(auto fp = interface->open(ID::System, name, File::Read, File::Required)) {
if(auto fp = platform->open(ID::System, name, File::Read, File::Required)) {
fp->read(bios.data, bios.size);
}
}
if(!cartridge.load()) return false;
serializeInit();
this->interface = interface;
return _loaded = true;
}
@@ -61,13 +66,13 @@ auto System::unload() -> void {
}
auto System::run() -> void {
while(scheduler.enter() != Scheduler::Event::Frame);
if(scheduler.enter() == Scheduler::Event::Frame) ppu.refresh();
}
auto System::runToSave() -> void {
scheduler.synchronize(cpu.thread);
scheduler.synchronize(ppu.thread);
scheduler.synchronize(apu.thread);
scheduler.synchronize(cpu);
scheduler.synchronize(ppu);
scheduler.synchronize(apu);
}
}

View File

@@ -19,7 +19,7 @@ struct System {
auto init() -> void;
auto term() -> void;
auto load() -> bool;
auto load(Emulator::Interface*) -> bool;
auto save() -> void;
auto unload() -> void;
auto power() -> void;
@@ -38,6 +38,9 @@ struct System {
auto serializeAll(serializer&) -> void;
auto serializeInit() -> void;
private:
Emulator::Interface* interface = nullptr;
struct Information {
string manifest;
} information;

17
higan/md/GNUmakefile Normal file
View File

@@ -0,0 +1,17 @@
processors += m68k z80
objects += md-interface
objects += md-cpu md-apu md-vdp md-psg md-ym2612
objects += md-system md-cartridge md-bus
objects += md-controller
obj/md-interface.o: md/interface/interface.cpp $(call rwildcard,md/interface)
obj/md-cpu.o: md/cpu/cpu.cpp $(call rwildcard,md/cpu)
obj/md-apu.o: md/apu/apu.cpp $(call rwildcard,md/apu)
obj/md-vdp.o: md/vdp/vdp.cpp $(call rwildcard,md/vdp)
obj/md-psg.o: md/psg/psg.cpp $(call rwildcard,md/psg)
obj/md-ym2612.o: md/ym2612/ym2612.cpp $(call rwildcard,md/ym2612)
obj/md-system.o: md/system/system.cpp $(call rwildcard,md/system)
obj/md-cartridge.o: md/cartridge/cartridge.cpp $(call rwildcard,md/cartridge)
obj/md-bus.o: md/bus/bus.cpp $(call rwildcard,md/bus)
obj/md-controller.o: md/controller/controller.cpp $(call rwildcard,md/controller)

26
higan/md/apu/apu.cpp Normal file
View File

@@ -0,0 +1,26 @@
#include <md/md.hpp>
namespace MegaDrive {
APU apu;
auto APU::Enter() -> void {
while(true) scheduler.synchronize(), apu.main();
}
auto APU::main() -> void {
step(1);
}
auto APU::step(uint clocks) -> void {
Thread::step(clocks);
synchronize(cpu);
}
auto APU::power() -> void {
Z80::bus = &busAPU;
Z80::power();
create(APU::Enter, system.colorburst());
}
}

11
higan/md/apu/apu.hpp Normal file
View File

@@ -0,0 +1,11 @@
//Zilog Z80
struct APU : Processor::Z80, Thread {
static auto Enter() -> void;
auto main() -> void;
auto step(uint clocks) -> void;
auto power() -> void;
};
extern APU apu;

94
higan/md/bus/bus.cpp Normal file
View File

@@ -0,0 +1,94 @@
#include <md/md.hpp>
namespace MegaDrive {
BusCPU busCPU;
BusAPU busAPU;
auto BusCPU::readByte(uint24 addr) -> uint16 {
if(addr < 0x400000) return cartridge.read(addr & ~1).byte(!addr.bit(0));
if(addr < 0xa00000) return 0x0000;
if(addr < 0xa10000) return 0x0000;
if(addr < 0xa10020) return readIO(addr);
if(addr < 0xc00000) return 0x0000;
if(addr < 0xe00000) return vdp.read(addr & ~1).byte(!addr.bit(0));
return ram[addr & 0xffff];
}
auto BusCPU::readWord(uint24 addr) -> uint16 {
if(addr < 0x400000) return cartridge.read(addr);
if(addr < 0xa00000) return 0x0000;
if(addr < 0xa10000) return 0x0000;
if(addr < 0xa10020) return readIO(addr);
if(addr < 0xc00000) return 0x0000;
if(addr < 0xe00000) return vdp.read(addr);
uint16 data = ram[addr + 0 & 0xffff] << 8;
return data | ram[addr + 1 & 0xffff] << 0;
}
auto BusCPU::writeByte(uint24 addr, uint16 data) -> void {
if(addr < 0x400000) return cartridge.write(addr & ~1, data << 8 | data << 0);
if(addr < 0xa00000) return;
if(addr < 0xa10000) return;
if(addr < 0xa10020) return writeIO(addr, data);
if(addr < 0xc00000) return;
if(addr < 0xe00000) return vdp.write(addr & ~1, data << 8 | data << 0);
ram[addr & 0xffff] = data;
}
auto BusCPU::writeWord(uint24 addr, uint16 data) -> void {
if(addr < 0x400000) return cartridge.write(addr, data);
if(addr < 0xa00000) return;
if(addr < 0xa10000) return;
if(addr < 0xa10020) return writeIO(addr, data);
if(addr < 0xc00000) return;
if(addr < 0xe00000) return vdp.write(addr, data);
ram[addr + 0 & 0xffff] = data >> 8;
ram[addr + 1 & 0xffff] = data >> 0;
}
//
auto BusCPU::readIO(uint24 addr) -> uint16 {
switch(addr & ~1) {
case 0xa10002: return peripherals.controllerPort1->readData();
case 0xa10004: return peripherals.controllerPort2->readData();
case 0xa10006: return peripherals.extensionPort->readData();
case 0xa10008: return peripherals.controllerPort1->readControl();
case 0xa1000a: return peripherals.controllerPort2->readControl();
case 0xa1000c: return peripherals.extensionPort->readControl();
}
return 0x0000;
}
auto BusCPU::writeIO(uint24 addr, uint16 data) -> void {
switch(addr & ~1) {
case 0xa10002: return peripherals.controllerPort1->writeData(data);
case 0xa10004: return peripherals.controllerPort2->writeData(data);
case 0xa10006: return peripherals.extensionPort->writeData(data);
case 0xa10008: return peripherals.controllerPort1->writeControl(data);
case 0xa1000a: return peripherals.controllerPort2->writeControl(data);
case 0xa1000c: return peripherals.extensionPort->writeControl(data);
}
}
//
auto BusAPU::read(uint16 addr) -> uint8 {
return 0x00;
}
auto BusAPU::write(uint16 addr, uint8 data) -> void {
}
auto BusAPU::in(uint8 addr) -> uint8 {
return 0x00;
}
auto BusAPU::out(uint8 addr, uint8 data) -> void {
}
}

22
higan/md/bus/bus.hpp Normal file
View File

@@ -0,0 +1,22 @@
struct BusCPU : Processor::M68K::Bus {
auto readByte(uint24 addr) -> uint16 override;
auto readWord(uint24 addr) -> uint16 override;
auto writeByte(uint24 addr, uint16 data) -> void override;
auto writeWord(uint24 addr, uint16 data) -> void override;
auto readIO(uint24 addr) -> uint16;
auto writeIO(uint24 addr, uint16 data) -> void;
private:
uint8 ram[64 * 1024];
};
struct BusAPU : Processor::Z80::Bus {
auto read(uint16 addr) -> uint8 override;
auto write(uint16 addr, uint8 data) -> void override;
auto in(uint8 addr) -> uint8 override;
auto out(uint8 addr, uint8 data) -> void override;
};
extern BusCPU busCPU;
extern BusAPU busAPU;

View File

@@ -0,0 +1,81 @@
#include <md/md.hpp>
namespace MegaDrive {
Cartridge cartridge;
auto Cartridge::load() -> bool {
information = {};
if(auto pathID = platform->load(ID::MegaDrive, "Mega Drive", "md")) {
information.pathID = pathID();
} else return false;
if(auto fp = platform->open(pathID(), "manifest.bml", File::Read, File::Required)) {
information.manifest = fp->reads();
} else return false;
auto document = BML::unserialize(information.manifest);
information.title = document["information/title"].text();
if(auto node = document["board/rom"]) {
rom.size = node["size"].natural();
rom.mask = bit::round(rom.size) - 1;
if(rom.size) {
rom.data = new uint8[rom.mask + 1];
if(auto name = node["name"].text()) {
if(auto fp = platform->open(pathID(), name, File::Read, File::Required)) {
fp->read(rom.data, rom.size);
}
}
}
}
if(auto node = document["board/ram"]) {
ram.size = node["size"].natural();
ram.mask = bit::round(ram.size) - 1;
if(ram.size) {
ram.data = new uint8[ram.mask + 1];
if(auto name = node["name"].text()) {
if(auto fp = platform->open(pathID(), name, File::Read)) {
fp->read(ram.data, ram.size);
}
}
}
}
return true;
}
auto Cartridge::save() -> void {
auto document = BML::unserialize(information.manifest);
if(auto name = document["board/ram/name"].text()) {
if(auto fp = platform->open(pathID(), name, File::Write)) {
fp->write(ram.data, ram.size);
}
}
}
auto Cartridge::unload() -> void {
delete[] rom.data;
delete[] ram.data;
rom = Memory();
ram = Memory();
}
auto Cartridge::power() -> void {
}
auto Cartridge::reset() -> void {
}
auto Cartridge::read(uint24 addr) -> uint16 {
uint16 data = rom.data[addr + 0 & rom.mask] << 8;
return data | rom.data[addr + 1 & rom.mask] << 0;
}
auto Cartridge::write(uint24 addr, uint16 data) -> void {
}
}

View File

@@ -0,0 +1,33 @@
struct Cartridge {
auto pathID() const -> uint { return information.pathID; }
auto sha256() const -> string { return information.sha256; }
auto manifest() const -> string { return information.manifest; }
auto title() const -> string { return information.title; }
auto load() -> bool;
auto save() -> void;
auto unload() -> void;
auto power() -> void;
auto reset() -> void;
auto read(uint24 addr) -> uint16;
auto write(uint24 addr, uint16 data) -> void;
struct Information {
uint pathID = 0;
string sha256;
string manifest;
string title;
} information;
struct Memory {
uint8* data = nullptr;
uint size = 0;
uint mask = 0;
};
Memory rom;
Memory ram;
};
extern Cartridge cartridge;

View File

@@ -0,0 +1,28 @@
#include <md/md.hpp>
namespace MegaDrive {
#include "gamepad/gamepad.cpp"
Controller::Controller(uint port) : port(port) {
if(!handle()) create(Controller::Enter, 100);
}
Controller::~Controller() {
}
auto Controller::Enter() -> void {
while(true) {
scheduler.synchronize();
if(peripherals.controllerPort1->active()) peripherals.controllerPort1->main();
if(peripherals.controllerPort2->active()) peripherals.controllerPort2->main();
if(peripherals.extensionPort->active()) peripherals.extensionPort->main();
}
}
auto Controller::main() -> void {
step(1);
synchronize(cpu);
}
}

View File

@@ -0,0 +1,17 @@
struct Controller : Thread {
Controller(uint port);
virtual ~Controller();
static auto Enter() -> void;
auto main() -> void;
virtual auto readData() -> uint8 { return 0xff; }
virtual auto writeData(uint8 data) -> void {}
virtual auto readControl() -> uint8 { return 0x00; }
virtual auto writeControl(uint8 data) -> void {}
const uint port;
};
#include "gamepad/gamepad.hpp"

View File

@@ -0,0 +1,30 @@
Gamepad::Gamepad(uint port) : Controller(port) {
}
auto Gamepad::readData() -> uint8 {
uint6 data;
if(select == 0) {
data.bit(0) = platform->inputPoll(port, ID::Device::Gamepad, Up);
data.bit(1) = platform->inputPoll(port, ID::Device::Gamepad, Down);
data.bit(2) = 1;
data.bit(3) = 1;
data.bit(4) = platform->inputPoll(port, ID::Device::Gamepad, A);
data.bit(5) = platform->inputPoll(port, ID::Device::Gamepad, Start);
} else {
data.bit(0) = platform->inputPoll(port, ID::Device::Gamepad, Up);
data.bit(1) = platform->inputPoll(port, ID::Device::Gamepad, Down);
data.bit(2) = platform->inputPoll(port, ID::Device::Gamepad, Left);
data.bit(3) = platform->inputPoll(port, ID::Device::Gamepad, Right);
data.bit(4) = platform->inputPoll(port, ID::Device::Gamepad, B);
data.bit(5) = platform->inputPoll(port, ID::Device::Gamepad, C);
}
data = ~data;
return latch << 7 | select << 6 | data;
}
auto Gamepad::writeData(uint8 data) -> void {
select = data.bit(6);
latch = data.bit(7);
}

View File

@@ -0,0 +1,13 @@
struct Gamepad : Controller {
enum : uint {
Up, Down, Left, Right, A, B, C, X, Y, Z, Start,
};
Gamepad(uint port);
auto readData() -> uint8 override;
auto writeData(uint8 data) -> void override;
boolean select;
boolean latch;
};

85
higan/md/cpu/cpu.cpp Normal file
View File

@@ -0,0 +1,85 @@
#include <md/md.hpp>
namespace MegaDrive {
CPU cpu;
auto CPU::Enter() -> void {
cpu.boot();
while(true) scheduler.synchronize(), cpu.main();
}
auto CPU::boot() -> void {
r.a[7] = bus->readWord(0) << 16 | bus->readWord(2) << 0;
r.pc = bus->readWord(4) << 16 | bus->readWord(6) << 0;
}
auto CPU::main() -> void {
#if 0
static file fp;
if(!fp) fp.open({Path::user(), "Desktop/tracer.log"}, file::mode::write);
fp.print(pad(disassemble(r.pc), -60, ' '), " ", disassembleRegisters().replace("\n", " "), "\n");
#endif
if(state.interruptPending) {
if(state.interruptPending.bit((uint)Interrupt::HorizontalBlank)) {
if(4 > r.i) {
state.interruptPending.bit((uint)Interrupt::HorizontalBlank) = 0;
return exception(Exception::Interrupt, Vector::HorizontalBlank, 4);
}
}
if(state.interruptPending.bit((uint)Interrupt::VerticalBlank)) {
if(6 > r.i) {
state.interruptPending.bit((uint)Interrupt::VerticalBlank) = 0;
return exception(Exception::Interrupt, Vector::VerticalBlank, 6);
}
}
}
instruction();
}
auto CPU::step(uint clocks) -> void {
while(wait) {
Thread::step(1);
synchronize();
}
Thread::step(clocks);
synchronize();
}
auto CPU::synchronize() -> void {
synchronize(apu);
synchronize(vdp);
synchronize(psg);
synchronize(ym2612);
for(auto peripheral : peripherals) synchronize(*peripheral);
}
auto CPU::raise(Interrupt interrupt) -> void {
if(!state.interruptLine.bit((uint)interrupt)) {
state.interruptLine.bit((uint)interrupt) = 1;
state.interruptPending.bit((uint)interrupt) = 1;
}
}
auto CPU::lower(Interrupt interrupt) -> void {
state.interruptLine.bit((uint)interrupt) = 0;
state.interruptPending.bit((uint)interrupt) = 0;
}
auto CPU::power() -> void {
M68K::bus = &busCPU;
M68K::power();
}
auto CPU::reset() -> void {
M68K::reset();
create(CPU::Enter, system.colorburst() * 15.0 / 7.0);
memory::fill(&state, sizeof(State));
}
}

32
higan/md/cpu/cpu.hpp Normal file
View File

@@ -0,0 +1,32 @@
//Motorola 68000
struct CPU : Processor::M68K, Thread {
enum class Interrupt : uint {
HorizontalBlank,
VerticalBlank,
};
using Thread::synchronize;
static auto Enter() -> void;
auto boot() -> void;
auto main() -> void;
auto step(uint clocks) -> void override;
auto synchronize() -> void;
auto raise(Interrupt) -> void;
auto lower(Interrupt) -> void;
auto power() -> void;
auto reset() -> void;
vector<Thread*> peripherals;
private:
struct State {
uint32 interruptLine;
uint32 interruptPending;
} state;
};
extern CPU cpu;

View File

@@ -0,0 +1,144 @@
#include <md/md.hpp>
namespace MegaDrive {
Settings settings;
Interface::Interface() {
information.manufacturer = "Sega";
information.name = "Mega Drive";
information.overscan = true;
information.resettable = true;
information.capability.states = false;
information.capability.cheats = false;
media.append({ID::MegaDrive, "Mega Drive", "md"});
Port controllerPort1{ID::Port::Controller1, "Controller Port 1"};
Port controllerPort2{ID::Port::Controller2, "Controller Port 2"};
Port extensionPort{ID::Port::Extension, "Extension Port"};
{ Device device{ID::Device::None, "None"};
controllerPort1.devices.append(device);
controllerPort2.devices.append(device);
extensionPort.devices.append(device);
}
{ Device device{ID::Device::Gamepad, "Gamepad"};
device.inputs.append({0, "Up" });
device.inputs.append({0, "Down" });
device.inputs.append({0, "Left" });
device.inputs.append({0, "Right"});
device.inputs.append({0, "A" });
device.inputs.append({0, "B" });
device.inputs.append({0, "C" });
device.inputs.append({0, "X" });
device.inputs.append({0, "Y" });
device.inputs.append({0, "Z" });
device.inputs.append({0, "Start"});
controllerPort1.devices.append(device);
controllerPort2.devices.append(device);
}
ports.append(move(controllerPort1));
ports.append(move(controllerPort2));
ports.append(move(extensionPort));
}
auto Interface::manifest() -> string {
return cartridge.manifest();
}
auto Interface::title() -> string {
return cartridge.title();
}
auto Interface::videoSize() -> VideoSize {
return {1280, 480};
}
auto Interface::videoSize(uint width, uint height, bool arc) -> VideoSize {
uint w = 320;
uint h = 240;
uint m = min(width / w, height / h);
return {w * m, h * m};
}
auto Interface::videoFrequency() -> double {
return 60.0;
}
auto Interface::videoColors() -> uint32 {
return 1 << 9;
}
auto Interface::videoColor(uint32 color) -> uint64 {
uint R = color.bits(0,2);
uint G = color.bits(3,5);
uint B = color.bits(6,8);
uint64 r = image::normalize(R, 3, 16);
uint64 g = image::normalize(G, 3, 16);
uint64 b = image::normalize(B, 3, 16);
return r << 32 | g << 16 | b << 0;
}
auto Interface::audioFrequency() -> double {
return 52'000.0;
}
auto Interface::loaded() -> bool {
return system.loaded();
}
auto Interface::load(uint id) -> bool {
return system.load(this);
}
auto Interface::save() -> void {
system.save();
}
auto Interface::unload() -> void {
system.unload();
}
auto Interface::connect(uint port, uint device) -> void {
peripherals.connect(port, device);
}
auto Interface::power() -> void {
system.power();
}
auto Interface::reset() -> void {
system.reset();
}
auto Interface::run() -> void {
system.run();
}
auto Interface::serialize() -> serializer {
return {};
}
auto Interface::unserialize(serializer& s) -> bool {
return false;
}
auto Interface::cap(const string& name) -> bool {
return false;
}
auto Interface::get(const string& name) -> any {
return {};
}
auto Interface::set(const string& name, const any& value) -> bool {
return false;
}
}

View File

@@ -0,0 +1,63 @@
namespace MegaDrive {
struct ID {
enum : uint {
System,
MegaDrive,
};
struct Port { enum : uint {
Controller1,
Controller2,
Extension,
};};
struct Device { enum : uint {
None,
Gamepad,
};};
};
struct Interface : Emulator::Interface {
using Emulator::Interface::load;
Interface();
auto manifest() -> string override;
auto title() -> string override;
auto videoSize() -> VideoSize override;
auto videoSize(uint width, uint height, bool arc) -> VideoSize override;
auto videoFrequency() -> double override;
auto videoColors() -> uint32 override;
auto videoColor(uint32 color) -> uint64 override;
auto audioFrequency() -> double override;
auto loaded() -> bool override;
auto load(uint id) -> bool override;
auto save() -> void override;
auto unload() -> void override;
auto connect(uint port, uint device) -> void override;
auto power() -> void override;
auto reset() -> void override;
auto run() -> void override;
auto serialize() -> serializer override;
auto unserialize(serializer&) -> bool override;
auto cap(const string& name) -> bool override;
auto get(const string& name) -> any override;
auto set(const string& name, const any& value) -> bool override;
};
struct Settings {
uint controllerPort1 = 0;
uint controllerPort2 = 0;
uint extensionPort = 0;
};
extern Settings settings;
}

52
higan/md/md.hpp Normal file
View File

@@ -0,0 +1,52 @@
#pragma once
//license: GPLv3
//started: 2016-07-08
#include <emulator/emulator.hpp>
#include <emulator/thread.hpp>
#include <emulator/scheduler.hpp>
#include <processor/m68k/m68k.hpp>
#include <processor/z80/z80.hpp>
namespace MegaDrive {
#define platform Emulator::platform
using File = Emulator::File;
using Scheduler = Emulator::Scheduler;
extern Scheduler scheduler;
struct Wait {
enum : uint {
VDP_DMA = 1 << 0,
};
};
struct Thread : Emulator::Thread {
auto create(auto (*entrypoint)() -> void, double frequency) -> void {
Emulator::Thread::create(entrypoint, frequency);
scheduler.append(*this);
wait = 0;
}
inline auto synchronize(Thread& thread) -> void {
if(clock() >= thread.clock()) scheduler.resume(thread);
}
uint wait = 0;
};
#include <md/controller/controller.hpp>
#include <md/cpu/cpu.hpp>
#include <md/apu/apu.hpp>
#include <md/vdp/vdp.hpp>
#include <md/psg/psg.hpp>
#include <md/ym2612/ym2612.hpp>
#include <md/system/system.hpp>
#include <md/cartridge/cartridge.hpp>
#include <md/bus/bus.hpp>
}
#include <md/interface/interface.hpp>

29
higan/md/psg/psg.cpp Normal file
View File

@@ -0,0 +1,29 @@
#include <md/md.hpp>
namespace MegaDrive {
PSG psg;
auto PSG::Enter() -> void {
while(true) scheduler.synchronize(), psg.main();
}
auto PSG::main() -> void {
stream->sample(0.0, 0.0);
step(1);
}
auto PSG::step(uint clocks) -> void {
Thread::step(clocks);
synchronize(cpu);
}
auto PSG::power() -> void {
}
auto PSG::reset() -> void {
create(PSG::Enter, 52'000); //system.colorburst());
stream = Emulator::audio.createStream(2, 52'000.0);
}
}

14
higan/md/psg/psg.hpp Normal file
View File

@@ -0,0 +1,14 @@
//TI SN76489
struct PSG : Thread {
shared_pointer<Emulator::Stream> stream;
static auto Enter() -> void;
auto main() -> void;
auto step(uint clocks) -> void;
auto power() -> void;
auto reset() -> void;
};
extern PSG psg;

View File

@@ -0,0 +1,55 @@
Peripherals peripherals;
auto Peripherals::unload() -> void {
delete controllerPort1;
delete controllerPort2;
delete extensionPort;
controllerPort1 = nullptr;
controllerPort2 = nullptr;
extensionPort = nullptr;
}
auto Peripherals::reset() -> void {
connect(ID::Port::Controller1, settings.controllerPort1);
connect(ID::Port::Controller2, settings.controllerPort2);
connect(ID::Port::Extension, settings.extensionPort);
}
auto Peripherals::connect(uint port, uint device) -> void {
if(port == ID::Port::Controller1) {
settings.controllerPort1 = device;
if(!system.loaded()) return;
delete controllerPort1;
switch(device) { default:
case ID::Device::None: controllerPort1 = new Controller(0); break;
case ID::Device::Gamepad: controllerPort1 = new Gamepad(0); break;
}
}
if(port == ID::Port::Controller2) {
settings.controllerPort2 = device;
if(!system.loaded()) return;
delete controllerPort2;
switch(device) { default:
case ID::Device::None: controllerPort2 = new Controller(1); break;
case ID::Device::Gamepad: controllerPort2 = new Gamepad(1); break;
}
}
if(port == ID::Port::Extension) {
settings.extensionPort = device;
if(!system.loaded()) return;
delete extensionPort;
switch(device) { default:
case ID::Device::None: extensionPort = new Controller(2); break;
}
}
cpu.peripherals.reset();
cpu.peripherals.append(controllerPort1);
cpu.peripherals.append(controllerPort2);
cpu.peripherals.append(extensionPort);
}

View File

@@ -0,0 +1,67 @@
#include <md/md.hpp>
namespace MegaDrive {
#include "peripherals.cpp"
System system;
Scheduler scheduler;
auto System::run() -> void {
if(scheduler.enter() == Scheduler::Event::Frame) vdp.refresh();
}
auto System::load(Emulator::Interface* interface) -> bool {
information = {};
if(auto fp = platform->open(ID::System, "manifest.bml", File::Read, File::Required)) {
information.manifest = fp->reads();
} else return false;
auto document = BML::unserialize(information.manifest);
if(!cartridge.load()) return false;
information.colorburst = Emulator::Constants::Colorburst::NTSC;
this->interface = interface;
return information.loaded = true;
}
auto System::save() -> void {
cartridge.save();
}
auto System::unload() -> void {
peripherals.unload();
cartridge.unload();
}
auto System::power() -> void {
cartridge.power();
cpu.power();
apu.power();
vdp.power();
psg.power();
ym2612.power();
reset();
}
auto System::reset() -> void {
Emulator::video.reset();
Emulator::video.setInterface(interface);
Emulator::video.setPalette();
Emulator::audio.reset();
Emulator::audio.setInterface(interface);
scheduler.reset();
cartridge.reset();
cpu.reset();
apu.power();
vdp.reset();
psg.reset();
ym2612.reset();
scheduler.primary(cpu);
peripherals.reset();
}
}

View File

@@ -0,0 +1,34 @@
struct System {
auto loaded() const -> bool { return information.loaded; }
auto colorburst() const -> double { return information.colorburst; }
auto run() -> void;
auto load(Emulator::Interface*) -> bool;
auto save() -> void;
auto unload() -> void;
auto power() -> void;
auto reset() -> void;
private:
Emulator::Interface* interface = nullptr;
struct Information {
bool loaded = false;
string manifest;
double colorburst = 0.0;
} information;
};
struct Peripherals {
auto unload() -> void;
auto reset() -> void;
auto connect(uint port, uint device) -> void;
Controller* controllerPort1 = nullptr;
Controller* controllerPort2 = nullptr;
Controller* extensionPort = nullptr;
};
extern System system;
extern Peripherals peripherals;

View File

@@ -0,0 +1,85 @@
auto VDP::Background::isWindowed(uint x, uint y) -> bool {
if((x < io.horizontalOffset) ^ io.horizontalDirection) return true;
if((y < io.verticalOffset ) ^ io.verticalDirection ) return true;
return false;
}
auto VDP::Background::updateHorizontalScroll(uint y) -> void {
if(id == ID::Window) return;
uint15 address = io.horizontalScrollAddress;
static const uint mask[] = {0u, 7u, ~7u, ~0u};
address += (y & mask[io.horizontalScrollMode]) << 1;
address += id == ID::PlaneB;
state.horizontalScroll = vdp.vram[address].bits(0,9);
}
auto VDP::Background::updateVerticalScroll(uint x, uint y) -> void {
if(id == ID::Window) return;
auto address = (x >> 4 & 0 - io.verticalScrollMode) << 1;
address += id == ID::PlaneB;
state.verticalScroll = vdp.vsram[address];
}
auto VDP::Background::nametableAddress() -> uint15 {
if(id == ID::Window && vdp.screenWidth() == 320) return io.nametableAddress & ~0x0400;
return io.nametableAddress;
}
auto VDP::Background::nametableWidth() -> uint {
if(id == ID::Window) return vdp.screenWidth() == 320 ? 64 : 32;
return 32 * (1 + io.nametableWidth);
}
auto VDP::Background::nametableHeight() -> uint {
if(id == ID::Window) return 32;
return 32 * (1 + io.nametableHeight);
}
auto VDP::Background::scanline(uint y) -> void {
updateHorizontalScroll(y);
}
auto VDP::Background::run(uint x, uint y) -> void {
updateVerticalScroll(x, y);
output.priority = 0;
output.color = 0;
x -= state.horizontalScroll;
y += state.verticalScroll;
uint width = nametableWidth();
uint height = nametableHeight();
uint tileX = x >> 3 & width - 1;
uint tileY = y >> 3 & height - 1;
auto address = nametableAddress();
address += (tileY * width + tileX) & 0x0fff;
uint16 tileAttributes = vdp.vram[address];
uint15 tileAddress = tileAttributes.bits(0,10) << 4;
uint pixelX = (x & 7) ^ (tileAttributes.bit(11) ? 7 : 0);
uint pixelY = (y & 7) ^ (tileAttributes.bit(12) ? 7 : 0);
tileAddress += pixelY << 1 | pixelX >> 2;
uint16 tileData = vdp.vram[tileAddress];
uint4 color = tileData >> (((pixelX & 3) ^ 3) << 2);
if(color) {
output.color = tileAttributes.bits(13,14) << 4 | color;
output.priority = tileAttributes.bit(15);
}
}
auto VDP::Background::power() -> void {
}
auto VDP::Background::reset() -> void {
memory::fill(&io, sizeof(IO));
memory::fill(&state, sizeof(State));
}

36
higan/md/vdp/dma.cpp Normal file
View File

@@ -0,0 +1,36 @@
auto VDP::dmaRun() -> void {
if(!io.dmaEnable) return;
if(!io.command.bit(5)) return;
if(io.dmaMode <= 1) return dmaLoad();
if(io.dmaMode == 2) return dmaFill();
if(io.dmaMode == 3) return dmaCopy();
}
auto VDP::dmaLoad() -> void {
cpu.wait |= Wait::VDP_DMA;
auto data = busCPU.readWord(io.dmaMode.bit(0) << 23 | io.dmaSource << 1);
writeDataPort(data);
io.dmaSource.bits(0,15)++;
if(--io.dmaLength == 0) {
io.command.bit(5) = 0;
cpu.wait &=~ Wait::VDP_DMA;
}
}
auto VDP::dmaFill() -> void {
if(io.dmaFillWait) return;
auto data = io.dmaFillByte;
writeDataPort(data << 8 | data << 0);
io.dmaSource.bits(0,15)++;
if(--io.dmaLength == 0) {
io.command.bit(5) = 0;
}
}
auto VDP::dmaCopy() -> void {
}

318
higan/md/vdp/io.cpp Normal file
View File

@@ -0,0 +1,318 @@
auto VDP::read(uint24 addr) -> uint16 {
switch(addr & 0xc0001e) {
//data port
case 0xc00000: case 0xc00002: {
return readDataPort();
}
//control port
case 0xc00004: case 0xc00006: {
return readControlPort();
}
//counter
case 0xc00008: case 0xc0000a: case 0xc0000c: case 0xc0000e: {
return state.y << 8 | (state.x >> 1) << 0;
}
}
return 0x0000;
}
auto VDP::write(uint24 addr, uint16 data) -> void {
switch(addr & 0xc0001e) {
//data port
case 0xc00000: case 0xc00002: {
return writeDataPort(data);
}
//control port
case 0xc00004: case 0xc00006: {
return writeControlPort(data);
}
}
}
//
auto VDP::readDataPort() -> uint16 {
io.commandPending = false;
//VRAM read
if(io.command.bits(0,3) == 0) {
auto address = io.address.bits(1,15);
auto data = vram[address];
io.address += io.dataIncrement;
return data;
}
//VSRAM read
if(io.command.bits(0,3) == 4) {
auto address = io.address.bits(1,6);
if(address >= 40) return 0x0000;
auto data = vsram[address];
io.address += io.dataIncrement;
return data;
}
//CRAM read
if(io.command.bits(0,3) == 8) {
auto address = io.address.bits(1,6);
auto data = cram[address];
io.address += io.dataIncrement;
return data.bits(0,2) << 1 | data.bits(3,5) << 2 | data.bits(6,8) << 3;
}
return 0x0000;
}
auto VDP::writeDataPort(uint16 data) -> void {
io.commandPending = false;
//DMA VRAM fill
if(io.dmaFillWait.lower()) {
io.dmaFillByte = data >> 8;
}
//VRAM write
if(io.command.bits(0,3) == 1) {
auto address = io.address.bits(1,15);
if(io.address.bit(0)) data = data >> 8 | data << 8;
vram[address] = data;
if(address >= sprite.io.attributeAddress && address < sprite.io.attributeAddress + 320) {
sprite.write(address, data);
}
io.address += io.dataIncrement;
return;
}
//VSRAM write
if(io.command.bits(0,3) == 5) {
auto address = io.address.bits(1,6);
if(address >= 40) return;
//data format: ---- --yy yyyy yyyy
vsram[address] = data.bits(0,9);
io.address += io.dataIncrement;
return;
}
//CRAM write
if(io.command.bits(0,3) == 3) {
auto address = io.address.bits(1,6);
//data format: ---- bbb- ggg- rrr-
cram[address] = data.bits(1,3) << 0 | data.bits(5,7) << 3 | data.bits(9,11) << 6;
io.address += io.dataIncrement;
return;
}
}
//
auto VDP::readControlPort() -> uint16 {
io.commandPending = false;
uint16 result = 0b0011'0100'0000'0000;
result |= 1 << 9; //FIFO empty
result |= (state.y >= 240) << 3; //vertical blank
result |= (state.y >= 240 || state.x >= 320) << 2; //horizontal blank
result |= io.command.bit(5) << 1; //DMA active
return result;
}
auto VDP::writeControlPort(uint16 data) -> void {
//print("[VDPC] ", hex(data, 4L), "\n");
//command write (lo)
if(io.commandPending) {
io.commandPending = false;
io.command.bits(2,5) = data.bits(4,7);
io.address.bits(14,15) = data.bits(0,1);
io.dmaFillWait = io.dmaMode == 2 && io.command.bits(4,5) == 2;
return;
}
//command write (hi)
if(data.bits(14,15) != 2) {
io.commandPending = true;
io.command.bits(0,1) = data.bits(14,15);
io.address.bits(0,13) = data.bits(0,13);
return;
}
//register write (d13 is ignored)
if(data.bits(14,15) == 2)
switch(data.bits(8,12)) {
//mode register 1
case 0x00: {
io.displayOverlayEnable = data.bit(0);
io.counterLatch = data.bit(1);
io.horizontalBlankInterruptEnable = data.bit(4);
io.leftColumnBlank = data.bit(5);
return;
}
//mode register 2
case 0x01: {
io.videoMode = data.bit(2);
io.overscan = data.bit(3);
io.dmaEnable = data.bit(4);
io.verticalBlankInterruptEnable = data.bit(5);
io.displayEnable = data.bit(6);
io.externalVRAM = data.bit(7);
if(!io.dmaEnable) io.command.bit(5) = 0;
return;
}
//plane A name table location
case 0x02: {
planeA.io.nametableAddress = data.bits(3,6) << 12;
return;
}
//window name table location
case 0x03: {
window.io.nametableAddress = data.bits(1,6) << 10;
return;
}
//plane B name table location
case 0x04: {
planeB.io.nametableAddress = data.bits(0,3) << 12;
return;
}
//sprite attribute table location
case 0x05: {
sprite.io.attributeAddress = data.bits(0,7) << 8;
return;
}
//sprite pattern base address
case 0x06: {
sprite.io.nametableAddressBase = data.bit(5);
return;
}
//background color
case 0x07: {
io.backgroundColor = data.bits(0,5);
return;
}
//horizontal interrupt counter
case 0x0a: {
io.horizontalInterruptCounter = data.bits(0,7);
return;
}
//mode register 3
case 0x0b: {
planeA.io.horizontalScrollMode = data.bits(0,1);
planeB.io.horizontalScrollMode = data.bits(0,1);
planeA.io.verticalScrollMode = data.bit(2);
planeB.io.verticalScrollMode = data.bit(2);
io.externalInterruptEnable = data.bit(3);
return;
}
//mode register 4
case 0x0c: {
io.tileWidth = data.bit(0) | data.bit(7) << 1;
io.interlaceMode = data.bits(1,2);
io.shadowHighlightEnable = data.bit(3);
io.externalColorEnable = data.bit(4);
io.horizontalSync = data.bit(5);
io.verticalSync = data.bit(6);
return;
}
//horizontal scroll data location
case 0x0d: {
planeA.io.horizontalScrollAddress = data.bits(0,6) << 9;
planeB.io.horizontalScrollAddress = data.bits(0,6) << 9;
return;
}
//nametable pattern base address
case 0x0e: {
io.nametableBasePatternA = data.bit(0);
io.nametableBasePatternB = data.bit(1);
return;
}
//data port auto-increment value
case 0x0f: {
io.dataIncrement = data.bits(0,7);
return;
}
//plane size
case 0x10: {
planeA.io.nametableWidth = data.bits(0,1);
planeB.io.nametableWidth = data.bits(0,1);
planeA.io.nametableHeight = data.bits(4,5);
planeB.io.nametableHeight = data.bits(4,5);
return;
}
//window plane horizontal position
case 0x11: {
window.io.horizontalDirection = data.bit(7);
window.io.horizontalOffset = data.bits(0,4) << 4;
return;
}
//window plane vertical position
case 0x12: {
window.io.verticalDirection = data.bit(7);
window.io.verticalOffset = data.bits(0,4) << 3;
return;
}
//DMA length
case 0x13: {
io.dmaLength.bits(0,7) = data.bits(0,7);
return;
}
//DMA length
case 0x14: {
io.dmaLength.bits(8,15) = data.bits(0,7);
return;
}
//DMA source
case 0x15: {
io.dmaSource.bits(0,7) = data.bits(0,7);
return;
}
//DMA source
case 0x16: {
io.dmaSource.bits(8,15) = data.bits(0,7);
return;
}
//DMA source
case 0x17: {
io.dmaSource.bits(16,21) = data.bits(0,5);
io.dmaMode = data.bits(6,7);
return;
}
//unused
default: {
return;
}
}
}

44
higan/md/vdp/render.cpp Normal file
View File

@@ -0,0 +1,44 @@
auto VDP::scanline() -> void {
state.x = 0;
if(++state.y >= 262) state.y = 0;
if(state.y < screenHeight()) {
planeA.scanline(state.y);
window.scanline(state.y);
planeB.scanline(state.y);
sprite.scanline(state.y);
}
if(state.y == 240) scheduler.exit(Scheduler::Event::Frame);
state.output = buffer + (state.y * 2 + 0) * 1280;
}
auto VDP::run() -> void {
if(!io.displayEnable) return outputPixel(0);
if(state.y >= screenHeight()) return outputPixel(0);
auto& planeA = window.isWindowed(state.x, state.y) ? window : this->planeA;
planeA.run(state.x, state.y);
planeB.run(state.x, state.y);
sprite.run(state.x, state.y);
auto output = io.backgroundColor;
if(auto color = planeB.output.color) output = color;
if(auto color = planeA.output.color) output = color;
if(auto color = sprite.output.color) output = color;
if(planeB.output.priority) if(auto color = planeB.output.color) output = color;
if(planeA.output.priority) if(auto color = planeA.output.color) output = color;
if(sprite.output.priority) if(auto color = sprite.output.color) output = color;
outputPixel(cram[output]);
state.x++;
}
auto VDP::outputPixel(uint9 color) -> void {
for(auto n : range(4)) {
state.output[ 0 + n] = color;
state.output[1280 + n] = color;
}
state.output += 4;
}

90
higan/md/vdp/sprite.cpp Normal file
View File

@@ -0,0 +1,90 @@
auto VDP::Sprite::write(uint9 address, uint16 data) -> void {
if(address > 320) return;
auto& object = oam[address >> 2];
switch(address.bits(0,1)) {
case 0: {
object.y = data.bits(0,8);
break;
}
case 1: {
object.link = data.bits(0,6);
object.height = 1 + data.bits(8,9) << 3;
object.width = 1 + data.bits(10,11) << 3;
break;
}
case 2: {
object.address = data.bits(0,10) << 4;
object.horizontalFlip = data.bit(11);
object.verticalFlip = data.bit(12);
object.palette = data.bits(13,14);
object.priority = data.bit(15);
break;
}
case 3: {
object.x = data.bits(0,8);
break;
}
}
}
auto VDP::Sprite::scanline(uint y) -> void {
objects.reset();
uint7 link = 0;
uint tiles = 0;
do {
auto& object = oam[link];
link = object.link;
if(128 + y < object.y) continue;
if(128 + y >= object.y + object.height) continue;
if(object.x == 0) break;
objects.append(object);
tiles += object.width >> 3;
} while(link && link < 80 && objects.size() < 20 && tiles < 40);
}
auto VDP::Sprite::run(uint x, uint y) -> void {
output.priority = 0;
output.color = 0;
for(auto& o : objects) {
if(128 + x < o.x) continue;
if(128 + x >= o.x + o.width) continue;
uint objectX = 128 + x - o.x;
uint objectY = 128 + y - o.y;
if(o.horizontalFlip) objectX = (o.width - 1) - objectX;
if(o.verticalFlip) objectY = (o.height - 1) - objectY;
uint tileX = objectX >> 3;
uint tileY = objectY >> 3;
uint tileNumber = tileX * (o.height >> 3) + tileY;
uint15 tileAddress = o.address + (tileNumber << 4);
uint pixelX = objectX & 7;
uint pixelY = objectY & 7;
tileAddress += pixelY << 1 | pixelX >> 2;
uint16 tileData = vdp.vram[tileAddress];
uint4 color = tileData >> (((pixelX & 3) ^ 3) << 2);
if(color) {
output.color = o.palette << 4 | color;
output.priority = o.priority;
break;
}
}
}
auto VDP::Sprite::power() -> void {
}
auto VDP::Sprite::reset() -> void {
memory::fill(&io, sizeof(IO));
}

72
higan/md/vdp/vdp.cpp Normal file
View File

@@ -0,0 +1,72 @@
#include <md/md.hpp>
namespace MegaDrive {
VDP vdp;
#include "io.cpp"
#include "dma.cpp"
#include "render.cpp"
#include "background.cpp"
#include "sprite.cpp"
auto VDP::Enter() -> void {
while(true) scheduler.synchronize(), vdp.main();
}
auto VDP::main() -> void {
scanline();
if(state.y < screenHeight()) {
if(state.y == 0) {
cpu.lower(CPU::Interrupt::VerticalBlank);
}
cpu.lower(CPU::Interrupt::HorizontalBlank);
for(uint x : range(320)) {
run();
step(4);
}
if(io.horizontalBlankInterruptEnable) {
cpu.raise(CPU::Interrupt::HorizontalBlank);
}
step(430);
} else {
if(state.y == screenHeight()) {
if(io.verticalBlankInterruptEnable) {
cpu.raise(CPU::Interrupt::VerticalBlank);
}
}
step(1710);
}
}
auto VDP::step(uint clocks) -> void {
while(clocks--) {
dmaRun();
Thread::step(1);
synchronize(cpu);
}
}
auto VDP::refresh() -> void {
Emulator::video.refresh(buffer, 1280 * sizeof(uint32), 1280, 480);
}
auto VDP::power() -> void {
planeA.power();
window.power();
planeB.power();
sprite.power();
}
auto VDP::reset() -> void {
create(VDP::Enter, system.colorburst() * 15.0 / 2.0);
memory::fill(&io, sizeof(IO));
memory::fill(&state, sizeof(State));
planeA.reset();
window.reset();
planeB.reset();
sprite.reset();
}
}

194
higan/md/vdp/vdp.hpp Normal file
View File

@@ -0,0 +1,194 @@
//Yamaha YM7101
struct VDP : Thread {
static auto Enter() -> void;
auto main() -> void;
auto step(uint clocks) -> void;
auto refresh() -> void;
auto power() -> void;
auto reset() -> void;
//io.cpp
auto read(uint24 addr) -> uint16;
auto write(uint24 addr, uint16 data) -> void;
auto readDataPort() -> uint16;
auto writeDataPort(uint16 data) -> void;
auto readControlPort() -> uint16;
auto writeControlPort(uint16 data) -> void;
//dma.cpp
auto dmaRun() -> void;
auto dmaLoad() -> void;
auto dmaFill() -> void;
auto dmaCopy() -> void;
//render.cpp
auto scanline() -> void;
auto run() -> void;
auto outputPixel(uint9 color) -> void;
//background.cpp
struct Background {
enum class ID : uint { PlaneA, Window, PlaneB } id;
auto isWindowed(uint x, uint y) -> bool;
auto updateHorizontalScroll(uint y) -> void;
auto updateVerticalScroll(uint x, uint y) -> void;
auto nametableAddress() -> uint15;
auto nametableWidth() -> uint;
auto nametableHeight() -> uint;
auto scanline(uint y) -> void;
auto run(uint x, uint y) -> void;
auto power() -> void;
auto reset() -> void;
struct IO {
uint15 nametableAddress;
//PlaneA, PlaneB
uint2 nametableWidth;
uint2 nametableHeight;
uint15 horizontalScrollAddress;
uint2 horizontalScrollMode;
uint1 verticalScrollMode;
//Window
uint1 horizontalDirection;
uint10 horizontalOffset;
uint1 verticalDirection;
uint10 verticalOffset;
} io;
struct State {
uint10 horizontalScroll;
uint10 verticalScroll;
} state;
struct Output {
uint6 color;
boolean priority;
} output;
};
Background planeA{Background::ID::PlaneA};
Background window{Background::ID::Window};
Background planeB{Background::ID::PlaneB};
//sprite.cpp
struct Sprite {
auto write(uint9 addr, uint16 data) -> void;
auto scanline(uint y) -> void;
auto run(uint x, uint y) -> void;
auto power() -> void;
auto reset() -> void;
struct IO {
uint15 attributeAddress;
uint1 nametableAddressBase;
} io;
struct Object {
uint9 x;
uint9 y;
uint width;
uint height;
bool horizontalFlip;
bool verticalFlip;
uint2 palette;
uint1 priority;
uint15 address;
uint7 link;
};
struct Output {
uint6 color;
boolean priority;
} output;
array<Object, 80> oam;
array<Object, 20> objects;
};
Sprite sprite;
private:
auto screenWidth() const -> uint { return io.tileWidth ? 320 : 256; }
auto screenHeight() const -> uint { return io.overscan ? 240 : 224; }
uint16 vram[32768];
uint16 vramExpansion[32768]; //not present in stock Mega Drive hardware
uint9 cram[64];
uint10 vsram[40];
struct IO {
//internal state
boolean dmaFillWait;
uint8 dmaFillByte;
//command
uint6 command;
uint16 address;
boolean commandPending;
//$00 mode register 1
uint1 displayOverlayEnable;
uint1 counterLatch;
uint1 horizontalBlankInterruptEnable;
uint1 leftColumnBlank;
//$01 mode register 2
uint1 videoMode; //0 = Master System; 1 = Mega Drive
uint1 overscan; //0 = 224 lines; 1 = 240 lines
uint1 dmaEnable;
uint1 verticalBlankInterruptEnable;
uint1 displayEnable;
uint1 externalVRAM;
//$07 background color
uint6 backgroundColor;
//$0a horizontal interrupt counter
uint8 horizontalInterruptCounter;
//$0b mode register 3
uint1 externalInterruptEnable;
//$0c mode register 4
uint2 tileWidth;
uint2 interlaceMode;
uint1 shadowHighlightEnable;
uint1 externalColorEnable;
uint1 horizontalSync;
uint1 verticalSync;
//$0e nametable pattern base address
uint1 nametableBasePatternA;
uint1 nametableBasePatternB;
//$0f data port auto-increment value
uint8 dataIncrement;
//$13-$14 DMA length
uint16 dmaLength;
//$15-$17 DMA source
uint22 dmaSource;
uint2 dmaMode;
} io;
struct State {
uint32* output = nullptr;
uint x;
uint y;
} state;
uint32 buffer[1280 * 480];
};
extern VDP vdp;

View File

@@ -0,0 +1,27 @@
#include <md/md.hpp>
namespace MegaDrive {
YM2612 ym2612;
auto YM2612::Enter() -> void {
while(true) scheduler.synchronize(), ym2612.main();
}
auto YM2612::main() -> void {
step(1);
}
auto YM2612::step(uint clocks) -> void {
Thread::step(clocks);
synchronize(cpu);
}
auto YM2612::power() -> void {
}
auto YM2612::reset() -> void {
create(YM2612::Enter, system.colorburst() * 15.0 / 7.0);
}
}

View File

@@ -0,0 +1,12 @@
//Yamaha YM2612
struct YM2612 : Thread {
static auto Enter() -> void;
auto main() -> void;
auto step(uint clocks) -> void;
auto power() -> void;
auto reset() -> void;
};
extern YM2612 ym2612;

15
higan/ms/GNUmakefile Normal file
View File

@@ -0,0 +1,15 @@
processors += z80
objects += ms-interface
objects += ms-cpu ms-vdp ms-psg
objects += ms-system ms-cartridge ms-bus
objects += ms-controller
obj/ms-interface.o: ms/interface/interface.cpp $(call rwildcard,ms/interface)
obj/ms-cpu.o: ms/cpu/cpu.cpp $(call rwildcard,ms/cpu)
obj/ms-vdp.o: ms/vdp/vdp.cpp $(call rwildcard,ms/vdp)
obj/ms-psg.o: ms/psg/psg.cpp $(call rwildcard,ms/psg)
obj/ms-system.o: ms/system/system.cpp $(call rwildcard,ms/system)
obj/ms-cartridge.o: ms/cartridge/cartridge.cpp $(call rwildcard,ms/cartridge)
obj/ms-bus.o: ms/bus/bus.cpp $(call rwildcard,ms/bus)
obj/ms-controller.o: ms/controller/controller.cpp $(call rwildcard,ms/controller)

86
higan/ms/bus/bus.cpp Normal file
View File

@@ -0,0 +1,86 @@
#include <ms/ms.hpp>
namespace MasterSystem {
Bus bus;
auto Bus::read(uint16 addr) -> uint8 {
if(auto data = cartridge.read(addr)) return data();
if(addr >= 0xc000) return ram[addr & 0x1fff];
return 0x00;
}
auto Bus::write(uint16 addr, uint8 data) -> void {
if(cartridge.write(addr, data)) return;
if(addr >= 0xc000) ram[addr & 0x1fff] = data;
}
auto Bus::in(uint8 addr) -> uint8 {
switch(addr >> 6) {
case 0: {
if(system.model() == Model::GameGear) {
bool start = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 6);
return start << 7 | 0x7f;
}
return 0xff; //SMS1 = MDR, SMS2 = 0xff
}
case 1: {
return !addr.bit(0) ? vdp.vcounter() : vdp.hcounter();
}
case 2: {
return !addr.bit(0) ? vdp.data() : vdp.status();
}
case 3: {
if(system.model() == Model::MasterSystem) {
bool reset = !platform->inputPoll(ID::Port::Hardware, ID::Device::MasterSystemControls, 0);
auto port1 = peripherals.controllerPort1->readData();
auto port2 = peripherals.controllerPort2->readData();
if(addr.bit(0) == 0) {
return port1.bits(0,5) << 0 | port2.bits(0,1) << 6;
} else {
return port2.bits(2,5) << 0 | reset << 4 | 1 << 5 | port1.bit(6) << 6 | port2.bit(6) << 7;
}
}
if(system.model() == Model::GameGear) {
bool up = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 0);
bool down = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 1);
bool left = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 2);
bool right = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 3);
bool one = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 4);
bool two = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 5);
if(!up && !down) up = 1, down = 1;
if(!left && !right) left = 1, right = 1;
if(addr.bit(0) == 0) {
return up << 0 | down << 1 | left << 2 | right << 3 | one << 4 | two << 5 | 1 << 6 | 1 << 7;
} else {
return 0xff;
}
}
return 0xff;
}
}
return 0xff;
}
auto Bus::out(uint8 addr, uint8 data) -> void {
switch(addr >> 6) {
case 2: {
return !addr.bit(0) ? vdp.data(data) : vdp.control(data);
}
case 3: {
return; //unmapped
}
}
}
}

12
higan/ms/bus/bus.hpp Normal file
View File

@@ -0,0 +1,12 @@
struct Bus : Processor::Z80::Bus {
auto read(uint16 addr) -> uint8 override;
auto write(uint16 addr, uint8 data) -> void override;
auto in(uint8 addr) -> uint8 override;
auto out(uint8 addr, uint8 data) -> void override;
private:
uint8 ram[0x2000];
};
extern Bus bus;

View File

@@ -0,0 +1,109 @@
#include <ms/ms.hpp>
namespace MasterSystem {
Cartridge cartridge;
#include "mapper.cpp"
auto Cartridge::load() -> bool {
information = {};
switch(system.model()) {
case Model::MasterSystem:
if(auto pathID = platform->load(ID::MasterSystem, "Master System", "ms")) {
information.pathID = pathID();
} else return false;
break;
case Model::GameGear:
if(auto pathID = platform->load(ID::GameGear, "Game Gear", "gg")) {
information.pathID = pathID();
} else return false;
break;
}
if(auto fp = platform->open(pathID(), "manifest.bml", File::Read, File::Required)) {
information.manifest = fp->reads();
} else return false;
auto document = BML::unserialize(information.manifest);
information.title = document["information/title"].text();
if(auto node = document["board/rom"]) {
rom.size = node["size"].natural();
rom.mask = bit::round(rom.size) - 1;
if(rom.size) {
rom.data = new uint8[rom.mask + 1];
if(auto name = node["name"].text()) {
if(auto fp = platform->open(pathID(), name, File::Read, File::Required)) {
fp->read(rom.data, rom.size);
}
}
}
}
if(auto node = document["board/ram"]) {
ram.size = node["size"].natural();
ram.mask = bit::round(ram.size) - 1;
if(ram.size) {
ram.data = new uint8[ram.mask + 1];
if(auto name = node["name"].text()) {
if(auto fp = platform->open(pathID(), name, File::Read)) {
fp->read(ram.data, ram.size);
}
}
}
}
return true;
}
auto Cartridge::save() -> void {
auto document = BML::unserialize(information.manifest);
if(auto name = document["board/ram/name"].text()) {
if(auto fp = platform->open(pathID(), name, File::Write)) {
fp->write(ram.data, ram.size);
}
}
}
auto Cartridge::unload() -> void {
delete[] rom.data;
delete[] ram.data;
rom = {};
ram = {};
}
auto Cartridge::power() -> void {
memory::fill(&mapper, sizeof(Mapper));
mapper.romPage0 = 0;
mapper.romPage1 = 1;
mapper.romPage2 = 2;
}
auto Cartridge::Memory::mirror(uint addr, uint size) -> uint {
uint base = 0;
uint mask = 1 << 21;
while(addr >= size) {
while(!(addr & mask)) mask >>= 1;
addr -= mask;
if(size > mask) {
size -= mask;
base += mask;
}
mask >>= 1;
}
return base + addr;
}
auto Cartridge::Memory::read(uint addr) -> uint8 {
if(!size) return 0x00;
return this->data[mirror(addr, size)];
}
auto Cartridge::Memory::write(uint addr, uint8 data) -> void {
if(!size) return;
this->data[mirror(addr, size)] = data;
}
}

View File

@@ -0,0 +1,57 @@
struct Cartridge {
auto pathID() const -> uint { return information.pathID; }
auto sha256() const -> string { return information.sha256; }
auto manifest() const -> string { return information.manifest; }
auto title() const -> string { return information.title; }
auto load() -> bool;
auto save() -> void;
auto unload() -> void;
auto power() -> void;
//mapper.cpp
auto read(uint16 addr) -> maybe<uint8>;
auto write(uint16 addr, uint8 data) -> bool;
private:
struct Information {
uint pathID = 0;
string sha256;
string manifest;
string title;
} information;
struct Memory {
uint8* data = nullptr;
uint size = 0;
uint mask = 0;
static auto mirror(uint addr, uint size) -> uint;
auto read(uint addr) -> uint8;
auto write(uint addr, uint8 data) -> void;
};
Memory rom;
Memory ram;
struct Mapper {
//$fffc
uint2 shift;
uint1 ramPage2;
uint1 ramEnablePage2;
uint1 ramEnablePage3;
uint1 romWriteEnable;
//$fffd
uint8 romPage0;
//$fffe
uint8 romPage1;
//$ffff
uint8 romPage2;
} mapper;
};
extern Cartridge cartridge;

View File

@@ -0,0 +1,92 @@
auto Cartridge::read(uint16 addr) -> maybe<uint8> {
uint2 page = addr >> 14;
addr &= 0x3fff;
switch(page) {
case 0: {
if(addr <= 0x03ff) return rom.read(addr);
return rom.read(mapper.romPage0 << 14 | addr);
}
case 1: {
return rom.read(mapper.romPage1 << 14 | addr);
}
case 2: {
if(mapper.ramEnablePage2) {
return ram.read(mapper.ramPage2 << 14 | addr);
}
return rom.read(mapper.romPage2 << 14 | addr);
}
case 3: {
if(mapper.ramEnablePage3) {
return ram.read(addr);
}
return nothing;
}
}
unreachable;
}
auto Cartridge::write(uint16 addr, uint8 data) -> bool {
if(addr == 0xfffc) {
mapper.shift = data.bits(0,1);
mapper.ramPage2 = data.bit(2);
mapper.ramEnablePage2 = data.bit(3);
mapper.ramEnablePage3 = data.bit(4);
mapper.romWriteEnable = data.bit(7);
}
if(addr == 0xfffd) {
mapper.romPage0 = data;
}
if(addr == 0xfffe) {
mapper.romPage1 = data;
}
if(addr == 0xffff) {
mapper.romPage2 = data;
}
uint2 page = addr >> 14;
addr &= 0x3fff;
switch(page) {
case 0: {
return false;
}
case 1: {
return false;
}
case 2: {
if(mapper.ramEnablePage2) {
ram.write(mapper.ramPage2 << 14 | addr, data);
return true;
}
return false;
}
case 3: {
if(mapper.ramEnablePage3) {
ram.write(addr, data);
return true;
}
return false;
}
}
unreachable;
}

View File

@@ -0,0 +1,27 @@
#include <ms/ms.hpp>
namespace MasterSystem {
#include "gamepad/gamepad.cpp"
Controller::Controller(uint port) : port(port) {
if(!handle()) create(Controller::Enter, 100);
}
Controller::~Controller() {
}
auto Controller::Enter() -> void {
while(true) {
scheduler.synchronize();
if(auto device = peripherals.controllerPort1) if(device->active()) device->main();
if(auto device = peripherals.controllerPort2) if(device->active()) device->main();
}
}
auto Controller::main() -> void {
step(1);
synchronize(cpu);
}
}

Some files were not shown because too many files have changed in this diff Show More