mirror of
https://github.com/ares-emulator/ares.git
synced 2026-11-09 17:03:16 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1980fab09 | ||
|
|
ebb234ba5f | ||
|
|
2cc7fe30b4 | ||
|
|
5c3c872b78 | ||
|
|
36bf915244 | ||
|
|
045a0f7e79 | ||
|
|
a209e6ffbe | ||
|
|
3bf672dd97 | ||
|
|
157ddf3e8f | ||
|
|
ea23bf53ae | ||
|
|
d4598e1d01 | ||
|
|
f9a8564af0 | ||
|
|
6d66b1136d | ||
|
|
b01f18c34c | ||
|
|
1ebdb69516 | ||
|
|
add0f74387 | ||
|
|
b20f70f333 | ||
|
|
9aebf7bc6b | ||
|
|
a7bf219d5d | ||
|
|
04118be59a | ||
|
|
f24d17859f | ||
|
|
35fd80bde7 | ||
|
|
ccf1c00b58 | ||
|
|
f4520d41ec | ||
|
|
e308cf4275 | ||
|
|
192e53bb87 | ||
|
|
0ed9edfcdb | ||
|
|
764fe1974a | ||
|
|
a55d640459 | ||
|
|
6010bffe5d | ||
|
|
e492268025 | ||
|
|
a36c26c997 | ||
|
|
a3945e5772 | ||
|
|
6b6233b3af | ||
|
|
9f63cb1b99 | ||
|
|
49c39e0e4d | ||
|
|
7dec0b2a3c | ||
|
|
f288280ceb | ||
|
|
c6c5f4669c | ||
|
|
397b9c4505 | ||
|
|
7e2cfb6d40 | ||
|
|
970dcea0ac | ||
|
|
402c146a53 | ||
|
|
a471c150c9 | ||
|
|
ea38ea2537 | ||
|
|
09b326ae86 | ||
|
|
a60f667b25 | ||
|
|
6f7cf1bd31 | ||
|
|
8751bc433f | ||
|
|
afbee40bc0 | ||
|
|
81e9be64d2 | ||
|
|
0cd4972d11 | ||
|
|
938609818f | ||
|
|
487491e6f2 | ||
|
|
a6ccc7e5d2 | ||
|
|
39dd39cc09 | ||
|
|
1a017ff697 | ||
|
|
591adfcb25 |
No files matched your search
@@ -1,3 +0,0 @@
|
||||
higan/profile/WonderSwan.sys/internal.ram
|
||||
higan/profile/WonderSwan Color.sys/internal.ram
|
||||
docs_build/
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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/systems/* higan-nightly/
|
||||
- cp -a shaders "higan-nightly/Video Shaders"
|
||||
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/systems/* higan-nightly/
|
||||
- cp -a shaders "higan-nightly/Video Shaders"
|
||||
artifacts:
|
||||
paths:
|
||||
- higan-nightly/*
|
||||
cache:
|
||||
paths:
|
||||
- icarus/obj/*.o
|
||||
- higan/obj/*.o
|
||||
@@ -1,9 +0,0 @@
|
||||
Contributing to higan
|
||||
=====================
|
||||
|
||||
If you would like to propose a change to higan,
|
||||
you should create an account on the [official forums][f],
|
||||
go to the "Projects" forum and the "higan" subforum,
|
||||
and post your idea in a new topic there.
|
||||
|
||||
[f]: https://board.byuu.org/
|
||||
@@ -1,48 +0,0 @@
|
||||
The unofficial higan repository
|
||||
===============================
|
||||
|
||||
higan emulates a number of classic videogame consoles of the 1980s and 1990s,
|
||||
allowing you to play classic games on a modern general-purpose computer.
|
||||
|
||||
This repository includes
|
||||
the source-code for
|
||||
stable and WIP releases of higan,
|
||||
starting during the development of v068.
|
||||
It also includes community-maintained documentation.
|
||||
|
||||
Basically,
|
||||
apart from `.gitignore` files,
|
||||
anything in the
|
||||
[higan](higan/),
|
||||
[hiro](hiro/),
|
||||
[icarus](icarus/),
|
||||
[libco](libco/),
|
||||
[nall](nall/),
|
||||
[ruby](ruby/),
|
||||
or [shaders](shaders/)
|
||||
directories should be exactly as it appeared in official releases.
|
||||
Everything else has been added for various reasons.
|
||||
|
||||
Official higan resources
|
||||
------------------------
|
||||
|
||||
- [Official homepage](https://byuu.org/emulation/higan/)
|
||||
- [Official forum](https://board.byuu.org/viewforum.php?f=4)
|
||||
|
||||
Unofficial higan resources
|
||||
--------------------------
|
||||
|
||||
- Documentation for
|
||||
[the current stable version][stadocs]
|
||||
- [Source code repository](https://gitlab.com/higan/higan/)
|
||||
archives official higan releases
|
||||
and WIP snapshots
|
||||
since approximately v067r21
|
||||
- [Latest WIP build for Windows][wipwin]
|
||||
- Documentation for
|
||||
[the latest WIP version][wipdocs]
|
||||
|
||||
|
||||
[wipwin]: https://gitlab.com/higan/higan/-/jobs/artifacts/master/download?job=windows-x86_64-binaries
|
||||
[stadocs]: https://higan.readthedocs.io/
|
||||
[wipdocs]: https://higan.readthedocs.io/en/latest/
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This uses the official W3C link-checker tool:
|
||||
#
|
||||
# https://github.com/w3c/link-checker
|
||||
#
|
||||
checklink \
|
||||
--summary \
|
||||
--broken \
|
||||
--location=http://127.0.0.1:8000/ \
|
||||
--exclude 'github.com|board.byuu.org' \
|
||||
http://127.0.0.1:8000/
|
||||
@@ -1,141 +0,0 @@
|
||||
What is a game folder?
|
||||
----------------------
|
||||
|
||||
TODO
|
||||
|
||||
Why game folders?
|
||||
-----------------
|
||||
|
||||
A game is more than just
|
||||
the raw data originally encased in a game's ROM chip.
|
||||
If a game allows you to save your progress,
|
||||
that information needs to be stored somewhere.
|
||||
If you use an emulator's [save states](#save-states),
|
||||
those save states need to be stored somewhere.
|
||||
If you use Game Genie or Pro Action Replay codes,
|
||||
information about what codes exist,
|
||||
what codes are enabled,
|
||||
and what they do
|
||||
needs to be stored somewhere.
|
||||
|
||||
On the technical side,
|
||||
a physical game cartridge contains a circuit board
|
||||
that makes the game data available to the console,
|
||||
and different games used circuit boards that work differently.
|
||||
That circuit-layout information needs to be stored somewhere.
|
||||
Some games included custom processors
|
||||
to do calculations the base console could not do quickly enough
|
||||
(like the SuperFX chip used in _StarFox_ for the Super Famicom)
|
||||
and information about extra chips needs to be stored somewhere.
|
||||
Some of those custom processors require extra data to work
|
||||
that's not part of the main game data
|
||||
(like the DSP chip used in Super Mario Kart for the Super Famicom)
|
||||
and that data needs to be stored somewhere too.
|
||||
|
||||
higan keeps all this game-related information together
|
||||
in a single place:
|
||||
a game folder in the higan library.
|
||||
|
||||
For a more detailed motivation for game folders,
|
||||
see [Game Paks on the higan website][gp]
|
||||
|
||||
[gp]: https://byuu.org/emulation/higan/game-paks
|
||||
|
||||
What is a manifest?
|
||||
-------------------
|
||||
|
||||
TODO
|
||||
|
||||
The most important file in a game folder is `manifest.bml`,
|
||||
which describes how all the other files should be wired together
|
||||
to create a runnable game cartridge.
|
||||
However,
|
||||
the manifest format has occasionally changed
|
||||
as new emulation details were uncovered
|
||||
that could not be represented in the old format.
|
||||
Therefore,
|
||||
icarus [defaults](#the-icarus-settings-dialog)
|
||||
to not writing out manifests when it imports games,
|
||||
and higan [defaults](#the-configuration-dialog)
|
||||
to ignoring manifests that are present.
|
||||
Instead,
|
||||
when higan loads a game,
|
||||
it will ask icarus to generate a temporary manifest in the latest format,
|
||||
based on the files present in the game folder
|
||||
and how they are likely to go together.
|
||||
You can view this temporary manifest
|
||||
in [the Manifest Viewer](#the-manifest-viewer).
|
||||
|
||||
What's in a game folder?
|
||||
------------------------
|
||||
|
||||
As mentioned [above](#why-game-folders),
|
||||
a game folder collects all the information relevant
|
||||
to emulating a particular game.
|
||||
Not all of the following files
|
||||
are relevant to every emulated console,
|
||||
or to every game on a given console,
|
||||
but they may be relevantunder particular circumstances.
|
||||
|
||||
All the files directly in the game folder
|
||||
are expected to be useful
|
||||
to all emulators that support them:
|
||||
|
||||
- `manifest.bml`:
|
||||
The [manifest](#what-is-a-manifest)
|
||||
for this game folder.
|
||||
- `program.rom`:
|
||||
For most consoles,
|
||||
this contains
|
||||
the executable instructions and graphics data
|
||||
from the cartridge's ROM chips.
|
||||
For the Famicom,
|
||||
this contains only the executable instructions.
|
||||
- `character.rom`:
|
||||
For the Famicom,
|
||||
this contains only the graphics data
|
||||
from the cartridge's ROM chips.
|
||||
- `ines.rom`:
|
||||
While other consoles typically include enough hints
|
||||
in `program.rom` for icarus to generate a manifest,
|
||||
the Famicom does not.
|
||||
Famicom games not stored in game folders
|
||||
typically include an "iNES header" to store that information,
|
||||
which icarus preserves after import as `ines.rom`.
|
||||
- `save.ram`:
|
||||
Games that include a save feature
|
||||
will create this file.
|
||||
Note that it is only written to disk
|
||||
when higan exits gracefully,
|
||||
if higan crashes or is forced to quit,
|
||||
in-game saves may be lost.
|
||||
Other emulators sometimes call this an "SRAM file",
|
||||
even though the same filename is used
|
||||
for cartridges that use EEPROM or Flash storage,
|
||||
not just battery-backed Static RAM.
|
||||
- `rtc.ram`:
|
||||
Games that include a calendar or real-time clock
|
||||
will create this file.
|
||||
- `*.data.rom`, `*.program.rom`:
|
||||
Files named like this are usually
|
||||
[co-processor firmware](#importing-and-playing-games-with-co-processor-firmware).
|
||||
- `msu1.rom`:
|
||||
Holds streamable data for
|
||||
[the MSU-1](#importing-and-playing-MSU-1-games).
|
||||
- `track-*.pcm`:
|
||||
Holds streamable audio for
|
||||
[the MSU-1](#importing-and-playing-MSU-1-games).
|
||||
|
||||
Files that are only useful to higan specifically
|
||||
are placed in a `higan` subdirectory:
|
||||
|
||||
- `cheats.bml`:
|
||||
All information present in
|
||||
[the Cheat Editor](#the-cheat-editor)
|
||||
is stored here.
|
||||
- `states/quick/slot-*.bst`:
|
||||
All the save states made to
|
||||
[Quick state slots](#quick-states).
|
||||
- `states/managed/slot-*.bst`:
|
||||
All the save states made with
|
||||
[the State Manager](#the-state-manager).
|
||||
@@ -1,56 +0,0 @@
|
||||
higan maintains a "game library"
|
||||
containing all the games you've played.
|
||||
|
||||
- In Windows,
|
||||
the default location of
|
||||
the game library is the `Emulation` folder
|
||||
inside your profile folder
|
||||
(To find your profile folder,
|
||||
press `Win+R` to open the Run dialog,
|
||||
then type `%USERPROFILE%` and press Enter).
|
||||
- In Linux,
|
||||
the default location of
|
||||
the game library is the `Emulation` directory
|
||||
inside your home directory.
|
||||
- On all platforms,
|
||||
the game library location can be configured.
|
||||
See [Moving the Game Library](#moving-the-game-library)
|
||||
below.
|
||||
|
||||
Inside the library directory there is a subdirectory for each system,
|
||||
and inside each system directory are the game folders
|
||||
for each imported game.
|
||||
|
||||
For more information about game folders,
|
||||
see [Why game folders?](#why-game-folders)
|
||||
and [What's in a game folder?](#whats-in-a-game-folder)
|
||||
below.
|
||||
|
||||
Moving the game library
|
||||
-----------------------
|
||||
|
||||
Moving the game library is a little complicated,
|
||||
because there's two parts to it:
|
||||
telling icarus where to put imported games,
|
||||
and telling higan where to find them.
|
||||
|
||||
1. If necessary,
|
||||
create the folder you want higan to use
|
||||
as its game library.
|
||||
1. Launch icarus,
|
||||
then click the "Settings ..." button in the lower-right,
|
||||
to open the Settings dialog.
|
||||
1. Click the "Change ..." button on the right.
|
||||
A [filesystem browser](#the-filesystem-browser) window will appear,
|
||||
allowing you to choose
|
||||
where imported games will be stored.
|
||||
1. Launch higan,
|
||||
then from the Settings menu,
|
||||
choose "Configuration ..."
|
||||
to open [the Configuration dialog](#the-configuration-dialog).
|
||||
1. Click the Advanced tab
|
||||
then click the "Change ..." button.
|
||||
A [filesystem browser](#the-filesystem-browser) will appear,
|
||||
allowing you to choose the same directory again.
|
||||
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
A real game console
|
||||
is a complex piece of hardware,
|
||||
with electricity flowing through it
|
||||
in complex patterns that can't easily
|
||||
be paused or recorded.
|
||||
However,
|
||||
an emulated console is pure software:
|
||||
it only changes when the emulation software
|
||||
deliberately updates it,
|
||||
so the emulator software can save
|
||||
the entire state of the emulated console
|
||||
to disk,
|
||||
and weeks or months later
|
||||
bring it back to life
|
||||
as though nothing had ever happened.
|
||||
|
||||
Save states versus in-game saves
|
||||
--------------------------------
|
||||
|
||||
Some games include their own systems
|
||||
for saving and restoring the player's progress.
|
||||
Here are some of the differences
|
||||
between save states and in-game saves:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Save states</th>
|
||||
<th>In-game saves</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Work exactly the same way
|
||||
in every game
|
||||
</td>
|
||||
<td>
|
||||
Works differently in different games,
|
||||
some games don't support it at all
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Save at any time,
|
||||
anywhere in the game
|
||||
</td>
|
||||
<td>
|
||||
Save only at special save-points
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Loading a save
|
||||
puts the game back exactly how it was
|
||||
when you pressed save
|
||||
</td>
|
||||
<td>
|
||||
Loading a save
|
||||
restores some things
|
||||
(like the player's inventory
|
||||
or remaining lives),
|
||||
but may
|
||||
forget others
|
||||
(like taking you back to the beginning of the level)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
You can have dozens of save states
|
||||
</td>
|
||||
<td>
|
||||
Most games limit you to about 3 saves
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Can only be loaded
|
||||
by the same version of the same emulator
|
||||
that created it
|
||||
</td>
|
||||
<td>
|
||||
Works with any version of any emulator,
|
||||
can sometimes even be copied to or from physical cartridges
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
**Note:**
|
||||
Loading a save state
|
||||
will reset the entire emulated console
|
||||
to the way it was when the save state was created,
|
||||
*including in-game saves*.
|
||||
If you create a save state,
|
||||
then make an in-game save,
|
||||
then load the save state,
|
||||
**your in-game save will be lost**.
|
||||
Don't do that.
|
||||
|
||||
Quick states
|
||||
------------
|
||||
|
||||
higan has five Quick State slots,
|
||||
which can be used from
|
||||
[the Tools menu](../interface/higan.md#the-tools-menu),
|
||||
or with the appropriate
|
||||
[hotkeys](../interface/higan-config.md#hotkeys).
|
||||
|
||||
Quick states are useful
|
||||
as extra checkpoints
|
||||
in games that don't have them,
|
||||
or where they aren't close enough together.
|
||||
Map the "Save Quick State" and "Load Quick State" hotkeys
|
||||
to your controller,
|
||||
and you can cheese your way through just about anything.
|
||||
|
||||
Manager states
|
||||
--------------
|
||||
|
||||
higan's
|
||||
[State Manager](../interface/higan-tools.md#the-state-manager)
|
||||
allows you to create over a hundred save states,
|
||||
and add a helpful description to each one.
|
||||
|
||||
Manager States are more cumbersome to use than Quick States,
|
||||
since they cannot be accessed with hotkeys,
|
||||
but are useful when you want quick access
|
||||
to many different parts of a game.
|
||||
-279
@@ -1,279 +0,0 @@
|
||||
I see "tearing" when a game scrolls. How can I enable vsync?
|
||||
------------------------------------------------------------
|
||||
|
||||
higan supports synchronizing video output
|
||||
to the display's vertical-synchronization (or "vsync") signal,
|
||||
but the option is hidden
|
||||
because it often causes more problems than it solves
|
||||
(see the next question).
|
||||
|
||||
To enable video synchronization:
|
||||
|
||||
- Open the higan's configuration file, `settings.bml`
|
||||
- On Windows, look in `%LOCALAPPDATA%\higan`
|
||||
or beside `higan.exe`
|
||||
- On Linux, look in `~/.local/share/higan`
|
||||
- Open it in your favourite text editor
|
||||
(Windows Notepad will corrupt the file,
|
||||
use WordPad if you don't have anything better)
|
||||
- Find the line that says "Video"
|
||||
- Somewhere below that, find an indented line
|
||||
that says "Synchronize:false".
|
||||
- Change "false" to "true"
|
||||
- Save your changes to `settings.bml`
|
||||
and restart higan
|
||||
|
||||
Why is video synchronization a problem for higan?
|
||||
-------------------------------------------------
|
||||
|
||||
**The short version:**
|
||||
Turning on video synchronization
|
||||
cleans up video tearing,
|
||||
turning on audio synchronization
|
||||
cleans up audio glitches,
|
||||
but turning on both
|
||||
makes audio glitches worse.
|
||||
|
||||
**The long version:**
|
||||
Enabling video synchronization
|
||||
locks the frame-rate of the emulated console
|
||||
to the frame-rate of your computer's display.
|
||||
If your display's refresh rate exactly matches
|
||||
the emulated console's,
|
||||
games play at the correct speed
|
||||
and everything's fine.
|
||||
|
||||
However,
|
||||
modern 60Hz displays do not always match
|
||||
the emulated console's refresh rate:
|
||||
|
||||
- The Super Famicom usually runs a little faster than 60Hz
|
||||
- the PAL variants of most consoles run at 50Hz
|
||||
- the WonderSwan runs at 75Hz
|
||||
- While the Game Boy does run its LCD at 60Hz
|
||||
it can turn it off and on at any time,
|
||||
requiring emulation to pause
|
||||
until it can get back in sync
|
||||
with the computer display.
|
||||
|
||||
Because of these frame-rate differences,
|
||||
enabling video synchronization
|
||||
can force games to run
|
||||
faster or slower than intended.
|
||||
|
||||
The consoles that higan emulates
|
||||
produce video frames and audio samples at a particular rate.
|
||||
If video synchronization causes
|
||||
the emulated console to run, say, 5% faster than intended,
|
||||
that means audio samples are also being produced 5% faster.
|
||||
You might not notice the changed game speed,
|
||||
but you'll almost certainly notice
|
||||
the game's audio glitching constantly
|
||||
as your sound card tries to keep up.
|
||||
|
||||
Enabling
|
||||
[audio synchronization](interface/higan.md#the-settings-menu)
|
||||
normally fixes this kind of audio glitching,
|
||||
but with video synchronization it makes things worse:
|
||||
audio is likely to glitch
|
||||
while higan waits for a video frame to be shown,
|
||||
and video is likely to stutter
|
||||
while higan waits for an audio buffer to complete.
|
||||
|
||||
Games run too fast
|
||||
------------------
|
||||
|
||||
higan runs as fast as it can,
|
||||
but it will pause and wait
|
||||
for the audio and video drivers to catch up
|
||||
if [Synchronize Audio](interface/higan.md#the-settings-menu)
|
||||
and [video synchronization][vsync]
|
||||
are enabled, respectively.
|
||||
If games are running way too fast, here's some things to check:
|
||||
|
||||
- Make sure "Synchronize Audio" is ticked in
|
||||
[the Settings menu](interface/higan.md#the-settings-menu)
|
||||
- Make sure the Audio driver is not set to "None"
|
||||
in [the Advanced settings](interface/higan-config.md#advanced)
|
||||
(remember to restart higan if you change driver settings)
|
||||
- Make sure your computer has speakers or headphones connected
|
||||
(some computers disable all audio if no ouputs are available)
|
||||
- If you want the game to be silent,
|
||||
tick "Mute Audio" in
|
||||
[the Settings menu](interface/higan.md#the-settings-menu)
|
||||
|
||||
[vsync]: #i-see-tearing-when-a-game-scrolls-how-can-i-enable-vsync
|
||||
|
||||
Games run too slow
|
||||
------------------
|
||||
|
||||
Of all the consoles higan can emulate,
|
||||
higan's Super Famicom emulation
|
||||
is the most resource intensive.
|
||||
Full-speed emulation for the Super Famicom base unit
|
||||
requires an Intel Core 2 Duo (or AMD equivalent),
|
||||
full-speed for games with the SuperFX chip
|
||||
requires an Intel Ivy Bridge (or equivalent),
|
||||
full-speed for the wireframe animations in Mega Man X2
|
||||
requires an even faster computer.
|
||||
Low-power CPUs like ARM chips,
|
||||
or Intel Atom and Celeron CPUS
|
||||
generally aren't fast enough to emulate the Super Famicom with higan,
|
||||
although other emulated consoles may work.
|
||||
|
||||
If your computer meets the general speed requirements
|
||||
but games run too slowly,
|
||||
try choosing a different
|
||||
[audio driver](interface/higan-config.md#advanced),
|
||||
since that's usually what drives higan's timing.
|
||||
|
||||
On some computers,
|
||||
the operating system's power-management system
|
||||
may be confused by higan's unusual pattern of CPU usage
|
||||
(it runs furiously to calculate the next video frame
|
||||
and the next few milliseconds of audio,
|
||||
then stops dead as it waits for output to complete).
|
||||
If holding down
|
||||
the [fast forward hotkey](interface/higan-config.md#hotkeys)
|
||||
runs too fast but the game normally runs too slow,
|
||||
try disabling "power saver" mode
|
||||
or enabling "performance" mode.
|
||||
|
||||
On Linux specifically,
|
||||
if your desktop environment doesn't provide a way
|
||||
to change the CPU usage governor,
|
||||
you can temporarily force your CPUs to run at full-speed
|
||||
by running the following command in a terminal:
|
||||
|
||||
echo "performance" | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
|
||||
|
||||
To put things back to normal,
|
||||
reboot the computer, or run this command instead:
|
||||
|
||||
echo "ondemand" | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
|
||||
|
||||
Why is higan so much slower than other emulators?
|
||||
-------------------------------------------------
|
||||
|
||||
The consoles higan emulates
|
||||
generally contain different devices
|
||||
(say, a CPU, an audio chip and a video chip)
|
||||
that do their own thing,
|
||||
but can interrupt each other at any time.
|
||||
Games can and do depend on timing details like
|
||||
"when the audio chip is done playing this sound,
|
||||
it will interrupt the CPU at exactly the right time
|
||||
for the CPU to fiddle with the video chip".
|
||||
higan is therefore very cautious about timing:
|
||||
while it's emulating the audio chip (for example),
|
||||
at every point the emulated CPU *might* interrupt
|
||||
the emulated audio chip,
|
||||
higan switches to emulating the CPU up to the same point
|
||||
to find out whether the CPU *will* interrupt it.
|
||||
|
||||
In this way,
|
||||
higan is a little bit like
|
||||
an office-worker trying to do the jobs of three other people
|
||||
by running from desk to desk,
|
||||
sending the same emails
|
||||
that those three people would send to each other,
|
||||
leaving themselves a note at each desk to remind themselves
|
||||
where they were up to when they come back.
|
||||
Although this constant switching
|
||||
is slow and inefficient,
|
||||
higan does it
|
||||
to ensure the emulated console
|
||||
always matches the behaviour
|
||||
of the original.
|
||||
|
||||
Other emulators,
|
||||
especially ones that are popular on phones and tablets,
|
||||
typically work differently.
|
||||
Although emulated devices *can* interrupt each other at any time,
|
||||
usually they don't,
|
||||
and often the only exceptions are a handful of unpopular games.
|
||||
So it's pretty safe
|
||||
(and much, much more efficient)
|
||||
to let certain devices run "ahead" of the others.
|
||||
For example,
|
||||
letting the video chip render an entire scanline at a time
|
||||
instead of working pixel-by-pixel.
|
||||
The downside is that some games don't work properly
|
||||
in these emulators,
|
||||
with game-breaking bugs,
|
||||
minor visual glitches,
|
||||
or anything in between.
|
||||
|
||||
Some emulators try to recognise
|
||||
when the user loads a specific problematic game,
|
||||
and will adjust the shortcuts the emulator takes
|
||||
so that the game runs correctly.
|
||||
This is a good way to increase game compatibility
|
||||
without losing efficiency,
|
||||
but it means the emulator does not accurately reflect
|
||||
how the original console worked,
|
||||
and it requires the emulator authors to diagnose and work around
|
||||
each individual supported game,
|
||||
instead of having one emulator that works for everything.
|
||||
|
||||
Why can't higan use multiple CPU cores?
|
||||
---------------------------------------
|
||||
|
||||
These days,
|
||||
most computers contain multiple CPU cores,
|
||||
allowing them to run different programs,
|
||||
or different parts of the same program
|
||||
at the same time.
|
||||
Since higan requires high CPU performance,
|
||||
sometimes people suggest that it should split its work
|
||||
into multiple threads
|
||||
so it can run across multiple cores
|
||||
instead of stressing one core while the others lie idle.
|
||||
After all,
|
||||
the original consoles higan emulates
|
||||
had separate devices running independently,
|
||||
so why not run each emulated device in a separate thread?
|
||||
|
||||
The big problem with this approach is timing.
|
||||
The devices in a physical Super Famicom (for example)
|
||||
run at whatever speed they run at,
|
||||
and talk to each other whenever they feel like.
|
||||
Because every Super Famicom uses the exact same components,
|
||||
every Super Famicom runs compatible games at the exact same speed,
|
||||
and games can blindly assume that
|
||||
when operation X is complete on device A,
|
||||
device B will have finished operation Y
|
||||
and be ready to do something new.
|
||||
Meanwhile, higan's emulated components
|
||||
take an unpredictable amount of time to do their work,
|
||||
so without deliberate synchronization
|
||||
things would break almost immediately.
|
||||
|
||||
It's not practical to make higan's emulated devices
|
||||
do their work in exactly the same amount of time
|
||||
as their hardware counterparts.
|
||||
The problem is forty years of technology
|
||||
designed to make programs run as fast as possible:
|
||||
optimizing compilers and superscalar, out-of-order CPU architectures
|
||||
change programs to make them faster,
|
||||
speeding up some programs more than others
|
||||
in ways that are very difficult to understand and predict.
|
||||
Even if higan's emulated devices
|
||||
ran at the exact, correct speed
|
||||
on one particular computer,
|
||||
they'd still run differently on any other computer,
|
||||
or with a smarter compiler,
|
||||
or with a smarter CPU.
|
||||
|
||||
Since higan needs its emulated components
|
||||
to run at particular speeds,
|
||||
and they won't run at those speeds naturally,
|
||||
it must force them manually.
|
||||
An emulated device runs for a little while,
|
||||
then all the others are run until they catch up.
|
||||
It's this careful management,
|
||||
regular stopping and starting,
|
||||
that makes higan slow,
|
||||
not the actual emulation of each device,
|
||||
and so it doesn't make sense
|
||||
for higan to be multi-threaded.
|
||||
@@ -1,145 +0,0 @@
|
||||
Unfortunately,
|
||||
there's no standard for
|
||||
displaying video,
|
||||
playing audio,
|
||||
and accepting input from game controllers
|
||||
that works on every operating system.
|
||||
Or rather,
|
||||
there's many standards,
|
||||
and different ones work best
|
||||
on different computers.
|
||||
Therefore,
|
||||
higan comes with "drivers"
|
||||
for video, audio and input,
|
||||
so you can find the one that works best for you.
|
||||
To see what drivers you're currently using,
|
||||
or to choose different ones,
|
||||
go to
|
||||
[the Advanced tab](../interface/higan-config.md#Advanced)
|
||||
of the Settings window.
|
||||
|
||||
Here are the most notable drivers
|
||||
for each platform
|
||||
for each category.
|
||||
If your copy of higan
|
||||
includes a driver not listed here,
|
||||
it's probably a reasonable choice,
|
||||
so try it out and see how you like it.
|
||||
|
||||
**Note:** After changing any driver,
|
||||
you must restart higan for the change to take effect.
|
||||
|
||||
**Note:**
|
||||
Video, Audio and Input
|
||||
all have a special driver named "None".
|
||||
This is a dummy driver that does nothing
|
||||
(draws no video,
|
||||
plays no audio,
|
||||
accepts no input),
|
||||
so don't choose it unless you're
|
||||
trying to diagnose problems with other drivers.
|
||||
|
||||
Video
|
||||
-----
|
||||
|
||||
On Windows:
|
||||
|
||||
- **OpenGL** is usually the best choice,
|
||||
since it supports [custom shaders](shaders.md),
|
||||
however it does require support for OpenGL 3.2
|
||||
which excludes some integrated graphics chipsets
|
||||
and old graphics cards.
|
||||
- **Direct3D** is a good choice
|
||||
if OpenGL is unavailable.
|
||||
It also allows
|
||||
[Exclusive fullscreen](../interface/higan-config.md#video),
|
||||
bypassing Windows' desktop compositor.
|
||||
- **GDI** is the safest choice,
|
||||
but performs very poorly at large sizes.
|
||||
|
||||
On Linux:
|
||||
|
||||
- **OpenGL** is the best choice,
|
||||
since it's fast
|
||||
and it supports [custom shaders](shaders.md),
|
||||
but requires OpenGL 3.2.
|
||||
You can check what version of OpenGL
|
||||
your system supports by running
|
||||
`glxinfo | grep 'core profile version'`
|
||||
in a terminal.
|
||||
- **XVideo** is also fast,
|
||||
but may be low-quality,
|
||||
and generally only supports the "Blur" shader,
|
||||
not "None".
|
||||
- **XShm** is the safest choice,
|
||||
but performs very poorly at large sizes.
|
||||
|
||||
Audio
|
||||
-----
|
||||
|
||||
On Windows:
|
||||
|
||||
- **ASIO** offers the lowest possible latency,
|
||||
but is the least likely to work on any given computer.
|
||||
- **WASAPI** offers low latency,
|
||||
but is only slightly more likely to work.
|
||||
It also offers
|
||||
[Exclusive Mode](../interface/higan-config.md#audio),
|
||||
which can improve audio quality and lower latency,
|
||||
but may be better or worse than shared mode
|
||||
in practice.
|
||||
- **XAudio2** is a good choice,
|
||||
but it requires the latest (June 2010) version
|
||||
of the [DirectX 9 End-User Runtime][dx9]
|
||||
to be installed.
|
||||
- **DirectSound** is the safest choice,
|
||||
and not a bad one.
|
||||
|
||||
[dx9]: https://www.microsoft.com/en-us/download/details.aspx?id=35
|
||||
|
||||
On Linux:
|
||||
|
||||
- **PulseAudio** or **PulseAudioSimple**
|
||||
are almost certainly the drivers to use,
|
||||
since almost every Linux distribution uses
|
||||
[PulseAudio](https://en.wikipedia.org/wiki/PulseAudio)
|
||||
to manage audio output.
|
||||
The two drivers should behave identically,
|
||||
but some users report one working better than the other.
|
||||
- **ALSA** is a good choice
|
||||
for Linux distributions that do not use PulseAudio.
|
||||
If PulseAudio is running,
|
||||
this will wind up using PulseAudio's ALSA emulation,
|
||||
or failing due to "sound card already in use",
|
||||
unless you use a tool like `pasuspender`.
|
||||
- **OSS** may be useful
|
||||
for non-Linux platforms,
|
||||
or for Linux systems with the third-party OSSv4
|
||||
kernel drivers installed.
|
||||
For most Linux systems,
|
||||
this will use ALSA's OSS emulation,
|
||||
PulseAudio's OSS emulation,
|
||||
or not work at all.
|
||||
|
||||
TODO: If the audio driver is set to None,
|
||||
or you have no audio device,
|
||||
Sync Audio does not work
|
||||
and games will run in fast-forward
|
||||
unless you enable Sync Video.
|
||||
https://board.byuu.org/viewtopic.php?p=44138#p44138
|
||||
|
||||
Input
|
||||
-----
|
||||
|
||||
On Windows,
|
||||
"Windows" is the only driver available,
|
||||
and uses RawInput for keyboard and mouse input,
|
||||
XInput for Xbox controllers,
|
||||
and DirectInput for other controllers.
|
||||
|
||||
On Linux:
|
||||
|
||||
- **udev** supports every input device,
|
||||
but requires a modern Linux system.
|
||||
- **Xlib** is the safest choice,
|
||||
but only supports keyboard and mouse input.
|
||||
@@ -1,560 +0,0 @@
|
||||
Before it can load a game,
|
||||
higan requires that all the game's data
|
||||
be stored correctly in
|
||||
[the Game Library](../concepts/game-library.md).
|
||||
For [regular games](#regular-games)
|
||||
this is simple,
|
||||
but some games require special treatment,
|
||||
especially games that make use of
|
||||
unusual hardware.
|
||||
|
||||
Regular games
|
||||
-------------
|
||||
|
||||
icarus supports importing games
|
||||
in the most commonly-used formats
|
||||
for each supported console,
|
||||
and also those same formats inside `.zip` files.
|
||||
More advanced compression formats
|
||||
like RAR or 7-zip are not supported.
|
||||
|
||||
For most games
|
||||
that do not use special chips or co-processors,
|
||||
importing a game is straight-forward.
|
||||
From [the Library menu](#the-library-menu)
|
||||
choose "Load ROM File ..."
|
||||
to open [a filesystem browser](#the-filesystem-browser),
|
||||
choose the game you want to play,
|
||||
and it will be imported into the library and loaded.
|
||||
|
||||
To play the game again
|
||||
select the console the game runs on from
|
||||
[the Library menu](#the-library-menu)
|
||||
to open another [filesystem browser](#the-filesystem-browser)
|
||||
that lists all the previously-imported games for that platform.
|
||||
|
||||
Games with co-processor firmware
|
||||
--------------------------------
|
||||
|
||||
Many games included extra chips inside the game cartridge,
|
||||
to provide enhanced capabilities of one kind or another.
|
||||
Sometimes,
|
||||
those extra chips were separate CPUs
|
||||
running their own separate firmware,
|
||||
and for those cases
|
||||
higan requires a copy of the co-processor firmware
|
||||
as well as the actual game.
|
||||
Unfortunately,
|
||||
like games themselves,
|
||||
co-processor firmware cannot legally be distributed,
|
||||
so you'll need to obtain
|
||||
copies of the relevant firmware data
|
||||
yourself.
|
||||
|
||||
To import a game that requires co-processor firmware,
|
||||
you must copy the required firmware files
|
||||
beside the game you want to import.
|
||||
For example,
|
||||
if you want to import Megaman X2,
|
||||
which is stored in the file `mmx2.sfc`,
|
||||
the file `cx4.data.rom`
|
||||
must be placed in the same folder
|
||||
for the import to succeed.
|
||||
|
||||
Wikipedia [lists which Super Famicom games use which co-processors][wpec],
|
||||
although not all co-processors require separate firmware.
|
||||
Once you've figured out which co-processor
|
||||
(if any)
|
||||
is used by the game you want to import,
|
||||
here's the firmware files you'll need:
|
||||
|
||||
[wpec]: https://en.wikipedia.org/wiki/List_of_Super_NES_enhancement_chips#List_of_Super_NES_games_that_use_enhancement_chips
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Co-processor</th>
|
||||
<th>Filename</th>
|
||||
<th>Size (bytes)</th>
|
||||
<th>SHA256</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">CX4</th>
|
||||
<td><code>cx4.data.rom</code></td>
|
||||
<td>3072</td>
|
||||
<td><code>ae8d4d1961b93421ff00b3caa1d0f0ce7783e749772a3369c36b3dbf0d37ef18</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" rowspan=2>DSP1/1A<br><sup>See Note 1</sup></th>
|
||||
<td><code>dsp1.data.rom</code></td>
|
||||
<td>2048</td>
|
||||
<td><code>0b5da6533e55852ee8fc397977ec5576c5b9f1fb2e05656d8f87123a121b076e</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>dsp1.program.rom</code></td>
|
||||
<td>6144</td>
|
||||
<td><code>269584b347a22953a2989494c850a7c1c027f4ca5add517a60e0c7d8833d0fac</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" rowspan=2>DSP1B<br><sup>See Note 2</sup></th>
|
||||
<td><code>dsp1b.data.rom</code></td>
|
||||
<td>2048</td>
|
||||
<td><code>8546cbac530830446bb8a277f6b139d4ad64d650bdbac7e4e150e2f095665049</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>dsp1b.program.rom</code></td>
|
||||
<td>6144</td>
|
||||
<td><code>2eccb54a8f89374911f7e2db48f1b4cde855655e28103f7bda2982a5b418a187</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" rowspan=2>DSP2</th>
|
||||
<td><code>dsp2.data.rom</code></td>
|
||||
<td>2048</td>
|
||||
<td><code>3beef9bffdc1e84c9f99f3301d8bd3e520d2e62909a995320f9faeae8f46ec11</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>dsp2.program.rom</code></td>
|
||||
<td>6144</td>
|
||||
<td><code>62a2ef8d2d7db638f4ec0fbcebf0e5bf18a75ee95be06e885d9519a10487f0da</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" rowspan="2">DSP3</th>
|
||||
<td><code>dsp3.data.rom</code></td>
|
||||
<td>2048</td>
|
||||
<td><code>7fe51796e9c97fee1fa2aab40592b7c78997f67dd00333e69d0f79a12f3cb69f</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>dsp3.program.rom</code></td>
|
||||
<td>6144</td>
|
||||
<td><code>aea7b622e7c1de346cb15d16afcbedf92b6798507e179f83ed2a4cff40d0e663</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" rowspan="2">DSP4</th>
|
||||
<td><code>dsp4.data.rom</code></td>
|
||||
<td>2048</td>
|
||||
<td><code>ef3ffb4256dd896a60213269b4e2d3bdd120c97e2fd623bddabbf43c2be422af</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>dsp4.program.rom</code></td>
|
||||
<td>6144</td>
|
||||
<td><code>89b1826e6038be3a0ea0f923e85d589ff6f02dc1a1819fb2ec8c0cea5b333dcd</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" rowspan="2">ST010</th>
|
||||
<td><code>st010.data.rom</code></td>
|
||||
<td>4096</td>
|
||||
<td><code>dc7056a51b53993d7a8ba5bacf9501f785d2fce5e5be748e9ff2737c5938d4a5</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>st010.program.rom</code></td>
|
||||
<td>49152</td>
|
||||
<td><code>2c1f74bb5f466d81c64c326e71ac054489efe1abc9a5d6f91aac7747f2ddab67</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" rowspan="2">ST011</th>
|
||||
<td><code>st011.data.rom</code></td>
|
||||
<td>4096</td>
|
||||
<td><code>b5377d1bebe8adc507a024b6e2b9b8fdf4877e451da84fbad05dff4e4a70311e</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>st011.program.rom</code></td>
|
||||
<td>49152</td>
|
||||
<td><code>d90a5cda380e81cb9ba11a9da7539b173c49b31bedc7a3ac9c3c8b3f97e89e14</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" rowspan="2">ST018</th>
|
||||
<td><code>st018.data.rom</code></td>
|
||||
<td>32768</td>
|
||||
<td><code>b5377d1bebe8adc507a024b6e2b9b8fdf4877e451da84fbad05dff4e4a70311e</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>st018.program.rom</code></td>
|
||||
<td>131072</td>
|
||||
<td><code>d90a5cda380e81cb9ba11a9da7539b173c49b31bedc7a3ac9c3c8b3f97e89e14</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
**Note 1:**
|
||||
The DSP1 and DSP1A are physically different,
|
||||
but the firmware inside is identical.
|
||||
|
||||
**Note 2:**
|
||||
The DSP1B is very similar to the DSP1A,
|
||||
but has some bugs fixed.
|
||||
Note that icarus' heuristics cannot distinguish between
|
||||
a game that uses DSP1
|
||||
and one that uses DSP1B,
|
||||
so if it cannot find your game in its manifest database,
|
||||
it will assume it uses DSP1B.
|
||||
Many games work just as well with either DSP1 or DSP1B,
|
||||
but Pilotwings is a notable exception.
|
||||
|
||||
If you try to import a game
|
||||
using the "Import ROM Files ..." option
|
||||
in [the Library menu](#the-library-menu)
|
||||
(or using icarus directly)
|
||||
but do not have the required firmware files
|
||||
in the correct place,
|
||||
a window will appear saying
|
||||
"Import completed, but with 1 errors. View log?"
|
||||
(or howevery many games were lacking the correct firmware).
|
||||
If you press "Yes",
|
||||
a new window will appear listing the games that couldn't be imported,
|
||||
and at least one firmware file that was missing or incorrect, like this:
|
||||
|
||||
> [smk.zip] firmware (dsp1b.program.rom) missing or invalid
|
||||
|
||||
If you try to import a game
|
||||
using the "Load ROM File ..." option
|
||||
in [the Library menu](#the-library-menu)
|
||||
but do not have the required firmware files
|
||||
in the correct place,
|
||||
nothing will happen,
|
||||
and higan will just sit there
|
||||
with "No cartridge loaded" in
|
||||
[the status bar](#the-status-bar).
|
||||
|
||||
Once a game with co-processor firmware is imported,
|
||||
you can play it just like any [regular game](#importing-and-playing-regular-games).
|
||||
|
||||
Satellaview games
|
||||
-----------------
|
||||
|
||||
The [Satellaview][wpbsx]
|
||||
was a satellite modem peripheral
|
||||
released for the Super Famicom in Japan.
|
||||
As well as the actual modem
|
||||
(designed to sit underneath the Super Famicom),
|
||||
it also included a cartridge
|
||||
with software to control the modem,
|
||||
browse online services,
|
||||
and download games and data.
|
||||
This control cartridge was called
|
||||
*BS-X Sore wa Namae o Nusumareta Machi no Monogatari*,
|
||||
which translates as
|
||||
*BS-X The Story of The Town Whose Name Was Stolen*.
|
||||
|
||||
[wpbsx]: https://en.wikipedia.org/wiki/Satellaview
|
||||
|
||||
The control cartridge had a slot that accepted
|
||||
rewritable "memory paks",
|
||||
so that people could store the games and data they downloaded.
|
||||
A small number of games that did not use the Satellaview modem
|
||||
also had a memory pak slot,
|
||||
so the game's publishers could
|
||||
publish extra content for the game
|
||||
via the Satellaview service
|
||||
after the game's release.
|
||||
For the benefit of people who didn't own a Satellaview
|
||||
some read-only memory paks
|
||||
were sold in retail stores
|
||||
containing extra content for specific games.
|
||||
|
||||
Importing a game that has a slot for a memory pak
|
||||
is just like [importing a regular game](#importing-and-playing-regular-games).
|
||||
|
||||
Importing a memory pak is like importing a regular game,
|
||||
but the name of the memory pak file *must* end in `.bs`
|
||||
(if it's in a `.zip` file,
|
||||
that's OK,
|
||||
but the name *inside* the `.zip` file
|
||||
must end in `.bs`)
|
||||
in order for it to be successfully imported.
|
||||
Sometimes memory pak filenames end in `(BSROM).sfc`,
|
||||
which will make higan try to import them as
|
||||
regular Super Famicom games,
|
||||
and fail miserably.
|
||||
Rename the file and it should work beautifully.
|
||||
|
||||
Playing a game that has a slot for a memory pak
|
||||
is just like playing a regular game,
|
||||
but after you have selected which game you want to play
|
||||
higan will open another
|
||||
[filesystem browser](#the-filesystem-browser)
|
||||
to let you pick which previously-imported memory pak
|
||||
you want to insert into the game.
|
||||
If you press "Cancel" at this point,
|
||||
the game will load without any cartridge in its memory pak slot.
|
||||
|
||||
If you load the control cartridge into higan,
|
||||
make sure the emulated Satellaview
|
||||
is connected to the emulated Super Famicom's expansion port
|
||||
by going to the "Super Famicom" menu,
|
||||
selecting the "Expansion Port" sub-menu,
|
||||
and choosing "Satellaview".
|
||||
If the expansion port was previously
|
||||
configured with a different option,
|
||||
power-cycle the Super Famicom
|
||||
(also in the "Super Famicom" menu)
|
||||
to make sure the control cartridge will find the Satellaview
|
||||
when it starts up.
|
||||
Note that higan's Satellaview emulation is not very accurate,
|
||||
so the control cartridge may not work as it should.
|
||||
|
||||
Playing a memory pak on its own doesn't make much sense,
|
||||
it's not a standalone cartridge.
|
||||
Play a game with a memory pak slot,
|
||||
and choose which memory pak you want when higan asks for it.
|
||||
|
||||
For more information about the Satellaview service,
|
||||
a translation patch for the control cartridge
|
||||
and emulators that do a better job of Satellaview emulation,
|
||||
see [the BS-X Project](https://bsxproj.superfamicom.org/).
|
||||
|
||||
Sufami Turbo games
|
||||
------------------
|
||||
|
||||
The [Sufami Turbo][wpst]
|
||||
was a special cartridge released
|
||||
for the Super Famicom in Japan.
|
||||
The Sufami Turbo on its own does nothing,
|
||||
but it has two slots in the top
|
||||
that accept Sufami Turbo mini-cartridges.
|
||||
The game in slot A is the one that actually plays,
|
||||
but some games can make use of additional data
|
||||
from a game in slot B.
|
||||
|
||||
Importing the Sufami Turbo cartridge
|
||||
is just like [importing a regular game](#importing-and-playing-regular-games).
|
||||
|
||||
Importing a mini-cartridge is like importing a regular game,
|
||||
but the name of the memory pak file *must* end in `.st`
|
||||
(if it's in a `.zip` file,
|
||||
that's OK,
|
||||
but the name *inside* the `.zip` file
|
||||
must end in `.st`)
|
||||
in order for it to be successfully imported.
|
||||
Sometimes memory pak filenames end in `(ST).sfc`,
|
||||
which will make higan try to import them as
|
||||
regular Super Famicom games,
|
||||
and fail miserably.
|
||||
Rename the file and it should work beautifully.
|
||||
|
||||
To play a Sufami Turbo game,
|
||||
load the Sufami Turbo cartridge like any other game.
|
||||
higan will open another
|
||||
[filesystem browser](#the-filesystem-browser)
|
||||
to let you pick which previously-imported mini-cartridge
|
||||
you want to insert into slot A.
|
||||
If you press "Cancel" at this point,
|
||||
the Sufami Turbo cartridge will boot without anything in slot A,
|
||||
which just displays an image telling you
|
||||
to turn off your Super Famicom,
|
||||
insert a game into slot A,
|
||||
and try again.
|
||||
If you chose a cartridge for slot A,
|
||||
higan will yet open another
|
||||
filesystem browser
|
||||
to let you choose a mini-cartridge for slot B.
|
||||
If you press "Cancel" at this point,
|
||||
the Sufami Turbo cartridge will boot without anything in slot B.
|
||||
|
||||
[wpst]: https://en.wikipedia.org/wiki/Sufami_Turbo
|
||||
|
||||
Super Game Boy games
|
||||
--------------------
|
||||
|
||||
The Super Game Boy was a special cartridge
|
||||
released for the Super Famicom
|
||||
(and all its regional variants around the world)
|
||||
that allowed Game Boy games to be played
|
||||
via the Super Famicom's controllers and video output.
|
||||
The Super Game Boy 2 was released in Japan,
|
||||
and had some minor extra features
|
||||
beyond the original Super Game Boy,
|
||||
but importing and playing games
|
||||
works the same way in higan.
|
||||
|
||||
The Super Game Boy cartrige includes
|
||||
the complete hardware of an original
|
||||
(black-and-white)
|
||||
Game Boy,
|
||||
so it needs a boot ROM:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cartridge</th>
|
||||
<th>Filename</th>
|
||||
<th>Size (bytes)</th>
|
||||
<th>SHA256</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">SGB</th>
|
||||
<td><code>sgb.boot.rom</code></td>
|
||||
<td>256</td>
|
||||
<td><code>0e4ddff32fc9d1eeaae812a157dd246459b00c9e14f2f61751f661f32361e360</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">SGB2</th>
|
||||
<td><code>sgb.boot.rom</code></td>
|
||||
<td>256</td>
|
||||
<td><code>fd243c4fb27008986316ce3df29e9cfbcdc0cd52704970555a8bb76edbec3988</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Yes,
|
||||
the SGB and SGB2 have different firmware,
|
||||
but higan expects the same filename for both.
|
||||
|
||||
To import the SGB base cartridge,
|
||||
you must copy the required firmware file
|
||||
into the same directory.
|
||||
Then you may import it just like
|
||||
[a regular game](#importing-and-playing-regular-games).
|
||||
|
||||
To play a Game Boy game in Super Game Boy mode,
|
||||
load the Super Game Boy cartridge like any other game.
|
||||
higan will open another
|
||||
[filesystem browser](#the-filesystem-browser)
|
||||
to let you pick which previously-imported Game Boy game
|
||||
you want to insert into the Super Game Boy.
|
||||
If you press "Cancel" at this point,
|
||||
higan will crash, so don't do that.
|
||||
|
||||
Note that only games for the original, black-and-white Game Boy
|
||||
can be used with the Super Game Boy.
|
||||
Some games designed for the Game Boy Color
|
||||
were backward compatible with the original Game Boy
|
||||
and hence the Super Game Boy;
|
||||
see [Playing Game Boy Colour games in Game Boy mode][blackcarts]
|
||||
for details.
|
||||
|
||||
[blackcarts]: #playing-game-boy-color-games-in-game-boy-mode
|
||||
|
||||
MSU-1 games
|
||||
-----------
|
||||
|
||||
The MSU-1 is a fictional expansion chip
|
||||
invented by higan's author byuu
|
||||
for use with Super Famicom games,
|
||||
designed to allow streaming data and audio.
|
||||
Although the MSU-1 is not specific
|
||||
to any particular storage medium,
|
||||
it gives the Super Famicom similar capabilities
|
||||
to CD-based add-ons
|
||||
like the Mega Drive's Mega CD
|
||||
and the PC Engine's CD-ROM²,
|
||||
such as CD-quality music and full-motion video.
|
||||
|
||||
One thing to be aware of
|
||||
when importing an MSU-1 game
|
||||
is that early firmware versions
|
||||
of the [SD2SNES][sd2snes] programmable cartridge
|
||||
had a bug that caused MSU-1 music to play too quietly.
|
||||
Skipping over [the full details][msu1vol],
|
||||
the short version is this:
|
||||
|
||||
- If offered the choice between "boosted" or non-boosted audio,
|
||||
you want the non-boosted version.
|
||||
- If an MSU-1 mod for a commercial game offers
|
||||
"emulator" and "hardware" versions of the patch file,
|
||||
it means the audio tracks are already boosted.
|
||||
- Some
|
||||
[third](https://www.zeldix.net/t1265-#18320)
|
||||
[parties](https://www.zeldix.net/t1339-#19818)
|
||||
have created replacement, non-boosted audio tracks
|
||||
for the most popular MSU-1 mods.
|
||||
If the mod you want to play has a replacement pack,
|
||||
use it with the "hardware" version of the patch.
|
||||
- Even without access to non-boosted audio tracks,
|
||||
it may be that the existing audio is only slightly boosted,
|
||||
so try the "hardware" version first, for best quality.
|
||||
- If the audio tracks are heavily boosted,
|
||||
the "hardware" patch may sound terrible,
|
||||
distorting and clipping,
|
||||
in which case try the "emulator" patch.
|
||||
|
||||
To import an MSU-1 game:
|
||||
|
||||
1. If you have a single, large file
|
||||
with the `.msu1` extension,
|
||||
that is a pack for use with [Mercurial Magic][mermag],
|
||||
which can automatically set up a game folder
|
||||
in the correct format.
|
||||
Go read Mercurial Magic's documentation
|
||||
instead of these instructions.
|
||||
2. Otherwise,
|
||||
import the Super Famicom ROM with icarus,
|
||||
[like a regular game](#importing-and-playing-regular-games).
|
||||
- If this is a homebrew game with MSU-1 support,
|
||||
there will probably be an ordinary ROM
|
||||
whose name ends in `.sfc`,
|
||||
which is the file you want to import.
|
||||
- If this is a commercial game modded for MSU-1 support,
|
||||
there will probably be a patch file
|
||||
whose name ends in `.ips` or `.bps`.
|
||||
Get a copy of the correct version of the commercial game,
|
||||
apply the patch with a tool like [Flips][flips],
|
||||
then import the patched file.
|
||||
- If there's "hardware" and "emulator" versions of the patch,
|
||||
see "One thing to be aware of..." above.
|
||||
3. Find the game folder in [the game library](#the-game-library)
|
||||
that icarus created when it imported the game.
|
||||
4. Copy the MSU-1 data file into the game folder.
|
||||
- This should be named `msu1.rom`
|
||||
- If there's no file by that name,
|
||||
look for a file with a `.msu` extension
|
||||
and rename it to `msu1.rom`.
|
||||
- If there's no file ending in `.msu` either,
|
||||
create an empty file named `msu1.rom`.
|
||||
5. Copy the audio tracks into the game folder.
|
||||
- If you have to choose between two sets of audio files,
|
||||
see "One thing to be aware of..." above.
|
||||
- These should be named
|
||||
`track-1.pcm`,
|
||||
`track-2.pcm`,
|
||||
... `track-9.pcm`,
|
||||
`track-10.pcm`,
|
||||
etc.
|
||||
- If there's no files with those names,
|
||||
there should be other numbered `.pcm` files
|
||||
that you can rename to match what higan expects.
|
||||
- If the `.pcm` files have no numbers in the filenames,
|
||||
there maybe a `.bml` or `.xml` file that lists
|
||||
which number goes with which file.
|
||||
- If there's no `.pcm` files at all,
|
||||
that's OK,
|
||||
this game probably just doesn't use the audio-playback feature.
|
||||
|
||||
Once the game folder is set up,
|
||||
playing an MSU-1 game is just like
|
||||
[a regular game](#importing-and-playing-regular-games).
|
||||
|
||||
[sd2snes]: https://sd2snes.de/
|
||||
[flips]: http://www.romhacking.net/utilities/1040/
|
||||
[msu1vol]: http://blog.qwertymodo.com/2017/07/the-msu-1-volume-fiasco-explained.html
|
||||
[mermag]: https://github.com/hex-usr/Mercurial-Magic/
|
||||
|
||||
Patched games
|
||||
-------------
|
||||
|
||||
The console emulation community
|
||||
has a long and vibrant history of game modding,
|
||||
or [ROM hacking][rhdn],
|
||||
including fan-translations,
|
||||
new levels for existing games,
|
||||
and more.
|
||||
Since distributing the modified versions of existing games
|
||||
would be copyright infringement,
|
||||
the changes are typically distributed as "patches",
|
||||
a file containing a list of modifications to make,
|
||||
that can be automatically applied by a "patcher" tool
|
||||
like [Flips][flips].
|
||||
|
||||
higan does not support soft-patching,
|
||||
so if you want to play a patched game in higan,
|
||||
you will need to use a patcher to apply it yourself,
|
||||
creating a new, patched copy of the game.
|
||||
|
||||
Then you can import and play the patched game just like
|
||||
[a regular game](#importing-and-playing-regular-games).
|
||||
|
||||
[rhdn]: http://www.romhacking.net/
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
Most of the consoles higan emulates
|
||||
were designed for the low resolution of NTSC televisions,
|
||||
and their video output is often chunky and blocky
|
||||
by today's standards.
|
||||
Shaders customise how a console's video output
|
||||
is drawn to the computer screen,
|
||||
and can apply just about any effect you can imagine.
|
||||
|
||||
Most [drivers](drivers.md)
|
||||
only support these shaders
|
||||
(some only support one or the other):
|
||||
|
||||
- **None** draws each computer pixel
|
||||
in the same colour as the nearest console pixel.
|
||||
This is sometimes called "nearest neighbour scaling",
|
||||
and produces crisp, blocky output.
|
||||
- **Blur** draws each computer pixel
|
||||
as the weighted average colour
|
||||
of the four nearest console pixels.
|
||||
This is sometimes called "bilinear scaling",
|
||||
and hides some of the blockiness
|
||||
at the expense of blurring edges.
|
||||
|
||||
However,
|
||||
the OpenGL driver supports custom shaders,
|
||||
in addition to the above.
|
||||
|
||||
**Note:**
|
||||
For technical reasons,
|
||||
higan's emulation of certain consoles
|
||||
can produce surprising behaviour
|
||||
in certain shaders,
|
||||
particularly shaders that compare each console pixel
|
||||
with its neigbours.
|
||||
See [Console-specific Notes](../notes.md) for details.
|
||||
|
||||
# Where to get shaders
|
||||
|
||||
- higan includes some simple example shaders.
|
||||
If your copy of higan did not come with shaders,
|
||||
you can get them from
|
||||
[the unofficial higan repository](https://gitlab.com/higan/higan/tree/master/shaders).
|
||||
- [quark-shaders](https://github.com/hizzlekizzle/quark-shaders)
|
||||
contains many high-quality shaders for use with higan.
|
||||
- You can write your own.
|
||||
|
||||
# How to install shaders
|
||||
|
||||
Make sure the shader you want to install
|
||||
is in the correct format:
|
||||
it should be a folder whose name ends in `.shader`,
|
||||
it should contain a file named `manifest.bml`,
|
||||
and probably some `*.fs` or `*.vs` files.
|
||||
|
||||
Place the shader folder inside
|
||||
the `Video Shaders` directory
|
||||
of your higan installation.
|
||||
If you don't have a `Video Shaders` directory,
|
||||
create it beside the `*.sys` directories
|
||||
like `Game Boy Advance.sys` and `Super Famicom.sys`.
|
||||
|
||||
- On Windows,
|
||||
this is probably the directory containing `higan.exe`
|
||||
- On Linux,
|
||||
this is probably `~/.local/share/higan`
|
||||
|
||||
Launch higan,
|
||||
open the Settings menu,
|
||||
and choose "Advanced ..."
|
||||
to open [the Advanced tab](../interface/higan-config.md#advanced)
|
||||
of the Settings dialog.
|
||||
Under "Driver Selection",
|
||||
make sure "Video" is set to "OpenGL".
|
||||
If you changed the video driver,
|
||||
you'll need to restart higan
|
||||
for the change to take effect.
|
||||
|
||||
Open the Settings menu again,
|
||||
choose the "Video Shader" submenu,
|
||||
and now the shaders you installed
|
||||
should be listed at the bottom of the menu.
|
||||
|
||||
Load a game
|
||||
(so you can see the results)
|
||||
and switch between shaders
|
||||
to see what they do
|
||||
and pick your favourite!
|
||||
|
||||
# Notable examples
|
||||
|
||||
The quark-shaders repository
|
||||
contains lots of carefully-crafted shaders,
|
||||
but some are particularly noteworthy:
|
||||
|
||||
- **AANN** implements "anti-aliased nearest neighbour" scaling.
|
||||
If the console's video is not displayed
|
||||
at an exact multple of the console's native resolution,
|
||||
rounding errors cause normal nearest-neighbour scaling
|
||||
to draw some rows and columns wider than others,
|
||||
which many people find ugly and distracting.
|
||||
This is very common when
|
||||
higan's aspect-ratio correction mode
|
||||
is enabled.
|
||||
AANN uses very slight anti-aliasing
|
||||
to hide the rounding errors,
|
||||
leaving the overall image as crisp as nearest-neighbour.
|
||||
- **Gameboy** emulates the squarish aspect-ratio
|
||||
greenish-colours
|
||||
and limited palette
|
||||
of the original Game Boy.
|
||||
At larger scales,
|
||||
you can even see the fine gaps between each pixel,
|
||||
and the shadow that dark colours would cast
|
||||
on the LCD background.
|
||||
- **NTSC** performs NTSC encoding,
|
||||
bandwidth limiting,
|
||||
and NTSC decoding of the video image to recreate
|
||||
the colour fringing,
|
||||
blurring
|
||||
and shimmer
|
||||
that most game players would have seen
|
||||
on real televisions.
|
||||
This is important because
|
||||
some games depended on NTSC artifacts
|
||||
to display colours outside the console's official palette
|
||||
or to create effects like transparency.
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
higan, the multi-system emulator
|
||||
================================
|
||||
|
||||
higan emulates a number of classic videogame consoles of the 1980s and 1990s,
|
||||
allowing you to play classic games on a modern general-purpose computer.
|
||||
|
||||
About higan
|
||||
-----------
|
||||
|
||||
As of v102,
|
||||
higan has top-tier support for the following consoles:
|
||||
|
||||
- Nintendo Super Famicom/Super Nintendo Entertainment System,
|
||||
including addon hardware:
|
||||
- Super Game Boy
|
||||
- Sufami Turbo
|
||||
- Nintendo Game Boy Advance
|
||||
|
||||
It also includes some level of support for these consoles:
|
||||
|
||||
- Satellaview addon for the Super Famicom
|
||||
- Nintendo Famicom/Nintendo Entertainment System
|
||||
- Nintendo Game Boy
|
||||
- Nintendo Game Boy Color
|
||||
- Sega Master System
|
||||
- Sega Game Gear
|
||||
- Sega Megadrive/Genesis
|
||||
- NEC PC Engine/TurboGrafx 16 (but not the CD-ROM² System/TurboGrafx-CD)
|
||||
- NEC SuperGrafx
|
||||
- Bandai Wonderswan
|
||||
- Bandai Wonderswan Color
|
||||
|
||||
**Note:** Some consoles were released under different names
|
||||
in different geographic regions.
|
||||
To avoid listing all possible names
|
||||
every time such a console is mentioned,
|
||||
higan uses the name from the console's region of origin.
|
||||
In practice,
|
||||
that means Japanese names:
|
||||
"Famicom" and "Super Famicom" instead of NES and SNES,
|
||||
"Mega Drive" instead of "Genesis",
|
||||
"PC Engine" instead of "TurboGrafx-16".
|
||||
|
||||
higan is actively supported on
|
||||
FreeBSD 10 and above, and
|
||||
Microsoft Windows 7 and above.
|
||||
It also includes some level of support
|
||||
for GNU/Linux and macOS.
|
||||
|
||||
If you want to install higan and try it out,
|
||||
see the [Quick Start](#quick-start) section below.
|
||||
|
||||
higan is officially spelled with a lowercase "h", not a capital.
|
||||
|
||||
About this document
|
||||
-------------------
|
||||
|
||||
This is the unofficial higan README,
|
||||
a community-maintained introduction and reference.
|
||||
It may be out of date
|
||||
by the time you read this,
|
||||
and it may contain errors or omissions.
|
||||
If you find something that's wrong,
|
||||
or you have a suggestion,
|
||||
see "Unofficial higan resources" below.
|
||||
|
||||
Official higan resources
|
||||
------------------------
|
||||
|
||||
- [Official homepage](https://byuu.org/emulation/higan/)
|
||||
- [Official forum](https://board.byuu.org/viewforum.php?f=4)
|
||||
|
||||
Unofficial higan resources
|
||||
--------------------------
|
||||
|
||||
- [Source code repository](https://gitlab.com/higan/higan/)
|
||||
archives official higan releases
|
||||
and WIP snapshots
|
||||
since approximately v067r21.
|
||||
- [Quark shader repository](https://github.com/hizzlekizzle/quark-shaders)
|
||||
collects shaders that higan can use
|
||||
to add special effects like TV scanlines to its video output,
|
||||
or smarter algorithms for scaling up to modern PC resolutions.
|
||||
See [Installing custom shaders][shaders] below for details.
|
||||
- [Mercurial Magic](https://github.com/hex-usr/Mercurial-Magic/)
|
||||
is a tool for converting MSU-1 games and mods into a format
|
||||
higan can use.
|
||||
See [Importing MSU-1 games][msu1] below for details.
|
||||
|
||||
[shaders]: #installing-custom-shaders
|
||||
[msu1]: #importing-msu-1-games
|
||||
|
||||
There are also other projects
|
||||
based on current or older versions of higan,
|
||||
in whole or in part,
|
||||
that you might want to check out.
|
||||
|
||||
- [Mednafen](https://mednafen.github.io/)
|
||||
is another multi-system emulator.
|
||||
Its Super Famicom emulation is based on bsnes v059,
|
||||
from the time before bsnes was renamed to higan.
|
||||
- [BizHawk](http://tasvideos.org/BizHawk.html)
|
||||
is another multi-system emulator,
|
||||
specialising in the creation of
|
||||
tool-assisted speedruns.
|
||||
Its Super Famicom emulation is based on bsnes v087.
|
||||
- [nSide](https://github.com/hex-usr/nSide)
|
||||
is a fork of higan that greatly enhances
|
||||
its NES emulation support,
|
||||
and adds minor features to the other cores too.
|
||||
It also restores the "balanced" Super Famicom emulation core
|
||||
that was removed from higan in v099,
|
||||
which is less CPU intensive
|
||||
than the current accuracy-focussed core.
|
||||
- [bsnes-plus](https://github.com/devinacker/bsnes-plus)
|
||||
is a fork of bsnes v073
|
||||
that adds improved support for debugging Super Famicom software.
|
||||
@@ -1,52 +0,0 @@
|
||||
Installing the GBA BIOS
|
||||
-----------------------
|
||||
|
||||
For most of the systems higan emulates,
|
||||
the console itself contains (almost) no actual software,
|
||||
so emulating the system does not require
|
||||
infringing the copyright of the hardware manufacturer.
|
||||
However,
|
||||
the Game Boy Advance is different:
|
||||
every device contains a standard library of software routines
|
||||
for [common functions games require][bios],
|
||||
often called a "BIOS"
|
||||
by analogy with the Basic Input/Output System
|
||||
used in IBM PC compatibles.
|
||||
|
||||
For the same legal reasons that commercial games
|
||||
cannot be distributed with emulators,
|
||||
the GBA BIOS cannot be distributed with higan,
|
||||
but is required for GBA software to run.
|
||||
|
||||
If you have a real GBA and a flashcart,
|
||||
the Internet contains many tools
|
||||
that will extract the BIOS image so it can be copied
|
||||
to your desktop computer.
|
||||
The correct GBA BIOS file is exactly 16384 bytes long,
|
||||
and has the SHA-256 hash
|
||||
fd2547724b505f487e6dcb29ec2ecff3af35a841a77ab2e85fd87350abd36570.
|
||||
|
||||
Once you have the correct BIOS file:
|
||||
|
||||
1. rename it to `bios.rom`
|
||||
- if you're using Windows,
|
||||
turn off "hide extensions for known file types"
|
||||
so you don't wind up with a file called
|
||||
`bios.rom.dat`
|
||||
or whatever the file's original extension was.
|
||||
2. Copy the file into higan's `Game Boy Advance.sys` directory,
|
||||
alongside the `manifest.bml` file that is already there.
|
||||
- In Windows,
|
||||
find `Game Boy Advance.sys` in the same folder
|
||||
as `higan.exe`
|
||||
- In Linux,
|
||||
find `Game Boy Advance.sys` in
|
||||
`~/.local/share/higan/`
|
||||
|
||||
**Note:**
|
||||
If you upgrade this version of higan to a newer version,
|
||||
make sure the `bios.rom` file
|
||||
winds up in the `Game Boy Advance.sys` directory
|
||||
of the new version.
|
||||
|
||||
[bios]: http://problemkaputt.de/gbatek.htm#biosfunctions
|
||||
@@ -1,136 +0,0 @@
|
||||
Compiling from source on Linux
|
||||
------------------------------
|
||||
|
||||
You will need a copy of the higan source-code.
|
||||
If you download an official release from the higan homepage,
|
||||
you will need [7-zip][7z] or a compatible tool to extract it.
|
||||
Alternatively,
|
||||
you may obtain higan source code from
|
||||
[the unofficial git repo](https://gitlab.com/higan/higan/)
|
||||
using the Git source-code management tool,
|
||||
or by clicking the download button on the right-hand side of the web-page
|
||||
and choosing an archive format.
|
||||
|
||||
You will also need GCC 4.9 or higher,
|
||||
including the C and C++ compiler,
|
||||
GNU Make,
|
||||
and development files
|
||||
(headers, etc.)
|
||||
for the following libraries:
|
||||
|
||||
- GTK 2.x
|
||||
- PulseAudio
|
||||
- Mesa
|
||||
- gtksourceview 2.x
|
||||
- Cairo
|
||||
- SDL 1.2
|
||||
- libXv
|
||||
- libAO
|
||||
- OpenAL
|
||||
- udev
|
||||
|
||||
On a Debian-derived Linux distribution,
|
||||
you can install everything you need with a command like:
|
||||
|
||||
sudo apt-get 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
|
||||
|
||||
Once you have all the dependencies installed,
|
||||
you may build and install higan.
|
||||
|
||||
Note: Run these commands as yourself,
|
||||
**do not run them as root**
|
||||
(no `sudo`, no `su`, etc.),
|
||||
because higan does not support
|
||||
being installed system-wide.
|
||||
|
||||
1. Put the higan source code in some convenient location,
|
||||
like `~/higan-src`
|
||||
2. Open a terminal window
|
||||
3. Type `cd ~/higan-src`
|
||||
(or wherever you put the higan source)
|
||||
and press Enter
|
||||
4. Type `make -C icarus compiler=g++` and press Enter
|
||||
to build the icarus import tool
|
||||
5. Type `make -C higan compiler=g++` and press Enter
|
||||
to build the main higan executable
|
||||
|
||||
Installing a compiled build on Linux
|
||||
------------------------------------
|
||||
|
||||
Assuming you have successfully compiled higan
|
||||
as described in the previous section:
|
||||
|
||||
1. Open a terminal window
|
||||
2. Type `cd ~/higan-src`
|
||||
(or wherever you put the higan source)
|
||||
and press Enter
|
||||
3. Type `make -C icarus install` and press Enter
|
||||
to install icarus and its game database
|
||||
4. Type `make -C higan install` and press Enter
|
||||
to install higan and its supporting files
|
||||
|
||||
This installs higan and its associated data files
|
||||
into the `~/.local` directory hierarchy.
|
||||
|
||||
To confirm higan is installed correctly,
|
||||
type `higan` in a terminal and press Enter.
|
||||
If the higan window appears,
|
||||
everything is working.
|
||||
On the other hand,
|
||||
if you get an error message like "command not found",
|
||||
you should double-check that the directory `~/.local/bin`
|
||||
is included in your `$PATH` environment variable
|
||||
by running the following command in a terminal:
|
||||
|
||||
echo "$PATH" | tr ':' '\n' | grep ~/.local/bin
|
||||
|
||||
If the above command prints the full path of `~/.local/bin`
|
||||
(for example: `/home/yourname/.local/bin`)
|
||||
then you should be good.
|
||||
If it prints nothing,
|
||||
you need to add the following line to `~/.profile`:
|
||||
|
||||
export PATH=~/.local/bin:$PATH
|
||||
|
||||
(this line must be in `~/.profile` because
|
||||
most GUIs do not read any other files at login)
|
||||
|
||||
If you also have a `~/.bash_profile`,
|
||||
make sure it reads the contents of `~/.profile`
|
||||
with a line like this:
|
||||
|
||||
source ~/.profile
|
||||
|
||||
You will need to log out and log back in
|
||||
for changes to `~/.profile` or `~/.bash_profile`
|
||||
to take effect.
|
||||
|
||||
Before you can actually play games,
|
||||
you'll need to [import them](#the-game-library)
|
||||
and [configure higan](#configuring-higan).
|
||||
If you want to play Game Boy Advance games,
|
||||
you will need [a GBA BIOS](#installing-the-gba-bios).
|
||||
|
||||
Uninstalling a compiled build on Linux
|
||||
--------------------------------------
|
||||
|
||||
To uninstall higan,
|
||||
as installed by the above instructions:
|
||||
|
||||
1. Open a terminal window
|
||||
2. Type `cd ~/higan-src`
|
||||
(or wherever you put the higan source)
|
||||
and press Enter
|
||||
3. Type `make -C icarus uninstall` and press Enter
|
||||
4. Type `make -C higan uninstall` and press Enter
|
||||
|
||||
To remove higan's configuration,
|
||||
delete the directory `~/.config/higan` as well.
|
||||
|
||||
To remove the games imported into higan's library
|
||||
(including in-game saves and save-states),
|
||||
delete the directory `~/Emulation`.
|
||||
|
||||
You may also wish to delete the higan source directory.
|
||||
@@ -1,163 +0,0 @@
|
||||
Installing an official release on Windows
|
||||
-----------------------------------------
|
||||
|
||||
Official higan releases are distributed in [7-zip][7z] archives.
|
||||
You will need to install 7-zip,
|
||||
or another compatible archiving tool,
|
||||
to install higan.
|
||||
|
||||
[7z]: http://www.7-zip.org/
|
||||
|
||||
Once you have a suitable archiving tool,
|
||||
extract the contents of the higan archive into a new folder.
|
||||
|
||||
When you're done,
|
||||
the new folder should contain `higan.exe` and `icarus.exe`
|
||||
along with other assorted files and directories
|
||||
that describe the systems higan emulates.
|
||||
|
||||
You may put that folder wherever you like.
|
||||
|
||||
To run higan, open the `higan.exe` file.
|
||||
|
||||
Before you can actually play games,
|
||||
you'll need to [import them](#the-game-library)
|
||||
and [configure higan](#configuring-higan).
|
||||
If you want to play Game Boy Advance games,
|
||||
you will need [a GBA BIOS](#installing-the-gba-bios).
|
||||
|
||||
Uninstalling an official release on Windows
|
||||
-------------------------------------------
|
||||
|
||||
Delete the folder containing `higan.exe`
|
||||
and the other associated data from the original archive.
|
||||
|
||||
To remove higan's configuration:
|
||||
|
||||
1. Press Win+R to open the Run dialog
|
||||
2. Type `%LOCALAPPDATA%` and press Enter
|
||||
to open the folder where higan's configuration data lives
|
||||
3. Delete the subdirectories named `icarus` and `higan`
|
||||
if they exist.
|
||||
|
||||
You might also want to remove the games imported into higan's library
|
||||
(including in-game saves and save-states):
|
||||
|
||||
1. Press Win+R to open the Run dialog
|
||||
2. Type `%USERPROFILE%` and press Enter
|
||||
to open the folder where higan keeps its game library
|
||||
3. Delete the folder named `Emulation` if it exists
|
||||
|
||||
Compiling from source on Windows
|
||||
--------------------------------
|
||||
|
||||
You will need a copy of the higan source-code.
|
||||
If you download an official release from the higan homepage,
|
||||
you will need [7-zip][7z] or a compatible tool to extract it.
|
||||
Alternatively,
|
||||
you may obtain higan source code from
|
||||
[the unofficial git repo](https://gitlab.com/higan/higan/)
|
||||
using the Git source-code management tool,
|
||||
or by clicking the download button on the right-hand side of the web-page
|
||||
and choosing an archive format.
|
||||
|
||||
You will need a C++ compiler to compile higan.
|
||||
We recommend installing [TDM64-GCC][tdm],
|
||||
preferably the latest version
|
||||
but anything newer than 4.9 should be fine.
|
||||
higan does not support building with clang++
|
||||
(Clang is still not quite there yet for Windows)
|
||||
nor Microsoft Visual C++
|
||||
(last we checked, it didn't support all the C++ features higan uses).
|
||||
|
||||
**Note:** Make sure you get TDM64-GCC,
|
||||
not TDM-GCC.
|
||||
When compiled in x86 (32-bit) mode,
|
||||
higan may crash at startup
|
||||
because gcc targeting x86 does not support
|
||||
Windows' structured exception handling (SEH).
|
||||
Also,
|
||||
historically in x86 mode
|
||||
gcc has miscompiled a part of the NES emulation core.
|
||||
See the higan forum
|
||||
[for](https://board.byuu.org/viewtopic.php?p=41977#p41977)
|
||||
[details](https://board.byuu.org/viewtopic.php?p=42253#p42253).
|
||||
|
||||
Once you've installed mingw-w64,
|
||||
open a command-prompt window,
|
||||
type `g++ --version`
|
||||
then press Enter
|
||||
to check it's installed correctly.
|
||||
You should see a message like
|
||||
|
||||
g++ 1.2.3 20010101
|
||||
Copyright (C) 2001 Free Software Foundation, Inc.
|
||||
This is free software; see the source for copying conditions. There is NO
|
||||
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
...except it should mention the version of mingw that you installed
|
||||
and the corresponding dates.
|
||||
If you see an error message like "command not found"
|
||||
or "bad command or filename",
|
||||
you may need to add mingw's "bin" folder
|
||||
to your computer's `%PATH%`.
|
||||
See the mingw documentation for help with that.
|
||||
|
||||
Once mingw is installed and available from the command prompt:
|
||||
|
||||
1. Put the higan source code in some convenient location,
|
||||
like `C:\higan-src`
|
||||
2. Open the command-prompt
|
||||
3. Type `cd C:\higan-src`
|
||||
(or wherever you put the higan source)
|
||||
and press Enter
|
||||
4. Type `mingw32-make -C icarus compiler=g++` and press Enter
|
||||
to build the icarus import tool
|
||||
5. Type `mingw32-make -C higan compiler=g++` and press Enter
|
||||
to build the main higan executable
|
||||
|
||||
[tdm]: http://tdm-gcc.tdragon.net/download
|
||||
|
||||
Installing a compiled build on Windows
|
||||
--------------------------------------
|
||||
|
||||
1. In Windows Explorer,
|
||||
create the folder where you want higan to live
|
||||
2. Assuming you built higan in `C:\higan-src`,
|
||||
copy `C:\higan-src\icarus\out\icarus.exe`
|
||||
into the new folder
|
||||
3. Copy `C:\higan-src\icarus\Database` and its contents
|
||||
into the new folder
|
||||
4. Copy `C:\higan-src\higan\out\higan.exe`
|
||||
into the new folder
|
||||
5. Copy all the `*.sys` directories
|
||||
in `C:\higan-src\higan\systems`
|
||||
into the new folder
|
||||
|
||||
The new folder should now contain
|
||||
`icarus.exe`,
|
||||
`higan.exe`,
|
||||
a folder named `Database`,
|
||||
and half a dozen folders named after the systems higan emulates
|
||||
with `.sys` at the end.
|
||||
This is what you would get by downloading an official build,
|
||||
as described under
|
||||
[Installing an official release on Windows][instwin]
|
||||
above.
|
||||
|
||||
[instwin]: #installing-an-official-release-on-windows
|
||||
|
||||
Before you can actually play games,
|
||||
you'll need to [import them](#the-game-library)
|
||||
and [configure higan](#configuring-higan).
|
||||
If you want to play Game Boy Advance games,
|
||||
you will need [a GBA BIOS](#installing-the-gba-bios).
|
||||
|
||||
Uninstalling a compiled build on Windows
|
||||
----------------------------------------
|
||||
|
||||
The process is the same as
|
||||
[Uninstalling an official release on Windows][uninstwin]
|
||||
above. You may also wish to delete the higan source folder.
|
||||
|
||||
[uninstwin]: #uninstalling-an-official-release-on-windows
|
||||
@@ -1,61 +0,0 @@
|
||||
The Filesystem Browser
|
||||
----------------------
|
||||
|
||||
Sometimes higan will need
|
||||
to ask you to choose a file or folder.
|
||||
For this, it uses a special Filesystem Browser dialog.
|
||||
Although many operating systems provide a native filesystem browser,
|
||||
they do not all allow the same customizations.
|
||||
Therefore,
|
||||
higan provides its own filesystem browser
|
||||
that works the same way on every platform.
|
||||
|
||||
The filesystem browser shows the contents of some particular folder,
|
||||
and allows you to select one of those items.
|
||||
|
||||
Across the top of the window,
|
||||
a text-box displays the path of the current folder.
|
||||
If you want to browse a specific path,
|
||||
you may edit the contents of this box
|
||||
and press Enter to switch to the new location.
|
||||
|
||||
The button with two blue arrows at the top-right is "Refresh".
|
||||
Pressing this button will check for
|
||||
added (or removed) items in the current folder,
|
||||
and add (or remove) them to (or from) the list.
|
||||
|
||||
The button with the house at the top-right is "Home".
|
||||
Pressing this button will switch to your home folder.
|
||||
|
||||
The button with the green up-arrow at the top right is "Parent".
|
||||
Pressing this button will
|
||||
switch to the parent of the current folder.
|
||||
|
||||
Most of the filesystem browser lists the contents
|
||||
of the current directory.
|
||||
Double-clicking a folder,
|
||||
or selecting it and pressing Enter,
|
||||
will switch to showing the contents of that directory.
|
||||
If the list has keyboard focus,
|
||||
typing any text will jump to the first inem in the list
|
||||
whose name begins with the text you typed.
|
||||
|
||||
If a drop-down list appears in the bottom-left,
|
||||
it allows you to choose which files appear in the list,
|
||||
based on file-extension.
|
||||
|
||||
If this filesystem browser is asking for a file,
|
||||
you can choose one
|
||||
by double-clicking it,
|
||||
by selecting it and pressing Enter,
|
||||
or by selecting it and clicking the "Select" button in the bottom-right.
|
||||
|
||||
If this filesystem browser is asking for a directory,
|
||||
you can choose one
|
||||
by selecting it and clicking the "Select" button in the bottom-right.
|
||||
Double-clicking
|
||||
or selecting and pressing Enter don't work,
|
||||
they just switch to viewing that directory.
|
||||
|
||||
The "Cancel" button in the bottom-right
|
||||
closes the filesystem browser without selecting anything.
|
||||
@@ -1,40 +0,0 @@
|
||||
|
||||
# Synopsis
|
||||
|
||||
> higan [*\-\-fullscreen*] [*PATH*]
|
||||
|
||||
# Description
|
||||
|
||||
TODO: Put `NTSC-J|`, `NTSC-U|` or `PAL|`
|
||||
at the beginning of the path
|
||||
to force a region
|
||||
for consoles where it can't be detected.
|
||||
|
||||
When launched with `--fullscreen`,
|
||||
higan will automatically enter full-screen mode
|
||||
when it starts.
|
||||
This is not much use unless you also specify `PATH`,
|
||||
because you won't be able to load a game
|
||||
until you exit full-screen mode
|
||||
by pressing the "Toggle Fullscreen"
|
||||
[hotkey](higan-config.md#hotkeys).
|
||||
|
||||
When `PATH` is the path to an existing
|
||||
[game folder](../concepts/game-folders.md)
|
||||
for any supported console,
|
||||
that game will be loaded when higan starts.
|
||||
|
||||
When `PATH` is the path to a ROM file
|
||||
for any supported console,
|
||||
it will be imported into a game folder in
|
||||
[the Game Library](../concepts/game-library.md),
|
||||
and then loaded from there when higan starts.
|
||||
|
||||
# Examples
|
||||
|
||||
Play a previously-imported copy of Super Mario World
|
||||
in full-screen (assuming Linux defaults):
|
||||
|
||||
```sh
|
||||
higan --fullscreen ~/Emulation/"Super Famicom"/"Super Mario World.sfc"
|
||||
```
|
||||
@@ -1,298 +0,0 @@
|
||||
TODO: Rename this file to "higan-settings.md"
|
||||
|
||||
The Settings window
|
||||
appears when you choose
|
||||
one of the items at the bottom of
|
||||
[the Settings menu](higan.md#the-settings-menu).
|
||||
It contains less-frequently-modified configuration options.
|
||||
Most of these can be safely ignored,
|
||||
or set once and never changed again.
|
||||
|
||||
This window has a tab for each main category of options:
|
||||
|
||||
Video
|
||||
=====
|
||||
|
||||
This tab contains options that affect
|
||||
how higan displays
|
||||
the emulated console's video output.
|
||||
|
||||
- **Saturation**: adjusts the vibrancy of colours displayed,
|
||||
where 0% makes things pure grey,
|
||||
100% is normal,
|
||||
and 200% is garishly brightly coloured.
|
||||
- **Gamma**: adjusts how bright mid-range colours are
|
||||
compared to the brightest colours,
|
||||
where 100% is normal,
|
||||
and 200% makes mid-range colours much darker.
|
||||
- **Luminance**: adjusts the overall brightness,
|
||||
where 100% is normal,
|
||||
and 0% is totally black.
|
||||
- **Overscan Mask**: hides parts of
|
||||
the video output that would have been hidden
|
||||
by the bezel around the edge of
|
||||
a standard-definition television screen.
|
||||
Some games (particularly on the Famicom)
|
||||
displayed random glitchy output in this area,
|
||||
which can be distracting.
|
||||
The units are "pixels in the emulated console's standard video-mode".
|
||||
For example, setting "Horizontal" to 8
|
||||
will clip 8/256ths from the left and right sides
|
||||
of the Super Famicom's video output,
|
||||
whether the Super Famicom is in
|
||||
lo-res (256px) or hi-res (512px)
|
||||
mode.
|
||||
- **Aspect Correction**:
|
||||
(in both Windowed Mode and Fullscreen Mode)
|
||||
stretches the image to match the aspect ratio
|
||||
produced by the original console hardware,
|
||||
but can cause a "ripple" effect,
|
||||
due to rounding errors.
|
||||
- **Resize Window to Viewport**:
|
||||
(under "Windowed mode")
|
||||
causes higan to resize its window
|
||||
to fit snugly around the emulated console's video
|
||||
whenever it changes size:
|
||||
because a game was loaded for a different console
|
||||
with a different display size or aspect ratio,
|
||||
because the "Overscan Mask" controls were adjusted,
|
||||
because the game switched to a different video mode,
|
||||
because the user pressed the "Rotate Display" hotkey,
|
||||
etc.
|
||||
When this option is disabled,
|
||||
the higan window stays at a fixed size,
|
||||
large enough to contain the video for any supported console,
|
||||
padded with black borders for all smaller video modes.
|
||||
- **Resize Viewport to Window**:
|
||||
(under "Fullscreen mode")
|
||||
causes higan to stretch the emulated console's video output
|
||||
to touch the edges of the screen.
|
||||
Since most screens are not an exact multiple
|
||||
of the size of all emulated consoles,
|
||||
this may cause a "ripple" effect,
|
||||
due to rounding errors.
|
||||
When this option is disabled,
|
||||
higan stretches the emulated console's video output
|
||||
to the largest exact multiple
|
||||
of the emulated console's video output
|
||||
that is smaller than or equal to the screen size.
|
||||
- TODO: Update this to match 103r11, or whatever the latest version is.
|
||||
|
||||
Audio
|
||||
=====
|
||||
|
||||
This tab contains options that affect
|
||||
how higan reproduces
|
||||
the emulated console's audio output.
|
||||
|
||||
- **Device**: allows you to choose
|
||||
which audio device higan sends
|
||||
the emulated game's audio to.
|
||||
- **Frequency**: controls the sample-rate that higan will use
|
||||
when generating audio.
|
||||
If your PC's audio hardware has a "native" sample-rate
|
||||
and you know what it is,
|
||||
pick that.
|
||||
Otherwise,
|
||||
44.1kHz or 48kHz should be fine.
|
||||
- **Latency**: controls how much audio output higan calculates in advance.
|
||||
Higher values reduce the chance of
|
||||
"popping" or "glitching" noises,
|
||||
but increase the delay between an action occurring on-screen
|
||||
and the corresponding sound-effect being played.
|
||||
- **Exclusive Mode**: appears
|
||||
if the current audio driver
|
||||
allows higan to take exclusive control of your PC's audio output,
|
||||
so no other applications can play sounds.
|
||||
This can improve audio quality,
|
||||
and lower the effective audio latency.
|
||||
- **Volume**: controls the overall loudness of
|
||||
the emulated console's audio,
|
||||
where 100% is normal volume,
|
||||
and 0% is complete silence.
|
||||
- **Balance**: controls the relative loudness of
|
||||
the left and right speakers,
|
||||
where 0% means only the left speaker produces sound,
|
||||
50% means both speakers produce sound equally,
|
||||
and 100% means only the right speaker produces sound.
|
||||
- **Reverb**: adds a slight reverberation effect
|
||||
to the emulated console's audio output,
|
||||
as though the console were in a tunnel or small room.
|
||||
|
||||
Input
|
||||
=====
|
||||
|
||||
This tab controls which PC inputs
|
||||
are used for which emulated controllers.
|
||||
The exact PC inputs that can be mapped
|
||||
depend on [the input driver](#drivers).
|
||||
|
||||
- **Pause Emulation**: automatically pauses emulation
|
||||
when the main higan window
|
||||
is not the current foreground window.
|
||||
- **Allow Input**: can be ticked
|
||||
when "Pause Emulation" is *not* ticked,
|
||||
and allows configured inputs to keep affecting higan
|
||||
even when higan is running in the background.
|
||||
This is particularly relevant if
|
||||
you configure your PC keyboard to control higan:
|
||||
if you tick this box,
|
||||
and switch to a different application
|
||||
leaving higan running in the background,
|
||||
typing in that other application may affect
|
||||
the emulated game running in higan
|
||||
even though you can't see it!
|
||||
- The console selector chooses which console's inputs
|
||||
to display in the mapping list below.
|
||||
- The port selector chooses which port of the selected console
|
||||
to display in the mapping list below.
|
||||
- The controller selector chooses which controller
|
||||
associated with the given console and port
|
||||
to display in the mapping list below.
|
||||
- The mapping list includes
|
||||
every button and axis on the selected controller,
|
||||
and the PC inputs that are mapped to it
|
||||
when it is connected to the selected port of the selected console.
|
||||
- **Erase**: removes the mapping
|
||||
for the selected button or axis.
|
||||
- **Reset**: removes all the mappings currently in the list.
|
||||
- TODO: Mention that controllers must be connected
|
||||
in the console menu
|
||||
before they can be used.
|
||||
|
||||
To map
|
||||
a keyboard or gamepad button on your PC to
|
||||
a controller button,
|
||||
double-click the controller button in the list,
|
||||
or select it and press Enter.
|
||||
The window will grey out,
|
||||
and a message will appear in the bottom left:
|
||||
"Press a key or button to map [the button]".
|
||||
Press the key or button you want to map,
|
||||
and it should appear in the list
|
||||
next to the controller button it is mapped to.
|
||||
|
||||
To map
|
||||
a mouse button on your PC to
|
||||
a controller button,
|
||||
select the controller button in the list,
|
||||
then click one of the "Mouse Left",
|
||||
"Mouse Middle",
|
||||
or "Mouse Right" buttons in the bottom-left of the window.
|
||||
|
||||
To map
|
||||
a joystick axis on your PC to
|
||||
a controller axis,
|
||||
double-click the axis in the list,
|
||||
or select it and press Enter.
|
||||
The window will grey out,
|
||||
and a message will appear in the bottom left:
|
||||
"Press a key or button to map [the axis]".
|
||||
Press the joystick in the direction you want to map,
|
||||
and it should appear in the list
|
||||
next to the controller button it is mapped to.
|
||||
|
||||
To map
|
||||
a mouse axis on your PC to
|
||||
a controller axis,
|
||||
select the axis in the list,
|
||||
then click one of the
|
||||
"Mouse X-axis",
|
||||
or "Mouse Y-axis"
|
||||
buttons in the bottom-left of the window.
|
||||
|
||||
If you start mapping a button or axis,
|
||||
but decide you don't want to,
|
||||
you can press Escape
|
||||
to exit the "Press a key or button to map..." mode
|
||||
without actually mapping anything.
|
||||
|
||||
The "Rumble" setting
|
||||
for the Game Boy Advance is treated like a button,
|
||||
and can be mapped to a PC gamepad.
|
||||
When the emulated Game Boy Advance
|
||||
tries to use the rumble feature
|
||||
of the Game Boy Player,
|
||||
higan will turn on the force-feedback
|
||||
of whatever gamepad the mapped button is part of.
|
||||
|
||||
|
||||
Hotkeys
|
||||
=======
|
||||
|
||||
This tab is like "Inputs" above,
|
||||
except it contains controls for higan itself,
|
||||
instead of for the emulated console.
|
||||
|
||||
- **Toggle Fullscreen**: puts higan into fullscreen mode,
|
||||
where the menu and status bar are hidden,
|
||||
and the emulated console's video output
|
||||
is enlarged to cover the entire screen.
|
||||
Toggling fullscreen also automatically captures the mouse.
|
||||
- **Toggle Mouse Capture**: hides the usual mouse-cursor,
|
||||
and captures the mouse so it cannot leave the higan window.
|
||||
This is useful when the mouse is being used to emulate
|
||||
a light-gun controller like the Super Scope.
|
||||
- **Save Quick State**: saves the current state of the emulated console
|
||||
to the currently-selected Quick State slot.
|
||||
- **Load Quick State**: restores the emulated console
|
||||
to the state saved in the currently-selected Quick State slot.
|
||||
- **Decrement Quick State**: selects the previous Quick State slot.
|
||||
The status bar will briefly display the new current slot number.
|
||||
- **Increment Quick State**: selects the next Quick State slot.
|
||||
The status bar will briefly display the new current slot number.
|
||||
- **Pause Emulation**: pauses the emulated console
|
||||
until the Pause Emulation hotkey is pressed a second time.
|
||||
- **Fast Forward**: disables audio and video synchronisation
|
||||
for as long as it's held down,
|
||||
so emulation proceeds as quickly as possible.
|
||||
If your PC struggles to hit "real time"
|
||||
(60fps for most emulated consoles),
|
||||
this likely won't have any effect.
|
||||
- **Power Cycle**: turns the emulated console off and back on,
|
||||
(a "hard reset"),
|
||||
just like the "Power Cycle" menu item
|
||||
in [the console menu](#the-console-menu).
|
||||
- **Rotate Display**: will toggle the display
|
||||
of the Game Boy Advance
|
||||
and WonderSwan (Color)
|
||||
between the usual landscape orientation
|
||||
and a portrait orientation (90° counter-clockwise).
|
||||
These consoles have games
|
||||
that expect the player to hold the console
|
||||
in a different way.
|
||||
|
||||
Advanced
|
||||
========
|
||||
|
||||
This tab contains all the settings
|
||||
that didn't fit into one of the other categories.
|
||||
|
||||
- **Video**: controls how higan will draw
|
||||
the emulated console's video output
|
||||
to the PC screen.
|
||||
"None" means no video will be drawn.
|
||||
See [Drivers](#drivers) for details.
|
||||
- **Audio**: controls how higan will present
|
||||
the emulated console's audio output.
|
||||
"None" means no audio will be played.
|
||||
See [Drivers](#drivers) for details.
|
||||
- **Input**: controls how higan checks for input
|
||||
from the PC's input devices.
|
||||
"None" means the emulated console cannot be controlled.
|
||||
See [Drivers](#drivers) for details.
|
||||
- **Location**: selects where the [Game Library](#the-game-library)
|
||||
looks for games to load.
|
||||
See [Moving the Game Library](#moving-the-game-library)
|
||||
for more information.
|
||||
- **Ignore Manifests**: makes higan ignore the manifest file
|
||||
in the a loaded game's [game folder](#why-game-folders)
|
||||
in favour of asking icarus
|
||||
to guess a manifest on the fly.
|
||||
This means that incompatible or incorrect manifests
|
||||
generated by old versions of icarus
|
||||
won't cause problems,
|
||||
but means you can't fix incorrect manifests
|
||||
generated by the current version of icarus.
|
||||
See also the "Create Manifests" option in
|
||||
[the icarus Settings dialog](#the-icarus-settings-dialog).
|
||||
@@ -1,145 +0,0 @@
|
||||
The Tools window
|
||||
appears when you choose
|
||||
one of the items at the bottom of
|
||||
[the Tools menu](higan.md#the-tools-menu).
|
||||
|
||||
The window has a tab for each tool:
|
||||
|
||||
The Cheat Editor
|
||||
----------------
|
||||
|
||||
For some consoles,
|
||||
higan supports applying temporary changes to the code of a running game.
|
||||
For example,
|
||||
you could disable the code that registers when the player takes damage,
|
||||
resulting in an "invulnerability" mode.
|
||||
Currently,
|
||||
higan supports cheats for the following consoles:
|
||||
|
||||
- Famicom
|
||||
- Super Famicom
|
||||
- Game Boy
|
||||
- Master System
|
||||
- PC Engine
|
||||
- Wonder Swan
|
||||
|
||||
A cheat code of the format `addr=data`
|
||||
will cause the emulated console to obtain `data`
|
||||
whenever it reads from memory address `addr`.
|
||||
A cheat code of the format `addr=comp?data`
|
||||
will cause reads from `addr` to obtain `data`,
|
||||
but only if the true value at `addr` is `comp`.
|
||||
In both formats,
|
||||
`data` is a single byte expressed as two hexadecimal digits,
|
||||
`comp` is also a single byte expressed as two hexadecimal digits,
|
||||
and `addr` is a memory address in the emulated console,
|
||||
expressed as however many hexadecimal digits are required
|
||||
for the console in question
|
||||
(typically 4 for 8-bit CPUs,
|
||||
6 for 16-bit CPUs,
|
||||
and 8 for 32-bit CPUs).
|
||||
|
||||
For compatibility with older versions of higan,
|
||||
the older syntaxes of `addr/data` and `addr/comp/data`
|
||||
are still supported.
|
||||
|
||||
For cheats that require more than a single-byte change,
|
||||
higan allows multiple codes to be combined with `+`
|
||||
so that all of them can have a single description
|
||||
and be toggled with a single click.
|
||||
For example,
|
||||
in Super Mario World,
|
||||
you can lock the time to 999 with these codes:
|
||||
`7e0f31=09+7e0f32=09+7e0f33=09`.
|
||||
|
||||
Changes made in the Cheat Editor are saved to disk
|
||||
when the game is unloaded,
|
||||
or when higan exits.
|
||||
higan stores the known cheats for a particular game
|
||||
in `higan/cheats.bml`
|
||||
inside the corresponding game folder
|
||||
in [the Game Library](#the-game-library).
|
||||
|
||||
If your copy of higan includes a cheat database
|
||||
(a file named `cheats.bml`
|
||||
in the same directory as `Super Famicom.sys`
|
||||
and the other `*.sys` directories),
|
||||
you can click the "Find Codes ..." button in the bottom left
|
||||
to load all known cheats for the currently-running game.
|
||||
|
||||
To add a new cheat,
|
||||
select an unused row in the list,
|
||||
then type the relevant codes in the "Code(s)" field at the bottom,
|
||||
and a description in the "Description" field.
|
||||
|
||||
To enable or disable an existing cheat,
|
||||
tick the checkbox in the first column of the list.
|
||||
The code should take effect immediately.
|
||||
|
||||
To clear out an existing cheat,
|
||||
select it from the list
|
||||
and click the "Erase" button in the bottom right,
|
||||
or just manually delete
|
||||
the contents of the "Code(s)" and "Description" fields.
|
||||
|
||||
To clear out all existing cheats,
|
||||
click the "Reset" button in the bottom right.
|
||||
|
||||
The State Manager
|
||||
-----------------
|
||||
|
||||
The State Manager allows you to create,
|
||||
load,
|
||||
and remove Manager states.
|
||||
For more information on Manager states,
|
||||
quick states,
|
||||
saved games
|
||||
and how they compare,
|
||||
see [Save States](#save-states).
|
||||
|
||||
To create a new manager state,
|
||||
or to replace an existing one,
|
||||
select the slot in the list
|
||||
then click "Save" in the bottom-left corner.
|
||||
You can then type a description in the "Description" field,
|
||||
to help you find the state again later.
|
||||
|
||||
To rename a state,
|
||||
select the slot in the list
|
||||
and edit the "Description" field.
|
||||
|
||||
To load a state,
|
||||
select the slot in the list
|
||||
and click "Load" in the bottom-left corner,
|
||||
or just double-click it.
|
||||
|
||||
To clear the state out of a slot,
|
||||
select the slot in the list
|
||||
and click "Erase" in the bottom-right corner.
|
||||
|
||||
To clear all the slots at once,
|
||||
click "Reset" in the bottom-right corner.
|
||||
|
||||
The Manifest Viewer
|
||||
-------------------
|
||||
|
||||
As mentioned in
|
||||
[Why game folders?](#why-game-folders),
|
||||
a game cartridge contains
|
||||
more than just the raw data of the game.
|
||||
|
||||
higan uses a "manifest" to
|
||||
describe how the various parts of a game cartridge
|
||||
are wired up together,
|
||||
and the Manifest Viewer lets you examine
|
||||
the configuration higan is using for the currently-running game.
|
||||
|
||||
For some games,
|
||||
an actual cartridge has been taken apart and carefully examined
|
||||
and its configuration has been recorded in icarus' database,
|
||||
so the manifest icarus produces
|
||||
is guaranteed accurate.
|
||||
For games that do not exist in icarus' database,
|
||||
icarus will make a reasonable guess.
|
||||
This is enough to get the game running,
|
||||
but does not necessarily reflect the original cartridge.
|
||||
@@ -1,290 +0,0 @@
|
||||
When you launch higan,
|
||||
the main window appears,
|
||||
with a menu-bar across the top,
|
||||
a status-bar across the bottom,
|
||||
and a large area in the middle where the game's video output appears.
|
||||
|
||||
The Library menu
|
||||
----------------
|
||||
|
||||
The Library menu allows you
|
||||
to import games into higan's game library,
|
||||
and to load games from the library.
|
||||
higan organises the games in your library
|
||||
according to which console they were intended to run on.
|
||||
|
||||
To play a game for a particular console from your library,
|
||||
click on the Library menu,
|
||||
click on the console manufacturer submenu
|
||||
(Nintendo for the Super Famicom,
|
||||
Bandai for the WonderSwan,
|
||||
etc.)
|
||||
then click on the console menu item.
|
||||
A window will appear listing all the games in your library
|
||||
for that particular console.
|
||||
Select the game you want to play
|
||||
and click the Open button,
|
||||
or just double-click the game,
|
||||
and it will begin playing as though you'd just turned on the console.
|
||||
|
||||
To add a new game to your library,
|
||||
choose "Load ROM File ..." from the Library menu.
|
||||
A [filesystem browser](#the-filesystem-browser) will appear,
|
||||
allowing you to pick any ROM image for any supported system,
|
||||
with any of the most common file extensions.
|
||||
It also allows loading ROM images from `.zip` archives,
|
||||
if the archive contains a single ROM image.
|
||||
|
||||
**Note:** Some games require extra steps to import correctly;
|
||||
see [the Game Library](#the-game-library) for details.
|
||||
|
||||
TODO: Mention the region-picker.
|
||||
|
||||
To add many games at once,
|
||||
run icarus,
|
||||
or choose "Import ROM Files ..." from the Library menu
|
||||
(which just runs icarus anyway).
|
||||
See [the icarus interface](#the-icarus-interface)
|
||||
for more information about bulk-importing.
|
||||
|
||||
For more information about the higan game library,
|
||||
see [The Game Library](#the-game-library) below.
|
||||
|
||||
The console menu
|
||||
---------------
|
||||
|
||||
**Note:**
|
||||
The console menu does not appear
|
||||
until a game is loaded.
|
||||
Also,
|
||||
it's not named "console",
|
||||
it's named for the kind of console
|
||||
the loaded game runs on.
|
||||
For example,
|
||||
when playing a Game Boy game,
|
||||
you will have a "Game Boy" menu.
|
||||
|
||||
The console menu contains commands relevant
|
||||
to the particular console being emulated.
|
||||
All consoles will have some of the following items,
|
||||
but few consoles have all of them.
|
||||
|
||||
- **Controller Port 1**
|
||||
allows you
|
||||
to connect different emulated controllers
|
||||
to the first controller port,
|
||||
if there is one.
|
||||
- See [the Configuration dialog](#the-configuration-dialog)
|
||||
for information about configuring
|
||||
which host controller inputs are used
|
||||
for the emulated controllers.
|
||||
- This menu appears for the Famicom,
|
||||
even though the Famicom did not support alternate controllers,
|
||||
because the Famicom emulation core also emulates the NES,
|
||||
which did.
|
||||
- **Controller Port 2**
|
||||
allows you
|
||||
to connect different emulated controllers
|
||||
to the second controller port,
|
||||
if there is one.
|
||||
- See [the Configuration dialog](#the-configuration-dialog)
|
||||
for information about configuring
|
||||
which host controller inputs are used
|
||||
for the emulated controllers.
|
||||
- This menu appears for the Famicom,
|
||||
even though the Famicom did not support alternate controllers,
|
||||
because the Famicom emulation core also emulates the NES,
|
||||
which did.
|
||||
- **Expansion Port**
|
||||
allows you
|
||||
to connect different emulated devices
|
||||
to the console's expansion port,
|
||||
if there is one.
|
||||
- For the Super Famicom,
|
||||
the [21fx][21fx] is a homebrew device
|
||||
that allows a program running on a PC
|
||||
to control a physical Super Famicom (or SNES).
|
||||
This option allows the same program
|
||||
to control the emulated SNES,
|
||||
for development or testing.
|
||||
- **Power Cycle**
|
||||
restarts the loaded game
|
||||
as though the emulated console were switched off and on again.
|
||||
- **Unload**
|
||||
stops the current game,
|
||||
as though the emulated console were switched off.
|
||||
You can load the same or a different game
|
||||
from [the Library menu](#the-library-menu).
|
||||
|
||||
[21fx]: https://github.com/defparam/21FX
|
||||
|
||||
The Settings menu
|
||||
-----------------
|
||||
|
||||
The Settings menu allows you to configure things
|
||||
that aren't specific to any particular console.
|
||||
|
||||
- **Video Scale** determines the size and shape
|
||||
of the emulated console's video output
|
||||
in windowed mode
|
||||
(as opposed to fullscreen).
|
||||
- **Video Emulation** applies various effects
|
||||
to the emulated console's video output
|
||||
to reproduce some behaviours
|
||||
that aren't technically part of the console itself.
|
||||
- "Blurring" simulates the limited horizontal resolution
|
||||
of standard-definition TVs
|
||||
by blurring together horizontally-adjacent pixels.
|
||||
Games like Jurassic Park for the Super Famicom
|
||||
depend on this to emulate a transparency effect.
|
||||
For hand-held consoles like the Game Boy Advance,
|
||||
this simulates the slow response time
|
||||
of the cheap LCD screens these consoles used
|
||||
by blurring each output frame with the previous one.
|
||||
- "Colors" simulates the way a console's display device
|
||||
differs from modern computer monitor's colour reproduction.
|
||||
In particular,
|
||||
it simulates the slightly-different gamma correction
|
||||
used by the Super Famicom,
|
||||
the dim, washed out colours of the original Game Boy Advance,
|
||||
and the pea-green display of the original Game Boy.
|
||||
- **Video Shader** controls
|
||||
how the low-resolution video output of the emulated console
|
||||
is scaled up to suit modern high-resolution displays.
|
||||
The availability of items in this submenu depends on
|
||||
which video driver higan is using,
|
||||
so see [Drivers](#drivers) for more information.
|
||||
- "None" draws each output pixel according to
|
||||
the colour of the single nearest input pixel,
|
||||
sometimes called "nearest neighbour" scaling.
|
||||
This produces unnaturally crisp and blocky images.
|
||||
- "Blur" draws each output pixel by
|
||||
averaging the colours of the four nearest input pixels,
|
||||
sometimes called "bilinear" scaling.
|
||||
This produces unnaturally blurry images.
|
||||
- When using the OpenGL [driver](#drivers),
|
||||
an additional item appears in this menu for
|
||||
each installed Quark shader.
|
||||
See [Installing custom shaders](#installing-custom-shaders)
|
||||
for details.
|
||||
- **Synchronize Audio**
|
||||
causes higan to wait for audio playback to complete
|
||||
before resuming emulation.
|
||||
This should reduce popping and glitching noises,
|
||||
and slows the emulation down to approximately the correct speed.
|
||||
If your PC cannot emulate at full-speed,
|
||||
(60fps for most consoles, 75fps for WonderSwan)
|
||||
this has no noticable effect.
|
||||
- **Mute Audio**
|
||||
causes higan to not output sound from the emulated console.
|
||||
The sound hardware is still emulated.
|
||||
- **Show Status Bar**
|
||||
causes higan to show or hide the status bar
|
||||
at the bottom of the window.
|
||||
This option has no effect in full-screen mode.
|
||||
See [The status bar](#the-status-bar) for more information.
|
||||
- **Video ...**
|
||||
opens the Video tab of [the Configuration dialog][cfgdlg].
|
||||
- **Audio ...**
|
||||
opens the Audio tab of [the Configuration dialog][cfgdlg].
|
||||
- **Input ...**
|
||||
opens the Input tab of [the Configuration dialog][cfgdlg].
|
||||
- **Hotkey ...**
|
||||
opens the Hotkeys tab of [the Configuration dialog][cfgdlg].
|
||||
- **Advanced ...**
|
||||
opens the Advanced tab of [the Configuration dialog][cfgdlg].
|
||||
|
||||
[svsa]: #why-do-synchronize-video-and-synchronize-audio-conflict
|
||||
[cfgdlg]: #the-configuration-dialog
|
||||
|
||||
The Tools menu
|
||||
--------------
|
||||
|
||||
The Tools menu
|
||||
contains features for manipulating the emulated console.
|
||||
|
||||
- **Save Quick State**
|
||||
stores the current state of the emulated console
|
||||
into one of the quick state slots.
|
||||
See [Save States](#save-states) for more information.
|
||||
- **Load Quick State**
|
||||
restores the emulated console to
|
||||
a state previously saved to one of the quick state slots.
|
||||
See [Save States](#save-states) for more information.
|
||||
- **Cheat Editor**
|
||||
opens [the Cheat Editor window](#the-cheat-editor)
|
||||
- **State Manager**
|
||||
opens [the State Manager window](#the-state-manager)
|
||||
- **Manifest Viewer**
|
||||
opens [the Manifest Viewer window](#the-manifest-viewer)
|
||||
|
||||
The Help menu
|
||||
-------------
|
||||
|
||||
The Help menu contains information about higan itself.
|
||||
|
||||
- **Documentation**
|
||||
loads the official higan documentation
|
||||
in your web-browser.
|
||||
- **About**
|
||||
opens the About dialog,
|
||||
which displays basic information about higan,
|
||||
including the version number.
|
||||
|
||||
The status bar
|
||||
--------------
|
||||
|
||||
The status bar appears
|
||||
at the bottom of the main higan window,
|
||||
while "Show Status Bar" is ticked in [the Settings menu](#the-settings-menu).
|
||||
|
||||
Before any game is loaded,
|
||||
the status bar displays "No cartridge loaded".
|
||||
|
||||
When a game is loaded and running,
|
||||
the status bar displays the current emulation speeed
|
||||
in frames-per-second.
|
||||
For PAL-based consoles,
|
||||
this should be around 50 FPS for "full speed" emulation,
|
||||
for NTSC and most portable consoles the ideal speed is 60 FPS,
|
||||
but the WonderSwan runs at 75 FPS.
|
||||
If the number is too low,
|
||||
you may need a faster computer,
|
||||
or a faster [video driver](#drivers).
|
||||
If the number is too high,
|
||||
you may need to [Synchronize Audio](#the-settings-menu),
|
||||
or you may have pressed the "turbo" [hotkey](#the-configuration-dialog).
|
||||
|
||||
The status bar displays "Paused"
|
||||
if you have pressed the "pause" [hotkey](#the-configuration-dialog),
|
||||
or if "When focus is lost: Pause Emulation" is ticked
|
||||
in [the Input tab of the Configuration dialog](#the-configuration-dialog)
|
||||
and the main higan window is not the foreground window.
|
||||
To resume emulation,
|
||||
make sure the main higan window is in the foreground,
|
||||
and/or press the "pause" hotkey.
|
||||
|
||||
The status bar briefly displays "Selected quick state slot X"
|
||||
(where X is one of the Quick State slot numbers)
|
||||
when you press the "Increment Quick State"
|
||||
or "Decrement Quick State"
|
||||
hotkeys,
|
||||
to show which Quick State slot will be used
|
||||
the next time you press the "Save Quick State"
|
||||
or "Load Quick State" hotkeys.
|
||||
|
||||
The status bar briefly displays "Slot X quick state does not exist"
|
||||
(where X is one of the Quick State slot numbers)
|
||||
when you choose a slot from the
|
||||
[Tools](#the-tools-menu) → "Load Quick State"
|
||||
sub-menu that has not had a save-state saved to it,
|
||||
or when you press the "Load Quick State" hotkey
|
||||
while the current Quick State slot has not had a save-state saved to it,
|
||||
|
||||
The status bar briefly displays "Power cycled"
|
||||
when you choose "Power Cycle" from [the console menu](#the-console menu),
|
||||
or press the "Power Cycle" hotkey.
|
||||
|
||||
The status bar briefly displays "Display rotation not supported"
|
||||
when you press the "Rotate Display" hotkey
|
||||
while the emulated console does not support display rotation.
|
||||
@@ -1,60 +0,0 @@
|
||||
When launching icarus,
|
||||
directly or by picking "Import ROM Files ..."
|
||||
from higan's [Library menu](#the-library-menu),
|
||||
the main icarus window appears.
|
||||
This is [a filesystem browser](#the-filesystem-browser),
|
||||
with customisations:
|
||||
|
||||
- The filesystem browser only lists
|
||||
files with extensions typically used for ROM dumps from
|
||||
consoles higan emulates,
|
||||
plus `.zip` files since ROM dumps are often compressed.
|
||||
- Each matching file has a check-box next to it.
|
||||
- You can tick the check-box next to every file at once
|
||||
by pressing "Select All" in the bottom-left.
|
||||
- You can un-tick all the check-boxes
|
||||
by pressing "Unselect All" in the bottom-left.
|
||||
|
||||
Pressing "Settings ..." in the bottom-right
|
||||
opens [the icarus Settings dialog](#the-icarus-settings-dialog).
|
||||
|
||||
Pressing "Import ..." in the bottom-right
|
||||
will close the filesystem browser
|
||||
then try to import all the files
|
||||
whose check-boxes are ticked
|
||||
into [the Game Library](#the-game-library).
|
||||
icarus displays a progress dialog during the import process.
|
||||
|
||||
**Note:** Some games require extra steps to import correctly;
|
||||
see [the Game Library](#the-game-library) for details.
|
||||
|
||||
The icarus Settings dialog
|
||||
--------------------------
|
||||
|
||||
The icarus Settings dialog contains the following settings:
|
||||
|
||||
- **Library Location** determines
|
||||
where icarus puts the games it imports.
|
||||
See [Moving the Game Library](#moving-the-game-library)
|
||||
for details.
|
||||
- **Create Manifests** causes icarus
|
||||
to write out a manifest file describing
|
||||
each imported game
|
||||
to that game's [game folder](#whats-in-a-game-folder).
|
||||
This means that higan doesn't have to regenerate
|
||||
the manifest each time an imported game is loaded,
|
||||
but it means that a future version of higan
|
||||
with an incompatible manifest format
|
||||
may be unable to play these games.
|
||||
Note that higan also has an "Ignore Manifests" option
|
||||
in the Advanced tab of
|
||||
[its Configuration dialog](#the-configuration-dialog).
|
||||
- **Use Database** causes icarus to use manifest information
|
||||
from its database of known-good manifests,
|
||||
if it's importing a game it recognises.
|
||||
For unrecognised games,
|
||||
and for all games if this box is unticked,
|
||||
icarus gueses the manifest data.
|
||||
This option is still relevant when "Create Manifests" is unticked:
|
||||
higan uses icarus to generate a manifest when a game is loaded,
|
||||
not just at import-time.
|
||||
-311
@@ -1,311 +0,0 @@
|
||||
The consoles that higan emulates
|
||||
are similar in many ways,
|
||||
but some of them do have particular quirks
|
||||
that you should be aware of.
|
||||
|
||||
Video Shaders and TV-based consoles
|
||||
-----------------------------------
|
||||
|
||||
[Video Shaders](guides/shaders.md)
|
||||
customize how higan scales
|
||||
the low-resolution video of the emulated console
|
||||
up to the high-resolution of the computer display.
|
||||
Simple shaders
|
||||
(like "None"
|
||||
and the third-party "AANN" shader)
|
||||
just blindly scale up the images they're given,
|
||||
but sophisticated shaders
|
||||
(such as the third-party "xBR" shader)
|
||||
try to produce higher-quality output
|
||||
by recognising particular patterns of pixel,
|
||||
like taking three diagonal pixels
|
||||
and turning that into a smooth diagonal line.
|
||||
|
||||
These shaders assume that
|
||||
each pixel drawn by the game's artists
|
||||
becomes a single pixel in the video output they analyze.
|
||||
Many of the consoles higan emulates
|
||||
can only output video at one specific resolution,
|
||||
so this "one pixel equals one pixel" rule holds true,
|
||||
and pattern-based shaders like "xBR" work just fine.
|
||||
|
||||
Unfortunately,
|
||||
this is not the case for the Super Famicom.
|
||||
The "normal" video mode
|
||||
draws 256 pixels across the width of the screen,
|
||||
but the "high resolution" mode draws 512.
|
||||
Since Super Famicom games can enable hi-res mode at any time
|
||||
(even halfway through a frame),
|
||||
higan always renders Super Famicom video output 512 pixels wide,
|
||||
just in case.
|
||||
This means that in "normal" mode,
|
||||
each pixel drawn by the game's artists
|
||||
becomes two pixels in the video output,
|
||||
breaking the assumption
|
||||
that pattern-based shaders are based on.
|
||||
|
||||
The Super Famicom has a similar issue in the vertical direction:
|
||||
normally,
|
||||
an NTSC-based Super Famicom draws about 240 rows of output every frame,
|
||||
sometimes referred to as "240p" video.
|
||||
When a game turns on "interlaced" mode,
|
||||
it draws the 240 odd-numbered lines of one frame,
|
||||
then the 240 even-numbered lines of the next,
|
||||
and so forth.
|
||||
This is sometimes referred to as "480i" video.
|
||||
Although interlaced mode cannot be enabled mid-frame
|
||||
like high-resolution mode,
|
||||
resolution switching is still complex,
|
||||
so higan always draws all 480 lines of video output.
|
||||
This means for a normal, non-interlaced game,
|
||||
each pixel drawn by the game's artists
|
||||
becomes four pixels in the video output
|
||||
(two horizontally and two vertically)
|
||||
making pattern-based shaders even less useful.
|
||||
It also breaks most scanline-emulation shaders,
|
||||
since they typically draw a scanline
|
||||
for each row of pixels in the video output.
|
||||
|
||||
The Mega Drive has similar problems
|
||||
to the Super Famicom.
|
||||
It has the same behaviour with interlacing,
|
||||
but its high-resolution mode switches
|
||||
from 256 pixels across to 320 pixels across.
|
||||
Therefore in normal mode,
|
||||
each pixel drawn by the game's artists
|
||||
becomes five pixels in the video output,
|
||||
while in high-resolution mode,
|
||||
each pixel drawn by the game's artists
|
||||
becomes four pixels in the video output
|
||||
(or 10 and 8 pixels in non-interlaced mode).
|
||||
|
||||
The PC Engine does not support an interlaced mode,
|
||||
but its horizontal resolution is much more flexible
|
||||
than the Super Famicom or Mega Drive,
|
||||
and so it has the same problems with shaders.
|
||||
|
||||
Music and Sound Effect Volume on the Mega Drive
|
||||
-----------------------------------------------
|
||||
|
||||
The Mega Drive has two different audio-generating chips:
|
||||
|
||||
- the SN76489 or "PSG" chip,
|
||||
inherited from the Master System,
|
||||
mostly used for sound-effects
|
||||
like Sonic picking up rings
|
||||
- the YM2612 or "FM" chip,
|
||||
mostly used for music
|
||||
|
||||
With two different sound sources,
|
||||
it's important that they have similar volumes,
|
||||
or the sound-effects will drown out the music,
|
||||
or vice-versa.
|
||||
Sega did *not* do this,
|
||||
and different hardware revisions
|
||||
used different relative volumes.
|
||||
|
||||
higan currently
|
||||
sets the PSG volume to [125% of the FM volume][vol],
|
||||
based on [a Sega Genesis model 1 VA6][va6] that byuu owns.
|
||||
If you feel sound-effects in higan's Mega Drive core
|
||||
are too loud or too quiet,
|
||||
you may be comparing it
|
||||
to a Mega Drive calibrated to a different scale
|
||||
(or to an emulator tweaked to match such a Mega Drive).
|
||||
|
||||
[vol]: https://board.byuu.org/viewtopic.php?p=42482#p42482
|
||||
[va6]: https://board.byuu.org/viewtopic.php?p=42195#p42195
|
||||
|
||||
Playing Game Boy Colour games in Game Boy mode
|
||||
----------------------------------------------
|
||||
|
||||
Games for the original Game Boy
|
||||
came in solid grey cartridges,
|
||||
and only supported four-shade greyscale graphics.
|
||||
ROM files for these games
|
||||
typically have filenames ending in `.gb`.
|
||||
|
||||
The Game Boy Color played all the original Game Boy games,
|
||||
but extended the hardware to support colour graphics.
|
||||
Games that required
|
||||
the extra hardware in the Game Boy Color
|
||||
came in transparent cartridges,
|
||||
and had a slightly different shape
|
||||
to prevent them from being used in original Game Boys..
|
||||
ROM files for these games
|
||||
typically have filenames ending in `.gbc`.
|
||||
|
||||
However,
|
||||
there were also some games
|
||||
that could use colour if it was available,
|
||||
but would stick to greyscale if it wasn't.
|
||||
These games came in black cartridges.
|
||||
ROM files for these games
|
||||
typically have filenames ending in `.gbc`
|
||||
(since they are genuinely designed for the Game Boy Color)
|
||||
or `.gbb`.
|
||||
|
||||
Sometimes people ask
|
||||
for higan to include these backwards-compatible Game Boy Color games
|
||||
when asking for a Game Boy game to load.
|
||||
However,
|
||||
this would make higan much more complex
|
||||
for not much benefit:
|
||||
it's just the same game as in Color mode,
|
||||
but with bits chopped off.
|
||||
You might as well play backward-compatible games
|
||||
in Game Boy Color mode
|
||||
and get the full experience the developers intended.
|
||||
|
||||
If you really, really want to see
|
||||
what a particular game's backward-compatible mode looked like,
|
||||
change the filename to end with `.gb`
|
||||
(instead of `.gbc` or `.gbb`)
|
||||
before [importing it](guides/import.md).
|
||||
If you want to experiment
|
||||
with loading in-game saves from colour-mode in monochrome mode
|
||||
or vice-versa,
|
||||
you can import the game once with `.gb`
|
||||
and once with `.gbc`,
|
||||
then manually copy files between the
|
||||
[game folders](concepts/game-folders.md)
|
||||
in the "Game Boy" and "Game Boy Color" sub-folders
|
||||
of the [Game Library](concepts/game-library.md) folder.
|
||||
|
||||
Do not expect save-states to be compatible between
|
||||
Game Boy and Game Boy Color.
|
||||
|
||||
In-Game Saves and the Game Boy Advance
|
||||
--------------------------------------
|
||||
|
||||
For most of the consoles that higan emulates,
|
||||
in-game saves are simple:
|
||||
the cartridge contains some battery-backed RAM
|
||||
that the game accesses like any other memory,
|
||||
and the game's internal header usually contains some hint
|
||||
about where in memory the save data appears
|
||||
and how large it is.
|
||||
|
||||
The Game Boy Advance is different.
|
||||
By the time of the GBA,
|
||||
many save-storage technologies were available,
|
||||
most with a more complex interface than plain memory.
|
||||
Frustratingly, the GBA's internal header
|
||||
does *not* describe which storage variant the game expects.
|
||||
Therefore,
|
||||
when importing a GBA game,
|
||||
higan must guess which storage type to use
|
||||
and sometimes it guesses incorrectly.
|
||||
|
||||
If higan guesses incorrectly for a game you want to play,
|
||||
you will need to turn on
|
||||
"Create manifests" in
|
||||
[the Icarus settings dialog](interface/icarus.md#the-icarus-settings-dialog),
|
||||
turn off
|
||||
"Ignore manifests" in
|
||||
[higan's Advanced settings](interface/higan-config.md#advanced),
|
||||
re-import the game,
|
||||
and edit `manifest.bml` in
|
||||
[the game folder](concepts/game-folders.md)
|
||||
to describe the correct storage type.
|
||||
Try importing other GBA games to see what save types they use.
|
||||
|
||||
For more discussion of the GBA save type mess,
|
||||
see [What's the deal with... GBA save files?][gbasaves]
|
||||
|
||||
[gbasaves]: http://zork.net/~st/jottings/GBA_saves.html
|
||||
|
||||
Rumble compatibility for Game Boy (Color)
|
||||
-----------------------------------------
|
||||
|
||||
The Game Boy and Game Boy Color did not natively support
|
||||
any kind of rumble or force-feedback system,
|
||||
but some game cartridges (such as Pokémon Pinball)
|
||||
included a rumble motor within the cartridge itself.
|
||||
|
||||
Because higan does not currently support
|
||||
game-specific controller features,
|
||||
to experience the rumble effect in higan
|
||||
you'll need to configure the console itself:
|
||||
|
||||
- Open
|
||||
[higan's Input settings](interface/higan-config.md#input)
|
||||
- In the list of consoles,
|
||||
select Game Boy, or Game Boy Color
|
||||
depending on which console you want to use to play the game
|
||||
- In the list of inputs,
|
||||
double-click "Rumble"
|
||||
or select it and press Enter
|
||||
- Press any button on the gamepad that should shake
|
||||
when the game turns on the rumble effect.
|
||||
|
||||
Rumble compatibility for Game Boy Advance
|
||||
-----------------------------------------
|
||||
|
||||
The original Game Boy Advance
|
||||
and the Game Boy Advance SP
|
||||
did not support any kind of rumble or force-feedback system,
|
||||
but the Game Boy Player addon for the Gamecube
|
||||
allowed Game Boy Advance games
|
||||
to use the rumble feature in Gamecube controllers.
|
||||
|
||||
Because rumble is a feature of the Game Boy Player,
|
||||
to experience the rumble effect in higan
|
||||
you'll need to configure the console itself:
|
||||
|
||||
- Open
|
||||
[higan's Input settings](interface/higan-config.md#input)
|
||||
- In the list of consoles,
|
||||
select Game Boy Advance
|
||||
- In the list of inputs,
|
||||
double-click "Rumble"
|
||||
or select it and press Enter
|
||||
- Press any button on the gamepad that should shake
|
||||
when the game turns on the rumble effect.
|
||||
|
||||
As well as the Game Boy Player rumble feature,
|
||||
some Game Boy Advance cartridges
|
||||
included a rumble motor within the cartridge itself.
|
||||
higan does not support this rumble technology,
|
||||
but that's not a big deal:
|
||||
the only two such games are *Drill Dozer*,
|
||||
which can use Game Boy Player rumble,
|
||||
and *WarioWare: Twisted*,
|
||||
which doesn't work anyway
|
||||
because it requires gyroscope hardware
|
||||
that higan does not yet emulate.
|
||||
|
||||
Game Boy Advance rotation
|
||||
-------------------------
|
||||
|
||||
Some Game Boy Advance homebrew games,
|
||||
as well as a bonus mode in *Dr Mario + Puzzle League*,
|
||||
expect the player to physically rotate the device
|
||||
so the screen is tall rather than wide.
|
||||
higan supports this feature with
|
||||
a Rotate [hotkey](interface/higan-config.md#hotkeys).
|
||||
|
||||
When the user presses the Rotate hotkey,
|
||||
the console's video output is rotated 90° anti-clockwise,
|
||||
and the directional-pad controls are also rotated
|
||||
so that (for example) pushing the button for "up"
|
||||
sends the signal "right" to the emulated console,
|
||||
so that the player character moves "up" on the rotated screen.
|
||||
|
||||
WonderSwan rotation
|
||||
-------------------
|
||||
|
||||
The WonderSwan hardware
|
||||
included multiple sets of buttons
|
||||
so the player could hold the device
|
||||
vertically or horizontally.
|
||||
*Makaimura for WonderSwan* includes a level
|
||||
that requires the player to repeatedly rotate
|
||||
the device as they play.
|
||||
higan supports this feature with
|
||||
a Rotate [hotkey](interface/higan-config.md#hotkeys).
|
||||
|
||||
When the user presses the Rotate hotkey,
|
||||
the console's video output is rotated 90° anti-clockwise,
|
||||
and the X and Y button cluster mappings
|
||||
are adjusted to match.
|
||||
@@ -1,7 +0,0 @@
|
||||
TODO
|
||||
|
||||
- install
|
||||
- configure inputs
|
||||
- load a game
|
||||
- connect a controller
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
build := release
|
||||
include ../nall/GNUmakefile
|
||||
|
||||
target := tomoko
|
||||
objects := libco emulator audio video resource
|
||||
# console := true
|
||||
|
||||
flags += -I. -I..
|
||||
|
||||
ifeq ($(platform),windows)
|
||||
ifeq ($(console),true)
|
||||
link += -mconsole
|
||||
else
|
||||
link += -mwindows
|
||||
endif
|
||||
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)
|
||||
flags += -march=native
|
||||
else ifneq ($(filter $(platform),linux bsd),)
|
||||
flags += -march=native -fopenmp
|
||||
link += -fopenmp
|
||||
link += -Wl,-export-dynamic
|
||||
link += -lX11 -lXext
|
||||
else
|
||||
$(error "unsupported platform")
|
||||
endif
|
||||
|
||||
compile = \
|
||||
$(strip \
|
||||
$(if $(filter %.c,$<), \
|
||||
$(compiler) $(cflags) $(flags) $1 -c $< -o $@, \
|
||||
$(if $(filter %.cpp,$<), \
|
||||
$(compiler) $(cppflags) $(flags) $1 -c $< -o $@ \
|
||||
) \
|
||||
) \
|
||||
)
|
||||
|
||||
%.o: $<; $(call compile)
|
||||
|
||||
all: build;
|
||||
|
||||
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
|
||||
|
||||
clean:
|
||||
-@$(call delete,out/*)
|
||||
-@$(call delete,obj/*)
|
||||
@@ -1,99 +0,0 @@
|
||||
#include <emulator/emulator.hpp>
|
||||
|
||||
namespace Emulator {
|
||||
|
||||
#include "stream.cpp"
|
||||
Audio audio;
|
||||
|
||||
auto Audio::reset(maybe<uint> channels_, maybe<double> frequency_) -> void {
|
||||
interface = nullptr;
|
||||
|
||||
if(channels_) channels = channels_();
|
||||
if(frequency_) frequency = frequency_();
|
||||
|
||||
streams.reset();
|
||||
reverb.reset();
|
||||
|
||||
reverb.resize(channels);
|
||||
for(auto c : range(channels)) {
|
||||
reverb[c].resize(7);
|
||||
reverb[c][0].resize(1229);
|
||||
reverb[c][1].resize(1559);
|
||||
reverb[c][2].resize(1907);
|
||||
reverb[c][3].resize(4057);
|
||||
reverb[c][4].resize(8117);
|
||||
reverb[c][5].resize(8311);
|
||||
reverb[c][6].resize(9931);
|
||||
}
|
||||
}
|
||||
|
||||
auto Audio::setInterface(Interface* interface) -> void {
|
||||
this->interface = interface;
|
||||
}
|
||||
|
||||
auto Audio::setFrequency(double frequency) -> void {
|
||||
this->frequency = frequency;
|
||||
for(auto& stream : streams) {
|
||||
stream->setFrequency(stream->inputFrequency, frequency);
|
||||
}
|
||||
}
|
||||
|
||||
auto Audio::setVolume(double volume) -> void {
|
||||
this->volume = volume;
|
||||
}
|
||||
|
||||
auto Audio::setBalance(double balance) -> void {
|
||||
this->balance = balance;
|
||||
}
|
||||
|
||||
auto Audio::setReverb(bool enabled) -> void {
|
||||
this->reverbEnable = enabled;
|
||||
}
|
||||
|
||||
auto Audio::createStream(uint channels, double frequency) -> shared_pointer<Stream> {
|
||||
shared_pointer<Stream> stream = new Stream;
|
||||
stream->reset(channels, frequency, this->frequency);
|
||||
streams.append(stream);
|
||||
return stream;
|
||||
}
|
||||
|
||||
auto Audio::process() -> void {
|
||||
while(true) {
|
||||
for(auto& stream : streams) {
|
||||
if(!stream->pending()) return;
|
||||
}
|
||||
|
||||
double samples[channels];
|
||||
for(auto& sample : samples) sample = 0.0;
|
||||
|
||||
for(auto& stream : streams) {
|
||||
double buffer[16];
|
||||
uint length = stream->read(buffer), offset = 0;
|
||||
|
||||
for(auto& sample : samples) {
|
||||
sample += buffer[offset];
|
||||
if(++offset >= length) offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for(auto c : range(channels)) {
|
||||
samples[c] = max(-1.0, min(+1.0, samples[c] * volume));
|
||||
|
||||
if(reverbEnable) {
|
||||
samples[c] *= 0.125;
|
||||
for(auto n : range(7)) samples[c] += 0.125 * reverb[c][n].last();
|
||||
for(auto n : range(7)) reverb[c][n].write(samples[c]);
|
||||
samples[c] *= 8.000;
|
||||
}
|
||||
}
|
||||
|
||||
if(channels == 2) {
|
||||
if(balance < 0.0) samples[1] *= 1.0 + balance;
|
||||
if(balance > 0.0) samples[0] *= 1.0 - balance;
|
||||
}
|
||||
|
||||
platform->audioSample(samples, channels);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nall/dsp/iir/one-pole.hpp>
|
||||
#include <nall/dsp/iir/biquad.hpp>
|
||||
#include <nall/dsp/resampler/cubic.hpp>
|
||||
|
||||
namespace Emulator {
|
||||
|
||||
struct Interface;
|
||||
struct Audio;
|
||||
struct Filter;
|
||||
struct Stream;
|
||||
|
||||
struct Audio {
|
||||
auto reset(maybe<uint> channels = nothing, maybe<double> frequency = nothing) -> void;
|
||||
auto setInterface(Interface* interface) -> void;
|
||||
|
||||
auto setFrequency(double frequency) -> void;
|
||||
auto setVolume(double volume) -> void;
|
||||
auto setBalance(double balance) -> void;
|
||||
auto setReverb(bool enabled) -> void;
|
||||
|
||||
auto createStream(uint channels, double frequency) -> shared_pointer<Stream>;
|
||||
|
||||
private:
|
||||
auto process() -> void;
|
||||
|
||||
Interface* interface = nullptr;
|
||||
vector<shared_pointer<Stream>> streams;
|
||||
|
||||
uint channels = 0;
|
||||
double frequency = 0.0;
|
||||
|
||||
double volume = 1.0;
|
||||
double balance = 0.0;
|
||||
|
||||
bool reverbEnable = false;
|
||||
vector<vector<queue<double>>> reverb;
|
||||
|
||||
friend class Stream;
|
||||
};
|
||||
|
||||
struct Filter {
|
||||
enum class Order : uint { First, Second };
|
||||
enum class Type : uint { LowPass, HighPass };
|
||||
|
||||
Order order;
|
||||
DSP::IIR::OnePole onePole; //first-order
|
||||
DSP::IIR::Biquad biquad; //second-order
|
||||
};
|
||||
|
||||
struct Stream {
|
||||
auto reset(uint channels, double inputFrequency, double outputFrequency) -> void;
|
||||
|
||||
auto setFrequency(double inputFrequency, maybe<double> outputFrequency = nothing) -> void;
|
||||
|
||||
auto addFilter(Filter::Order order, Filter::Type type, double cutoffFrequency, uint passes = 1) -> void;
|
||||
|
||||
auto pending() const -> bool;
|
||||
auto read(double samples[]) -> uint;
|
||||
auto write(const double samples[]) -> void;
|
||||
|
||||
template<typename... P> auto sample(P&&... p) -> void {
|
||||
double samples[sizeof...(P)] = {forward<P>(p)...};
|
||||
write(samples);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Channel {
|
||||
vector<Filter> filters;
|
||||
DSP::Resampler::Cubic resampler;
|
||||
};
|
||||
vector<Channel> channels;
|
||||
double inputFrequency;
|
||||
double outputFrequency;
|
||||
|
||||
friend class Audio;
|
||||
};
|
||||
|
||||
extern Audio audio;
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
auto Stream::reset(uint channels_, double inputFrequency, double outputFrequency) -> void {
|
||||
this->inputFrequency = inputFrequency;
|
||||
this->outputFrequency = outputFrequency;
|
||||
|
||||
channels.reset();
|
||||
channels.resize(channels_);
|
||||
|
||||
for(auto& channel : channels) {
|
||||
channel.filters.reset();
|
||||
channel.resampler.reset(inputFrequency, outputFrequency);
|
||||
}
|
||||
}
|
||||
|
||||
auto Stream::setFrequency(double inputFrequency, maybe<double> outputFrequency) -> void {
|
||||
this->inputFrequency = inputFrequency;
|
||||
if(outputFrequency) this->outputFrequency = outputFrequency();
|
||||
|
||||
for(auto& channel : channels) {
|
||||
channel.resampler.reset(this->inputFrequency, this->outputFrequency);
|
||||
}
|
||||
}
|
||||
|
||||
auto Stream::addFilter(Filter::Order order, Filter::Type type, double cutoffFrequency, uint passes) -> void {
|
||||
for(auto& channel : channels) {
|
||||
for(auto pass : range(passes)) {
|
||||
Filter filter{order};
|
||||
|
||||
if(order == Filter::Order::First) {
|
||||
DSP::IIR::OnePole::Type _type;
|
||||
if(type == Filter::Type::LowPass) _type = DSP::IIR::OnePole::Type::LowPass;
|
||||
if(type == Filter::Type::HighPass) _type = DSP::IIR::OnePole::Type::HighPass;
|
||||
filter.onePole.reset(_type, cutoffFrequency, inputFrequency);
|
||||
}
|
||||
|
||||
if(order == Filter::Order::Second) {
|
||||
DSP::IIR::Biquad::Type _type;
|
||||
if(type == Filter::Type::LowPass) _type = DSP::IIR::Biquad::Type::LowPass;
|
||||
if(type == Filter::Type::HighPass) _type = DSP::IIR::Biquad::Type::HighPass;
|
||||
double q = DSP::IIR::Biquad::butterworth(passes * 2, pass);
|
||||
filter.biquad.reset(_type, cutoffFrequency, inputFrequency, q);
|
||||
}
|
||||
|
||||
channel.filters.append(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Stream::pending() const -> bool {
|
||||
return channels && channels[0].resampler.pending();
|
||||
}
|
||||
|
||||
auto Stream::read(double samples[]) -> uint {
|
||||
for(auto c : range(channels)) samples[c] = channels[c].resampler.read();
|
||||
return channels.size();
|
||||
}
|
||||
|
||||
auto Stream::write(const double samples[]) -> void {
|
||||
for(auto c : range(channels)) {
|
||||
double sample = samples[c] + 1e-25; //constant offset used to suppress denormals
|
||||
for(auto& filter : channels[c].filters) {
|
||||
switch(filter.order) {
|
||||
case Filter::Order::First: sample = filter.onePole.process(sample); break;
|
||||
case Filter::Order::Second: sample = filter.biquad.process(sample); break;
|
||||
}
|
||||
}
|
||||
channels[c].resampler.write(sample);
|
||||
}
|
||||
|
||||
audio.process();
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity type="win32" name="higan" version="1.0.0.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAware>false</dpiAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
@@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Name=higan
|
||||
Comment=Emulator
|
||||
Exec=higan
|
||||
Icon=higan
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Game;Emulator;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.byuu.higan</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>higan</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>higan</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>higan.icns</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,2 +0,0 @@
|
||||
1 24 "higan.Manifest"
|
||||
2 ICON DISCARDABLE "higan.ico"
|
||||
@@ -1,48 +0,0 @@
|
||||
#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.transform("=?", "//").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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#include <emulator/emulator.hpp>
|
||||
|
||||
namespace Emulator {
|
||||
|
||||
Platform* platform = nullptr;
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nall/nall.hpp>
|
||||
#include <nall/vfs.hpp>
|
||||
using namespace nall;
|
||||
|
||||
#include "types.hpp"
|
||||
#include <libco/libco.h>
|
||||
#include <audio/audio.hpp>
|
||||
#include <video/video.hpp>
|
||||
#include <resource/resource.hpp>
|
||||
|
||||
namespace Emulator {
|
||||
static const string Name = "higan";
|
||||
static const string Version = "104";
|
||||
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 = "104";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
//nall/vfs shorthand constants for open(), load()
|
||||
namespace File {
|
||||
static const auto Read = vfs::file::mode::read;
|
||||
static const auto Write = vfs::file::mode::write;
|
||||
static const auto Optional = false;
|
||||
static const auto Required = true;
|
||||
};
|
||||
}
|
||||
|
||||
#include "platform.hpp"
|
||||
#include "interface.hpp"
|
||||
@@ -1,84 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace Emulator {
|
||||
|
||||
struct Interface {
|
||||
struct Information {
|
||||
string manufacturer;
|
||||
string name;
|
||||
bool overscan;
|
||||
} information;
|
||||
|
||||
struct Medium {
|
||||
uint id;
|
||||
string name;
|
||||
string type; //extension
|
||||
};
|
||||
vector<Medium> media;
|
||||
|
||||
struct Device {
|
||||
uint id;
|
||||
string name;
|
||||
struct Input {
|
||||
uint type; //0 = digital, 1 = analog (relative), 2 = rumble
|
||||
string name;
|
||||
};
|
||||
vector<Input> inputs;
|
||||
};
|
||||
|
||||
struct Port {
|
||||
uint id;
|
||||
string name;
|
||||
vector<Device> devices;
|
||||
};
|
||||
vector<Port> ports;
|
||||
|
||||
//information
|
||||
virtual auto manifest() -> string = 0;
|
||||
virtual auto title() -> string = 0;
|
||||
|
||||
//video information
|
||||
struct VideoResolution {
|
||||
uint width;
|
||||
uint height;
|
||||
uint internalWidth;
|
||||
uint internalHeight;
|
||||
double aspectCorrection;
|
||||
};
|
||||
virtual auto videoResolution() -> VideoResolution = 0;
|
||||
virtual auto videoColors() -> uint32 = 0;
|
||||
virtual auto videoColor(uint32 color) -> uint64 = 0;
|
||||
|
||||
//media interface
|
||||
virtual auto loaded() -> bool { return false; }
|
||||
virtual auto sha256() -> string { return ""; }
|
||||
virtual auto load(uint id) -> bool { return false; }
|
||||
virtual auto save() -> void {}
|
||||
virtual auto unload() -> void {}
|
||||
|
||||
//system interface
|
||||
virtual auto connect(uint port, uint device) -> void {}
|
||||
virtual auto power() -> void {}
|
||||
virtual auto run() -> void {}
|
||||
|
||||
//time functions
|
||||
virtual auto rtc() -> bool { return false; }
|
||||
virtual auto rtcSynchronize() -> void {}
|
||||
|
||||
//state functions
|
||||
virtual auto serialize() -> serializer = 0;
|
||||
virtual auto unserialize(serializer&) -> bool = 0;
|
||||
|
||||
//cheat functions
|
||||
virtual auto cheatSet(const string_vector& = {}) -> void {}
|
||||
|
||||
//settings
|
||||
virtual auto cap(const string& name) -> bool { return false; }
|
||||
virtual auto get(const string& name) -> any { return {}; }
|
||||
virtual auto set(const string& name, const any& value) -> bool { return false; }
|
||||
|
||||
//shared functions
|
||||
auto videoColor(uint16 r, uint16 g, uint16 b) -> uint32;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace Emulator {
|
||||
|
||||
struct Platform {
|
||||
struct Load {
|
||||
Load() : _pathID(nothing) {}
|
||||
Load(uint pathID, string option = "") : _pathID(pathID), _option(option) {}
|
||||
|
||||
explicit operator bool() const { return (bool)_pathID; }
|
||||
auto pathID() const -> uint { return _pathID(); }
|
||||
auto option() const -> string { return _option; }
|
||||
|
||||
private:
|
||||
maybe<uint> _pathID;
|
||||
string _option;
|
||||
};
|
||||
|
||||
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, string_vector options = {}) -> Load { return {}; }
|
||||
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"); }
|
||||
};
|
||||
|
||||
extern Platform* platform;
|
||||
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
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>;
|
||||
@@ -1,13 +0,0 @@
|
||||
processors += mos6502
|
||||
|
||||
objects += fc-interface fc-system fc-controller
|
||||
objects += fc-memory fc-cartridge fc-cpu fc-apu fc-ppu
|
||||
|
||||
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-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/)
|
||||
@@ -1,319 +0,0 @@
|
||||
#include <fc/fc.hpp>
|
||||
|
||||
namespace Famicom {
|
||||
|
||||
#include "envelope.cpp"
|
||||
#include "sweep.cpp"
|
||||
#include "pulse.cpp"
|
||||
#include "triangle.cpp"
|
||||
#include "noise.cpp"
|
||||
#include "dmc.cpp"
|
||||
#include "serialization.cpp"
|
||||
APU apu;
|
||||
|
||||
APU::APU() {
|
||||
for(uint amp : range(32)) {
|
||||
if(amp == 0) {
|
||||
pulseDAC[amp] = 0;
|
||||
} else {
|
||||
pulseDAC[amp] = 16384.0 * 95.88 / (8128.0 / amp + 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
for(uint dmc_amp : range(128)) {
|
||||
for(uint triangle_amp : range(16)) {
|
||||
for(uint noise_amp : range(16)) {
|
||||
if(dmc_amp == 0 && triangle_amp == 0 && noise_amp == 0) {
|
||||
dmcTriangleNoiseDAC[dmc_amp][triangle_amp][noise_amp] = 0;
|
||||
} else {
|
||||
dmcTriangleNoiseDAC[dmc_amp][triangle_amp][noise_amp]
|
||||
= 16384.0 * 159.79 / (100.0 + 1.0 / (triangle_amp / 8227.0 + noise_amp / 12241.0 + dmc_amp / 22638.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto APU::Enter() -> void {
|
||||
while(true) scheduler.synchronize(), apu.main();
|
||||
}
|
||||
|
||||
auto APU::main() -> void {
|
||||
uint pulse_output, triangle_output, noise_output, dmc_output;
|
||||
|
||||
pulse_output = pulse[0].clock();
|
||||
pulse_output += pulse[1].clock();
|
||||
triangle_output = triangle.clock();
|
||||
noise_output = noise.clock();
|
||||
dmc_output = dmc.clock();
|
||||
|
||||
clockFrameCounterDivider();
|
||||
|
||||
int output = 0;
|
||||
output += pulseDAC[pulse_output];
|
||||
output += dmcTriangleNoiseDAC[dmc_output][triangle_output][noise_output];
|
||||
output += cartridgeSample;
|
||||
stream->sample(sclamp<16>(output) / 32768.0);
|
||||
|
||||
tick();
|
||||
}
|
||||
|
||||
auto APU::tick() -> void {
|
||||
Thread::step(rate());
|
||||
synchronize(cpu);
|
||||
}
|
||||
|
||||
auto APU::setIRQ() -> void {
|
||||
cpu.apuLine(frame.irqPending || dmc.irqPending);
|
||||
}
|
||||
|
||||
auto APU::setSample(int16 sample) -> void {
|
||||
cartridgeSample = sample;
|
||||
}
|
||||
|
||||
auto APU::power() -> void {
|
||||
create(APU::Enter, system.frequency());
|
||||
stream = Emulator::audio.createStream(1, frequency() / rate());
|
||||
stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 90.0);
|
||||
stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 440.0);
|
||||
stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::LowPass, 14000.0);
|
||||
stream->addFilter(Emulator::Filter::Order::Second, Emulator::Filter::Type::LowPass, 20000.0, 3);
|
||||
|
||||
pulse[0].power();
|
||||
pulse[1].power();
|
||||
triangle.power();
|
||||
noise.power();
|
||||
dmc.power();
|
||||
|
||||
frame.irqPending = 0;
|
||||
|
||||
frame.mode = 0;
|
||||
frame.counter = 0;
|
||||
frame.divider = 1;
|
||||
|
||||
enabledChannels = 0;
|
||||
cartridgeSample = 0;
|
||||
|
||||
setIRQ();
|
||||
}
|
||||
|
||||
auto APU::readIO(uint16 addr) -> uint8 {
|
||||
switch(addr) {
|
||||
|
||||
case 0x4015: {
|
||||
uint8 result = 0x00;
|
||||
result |= pulse[0].lengthCounter ? 0x01 : 0;
|
||||
result |= pulse[1].lengthCounter ? 0x02 : 0;
|
||||
result |= triangle.lengthCounter ? 0x04 : 0;
|
||||
result |= noise.lengthCounter ? 0x08 : 0;
|
||||
result |= dmc.lengthCounter ? 0x10 : 0;
|
||||
result |= frame.irqPending ? 0x40 : 0;
|
||||
result |= dmc.irqPending ? 0x80 : 0;
|
||||
|
||||
frame.irqPending = false;
|
||||
setIRQ();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto APU::writeIO(uint16 addr, uint8 data) -> void {
|
||||
const uint n = (addr >> 2) & 1; //pulse#
|
||||
|
||||
switch(addr) {
|
||||
|
||||
case 0x4000: case 0x4004: {
|
||||
pulse[n].duty = data >> 6;
|
||||
pulse[n].envelope.loopMode = data & 0x20;
|
||||
pulse[n].envelope.useSpeedAsVolume = data & 0x10;
|
||||
pulse[n].envelope.speed = data & 0x0f;
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4001: case 0x4005: {
|
||||
pulse[n].sweep.enable = data & 0x80;
|
||||
pulse[n].sweep.period = (data & 0x70) >> 4;
|
||||
pulse[n].sweep.decrement = data & 0x08;
|
||||
pulse[n].sweep.shift = data & 0x07;
|
||||
pulse[n].sweep.reload = true;
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4002: case 0x4006: {
|
||||
pulse[n].period = (pulse[n].period & 0x0700) | (data << 0);
|
||||
pulse[n].sweep.pulsePeriod = (pulse[n].sweep.pulsePeriod & 0x0700) | (data << 0);
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4003: case 0x4007: {
|
||||
pulse[n].period = (pulse[n].period & 0x00ff) | (data << 8);
|
||||
pulse[n].sweep.pulsePeriod = (pulse[n].sweep.pulsePeriod & 0x00ff) | (data << 8);
|
||||
|
||||
pulse[n].dutyCounter = 0;
|
||||
pulse[n].envelope.reloadDecay = true;
|
||||
|
||||
if(enabledChannels & (1 << n)) {
|
||||
pulse[n].lengthCounter = lengthCounterTable[(data >> 3) & 0x1f];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4008: {
|
||||
triangle.haltLengthCounter = data & 0x80;
|
||||
triangle.linearLength = data & 0x7f;
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x400a: {
|
||||
triangle.period = (triangle.period & 0x0700) | (data << 0);
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x400b: {
|
||||
triangle.period = (triangle.period & 0x00ff) | (data << 8);
|
||||
|
||||
triangle.reloadLinear = true;
|
||||
|
||||
if(enabledChannels & (1 << 2)) {
|
||||
triangle.lengthCounter = lengthCounterTable[(data >> 3) & 0x1f];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x400c: {
|
||||
noise.envelope.loopMode = data & 0x20;
|
||||
noise.envelope.useSpeedAsVolume = data & 0x10;
|
||||
noise.envelope.speed = data & 0x0f;
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x400e: {
|
||||
noise.shortMode = data & 0x80;
|
||||
noise.period = data & 0x0f;
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x400f: {
|
||||
noise.envelope.reloadDecay = true;
|
||||
|
||||
if(enabledChannels & (1 << 3)) {
|
||||
noise.lengthCounter = lengthCounterTable[(data >> 3) & 0x1f];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4010: {
|
||||
dmc.irqEnable = data & 0x80;
|
||||
dmc.loopMode = data & 0x40;
|
||||
dmc.period = data & 0x0f;
|
||||
|
||||
dmc.irqPending = dmc.irqPending && dmc.irqEnable && !dmc.loopMode;
|
||||
setIRQ();
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4011: {
|
||||
dmc.dacLatch = data & 0x7f;
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4012: {
|
||||
dmc.addrLatch = data;
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4013: {
|
||||
dmc.lengthLatch = data;
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4015: {
|
||||
if((data & 0x01) == 0) pulse[0].lengthCounter = 0;
|
||||
if((data & 0x02) == 0) pulse[1].lengthCounter = 0;
|
||||
if((data & 0x04) == 0) triangle.lengthCounter = 0;
|
||||
if((data & 0x08) == 0) noise.lengthCounter = 0;
|
||||
|
||||
(data & 0x10) ? dmc.start() : dmc.stop();
|
||||
dmc.irqPending = false;
|
||||
|
||||
setIRQ();
|
||||
enabledChannels = data & 0x1f;
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4017: {
|
||||
frame.mode = data >> 6;
|
||||
|
||||
frame.counter = 0;
|
||||
if(frame.mode & 2) clockFrameCounter();
|
||||
if(frame.mode & 1) {
|
||||
frame.irqPending = false;
|
||||
setIRQ();
|
||||
}
|
||||
frame.divider = FrameCounter::NtscPeriod;
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
auto APU::clockFrameCounter() -> void {
|
||||
frame.counter++;
|
||||
|
||||
if(frame.counter & 1) {
|
||||
pulse[0].clockLength();
|
||||
pulse[0].sweep.clock(0);
|
||||
pulse[1].clockLength();
|
||||
pulse[1].sweep.clock(1);
|
||||
triangle.clockLength();
|
||||
noise.clockLength();
|
||||
}
|
||||
|
||||
pulse[0].envelope.clock();
|
||||
pulse[1].envelope.clock();
|
||||
triangle.clockLinearLength();
|
||||
noise.envelope.clock();
|
||||
|
||||
if(frame.counter == 0) {
|
||||
if(frame.mode & 2) frame.divider += FrameCounter::NtscPeriod;
|
||||
if(frame.mode == 0) {
|
||||
frame.irqPending = true;
|
||||
setIRQ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto APU::clockFrameCounterDivider() -> void {
|
||||
frame.divider -= 2;
|
||||
if(frame.divider <= 0) {
|
||||
clockFrameCounter();
|
||||
frame.divider += FrameCounter::NtscPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
const uint8 APU::lengthCounterTable[32] = {
|
||||
0x0a, 0xfe, 0x14, 0x02, 0x28, 0x04, 0x50, 0x06, 0xa0, 0x08, 0x3c, 0x0a, 0x0e, 0x0c, 0x1a, 0x0e,
|
||||
0x0c, 0x10, 0x18, 0x12, 0x30, 0x14, 0x60, 0x16, 0xc0, 0x18, 0x48, 0x1a, 0x10, 0x1c, 0x20, 0x1e,
|
||||
};
|
||||
|
||||
const uint16 APU::noisePeriodTableNTSC[16] = {
|
||||
4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068,
|
||||
};
|
||||
|
||||
const uint16 APU::noisePeriodTablePAL[16] = {
|
||||
4, 8, 14, 30, 60, 88, 118, 148, 188, 236, 354, 472, 708, 944, 1890, 3778,
|
||||
};
|
||||
|
||||
const uint16 APU::dmcPeriodTableNTSC[16] = {
|
||||
428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54,
|
||||
};
|
||||
|
||||
const uint16 APU::dmcPeriodTablePAL[16] = {
|
||||
398, 354, 316, 298, 276, 236, 210, 198, 176, 148, 132, 118, 98, 78, 66, 50,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
struct APU : Thread {
|
||||
shared_pointer<Emulator::Stream> stream;
|
||||
|
||||
inline auto rate() const -> uint { return Region::PAL() ? 16 : 12; }
|
||||
|
||||
//apu.cpp
|
||||
APU();
|
||||
|
||||
static auto Enter() -> void;
|
||||
auto main() -> void;
|
||||
auto tick() -> void;
|
||||
auto setIRQ() -> void;
|
||||
auto setSample(int16 sample) -> void;
|
||||
|
||||
auto power() -> void;
|
||||
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
|
||||
//serialization.cpp
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct Envelope {
|
||||
auto volume() const -> uint;
|
||||
auto clock() -> void;
|
||||
|
||||
auto power() -> void;
|
||||
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
uint4 speed;
|
||||
bool useSpeedAsVolume;
|
||||
bool loopMode;
|
||||
|
||||
bool reloadDecay;
|
||||
uint8 decayCounter;
|
||||
uint4 decayVolume;
|
||||
};
|
||||
|
||||
struct Sweep {
|
||||
auto checkPeriod() -> bool;
|
||||
auto clock(uint channel) -> void;
|
||||
|
||||
auto power() -> void;
|
||||
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
uint8 shift;
|
||||
bool decrement;
|
||||
uint3 period;
|
||||
uint8 counter;
|
||||
bool enable;
|
||||
bool reload;
|
||||
uint11 pulsePeriod;
|
||||
};
|
||||
|
||||
struct Pulse {
|
||||
auto clockLength() -> void;
|
||||
auto checkPeriod() -> bool;
|
||||
auto clock() -> uint8;
|
||||
|
||||
auto power() -> void;
|
||||
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
uint lengthCounter;
|
||||
|
||||
Envelope envelope;
|
||||
Sweep sweep;
|
||||
|
||||
uint2 duty;
|
||||
uint3 dutyCounter;
|
||||
|
||||
uint11 period;
|
||||
uint periodCounter;
|
||||
} pulse[2];
|
||||
|
||||
struct Triangle {
|
||||
auto clockLength() -> void;
|
||||
auto clockLinearLength() -> void;
|
||||
auto clock() -> uint8;
|
||||
|
||||
auto power() -> void;
|
||||
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
uint lengthCounter;
|
||||
|
||||
uint8 linearLength;
|
||||
bool haltLengthCounter;
|
||||
|
||||
uint11 period;
|
||||
uint periodCounter;
|
||||
|
||||
uint5 stepCounter;
|
||||
uint8 linearLengthCounter;
|
||||
bool reloadLinear;
|
||||
} triangle;
|
||||
|
||||
struct Noise {
|
||||
auto clockLength() -> void;
|
||||
auto clock() -> uint8;
|
||||
|
||||
auto power() -> void;
|
||||
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
uint lengthCounter;
|
||||
|
||||
Envelope envelope;
|
||||
|
||||
uint4 period;
|
||||
uint periodCounter;
|
||||
|
||||
bool shortMode;
|
||||
uint15 lfsr;
|
||||
} noise;
|
||||
|
||||
struct DMC {
|
||||
auto start() -> void;
|
||||
auto stop() -> void;
|
||||
auto clock() -> uint8;
|
||||
|
||||
auto power() -> void;
|
||||
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
uint lengthCounter;
|
||||
bool irqPending;
|
||||
|
||||
uint4 period;
|
||||
uint periodCounter;
|
||||
|
||||
bool irqEnable;
|
||||
bool loopMode;
|
||||
|
||||
uint8 dacLatch;
|
||||
uint8 addrLatch;
|
||||
uint8 lengthLatch;
|
||||
|
||||
uint15 readAddr;
|
||||
uint dmaDelayCounter;
|
||||
|
||||
uint3 bitCounter;
|
||||
bool dmaBufferValid;
|
||||
uint8 dmaBuffer;
|
||||
|
||||
bool sampleValid;
|
||||
uint8 sample;
|
||||
} dmc;
|
||||
|
||||
struct FrameCounter {
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
enum : uint { NtscPeriod = 14915 }; //~(21.477MHz / 6 / 240hz)
|
||||
|
||||
bool irqPending;
|
||||
|
||||
uint2 mode;
|
||||
uint2 counter;
|
||||
int divider;
|
||||
};
|
||||
|
||||
auto clockFrameCounter() -> void;
|
||||
auto clockFrameCounterDivider() -> void;
|
||||
|
||||
FrameCounter frame;
|
||||
|
||||
uint8 enabledChannels;
|
||||
int16 cartridgeSample;
|
||||
|
||||
int16 pulseDAC[32];
|
||||
int16 dmcTriangleNoiseDAC[128][16][16];
|
||||
|
||||
static const uint8 lengthCounterTable[32];
|
||||
static const uint16 dmcPeriodTableNTSC[16];
|
||||
static const uint16 dmcPeriodTablePAL[16];
|
||||
static const uint16 noisePeriodTableNTSC[16];
|
||||
static const uint16 noisePeriodTablePAL[16];
|
||||
};
|
||||
|
||||
extern APU apu;
|
||||
@@ -1,89 +0,0 @@
|
||||
auto APU::DMC::start() -> void {
|
||||
if(lengthCounter == 0) {
|
||||
readAddr = 0x4000 + (addrLatch << 6);
|
||||
lengthCounter = (lengthLatch << 4) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
auto APU::DMC::stop() -> void {
|
||||
lengthCounter = 0;
|
||||
dmaDelayCounter = 0;
|
||||
cpu.rdyLine(1);
|
||||
cpu.rdyAddr(false);
|
||||
}
|
||||
|
||||
auto APU::DMC::clock() -> uint8 {
|
||||
uint8 result = dacLatch;
|
||||
|
||||
if(dmaDelayCounter > 0) {
|
||||
dmaDelayCounter--;
|
||||
|
||||
if(dmaDelayCounter == 1) {
|
||||
cpu.rdyAddr(true, 0x8000 | readAddr);
|
||||
} else if(dmaDelayCounter == 0) {
|
||||
cpu.rdyLine(1);
|
||||
cpu.rdyAddr(false);
|
||||
|
||||
dmaBuffer = cpu.mdr();
|
||||
dmaBufferValid = true;
|
||||
lengthCounter--;
|
||||
readAddr++;
|
||||
|
||||
if(lengthCounter == 0) {
|
||||
if(loopMode) {
|
||||
start();
|
||||
} else if(irqEnable) {
|
||||
irqPending = true;
|
||||
apu.setIRQ();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(--periodCounter == 0) {
|
||||
if(sampleValid) {
|
||||
int delta = (((sample >> bitCounter) & 1) << 2) - 2;
|
||||
uint data = dacLatch + delta;
|
||||
if((data & 0x80) == 0) dacLatch = data;
|
||||
}
|
||||
|
||||
if(++bitCounter == 0) {
|
||||
if(dmaBufferValid) {
|
||||
sampleValid = true;
|
||||
sample = dmaBuffer;
|
||||
dmaBufferValid = false;
|
||||
} else {
|
||||
sampleValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
periodCounter = Region::PAL() ? dmcPeriodTablePAL[period] : dmcPeriodTableNTSC[period];
|
||||
}
|
||||
|
||||
if(lengthCounter > 0 && !dmaBufferValid && dmaDelayCounter == 0) {
|
||||
cpu.rdyLine(0);
|
||||
dmaDelayCounter = 4;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto APU::DMC::power() -> void {
|
||||
lengthCounter = 0;
|
||||
irqPending = 0;
|
||||
|
||||
period = 0;
|
||||
periodCounter = Region::PAL() ? dmcPeriodTablePAL[0] : dmcPeriodTableNTSC[0];
|
||||
irqEnable = 0;
|
||||
loopMode = 0;
|
||||
dacLatch = 0;
|
||||
addrLatch = 0;
|
||||
lengthLatch = 0;
|
||||
readAddr = 0;
|
||||
dmaDelayCounter = 0;
|
||||
bitCounter = 0;
|
||||
dmaBufferValid = 0;
|
||||
dmaBuffer = 0;
|
||||
sampleValid = 0;
|
||||
sample = 0;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
auto APU::Envelope::volume() const -> uint {
|
||||
return useSpeedAsVolume ? speed : decayVolume;
|
||||
}
|
||||
|
||||
auto APU::Envelope::clock() -> void {
|
||||
if(reloadDecay) {
|
||||
reloadDecay = false;
|
||||
decayVolume = 0x0f;
|
||||
decayCounter = speed + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if(--decayCounter == 0) {
|
||||
decayCounter = speed + 1;
|
||||
if(decayVolume || loopMode) decayVolume--;
|
||||
}
|
||||
}
|
||||
|
||||
auto APU::Envelope::power() -> void {
|
||||
speed = 0;
|
||||
useSpeedAsVolume = 0;
|
||||
loopMode = 0;
|
||||
reloadDecay = 0;
|
||||
decayCounter = 0;
|
||||
decayVolume = 0;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
auto APU::Noise::clockLength() -> void {
|
||||
if(envelope.loopMode == 0) {
|
||||
if(lengthCounter > 0) lengthCounter--;
|
||||
}
|
||||
}
|
||||
|
||||
auto APU::Noise::clock() -> uint8 {
|
||||
if(lengthCounter == 0) return 0;
|
||||
|
||||
uint8 result = (lfsr & 1) ? envelope.volume() : 0;
|
||||
|
||||
if(--periodCounter == 0) {
|
||||
uint feedback;
|
||||
|
||||
if(shortMode) {
|
||||
feedback = ((lfsr >> 0) & 1) ^ ((lfsr >> 6) & 1);
|
||||
} else {
|
||||
feedback = ((lfsr >> 0) & 1) ^ ((lfsr >> 1) & 1);
|
||||
}
|
||||
|
||||
lfsr = (lfsr >> 1) | (feedback << 14);
|
||||
periodCounter = Region::PAL() ? apu.noisePeriodTablePAL[period] : apu.noisePeriodTableNTSC[period];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto APU::Noise::power() -> void {
|
||||
lengthCounter = 0;
|
||||
|
||||
envelope.speed = 0;
|
||||
envelope.useSpeedAsVolume = 0;
|
||||
envelope.loopMode = 0;
|
||||
envelope.reloadDecay = 0;
|
||||
envelope.decayCounter = 0;
|
||||
envelope.decayVolume = 0;
|
||||
|
||||
period = 0;
|
||||
periodCounter = 1;
|
||||
shortMode = 0;
|
||||
lfsr = 1;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
auto APU::Pulse::clockLength() -> void {
|
||||
if(envelope.loopMode == 0) {
|
||||
if(lengthCounter) lengthCounter--;
|
||||
}
|
||||
}
|
||||
|
||||
auto APU::Pulse::clock() -> uint8 {
|
||||
if(!sweep.checkPeriod()) return 0;
|
||||
if(lengthCounter == 0) return 0;
|
||||
|
||||
static const uint dutyTable[4][8] = {
|
||||
{0, 0, 0, 0, 0, 0, 0, 1}, //12.5%
|
||||
{0, 0, 0, 0, 0, 0, 1, 1}, //25.0%
|
||||
{0, 0, 0, 0, 1, 1, 1, 1}, //50.0%
|
||||
{1, 1, 1, 1, 1, 1, 0, 0}, //25.0% (negated)
|
||||
};
|
||||
uint8 result = dutyTable[duty][dutyCounter] ? envelope.volume() : 0;
|
||||
if(sweep.pulsePeriod < 0x008) result = 0;
|
||||
|
||||
if(--periodCounter == 0) {
|
||||
periodCounter = (sweep.pulsePeriod + 1) * 2;
|
||||
dutyCounter--;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto APU::Pulse::power() -> void {
|
||||
envelope.power();
|
||||
sweep.power();
|
||||
|
||||
lengthCounter = 0;
|
||||
|
||||
duty = 0;
|
||||
dutyCounter = 0;
|
||||
period = 0;
|
||||
periodCounter = 1;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
auto APU::serialize(serializer& s) -> void {
|
||||
Thread::serialize(s);
|
||||
|
||||
pulse[0].serialize(s);
|
||||
pulse[1].serialize(s);
|
||||
triangle.serialize(s);
|
||||
dmc.serialize(s);
|
||||
frame.serialize(s);
|
||||
|
||||
s.integer(enabledChannels);
|
||||
s.integer(cartridgeSample);
|
||||
}
|
||||
|
||||
auto APU::Envelope::serialize(serializer& s) -> void {
|
||||
s.integer(speed);
|
||||
s.integer(useSpeedAsVolume);
|
||||
s.integer(loopMode);
|
||||
|
||||
s.integer(reloadDecay);
|
||||
s.integer(decayCounter);
|
||||
s.integer(decayVolume);
|
||||
}
|
||||
|
||||
auto APU::Sweep::serialize(serializer& s) -> void {
|
||||
s.integer(shift);
|
||||
s.integer(decrement);
|
||||
s.integer(period);
|
||||
s.integer(counter);
|
||||
s.integer(enable);
|
||||
s.integer(reload);
|
||||
s.integer(pulsePeriod);
|
||||
}
|
||||
|
||||
auto APU::Pulse::serialize(serializer& s) -> void {
|
||||
s.integer(lengthCounter);
|
||||
|
||||
envelope.serialize(s);
|
||||
sweep.serialize(s);
|
||||
|
||||
s.integer(duty);
|
||||
s.integer(dutyCounter);
|
||||
|
||||
s.integer(period);
|
||||
s.integer(periodCounter);
|
||||
}
|
||||
|
||||
auto APU::Triangle::serialize(serializer& s) -> void {
|
||||
s.integer(lengthCounter);
|
||||
|
||||
s.integer(linearLength);
|
||||
s.integer(haltLengthCounter);
|
||||
|
||||
s.integer(period);
|
||||
s.integer(periodCounter);
|
||||
|
||||
s.integer(stepCounter);
|
||||
s.integer(linearLengthCounter);
|
||||
s.integer(reloadLinear);
|
||||
}
|
||||
|
||||
auto APU::Noise::serialize(serializer& s) -> void {
|
||||
s.integer(lengthCounter);
|
||||
|
||||
envelope.serialize(s);
|
||||
|
||||
s.integer(period);
|
||||
s.integer(periodCounter);
|
||||
|
||||
s.integer(shortMode);
|
||||
s.integer(lfsr);
|
||||
}
|
||||
|
||||
auto APU::DMC::serialize(serializer& s) -> void {
|
||||
s.integer(lengthCounter);
|
||||
s.integer(irqPending);
|
||||
|
||||
s.integer(period);
|
||||
s.integer(periodCounter);
|
||||
|
||||
s.integer(irqEnable);
|
||||
s.integer(loopMode);
|
||||
|
||||
s.integer(dacLatch);
|
||||
s.integer(addrLatch);
|
||||
s.integer(lengthLatch);
|
||||
|
||||
s.integer(readAddr);
|
||||
s.integer(dmaDelayCounter);
|
||||
|
||||
s.integer(bitCounter);
|
||||
s.integer(dmaBufferValid);
|
||||
s.integer(dmaBuffer);
|
||||
|
||||
s.integer(sampleValid);
|
||||
s.integer(sample);
|
||||
}
|
||||
|
||||
auto APU::FrameCounter::serialize(serializer& s) -> void {
|
||||
s.integer(irqPending);
|
||||
|
||||
s.integer(mode);
|
||||
s.integer(counter);
|
||||
s.integer(divider);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
auto APU::Sweep::checkPeriod() -> bool {
|
||||
if(pulsePeriod > 0x7ff) return false;
|
||||
|
||||
if(decrement == 0) {
|
||||
if((pulsePeriod + (pulsePeriod >> shift)) & 0x800) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto APU::Sweep::clock(uint channel) -> void {
|
||||
if(--counter == 0) {
|
||||
counter = period + 1;
|
||||
if(enable && shift && pulsePeriod > 8) {
|
||||
int delta = pulsePeriod >> shift;
|
||||
|
||||
if(decrement) {
|
||||
pulsePeriod -= delta;
|
||||
if(channel == 0) pulsePeriod--;
|
||||
} else if((pulsePeriod + delta) < 0x800) {
|
||||
pulsePeriod += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(reload) {
|
||||
reload = false;
|
||||
counter = period + 1;
|
||||
}
|
||||
}
|
||||
|
||||
auto APU::Sweep::power() -> void {
|
||||
shift = 0;
|
||||
decrement = 0;
|
||||
period = 0;
|
||||
counter = 1;
|
||||
enable = 0;
|
||||
reload = 0;
|
||||
pulsePeriod = 0;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
auto APU::Triangle::clockLength() -> void {
|
||||
if(haltLengthCounter == 0) {
|
||||
if(lengthCounter > 0) lengthCounter--;
|
||||
}
|
||||
}
|
||||
|
||||
auto APU::Triangle::clockLinearLength() -> void {
|
||||
if(reloadLinear) {
|
||||
linearLengthCounter = linearLength;
|
||||
} else if(linearLengthCounter) {
|
||||
linearLengthCounter--;
|
||||
}
|
||||
|
||||
if(haltLengthCounter == 0) reloadLinear = false;
|
||||
}
|
||||
|
||||
auto APU::Triangle::clock() -> uint8 {
|
||||
uint8 result = stepCounter & 0x0f;
|
||||
if((stepCounter & 0x10) == 0) result ^= 0x0f;
|
||||
if(lengthCounter == 0 || linearLengthCounter == 0) return result;
|
||||
|
||||
if(--periodCounter == 0) {
|
||||
stepCounter++;
|
||||
periodCounter = period + 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto APU::Triangle::power() -> void {
|
||||
lengthCounter = 0;
|
||||
|
||||
linearLength = 0;
|
||||
haltLengthCounter = 0;
|
||||
period = 0;
|
||||
periodCounter = 1;
|
||||
stepCounter = 0;
|
||||
linearLengthCounter = 0;
|
||||
reloadLinear = 0;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
//BANDAI-FCG
|
||||
|
||||
struct BandaiFCG : Board {
|
||||
BandaiFCG(Markup::Node& document) : Board(document) {
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
if(irqCounterEnable) {
|
||||
if(--irqCounter == 0xffff) {
|
||||
cpu.irqLine(1);
|
||||
irqCounterEnable = false;
|
||||
}
|
||||
}
|
||||
|
||||
tick();
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) const -> uint {
|
||||
switch(mirror) {
|
||||
case 0: return ((addr & 0x0400) >> 0) | (addr & 0x03ff);
|
||||
case 1: return ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
case 2: return 0x0000 | (addr & 0x03ff);
|
||||
case 3: return 0x0400 | (addr & 0x03ff);
|
||||
}
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr & 0x8000) {
|
||||
bool region = addr & 0x4000;
|
||||
uint bank = (region == 0 ? prgBank : (uint8)0x0f);
|
||||
return prgrom.read((bank << 14) | (addr & 0x3fff));
|
||||
}
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr >= 0x6000) {
|
||||
switch(addr & 15) {
|
||||
case 0x00: case 0x01: case 0x02: case 0x03:
|
||||
case 0x04: case 0x05: case 0x06: case 0x07:
|
||||
chrBank[addr & 7] = data;
|
||||
break;
|
||||
case 0x08:
|
||||
prgBank = data & 0x0f;
|
||||
break;
|
||||
case 0x09:
|
||||
mirror = data & 0x03;
|
||||
break;
|
||||
case 0x0a:
|
||||
cpu.irqLine(0);
|
||||
irqCounterEnable = data & 0x01;
|
||||
irqCounter = irqLatch;
|
||||
break;
|
||||
case 0x0b:
|
||||
irqLatch = (irqLatch & 0xff00) | (data << 0);
|
||||
break;
|
||||
case 0x0c:
|
||||
irqLatch = (irqLatch & 0x00ff) | (data << 8);
|
||||
break;
|
||||
case 0x0d:
|
||||
//todo: serial EEPROM support
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM(addrCIRAM(addr));
|
||||
addr = (chrBank[addr >> 10] << 10) | (addr & 0x03ff);
|
||||
return Board::readCHR(addr);
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(addrCIRAM(addr), data);
|
||||
addr = (chrBank[addr >> 10] << 10) | (addr & 0x03ff);
|
||||
return Board::writeCHR(addr, data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
reset();
|
||||
}
|
||||
|
||||
auto reset() -> void {
|
||||
for(auto& n : chrBank) n = 0;
|
||||
prgBank = 0;
|
||||
mirror = 0;
|
||||
irqCounterEnable = 0;
|
||||
irqCounter = 0;
|
||||
irqLatch = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
|
||||
s.array(chrBank);
|
||||
s.integer(prgBank);
|
||||
s.integer(mirror);
|
||||
s.integer(irqCounterEnable);
|
||||
s.integer(irqCounter);
|
||||
s.integer(irqLatch);
|
||||
}
|
||||
|
||||
uint8 chrBank[8];
|
||||
uint8 prgBank;
|
||||
uint2 mirror;
|
||||
bool irqCounterEnable;
|
||||
uint16 irqCounter;
|
||||
uint16 irqLatch;
|
||||
};
|
||||
@@ -1,229 +0,0 @@
|
||||
#include "bandai-fcg.cpp"
|
||||
#include "konami-vrc1.cpp"
|
||||
#include "konami-vrc2.cpp"
|
||||
#include "konami-vrc3.cpp"
|
||||
#include "konami-vrc4.cpp"
|
||||
#include "konami-vrc6.cpp"
|
||||
#include "konami-vrc7.cpp"
|
||||
#include "nes-axrom.cpp"
|
||||
#include "nes-bnrom.cpp"
|
||||
#include "nes-cnrom.cpp"
|
||||
#include "nes-exrom.cpp"
|
||||
#include "nes-fxrom.cpp"
|
||||
#include "nes-gxrom.cpp"
|
||||
#include "nes-hkrom.cpp"
|
||||
#include "nes-nrom.cpp"
|
||||
#include "nes-pxrom.cpp"
|
||||
#include "nes-sxrom.cpp"
|
||||
#include "nes-txrom.cpp"
|
||||
#include "nes-uxrom.cpp"
|
||||
#include "sunsoft-5b.cpp"
|
||||
|
||||
Board::Board(Markup::Node& document) {
|
||||
cartridge.board = this;
|
||||
auto board = document["board"];
|
||||
|
||||
information.type = board["id"].text();
|
||||
information.battery = (bool)board["prg/ram/name"];
|
||||
|
||||
auto prom = board["prg/rom"];
|
||||
auto pram = board["prg/ram"];
|
||||
auto crom = board["chr/rom"];
|
||||
auto cram = board["chr/ram"];
|
||||
|
||||
prgrom.size = prom["size"].natural();
|
||||
prgram.size = pram["size"].natural();
|
||||
chrrom.size = crom["size"].natural();
|
||||
chrram.size = cram["size"].natural();
|
||||
|
||||
if(prgrom.size) prgrom.data = new uint8_t[prgrom.size]();
|
||||
if(prgram.size) prgram.data = new uint8_t[prgram.size]();
|
||||
if(chrrom.size) chrrom.data = new uint8_t[chrrom.size]();
|
||||
if(chrram.size) chrram.data = new uint8_t[chrram.size]();
|
||||
|
||||
if(prgrom.name = prom["name"].text()) {
|
||||
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 = 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 = 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 = platform->open(cartridge.pathID(), chrram.name, File::Read)) {
|
||||
fp->read(chrram.data, min(chrram.size, fp->size()));
|
||||
}
|
||||
}
|
||||
|
||||
prgram.writable = true;
|
||||
chrram.writable = true;
|
||||
}
|
||||
|
||||
auto Board::save() -> void {
|
||||
auto document = BML::unserialize(cartridge.manifest());
|
||||
|
||||
if(auto name = document["board/prg/ram/name"].text()) {
|
||||
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 = platform->open(cartridge.pathID(), name, File::Write)) {
|
||||
fp->write(chrram.data, chrram.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Board::Memory::read(uint addr) const -> uint8 {
|
||||
return data[mirror(addr, size)];
|
||||
}
|
||||
|
||||
auto Board::Memory::write(uint addr, uint8 byte) -> void {
|
||||
if(writable) data[mirror(addr, size)] = byte;
|
||||
}
|
||||
|
||||
auto Board::mirror(uint addr, uint size) -> uint {
|
||||
uint base = 0;
|
||||
if(size) {
|
||||
uint mask = 1 << 23;
|
||||
while(addr >= size) {
|
||||
while(!(addr & mask)) mask >>= 1;
|
||||
addr -= mask;
|
||||
if(size > mask) {
|
||||
size -= mask;
|
||||
base += mask;
|
||||
}
|
||||
mask >>= 1;
|
||||
}
|
||||
base += addr;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
auto Board::main() -> void {
|
||||
cartridge.step(cartridge.rate() * 4095);
|
||||
tick();
|
||||
}
|
||||
|
||||
auto Board::tick() -> void {
|
||||
cartridge.step(cartridge.rate());
|
||||
cartridge.synchronize(cpu);
|
||||
}
|
||||
|
||||
auto Board::readCHR(uint addr) -> uint8 {
|
||||
if(chrram.size) return chrram.data[mirror(addr, chrram.size)];
|
||||
if(chrrom.size) return chrrom.data[mirror(addr, chrrom.size)];
|
||||
return 0u;
|
||||
}
|
||||
|
||||
auto Board::writeCHR(uint addr, uint8 data) -> void {
|
||||
if(chrram.size) chrram.data[mirror(addr, chrram.size)] = data;
|
||||
}
|
||||
|
||||
auto Board::power() -> void {
|
||||
}
|
||||
|
||||
auto Board::serialize(serializer& s) -> void {
|
||||
if(prgram.size) s.array(prgram.data, prgram.size);
|
||||
if(chrram.size) s.array(chrram.data, chrram.size);
|
||||
}
|
||||
|
||||
auto Board::load(string manifest) -> Board* {
|
||||
auto document = BML::unserialize(manifest);
|
||||
cartridge.information.title = document["information/title"].text();
|
||||
|
||||
string type = document["board/id"].text();
|
||||
|
||||
if(type == "BANDAI-FCG" ) return new BandaiFCG(document);
|
||||
|
||||
if(type == "KONAMI-VRC-1") return new KonamiVRC1(document);
|
||||
if(type == "KONAMI-VRC-2") return new KonamiVRC2(document);
|
||||
if(type == "KONAMI-VRC-3") return new KonamiVRC3(document);
|
||||
if(type == "KONAMI-VRC-4") return new KonamiVRC4(document);
|
||||
if(type == "KONAMI-VRC-6") return new KonamiVRC6(document);
|
||||
if(type == "KONAMI-VRC-7") return new KonamiVRC7(document);
|
||||
|
||||
if(type == "NES-AMROM" ) return new NES_AxROM(document);
|
||||
if(type == "NES-ANROM" ) return new NES_AxROM(document);
|
||||
if(type == "NES-AN1ROM" ) return new NES_AxROM(document);
|
||||
if(type == "NES-AOROM" ) return new NES_AxROM(document);
|
||||
|
||||
if(type == "NES-BNROM" ) return new NES_BNROM(document);
|
||||
|
||||
if(type == "NES-CNROM" ) return new NES_CNROM(document);
|
||||
|
||||
if(type == "NES-EKROM" ) return new NES_ExROM(document);
|
||||
if(type == "NES-ELROM" ) return new NES_ExROM(document);
|
||||
if(type == "NES-ETROM" ) return new NES_ExROM(document);
|
||||
if(type == "NES-EWROM" ) return new NES_ExROM(document);
|
||||
|
||||
if(type == "NES-FJROM" ) return new NES_FxROM(document);
|
||||
if(type == "NES-FKROM" ) return new NES_FxROM(document);
|
||||
|
||||
if(type == "NES-GNROM" ) return new NES_GxROM(document);
|
||||
if(type == "NES-MHROM" ) return new NES_GxROM(document);
|
||||
|
||||
if(type == "NES-HKROM" ) return new NES_HKROM(document);
|
||||
|
||||
if(type == "NES-NROM" ) return new NES_NROM(document);
|
||||
if(type == "NES-NROM-128") return new NES_NROM(document);
|
||||
if(type == "NES-NROM-256") return new NES_NROM(document);
|
||||
|
||||
if(type == "NES-PEEOROM" ) return new NES_PxROM(document);
|
||||
if(type == "NES-PNROM" ) return new NES_PxROM(document);
|
||||
|
||||
if(type == "NES-SAROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SBROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SCROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SC1ROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SEROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SFROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SFEXPROM") return new NES_SxROM(document);
|
||||
if(type == "NES-SGROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SHROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SH1ROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SIROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SJROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SKROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SLROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SL1ROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SL2ROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SL3ROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SLRROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SMROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SNROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SOROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SUROM" ) return new NES_SxROM(document);
|
||||
if(type == "NES-SXROM" ) return new NES_SxROM(document);
|
||||
|
||||
if(type == "NES-TBROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TEROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TFROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TGROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TKROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TKSROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TLROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TL1ROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TL2ROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TLSROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TNROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TQROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TR1ROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TSROM" ) return new NES_TxROM(document);
|
||||
if(type == "NES-TVROM" ) return new NES_TxROM(document);
|
||||
|
||||
if(type == "NES-UNROM" ) return new NES_UxROM(document);
|
||||
if(type == "NES-UOROM" ) return new NES_UxROM(document);
|
||||
|
||||
if(type == "SUNSOFT-5B" ) return new Sunsoft5B(document);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
struct Board {
|
||||
struct Memory {
|
||||
inline Memory(uint8_t* data, uint size) : data(data), size(size) {}
|
||||
inline Memory() : data(nullptr), size(0u), writable(false) {}
|
||||
inline ~Memory() { if(data) delete[] data; }
|
||||
|
||||
inline auto read(uint addr) const -> uint8;
|
||||
inline auto write(uint addr, uint8 data) -> void;
|
||||
|
||||
string name;
|
||||
uint8_t* data = nullptr;
|
||||
uint size = 0;
|
||||
bool writable = false;
|
||||
};
|
||||
|
||||
virtual ~Board() = default;
|
||||
|
||||
static auto mirror(uint addr, uint size) -> uint;
|
||||
|
||||
Board(Markup::Node& document);
|
||||
auto save() -> void;
|
||||
|
||||
virtual auto main() -> void;
|
||||
virtual auto tick() -> void;
|
||||
|
||||
virtual auto readPRG(uint addr) -> uint8 = 0;
|
||||
virtual auto writePRG(uint addr, uint8 data) -> void = 0;
|
||||
|
||||
virtual auto readCHR(uint addr) -> uint8;
|
||||
virtual auto writeCHR(uint addr, uint8 data) -> void;
|
||||
|
||||
virtual inline auto scanline(uint y) -> void {}
|
||||
|
||||
virtual auto power() -> void;
|
||||
|
||||
virtual auto serialize(serializer&) -> void;
|
||||
|
||||
static auto load(string manifest) -> Board*;
|
||||
|
||||
struct Information {
|
||||
string type;
|
||||
bool battery;
|
||||
} information;
|
||||
|
||||
Memory prgrom;
|
||||
Memory prgram;
|
||||
Memory chrrom;
|
||||
Memory chrram;
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
struct KonamiVRC1 : Board {
|
||||
KonamiVRC1(Markup::Node& document) : Board(document), vrc1(*this) {
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr & 0x8000) return prgrom.read(vrc1.addrPRG(addr));
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x8000) return vrc1.writeIO(addr, data);
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM(vrc1.addrCIRAM(addr));
|
||||
return Board::readCHR(vrc1.addrCHR(addr));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(vrc1.addrCIRAM(addr), data);
|
||||
return Board::writeCHR(vrc1.addrCHR(addr), data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
vrc1.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
vrc1.serialize(s);
|
||||
}
|
||||
|
||||
VRC1 vrc1;
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
struct KonamiVRC2 : Board {
|
||||
KonamiVRC2(Markup::Node& document) : Board(document), vrc2(*this) {
|
||||
settings.pinout.a0 = 1 << document["board/chip/pinout/a0"].natural();
|
||||
settings.pinout.a1 = 1 << document["board/chip/pinout/a1"].natural();
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr < 0x6000) return cpu.mdr();
|
||||
if(addr < 0x8000) return vrc2.readRAM(addr);
|
||||
return prgrom.read(vrc2.addrPRG(addr));
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr < 0x6000) return;
|
||||
if(addr < 0x8000) return vrc2.writeRAM(addr, data);
|
||||
|
||||
bool a0 = (addr & settings.pinout.a0);
|
||||
bool a1 = (addr & settings.pinout.a1);
|
||||
addr &= 0xfff0;
|
||||
addr |= (a0 << 0) | (a1 << 1);
|
||||
return vrc2.writeIO(addr, data);
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM(vrc2.addrCIRAM(addr));
|
||||
return Board::readCHR(vrc2.addrCHR(addr));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(vrc2.addrCIRAM(addr), data);
|
||||
return Board::writeCHR(vrc2.addrCHR(addr), data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
vrc2.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
vrc2.serialize(s);
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
struct Pinout {
|
||||
uint a0;
|
||||
uint a1;
|
||||
} pinout;
|
||||
} settings;
|
||||
|
||||
VRC2 vrc2;
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
struct KonamiVRC3 : Board {
|
||||
KonamiVRC3(Markup::Node& document) : Board(document), vrc3(*this) {
|
||||
settings.mirror = document["board/mirror/mode"].text() == "vertical" ? 1 : 0;
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
vrc3.main();
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if((addr & 0xe000) == 0x6000) return prgram.read(addr & 0x1fff);
|
||||
if(addr & 0x8000) return prgrom.read(vrc3.addrPRG(addr));
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x6000) return prgram.write(addr & 0x1fff, data);
|
||||
if(addr & 0x8000) return vrc3.writeIO(addr, data);
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.readCIRAM(addr & 0x07ff);
|
||||
}
|
||||
return chrram.read(addr);
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.writeCIRAM(addr & 0x07ff, data);
|
||||
}
|
||||
return chrram.write(addr, data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
vrc3.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
vrc3.serialize(s);
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
bool mirror; //0 = horizontal, 1 = vertical
|
||||
} settings;
|
||||
|
||||
VRC3 vrc3;
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
struct KonamiVRC4 : Board {
|
||||
KonamiVRC4(Markup::Node& document) : Board(document), vrc4(*this) {
|
||||
settings.pinout.a0 = 1 << document["board/chip/pinout/a0"].natural();
|
||||
settings.pinout.a1 = 1 << document["board/chip/pinout/a1"].natural();
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
return vrc4.main();
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr < 0x6000) return cpu.mdr();
|
||||
if(addr < 0x8000) return prgram.read(addr);
|
||||
return prgrom.read(vrc4.addrPRG(addr));
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr < 0x6000) return;
|
||||
if(addr < 0x8000) return prgram.write(addr, data);
|
||||
|
||||
bool a0 = (addr & settings.pinout.a0);
|
||||
bool a1 = (addr & settings.pinout.a1);
|
||||
addr &= 0xfff0;
|
||||
addr |= (a1 << 1) | (a0 << 0);
|
||||
return vrc4.writeIO(addr, data);
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM(vrc4.addrCIRAM(addr));
|
||||
return Board::readCHR(vrc4.addrCHR(addr));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(vrc4.addrCIRAM(addr), data);
|
||||
return Board::writeCHR(vrc4.addrCHR(addr), data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
vrc4.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
vrc4.serialize(s);
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
struct Pinout {
|
||||
uint a0;
|
||||
uint a1;
|
||||
} pinout;
|
||||
} settings;
|
||||
|
||||
VRC4 vrc4;
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
struct KonamiVRC6 : Board {
|
||||
KonamiVRC6(Markup::Node& document) : Board(document), vrc6(*this) {
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8{
|
||||
if((addr & 0xe000) == 0x6000) return vrc6.readRAM(addr);
|
||||
if(addr & 0x8000) return prgrom.read(vrc6.addrPRG(addr));
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x6000) return vrc6.writeRAM(addr, data);
|
||||
if(addr & 0x8000) {
|
||||
addr = (addr & 0xf003);
|
||||
if(prgram.size) addr = (addr & ~3) | ((addr & 2) >> 1) | ((addr & 1) << 1);
|
||||
return vrc6.writeIO(addr, data);
|
||||
}
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM(vrc6.addrCIRAM(addr));
|
||||
return Board::readCHR(vrc6.addrCHR(addr));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(vrc6.addrCIRAM(addr), data);
|
||||
return Board::writeCHR(vrc6.addrCHR(addr), data);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
vrc6.serialize(s);
|
||||
}
|
||||
|
||||
auto main() -> void { vrc6.main(); }
|
||||
auto power() -> void { vrc6.power(); }
|
||||
|
||||
VRC6 vrc6;
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
struct KonamiVRC7 : Board {
|
||||
KonamiVRC7(Markup::Node& document) : Board(document), vrc7(*this) {
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
return vrc7.main();
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr < 0x6000) return cpu.mdr();
|
||||
if(addr < 0x8000) return prgram.read(addr);
|
||||
return prgrom.read(vrc7.addrPRG(addr));
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr < 0x6000) return;
|
||||
if(addr < 0x8000) return prgram.write(addr, data);
|
||||
return vrc7.writeIO(addr, data);
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM(vrc7.addrCIRAM(addr));
|
||||
return chrram.read(vrc7.addrCHR(addr));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(vrc7.addrCIRAM(addr), data);
|
||||
return chrram.write(vrc7.addrCHR(addr), data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
vrc7.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
vrc7.serialize(s);
|
||||
}
|
||||
|
||||
VRC7 vrc7;
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
//NES-AMROM
|
||||
//NES-ANROM
|
||||
//NES-AN1ROM
|
||||
//NES-AOROM
|
||||
|
||||
struct NES_AxROM : Board {
|
||||
NES_AxROM(Markup::Node& document) : Board(document) {
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr & 0x8000) return prgrom.read((prgBank << 15) | (addr & 0x7fff));
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x8000) {
|
||||
prgBank = data & 0x0f;
|
||||
mirrorSelect = data & 0x10;
|
||||
}
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM((mirrorSelect << 10) | (addr & 0x03ff));
|
||||
return Board::readCHR(addr);
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM((mirrorSelect << 10) | (addr & 0x03ff), data);
|
||||
return Board::writeCHR(addr, data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
prgBank = 0x0f;
|
||||
mirrorSelect = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
|
||||
s.integer(prgBank);
|
||||
s.integer(mirrorSelect);
|
||||
}
|
||||
|
||||
uint4 prgBank;
|
||||
bool mirrorSelect;
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
//NES-BN-ROM-01
|
||||
|
||||
struct NES_BNROM : Board {
|
||||
NES_BNROM(Markup::Node& document) : Board(document) {
|
||||
settings.mirror = document["board/mirror/mode"].text() == "vertical" ? 1 : 0;
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr & 0x8000) return prgrom.read((prgBank << 15) | (addr & 0x7fff));
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x8000) prgBank = data & 0x03;
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.readCIRAM(addr);
|
||||
}
|
||||
return Board::readCHR(addr);
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.writeCIRAM(addr, data);
|
||||
}
|
||||
return Board::writeCHR(addr, data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
prgBank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
s.integer(prgBank);
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
bool mirror; //0 = horizontal, 1 = vertical
|
||||
} settings;
|
||||
|
||||
uint2 prgBank;
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
//NES-CNROM
|
||||
|
||||
struct NES_CNROM : Board {
|
||||
NES_CNROM(Markup::Node& document) : Board(document) {
|
||||
settings.mirror = document["board/mirror/mode"].text() == "vertical" ? 1 : 0;
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr & 0x8000) return prgrom.read(addr & 0x7fff);
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x8000) chrBank = data & 0x03;
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.readCIRAM(addr & 0x07ff);
|
||||
}
|
||||
addr = (chrBank * 0x2000) + (addr & 0x1fff);
|
||||
return Board::readCHR(addr);
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.writeCIRAM(addr & 0x07ff, data);
|
||||
}
|
||||
addr = (chrBank * 0x2000) + (addr & 0x1fff);
|
||||
Board::writeCHR(addr, data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
chrBank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
s.integer(chrBank);
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
bool mirror; //0 = horizontal, 1 = vertical
|
||||
} settings;
|
||||
|
||||
uint2 chrBank;
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
struct NES_ExROM : Board {
|
||||
NES_ExROM(Markup::Node& document) : Board(document), mmc5(*this) {
|
||||
revision = Revision::ELROM;
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
mmc5.main();
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
return mmc5.readPRG(addr);
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
mmc5.writePRG(addr, data);
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
return mmc5.readCHR(addr);
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
mmc5.writeCHR(addr, data);
|
||||
}
|
||||
|
||||
auto scanline(uint y) -> void {
|
||||
mmc5.scanline(y);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
mmc5.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
mmc5.serialize(s);
|
||||
}
|
||||
|
||||
enum class Revision : uint {
|
||||
EKROM,
|
||||
ELROM,
|
||||
ETROM,
|
||||
EWROM,
|
||||
} revision;
|
||||
|
||||
MMC5 mmc5;
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
//MMC4
|
||||
|
||||
struct NES_FxROM : Board {
|
||||
NES_FxROM(Markup::Node& document) : Board(document) {
|
||||
revision = Revision::FKROM;
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr < 0x6000) return cpu.mdr();
|
||||
if(addr < 0x8000) return prgram.read(addr);
|
||||
uint bank = addr < 0xc000 ? prgBank : (uint4)0x0f;
|
||||
return prgrom.read((bank * 0x4000) | (addr & 0x3fff));
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr < 0x6000) return;
|
||||
if(addr < 0x8000) return prgram.write(addr, data);
|
||||
|
||||
switch(addr & 0xf000) {
|
||||
case 0xa000: prgBank = data & 0x0f; break;
|
||||
case 0xb000: chrBank[0][0] = data & 0x1f; break;
|
||||
case 0xc000: chrBank[0][1] = data & 0x1f; break;
|
||||
case 0xd000: chrBank[1][0] = data & 0x1f; break;
|
||||
case 0xe000: chrBank[1][1] = data & 0x1f; break;
|
||||
case 0xf000: mirror = data & 0x01; break;
|
||||
}
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) const -> uint {
|
||||
switch(mirror) {
|
||||
case 0: return ((addr & 0x0400) >> 0) | (addr & 0x03ff); //vertical mirroring
|
||||
case 1: return ((addr & 0x0800) >> 1) | (addr & 0x03ff); //horizontal mirroring
|
||||
}
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM(addrCIRAM(addr));
|
||||
bool region = addr & 0x1000;
|
||||
uint bank = chrBank[region][latch[region]];
|
||||
if((addr & 0x0ff8) == 0x0fd8) latch[region] = 0;
|
||||
if((addr & 0x0ff8) == 0x0fe8) latch[region] = 1;
|
||||
return Board::readCHR((bank * 0x1000) | (addr & 0x0fff));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(addrCIRAM(addr), data);
|
||||
bool region = addr & 0x1000;
|
||||
uint bank = chrBank[region][latch[region]];
|
||||
if((addr & 0x0ff8) == 0x0fd8) latch[region] = 0;
|
||||
if((addr & 0x0ff8) == 0x0fe8) latch[region] = 1;
|
||||
return Board::writeCHR((bank * 0x1000) | (addr & 0x0fff), data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
prgBank = 0;
|
||||
chrBank[0][0] = 0;
|
||||
chrBank[0][1] = 0;
|
||||
chrBank[1][0] = 0;
|
||||
chrBank[1][1] = 0;
|
||||
mirror = 0;
|
||||
latch[0] = 0;
|
||||
latch[1] = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
|
||||
s.integer(prgBank);
|
||||
s.integer(chrBank[0][0]);
|
||||
s.integer(chrBank[0][1]);
|
||||
s.integer(chrBank[1][0]);
|
||||
s.integer(chrBank[1][1]);
|
||||
s.integer(mirror);
|
||||
s.array(latch);
|
||||
}
|
||||
|
||||
enum Revision : uint {
|
||||
FJROM,
|
||||
FKROM,
|
||||
} revision;
|
||||
|
||||
uint4 prgBank;
|
||||
uint5 chrBank[2][2];
|
||||
bool mirror;
|
||||
bool latch[2];
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
//NES-GNROM
|
||||
//NES-MHROM
|
||||
|
||||
struct NES_GxROM : Board {
|
||||
NES_GxROM(Markup::Node& document) : Board(document) {
|
||||
settings.mirror = document["board/mirror/mode"].text() == "vertical" ? 1 : 0;
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr & 0x8000) return prgrom.read((prgBank << 15) | (addr & 0x7fff));
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x8000) {
|
||||
prgBank = (data & 0x30) >> 4;
|
||||
chrBank = (data & 0x03) >> 0;
|
||||
}
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.readCIRAM(addr & 0x07ff);
|
||||
}
|
||||
addr = (chrBank * 0x2000) + (addr & 0x1fff);
|
||||
return Board::readCHR(addr);
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.writeCIRAM(addr & 0x07ff, data);
|
||||
}
|
||||
addr = (chrBank * 0x2000) + (addr & 0x1fff);
|
||||
Board::writeCHR(addr, data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
prgBank = 0;
|
||||
chrBank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
s.integer(prgBank);
|
||||
s.integer(chrBank);
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
bool mirror; //0 = horizontal, 1 = vertical
|
||||
} settings;
|
||||
|
||||
uint2 prgBank;
|
||||
uint2 chrBank;
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
struct NES_HKROM : Board {
|
||||
NES_HKROM(Markup::Node& document) : Board(document), mmc6(*this) {
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
mmc6.main();
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if((addr & 0xf000) == 0x7000) return mmc6.readRAM(addr);
|
||||
if(addr & 0x8000) return prgrom.read(mmc6.addrPRG(addr));
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if((addr & 0xf000) == 0x7000) return mmc6.writeRAM(addr, data);
|
||||
if(addr & 0x8000) return mmc6.writeIO(addr, data);
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
mmc6.irqTest(addr);
|
||||
if(addr & 0x2000) return ppu.readCIRAM(mmc6.addrCIRAM(addr));
|
||||
return Board::readCHR(mmc6.addrCHR(addr));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
mmc6.irqTest(addr);
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(mmc6.addrCIRAM(addr), data);
|
||||
return Board::writeCHR(mmc6.addrCHR(addr), data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
mmc6.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
mmc6.serialize(s);
|
||||
}
|
||||
|
||||
MMC6 mmc6;
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
//NES-NROM-128
|
||||
//NES-NROM-256
|
||||
|
||||
struct NES_NROM : Board {
|
||||
NES_NROM(Markup::Node& document) : Board(document) {
|
||||
settings.mirror = document["board/mirror/mode"].text() == "vertical" ? 1 : 0;
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr & 0x8000) return prgrom.read(addr);
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.readCIRAM(addr & 0x07ff);
|
||||
}
|
||||
if(chrram.size) return chrram.read(addr);
|
||||
return chrrom.read(addr);
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.writeCIRAM(addr & 0x07ff, data);
|
||||
}
|
||||
if(chrram.size) return chrram.write(addr, data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
bool mirror; //0 = horizontal, 1 = vertical
|
||||
} settings;
|
||||
};
|
||||
@@ -1,92 +0,0 @@
|
||||
//MMC2
|
||||
|
||||
struct NES_PxROM : Board {
|
||||
NES_PxROM(Markup::Node& document) : Board(document) {
|
||||
revision = Revision::PNROM;
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr < 0x6000) return cpu.mdr();
|
||||
if(addr < 0x8000) return prgram.read(addr);
|
||||
uint bank = 0;
|
||||
switch((addr / 0x2000) & 3) {
|
||||
case 0: bank = prgBank; break;
|
||||
case 1: bank = 0x0d; break;
|
||||
case 2: bank = 0x0e; break;
|
||||
case 3: bank = 0x0f; break;
|
||||
}
|
||||
return prgrom.read((bank * 0x2000) | (addr & 0x1fff));
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr < 0x6000) return;
|
||||
if(addr < 0x8000) return prgram.write(addr, data);
|
||||
|
||||
switch(addr & 0xf000) {
|
||||
case 0xa000: prgBank = data & 0x0f; break;
|
||||
case 0xb000: chrBank[0][0] = data & 0x1f; break;
|
||||
case 0xc000: chrBank[0][1] = data & 0x1f; break;
|
||||
case 0xd000: chrBank[1][0] = data & 0x1f; break;
|
||||
case 0xe000: chrBank[1][1] = data & 0x1f; break;
|
||||
case 0xf000: mirror = data & 0x01; break;
|
||||
}
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) const -> uint {
|
||||
switch(mirror) {
|
||||
case 0: return ((addr & 0x0400) >> 0) | (addr & 0x03ff); //vertical mirroring
|
||||
case 1: return ((addr & 0x0800) >> 1) | (addr & 0x03ff); //horizontal mirroring
|
||||
}
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM(addrCIRAM(addr));
|
||||
bool region = addr & 0x1000;
|
||||
uint bank = chrBank[region][latch[region]];
|
||||
if((addr & 0x0ff8) == 0x0fd8) latch[region] = 0;
|
||||
if((addr & 0x0ff8) == 0x0fe8) latch[region] = 1;
|
||||
return Board::readCHR((bank * 0x1000) | (addr & 0x0fff));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(addrCIRAM(addr), data);
|
||||
bool region = addr & 0x1000;
|
||||
uint bank = chrBank[region][latch[region]];
|
||||
if((addr & 0x0ff8) == 0x0fd8) latch[region] = 0;
|
||||
if((addr & 0x0ff8) == 0x0fe8) latch[region] = 1;
|
||||
return Board::writeCHR((bank * 0x1000) | (addr & 0x0fff), data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
prgBank = 0;
|
||||
chrBank[0][0] = 0;
|
||||
chrBank[0][1] = 0;
|
||||
chrBank[1][0] = 0;
|
||||
chrBank[1][1] = 0;
|
||||
mirror = 0;
|
||||
latch[0] = 0;
|
||||
latch[1] = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
|
||||
s.integer(prgBank);
|
||||
s.integer(chrBank[0][0]);
|
||||
s.integer(chrBank[0][1]);
|
||||
s.integer(chrBank[1][0]);
|
||||
s.integer(chrBank[1][1]);
|
||||
s.integer(mirror);
|
||||
s.array(latch);
|
||||
}
|
||||
|
||||
enum Revision : uint {
|
||||
PEEOROM,
|
||||
PNROM,
|
||||
} revision;
|
||||
|
||||
uint4 prgBank;
|
||||
uint5 chrBank[2][2];
|
||||
bool mirror;
|
||||
bool latch[2];
|
||||
};
|
||||
@@ -1,95 +0,0 @@
|
||||
struct NES_SxROM : Board {
|
||||
NES_SxROM(Markup::Node& document) : Board(document), mmc1(*this) {
|
||||
revision = Revision::SXROM;
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
return mmc1.main();
|
||||
}
|
||||
|
||||
auto addrRAM(uint addr) -> uint {
|
||||
uint bank = 0;
|
||||
if(revision == Revision::SOROM) bank = (mmc1.chrBank[0] & 0x08) >> 3;
|
||||
if(revision == Revision::SUROM) bank = (mmc1.chrBank[0] & 0x0c) >> 2;
|
||||
if(revision == Revision::SXROM) bank = (mmc1.chrBank[0] & 0x0c) >> 2;
|
||||
return (bank << 13) | (addr & 0x1fff);
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if((addr & 0xe000) == 0x6000) {
|
||||
if(revision == Revision::SNROM) {
|
||||
if(mmc1.chrBank[0] & 0x10) return cpu.mdr();
|
||||
}
|
||||
if(mmc1.ramDisable) return 0x00;
|
||||
return prgram.read(addrRAM(addr));
|
||||
}
|
||||
|
||||
if(addr & 0x8000) {
|
||||
addr = mmc1.addrPRG(addr);
|
||||
if(revision == Revision::SXROM) {
|
||||
addr |= ((mmc1.chrBank[0] & 0x10) >> 4) << 18;
|
||||
}
|
||||
return prgrom.read(addr);
|
||||
}
|
||||
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x6000) {
|
||||
if(revision == Revision::SNROM) {
|
||||
if(mmc1.chrBank[0] & 0x10) return;
|
||||
}
|
||||
if(mmc1.ramDisable) return;
|
||||
return prgram.write(addrRAM(addr), data);
|
||||
}
|
||||
|
||||
if(addr & 0x8000) return mmc1.writeIO(addr, data);
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM(mmc1.addrCIRAM(addr));
|
||||
return Board::readCHR(mmc1.addrCHR(addr));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(mmc1.addrCIRAM(addr), data);
|
||||
return Board::writeCHR(mmc1.addrCHR(addr), data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
mmc1.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
mmc1.serialize(s);
|
||||
}
|
||||
|
||||
enum class Revision : uint {
|
||||
SAROM,
|
||||
SBROM,
|
||||
SCROM,
|
||||
SC1ROM,
|
||||
SEROM,
|
||||
SFROM,
|
||||
SGROM,
|
||||
SHROM,
|
||||
SH1ROM,
|
||||
SIROM,
|
||||
SJROM,
|
||||
SKROM,
|
||||
SLROM,
|
||||
SL1ROM,
|
||||
SL2ROM,
|
||||
SL3ROM,
|
||||
SLRROM,
|
||||
SMROM,
|
||||
SNROM,
|
||||
SOROM,
|
||||
SUROM,
|
||||
SXROM,
|
||||
} revision;
|
||||
|
||||
MMC1 mmc1;
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
struct NES_TxROM : Board {
|
||||
NES_TxROM(Markup::Node& document) : Board(document), mmc3(*this) {
|
||||
revision = Revision::TLROM;
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
mmc3.main();
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if((addr & 0xe000) == 0x6000) return mmc3.readRAM(addr);
|
||||
if(addr & 0x8000) return prgrom.read(mmc3.addrPRG(addr));
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x6000) return mmc3.writeRAM(addr, data);
|
||||
if(addr & 0x8000) return mmc3.writeIO(addr, data);
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
mmc3.irqTest(addr);
|
||||
if(addr & 0x2000) return ppu.readCIRAM(mmc3.addrCIRAM(addr));
|
||||
return Board::readCHR(mmc3.addrCHR(addr));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
mmc3.irqTest(addr);
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(mmc3.addrCIRAM(addr), data);
|
||||
return Board::writeCHR(mmc3.addrCHR(addr), data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
mmc3.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
mmc3.serialize(s);
|
||||
}
|
||||
|
||||
enum class Revision : uint {
|
||||
TBROM,
|
||||
TEROM,
|
||||
TFROM,
|
||||
TGROM,
|
||||
TKROM,
|
||||
TKSROM,
|
||||
TLROM,
|
||||
TL1ROM,
|
||||
TL2ROM,
|
||||
TLSROM,
|
||||
TNROM,
|
||||
TQROM,
|
||||
TR1ROM,
|
||||
TSROM,
|
||||
TVROM,
|
||||
} revision;
|
||||
|
||||
MMC3 mmc3;
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
//NES-UNROM
|
||||
//NES-UOROM
|
||||
|
||||
struct NES_UxROM : Board {
|
||||
NES_UxROM(Markup::Node& document) : Board(document) {
|
||||
settings.mirror = document["board/mirror/mode"].text() == "vertical" ? 1 : 0;
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if((addr & 0xc000) == 0x8000) return prgrom.read((prgBank << 14) | (addr & 0x3fff));
|
||||
if((addr & 0xc000) == 0xc000) return prgrom.read(( 0x0f << 14) | (addr & 0x3fff));
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x8000) prgBank = data & 0x0f;
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.readCIRAM(addr);
|
||||
}
|
||||
return Board::readCHR(addr);
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) {
|
||||
if(settings.mirror == 0) addr = ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
return ppu.writeCIRAM(addr, data);
|
||||
}
|
||||
return Board::writeCHR(addr, data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
prgBank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
|
||||
s.integer(prgBank);
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
bool mirror; //0 = horizontal, 1 = vertical
|
||||
} settings;
|
||||
|
||||
uint4 prgBank;
|
||||
};
|
||||
@@ -1,216 +0,0 @@
|
||||
//SUNSOFT-5B
|
||||
|
||||
struct Sunsoft5B : Board {
|
||||
Sunsoft5B(Markup::Node& document) : Board(document) {
|
||||
}
|
||||
|
||||
struct Pulse {
|
||||
auto clock() -> void {
|
||||
if(--counter == 0) {
|
||||
counter = frequency << 4;
|
||||
duty ^= 1;
|
||||
}
|
||||
output = duty ? volume : (uint4)0;
|
||||
if(disable) output = 0;
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
disable = 1;
|
||||
frequency = 1;
|
||||
volume = 0;
|
||||
|
||||
counter = 0;
|
||||
duty = 0;
|
||||
output = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
s.integer(disable);
|
||||
s.integer(frequency);
|
||||
s.integer(volume);
|
||||
|
||||
s.integer(counter);
|
||||
s.integer(duty);
|
||||
s.integer(output);
|
||||
}
|
||||
|
||||
bool disable;
|
||||
uint12 frequency;
|
||||
uint4 volume;
|
||||
|
||||
uint16 counter; //12-bit countdown + 4-bit phase
|
||||
uint1 duty;
|
||||
uint4 output;
|
||||
} pulse[3];
|
||||
|
||||
auto main() -> void {
|
||||
if(irqCounterEnable) {
|
||||
if(--irqCounter == 0xffff) {
|
||||
cpu.irqLine(irqEnable);
|
||||
}
|
||||
}
|
||||
|
||||
pulse[0].clock();
|
||||
pulse[1].clock();
|
||||
pulse[2].clock();
|
||||
int16 output = dac[pulse[0].output] + dac[pulse[1].output] + dac[pulse[2].output];
|
||||
apu.setSample(-output);
|
||||
|
||||
tick();
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if(addr < 0x6000) return cpu.mdr();
|
||||
|
||||
uint8 bank = 0x3f; //((addr & 0xe000) == 0xe000
|
||||
if((addr & 0xe000) == 0x6000) bank = prgBank[0];
|
||||
if((addr & 0xe000) == 0x8000) bank = prgBank[1];
|
||||
if((addr & 0xe000) == 0xa000) bank = prgBank[2];
|
||||
if((addr & 0xe000) == 0xc000) bank = prgBank[3];
|
||||
|
||||
bool ramEnable = bank & 0x80;
|
||||
bool ramSelect = bank & 0x40;
|
||||
bank &= 0x3f;
|
||||
|
||||
if(ramSelect) {
|
||||
if(!ramEnable) return cpu.mdr();
|
||||
return prgram.data[addr & 0x1fff];
|
||||
}
|
||||
|
||||
addr = (bank << 13) | (addr & 0x1fff);
|
||||
return prgrom.read(addr);
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x6000) {
|
||||
prgram.data[addr & 0x1fff] = data;
|
||||
}
|
||||
|
||||
if(addr == 0x8000) {
|
||||
mmuPort = data & 0x0f;
|
||||
}
|
||||
|
||||
if(addr == 0xa000) {
|
||||
switch(mmuPort) {
|
||||
case 0: chrBank[0] = data; break;
|
||||
case 1: chrBank[1] = data; break;
|
||||
case 2: chrBank[2] = data; break;
|
||||
case 3: chrBank[3] = data; break;
|
||||
case 4: chrBank[4] = data; break;
|
||||
case 5: chrBank[5] = data; break;
|
||||
case 6: chrBank[6] = data; break;
|
||||
case 7: chrBank[7] = data; break;
|
||||
case 8: prgBank[0] = data; break;
|
||||
case 9: prgBank[1] = data; break;
|
||||
case 10: prgBank[2] = data; break;
|
||||
case 11: prgBank[3] = data; break;
|
||||
case 12: mirror = data & 3; break;
|
||||
case 13:
|
||||
irqEnable = data & 0x80;
|
||||
irqCounterEnable = data & 0x01;
|
||||
if(irqEnable == 0) cpu.irqLine(0);
|
||||
break;
|
||||
case 14: irqCounter = (irqCounter & 0xff00) | (data << 0); break;
|
||||
case 15: irqCounter = (irqCounter & 0x00ff) | (data << 8); break;
|
||||
}
|
||||
}
|
||||
|
||||
if(addr == 0xc000) {
|
||||
apuPort = data & 0x0f;
|
||||
}
|
||||
|
||||
if(addr == 0xe000) {
|
||||
switch(apuPort) {
|
||||
case 0: pulse[0].frequency = (pulse[0].frequency & 0xff00) | (data << 0); break;
|
||||
case 1: pulse[0].frequency = (pulse[0].frequency & 0x00ff) | (data << 8); break;
|
||||
case 2: pulse[1].frequency = (pulse[1].frequency & 0xff00) | (data << 0); break;
|
||||
case 3: pulse[1].frequency = (pulse[1].frequency & 0x00ff) | (data << 8); break;
|
||||
case 4: pulse[2].frequency = (pulse[2].frequency & 0xff00) | (data << 0); break;
|
||||
case 5: pulse[2].frequency = (pulse[2].frequency & 0x00ff) | (data << 8); break;
|
||||
case 7:
|
||||
pulse[0].disable = data & 0x01;
|
||||
pulse[1].disable = data & 0x02;
|
||||
pulse[2].disable = data & 0x04;
|
||||
break;
|
||||
case 8: pulse[0].volume = data & 0x0f; break;
|
||||
case 9: pulse[1].volume = data & 0x0f; break;
|
||||
case 10: pulse[2].volume = data & 0x0f; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto addrCHR(uint addr) -> uint {
|
||||
uint8 bank = (addr >> 10) & 7;
|
||||
return (chrBank[bank] << 10) | (addr & 0x03ff);
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) -> uint {
|
||||
switch(mirror) {
|
||||
case 0: return ((addr & 0x0400) >> 0) | (addr & 0x03ff); //vertical
|
||||
case 1: return ((addr & 0x0800) >> 1) | (addr & 0x03ff); //horizontal
|
||||
case 2: return 0x0000 | (addr & 0x03ff); //first
|
||||
case 3: return 0x0400 | (addr & 0x03ff); //second
|
||||
}
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
if(addr & 0x2000) return ppu.readCIRAM(addrCIRAM(addr));
|
||||
return Board::readCHR(addrCHR(addr));
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) return ppu.writeCIRAM(addrCIRAM(addr), data);
|
||||
return Board::writeCHR(addrCHR(addr), data);
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
for(int n : range(16)) {
|
||||
double volume = 1.0 / pow(2, 1.0 / 2 * (15 - n));
|
||||
dac[n] = volume * 8192.0;
|
||||
}
|
||||
|
||||
mmuPort = 0;
|
||||
apuPort = 0;
|
||||
|
||||
for(auto& n : prgBank) n = 0;
|
||||
for(auto& n : chrBank) n = 0;
|
||||
mirror = 0;
|
||||
irqEnable = 0;
|
||||
irqCounterEnable = 0;
|
||||
irqCounter = 0;
|
||||
|
||||
pulse[0].power();
|
||||
pulse[1].power();
|
||||
pulse[2].power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
Board::serialize(s);
|
||||
|
||||
s.integer(mmuPort);
|
||||
s.integer(apuPort);
|
||||
|
||||
s.array(prgBank);
|
||||
s.array(chrBank);
|
||||
s.integer(mirror);
|
||||
s.integer(irqEnable);
|
||||
s.integer(irqCounterEnable);
|
||||
s.integer(irqCounter);
|
||||
|
||||
pulse[0].serialize(s);
|
||||
pulse[1].serialize(s);
|
||||
pulse[2].serialize(s);
|
||||
}
|
||||
|
||||
uint4 mmuPort;
|
||||
uint4 apuPort;
|
||||
|
||||
uint8 prgBank[4];
|
||||
uint8 chrBank[8];
|
||||
uint2 mirror;
|
||||
bool irqEnable;
|
||||
bool irqCounterEnable;
|
||||
uint16 irqCounter;
|
||||
|
||||
int16 dac[16];
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
#include <fc/fc.hpp>
|
||||
|
||||
namespace Famicom {
|
||||
|
||||
#include "chip/chip.cpp"
|
||||
#include "board/board.cpp"
|
||||
#include "serialization.cpp"
|
||||
Cartridge cartridge;
|
||||
|
||||
auto Cartridge::Enter() -> void {
|
||||
while(true) scheduler.synchronize(), cartridge.main();
|
||||
}
|
||||
|
||||
auto Cartridge::main() -> void {
|
||||
board->main();
|
||||
}
|
||||
|
||||
auto Cartridge::load() -> bool {
|
||||
if(auto loaded = platform->load(ID::Famicom, "Famicom", "fc", {"NTSC-J", "NTSC-U", "PAL"})) {
|
||||
information.pathID = loaded.pathID();
|
||||
information.region = loaded.option();
|
||||
} else return false;
|
||||
|
||||
if(auto fp = platform->open(pathID(), "manifest.bml", File::Read, File::Required)) {
|
||||
information.manifest = fp->reads();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
Board::load(information.manifest); //this call will set Cartridge::board if successful
|
||||
if(!board) return false;
|
||||
|
||||
Hash::SHA256 sha;
|
||||
sha.input(board->prgrom.data, board->prgrom.size);
|
||||
sha.input(board->chrrom.data, board->chrrom.size);
|
||||
information.sha256 = sha.digest();
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Cartridge::save() -> void {
|
||||
board->save();
|
||||
}
|
||||
|
||||
auto Cartridge::unload() -> void {
|
||||
delete board;
|
||||
board = nullptr;
|
||||
}
|
||||
|
||||
auto Cartridge::power() -> void {
|
||||
create(Cartridge::Enter, system.frequency());
|
||||
board->power();
|
||||
}
|
||||
|
||||
auto Cartridge::readPRG(uint addr) -> uint8 {
|
||||
return board->readPRG(addr);
|
||||
}
|
||||
|
||||
auto Cartridge::writePRG(uint addr, uint8 data) -> void {
|
||||
return board->writePRG(addr, data);
|
||||
}
|
||||
|
||||
auto Cartridge::readCHR(uint addr) -> uint8 {
|
||||
return board->readCHR(addr);
|
||||
}
|
||||
|
||||
auto Cartridge::writeCHR(uint addr, uint8 data) -> void {
|
||||
return board->writeCHR(addr, data);
|
||||
}
|
||||
|
||||
auto Cartridge::scanline(uint y) -> void {
|
||||
return board->scanline(y);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
#include "chip/chip.hpp"
|
||||
#include "board/board.hpp"
|
||||
|
||||
struct Cartridge : Thread {
|
||||
inline auto rate() const -> uint { return Region::PAL() ? 16 : 12; }
|
||||
|
||||
//cartridge.cpp
|
||||
static auto Enter() -> void;
|
||||
auto main() -> void;
|
||||
|
||||
auto pathID() const -> uint { return information.pathID; }
|
||||
auto region() const -> string { return information.region; }
|
||||
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;
|
||||
|
||||
//serialization.cpp
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct Information {
|
||||
uint pathID = 0;
|
||||
string region;
|
||||
string sha256;
|
||||
string manifest;
|
||||
string title;
|
||||
} information;
|
||||
|
||||
//privileged:
|
||||
Board* board = nullptr;
|
||||
|
||||
auto readPRG(uint addr) -> uint8;
|
||||
auto writePRG(uint addr, uint8 data) -> void;
|
||||
|
||||
auto readCHR(uint addr) -> uint8;
|
||||
auto writeCHR(uint addr, uint8 data) -> void;
|
||||
|
||||
//scanline() is for debugging purposes only:
|
||||
//boards must detect scanline edges on their own
|
||||
auto scanline(uint y) -> void;
|
||||
};
|
||||
|
||||
extern Cartridge cartridge;
|
||||
@@ -1,17 +0,0 @@
|
||||
#include "mmc1.cpp"
|
||||
#include "mmc3.cpp"
|
||||
#include "mmc5.cpp"
|
||||
#include "mmc6.cpp"
|
||||
#include "vrc1.cpp"
|
||||
#include "vrc2.cpp"
|
||||
#include "vrc3.cpp"
|
||||
#include "vrc4.cpp"
|
||||
#include "vrc6.cpp"
|
||||
#include "vrc7.cpp"
|
||||
|
||||
Chip::Chip(Board& board) : board(board) {
|
||||
}
|
||||
|
||||
auto Chip::tick() -> void {
|
||||
board.tick();
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
struct Board;
|
||||
|
||||
struct Chip {
|
||||
Chip(Board& board);
|
||||
auto tick() -> void;
|
||||
|
||||
Board& board;
|
||||
};
|
||||
@@ -1,125 +0,0 @@
|
||||
struct MMC1 : Chip {
|
||||
MMC1(Board& board) : Chip(board) {
|
||||
revision = Revision::MMC1B2;
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
if(writedelay) writedelay--;
|
||||
tick();
|
||||
}
|
||||
|
||||
auto addrPRG(uint addr) -> uint {
|
||||
bool region = addr & 0x4000;
|
||||
uint bank = (prgBank & ~1) + region;
|
||||
|
||||
if(prgSize) {
|
||||
bank = (region == 0 ? 0x0 : 0xf);
|
||||
if(region != prgMode) bank = prgBank;
|
||||
}
|
||||
|
||||
return (bank << 14) | (addr & 0x3fff);
|
||||
}
|
||||
|
||||
auto addrCHR(uint addr) -> uint {
|
||||
bool region = addr & 0x1000;
|
||||
uint bank = chrBank[region];
|
||||
if(chrMode == 0) bank = (chrBank[0] & ~1) | region;
|
||||
return (bank << 12) | (addr & 0x0fff);
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) -> uint {
|
||||
switch(mirror) {
|
||||
case 0: return 0x0000 | (addr & 0x03ff);
|
||||
case 1: return 0x0400 | (addr & 0x03ff);
|
||||
case 2: return ((addr & 0x0400) >> 0) | (addr & 0x03ff);
|
||||
case 3: return ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
}
|
||||
}
|
||||
|
||||
auto writeIO(uint addr, uint8 data) -> void {
|
||||
if(writedelay) return;
|
||||
writedelay = 2;
|
||||
|
||||
if(data & 0x80) {
|
||||
shiftaddr = 0;
|
||||
prgSize = 1;
|
||||
prgMode = 1;
|
||||
} else {
|
||||
shiftdata = ((data & 1) << 4) | (shiftdata >> 1);
|
||||
if(++shiftaddr == 5) {
|
||||
shiftaddr = 0;
|
||||
switch((addr >> 13) & 3) {
|
||||
case 0:
|
||||
chrMode = (shiftdata & 0x10);
|
||||
prgSize = (shiftdata & 0x08);
|
||||
prgMode = (shiftdata & 0x04);
|
||||
mirror = (shiftdata & 0x03);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
chrBank[0] = (shiftdata & 0x1f);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
chrBank[1] = (shiftdata & 0x1f);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
ramDisable = (shiftdata & 0x10);
|
||||
prgBank = (shiftdata & 0x0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
writedelay = 0;
|
||||
shiftaddr = 0;
|
||||
shiftdata = 0;
|
||||
|
||||
chrMode = 0;
|
||||
prgSize = 1;
|
||||
prgMode = 1;
|
||||
mirror = 0;
|
||||
chrBank[0] = 0;
|
||||
chrBank[1] = 1;
|
||||
ramDisable = 0;
|
||||
prgBank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
s.integer(writedelay);
|
||||
s.integer(shiftaddr);
|
||||
s.integer(shiftdata);
|
||||
|
||||
s.integer(chrMode);
|
||||
s.integer(prgSize);
|
||||
s.integer(prgMode);
|
||||
s.integer(mirror);
|
||||
s.array(chrBank);
|
||||
s.integer(ramDisable);
|
||||
s.integer(prgBank);
|
||||
}
|
||||
|
||||
enum class Revision : uint {
|
||||
MMC1,
|
||||
MMC1A,
|
||||
MMC1B1,
|
||||
MMC1B2,
|
||||
MMC1B3,
|
||||
MMC1C,
|
||||
} revision;
|
||||
|
||||
uint writedelay;
|
||||
uint shiftaddr;
|
||||
uint shiftdata;
|
||||
|
||||
bool chrMode;
|
||||
bool prgSize; //0 = 32K, 1 = 16K
|
||||
bool prgMode;
|
||||
uint2 mirror; //0 = first, 1 = second, 2 = vertical, 3 = horizontal
|
||||
uint5 chrBank[2];
|
||||
bool ramDisable;
|
||||
uint4 prgBank;
|
||||
};
|
||||
@@ -1,178 +0,0 @@
|
||||
struct MMC3 : Chip {
|
||||
MMC3(Board& board) : Chip(board) {
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
if(irqDelay) irqDelay--;
|
||||
cpu.irqLine(irqLine);
|
||||
tick();
|
||||
}
|
||||
|
||||
auto irqTest(uint addr) -> void {
|
||||
if(!(chrAbus & 0x1000) && (addr & 0x1000)) {
|
||||
if(irqDelay == 0) {
|
||||
if(irqCounter == 0) {
|
||||
irqCounter = irqLatch;
|
||||
} else if(--irqCounter == 0) {
|
||||
if(irqEnable) irqLine = 1;
|
||||
}
|
||||
}
|
||||
irqDelay = 6;
|
||||
}
|
||||
chrAbus = addr;
|
||||
}
|
||||
|
||||
auto addrPRG(uint addr) const -> uint {
|
||||
switch((addr >> 13) & 3) {
|
||||
case 0:
|
||||
if(prgMode == 1) return (0x3e << 13) | (addr & 0x1fff);
|
||||
return (prgBank[0] << 13) | (addr & 0x1fff);
|
||||
case 1:
|
||||
return (prgBank[1] << 13) | (addr & 0x1fff);
|
||||
case 2:
|
||||
if(prgMode == 0) return (0x3e << 13) | (addr & 0x1fff);
|
||||
return (prgBank[0] << 13) | (addr & 0x1fff);
|
||||
case 3:
|
||||
return (0x3f << 13) | (addr & 0x1fff);
|
||||
}
|
||||
}
|
||||
|
||||
auto addrCHR(uint addr) const -> uint {
|
||||
if(chrMode == 0) {
|
||||
if(addr <= 0x07ff) return (chrBank[0] << 10) | (addr & 0x07ff);
|
||||
if(addr <= 0x0fff) return (chrBank[1] << 10) | (addr & 0x07ff);
|
||||
if(addr <= 0x13ff) return (chrBank[2] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x17ff) return (chrBank[3] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x1bff) return (chrBank[4] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x1fff) return (chrBank[5] << 10) | (addr & 0x03ff);
|
||||
} else {
|
||||
if(addr <= 0x03ff) return (chrBank[2] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x07ff) return (chrBank[3] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x0bff) return (chrBank[4] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x0fff) return (chrBank[5] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x17ff) return (chrBank[0] << 10) | (addr & 0x07ff);
|
||||
if(addr <= 0x1fff) return (chrBank[1] << 10) | (addr & 0x07ff);
|
||||
}
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) const -> uint {
|
||||
if(mirror == 0) return ((addr & 0x0400) >> 0) | (addr & 0x03ff);
|
||||
if(mirror == 1) return ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
}
|
||||
|
||||
auto readRAM(uint addr) -> uint8 {
|
||||
if(ramEnable) return board.prgram.data[addr & 0x1fff];
|
||||
return 0x00;
|
||||
}
|
||||
|
||||
auto writeRAM(uint addr, uint8 data) -> void {
|
||||
if(ramEnable && !ramWriteProtect) board.prgram.data[addr & 0x1fff] = data;
|
||||
}
|
||||
|
||||
auto writeIO(uint addr, uint8 data) -> void {
|
||||
switch(addr & 0xe001) {
|
||||
case 0x8000:
|
||||
chrMode = data & 0x80;
|
||||
prgMode = data & 0x40;
|
||||
bankSelect = data & 0x07;
|
||||
break;
|
||||
|
||||
case 0x8001:
|
||||
switch(bankSelect) {
|
||||
case 0: chrBank[0] = data & ~1; break;
|
||||
case 1: chrBank[1] = data & ~1; break;
|
||||
case 2: chrBank[2] = data; break;
|
||||
case 3: chrBank[3] = data; break;
|
||||
case 4: chrBank[4] = data; break;
|
||||
case 5: chrBank[5] = data; break;
|
||||
case 6: prgBank[0] = data & 0x3f; break;
|
||||
case 7: prgBank[1] = data & 0x3f; break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xa000:
|
||||
mirror = data & 0x01;
|
||||
break;
|
||||
|
||||
case 0xa001:
|
||||
ramEnable = data & 0x80;
|
||||
ramWriteProtect = data & 0x40;
|
||||
break;
|
||||
|
||||
case 0xc000:
|
||||
irqLatch = data;
|
||||
break;
|
||||
|
||||
case 0xc001:
|
||||
irqCounter = 0;
|
||||
break;
|
||||
|
||||
case 0xe000:
|
||||
irqEnable = false;
|
||||
irqLine = 0;
|
||||
break;
|
||||
|
||||
case 0xe001:
|
||||
irqEnable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
chrMode = 0;
|
||||
prgMode = 0;
|
||||
bankSelect = 0;
|
||||
prgBank[0] = 0;
|
||||
prgBank[1] = 0;
|
||||
chrBank[0] = 0;
|
||||
chrBank[1] = 0;
|
||||
chrBank[2] = 0;
|
||||
chrBank[3] = 0;
|
||||
chrBank[4] = 0;
|
||||
chrBank[5] = 0;
|
||||
mirror = 0;
|
||||
ramEnable = 1;
|
||||
ramWriteProtect = 0;
|
||||
irqLatch = 0;
|
||||
irqCounter = 0;
|
||||
irqEnable = false;
|
||||
irqDelay = 0;
|
||||
irqLine = 0;
|
||||
|
||||
chrAbus = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
s.integer(chrMode);
|
||||
s.integer(prgMode);
|
||||
s.integer(bankSelect);
|
||||
s.array(prgBank);
|
||||
s.array(chrBank);
|
||||
s.integer(mirror);
|
||||
s.integer(ramEnable);
|
||||
s.integer(ramWriteProtect);
|
||||
s.integer(irqLatch);
|
||||
s.integer(irqCounter);
|
||||
s.integer(irqEnable);
|
||||
s.integer(irqDelay);
|
||||
s.integer(irqLine);
|
||||
|
||||
s.integer(chrAbus);
|
||||
}
|
||||
|
||||
bool chrMode;
|
||||
bool prgMode;
|
||||
uint3 bankSelect;
|
||||
uint8 prgBank[2];
|
||||
uint8 chrBank[6];
|
||||
bool mirror;
|
||||
bool ramEnable;
|
||||
bool ramWriteProtect;
|
||||
uint8 irqLatch;
|
||||
uint8 irqCounter;
|
||||
bool irqEnable;
|
||||
uint irqDelay;
|
||||
bool irqLine;
|
||||
|
||||
uint16 chrAbus;
|
||||
};
|
||||
@@ -1,486 +0,0 @@
|
||||
struct MMC5 : Chip {
|
||||
MMC5(Board& board) : Chip(board) {
|
||||
revision = Revision::MMC5;
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
//scanline() resets this; if no scanlines detected, enter video blanking period
|
||||
if(++cpuCycleCounter >= 200) blank(); //113-114 normal; ~2500 across Vblank period
|
||||
|
||||
cpu.irqLine(irqEnable && irqPending);
|
||||
tick();
|
||||
}
|
||||
|
||||
auto scanline(uint y) -> void {
|
||||
//used for testing only, to verify MMC5 scanline detection is accurate:
|
||||
//if(y != vcounter && y <= 240) print(y, " vs ", vcounter, "\n");
|
||||
}
|
||||
|
||||
auto accessPRG(bool write, uint addr, uint8 data = 0x00) -> uint8 {
|
||||
uint bank;
|
||||
|
||||
if((addr & 0xe000) == 0x6000) {
|
||||
bank = (ramSelect << 2) | ramBank;
|
||||
addr &= 0x1fff;
|
||||
} else if(prgMode == 0) {
|
||||
bank = prgBank[3] & ~3;
|
||||
addr &= 0x7fff;
|
||||
} else if(prgMode == 1) {
|
||||
if((addr & 0xc000) == 0x8000) bank = (prgBank[1] & ~1);
|
||||
if((addr & 0xe000) == 0xc000) bank = (prgBank[3] & ~1);
|
||||
addr &= 0x3fff;
|
||||
} else if(prgMode == 2) {
|
||||
if((addr & 0xe000) == 0x8000) bank = (prgBank[1] & ~1) | 0;
|
||||
if((addr & 0xe000) == 0xa000) bank = (prgBank[1] & ~1) | 1;
|
||||
if((addr & 0xe000) == 0xc000) bank = (prgBank[2]);
|
||||
if((addr & 0xe000) == 0xe000) bank = (prgBank[3]);
|
||||
addr &= 0x1fff;
|
||||
} else if(prgMode == 3) {
|
||||
if((addr & 0xe000) == 0x8000) bank = prgBank[0];
|
||||
if((addr & 0xe000) == 0xa000) bank = prgBank[1];
|
||||
if((addr & 0xe000) == 0xc000) bank = prgBank[2];
|
||||
if((addr & 0xe000) == 0xe000) bank = prgBank[3];
|
||||
addr &= 0x1fff;
|
||||
}
|
||||
|
||||
bool rom = bank & 0x80;
|
||||
bank &= 0x7f;
|
||||
|
||||
if(write == false) {
|
||||
if(rom) {
|
||||
return board.prgrom.read((bank << 13) | addr);
|
||||
} else {
|
||||
return board.prgram.read((bank << 13) | addr);
|
||||
}
|
||||
} else {
|
||||
if(rom) {
|
||||
board.prgrom.write((bank << 13) | addr, data);
|
||||
} else {
|
||||
if(prgramWriteProtect[0] == 2 && prgramWriteProtect[1] == 1) {
|
||||
board.prgram.write((bank << 13) | addr, data);
|
||||
}
|
||||
}
|
||||
return 0x00;
|
||||
}
|
||||
}
|
||||
|
||||
auto readPRG(uint addr) -> uint8 {
|
||||
if((addr & 0xfc00) == 0x5c00) {
|
||||
if(exramMode >= 2) return exram[addr & 0x03ff];
|
||||
return cpu.mdr();
|
||||
}
|
||||
|
||||
if(addr >= 0x6000) {
|
||||
return accessPRG(0, addr);
|
||||
}
|
||||
|
||||
switch(addr) {
|
||||
case 0x5204: {
|
||||
uint8 result = (irqPending << 7) | (inFrame << 6);
|
||||
irqPending = false;
|
||||
return result;
|
||||
}
|
||||
case 0x5205: return (multiplier * multiplicand) >> 0;
|
||||
case 0x5206: return (multiplier * multiplicand) >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
auto writePRG(uint addr, uint8 data) -> void {
|
||||
if((addr & 0xfc00) == 0x5c00) {
|
||||
//writes 0x00 *during* Vblank (not during screen rendering ...)
|
||||
if(exramMode == 0 || exramMode == 1) exram[addr & 0x03ff] = inFrame ? data : (uint8)0x00;
|
||||
if(exramMode == 2) exram[addr & 0x03ff] = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if(addr >= 0x6000) {
|
||||
accessPRG(1, addr, data);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(addr) {
|
||||
case 0x2000:
|
||||
sprite8x16 = data & 0x20;
|
||||
break;
|
||||
|
||||
case 0x2001:
|
||||
//if BG+sprites are disabled; enter video blanking period
|
||||
if((data & 0x18) == 0) blank();
|
||||
break;
|
||||
|
||||
case 0x5100: prgMode = data & 3; break;
|
||||
case 0x5101: chrMode = data & 3; break;
|
||||
|
||||
case 0x5102: prgramWriteProtect[0] = data & 3; break;
|
||||
case 0x5103: prgramWriteProtect[1] = data & 3; break;
|
||||
|
||||
case 0x5104:
|
||||
exramMode = data & 3;
|
||||
break;
|
||||
|
||||
case 0x5105:
|
||||
nametableMode[0] = (data & 0x03) >> 0;
|
||||
nametableMode[1] = (data & 0x0c) >> 2;
|
||||
nametableMode[2] = (data & 0x30) >> 4;
|
||||
nametableMode[3] = (data & 0xc0) >> 6;
|
||||
break;
|
||||
|
||||
case 0x5106:
|
||||
fillmodeTile = data;
|
||||
break;
|
||||
|
||||
case 0x5107:
|
||||
fillmodeColor = data & 3;
|
||||
fillmodeColor |= fillmodeColor << 2;
|
||||
fillmodeColor |= fillmodeColor << 4;
|
||||
break;
|
||||
|
||||
case 0x5113:
|
||||
ramSelect = data & 0x04;
|
||||
ramBank = data & 0x03;
|
||||
break;
|
||||
|
||||
case 0x5114: prgBank[0] = data; break;
|
||||
case 0x5115: prgBank[1] = data; break;
|
||||
case 0x5116: prgBank[2] = data; break;
|
||||
case 0x5117: prgBank[3] = data | 0x80; break;
|
||||
|
||||
case 0x5120: chrSpriteBank[0] = (chrBankHi << 8) | data; chrActive = 0; break;
|
||||
case 0x5121: chrSpriteBank[1] = (chrBankHi << 8) | data; chrActive = 0; break;
|
||||
case 0x5122: chrSpriteBank[2] = (chrBankHi << 8) | data; chrActive = 0; break;
|
||||
case 0x5123: chrSpriteBank[3] = (chrBankHi << 8) | data; chrActive = 0; break;
|
||||
case 0x5124: chrSpriteBank[4] = (chrBankHi << 8) | data; chrActive = 0; break;
|
||||
case 0x5125: chrSpriteBank[5] = (chrBankHi << 8) | data; chrActive = 0; break;
|
||||
case 0x5126: chrSpriteBank[6] = (chrBankHi << 8) | data; chrActive = 0; break;
|
||||
case 0x5127: chrSpriteBank[7] = (chrBankHi << 8) | data; chrActive = 0; break;
|
||||
|
||||
case 0x5128: chrBGBank[0] = (chrBankHi << 8) | data; chrActive = 1; break;
|
||||
case 0x5129: chrBGBank[1] = (chrBankHi << 8) | data; chrActive = 1; break;
|
||||
case 0x512a: chrBGBank[2] = (chrBankHi << 8) | data; chrActive = 1; break;
|
||||
case 0x512b: chrBGBank[3] = (chrBankHi << 8) | data; chrActive = 1; break;
|
||||
|
||||
case 0x5130:
|
||||
chrBankHi = data & 3;
|
||||
break;
|
||||
|
||||
case 0x5200:
|
||||
vsEnable = data & 0x80;
|
||||
vsSide = data & 0x40;
|
||||
vsTile = data & 0x1f;
|
||||
break;
|
||||
|
||||
case 0x5201:
|
||||
vsScroll = data;
|
||||
break;
|
||||
|
||||
case 0x5202:
|
||||
vsBank = data;
|
||||
break;
|
||||
|
||||
case 0x5203:
|
||||
irqLine = data;
|
||||
break;
|
||||
|
||||
case 0x5204:
|
||||
irqEnable = data & 0x80;
|
||||
break;
|
||||
|
||||
case 0x5205:
|
||||
multiplicand = data;
|
||||
break;
|
||||
|
||||
case 0x5206:
|
||||
multiplier = data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto chrSpriteAddr(uint addr) -> uint {
|
||||
if(chrMode == 0) {
|
||||
auto bank = chrSpriteBank[7];
|
||||
return (bank * 0x2000) + (addr & 0x1fff);
|
||||
}
|
||||
|
||||
if(chrMode == 1) {
|
||||
auto bank = chrSpriteBank[(addr / 0x1000) * 4 + 3];
|
||||
return (bank * 0x1000) + (addr & 0x0fff);
|
||||
}
|
||||
|
||||
if(chrMode == 2) {
|
||||
auto bank = chrSpriteBank[(addr / 0x0800) * 2 + 1];
|
||||
return (bank * 0x0800) + (addr & 0x07ff);
|
||||
}
|
||||
|
||||
if(chrMode == 3) {
|
||||
auto bank = chrSpriteBank[(addr / 0x0400)];
|
||||
return (bank * 0x0400) + (addr & 0x03ff);
|
||||
}
|
||||
}
|
||||
|
||||
auto chrBGAddr(uint addr) -> uint {
|
||||
addr &= 0x0fff;
|
||||
|
||||
if(chrMode == 0) {
|
||||
auto bank = chrBGBank[3];
|
||||
return (bank * 0x2000) + (addr & 0x0fff);
|
||||
}
|
||||
|
||||
if(chrMode == 1) {
|
||||
auto bank = chrBGBank[3];
|
||||
return (bank * 0x1000) + (addr & 0x0fff);
|
||||
}
|
||||
|
||||
if(chrMode == 2) {
|
||||
auto bank = chrBGBank[(addr / 0x0800) * 2 + 1];
|
||||
return (bank * 0x0800) + (addr & 0x07ff);
|
||||
}
|
||||
|
||||
if(chrMode == 3) {
|
||||
auto bank = chrBGBank[(addr / 0x0400)];
|
||||
return (bank * 0x0400) + (addr & 0x03ff);
|
||||
}
|
||||
}
|
||||
|
||||
auto chrVSAddr(uint addr) -> uint {
|
||||
return (vsBank * 0x1000) + (addr & 0x0ff8) + (vsVpos & 7);
|
||||
}
|
||||
|
||||
auto blank() -> void {
|
||||
inFrame = false;
|
||||
}
|
||||
|
||||
auto scanline() -> void {
|
||||
hcounter = 0;
|
||||
|
||||
if(inFrame == false) {
|
||||
inFrame = true;
|
||||
irqPending = false;
|
||||
vcounter = 0;
|
||||
} else {
|
||||
if(vcounter == irqLine) irqPending = true;
|
||||
vcounter++;
|
||||
}
|
||||
|
||||
cpuCycleCounter = 0;
|
||||
}
|
||||
|
||||
auto readCIRAM(uint addr) -> uint8 {
|
||||
if(vsFetch && (hcounter & 2) == 0) return exram[vsVpos / 8 * 32 + vsHpos / 8];
|
||||
if(vsFetch && (hcounter & 2) != 0) return exram[vsVpos / 32 * 8 + vsHpos / 32 + 0x03c0];
|
||||
|
||||
switch(nametableMode[(addr >> 10) & 3]) {
|
||||
case 0: return ppu.readCIRAM(0x0000 | (addr & 0x03ff));
|
||||
case 1: return ppu.readCIRAM(0x0400 | (addr & 0x03ff));
|
||||
case 2: return exramMode < 2 ? exram[addr & 0x03ff] : (uint8)0x00;
|
||||
case 3: return (hcounter & 2) == 0 ? fillmodeTile : fillmodeColor;
|
||||
}
|
||||
}
|
||||
|
||||
auto readCHR(uint addr) -> uint8 {
|
||||
chrAccess[0] = chrAccess[1];
|
||||
chrAccess[1] = chrAccess[2];
|
||||
chrAccess[2] = chrAccess[3];
|
||||
chrAccess[3] = addr;
|
||||
|
||||
//detect two unused nametable fetches at end of each scanline
|
||||
if((chrAccess[0] & 0x2000) == 0
|
||||
&& (chrAccess[1] & 0x2000)
|
||||
&& (chrAccess[2] & 0x2000)
|
||||
&& (chrAccess[3] & 0x2000)) scanline();
|
||||
|
||||
if(inFrame == false) {
|
||||
vsFetch = false;
|
||||
if(addr & 0x2000) return readCIRAM(addr);
|
||||
return board.chrrom.read(chrActive ? chrBGAddr(addr) : chrSpriteAddr(addr));
|
||||
}
|
||||
|
||||
bool bgFetch = (hcounter < 256 || hcounter >= 320);
|
||||
uint8 result = 0x00;
|
||||
|
||||
if((hcounter & 7) == 0) {
|
||||
vsHpos = hcounter >= 320 ? hcounter - 320 : hcounter + 16;
|
||||
vsVpos = vcounter + vsScroll;
|
||||
vsFetch = vsEnable && bgFetch && exramMode < 2
|
||||
&& (vsSide ? vsHpos / 8 >= vsTile : vsHpos / 8 < vsTile);
|
||||
if(vsVpos >= 240) vsVpos -= 240;
|
||||
|
||||
result = readCIRAM(addr);
|
||||
|
||||
exbank = (chrBankHi << 6) | (exram[addr & 0x03ff] & 0x3f);
|
||||
exattr = exram[addr & 0x03ff] >> 6;
|
||||
exattr |= exattr << 2;
|
||||
exattr |= exattr << 4;
|
||||
} else if((hcounter & 7) == 2) {
|
||||
result = readCIRAM(addr);
|
||||
if(bgFetch && exramMode == 1) result = exattr;
|
||||
} else {
|
||||
if(vsFetch) result = board.chrrom.read(chrVSAddr(addr));
|
||||
else if(sprite8x16 ? bgFetch : chrActive) result = board.chrrom.read(chrBGAddr(addr));
|
||||
else result = board.chrrom.read(chrSpriteAddr(addr));
|
||||
if(bgFetch && exramMode == 1) result = board.chrrom.read(exbank * 0x1000 + (addr & 0x0fff));
|
||||
}
|
||||
|
||||
hcounter += 2;
|
||||
return result;
|
||||
}
|
||||
|
||||
auto writeCHR(uint addr, uint8 data) -> void {
|
||||
if(addr & 0x2000) {
|
||||
switch(nametableMode[(addr >> 10) & 3]) {
|
||||
case 0: return ppu.writeCIRAM(0x0000 | (addr & 0x03ff), data);
|
||||
case 1: return ppu.writeCIRAM(0x0400 | (addr & 0x03ff), data);
|
||||
case 2: exram[addr & 0x03ff] = data; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
for(auto& n : exram) n = 0xff;
|
||||
|
||||
prgMode = 3;
|
||||
chrMode = 0;
|
||||
for(auto& n : prgramWriteProtect) n = 0;
|
||||
exramMode = 0;
|
||||
for(auto& n : nametableMode) n = 0;
|
||||
fillmodeTile = 0;
|
||||
fillmodeColor = 0;
|
||||
ramSelect = 0;
|
||||
ramBank = 0;
|
||||
prgBank[0] = 0x00;
|
||||
prgBank[1] = 0x00;
|
||||
prgBank[2] = 0x00;
|
||||
prgBank[3] = 0xff;
|
||||
for(auto& n : chrSpriteBank) n = 0;
|
||||
for(auto& n : chrBGBank) n = 0;
|
||||
chrBankHi = 0;
|
||||
vsEnable = 0;
|
||||
vsSide = 0;
|
||||
vsTile = 0;
|
||||
vsScroll = 0;
|
||||
vsBank = 0;
|
||||
irqLine = 0;
|
||||
irqEnable = 0;
|
||||
multiplicand = 0;
|
||||
multiplier = 0;
|
||||
|
||||
cpuCycleCounter = 0;
|
||||
irqCounter = 0;
|
||||
irqPending = 0;
|
||||
inFrame = 0;
|
||||
vcounter = 0;
|
||||
hcounter = 0;
|
||||
for(auto& n : chrAccess) n = 0;
|
||||
chrActive = 0;
|
||||
sprite8x16 = 0;
|
||||
|
||||
exbank = 0;
|
||||
exattr = 0;
|
||||
|
||||
vsFetch = 0;
|
||||
vsVpos = 0;
|
||||
vsHpos = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
s.array(exram);
|
||||
|
||||
s.integer(prgMode);
|
||||
s.integer(chrMode);
|
||||
for(auto& n : prgramWriteProtect) s.integer(n);
|
||||
s.integer(exramMode);
|
||||
for(auto& n : nametableMode) s.integer(n);
|
||||
s.integer(fillmodeTile);
|
||||
s.integer(fillmodeColor);
|
||||
s.integer(ramSelect);
|
||||
s.integer(ramBank);
|
||||
for(auto& n : prgBank) s.integer(n);
|
||||
for(auto& n : chrSpriteBank) s.integer(n);
|
||||
for(auto& n : chrBGBank) s.integer(n);
|
||||
s.integer(chrBankHi);
|
||||
s.integer(vsEnable);
|
||||
s.integer(vsSide);
|
||||
s.integer(vsTile);
|
||||
s.integer(vsScroll);
|
||||
s.integer(vsBank);
|
||||
s.integer(irqLine);
|
||||
s.integer(irqEnable);
|
||||
s.integer(multiplicand);
|
||||
s.integer(multiplier);
|
||||
|
||||
s.integer(cpuCycleCounter);
|
||||
s.integer(irqCounter);
|
||||
s.integer(irqPending);
|
||||
s.integer(inFrame);
|
||||
|
||||
s.integer(vcounter);
|
||||
s.integer(hcounter);
|
||||
for(auto& n : chrAccess) s.integer(n);
|
||||
s.integer(chrActive);
|
||||
s.integer(sprite8x16);
|
||||
|
||||
s.integer(exbank);
|
||||
s.integer(exattr);
|
||||
|
||||
s.integer(vsFetch);
|
||||
s.integer(vsVpos);
|
||||
s.integer(vsHpos);
|
||||
}
|
||||
|
||||
enum class Revision : uint {
|
||||
MMC5,
|
||||
MMC5B,
|
||||
} revision;
|
||||
|
||||
uint8 exram[1024];
|
||||
|
||||
//programmable registers
|
||||
|
||||
uint2 prgMode; //$5100
|
||||
uint2 chrMode; //$5101
|
||||
|
||||
uint2 prgramWriteProtect[2]; //$5102,$5103
|
||||
|
||||
uint2 exramMode; //$5104
|
||||
uint2 nametableMode[4]; //$5105
|
||||
uint8 fillmodeTile; //$5106
|
||||
uint8 fillmodeColor; //$5107
|
||||
|
||||
bool ramSelect; //$5113
|
||||
uint2 ramBank; //$5113
|
||||
uint8 prgBank[4]; //$5114-5117
|
||||
uint10 chrSpriteBank[8]; //$5120-5127
|
||||
uint10 chrBGBank[4]; //$5128-512b
|
||||
uint2 chrBankHi; //$5130
|
||||
|
||||
bool vsEnable; //$5200
|
||||
bool vsSide; //$5200
|
||||
uint5 vsTile; //$5200
|
||||
uint8 vsScroll; //$5201
|
||||
uint8 vsBank; //$5202
|
||||
|
||||
uint8 irqLine; //$5203
|
||||
bool irqEnable; //$5204
|
||||
|
||||
uint8 multiplicand; //$5205
|
||||
uint8 multiplier; //$5206
|
||||
|
||||
//status registers
|
||||
|
||||
uint cpuCycleCounter;
|
||||
uint irqCounter;
|
||||
bool irqPending;
|
||||
bool inFrame;
|
||||
|
||||
uint vcounter;
|
||||
uint hcounter;
|
||||
uint16 chrAccess[4];
|
||||
bool chrActive;
|
||||
bool sprite8x16;
|
||||
|
||||
uint8 exbank;
|
||||
uint8 exattr;
|
||||
|
||||
bool vsFetch;
|
||||
uint8 vsVpos;
|
||||
uint8 vsHpos;
|
||||
};
|
||||
@@ -1,189 +0,0 @@
|
||||
struct MMC6 : Chip {
|
||||
MMC6(Board& board) : Chip(board) {
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
if(irqDelay) irqDelay--;
|
||||
cpu.irqLine(irqLine);
|
||||
tick();
|
||||
}
|
||||
|
||||
auto irqTest(uint addr) -> void {
|
||||
if(!(chrAbus & 0x1000) && (addr & 0x1000)) {
|
||||
if(irqDelay == 0) {
|
||||
if(irqCounter == 0) {
|
||||
irqCounter = irqLatch;
|
||||
} else if(--irqCounter == 0) {
|
||||
if(irqEnable) irqLine = 1;
|
||||
}
|
||||
}
|
||||
irqDelay = 6;
|
||||
}
|
||||
chrAbus = addr;
|
||||
}
|
||||
|
||||
auto addrPRG(uint addr) const -> uint {
|
||||
switch((addr >> 13) & 3) {
|
||||
case 0:
|
||||
if(prgMode == 1) return (0x3e << 13) | (addr & 0x1fff);
|
||||
return (prgBank[0] << 13) | (addr & 0x1fff);
|
||||
case 1:
|
||||
return (prgBank[1] << 13) | (addr & 0x1fff);
|
||||
case 2:
|
||||
if(prgMode == 0) return (0x3e << 13) | (addr & 0x1fff);
|
||||
return (prgBank[0] << 13) | (addr & 0x1fff);
|
||||
case 3:
|
||||
return (0x3f << 13) | (addr & 0x1fff);
|
||||
}
|
||||
}
|
||||
|
||||
auto addrCHR(uint addr) const -> uint {
|
||||
if(chrMode == 0) {
|
||||
if(addr <= 0x07ff) return (chrBank[0] << 10) | (addr & 0x07ff);
|
||||
if(addr <= 0x0fff) return (chrBank[1] << 10) | (addr & 0x07ff);
|
||||
if(addr <= 0x13ff) return (chrBank[2] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x17ff) return (chrBank[3] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x1bff) return (chrBank[4] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x1fff) return (chrBank[5] << 10) | (addr & 0x03ff);
|
||||
} else {
|
||||
if(addr <= 0x03ff) return (chrBank[2] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x07ff) return (chrBank[3] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x0bff) return (chrBank[4] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x0fff) return (chrBank[5] << 10) | (addr & 0x03ff);
|
||||
if(addr <= 0x17ff) return (chrBank[0] << 10) | (addr & 0x07ff);
|
||||
if(addr <= 0x1fff) return (chrBank[1] << 10) | (addr & 0x07ff);
|
||||
}
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) const -> uint {
|
||||
if(mirror == 0) return ((addr & 0x0400) >> 0) | (addr & 0x03ff);
|
||||
if(mirror == 1) return ((addr & 0x0800) >> 1) | (addr & 0x03ff);
|
||||
}
|
||||
|
||||
auto readRAM(uint addr) -> uint8 {
|
||||
if(ramEnable == false) return cpu.mdr();
|
||||
if(ramReadable[0] == false && ramReadable[1] == false) return cpu.mdr();
|
||||
bool region = addr & 0x0200;
|
||||
if(ramReadable[region] == false) return 0x00;
|
||||
return board.prgram.read((region * 0x0200) + (addr & 0x01ff));
|
||||
}
|
||||
|
||||
auto writeRAM(uint addr, uint8 data) -> void {
|
||||
if(ramEnable == false) return;
|
||||
bool region = addr & 0x0200;
|
||||
if(ramWritable[region] == false) return;
|
||||
return board.prgram.write((region * 0x0200) + (addr & 0x01ff), data);
|
||||
}
|
||||
|
||||
auto writeIO(uint addr, uint8 data) -> void {
|
||||
switch(addr & 0xe001) {
|
||||
case 0x8000:
|
||||
chrMode = data & 0x80;
|
||||
prgMode = data & 0x40;
|
||||
ramEnable = data & 0x20;
|
||||
bankSelect = data & 0x07;
|
||||
if(ramEnable == false) {
|
||||
for(auto &n : ramReadable) n = false;
|
||||
for(auto &n : ramWritable) n = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x8001:
|
||||
switch(bankSelect) {
|
||||
case 0: chrBank[0] = data & ~1; break;
|
||||
case 1: chrBank[1] = data & ~1; break;
|
||||
case 2: chrBank[2] = data; break;
|
||||
case 3: chrBank[3] = data; break;
|
||||
case 4: chrBank[4] = data; break;
|
||||
case 5: chrBank[5] = data; break;
|
||||
case 6: prgBank[0] = data & 0x3f; break;
|
||||
case 7: prgBank[1] = data & 0x3f; break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xa000:
|
||||
mirror = data & 0x01;
|
||||
break;
|
||||
|
||||
case 0xa001:
|
||||
if(ramEnable == false) break;
|
||||
ramReadable[1] = data & 0x80;
|
||||
ramWritable[1] = data & 0x40;
|
||||
ramReadable[0] = data & 0x20;
|
||||
ramWritable[0] = data & 0x10;
|
||||
break;
|
||||
|
||||
case 0xc000:
|
||||
irqLatch = data;
|
||||
break;
|
||||
|
||||
case 0xc001:
|
||||
irqCounter = 0;
|
||||
break;
|
||||
|
||||
case 0xe000:
|
||||
irqEnable = false;
|
||||
irqLine = 0;
|
||||
break;
|
||||
|
||||
case 0xe001:
|
||||
irqEnable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
chrMode = 0;
|
||||
prgMode = 0;
|
||||
ramEnable = 0;
|
||||
bankSelect = 0;
|
||||
for(auto& n : prgBank) n = 0;
|
||||
for(auto& n : chrBank) n = 0;
|
||||
mirror = 0;
|
||||
for(auto& n : ramReadable) n = 0;
|
||||
for(auto& n : ramWritable) n = 0;
|
||||
irqLatch = 0;
|
||||
irqCounter = 0;
|
||||
irqEnable = 0;
|
||||
irqDelay = 0;
|
||||
irqLine = 0;
|
||||
|
||||
chrAbus = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
s.integer(chrMode);
|
||||
s.integer(prgMode);
|
||||
s.integer(ramEnable);
|
||||
s.integer(bankSelect);
|
||||
for(auto& n : prgBank) s.integer(n);
|
||||
for(auto& n : chrBank) s.integer(n);
|
||||
s.integer(mirror);
|
||||
for(auto& n : ramReadable) s.integer(n);
|
||||
for(auto& n : ramWritable) s.integer(n);
|
||||
s.integer(irqLatch);
|
||||
s.integer(irqCounter);
|
||||
s.integer(irqEnable);
|
||||
s.integer(irqDelay);
|
||||
s.integer(irqLine);
|
||||
|
||||
s.integer(chrAbus);
|
||||
}
|
||||
|
||||
bool chrMode;
|
||||
bool prgMode;
|
||||
bool ramEnable;
|
||||
uint3 bankSelect;
|
||||
uint8 prgBank[2];
|
||||
uint8 chrBank[6];
|
||||
bool mirror;
|
||||
bool ramReadable[2];
|
||||
bool ramWritable[2];
|
||||
uint8 irqLatch;
|
||||
uint8 irqCounter;
|
||||
bool irqEnable;
|
||||
uint irqDelay;
|
||||
bool irqLine;
|
||||
|
||||
uint16 chrAbus;
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
struct VRC1 : Chip {
|
||||
VRC1(Board& board) : Chip(board) {
|
||||
}
|
||||
|
||||
auto addrPRG(uint addr) const -> uint {
|
||||
uint bank = 0x0f;
|
||||
if((addr & 0xe000) == 0x8000) bank = prgBank[0];
|
||||
if((addr & 0xe000) == 0xa000) bank = prgBank[1];
|
||||
if((addr & 0xe000) == 0xc000) bank = prgBank[2];
|
||||
return (bank * 0x2000) + (addr & 0x1fff);
|
||||
}
|
||||
|
||||
auto addrCHR(uint addr) const -> uint {
|
||||
uint bank = chrBankLo[(bool)(addr & 0x1000)];
|
||||
bank |= chrBankHi[(bool)(addr & 0x1000)] << 4;
|
||||
return (bank * 0x1000) + (addr & 0x0fff);
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) const -> uint {
|
||||
switch(mirror) {
|
||||
case 0: return ((addr & 0x0400) >> 0) | (addr & 0x03ff); //vertical mirroring
|
||||
case 1: return ((addr & 0x0800) >> 1) | (addr & 0x03ff); //horizontal mirroring
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
auto writeIO(uint addr, uint8 data) -> void {
|
||||
switch(addr & 0xf000) {
|
||||
case 0x8000:
|
||||
prgBank[0] = data & 0x0f;
|
||||
break;
|
||||
|
||||
case 0x9000:
|
||||
chrBankHi[1] = data & 0x04;
|
||||
chrBankHi[0] = data & 0x02;
|
||||
mirror = data & 0x01;
|
||||
break;
|
||||
|
||||
case 0xa000:
|
||||
prgBank[1] = data & 0x0f;
|
||||
break;
|
||||
|
||||
case 0xc000:
|
||||
prgBank[2] = data & 0x0f;
|
||||
break;
|
||||
|
||||
case 0xe000:
|
||||
chrBankLo[0] = data & 0x0f;
|
||||
break;
|
||||
|
||||
case 0xf000:
|
||||
chrBankLo[1] = data & 0x0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
for(auto& n : prgBank) n = 0;
|
||||
for(auto& n : chrBankLo) n = 0;
|
||||
for(auto& n : chrBankHi) n = 0;
|
||||
mirror = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
for(auto& n : prgBank) s.integer(n);
|
||||
for(auto& n : chrBankLo) s.integer(n);
|
||||
for(auto& n : chrBankHi) s.integer(n);
|
||||
s.integer(mirror);
|
||||
}
|
||||
|
||||
uint4 prgBank[3];
|
||||
uint4 chrBankLo[2];
|
||||
bool chrBankHi[2];
|
||||
bool mirror;
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
struct VRC2 : Chip {
|
||||
VRC2(Board& board) : Chip(board) {
|
||||
}
|
||||
|
||||
auto addrPRG(uint addr) const -> uint {
|
||||
uint bank;
|
||||
switch(addr & 0xe000) {
|
||||
case 0x8000: bank = prgBank[0]; break;
|
||||
case 0xa000: bank = prgBank[1]; break;
|
||||
case 0xc000: bank = 0x1e; break;
|
||||
case 0xe000: bank = 0x1f; break;
|
||||
}
|
||||
return (bank * 0x2000) + (addr & 0x1fff);
|
||||
}
|
||||
|
||||
auto addrCHR(uint addr) const -> uint {
|
||||
uint bank = chrBank[addr / 0x0400];
|
||||
return (bank * 0x0400) + (addr & 0x03ff);
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) const -> uint {
|
||||
switch(mirror) {
|
||||
case 0: return ((addr & 0x0400) >> 0) | (addr & 0x03ff); //vertical mirroring
|
||||
case 1: return ((addr & 0x0800) >> 1) | (addr & 0x03ff); //horizontal mirroring
|
||||
case 2: return 0x0000 | (addr & 0x03ff); //one-screen mirroring (first)
|
||||
case 3: return 0x0400 | (addr & 0x03ff); //one-screen mirroring (second)
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
auto readRAM(uint addr) -> uint8 {
|
||||
if(board.prgram.size == 0) {
|
||||
if((addr & 0xf000) == 0x6000) return cpu.mdr() | latch;
|
||||
return cpu.mdr();
|
||||
}
|
||||
return board.prgram.read(addr & 0x1fff);
|
||||
}
|
||||
|
||||
auto writeRAM(uint addr, uint8 data) -> void {
|
||||
if(board.prgram.size == 0) {
|
||||
if((addr & 0xf000) == 0x6000) latch = data & 0x01;
|
||||
return;
|
||||
}
|
||||
return board.prgram.write(addr & 0x1fff, data);
|
||||
}
|
||||
|
||||
auto writeIO(uint addr, uint8 data) -> void {
|
||||
switch(addr) {
|
||||
case 0x8000: case 0x8001: case 0x8002: case 0x8003:
|
||||
prgBank[0] = data & 0x1f;
|
||||
break;
|
||||
|
||||
case 0x9000: case 0x9001: case 0x9002: case 0x9003:
|
||||
mirror = data & 0x03;
|
||||
break;
|
||||
|
||||
case 0xa000: case 0xa001: case 0xa002: case 0xa003:
|
||||
prgBank[1] = data & 0x1f;
|
||||
break;
|
||||
|
||||
case 0xb000: chrBank[0] = (chrBank[0] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xb001: chrBank[0] = (chrBank[0] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xb002: chrBank[1] = (chrBank[1] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xb003: chrBank[1] = (chrBank[1] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xc000: chrBank[2] = (chrBank[2] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xc001: chrBank[2] = (chrBank[2] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xc002: chrBank[3] = (chrBank[3] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xc003: chrBank[3] = (chrBank[3] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xd000: chrBank[4] = (chrBank[4] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xd001: chrBank[4] = (chrBank[4] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xd002: chrBank[5] = (chrBank[5] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xd003: chrBank[5] = (chrBank[5] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xe000: chrBank[6] = (chrBank[6] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xe001: chrBank[6] = (chrBank[6] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xe002: chrBank[7] = (chrBank[7] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xe003: chrBank[7] = (chrBank[7] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
}
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
for(auto& n : prgBank) n = 0;
|
||||
for(auto& n : chrBank) n = 0;
|
||||
mirror = 0;
|
||||
latch = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
for(auto& n : prgBank) s.integer(n);
|
||||
for(auto& n : chrBank) s.integer(n);
|
||||
s.integer(mirror);
|
||||
s.integer(latch);
|
||||
}
|
||||
|
||||
uint5 prgBank[2];
|
||||
uint8 chrBank[8];
|
||||
uint2 mirror;
|
||||
bool latch;
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
struct VRC3 : Chip {
|
||||
VRC3(Board& board) : Chip(board) {
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
if(irqEnable) {
|
||||
if(irqMode == 0) { //16-bit
|
||||
if(++irqCounter.w == 0) {
|
||||
irqLine = 1;
|
||||
irqEnable = irqAcknowledge;
|
||||
irqCounter.w = irqLatch;
|
||||
}
|
||||
}
|
||||
if(irqMode == 1) { //8-bit
|
||||
if(++irqCounter.l == 0) {
|
||||
irqLine = 1;
|
||||
irqEnable = irqAcknowledge;
|
||||
irqCounter.l = irqLatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cpu.irqLine(irqLine);
|
||||
tick();
|
||||
}
|
||||
|
||||
auto addrPRG(uint addr) const -> uint {
|
||||
uint bank = (addr < 0xc000 ? (uint)prgBank : 0x0f);
|
||||
return (bank * 0x4000) + (addr & 0x3fff);
|
||||
}
|
||||
|
||||
auto writeIO(uint addr, uint8 data) -> void {
|
||||
switch(addr & 0xf000) {
|
||||
case 0x8000: irqLatch = (irqLatch & 0xfff0) | ((data & 0x0f) << 0); break;
|
||||
case 0x9000: irqLatch = (irqLatch & 0xff0f) | ((data & 0x0f) << 4); break;
|
||||
case 0xa000: irqLatch = (irqLatch & 0xf0ff) | ((data & 0x0f) << 8); break;
|
||||
case 0xb000: irqLatch = (irqLatch & 0x0fff) | ((data & 0x0f) << 12); break;
|
||||
|
||||
case 0xc000:
|
||||
irqMode = data & 0x04;
|
||||
irqEnable = data & 0x02;
|
||||
irqAcknowledge = data & 0x01;
|
||||
if(irqEnable) irqCounter.w = irqLatch;
|
||||
break;
|
||||
|
||||
case 0xd000:
|
||||
irqLine = 0;
|
||||
irqEnable = irqAcknowledge;
|
||||
break;
|
||||
|
||||
case 0xf000:
|
||||
prgBank = data & 0x0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
prgBank = 0;
|
||||
irqMode = 0;
|
||||
irqEnable = 0;
|
||||
irqAcknowledge = 0;
|
||||
irqLatch = 0;
|
||||
irqCounter.w = 0;
|
||||
irqLine = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
s.integer(prgBank);
|
||||
s.integer(irqMode);
|
||||
s.integer(irqEnable);
|
||||
s.integer(irqAcknowledge);
|
||||
s.integer(irqLatch);
|
||||
s.integer(irqCounter.w);
|
||||
s.integer(irqLine);
|
||||
}
|
||||
|
||||
uint4 prgBank;
|
||||
bool irqMode;
|
||||
bool irqEnable;
|
||||
bool irqAcknowledge;
|
||||
uint16 irqLatch;
|
||||
struct {
|
||||
union {
|
||||
uint16_t w;
|
||||
struct { uint8_t order_lsb2(l, h); };
|
||||
};
|
||||
} irqCounter;
|
||||
bool irqLine;
|
||||
};
|
||||
@@ -1,173 +0,0 @@
|
||||
struct VRC4 : Chip {
|
||||
VRC4(Board& board) : Chip(board) {
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
if(irqEnable) {
|
||||
if(irqMode == 0) {
|
||||
irqScalar -= 3;
|
||||
if(irqScalar <= 0) {
|
||||
irqScalar += 341;
|
||||
if(irqCounter == 0xff) {
|
||||
irqCounter = irqLatch;
|
||||
irqLine = 1;
|
||||
} else {
|
||||
irqCounter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(irqMode == 1) {
|
||||
if(irqCounter == 0xff) {
|
||||
irqCounter = irqLatch;
|
||||
irqLine = 1;
|
||||
} else {
|
||||
irqCounter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cpu.irqLine(irqLine);
|
||||
tick();
|
||||
}
|
||||
|
||||
auto addrPRG(uint addr) const -> uint {
|
||||
uint bank = 0, banks = board.prgrom.size / 0x2000;
|
||||
switch(addr & 0xe000) {
|
||||
case 0x8000: bank = prgMode == 0 ? (uint)prgBank[0] : banks - 2; break;
|
||||
case 0xa000: bank = prgBank[1]; break;
|
||||
case 0xc000: bank = prgMode == 0 ? banks - 2 : (uint)prgBank[0]; break;
|
||||
case 0xe000: bank = banks - 1; break;
|
||||
}
|
||||
return (bank * 0x2000) + (addr & 0x1fff);
|
||||
}
|
||||
|
||||
auto addrCHR(uint addr) const -> uint {
|
||||
uint bank = chrBank[addr / 0x0400];
|
||||
return (bank * 0x0400) + (addr & 0x03ff);
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) const -> uint {
|
||||
switch(mirror) {
|
||||
case 0: return ((addr & 0x0400) >> 0) | (addr & 0x03ff); //vertical mirroring
|
||||
case 1: return ((addr & 0x0800) >> 1) | (addr & 0x03ff); //horizontal mirroring
|
||||
case 2: return 0x0000 | (addr & 0x03ff); //one-screen mirroring (first)
|
||||
case 3: return 0x0400 | (addr & 0x03ff); //one-screen mirroring (second)
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
auto writeIO(uint addr, uint8 data) -> void {
|
||||
switch(addr) {
|
||||
case 0x8000: case 0x8001: case 0x8002: case 0x8003:
|
||||
prgBank[0] = data & 0x1f;
|
||||
break;
|
||||
|
||||
case 0x9000: case 0x9001:
|
||||
mirror = data & 0x03;
|
||||
break;
|
||||
|
||||
case 0x9002: case 0x9003:
|
||||
prgMode = data & 0x02;
|
||||
break;
|
||||
|
||||
case 0xa000: case 0xa001: case 0xa002: case 0xa003:
|
||||
prgBank[1] = data & 0x1f;
|
||||
break;
|
||||
|
||||
case 0xb000: chrBank[0] = (chrBank[0] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xb001: chrBank[0] = (chrBank[0] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xb002: chrBank[1] = (chrBank[1] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xb003: chrBank[1] = (chrBank[1] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xc000: chrBank[2] = (chrBank[2] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xc001: chrBank[2] = (chrBank[2] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xc002: chrBank[3] = (chrBank[3] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xc003: chrBank[3] = (chrBank[3] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xd000: chrBank[4] = (chrBank[4] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xd001: chrBank[4] = (chrBank[4] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xd002: chrBank[5] = (chrBank[5] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xd003: chrBank[5] = (chrBank[5] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xe000: chrBank[6] = (chrBank[6] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xe001: chrBank[6] = (chrBank[6] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xe002: chrBank[7] = (chrBank[7] & 0xf0) | ((data & 0x0f) << 0); break;
|
||||
case 0xe003: chrBank[7] = (chrBank[7] & 0x0f) | ((data & 0x0f) << 4); break;
|
||||
|
||||
case 0xf000:
|
||||
irqLatch = (irqLatch & 0xf0) | ((data & 0x0f) << 0);
|
||||
break;
|
||||
|
||||
case 0xf001:
|
||||
irqLatch = (irqLatch & 0x0f) | ((data & 0x0f) << 4);
|
||||
break;
|
||||
|
||||
case 0xf002:
|
||||
irqMode = data & 0x04;
|
||||
irqEnable = data & 0x02;
|
||||
irqAcknowledge = data & 0x01;
|
||||
if(irqEnable) {
|
||||
irqCounter = irqLatch;
|
||||
irqScalar = 341;
|
||||
}
|
||||
irqLine = 0;
|
||||
break;
|
||||
|
||||
case 0xf003:
|
||||
irqEnable = irqAcknowledge;
|
||||
irqLine = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
prgMode = 0;
|
||||
for(auto& n : prgBank) n = 0;
|
||||
mirror = 0;
|
||||
for(auto& n : chrBank) n = 0;
|
||||
|
||||
irqLatch = 0;
|
||||
irqMode = 0;
|
||||
irqEnable = 0;
|
||||
irqAcknowledge = 0;
|
||||
|
||||
irqCounter = 0;
|
||||
irqScalar = 0;
|
||||
irqLine = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
s.integer(prgMode);
|
||||
for(auto& n : prgBank) s.integer(n);
|
||||
s.integer(mirror);
|
||||
for(auto& n : chrBank) s.integer(n);
|
||||
|
||||
s.integer(irqLatch);
|
||||
s.integer(irqMode);
|
||||
s.integer(irqEnable);
|
||||
s.integer(irqAcknowledge);
|
||||
|
||||
s.integer(irqCounter);
|
||||
s.integer(irqScalar);
|
||||
s.integer(irqLine);
|
||||
}
|
||||
|
||||
bool prgMode;
|
||||
uint5 prgBank[2];
|
||||
uint2 mirror;
|
||||
uint8 chrBank[8];
|
||||
|
||||
uint8 irqLatch;
|
||||
bool irqMode;
|
||||
bool irqEnable;
|
||||
bool irqAcknowledge;
|
||||
|
||||
uint8 irqCounter;
|
||||
int irqScalar;
|
||||
bool irqLine;
|
||||
};
|
||||
@@ -1,310 +0,0 @@
|
||||
struct VRC6 : Chip {
|
||||
VRC6(Board& board) : Chip(board) {
|
||||
}
|
||||
|
||||
struct Pulse {
|
||||
auto clock() -> void {
|
||||
if(--divider == 0) {
|
||||
divider = frequency + 1;
|
||||
cycle++;
|
||||
output = (mode == 1 || cycle > duty) ? volume : (uint4)0;
|
||||
}
|
||||
|
||||
if(enable == false) output = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
s.integer(mode);
|
||||
s.integer(duty);
|
||||
s.integer(volume);
|
||||
s.integer(enable);
|
||||
s.integer(frequency);
|
||||
|
||||
s.integer(divider);
|
||||
s.integer(cycle);
|
||||
s.integer(output);
|
||||
}
|
||||
|
||||
bool mode;
|
||||
uint3 duty;
|
||||
uint4 volume;
|
||||
bool enable;
|
||||
uint12 frequency;
|
||||
|
||||
uint12 divider;
|
||||
uint4 cycle;
|
||||
uint4 output;
|
||||
} pulse1, pulse2;
|
||||
|
||||
struct Sawtooth {
|
||||
auto clock() -> void {
|
||||
if(--divider == 0) {
|
||||
divider = frequency + 1;
|
||||
if(++phase == 0) {
|
||||
accumulator += rate;
|
||||
if(++stage == 7) {
|
||||
stage = 0;
|
||||
accumulator = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output = accumulator >> 3;
|
||||
if(enable == false) output = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
s.integer(rate);
|
||||
s.integer(enable);
|
||||
s.integer(frequency);
|
||||
|
||||
s.integer(divider);
|
||||
s.integer(phase);
|
||||
s.integer(stage);
|
||||
s.integer(accumulator);
|
||||
s.integer(output);
|
||||
}
|
||||
|
||||
uint6 rate;
|
||||
bool enable;
|
||||
uint12 frequency;
|
||||
|
||||
uint12 divider;
|
||||
uint1 phase;
|
||||
uint3 stage;
|
||||
uint8 accumulator;
|
||||
uint5 output;
|
||||
} sawtooth;
|
||||
|
||||
auto main() -> void {
|
||||
if(irqEnable) {
|
||||
if(irqMode == 0) {
|
||||
irqScalar -= 3;
|
||||
if(irqScalar <= 0) {
|
||||
irqScalar += 341;
|
||||
if(irqCounter == 0xff) {
|
||||
irqCounter = irqLatch;
|
||||
irqLine = 1;
|
||||
} else {
|
||||
irqCounter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(irqMode == 1) {
|
||||
if(irqCounter == 0xff) {
|
||||
irqCounter = irqLatch;
|
||||
irqLine = 1;
|
||||
} else {
|
||||
irqCounter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
cpu.irqLine(irqLine);
|
||||
|
||||
pulse1.clock();
|
||||
pulse2.clock();
|
||||
sawtooth.clock();
|
||||
int output = (pulse1.output + pulse2.output + sawtooth.output) << 7;
|
||||
apu.setSample(-output);
|
||||
|
||||
tick();
|
||||
}
|
||||
|
||||
auto addrPRG(uint addr) const -> uint {
|
||||
if((addr & 0xc000) == 0x8000) return (prgBank[0] << 14) | (addr & 0x3fff);
|
||||
if((addr & 0xe000) == 0xc000) return (prgBank[1] << 13) | (addr & 0x1fff);
|
||||
if((addr & 0xe000) == 0xe000) return ( 0xff << 13) | (addr & 0x1fff);
|
||||
}
|
||||
|
||||
auto addrCHR(uint addr) const -> uint {
|
||||
uint bank = chrBank[(addr >> 10) & 7];
|
||||
return (bank << 10) | (addr & 0x03ff);
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) const -> uint {
|
||||
switch(mirror) {
|
||||
case 0: return ((addr & 0x0400) >> 0) | (addr & 0x03ff); //vertical mirroring
|
||||
case 1: return ((addr & 0x0800) >> 1) | (addr & 0x03ff); //horizontal mirroring
|
||||
case 2: return 0x0000 | (addr & 0x03ff); //one-screen mirroring (first)
|
||||
case 3: return 0x0400 | (addr & 0x03ff); //one-screen mirroring (second)
|
||||
}
|
||||
}
|
||||
|
||||
auto readRAM(uint addr) -> uint8 {
|
||||
return board.prgram.data[addr & 0x1fff];
|
||||
}
|
||||
|
||||
auto writeRAM(uint addr, uint8 data) -> void {
|
||||
board.prgram.data[addr & 0x1fff] = data;
|
||||
}
|
||||
|
||||
auto writeIO(uint addr, uint8 data) -> void {
|
||||
switch(addr) {
|
||||
case 0x8000: case 0x8001: case 0x8002: case 0x8003:
|
||||
prgBank[0] = data;
|
||||
break;
|
||||
|
||||
case 0x9000:
|
||||
pulse1.mode = data & 0x80;
|
||||
pulse1.duty = (data & 0x70) >> 4;
|
||||
pulse1.volume = data & 0x0f;
|
||||
break;
|
||||
|
||||
case 0x9001:
|
||||
pulse1.frequency = (pulse1.frequency & 0x0f00) | ((data & 0xff) << 0);
|
||||
break;
|
||||
|
||||
case 0x9002:
|
||||
pulse1.frequency = (pulse1.frequency & 0x00ff) | ((data & 0x0f) << 8);
|
||||
pulse1.enable = data & 0x80;
|
||||
break;
|
||||
|
||||
case 0xa000:
|
||||
pulse2.mode = data & 0x80;
|
||||
pulse2.duty = (data & 0x70) >> 4;
|
||||
pulse2.volume = data & 0x0f;
|
||||
break;
|
||||
|
||||
case 0xa001:
|
||||
pulse2.frequency = (pulse2.frequency & 0x0f00) | ((data & 0xff) << 0);
|
||||
break;
|
||||
|
||||
case 0xa002:
|
||||
pulse2.frequency = (pulse2.frequency & 0x00ff) | ((data & 0x0f) << 8);
|
||||
pulse2.enable = data & 0x80;
|
||||
break;
|
||||
|
||||
case 0xb000:
|
||||
sawtooth.rate = data & 0x3f;
|
||||
break;
|
||||
|
||||
case 0xb001:
|
||||
sawtooth.frequency = (sawtooth.frequency & 0x0f00) | ((data & 0xff) << 0);
|
||||
break;
|
||||
|
||||
case 0xb002:
|
||||
sawtooth.frequency = (sawtooth.frequency & 0x00ff) | ((data & 0x0f) << 8);
|
||||
sawtooth.enable = data & 0x80;
|
||||
break;
|
||||
|
||||
case 0xb003:
|
||||
mirror = (data >> 2) & 3;
|
||||
break;
|
||||
|
||||
case 0xc000: case 0xc001: case 0xc002: case 0xc003:
|
||||
prgBank[1] = data;
|
||||
break;
|
||||
|
||||
case 0xd000: case 0xd001: case 0xd002: case 0xd003:
|
||||
chrBank[0 + (addr & 3)] = data;
|
||||
break;
|
||||
|
||||
case 0xe000: case 0xe001: case 0xe002: case 0xe003:
|
||||
chrBank[4 + (addr & 3)] = data;
|
||||
break;
|
||||
|
||||
case 0xf000:
|
||||
irqLatch = data;
|
||||
break;
|
||||
|
||||
case 0xf001:
|
||||
irqMode = data & 0x04;
|
||||
irqEnable = data & 0x02;
|
||||
irqAcknowledge = data & 0x01;
|
||||
if(irqEnable) {
|
||||
irqCounter = irqLatch;
|
||||
irqScalar = 341;
|
||||
}
|
||||
irqLine = 0;
|
||||
break;
|
||||
|
||||
case 0xf002:
|
||||
irqEnable = irqAcknowledge;
|
||||
irqLine = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
prgBank[0] = 0;
|
||||
prgBank[1] = 0;
|
||||
chrBank[0] = 0;
|
||||
chrBank[1] = 0;
|
||||
chrBank[2] = 0;
|
||||
chrBank[3] = 0;
|
||||
chrBank[4] = 0;
|
||||
chrBank[5] = 0;
|
||||
chrBank[6] = 0;
|
||||
chrBank[7] = 0;
|
||||
mirror = 0;
|
||||
irqLatch = 0;
|
||||
irqMode = 0;
|
||||
irqEnable = 0;
|
||||
irqAcknowledge = 0;
|
||||
|
||||
irqCounter = 0;
|
||||
irqScalar = 0;
|
||||
irqLine = 0;
|
||||
|
||||
pulse1.mode = 0;
|
||||
pulse1.duty = 0;
|
||||
pulse1.volume = 0;
|
||||
pulse1.enable = 0;
|
||||
pulse1.frequency = 0;
|
||||
|
||||
pulse1.divider = 1;
|
||||
pulse1.cycle = 0;
|
||||
pulse1.output = 0;
|
||||
|
||||
pulse2.mode = 0;
|
||||
pulse2.duty = 0;
|
||||
pulse2.volume = 0;
|
||||
pulse2.enable = 0;
|
||||
pulse2.frequency = 0;
|
||||
|
||||
pulse2.divider = 1;
|
||||
pulse2.cycle = 0;
|
||||
pulse2.output = 0;
|
||||
|
||||
sawtooth.rate = 0;
|
||||
sawtooth.enable = 0;
|
||||
sawtooth.frequency = 0;
|
||||
|
||||
sawtooth.divider = 1;
|
||||
sawtooth.phase = 0;
|
||||
sawtooth.stage = 0;
|
||||
sawtooth.accumulator = 0;
|
||||
sawtooth.output = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
pulse1.serialize(s);
|
||||
pulse2.serialize(s);
|
||||
sawtooth.serialize(s);
|
||||
|
||||
s.array(prgBank);
|
||||
s.array(chrBank);
|
||||
s.integer(mirror);
|
||||
s.integer(irqLatch);
|
||||
s.integer(irqMode);
|
||||
s.integer(irqEnable);
|
||||
s.integer(irqAcknowledge);
|
||||
|
||||
s.integer(irqCounter);
|
||||
s.integer(irqScalar);
|
||||
s.integer(irqLine);
|
||||
}
|
||||
|
||||
uint8 prgBank[2];
|
||||
uint8 chrBank[8];
|
||||
uint2 mirror;
|
||||
uint8 irqLatch;
|
||||
bool irqMode;
|
||||
bool irqEnable;
|
||||
bool irqAcknowledge;
|
||||
|
||||
uint8 irqCounter;
|
||||
int irqScalar;
|
||||
bool irqLine;
|
||||
};
|
||||
@@ -1,143 +0,0 @@
|
||||
//Konami VRC7
|
||||
//Yamaha YM2413 OPLL audio - not emulated
|
||||
|
||||
struct VRC7 : Chip {
|
||||
VRC7(Board& board) : Chip(board) {
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
if(irqEnable) {
|
||||
if(irqMode == 0) {
|
||||
irqScalar -= 3;
|
||||
if(irqScalar <= 0) {
|
||||
irqScalar += 341;
|
||||
if(irqCounter == 0xff) {
|
||||
irqCounter = irqLatch;
|
||||
irqLine = 1;
|
||||
} else {
|
||||
irqCounter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(irqMode == 1) {
|
||||
if(irqCounter == 0xff) {
|
||||
irqCounter = irqLatch;
|
||||
irqLine = 1;
|
||||
} else {
|
||||
irqCounter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
cpu.irqLine(irqLine);
|
||||
|
||||
tick();
|
||||
}
|
||||
|
||||
auto writeIO(uint addr, uint8 data) -> void {
|
||||
switch(addr) {
|
||||
case 0x8000: prgBank[0] = data; break;
|
||||
case 0x8010: prgBank[1] = data; break;
|
||||
case 0x9000: prgBank[2] = data; break;
|
||||
case 0x9010: break; //APU addr port
|
||||
case 0x9030: break; //APU data port
|
||||
case 0xa000: chrBank[0] = data; break;
|
||||
case 0xa010: chrBank[1] = data; break;
|
||||
case 0xb000: chrBank[2] = data; break;
|
||||
case 0xb010: chrBank[3] = data; break;
|
||||
case 0xc000: chrBank[4] = data; break;
|
||||
case 0xc010: chrBank[5] = data; break;
|
||||
case 0xd000: chrBank[6] = data; break;
|
||||
case 0xd010: chrBank[7] = data; break;
|
||||
case 0xe000: mirror = data & 0x03; break;
|
||||
|
||||
case 0xe010:
|
||||
irqLatch = data;
|
||||
break;
|
||||
|
||||
case 0xf000:
|
||||
irqMode = data & 0x04;
|
||||
irqEnable = data & 0x02;
|
||||
irqAcknowledge = data & 0x01;
|
||||
if(irqEnable) {
|
||||
irqCounter = irqLatch;
|
||||
irqScalar = 341;
|
||||
}
|
||||
irqLine = 0;
|
||||
break;
|
||||
|
||||
case 0xf010:
|
||||
irqEnable = irqAcknowledge;
|
||||
irqLine = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto addrPRG(uint addr) const -> uint {
|
||||
uint bank = 0;
|
||||
switch(addr & 0xe000) {
|
||||
case 0x8000: bank = prgBank[0]; break;
|
||||
case 0xa000: bank = prgBank[1]; break;
|
||||
case 0xc000: bank = prgBank[2]; break;
|
||||
case 0xe000: bank = 0xff; break;
|
||||
}
|
||||
return (bank * 0x2000) + (addr & 0x1fff);
|
||||
}
|
||||
|
||||
auto addrCHR(uint addr) const -> uint {
|
||||
uint bank = chrBank[addr / 0x0400];
|
||||
return (bank * 0x0400) + (addr & 0x03ff);
|
||||
}
|
||||
|
||||
auto addrCIRAM(uint addr) const -> uint {
|
||||
switch(mirror) {
|
||||
case 0: return ((addr & 0x0400) >> 0) | (addr & 0x03ff); //vertical mirroring
|
||||
case 1: return ((addr & 0x0800) >> 1) | (addr & 0x03ff); //horizontal mirroring
|
||||
case 2: return 0x0000 | (addr & 0x03ff); //one-screen mirroring (first)
|
||||
case 3: return 0x0400 | (addr & 0x03ff); //one-screen mirroring (second)
|
||||
}
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
for(auto& n : prgBank) n = 0;
|
||||
for(auto& n : chrBank) n = 0;
|
||||
mirror = 0;
|
||||
|
||||
irqLatch = 0;
|
||||
irqMode = 0;
|
||||
irqEnable = 0;
|
||||
irqAcknowledge = 0;
|
||||
|
||||
irqCounter = 0;
|
||||
irqScalar = 0;
|
||||
irqLine = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
s.array(prgBank);
|
||||
s.array(chrBank);
|
||||
s.integer(mirror);
|
||||
|
||||
s.integer(irqLatch);
|
||||
s.integer(irqMode);
|
||||
s.integer(irqEnable);
|
||||
s.integer(irqAcknowledge);
|
||||
|
||||
s.integer(irqCounter);
|
||||
s.integer(irqScalar);
|
||||
s.integer(irqLine);
|
||||
}
|
||||
|
||||
uint8 prgBank[3];
|
||||
uint8 chrBank[8];
|
||||
uint2 mirror;
|
||||
|
||||
uint8 irqLatch;
|
||||
bool irqMode;
|
||||
bool irqEnable;
|
||||
bool irqAcknowledge;
|
||||
|
||||
uint8 irqCounter;
|
||||
int irqScalar;
|
||||
bool irqLine;
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
auto Cartridge::serialize(serializer& s) -> void {
|
||||
Thread::serialize(s);
|
||||
return board->serialize(s);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#include <fc/fc.hpp>
|
||||
|
||||
namespace Famicom {
|
||||
|
||||
ControllerPort controllerPort1;
|
||||
ControllerPort controllerPort2;
|
||||
#include "gamepad/gamepad.cpp"
|
||||
|
||||
Controller::Controller(uint port) : port(port) {
|
||||
if(!handle()) create(Controller::Enter, 1);
|
||||
}
|
||||
|
||||
Controller::~Controller() {
|
||||
scheduler.remove(*this);
|
||||
}
|
||||
|
||||
auto Controller::Enter() -> void {
|
||||
while(true) {
|
||||
scheduler.synchronize();
|
||||
if(controllerPort1.device->active()) controllerPort1.device->main();
|
||||
if(controllerPort2.device->active()) controllerPort2.device->main();
|
||||
}
|
||||
}
|
||||
|
||||
auto Controller::main() -> void {
|
||||
step(1);
|
||||
synchronize(cpu);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
auto ControllerPort::connect(uint deviceID) -> void {
|
||||
if(!system.loaded()) return;
|
||||
delete device;
|
||||
|
||||
switch(deviceID) { default:
|
||||
case ID::Device::None: device = new Controller(port); break;
|
||||
case ID::Device::Gamepad: device = new Gamepad(port); break;
|
||||
}
|
||||
|
||||
cpu.peripherals.reset();
|
||||
if(auto device = controllerPort1.device) cpu.peripherals.append(device);
|
||||
if(auto device = controllerPort2.device) cpu.peripherals.append(device);
|
||||
}
|
||||
|
||||
auto ControllerPort::power(uint port) -> void {
|
||||
this->port = port;
|
||||
}
|
||||
|
||||
auto ControllerPort::unload() -> void {
|
||||
delete device;
|
||||
device = nullptr;
|
||||
}
|
||||
|
||||
auto ControllerPort::serialize(serializer& s) -> void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//Famicom controller port pinout:
|
||||
// ____
|
||||
// | \
|
||||
// |(7) \
|
||||
// |(2)(1)|
|
||||
// |(3)(5)|
|
||||
// |(4)(6)|
|
||||
// |______|
|
||||
//
|
||||
// pin name port1 port2
|
||||
// 1: +5v
|
||||
// 2: clock $4016 read $4016.d0 write
|
||||
// 3: latch $4016.d0 write $4016.d0 write
|
||||
// 4: data0 $4016.d0 read $4017.d0 read
|
||||
// 5: data3 $4016.d3 read $4017.d3 read
|
||||
// 6: data4 $4016.d4 read $4017.d4 read
|
||||
// 7: gnd
|
||||
|
||||
struct Controller : Thread {
|
||||
Controller(uint port);
|
||||
virtual ~Controller();
|
||||
static auto Enter() -> void;
|
||||
|
||||
virtual auto main() -> void;
|
||||
virtual auto data() -> uint3 { return 0; }
|
||||
virtual auto latch(bool data) -> void {}
|
||||
|
||||
const uint port;
|
||||
};
|
||||
|
||||
struct ControllerPort {
|
||||
auto connect(uint deviceID) -> void;
|
||||
|
||||
auto power(uint port) -> void;
|
||||
auto unload() -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
uint port;
|
||||
Controller* device = nullptr;
|
||||
};
|
||||
|
||||
extern ControllerPort controllerPort1;
|
||||
extern ControllerPort controllerPort2;
|
||||
|
||||
#include "gamepad/gamepad.hpp"
|
||||
@@ -1,36 +0,0 @@
|
||||
Gamepad::Gamepad(uint port) : Controller(port) {
|
||||
}
|
||||
|
||||
auto Gamepad::data() -> uint3 {
|
||||
if(counter >= 8) return 1;
|
||||
if(latched == 1) return platform->inputPoll(port, ID::Device::Gamepad, A);
|
||||
|
||||
switch(counter++) {
|
||||
case 0: return a;
|
||||
case 1: return b;
|
||||
case 2: return select;
|
||||
case 3: return start;
|
||||
case 4: return up && !down;
|
||||
case 5: return down && !up;
|
||||
case 6: return left && !right;
|
||||
case 7: return right && !left;
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
|
||||
auto Gamepad::latch(bool data) -> void {
|
||||
if(latched == data) return;
|
||||
latched = data;
|
||||
counter = 0;
|
||||
|
||||
if(latched == 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
struct Gamepad : Controller {
|
||||
enum : uint {
|
||||
Up, Down, Left, Right, B, A, Select, Start,
|
||||
};
|
||||
|
||||
Gamepad(uint port);
|
||||
auto data() -> uint3;
|
||||
auto latch(bool data) -> void;
|
||||
|
||||
private:
|
||||
bool latched = 0;
|
||||
uint counter = 0;
|
||||
|
||||
bool a = 0;
|
||||
bool b = 0;
|
||||
bool select = 0;
|
||||
bool start = 0;
|
||||
bool up = 0;
|
||||
bool down = 0;
|
||||
bool left = 0;
|
||||
bool right = 0;
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
#include <fc/fc.hpp>
|
||||
|
||||
namespace Famicom {
|
||||
|
||||
#include "memory.cpp"
|
||||
#include "timing.cpp"
|
||||
#include "serialization.cpp"
|
||||
CPU cpu;
|
||||
|
||||
auto CPU::Enter() -> void {
|
||||
while(true) scheduler.synchronize(), cpu.main();
|
||||
}
|
||||
|
||||
auto CPU::main() -> void {
|
||||
if(io.interruptPending) return interrupt();
|
||||
instruction();
|
||||
}
|
||||
|
||||
auto CPU::step(uint clocks) -> void {
|
||||
Thread::step(clocks);
|
||||
synchronize(apu);
|
||||
synchronize(ppu);
|
||||
synchronize(cartridge);
|
||||
for(auto peripheral : peripherals) synchronize(*peripheral);
|
||||
}
|
||||
|
||||
auto CPU::power() -> void {
|
||||
MOS6502::BCD = 0;
|
||||
MOS6502::power();
|
||||
create(CPU::Enter, system.frequency());
|
||||
|
||||
for(auto addr : range(0x0800)) ram[addr] = 0xff;
|
||||
ram[0x0008] = 0xf7;
|
||||
ram[0x0009] = 0xef;
|
||||
ram[0x000a] = 0xdf;
|
||||
ram[0x000f] = 0xbf;
|
||||
|
||||
r.pc.byte(0) = bus.read(0xfffc);
|
||||
r.pc.byte(1) = bus.read(0xfffd);
|
||||
|
||||
memory::fill(&io, sizeof(IO));
|
||||
io.rdyLine = 1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
struct CPU : Processor::MOS6502, Thread {
|
||||
inline auto rate() const -> uint { return Region::PAL() ? 16 : 12; }
|
||||
|
||||
//cpu.cpp
|
||||
static auto Enter() -> void;
|
||||
auto main() -> void;
|
||||
auto step(uint clocks) -> void;
|
||||
|
||||
auto power() -> void;
|
||||
|
||||
//memory.cpp
|
||||
auto readRAM(uint11 addr) -> uint8;
|
||||
auto writeRAM(uint11 addr, uint8 data) -> void;
|
||||
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
|
||||
auto readDebugger(uint16 addr) -> uint8 override;
|
||||
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
//timing.cpp
|
||||
auto read(uint16 addr) -> uint8 override;
|
||||
auto write(uint16 addr, uint8 data) -> void override;
|
||||
auto lastCycle() -> void override;
|
||||
auto nmi(uint16& vector) -> void override;
|
||||
|
||||
auto oamdma() -> void;
|
||||
|
||||
auto nmiLine(bool) -> void;
|
||||
auto irqLine(bool) -> void;
|
||||
auto apuLine(bool) -> void;
|
||||
|
||||
auto rdyLine(bool) -> void;
|
||||
auto rdyAddr(bool valid, uint16 value = 0) -> void;
|
||||
|
||||
//protected:
|
||||
vector<Thread*> peripherals;
|
||||
|
||||
uint8 ram[0x0800];
|
||||
|
||||
struct IO {
|
||||
bool interruptPending;
|
||||
bool nmiPending;
|
||||
bool nmiLine;
|
||||
bool irqLine;
|
||||
bool apuLine;
|
||||
|
||||
bool rdyLine;
|
||||
bool rdyAddrValid;
|
||||
uint16 rdyAddrValue;
|
||||
|
||||
bool oamdmaPending;
|
||||
uint8 oamdmaPage;
|
||||
} io;
|
||||
};
|
||||
|
||||
extern CPU cpu;
|
||||
@@ -1,49 +0,0 @@
|
||||
auto CPU::readRAM(uint11 addr) -> uint8 {
|
||||
return ram[addr];
|
||||
}
|
||||
|
||||
auto CPU::writeRAM(uint11 addr, uint8 data) -> void {
|
||||
ram[addr] = data;
|
||||
}
|
||||
|
||||
auto CPU::readIO(uint16 addr) -> uint8 {
|
||||
switch(addr) {
|
||||
|
||||
case 0x4016: {
|
||||
auto data = controllerPort1.device->data();
|
||||
return (mdr() & 0xc0) | data.bit(2) << 4 | data.bit(1) << 3 | data.bit(0) << 0;
|
||||
}
|
||||
|
||||
case 0x4017: {
|
||||
auto data = controllerPort2.device->data();
|
||||
return (mdr() & 0xc0) | data.bit(2) << 4 | data.bit(1) << 3 | data.bit(0) << 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return apu.readIO(addr);
|
||||
}
|
||||
|
||||
auto CPU::writeIO(uint16 addr, uint8 data) -> void {
|
||||
switch(addr) {
|
||||
|
||||
case 0x4014: {
|
||||
io.oamdmaPage = data;
|
||||
io.oamdmaPending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
case 0x4016: {
|
||||
controllerPort1.device->latch(data.bit(0));
|
||||
controllerPort2.device->latch(data.bit(0));
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return apu.writeIO(addr, data);
|
||||
}
|
||||
|
||||
auto CPU::readDebugger(uint16 addr) -> uint8 {
|
||||
return bus.read(addr);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
auto CPU::serialize(serializer& s) -> void {
|
||||
MOS6502::serialize(s);
|
||||
Thread::serialize(s);
|
||||
|
||||
s.array(ram);
|
||||
|
||||
s.integer(io.interruptPending);
|
||||
s.integer(io.nmiPending);
|
||||
s.integer(io.nmiLine);
|
||||
s.integer(io.irqLine);
|
||||
s.integer(io.apuLine);
|
||||
|
||||
s.integer(io.rdyLine);
|
||||
s.integer(io.rdyAddrValid);
|
||||
s.integer(io.rdyAddrValue);
|
||||
|
||||
s.integer(io.oamdmaPending);
|
||||
s.integer(io.oamdmaPage);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
auto CPU::read(uint16 addr) -> uint8 {
|
||||
if(io.oamdmaPending) {
|
||||
io.oamdmaPending = false;
|
||||
read(addr);
|
||||
oamdma();
|
||||
}
|
||||
|
||||
while(io.rdyLine == 0) {
|
||||
r.mdr = bus.read(io.rdyAddrValid ? io.rdyAddrValue : addr);
|
||||
step(rate());
|
||||
}
|
||||
|
||||
r.mdr = bus.read(addr);
|
||||
step(rate());
|
||||
return r.mdr;
|
||||
}
|
||||
|
||||
auto CPU::write(uint16 addr, uint8 data) -> void {
|
||||
bus.write(addr, r.mdr = data);
|
||||
step(rate());
|
||||
}
|
||||
|
||||
auto CPU::lastCycle() -> void {
|
||||
io.interruptPending = ((io.irqLine | io.apuLine) & ~r.p.i) | io.nmiPending;
|
||||
}
|
||||
|
||||
auto CPU::nmi(uint16& vector) -> void {
|
||||
if(io.nmiPending) {
|
||||
io.nmiPending = false;
|
||||
vector = 0xfffa;
|
||||
}
|
||||
}
|
||||
|
||||
auto CPU::oamdma() -> void {
|
||||
for(uint n : range(256)) {
|
||||
uint8 data = read(io.oamdmaPage << 8 | n);
|
||||
write(0x2004, data);
|
||||
}
|
||||
}
|
||||
|
||||
auto CPU::nmiLine(bool line) -> void {
|
||||
//edge-sensitive (0->1)
|
||||
if(!io.nmiLine && line) io.nmiPending = true;
|
||||
io.nmiLine = line;
|
||||
}
|
||||
|
||||
auto CPU::irqLine(bool line) -> void {
|
||||
//level-sensitive
|
||||
io.irqLine = line;
|
||||
}
|
||||
|
||||
auto CPU::apuLine(bool line) -> void {
|
||||
//level-sensitive
|
||||
io.apuLine = line;
|
||||
}
|
||||
|
||||
auto CPU::rdyLine(bool line) -> void {
|
||||
io.rdyLine = line;
|
||||
}
|
||||
|
||||
auto CPU::rdyAddr(bool valid, uint16 value) -> void {
|
||||
io.rdyAddrValid = valid;
|
||||
io.rdyAddrValue = value;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
//license: GPLv3
|
||||
//started: 2011-09-05
|
||||
|
||||
#include <emulator/emulator.hpp>
|
||||
#include <emulator/thread.hpp>
|
||||
#include <emulator/scheduler.hpp>
|
||||
#include <emulator/cheat.hpp>
|
||||
|
||||
#include <processor/mos6502/mos6502.hpp>
|
||||
|
||||
namespace Famicom {
|
||||
#define platform Emulator::platform
|
||||
namespace File = Emulator::File;
|
||||
using Scheduler = Emulator::Scheduler;
|
||||
using Cheat = Emulator::Cheat;
|
||||
extern Scheduler scheduler;
|
||||
extern Cheat cheat;
|
||||
|
||||
struct Thread : Emulator::Thread {
|
||||
auto create(auto (*entrypoint)() -> void, double frequency) -> void {
|
||||
Emulator::Thread::create(entrypoint, frequency);
|
||||
scheduler.append(*this);
|
||||
}
|
||||
|
||||
inline auto synchronize(Thread& thread) -> void {
|
||||
if(clock() >= thread.clock()) scheduler.resume(thread);
|
||||
}
|
||||
};
|
||||
|
||||
struct Region {
|
||||
static inline auto NTSCJ() -> bool;
|
||||
static inline auto NTSCU() -> bool;
|
||||
static inline auto PAL() -> bool;
|
||||
};
|
||||
|
||||
#include <fc/controller/controller.hpp>
|
||||
#include <fc/system/system.hpp>
|
||||
#include <fc/memory/memory.hpp>
|
||||
#include <fc/cartridge/cartridge.hpp>
|
||||
#include <fc/cpu/cpu.hpp>
|
||||
#include <fc/apu/apu.hpp>
|
||||
#include <fc/ppu/ppu.hpp>
|
||||
}
|
||||
|
||||
#include <fc/interface/interface.hpp>
|
||||
Loaded 100 of 2079 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user