mirror of
https://github.com/bsnes-emu/bsnes.git
synced 2025-09-16 13:02:11 +02:00
Compare commits
161 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
722a2d797d | ||
|
60345fe8b5 | ||
|
f3af7f177b | ||
|
5f67b2a8fc | ||
|
1ae228663d | ||
|
59eb05226f | ||
|
fe9820481f | ||
|
1faa263a4a | ||
|
45c8f09330 | ||
|
09c9a55588 | ||
|
defb1eedd3 | ||
|
accffa8d1b | ||
|
4c4a1dcb67 | ||
|
c52b05a0de | ||
|
5af4f45a25 | ||
|
a350195c8c | ||
|
55bae99bb6 | ||
|
0ca6aaf6b7 | ||
|
bc9e8c8f4a | ||
|
b26db6c9b8 | ||
|
246887d65c | ||
|
0749009652 | ||
|
3a967430eb | ||
|
ce8f008894 | ||
|
f6fab1a502 | ||
|
a8363c41ba | ||
|
e888a607c1 | ||
|
fb52220b1b | ||
|
7760931ffc | ||
|
df2492c75e | ||
|
6e6c29138e | ||
|
5a44f249a4 | ||
|
3c3f1de317 | ||
|
a99e601c87 | ||
|
a62425745b | ||
|
232bcc0fa7 | ||
|
926d65cfe0 | ||
|
37a738a1a4 | ||
|
3e0adb1798 | ||
|
f5333c333c | ||
|
427ffd97b8 | ||
|
8df735e6e2 | ||
|
b7edf2abcf | ||
|
50d5beda53 | ||
|
65dadd1576 | ||
|
6f9839df62 | ||
|
781f1ff4c0 | ||
|
66ef548781 | ||
|
cabb865d5b | ||
|
54ffe85479 | ||
|
362d29c6a7 | ||
|
ecbc3f6693 | ||
|
1cdd539115 | ||
|
1870788cb8 | ||
|
1b77a4ec8d | ||
|
b02c35edae | ||
|
e721064fe9 | ||
|
f1e48b90ff | ||
|
6099424f0f | ||
|
43c5368a30 | ||
|
f4bf02ba02 | ||
|
8c3d076bf4 | ||
|
788bdb0ae2 | ||
|
39822bc123 | ||
|
6ba931987b | ||
|
4c4339d313 | ||
|
d166c280c7 | ||
|
73a37beae1 | ||
|
b61ca253de | ||
|
0ed9737dd4 | ||
|
adddc1dc0a | ||
|
b021f5186c | ||
|
1a68cb65eb | ||
|
82a64bb3ab | ||
|
75f842cdb2 | ||
|
9362726c65 | ||
|
a3e1983d50 | ||
|
20e3fdd1e3 | ||
|
0bb6a3b0bc | ||
|
752139e66c | ||
|
2f08bc3b1c | ||
|
f4c043ce50 | ||
|
1bbe78f94e | ||
|
ef5f0e9c21 | ||
|
83ae1579a0 | ||
|
e0b4fe6419 | ||
|
efbfe854d2 | ||
|
5f2fbd0d72 | ||
|
e0c021660a | ||
|
89192cee6c | ||
|
e8b9b6cd7b | ||
|
3efe3cdfbf | ||
|
224bbe03f4 | ||
|
aad87da68a | ||
|
444ec756bd | ||
|
96ebd87db4 | ||
|
8ef704c16b | ||
|
380130fbd8 | ||
|
859c5b587b | ||
|
7eceacf78e | ||
|
b3b3690948 | ||
|
331c53c896 | ||
|
fd2cdc261c | ||
|
d366c6ff43 | ||
|
eef90a8f14 | ||
|
00fecd96a4 | ||
|
f23e54f6b1 | ||
|
54e48aabbe | ||
|
0902869032 | ||
|
7d1685f7f5 | ||
|
775be26350 | ||
|
d38893008c | ||
|
d250096e48 | ||
|
68d57dc46c | ||
|
acbc52ab07 | ||
|
7406d903e1 | ||
|
d739e33243 | ||
|
1b62f18139 | ||
|
d0d7436d50 | ||
|
08fc1f659a | ||
|
e8b2f22623 | ||
|
cec9d91b39 | ||
|
11a3195e84 | ||
|
1fb37f087f | ||
|
8ece9ed16b | ||
|
302b369781 | ||
|
ba384a7c48 | ||
|
55f19c3e0d | ||
|
406b6a61a5 | ||
|
1067566834 | ||
|
559eeccc89 | ||
|
a72ff8b7fa | ||
|
0b6f1df987 | ||
|
020caa546d | ||
|
c2975e6898 | ||
|
571760c747 | ||
|
7022d1aa51 | ||
|
e1223366a7 | ||
|
80841deaa5 | ||
|
d5c09c9ab1 | ||
|
8be474b0ac | ||
|
284e4c043e | ||
|
0b4e7fb5a5 | ||
|
f87c6b7ecb | ||
|
4129630d97 | ||
|
17697317d4 | ||
|
ed5ec58595 | ||
|
434e303ffb | ||
|
ee982f098a | ||
|
cbbf5ec114 | ||
|
7af270aa59 | ||
|
191a71b291 | ||
|
d4876a831f | ||
|
16f736307e | ||
|
40802b0b9f | ||
|
ff3750de4f | ||
|
78f341489e | ||
|
3517d5c4a4 | ||
|
ecc7e899e0 | ||
|
f6d7922e62 | ||
|
a2baea248f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
higan/profile/WonderSwan.sys/internal.ram
|
||||
higan/profile/WonderSwan Color.sys/internal.ram
|
||||
docs_build/
|
||||
|
@@ -12,8 +12,8 @@ linux-x86_64-binaries:
|
||||
- cp -a icarus/out/icarus higan-nightly/icarus
|
||||
- cp -a icarus/Database higan-nightly/
|
||||
- cp -a higan/out/higan higan-nightly/higan
|
||||
- cp -a higan/data/cheats.bml higan-nightly/
|
||||
- cp -a higan/systems/* higan-nightly/
|
||||
- cp -a shaders "higan-nightly/Video Shaders"
|
||||
artifacts:
|
||||
paths:
|
||||
- higan-nightly/*
|
||||
@@ -36,8 +36,8 @@ windows-x86_64-binaries:
|
||||
- cp -a icarus/out/icarus higan-nightly/icarus.exe
|
||||
- cp -a icarus/Database higan-nightly/
|
||||
- cp -a higan/out/higan higan-nightly/higan.exe
|
||||
- cp -a higan/data/cheats.bml higan-nightly/
|
||||
- cp -a higan/systems/* higan-nightly/
|
||||
- cp -a shaders "higan-nightly/Video Shaders"
|
||||
artifacts:
|
||||
paths:
|
||||
- higan-nightly/*
|
||||
|
9
CONTRIBUTING.md
Normal file
9
CONTRIBUTING.md
Normal file
@@ -0,0 +1,9 @@
|
||||
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/
|
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
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/
|
12
docs/checklinks.sh
Normal file
12
docs/checklinks.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/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/
|
141
docs/concepts/game-folders.md
Normal file
141
docs/concepts/game-folders.md
Normal file
@@ -0,0 +1,141 @@
|
||||
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).
|
56
docs/concepts/game-library.md
Normal file
56
docs/concepts/game-library.md
Normal file
@@ -0,0 +1,56 @@
|
||||
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.
|
||||
|
||||
|
129
docs/concepts/save-states.md
Normal file
129
docs/concepts/save-states.md
Normal file
@@ -0,0 +1,129 @@
|
||||
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
docs/faq.md
Normal file
279
docs/faq.md
Normal file
@@ -0,0 +1,279 @@
|
||||
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.
|
145
docs/guides/drivers.md
Normal file
145
docs/guides/drivers.md
Normal file
@@ -0,0 +1,145 @@
|
||||
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.
|
560
docs/guides/import.md
Normal file
560
docs/guides/import.md
Normal file
@@ -0,0 +1,560 @@
|
||||
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/
|
||||
|
126
docs/guides/shaders.md
Normal file
126
docs/guides/shaders.md
Normal file
@@ -0,0 +1,126 @@
|
||||
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
docs/index.md
Normal file
117
docs/index.md
Normal file
@@ -0,0 +1,117 @@
|
||||
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.
|
52
docs/install/general.md
Normal file
52
docs/install/general.md
Normal file
@@ -0,0 +1,52 @@
|
||||
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
|
136
docs/install/linux.md
Normal file
136
docs/install/linux.md
Normal file
@@ -0,0 +1,136 @@
|
||||
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.
|
163
docs/install/windows.md
Normal file
163
docs/install/windows.md
Normal file
@@ -0,0 +1,163 @@
|
||||
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
|
61
docs/interface/common.md
Normal file
61
docs/interface/common.md
Normal file
@@ -0,0 +1,61 @@
|
||||
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.
|
40
docs/interface/higan-cli.md
Normal file
40
docs/interface/higan-cli.md
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
# 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"
|
||||
```
|
298
docs/interface/higan-config.md
Normal file
298
docs/interface/higan-config.md
Normal file
@@ -0,0 +1,298 @@
|
||||
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).
|
145
docs/interface/higan-tools.md
Normal file
145
docs/interface/higan-tools.md
Normal file
@@ -0,0 +1,145 @@
|
||||
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.
|
290
docs/interface/higan.md
Normal file
290
docs/interface/higan.md
Normal file
@@ -0,0 +1,290 @@
|
||||
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.
|
60
docs/interface/icarus.md
Normal file
60
docs/interface/icarus.md
Normal file
@@ -0,0 +1,60 @@
|
||||
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
docs/notes.md
Normal file
311
docs/notes.md
Normal file
@@ -0,0 +1,311 @@
|
||||
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.
|
7
docs/qs.md
Normal file
7
docs/qs.md
Normal file
@@ -0,0 +1,7 @@
|
||||
TODO
|
||||
|
||||
- install
|
||||
- configure inputs
|
||||
- load a game
|
||||
- connect a controller
|
||||
|
@@ -31,6 +31,13 @@ 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;
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <nall/dsp/iir/one-pole.hpp>
|
||||
#include <nall/dsp/iir/biquad.hpp>
|
||||
#include <nall/dsp/resampler/cubic.hpp>
|
||||
|
||||
@@ -7,12 +8,14 @@ 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;
|
||||
@@ -37,15 +40,25 @@ private:
|
||||
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 addLowPassFilter(double cutoffFrequency, uint passes = 1) -> void;
|
||||
auto addHighPassFilter(double cutoffFrequency, uint passes = 1) -> 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;
|
||||
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)...};
|
||||
@@ -54,7 +67,7 @@ struct Stream {
|
||||
|
||||
private:
|
||||
struct Channel {
|
||||
vector<DSP::IIR::Biquad> filters;
|
||||
vector<Filter> filters;
|
||||
DSP::Resampler::Cubic resampler;
|
||||
};
|
||||
vector<Channel> channels;
|
||||
|
@@ -11,22 +11,36 @@ auto Stream::reset(uint channels_, double inputFrequency, double outputFrequency
|
||||
}
|
||||
}
|
||||
|
||||
auto Stream::addLowPassFilter(double cutoffFrequency, uint passes) -> void {
|
||||
auto Stream::setFrequency(double inputFrequency, maybe<double> outputFrequency) -> void {
|
||||
this->inputFrequency = inputFrequency;
|
||||
if(outputFrequency) this->outputFrequency = outputFrequency();
|
||||
|
||||
for(auto& channel : channels) {
|
||||
for(auto pass : range(passes)) {
|
||||
double q = DSP::IIR::Biquad::butterworth(passes * 2, pass);
|
||||
channel.filters.append(DSP::IIR::Biquad{});
|
||||
channel.filters.right().reset(DSP::IIR::Biquad::Type::LowPass, cutoffFrequency, inputFrequency, q);
|
||||
}
|
||||
channel.resampler.reset(this->inputFrequency, this->outputFrequency);
|
||||
}
|
||||
}
|
||||
|
||||
auto Stream::addHighPassFilter(double cutoffFrequency, uint passes) -> void {
|
||||
auto Stream::addFilter(Filter::Order order, Filter::Type type, double cutoffFrequency, uint passes) -> void {
|
||||
for(auto& channel : channels) {
|
||||
for(auto pass : range(passes)) {
|
||||
double q = DSP::IIR::Biquad::butterworth(passes * 2, pass);
|
||||
channel.filters.append(DSP::IIR::Biquad{});
|
||||
channel.filters.right().reset(DSP::IIR::Biquad::Type::HighPass, cutoffFrequency, inputFrequency, q);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,15 +49,20 @@ auto Stream::pending() const -> bool {
|
||||
return channels && channels[0].resampler.pending();
|
||||
}
|
||||
|
||||
auto Stream::read(double* samples) -> uint {
|
||||
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 {
|
||||
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) sample = filter.process(sample);
|
||||
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);
|
||||
}
|
||||
|
||||
|
@@ -6,4 +6,9 @@
|
||||
<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>
|
||||
|
@@ -11,7 +11,7 @@
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>higan.icns</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<false/>
|
||||
<true/>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
@@ -12,13 +12,13 @@ using namespace nall;
|
||||
|
||||
namespace Emulator {
|
||||
static const string Name = "higan";
|
||||
static const string Version = "103";
|
||||
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 = "103";
|
||||
static const string SerializerVersion = "104";
|
||||
|
||||
namespace Constants {
|
||||
namespace Colorburst {
|
||||
|
@@ -9,11 +9,6 @@ struct Interface {
|
||||
bool overscan;
|
||||
} information;
|
||||
|
||||
struct Region {
|
||||
string name;
|
||||
};
|
||||
vector<Region> regions;
|
||||
|
||||
struct Medium {
|
||||
uint id;
|
||||
string name;
|
||||
@@ -43,9 +38,14 @@ struct Interface {
|
||||
virtual auto title() -> string = 0;
|
||||
|
||||
//video information
|
||||
struct VideoSize { uint width, height; };
|
||||
virtual auto videoResolution() -> VideoSize = 0;
|
||||
virtual auto videoSize(uint width, uint height, bool arc) -> VideoSize = 0;
|
||||
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;
|
||||
|
||||
|
@@ -74,8 +74,10 @@ auto APU::setSample(int16 sample) -> void {
|
||||
auto APU::power() -> void {
|
||||
create(APU::Enter, system.frequency());
|
||||
stream = Emulator::audio.createStream(1, frequency() / rate());
|
||||
stream->addLowPassFilter(20000.0, 3);
|
||||
stream->addHighPassFilter(20.0, 3);
|
||||
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();
|
||||
@@ -151,7 +153,7 @@ auto APU::writeIO(uint16 addr, uint8 data) -> void {
|
||||
pulse[n].period = (pulse[n].period & 0x00ff) | (data << 8);
|
||||
pulse[n].sweep.pulsePeriod = (pulse[n].sweep.pulsePeriod & 0x00ff) | (data << 8);
|
||||
|
||||
pulse[n].dutyCounter = 7;
|
||||
pulse[n].dutyCounter = 0;
|
||||
pulse[n].envelope.reloadDecay = true;
|
||||
|
||||
if(enabledChannels & (1 << n)) {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
struct APU : Thread {
|
||||
shared_pointer<Emulator::Stream> stream;
|
||||
|
||||
inline auto rate() const -> uint { return Region::NTSC() ? 12 : 16; }
|
||||
inline auto rate() const -> uint { return Region::PAL() ? 16 : 12; }
|
||||
|
||||
//apu.cpp
|
||||
APU();
|
||||
|
@@ -57,7 +57,7 @@ auto APU::DMC::clock() -> uint8 {
|
||||
}
|
||||
}
|
||||
|
||||
periodCounter = Region::NTSC() ? dmcPeriodTableNTSC[period] : dmcPeriodTablePAL[period];
|
||||
periodCounter = Region::PAL() ? dmcPeriodTablePAL[period] : dmcPeriodTableNTSC[period];
|
||||
}
|
||||
|
||||
if(lengthCounter > 0 && !dmaBufferValid && dmaDelayCounter == 0) {
|
||||
@@ -73,7 +73,7 @@ auto APU::DMC::power() -> void {
|
||||
irqPending = 0;
|
||||
|
||||
period = 0;
|
||||
periodCounter = Region::NTSC() ? dmcPeriodTableNTSC[0] : dmcPeriodTablePAL[0];
|
||||
periodCounter = Region::PAL() ? dmcPeriodTablePAL[0] : dmcPeriodTableNTSC[0];
|
||||
irqEnable = 0;
|
||||
loopMode = 0;
|
||||
dacLatch = 0;
|
||||
|
@@ -19,7 +19,7 @@ auto APU::Noise::clock() -> uint8 {
|
||||
}
|
||||
|
||||
lfsr = (lfsr >> 1) | (feedback << 14);
|
||||
periodCounter = Region::NTSC() ? apu.noisePeriodTableNTSC[period] : apu.noisePeriodTablePAL[period];
|
||||
periodCounter = Region::PAL() ? apu.noisePeriodTablePAL[period] : apu.noisePeriodTableNTSC[period];
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@@ -8,13 +8,18 @@ auto APU::Pulse::clock() -> uint8 {
|
||||
if(!sweep.checkPeriod()) return 0;
|
||||
if(lengthCounter == 0) return 0;
|
||||
|
||||
static const uint dutyTable[] = {1, 2, 4, 6};
|
||||
uint8 result = (dutyCounter < dutyTable[duty]) ? envelope.volume() : 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++;
|
||||
dutyCounter--;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@@ -16,7 +16,7 @@ auto Cartridge::main() -> void {
|
||||
}
|
||||
|
||||
auto Cartridge::load() -> bool {
|
||||
if(auto loaded = platform->load(ID::Famicom, "Famicom", "fc", {"NTSC", "PAL"})) {
|
||||
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;
|
||||
|
@@ -2,7 +2,7 @@
|
||||
#include "board/board.hpp"
|
||||
|
||||
struct Cartridge : Thread {
|
||||
inline auto rate() const -> uint { return Region::NTSC() ? 12 : 16; }
|
||||
inline auto rate() const -> uint { return Region::PAL() ? 16 : 12; }
|
||||
|
||||
//cartridge.cpp
|
||||
static auto Enter() -> void;
|
||||
|
@@ -2,9 +2,11 @@
|
||||
|
||||
namespace Famicom {
|
||||
|
||||
ControllerPort controllerPort1;
|
||||
ControllerPort controllerPort2;
|
||||
#include "gamepad/gamepad.cpp"
|
||||
|
||||
Controller::Controller(bool port) : port(port) {
|
||||
Controller::Controller(uint port) : port(port) {
|
||||
if(!handle()) create(Controller::Enter, 1);
|
||||
}
|
||||
|
||||
@@ -15,8 +17,8 @@ Controller::~Controller() {
|
||||
auto Controller::Enter() -> void {
|
||||
while(true) {
|
||||
scheduler.synchronize();
|
||||
if(peripherals.controllerPort1->active()) peripherals.controllerPort1->main();
|
||||
if(peripherals.controllerPort2->active()) peripherals.controllerPort2->main();
|
||||
if(controllerPort1.device->active()) controllerPort1.device->main();
|
||||
if(controllerPort2.device->active()) controllerPort2.device->main();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,4 +27,32 @@ auto Controller::main() -> void {
|
||||
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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -17,9 +17,7 @@
|
||||
// 7: gnd
|
||||
|
||||
struct Controller : Thread {
|
||||
enum : bool { Port1 = 0, Port2 = 1 };
|
||||
|
||||
Controller(bool port);
|
||||
Controller(uint port);
|
||||
virtual ~Controller();
|
||||
static auto Enter() -> void;
|
||||
|
||||
@@ -27,7 +25,21 @@ struct Controller : Thread {
|
||||
virtual auto data() -> uint3 { return 0; }
|
||||
virtual auto latch(bool data) -> void {}
|
||||
|
||||
const bool port;
|
||||
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,4 +1,4 @@
|
||||
Gamepad::Gamepad(bool port) : Controller(port) {
|
||||
Gamepad::Gamepad(uint port) : Controller(port) {
|
||||
}
|
||||
|
||||
auto Gamepad::data() -> uint3 {
|
||||
|
@@ -3,7 +3,7 @@ struct Gamepad : Controller {
|
||||
Up, Down, Left, Right, B, A, Select, Start,
|
||||
};
|
||||
|
||||
Gamepad(bool port);
|
||||
Gamepad(uint port);
|
||||
auto data() -> uint3;
|
||||
auto latch(bool data) -> void;
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
struct CPU : Processor::MOS6502, Thread {
|
||||
inline auto rate() const -> uint { return Region::NTSC() ? 12 : 16; }
|
||||
inline auto rate() const -> uint { return Region::PAL() ? 16 : 12; }
|
||||
|
||||
//cpu.cpp
|
||||
static auto Enter() -> void;
|
||||
|
@@ -10,12 +10,12 @@ auto CPU::readIO(uint16 addr) -> uint8 {
|
||||
switch(addr) {
|
||||
|
||||
case 0x4016: {
|
||||
auto data = Famicom::peripherals.controllerPort1->data();
|
||||
auto data = controllerPort1.device->data();
|
||||
return (mdr() & 0xc0) | data.bit(2) << 4 | data.bit(1) << 3 | data.bit(0) << 0;
|
||||
}
|
||||
|
||||
case 0x4017: {
|
||||
auto data = Famicom::peripherals.controllerPort2->data();
|
||||
auto data = controllerPort2.device->data();
|
||||
return (mdr() & 0xc0) | data.bit(2) << 4 | data.bit(1) << 3 | data.bit(0) << 0;
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ auto CPU::writeIO(uint16 addr, uint8 data) -> void {
|
||||
}
|
||||
|
||||
case 0x4016: {
|
||||
Famicom::peripherals.controllerPort1->latch(data.bit(0));
|
||||
Famicom::peripherals.controllerPort2->latch(data.bit(0));
|
||||
controllerPort1.device->latch(data.bit(0));
|
||||
controllerPort2.device->latch(data.bit(0));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -30,7 +30,8 @@ namespace Famicom {
|
||||
};
|
||||
|
||||
struct Region {
|
||||
static inline auto NTSC() -> bool;
|
||||
static inline auto NTSCJ() -> bool;
|
||||
static inline auto NTSCU() -> bool;
|
||||
static inline auto PAL() -> bool;
|
||||
};
|
||||
|
||||
|
@@ -44,15 +44,8 @@ auto Interface::title() -> string {
|
||||
return cartridge.title();
|
||||
}
|
||||
|
||||
auto Interface::videoResolution() -> VideoSize {
|
||||
return {256, 240};
|
||||
}
|
||||
|
||||
auto Interface::videoSize(uint width, uint height, bool arc) -> VideoSize {
|
||||
uint w = 256 * (arc ? 8.0 / 7.0 : 1.0);
|
||||
uint h = 240;
|
||||
uint m = min(width / w, height / h);
|
||||
return {w * m, h * m};
|
||||
auto Interface::videoResolution() -> VideoResolution {
|
||||
return {256, 240, 256, 240, 8.0 / 7.0};
|
||||
}
|
||||
|
||||
auto Interface::videoColors() -> uint32 {
|
||||
@@ -132,7 +125,8 @@ auto Interface::unload() -> void {
|
||||
}
|
||||
|
||||
auto Interface::connect(uint port, uint device) -> void {
|
||||
peripherals.connect(port, device);
|
||||
if(port == ID::Port::Controller1) controllerPort1.connect(settings.controllerPort1 = device);
|
||||
if(port == ID::Port::Controller2) controllerPort2.connect(settings.controllerPort2 = device);
|
||||
}
|
||||
|
||||
auto Interface::power() -> void {
|
||||
|
@@ -26,8 +26,7 @@ struct Interface : Emulator::Interface {
|
||||
auto manifest() -> string override;
|
||||
auto title() -> string override;
|
||||
|
||||
auto videoResolution() -> VideoSize override;
|
||||
auto videoSize(uint width, uint height, bool arc) -> VideoSize override;
|
||||
auto videoResolution() -> VideoResolution override;
|
||||
auto videoColors() -> uint32 override;
|
||||
auto videoColor(uint32 color) -> uint64 override;
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
struct PPU : Thread {
|
||||
inline auto rate() const -> uint { return Region::NTSC() ? 4 : 5; }
|
||||
inline auto vlines() const -> uint { return Region::NTSC() ? 262 : 312; }
|
||||
inline auto rate() const -> uint { return Region::PAL() ? 5 : 4; }
|
||||
inline auto vlines() const -> uint { return Region::PAL() ? 312 : 262; }
|
||||
|
||||
//ppu.cpp
|
||||
static auto Enter() -> void;
|
||||
|
@@ -1,46 +0,0 @@
|
||||
Peripherals peripherals;
|
||||
|
||||
auto Peripherals::unload() -> void {
|
||||
delete controllerPort1;
|
||||
delete controllerPort2;
|
||||
controllerPort1 = nullptr;
|
||||
controllerPort2 = nullptr;
|
||||
}
|
||||
|
||||
auto Peripherals::reset() -> void {
|
||||
connect(ID::Port::Controller1, settings.controllerPort1);
|
||||
connect(ID::Port::Controller2, settings.controllerPort2);
|
||||
}
|
||||
|
||||
auto Peripherals::connect(uint port, uint device) -> void {
|
||||
if(port == ID::Port::Controller1) {
|
||||
settings.controllerPort1 = device;
|
||||
if(!system.loaded()) return;
|
||||
|
||||
delete controllerPort1;
|
||||
switch(device) { default:
|
||||
case ID::Device::None: controllerPort1 = new Controller(0); break;
|
||||
case ID::Device::Gamepad: controllerPort1 = new Gamepad(0); break;
|
||||
}
|
||||
}
|
||||
|
||||
if(port == ID::Port::Controller2) {
|
||||
settings.controllerPort2 = device;
|
||||
if(!system.loaded()) return;
|
||||
|
||||
delete controllerPort2;
|
||||
switch(device) { default:
|
||||
case ID::Device::None: controllerPort2 = new Controller(1); break;
|
||||
case ID::Device::Gamepad: controllerPort2 = new Gamepad(1); break;
|
||||
}
|
||||
}
|
||||
|
||||
if(port == ID::Port::Expansion) {
|
||||
settings.expansionPort = device;
|
||||
if(!system.loaded()) return;
|
||||
}
|
||||
|
||||
cpu.peripherals.reset();
|
||||
cpu.peripherals.append(controllerPort1);
|
||||
cpu.peripherals.append(controllerPort2);
|
||||
}
|
@@ -45,6 +45,8 @@ auto System::serializeAll(serializer& s) -> void {
|
||||
cpu.serialize(s);
|
||||
apu.serialize(s);
|
||||
ppu.serialize(s);
|
||||
controllerPort1.serialize(s);
|
||||
controllerPort2.serialize(s);
|
||||
}
|
||||
|
||||
auto System::serializeInit() -> void {
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Famicom {
|
||||
|
||||
#include "peripherals.cpp"
|
||||
#include "video.cpp"
|
||||
#include "serialization.cpp"
|
||||
System system;
|
||||
@@ -32,8 +31,12 @@ auto System::load(Emulator::Interface* interface) -> bool {
|
||||
auto document = BML::unserialize(information.manifest);
|
||||
if(!cartridge.load()) return false;
|
||||
|
||||
if(cartridge.region() == "NTSC") {
|
||||
information.region = Region::NTSC;
|
||||
if(cartridge.region() == "NTSC-J") {
|
||||
information.region = Region::NTSCJ;
|
||||
information.frequency = Emulator::Constants::Colorburst::NTSC * 6.0;
|
||||
}
|
||||
if(cartridge.region() == "NTSC-U") {
|
||||
information.region = Region::NTSCU;
|
||||
information.frequency = Emulator::Constants::Colorburst::NTSC * 6.0;
|
||||
}
|
||||
if(cartridge.region() == "PAL") {
|
||||
@@ -52,7 +55,9 @@ auto System::save() -> void {
|
||||
|
||||
auto System::unload() -> void {
|
||||
if(!loaded()) return;
|
||||
peripherals.unload();
|
||||
cpu.peripherals.reset();
|
||||
controllerPort1.unload();
|
||||
controllerPort2.unload();
|
||||
cartridge.unload();
|
||||
information.loaded = false;
|
||||
}
|
||||
@@ -72,7 +77,12 @@ auto System::power() -> void {
|
||||
apu.power();
|
||||
ppu.power();
|
||||
scheduler.primary(cpu);
|
||||
peripherals.reset();
|
||||
|
||||
controllerPort1.power(ID::Port::Controller1);
|
||||
controllerPort2.power(ID::Port::Controller2);
|
||||
|
||||
controllerPort1.connect(settings.controllerPort1);
|
||||
controllerPort2.connect(settings.controllerPort2);
|
||||
}
|
||||
|
||||
auto System::init() -> void {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
struct System {
|
||||
enum class Region : uint { NTSC, PAL };
|
||||
enum class Region : uint { NTSCJ, NTSCU, PAL };
|
||||
|
||||
auto loaded() const -> bool { return information.loaded; }
|
||||
auto region() const -> Region { return information.region; }
|
||||
@@ -33,7 +33,7 @@ private:
|
||||
|
||||
struct Information {
|
||||
bool loaded = false;
|
||||
Region region = Region::NTSC;
|
||||
Region region = Region::NTSCJ;
|
||||
double frequency = Emulator::Constants::Colorburst::NTSC * 6.0;
|
||||
string manifest;
|
||||
} information;
|
||||
@@ -41,17 +41,8 @@ private:
|
||||
uint _serializeSize = 0;
|
||||
};
|
||||
|
||||
struct Peripherals {
|
||||
auto unload() -> void;
|
||||
auto reset() -> void;
|
||||
auto connect(uint port, uint device) -> void;
|
||||
|
||||
Controller* controllerPort1 = nullptr;
|
||||
Controller* controllerPort2 = nullptr;
|
||||
};
|
||||
|
||||
extern System system;
|
||||
extern Peripherals peripherals;
|
||||
|
||||
auto Region::NTSC() -> bool { return system.region() == System::Region::NTSC; }
|
||||
auto Region::NTSCJ() -> bool { return system.region() == System::Region::NTSCJ; }
|
||||
auto Region::NTSCU() -> bool { return system.region() == System::Region::NTSCU; }
|
||||
auto Region::PAL() -> bool { return system.region() == System::Region::PAL; }
|
||||
|
@@ -55,8 +55,8 @@ auto APU::power() -> void {
|
||||
create(Enter, 2 * 1024 * 1024);
|
||||
if(!Model::SuperGameBoy()) {
|
||||
stream = Emulator::audio.createStream(2, frequency());
|
||||
stream->addLowPassFilter(20000.0, 3);
|
||||
stream->addHighPassFilter(20.0, 3);
|
||||
stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 20.0);
|
||||
stream->addFilter(Emulator::Filter::Order::Second, Emulator::Filter::Type::LowPass, 20000.0, 3);
|
||||
}
|
||||
for(uint n = 0xff10; n <= 0xff3f; n++) bus.mmio[n] = this;
|
||||
|
||||
|
@@ -2,17 +2,20 @@
|
||||
|
||||
namespace GameBoy {
|
||||
|
||||
Cartridge cartridge;
|
||||
#include "mbc0/mbc0.cpp"
|
||||
#include "mbc1/mbc1.cpp"
|
||||
#include "mbc1m/mbc1m.cpp"
|
||||
#include "mbc2/mbc2.cpp"
|
||||
#include "mbc3/mbc3.cpp"
|
||||
#include "mbc5/mbc5.cpp"
|
||||
#include "mbc6/mbc6.cpp"
|
||||
#include "mbc7/mbc7.cpp"
|
||||
#include "mmm01/mmm01.cpp"
|
||||
#include "huc1/huc1.cpp"
|
||||
#include "huc3/huc3.cpp"
|
||||
#include "tama/tama.cpp"
|
||||
#include "serialization.cpp"
|
||||
Cartridge cartridge;
|
||||
|
||||
auto Cartridge::load() -> bool {
|
||||
information = {};
|
||||
@@ -43,49 +46,46 @@ auto Cartridge::load() -> bool {
|
||||
auto board = document["board"];
|
||||
information.title = document["information/title"].text();
|
||||
|
||||
auto mapperid = document["board/mapper"].text();
|
||||
if(mapperid == "none" ) information.mapper = Mapper::MBC0;
|
||||
if(mapperid == "MBC1" ) information.mapper = Mapper::MBC1;
|
||||
if(mapperid == "MBC1M") information.mapper = Mapper::MBC1M;
|
||||
if(mapperid == "MBC2" ) information.mapper = Mapper::MBC2;
|
||||
if(mapperid == "MBC3" ) information.mapper = Mapper::MBC3;
|
||||
if(mapperid == "MBC5" ) information.mapper = Mapper::MBC5;
|
||||
if(mapperid == "MMM01") information.mapper = Mapper::MMM01;
|
||||
if(mapperid == "HuC1" ) information.mapper = Mapper::HuC1;
|
||||
if(mapperid == "HuC3" ) information.mapper = Mapper::HuC3;
|
||||
auto mapperID = document["board/mapper"].text();
|
||||
if(mapperID == "MBC0" ) mapper = &mbc0;
|
||||
if(mapperID == "MBC1" ) mapper = &mbc1;
|
||||
if(mapperID == "MBC1M") mapper = &mbc1m;
|
||||
if(mapperID == "MBC2" ) mapper = &mbc2;
|
||||
if(mapperID == "MBC3" ) mapper = &mbc3;
|
||||
if(mapperID == "MBC5" ) mapper = &mbc5;
|
||||
if(mapperID == "MBC6" ) mapper = &mbc6;
|
||||
if(mapperID == "MBC7" ) mapper = &mbc7;
|
||||
if(mapperID == "MMM01") mapper = &mmm01;
|
||||
if(mapperID == "HuC1" ) mapper = &huc1;
|
||||
if(mapperID == "HuC3" ) mapper = &huc3;
|
||||
if(mapperID == "TAMA" ) mapper = &tama;
|
||||
if(!mapper) mapper = &mbc0;
|
||||
|
||||
information.rtc = false;
|
||||
information.rumble = false;
|
||||
accelerometer = (bool)document["board/accelerometer"];
|
||||
rumble = (bool)document["board/rumble"];
|
||||
|
||||
rom.size = max(32768u, board["rom/size"].natural());
|
||||
rom.size = max(0x4000, document["board/rom/size"].natural());
|
||||
rom.data = (uint8*)memory::allocate(rom.size, 0xff);
|
||||
|
||||
ram.size = board["ram/size"].natural();
|
||||
ram.data = (uint8*)memory::allocate(ram.size, 0xff);
|
||||
|
||||
if(auto name = board["rom/name"].text()) {
|
||||
if(auto name = document["board/rom/name"].text()) {
|
||||
if(auto fp = platform->open(pathID(), name, File::Read, File::Required)) {
|
||||
fp->read(rom.data, min(rom.size, fp->size()));
|
||||
}
|
||||
}
|
||||
if(auto name = board["ram/name"].text()) {
|
||||
|
||||
ram.size = document["board/ram/size"].natural();
|
||||
ram.data = (uint8*)memory::allocate(ram.size, 0xff);
|
||||
if(auto name = document["board/ram/name"].text()) {
|
||||
if(auto fp = platform->open(pathID(), name, File::Read, File::Optional)) {
|
||||
fp->read(ram.data, min(ram.size, fp->size()));
|
||||
}
|
||||
}
|
||||
|
||||
information.battery = (bool)board["ram/name"];
|
||||
|
||||
switch(information.mapper) { default:
|
||||
case Mapper::MBC0: mapper = &mbc0; break;
|
||||
case Mapper::MBC1: mapper = &mbc1; break;
|
||||
case Mapper::MBC1M: mapper = &mbc1m; break;
|
||||
case Mapper::MBC2: mapper = &mbc2; break;
|
||||
case Mapper::MBC3: mapper = &mbc3; break;
|
||||
case Mapper::MBC5: mapper = &mbc5; break;
|
||||
case Mapper::MMM01: mapper = &mmm01; break;
|
||||
case Mapper::HuC1: mapper = &huc1; break;
|
||||
case Mapper::HuC3: mapper = &huc3; break;
|
||||
rtc.size = document["board/rtc/size"].natural();
|
||||
rtc.data = (uint8*)memory::allocate(rtc.size, 0xff);
|
||||
if(auto name = document["board/rtc/name"].text()) {
|
||||
if(auto fp = platform->open(pathID(), name, File::Read, File::Optional)) {
|
||||
fp->read(rtc.data, min(rtc.size, fp->size()));
|
||||
}
|
||||
}
|
||||
|
||||
information.sha256 = Hash::SHA256(rom.data, rom.size).digest();
|
||||
@@ -100,35 +100,21 @@ auto Cartridge::save() -> void {
|
||||
fp->write(ram.data, ram.size);
|
||||
}
|
||||
}
|
||||
|
||||
if(auto name = document["board/rtc/name"].text()) {
|
||||
if(auto fp = platform->open(pathID(), name, File::Write)) {
|
||||
fp->write(rtc.data, rtc.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::unload() -> void {
|
||||
delete[] rom.data;
|
||||
delete[] ram.data;
|
||||
delete[] rtc.data;
|
||||
rom = {};
|
||||
ram = {};
|
||||
}
|
||||
|
||||
auto Cartridge::readROM(uint addr) -> uint8 {
|
||||
if(addr >= rom.size) addr %= rom.size;
|
||||
return rom.data[addr];
|
||||
}
|
||||
|
||||
auto Cartridge::writeROM(uint addr, uint8 data) -> void {
|
||||
if(addr >= rom.size) addr %= rom.size;
|
||||
rom.data[addr] = data;
|
||||
}
|
||||
|
||||
auto Cartridge::readRAM(uint addr) -> uint8 {
|
||||
if(ram.size == 0) return 0xff;
|
||||
if(addr >= ram.size) addr %= ram.size;
|
||||
return ram.data[addr];
|
||||
}
|
||||
|
||||
auto Cartridge::writeRAM(uint addr, uint8 data) -> void {
|
||||
if(ram.size == 0) return;
|
||||
if(addr >= ram.size) addr %= ram.size;
|
||||
ram.data[addr] = data;
|
||||
rtc = {};
|
||||
}
|
||||
|
||||
auto Cartridge::readIO(uint16 addr) -> uint8 {
|
||||
@@ -140,10 +126,10 @@ auto Cartridge::readIO(uint16 addr) -> uint8 {
|
||||
if(Model::GameBoyColor()) data = system.bootROM.cgb;
|
||||
if(Model::SuperGameBoy()) data = system.bootROM.sgb;
|
||||
if(addr >= 0x0000 && addr <= 0x00ff) return data[addr];
|
||||
if(addr >= 0x0200 && addr <= 0x08ff && Model::GameBoyColor()) return data[addr - 256];
|
||||
if(addr >= 0x0200 && addr <= 0x08ff && Model::GameBoyColor()) return data[addr - 0x100];
|
||||
}
|
||||
|
||||
return mapper->readIO(addr);
|
||||
return mapper->read(addr);
|
||||
}
|
||||
|
||||
auto Cartridge::writeIO(uint16 addr, uint8 data) -> void {
|
||||
@@ -152,25 +138,33 @@ auto Cartridge::writeIO(uint16 addr, uint8 data) -> void {
|
||||
return;
|
||||
}
|
||||
|
||||
mapper->writeIO(addr, data);
|
||||
mapper->write(addr, data);
|
||||
}
|
||||
|
||||
auto Cartridge::power() -> void {
|
||||
bootromEnable = true;
|
||||
|
||||
mbc0.power();
|
||||
mbc1.power();
|
||||
mbc1m.power();
|
||||
mbc2.power();
|
||||
mbc3.power();
|
||||
mbc5.power();
|
||||
mmm01.power();
|
||||
huc1.power();
|
||||
huc3.power();
|
||||
|
||||
for(uint n = 0x0000; n <= 0x7fff; n++) bus.mmio[n] = this;
|
||||
for(uint n = 0xa000; n <= 0xbfff; n++) bus.mmio[n] = this;
|
||||
bus.mmio[0xff50] = this;
|
||||
|
||||
bootromEnable = true;
|
||||
|
||||
mapper->power();
|
||||
}
|
||||
|
||||
auto Cartridge::second() -> void {
|
||||
mapper->second();
|
||||
}
|
||||
|
||||
auto Cartridge::Memory::read(uint address) const -> uint8 {
|
||||
if(!size) return 0xff;
|
||||
if(address >= size) address %= size;
|
||||
return data[address];
|
||||
}
|
||||
|
||||
auto Cartridge::Memory::write(uint address, uint8 byte) -> void {
|
||||
if(!size) return;
|
||||
if(address >= size) address %= size;
|
||||
data[address] = byte;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -8,62 +8,55 @@ struct Cartridge : MMIO {
|
||||
auto save() -> void;
|
||||
auto unload() -> void;
|
||||
|
||||
auto readROM(uint addr) -> uint8;
|
||||
auto writeROM(uint addr, uint8 data) -> void;
|
||||
|
||||
auto readRAM(uint addr) -> uint8;
|
||||
auto writeRAM(uint addr, uint8 data) -> void;
|
||||
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
auto readIO(uint16 address) -> uint8;
|
||||
auto writeIO(uint16 address, uint8 data) -> void;
|
||||
|
||||
auto power() -> void;
|
||||
auto second() -> void;
|
||||
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct Information {
|
||||
uint pathID = 0;
|
||||
string sha256;
|
||||
string manifest;
|
||||
string title;
|
||||
} information;
|
||||
|
||||
struct Memory {
|
||||
auto read(uint address) const -> uint8;
|
||||
auto write(uint address, uint8 data) -> void;
|
||||
|
||||
uint8* data = nullptr;
|
||||
uint size = 0;
|
||||
} rom, ram, rtc;
|
||||
|
||||
bool bootromEnable = true;
|
||||
|
||||
private:
|
||||
struct Mapper {
|
||||
virtual auto second() -> void {}
|
||||
virtual auto read(uint16 address) -> uint8 = 0;
|
||||
virtual auto write(uint16 address, uint8 data) -> void = 0;
|
||||
virtual auto power() -> void = 0;
|
||||
virtual auto serialize(serializer&) -> void = 0;
|
||||
};
|
||||
Mapper* mapper = nullptr;
|
||||
bool accelerometer = false;
|
||||
bool rumble = false;
|
||||
|
||||
#include "mbc0/mbc0.hpp"
|
||||
#include "mbc1/mbc1.hpp"
|
||||
#include "mbc1m/mbc1m.hpp"
|
||||
#include "mbc2/mbc2.hpp"
|
||||
#include "mbc3/mbc3.hpp"
|
||||
#include "mbc5/mbc5.hpp"
|
||||
#include "mbc6/mbc6.hpp"
|
||||
#include "mbc7/mbc7.hpp"
|
||||
#include "mmm01/mmm01.hpp"
|
||||
#include "huc1/huc1.hpp"
|
||||
#include "huc3/huc3.hpp"
|
||||
|
||||
enum Mapper : uint {
|
||||
MBC0,
|
||||
MBC1,
|
||||
MBC1M,
|
||||
MBC2,
|
||||
MBC3,
|
||||
MBC5,
|
||||
MMM01,
|
||||
HuC1,
|
||||
HuC3,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
struct Information {
|
||||
uint pathID = 0;
|
||||
string sha256;
|
||||
string manifest;
|
||||
string title;
|
||||
|
||||
Mapper mapper = Mapper::Unknown;
|
||||
boolean ram;
|
||||
boolean battery;
|
||||
boolean rtc;
|
||||
boolean rumble;
|
||||
} information;
|
||||
|
||||
struct Memory {
|
||||
uint8* data = nullptr;
|
||||
uint size = 0;
|
||||
} rom, ram;
|
||||
|
||||
MMIO* mapper = nullptr;
|
||||
bool bootromEnable = true;
|
||||
#include "tama/tama.hpp"
|
||||
};
|
||||
|
||||
extern Cartridge cartridge;
|
||||
|
@@ -1,49 +1,54 @@
|
||||
auto Cartridge::HuC1::readIO(uint16 addr) -> uint8 {
|
||||
if((addr & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.readROM(addr);
|
||||
auto Cartridge::HuC1::read(uint16 address) -> uint8 {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.rom.read(address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.readROM(rom.select << 14 | (uint14)addr);
|
||||
if((address & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.rom.read(io.rom.bank << 14 | address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
return cartridge.readRAM(ram.select << 13 | (uint13)addr);
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
return cartridge.ram.read(io.ram.bank << 13 | address.bits(0,12));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::HuC1::writeIO(uint16 addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x0000) { //$0000-1fff
|
||||
ram.writable = data.bits(0,3) == 0x0a;
|
||||
auto Cartridge::HuC1::write(uint16 address, uint8 data) -> void {
|
||||
if((address & 0xe000) == 0x0000) { //$0000-1fff
|
||||
io.ram.writable = data.bits(0,3) == 0x0a;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x2000) { //$2000-3fff
|
||||
rom.select = data + (data == 0);
|
||||
if((address & 0xe000) == 0x2000) { //$2000-3fff
|
||||
io.rom.bank = data;
|
||||
if(!io.rom.bank) io.rom.bank = 0x01;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x4000) { //$4000-5fff
|
||||
ram.select = data;
|
||||
if((address & 0xe000) == 0x4000) { //$4000-5fff
|
||||
io.ram.bank = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x6000) { //$6000-7fff
|
||||
model = data.bit(0);
|
||||
if((address & 0xe000) == 0x6000) { //$6000-7fff
|
||||
io.model = data.bit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!ram.writable) return;
|
||||
return cartridge.writeRAM(ram.select << 13 | (uint13)addr, data);
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.writable) return;
|
||||
return cartridge.ram.write(io.ram.bank << 13 | address.bits(0,12), data);
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::HuC1::power() -> void {
|
||||
rom.select = 0x01;
|
||||
ram.writable = false;
|
||||
ram.select = 0x00;
|
||||
model = 0;
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::HuC1::serialize(serializer& s) -> void {
|
||||
s.integer(io.model);
|
||||
s.integer(io.rom.bank);
|
||||
s.integer(io.ram.writable);
|
||||
s.integer(io.ram.bank);
|
||||
}
|
||||
|
@@ -1,14 +1,17 @@
|
||||
struct HuC1 : MMIO {
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
struct HuC1 : Mapper {
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct ROM {
|
||||
uint8 select;
|
||||
} rom;
|
||||
struct RAM {
|
||||
bool writable;
|
||||
uint8 select;
|
||||
} ram;
|
||||
bool model;
|
||||
struct IO {
|
||||
uint1 model;
|
||||
struct ROM {
|
||||
uint8 bank = 0x01;
|
||||
} rom;
|
||||
struct RAM {
|
||||
uint1 writable;
|
||||
uint8 bank;
|
||||
} ram;
|
||||
} io;
|
||||
} huc1;
|
||||
|
@@ -1,49 +1,48 @@
|
||||
auto Cartridge::HuC3::readIO(uint16 addr) -> uint8 {
|
||||
if((addr & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.readROM(addr);
|
||||
auto Cartridge::HuC3::read(uint16 address) -> uint8 {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.rom.read(address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.readROM(rom.select << 14 | (uint14)addr);
|
||||
if((address & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.rom.read(io.rom.bank << 14 | address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(ram.enable) return cartridge.readRAM(ram.select << 13 | (uint13)addr);
|
||||
return 0x01; //does not return open collection
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.enable) return 0x01; //does not return open collection
|
||||
return cartridge.ram.read(io.ram.bank << 13 | address.bits(0,12));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::HuC3::writeIO(uint16 addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x0000) { //$0000-1fff
|
||||
ram.enable = data.bits(0,3) == 0x0a;
|
||||
auto Cartridge::HuC3::write(uint16 address, uint8 data) -> void {
|
||||
if((address & 0xe000) == 0x0000) { //$0000-1fff
|
||||
io.ram.enable = data.bits(0,3) == 0x0a;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x2000) { //$2000-3fff
|
||||
rom.select = data;
|
||||
if((address & 0xe000) == 0x2000) { //$2000-3fff
|
||||
io.rom.bank = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x4000) { //$4000-5fff
|
||||
ram.select = data;
|
||||
if((address & 0xe000) == 0x4000) { //$4000-5fff
|
||||
io.ram.bank = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x6000) { //$6000-7fff
|
||||
//unknown purpose
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(ram.enable) cartridge.writeRAM(ram.select << 13 | (uint13)addr, data);
|
||||
return;
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.enable) return;
|
||||
return cartridge.ram.write(io.ram.bank << 13 | address.bits(0,12), data);
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::HuC3::power() -> void {
|
||||
rom.select = 0x01;
|
||||
ram.enable = false;
|
||||
ram.select = 0x00;
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::HuC3::serialize(serializer& s) -> void {
|
||||
s.integer(io.rom.bank);
|
||||
s.integer(io.ram.enable);
|
||||
s.integer(io.ram.bank);
|
||||
}
|
||||
|
@@ -1,13 +1,16 @@
|
||||
struct HuC3 : MMIO {
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
struct HuC3 : Mapper {
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct ROM {
|
||||
uint8 select;
|
||||
} rom;
|
||||
struct RAM {
|
||||
bool enable;
|
||||
uint8 select;
|
||||
} ram;
|
||||
struct IO {
|
||||
struct ROM {
|
||||
uint8 bank = 0x01;
|
||||
} rom;
|
||||
struct RAM {
|
||||
uint1 enable;
|
||||
uint8 bank;
|
||||
} ram;
|
||||
} io;
|
||||
} huc3;
|
||||
|
@@ -1,21 +1,24 @@
|
||||
auto Cartridge::MBC0::readIO(uint16 addr) -> uint8 {
|
||||
if((addr & 0x8000) == 0x0000) { //$0000-7fff
|
||||
return cartridge.readROM(addr);
|
||||
auto Cartridge::MBC0::read(uint16 address) -> uint8 {
|
||||
if((address & 0x8000) == 0x0000) { //$0000-7fff
|
||||
return cartridge.rom.read(address.bits(0,14));
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
return cartridge.readRAM((uint13)addr);
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
return cartridge.ram.read(address.bits(0,12));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::MBC0::writeIO(uint16 addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
cartridge.writeRAM((uint13)addr, data);
|
||||
auto Cartridge::MBC0::write(uint16 address, uint8 data) -> void {
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
cartridge.ram.write(address.bits(0,12), data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::MBC0::power() -> void {
|
||||
}
|
||||
|
||||
auto Cartridge::MBC0::serialize(serializer& s) -> void {
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
struct MBC0 : MMIO {
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
struct MBC0 : Mapper {
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
} mbc0;
|
||||
|
@@ -1,66 +1,67 @@
|
||||
auto Cartridge::MBC1::readIO(uint16 addr) -> uint8 {
|
||||
if((addr & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.readROM(addr);
|
||||
auto Cartridge::MBC1::read(uint16 address) -> uint8 {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.rom.read(address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xc000) == 0x4000) { //$4000-7fff
|
||||
if(mode == 0) {
|
||||
return cartridge.readROM(ram.select << 19 | rom.select << 14 | (uint14)addr);
|
||||
if((address & 0xc000) == 0x4000) { //$4000-7fff
|
||||
if(io.mode == 0) {
|
||||
return cartridge.rom.read(io.ram.bank << 19 | io.rom.bank << 14 | address.bits(0,13));
|
||||
} else {
|
||||
return cartridge.readROM(rom.select << 14 | (uint14)addr);
|
||||
return cartridge.rom.read(io.rom.bank << 14 | address.bits(0,13));
|
||||
}
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(ram.enable) {
|
||||
if(mode == 0) {
|
||||
return cartridge.readRAM((uint13)addr);
|
||||
} else {
|
||||
return cartridge.readRAM(ram.select << 13 | (uint13)addr);
|
||||
}
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.enable) return 0xff;
|
||||
if(io.mode == 0) {
|
||||
return cartridge.ram.read(address.bits(0,12));
|
||||
} else {
|
||||
return cartridge.ram.read(io.ram.bank << 13 | address.bits(0,12));
|
||||
}
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::MBC1::writeIO(uint16 addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x0000) { //$0000-1fff
|
||||
ram.enable = (data & 0x0f) == 0x0a;
|
||||
auto Cartridge::MBC1::write(uint16 address, uint8 data) -> void {
|
||||
if((address & 0xe000) == 0x0000) { //$0000-1fff
|
||||
io.ram.enable = data.bits(0,3) == 0x0a;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x2000) { //$2000-3fff
|
||||
rom.select = (data & 0x1f) + ((data & 0x1f) == 0);
|
||||
if((address & 0xe000) == 0x2000) { //$2000-3fff
|
||||
io.rom.bank = data.bits(0,4);
|
||||
if(!io.rom.bank) io.rom.bank = 0x01;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x4000) { //$4000-5fff
|
||||
ram.select = data & 0x03;
|
||||
if((address & 0xe000) == 0x4000) { //$4000-5fff
|
||||
io.ram.bank = data.bits(0,1);
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x6000) { //$6000-7fff
|
||||
mode = data & 0x01;
|
||||
if((address & 0xe000) == 0x6000) { //$6000-7fff
|
||||
io.mode = data.bit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(ram.enable) {
|
||||
if(mode == 0) {
|
||||
cartridge.writeRAM(addr & 0x1fff, data);
|
||||
} else {
|
||||
cartridge.writeRAM(ram.select << 13 | (uint13)addr, data);
|
||||
}
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.enable) return;
|
||||
if(io.mode == 0) {
|
||||
return cartridge.ram.write(address.bits(0,12), data);
|
||||
} else {
|
||||
return cartridge.ram.write(io.ram.bank << 13 | address.bits(0,12), data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::MBC1::power() -> void {
|
||||
rom.select = 0x01;
|
||||
ram.enable = false;
|
||||
ram.select = 0x00;
|
||||
mode = 0;
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::MBC1::serialize(serializer& s) -> void {
|
||||
s.integer(io.mode);
|
||||
s.integer(io.rom.bank);
|
||||
s.integer(io.ram.enable);
|
||||
s.integer(io.ram.bank);
|
||||
}
|
||||
|
@@ -1,14 +1,17 @@
|
||||
struct MBC1 : MMIO {
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
struct MBC1 : Mapper {
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer& s) -> void;
|
||||
|
||||
struct ROM {
|
||||
uint8 select;
|
||||
} rom;
|
||||
struct RAM {
|
||||
bool enable;
|
||||
uint8 select;
|
||||
} ram;
|
||||
bool mode;
|
||||
struct IO {
|
||||
uint1 mode;
|
||||
struct ROM {
|
||||
uint8 bank = 0x01;
|
||||
} rom;
|
||||
struct RAM {
|
||||
uint1 enable;
|
||||
uint8 bank;
|
||||
} ram;
|
||||
} io;
|
||||
} mbc1;
|
||||
|
@@ -1,40 +1,43 @@
|
||||
auto Cartridge::MBC1M::readIO(uint16 addr) -> uint8 {
|
||||
if((addr & 0xc000) == 0x0000) { //$0000-3fff
|
||||
if(mode == 0) return cartridge.readROM((uint14)addr);
|
||||
return cartridge.readROM(rom.hi << 18 | (uint14)addr);
|
||||
auto Cartridge::MBC1M::read(uint16 address) -> uint8 {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
if(io.mode == 0) return cartridge.rom.read(address.bits(0,13));
|
||||
return cartridge.rom.read(io.rom.bank.bits(4,5) << 18 | address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.readROM(rom.hi << 18 | rom.lo << 14 | (uint14)addr);
|
||||
if((address & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.rom.read(io.rom.bank << 14 | address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
return cartridge.readRAM((uint13)addr);
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
return cartridge.ram.read(address.bits(0,12));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::MBC1M::writeIO(uint16 addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x2000) { //$2000-3fff
|
||||
rom.lo = data.bits(0,3);
|
||||
auto Cartridge::MBC1M::write(uint16 address, uint8 data) -> void {
|
||||
if((address & 0xe000) == 0x2000) { //$2000-3fff
|
||||
io.rom.bank.bits(0,3) = data.bits(0,3);
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x4000) { //$4000-5fff
|
||||
rom.hi = data.bits(0,1);
|
||||
if((address & 0xe000) == 0x4000) { //$4000-5fff
|
||||
io.rom.bank.bits(4,5) = data.bits(0,1);
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x6000) { //$6000-7fff
|
||||
mode = data.bit(0);
|
||||
if((address & 0xe000) == 0x6000) { //$6000-7fff
|
||||
io.mode = data.bit(0);
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
cartridge.writeRAM((uint13)addr, data);
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
cartridge.ram.write(address.bits(0,13), data);
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::MBC1M::power() -> void {
|
||||
rom.lo = 1;
|
||||
rom.hi = 0;
|
||||
mode = 0;
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::MBC1M::serialize(serializer& s) -> void {
|
||||
s.integer(io.mode);
|
||||
s.integer(io.rom.bank);
|
||||
}
|
||||
|
@@ -1,11 +1,13 @@
|
||||
struct MBC1M : MMIO {
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
struct MBC1M : Mapper {
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct ROM {
|
||||
uint4 lo;
|
||||
uint2 hi;
|
||||
} rom;
|
||||
uint1 mode;
|
||||
struct IO {
|
||||
uint1 mode;
|
||||
struct ROM {
|
||||
uint6 bank = 0x01;
|
||||
} rom;
|
||||
} io;
|
||||
} mbc1m;
|
||||
|
@@ -1,38 +1,61 @@
|
||||
auto Cartridge::MBC2::readIO(uint16 addr) -> uint8 {
|
||||
if((addr & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.readROM(addr);
|
||||
auto Cartridge::MBC2::read(uint16 address) -> uint8 {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.rom.read(address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.readROM(rom.select << 14 | (uint14)addr);
|
||||
if((address & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.rom.read(io.rom.bank << 14 | address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xee00) == 0xa000) { //$a000-a1ff
|
||||
if(ram.enable) return cartridge.readRAM((uint9)addr);
|
||||
return 0xff;
|
||||
if((address & 0xee01) == 0xa000) { //$a000-a1ff (even)
|
||||
if(!io.ram.enable) return 0xff;
|
||||
auto ram = cartridge.ram.read(address.bits(1,8));
|
||||
return 0xf0 | ram.bits(0,3);
|
||||
}
|
||||
|
||||
if((address & 0xee01) == 0xa001) { //$a000-a1ff (odd)
|
||||
if(!io.ram.enable) return 0xff;
|
||||
auto ram = cartridge.ram.read(address.bits(1,8));
|
||||
return 0xf0 | ram.bits(4,7);
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::MBC2::writeIO(uint16 addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x0000) { //$0000-1fff
|
||||
if(!addr.bit(8)) ram.enable = data.bits(0,3) == 0x0a;
|
||||
auto Cartridge::MBC2::write(uint16 address, uint8 data) -> void {
|
||||
if((address & 0xe000) == 0x0000) { //$0000-1fff
|
||||
if(!address.bit(8)) io.ram.enable = data.bits(0,3) == 0x0a;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x2000) { //$2000-3fff
|
||||
if( addr.bit(8)) rom.select = data.bits(0,3) + (data.bits(0,3) == 0);
|
||||
if((address & 0xe000) == 0x2000) { //$2000-3fff
|
||||
if(address.bit(8)) io.rom.bank = data.bits(0,3);
|
||||
if(!io.rom.bank) io.rom.bank = 0x01;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xee00) == 0xa000) { //$a000-a1ff
|
||||
if(ram.enable) cartridge.writeRAM((uint9)addr, data.bits(0,3));
|
||||
if((address & 0xee01) == 0xa000) { //$a000-a1ff (even)
|
||||
if(!io.ram.enable) return;
|
||||
auto ram = cartridge.ram.read(address.bits(1,8));
|
||||
ram.bits(0,3) = data.bits(0,3);
|
||||
cartridge.ram.write(address.bits(1,8), ram);
|
||||
return;
|
||||
}
|
||||
|
||||
if((address & 0xee01) == 0xa001) { //$a000-a1ff (odd)
|
||||
if(!io.ram.enable) return;
|
||||
auto ram = cartridge.ram.read(address.bits(1,8));
|
||||
ram.bits(4,7) = data.bits(0,3);
|
||||
cartridge.ram.write(address.bits(1,8), ram);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::MBC2::power() -> void {
|
||||
rom.select = 0x01;
|
||||
ram.enable = false;
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::MBC2::serialize(serializer& s) -> void {
|
||||
s.integer(io.rom.bank);
|
||||
s.integer(io.ram.enable);
|
||||
}
|
||||
|
@@ -1,12 +1,15 @@
|
||||
struct MBC2 : MMIO {
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
struct MBC2 : Mapper {
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct ROM {
|
||||
uint8 select;
|
||||
} rom;
|
||||
struct RAM {
|
||||
bool enable;
|
||||
} ram;
|
||||
struct IO {
|
||||
struct ROM {
|
||||
uint8 bank = 0x01;
|
||||
} rom;
|
||||
struct RAM {
|
||||
uint1 enable = 0;
|
||||
} ram;
|
||||
} io;
|
||||
} mbc2;
|
||||
|
@@ -1,118 +1,113 @@
|
||||
auto Cartridge::MBC3::second() -> void {
|
||||
if(!rtc.halt) {
|
||||
if(++rtc.second >= 60) {
|
||||
rtc.second = 0;
|
||||
if(++rtc.minute >= 60) {
|
||||
rtc.minute = 0;
|
||||
if(++rtc.hour >= 24) {
|
||||
rtc.hour = 0;
|
||||
if(++rtc.day >= 512) {
|
||||
rtc.day = 0;
|
||||
rtc.dayCarry = true;
|
||||
}
|
||||
if(io.rtc.halt) return;
|
||||
if(++io.rtc.second >= 60) {
|
||||
io.rtc.second = 0;
|
||||
if(++io.rtc.minute >= 60) {
|
||||
io.rtc.minute = 0;
|
||||
if(++io.rtc.hour >= 24) {
|
||||
io.rtc.hour = 0;
|
||||
if(++io.rtc.day == 0) {
|
||||
io.rtc.dayCarry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::MBC3::readIO(uint16 addr) -> uint8 {
|
||||
if((addr & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.readROM(addr);
|
||||
auto Cartridge::MBC3::read(uint16 address) -> uint8 {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.rom.read(address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.readROM(rom.select<< 14 | (uint14)addr);
|
||||
if((address & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.rom.read(io.rom.bank << 14 | address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(ram.enable) {
|
||||
if(ram.select <= 0x03) {
|
||||
return cartridge.readRAM(ram.select << 13 | (uint13)addr);
|
||||
}
|
||||
if(ram.select == 0x08) return rtc.latchSecond;
|
||||
if(ram.select == 0x09) return rtc.latchMinute;
|
||||
if(ram.select == 0x0a) return rtc.latchHour;
|
||||
if(ram.select == 0x0b) return rtc.latchDay;
|
||||
if(ram.select == 0x0c) return rtc.latchDayCarry << 7 | rtc.latchDay >> 8;
|
||||
}
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.enable) return 0xff;
|
||||
if(io.ram.bank <= 0x03) return cartridge.ram.read(io.ram.bank << 13 | address.bits(0,12));
|
||||
if(io.ram.bank == 0x08) return io.rtc.latchSecond;
|
||||
if(io.ram.bank == 0x09) return io.rtc.latchMinute;
|
||||
if(io.ram.bank == 0x0a) return io.rtc.latchHour;
|
||||
if(io.ram.bank == 0x0b) return io.rtc.latchDay;
|
||||
if(io.ram.bank == 0x0c) return io.rtc.latchDayCarry << 7 | io.rtc.latchDay >> 8;
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::MBC3::writeIO(uint16 addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x0000) { //$0000-1fff
|
||||
ram.enable = (data & 0x0f) == 0x0a;
|
||||
auto Cartridge::MBC3::write(uint16 address, uint8 data) -> void {
|
||||
if((address & 0xe000) == 0x0000) { //$0000-1fff
|
||||
io.ram.enable = data.bits(0,3) == 0x0a;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x2000) { //$2000-3fff
|
||||
rom.select = (data & 0x7f) + ((data & 0x7f) == 0);
|
||||
if((address & 0xe000) == 0x2000) { //$2000-3fff
|
||||
io.rom.bank = data.bits(0,6);
|
||||
if(!io.rom.bank) io.rom.bank = 0x01;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x4000) { //$4000-5fff
|
||||
ram.select = data;
|
||||
if((address & 0xe000) == 0x4000) { //$4000-5fff
|
||||
io.ram.bank = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x6000) { //$6000-7fff
|
||||
if(rtc.latch == 0 && data == 1) {
|
||||
rtc.latchSecond = rtc.second;
|
||||
rtc.latchMinute = rtc.minute;
|
||||
rtc.latchHour = rtc.hour;
|
||||
rtc.latchDay = rtc.day;
|
||||
rtc.latchDayCarry = rtc.dayCarry;
|
||||
if((address & 0xe000) == 0x6000) { //$6000-7fff
|
||||
if(io.rtc.latch == 0 && data == 1) {
|
||||
io.rtc.latchSecond = io.rtc.second;
|
||||
io.rtc.latchMinute = io.rtc.minute;
|
||||
io.rtc.latchHour = io.rtc.hour;
|
||||
io.rtc.latchDay = io.rtc.day;
|
||||
io.rtc.latchDayCarry = io.rtc.dayCarry;
|
||||
}
|
||||
rtc.latch = data;
|
||||
io.rtc.latch = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(ram.enable) {
|
||||
if(ram.select <= 0x03) {
|
||||
cartridge.writeRAM(ram.select << 13 | (uint13)addr, data);
|
||||
} else if(ram.select == 0x08) {
|
||||
if(data >= 60) data = 0;
|
||||
rtc.second = data;
|
||||
} else if(ram.select == 0x09) {
|
||||
if(data >= 60) data = 0;
|
||||
rtc.minute = data;
|
||||
} else if(ram.select == 0x0a) {
|
||||
if(data >= 24) data = 0;
|
||||
rtc.hour = data;
|
||||
} else if(ram.select == 0x0b) {
|
||||
rtc.day = (rtc.day & 0x0100) | data;
|
||||
} else if(ram.select == 0x0c) {
|
||||
rtc.day = ((data & 1) << 8) | (rtc.day & 0xff);
|
||||
rtc.halt = data & 0x40;
|
||||
rtc.dayCarry = data & 0x80;
|
||||
}
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.enable) return;
|
||||
if(io.ram.bank <= 0x03) {
|
||||
cartridge.ram.write(io.ram.bank << 13 | address.bits(0,12), data);
|
||||
} else if(io.ram.bank == 0x08) {
|
||||
if(data >= 60) data = 0;
|
||||
io.rtc.second = data;
|
||||
} else if(io.ram.bank == 0x09) {
|
||||
if(data >= 60) data = 0;
|
||||
io.rtc.minute = data;
|
||||
} else if(io.ram.bank == 0x0a) {
|
||||
if(data >= 24) data = 0;
|
||||
io.rtc.hour = data;
|
||||
} else if(io.ram.bank == 0x0b) {
|
||||
io.rtc.day.bits(0,7) = data.bits(0,7);
|
||||
} else if(io.ram.bank == 0x0c) {
|
||||
io.rtc.day.bit(8) = data.bit(0);
|
||||
io.rtc.halt = data.bit(6);
|
||||
io.rtc.dayCarry = data.bit(7);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::MBC3::power() -> void {
|
||||
rom.select = 0x01;
|
||||
|
||||
ram.enable = false;
|
||||
ram.select = 0x00;
|
||||
|
||||
rtc.latch = 0;
|
||||
|
||||
rtc.halt = true;
|
||||
rtc.second = 0;
|
||||
rtc.minute = 0;
|
||||
rtc.hour = 0;
|
||||
rtc.day = 0;
|
||||
rtc.dayCarry = false;
|
||||
|
||||
rtc.latchSecond = 0;
|
||||
rtc.latchMinute = 0;
|
||||
rtc.latchHour = 0;
|
||||
rtc.latchDay = 0;
|
||||
rtc.latchDayCarry = false;
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::MBC3::serialize(serializer& s) -> void {
|
||||
s.integer(io.rom.bank);
|
||||
s.integer(io.ram.enable);
|
||||
s.integer(io.ram.bank);
|
||||
s.integer(io.rtc.halt);
|
||||
s.integer(io.rtc.latch);
|
||||
s.integer(io.rtc.second);
|
||||
s.integer(io.rtc.minute);
|
||||
s.integer(io.rtc.hour);
|
||||
s.integer(io.rtc.day);
|
||||
s.integer(io.rtc.dayCarry);
|
||||
s.integer(io.rtc.latchSecond);
|
||||
s.integer(io.rtc.latchMinute);
|
||||
s.integer(io.rtc.latchHour);
|
||||
s.integer(io.rtc.latchDay);
|
||||
s.integer(io.rtc.latchDayCarry);
|
||||
}
|
||||
|
@@ -1,30 +1,33 @@
|
||||
struct MBC3 : MMIO {
|
||||
struct MBC3 : Mapper {
|
||||
auto second() -> void;
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer& s) -> void;
|
||||
|
||||
struct ROM {
|
||||
uint8 select;
|
||||
} rom;
|
||||
struct RAM {
|
||||
bool enable;
|
||||
uint8 select;
|
||||
} ram;
|
||||
struct RTC {
|
||||
bool latch;
|
||||
struct IO {
|
||||
struct ROM {
|
||||
uint8 bank = 0x01;
|
||||
} rom;
|
||||
struct RAM {
|
||||
uint1 enable;
|
||||
uint8 bank;
|
||||
} ram;
|
||||
struct RTC {
|
||||
uint1 halt = true;
|
||||
uint1 latch;
|
||||
|
||||
bool halt;
|
||||
uint second;
|
||||
uint minute;
|
||||
uint hour;
|
||||
uint day;
|
||||
bool dayCarry;
|
||||
uint8 second;
|
||||
uint8 minute;
|
||||
uint8 hour;
|
||||
uint9 day;
|
||||
uint1 dayCarry;
|
||||
|
||||
uint latchSecond;
|
||||
uint latchMinute;
|
||||
uint latchHour;
|
||||
uint latchDay;
|
||||
uint latchDayCarry;
|
||||
} rtc;
|
||||
uint8 latchSecond;
|
||||
uint8 latchMinute;
|
||||
uint8 latchHour;
|
||||
uint9 latchDay;
|
||||
uint1 latchDayCarry;
|
||||
} rtc;
|
||||
} io;
|
||||
} mbc3;
|
||||
|
@@ -1,49 +1,54 @@
|
||||
auto Cartridge::MBC5::readIO(uint16 addr) -> uint8 {
|
||||
if((addr & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.readROM(addr);
|
||||
auto Cartridge::MBC5::read(uint16 address) -> uint8 {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.rom.read(address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.readROM(rom.select << 14 | (uint14)addr);
|
||||
if((address & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.rom.read(io.rom.bank << 14 | address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(ram.enable) return cartridge.readRAM(ram.select << 13 | (uint13)addr);
|
||||
return 0xff;
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.enable) return 0xff;
|
||||
return cartridge.ram.read(io.ram.bank << 13 | address.bits(0,12));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::MBC5::writeIO(uint16 addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x0000) { //$0000-1fff
|
||||
ram.enable = data.bits(0,3) == 0x0a;
|
||||
auto Cartridge::MBC5::write(uint16 address, uint8 data) -> void {
|
||||
if((address & 0xe000) == 0x0000) { //$0000-1fff
|
||||
io.ram.enable = data.bits(0,3) == 0x0a;
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xf000) == 0x2000) { //$2000-2fff
|
||||
rom.select.byte(0) = data;
|
||||
if((address & 0xf000) == 0x2000) { //$2000-2fff
|
||||
io.rom.bank.bits(0,7) = data.bits(0,7);
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xf000) == 0x3000) { //$3000-3fff
|
||||
rom.select.byte(1) = data.bit(0);
|
||||
if((address & 0xf000) == 0x3000) { //$3000-3fff
|
||||
io.rom.bank.bit(8) = data.bit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x4000) { //$4000-5fff
|
||||
ram.select = data.bits(0,3);
|
||||
if((address & 0xe000) == 0x4000) { //$4000-5fff
|
||||
if(cartridge.rumble) platform->inputRumble(ID::Port::Hardware, ID::Device::Controls, 10, data.bit(3));
|
||||
io.ram.bank = data.bits(0,3);
|
||||
return;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(ram.enable) cartridge.writeRAM(ram.select << 13 | (uint13)addr, data);
|
||||
return;
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.enable) return;
|
||||
return cartridge.ram.write(io.ram.bank << 13 | address.bits(0,12), data);
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::MBC5::power() -> void {
|
||||
rom.select = 0x001;
|
||||
ram.enable = false;
|
||||
ram.select = 0x00;
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::MBC5::serialize(serializer& s) -> void {
|
||||
s.integer(io.rom.bank);
|
||||
s.integer(io.ram.enable);
|
||||
s.integer(io.ram.bank);
|
||||
}
|
||||
|
@@ -1,13 +1,16 @@
|
||||
struct MBC5 : MMIO {
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
struct MBC5 : Mapper {
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct ROM {
|
||||
uint9 select;
|
||||
} rom;
|
||||
struct RAM {
|
||||
bool enable;
|
||||
uint4 select;
|
||||
} ram;
|
||||
struct IO {
|
||||
struct ROM {
|
||||
uint9 bank = 0x01;
|
||||
} rom;
|
||||
struct RAM {
|
||||
uint1 enable;
|
||||
uint4 bank;
|
||||
} ram;
|
||||
} io;
|
||||
} mbc5;
|
||||
|
74
higan/gb/cartridge/mbc6/mbc6.cpp
Normal file
74
higan/gb/cartridge/mbc6/mbc6.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
auto Cartridge::MBC6::read(uint16 address) -> uint8 {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.rom.read(address.bits(0,13));
|
||||
}
|
||||
|
||||
if((address & 0xe000) == 0x4000) { //$4000-5fff
|
||||
return cartridge.rom.read(io.rom.bank[0] << 13 | address.bits(0,12));
|
||||
}
|
||||
|
||||
if((address & 0xe000) == 0x6000) { //$6000-7fff
|
||||
return cartridge.rom.read(io.rom.bank[1] << 13 | address.bits(0,12));
|
||||
}
|
||||
|
||||
if((address & 0xf000) == 0xa000) { //$a000-afff
|
||||
if(!io.ram.enable) return 0xff;
|
||||
return cartridge.ram.read(io.ram.bank[0] << 12 | address.bits(0,11));
|
||||
}
|
||||
|
||||
if((address & 0xf000) == 0xb000) { //$b000-bfff
|
||||
if(!io.ram.enable) return 0xff;
|
||||
return cartridge.ram.read(io.ram.bank[1] << 12 | address.bits(0,11));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::MBC6::write(uint16 address, uint8 data) -> void {
|
||||
if((address & 0xfc00) == 0x0000) {
|
||||
io.ram.enable = data.bits(0,3) == 0xa;
|
||||
return;
|
||||
}
|
||||
|
||||
if((address & 0xfc00) == 0x0400) {
|
||||
io.ram.bank[0] = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if((address & 0xfc00) == 0x0800) {
|
||||
io.ram.bank[1] = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if((address & 0xf800) == 0x2000) {
|
||||
io.rom.bank[0] = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if((address & 0xf800) == 0x3000) {
|
||||
io.rom.bank[1] = data;
|
||||
return;
|
||||
}
|
||||
|
||||
if((address & 0xf000) == 0xa000) { //$a000-afff
|
||||
if(!io.ram.enable) return;
|
||||
return cartridge.ram.write(io.ram.bank[0] << 12 | address.bits(0,11), data);
|
||||
}
|
||||
|
||||
if((address & 0xf000) == 0xb000) { //$b000-bfff
|
||||
if(!io.ram.enable) return;
|
||||
return cartridge.ram.write(io.ram.bank[1] << 12 | address.bits(0,11), data);
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::MBC6::power() -> void {
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::MBC6::serialize(serializer& s) -> void {
|
||||
s.integer(io.rom.bank[0]);
|
||||
s.integer(io.rom.bank[1]);
|
||||
s.integer(io.ram.enable);
|
||||
s.integer(io.ram.bank[0]);
|
||||
s.integer(io.ram.bank[1]);
|
||||
}
|
16
higan/gb/cartridge/mbc6/mbc6.hpp
Normal file
16
higan/gb/cartridge/mbc6/mbc6.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
struct MBC6 : Mapper {
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct IO {
|
||||
struct ROM {
|
||||
uint8 bank[2];
|
||||
} rom;
|
||||
struct RAM {
|
||||
uint1 enable;
|
||||
uint8 bank[2];
|
||||
} ram;
|
||||
} io;
|
||||
} mbc6;
|
86
higan/gb/cartridge/mbc7/mbc7.cpp
Normal file
86
higan/gb/cartridge/mbc7/mbc7.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
auto Cartridge::MBC7::read(uint16 address) -> uint8 {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.rom.read(address.bits(0,13));
|
||||
}
|
||||
|
||||
if((address & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.rom.read(io.rom.bank << 14 | address.bits(0,13));
|
||||
}
|
||||
|
||||
if((address & 0xf000) == 0xa000) { //$a000-afff
|
||||
if(!io.ram.enable[0] || !io.ram.enable[1]) return 0xff;
|
||||
|
||||
switch(address.bits(4,7)) {
|
||||
case 2: return io.accelerometer.x.bits(0, 7);
|
||||
case 3: return io.accelerometer.x.bits(8,15);
|
||||
case 4: return io.accelerometer.y.bits(0, 7);
|
||||
case 5: return io.accelerometer.y.bits(8,15);
|
||||
case 6: return 0x00; //z?
|
||||
case 7: return 0xff; //z?
|
||||
case 8: return 0xff;
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::MBC7::write(uint16 address, uint8 data) -> void {
|
||||
if((address & 0xe000) == 0x0000) { //$0000-1fff
|
||||
io.ram.enable[0] = data.bits(0,3) == 0xa;
|
||||
if(!io.ram.enable[0]) io.ram.enable[1] = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if((address & 0xe000) == 0x2000) { //$2000-3fff
|
||||
io.rom.bank = data;
|
||||
if(!io.rom.bank) io.rom.bank = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if((address & 0xe000) == 0x4000) { //$4000-5fff
|
||||
if(!io.ram.enable[0]) return;
|
||||
io.ram.enable[1] = data == 0x40;
|
||||
}
|
||||
|
||||
if((address & 0xf000) == 0xa000) { //$a000-afff
|
||||
if(!io.ram.enable[0] || !io.ram.enable[1]) return;
|
||||
|
||||
switch(address.bits(4,7)) {
|
||||
|
||||
case 0: {
|
||||
if(data != 0x55) break;
|
||||
io.accelerometer.x = 0x8000;
|
||||
io.accelerometer.y = 0x8000;
|
||||
break;
|
||||
}
|
||||
|
||||
case 1: {
|
||||
if(data != 0xaa) break;
|
||||
io.accelerometer.x = 0x8000 + platform->inputPoll(ID::Port::Hardware, ID::Device::Controls, 8);
|
||||
io.accelerometer.y = 0x8000 + platform->inputPoll(ID::Port::Hardware, ID::Device::Controls, 9);
|
||||
break;
|
||||
}
|
||||
|
||||
case 8: {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::MBC7::power() -> void {
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::MBC7::serialize(serializer& s) -> void {
|
||||
s.integer(io.rom.bank);
|
||||
s.integer(io.ram.enable[0]);
|
||||
s.integer(io.ram.enable[1]);
|
||||
s.integer(io.accelerometer.x);
|
||||
s.integer(io.accelerometer.y);
|
||||
}
|
19
higan/gb/cartridge/mbc7/mbc7.hpp
Normal file
19
higan/gb/cartridge/mbc7/mbc7.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
struct MBC7 : Mapper {
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct IO {
|
||||
struct ROM {
|
||||
uint8 bank = 0x01;
|
||||
} rom;
|
||||
struct RAM {
|
||||
uint1 enable[2];
|
||||
} ram;
|
||||
struct Accelerometer {
|
||||
uint16 x = 0x8000;
|
||||
uint16 y = 0x8000;
|
||||
} accelerometer;
|
||||
} io;
|
||||
} mbc7;
|
@@ -1,60 +1,65 @@
|
||||
auto Cartridge::MMM01::readIO(uint16 addr) -> uint8 {
|
||||
if((addr & 0x8000) == 0x0000) { //$0000-7fff
|
||||
if(mode == 0) return cartridge.readROM(addr);
|
||||
}
|
||||
auto Cartridge::MMM01::read(uint16 address) -> uint8 {
|
||||
if(io.mode == 0) {
|
||||
if((address & 0x8000) == 0x0000) { //$0000-7fff
|
||||
return cartridge.rom.read(cartridge.rom.size - 0x8000 + address.bits(0,14));
|
||||
}
|
||||
|
||||
if((addr & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.readROM(0x8000 + (rom.base << 14) + (uint14)addr);
|
||||
}
|
||||
return 0xff;
|
||||
} else {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.rom.read((io.rom.base << 14) + address.bits(0,13));
|
||||
}
|
||||
|
||||
if((addr & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.readROM(0x8000 + (rom.base << 14) + (rom.select<< 14) + (uint14)addr);
|
||||
}
|
||||
if((address & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.rom.read((io.rom.base << 14) + (io.rom.bank << 14) + address.bits(0,13));
|
||||
}
|
||||
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.enable) return 0xff;
|
||||
return cartridge.ram.read(io.ram.bank << 13 | address.bits(0,12));
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(ram.enable) return cartridge.readRAM(ram.select << 13 | (uint13)addr);
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::MMM01::writeIO(uint16 addr, uint8 data) -> void {
|
||||
if((addr & 0xe000) == 0x0000) { //$0000-1fff
|
||||
if(mode == 0) {
|
||||
mode = 1;
|
||||
} else {
|
||||
ram.enable= data.bits(0,3) == 0x0a;
|
||||
auto Cartridge::MMM01::write(uint16 address, uint8 data) -> void {
|
||||
if(io.mode == 0) {
|
||||
if((address & 0xe000) == 0x0000) { //$0000-1fff
|
||||
io.mode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x2000) { //$2000-3fff
|
||||
if(mode == 0) {
|
||||
rom.base = data.bits(0,5);
|
||||
} else {
|
||||
rom.select = data;
|
||||
if((address & 0xe000) == 0x2000) { //$2000-3fff
|
||||
io.rom.base = data.bits(0,5);
|
||||
}
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x4000) { //$4000-5fff
|
||||
if(mode == 1) {
|
||||
ram.select = data;
|
||||
} else {
|
||||
if((address & 0xe000) == 0x0000) { //$0000-1fff
|
||||
io.ram.enable = data.bits(0,3) == 0x0a;
|
||||
}
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0x6000) { //$6000-7fff
|
||||
//unknown purpose
|
||||
}
|
||||
if((address & 0xe000) == 0x2000) { //$2000-3fff
|
||||
io.rom.bank = data;
|
||||
}
|
||||
|
||||
if((addr & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(ram.enable) cartridge.writeRAM(ram.select << 13 | (uint13)addr, data);
|
||||
if((address & 0xe000) == 0x4000) { //$4000-5fff
|
||||
io.ram.bank = data;
|
||||
}
|
||||
|
||||
if((address & 0xe000) == 0xa000) { //$a000-bfff
|
||||
if(!io.ram.enable) return;
|
||||
cartridge.ram.write(io.ram.bank << 13 | address.bits(0,12), data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::MMM01::power() -> void {
|
||||
rom.base = 0x00;
|
||||
rom.select = 0x01;
|
||||
ram.enable = false;
|
||||
ram.select = 0x00;
|
||||
mode = 0;
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::MMM01::serialize(serializer& s) -> void {
|
||||
s.integer(io.mode);
|
||||
s.integer(io.rom.base);
|
||||
s.integer(io.rom.bank);
|
||||
s.integer(io.ram.enable);
|
||||
s.integer(io.ram.bank);
|
||||
}
|
||||
|
@@ -1,15 +1,18 @@
|
||||
struct MMM01 : MMIO {
|
||||
auto readIO(uint16 addr) -> uint8;
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
struct MMM01 : Mapper {
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer& s) -> void;
|
||||
|
||||
struct ROM {
|
||||
uint6 base;
|
||||
uint8 select;
|
||||
} rom;
|
||||
struct RAM {
|
||||
bool enable;
|
||||
uint8 select;
|
||||
} ram;
|
||||
bool mode;
|
||||
struct IO {
|
||||
uint1 mode;
|
||||
struct ROM {
|
||||
uint6 base;
|
||||
uint8 bank = 0x01;
|
||||
} rom;
|
||||
struct RAM {
|
||||
uint1 enable;
|
||||
uint8 bank;
|
||||
} ram;
|
||||
} io;
|
||||
} mmm01;
|
||||
|
@@ -1,51 +1,8 @@
|
||||
auto Cartridge::serialize(serializer& s) -> void {
|
||||
if(information.battery) s.array(ram.data, ram.size);
|
||||
if(ram.size) s.array(ram.data, ram.size);
|
||||
if(rtc.size) s.array(rtc.data, rtc.size);
|
||||
|
||||
s.integer(bootromEnable);
|
||||
|
||||
s.integer(mbc1.rom.select);
|
||||
s.integer(mbc1.ram.enable);
|
||||
s.integer(mbc1.ram.select);
|
||||
s.integer(mbc1.mode);
|
||||
|
||||
s.integer(mbc1m.rom.lo);
|
||||
s.integer(mbc1m.rom.hi);
|
||||
s.integer(mbc1m.mode);
|
||||
|
||||
s.integer(mbc2.rom.select);
|
||||
s.integer(mbc2.ram.enable);
|
||||
|
||||
s.integer(mbc3.rom.select);
|
||||
s.integer(mbc3.ram.enable);
|
||||
s.integer(mbc3.ram.select);
|
||||
s.integer(mbc3.rtc.latch);
|
||||
s.integer(mbc3.rtc.halt);
|
||||
s.integer(mbc3.rtc.second);
|
||||
s.integer(mbc3.rtc.minute);
|
||||
s.integer(mbc3.rtc.hour);
|
||||
s.integer(mbc3.rtc.day);
|
||||
s.integer(mbc3.rtc.dayCarry);
|
||||
s.integer(mbc3.rtc.latchSecond);
|
||||
s.integer(mbc3.rtc.latchMinute);
|
||||
s.integer(mbc3.rtc.latchHour);
|
||||
s.integer(mbc3.rtc.latchDay);
|
||||
s.integer(mbc3.rtc.latchDayCarry);
|
||||
|
||||
s.integer(mbc5.rom.select);
|
||||
s.integer(mbc5.ram.enable);
|
||||
s.integer(mbc5.ram.select);
|
||||
|
||||
s.integer(mmm01.rom.base);
|
||||
s.integer(mmm01.rom.select);
|
||||
s.integer(mmm01.ram.enable);
|
||||
s.integer(mmm01.ram.select);
|
||||
s.integer(mmm01.mode);
|
||||
|
||||
s.integer(huc1.rom.select);
|
||||
s.integer(huc1.ram.writable);
|
||||
s.integer(huc1.ram.select);
|
||||
s.integer(huc1.model);
|
||||
|
||||
s.integer(huc3.rom.select);
|
||||
s.integer(huc3.ram.enable);
|
||||
s.integer(huc3.ram.select);
|
||||
mapper->serialize(s);
|
||||
}
|
||||
|
236
higan/gb/cartridge/tama/tama.cpp
Normal file
236
higan/gb/cartridge/tama/tama.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
//U1: TAMA7: Mask ROM (512KB)
|
||||
//U2: TAMA5: Game Boy cartridge connector interface
|
||||
//U3: TAMA6: Toshiba TMP47C243M (4-bit MCU)
|
||||
//U4: RTC: Toshiba TC8521AM
|
||||
|
||||
//note: the TMP47C243M's 2048 x 8-bit program ROM is currently undumped
|
||||
//as such, high level emulation is used as a necessary evil
|
||||
|
||||
auto Cartridge::TAMA::second() -> void {
|
||||
if(++rtc.second >= 60) {
|
||||
rtc.second = 0;
|
||||
|
||||
if(++rtc.minute >= 60) {
|
||||
rtc.minute = 0;
|
||||
|
||||
if(rtc.hourMode == 0 && ++rtc.hour >= 12) {
|
||||
rtc.hour = 0;
|
||||
rtc.meridian++;
|
||||
}
|
||||
|
||||
if(rtc.hourMode == 1 && ++rtc.hour >= 24) {
|
||||
rtc.hour = 0;
|
||||
rtc.meridian = rtc.hour >= 12;
|
||||
}
|
||||
|
||||
if((rtc.hourMode == 0 && rtc.hour == 0 && rtc.meridian == 0)
|
||||
|| (rtc.hourMode == 1 && rtc.hour == 0)
|
||||
) {
|
||||
uint days[12] = {31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31};
|
||||
if(rtc.leapYear == 0) days[1] = 29; //extra day in February for leap years
|
||||
|
||||
if(++rtc.day > days[(rtc.month - 1) % 12]) {
|
||||
rtc.day = 1;
|
||||
|
||||
if(++rtc.month > 12) {
|
||||
rtc.month = 1;
|
||||
rtc.leapYear++;
|
||||
|
||||
if(++rtc.year >= 100) {
|
||||
rtc.year = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::TAMA::read(uint16 address) -> uint8 {
|
||||
if((address & 0xc000) == 0x0000) { //$0000-3fff
|
||||
return cartridge.rom.read(address.bits(0,13));
|
||||
}
|
||||
|
||||
if((address & 0xc000) == 0x4000) { //$4000-7fff
|
||||
return cartridge.rom.read(io.rom.bank << 14 | address.bits(0,13));
|
||||
}
|
||||
|
||||
if((address & 0xe001) == 0xa000) { //$a000-bfff (even)
|
||||
if(io.select == 0x0a) {
|
||||
return 0xf0 | io.ready;
|
||||
}
|
||||
|
||||
if(io.mode == 0 || io.mode == 1) {
|
||||
if(io.select == 0x0c) {
|
||||
return 0xf0 | io.output.bits(0,3);
|
||||
}
|
||||
|
||||
if(io.select == 0x0d) {
|
||||
return 0xf0 | io.output.bits(4,7);
|
||||
}
|
||||
}
|
||||
|
||||
if(io.mode == 2 || io.mode == 4) {
|
||||
if(io.select == 0x0c || io.select == 0x0d) {
|
||||
uint4 data;
|
||||
if(rtc.index == 0) data = rtc.minute % 10;
|
||||
if(rtc.index == 1) data = rtc.minute / 10;
|
||||
if(rtc.index == 2) data = rtc.hour % 10;
|
||||
if(rtc.index == 3) data = rtc.hour / 10;
|
||||
if(rtc.index == 4) data = rtc.day / 10;
|
||||
if(rtc.index == 5) data = rtc.day % 10;
|
||||
if(rtc.index == 6) data = rtc.month / 10;
|
||||
if(rtc.index == 7) data = rtc.month % 10;
|
||||
rtc.index++;
|
||||
return 0xf0 | data;
|
||||
}
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
if((address & 0xe001) == 0xa001) { //$a000-bfff (odd)
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto Cartridge::TAMA::write(uint16 address, uint8 data) -> void {
|
||||
auto toBCD = [](uint8 data) -> uint8 { return (data / 10) * 16 + (data % 10); };
|
||||
auto fromBCD = [](uint8 data) -> uint8 { return (data / 16) * 10 + (data % 16); };
|
||||
|
||||
if((address & 0xe001) == 0xa000) { //$a000-bfff (even)
|
||||
if(io.select == 0x00) {
|
||||
io.rom.bank.bits(0,3) = data.bits(0,3);
|
||||
}
|
||||
|
||||
if(io.select == 0x01) {
|
||||
io.rom.bank.bit(4) = data.bit(0);
|
||||
}
|
||||
|
||||
if(io.select == 0x04) {
|
||||
io.input.bits(0,3) = data.bits(0,3);
|
||||
}
|
||||
|
||||
if(io.select == 0x05) {
|
||||
io.input.bits(4,7) = data.bits(0,3);
|
||||
}
|
||||
|
||||
if(io.select == 0x06) {
|
||||
io.index.bit(4) = data.bit(0);
|
||||
io.mode = data.bits(1,3);
|
||||
}
|
||||
|
||||
if(io.select == 0x07) {
|
||||
io.index.bits(0,3) = data.bits(0,3);
|
||||
|
||||
if(io.mode == 0) {
|
||||
cartridge.ram.write(io.index, io.input);
|
||||
}
|
||||
|
||||
if(io.mode == 1) {
|
||||
io.output = cartridge.ram.read(io.index);
|
||||
}
|
||||
|
||||
if(io.mode == 2 && io.index == 0x04) {
|
||||
rtc.minute = fromBCD(io.input);
|
||||
}
|
||||
|
||||
if(io.mode == 2 && io.index == 0x05) {
|
||||
rtc.hour = fromBCD(io.input);
|
||||
rtc.meridian = rtc.hour >= 12;
|
||||
}
|
||||
|
||||
if(io.mode == 4 && io.index == 0x00 && io.input.bits(0,3) == 0x7) {
|
||||
uint8 day = toBCD(rtc.day);
|
||||
day.bits(0,3) = io.input.bits(4,7);
|
||||
rtc.day = fromBCD(day);
|
||||
}
|
||||
|
||||
if(io.mode == 4 && io.index == 0x00 && io.input.bits(0,3) == 0x8) {
|
||||
uint8 day = toBCD(rtc.day);
|
||||
day.bits(4,7) = io.input.bits(4,7);
|
||||
rtc.day = fromBCD(day);
|
||||
}
|
||||
|
||||
if(io.mode == 4 && io.index == 0x00 && io.input.bits(0,3) == 0x9) {
|
||||
uint8 month = toBCD(rtc.month);
|
||||
month.bits(0,3) = io.input.bits(4,7);
|
||||
rtc.month = fromBCD(month);
|
||||
}
|
||||
|
||||
if(io.mode == 4 && io.index == 0x00 && io.input.bits(0,3) == 0xa) {
|
||||
uint8 month = toBCD(rtc.month);
|
||||
month.bits(4,7) = io.input.bits(4,7);
|
||||
rtc.month = fromBCD(month);
|
||||
}
|
||||
|
||||
if(io.mode == 4 && io.index == 0x00 && io.input.bits(0,3) == 0xb) {
|
||||
uint8 year = toBCD(rtc.year);
|
||||
year.bits(0,3) = io.input.bits(4,7);
|
||||
rtc.year = fromBCD(year);
|
||||
}
|
||||
|
||||
if(io.mode == 4 && io.index == 0x00 && io.input.bits(0,3) == 0xc) {
|
||||
uint8 year = toBCD(rtc.year);
|
||||
year.bits(4,7) = io.input.bits(4,7);
|
||||
rtc.year = fromBCD(year);
|
||||
}
|
||||
|
||||
if(io.mode == 4 && io.index == 0x02 && io.input.bits(0,3) == 0xa) {
|
||||
rtc.hourMode = io.input.bit(4);
|
||||
rtc.second = 0; //hack: unclear where this is really being set (if it is at all)
|
||||
}
|
||||
|
||||
if(io.mode == 4 && io.index == 0x02 && io.input.bits(0,3) == 0xb) {
|
||||
rtc.leapYear = data.bits(4,5);
|
||||
}
|
||||
|
||||
if(io.mode == 4 && io.index == 0x02 && io.input.bits(0,3) == 0xe) {
|
||||
rtc.test = io.input.bits(4,7);
|
||||
}
|
||||
|
||||
if(io.mode == 2 && io.index == 0x06) {
|
||||
rtc.index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if((address & 0xe001) == 0xa001) { //$a000-bfff (odd)
|
||||
io.select = data.bits(0,3);
|
||||
|
||||
if(io.select == 0x0a) {
|
||||
io.ready = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::TAMA::power() -> void {
|
||||
io = {};
|
||||
}
|
||||
|
||||
auto Cartridge::TAMA::serialize(serializer& s) -> void {
|
||||
s.integer(io.ready);
|
||||
s.integer(io.select);
|
||||
s.integer(io.mode);
|
||||
s.integer(io.index);
|
||||
s.integer(io.input);
|
||||
s.integer(io.output);
|
||||
s.integer(io.rom.bank);
|
||||
|
||||
s.integer(rtc.year);
|
||||
s.integer(rtc.month);
|
||||
s.integer(rtc.day);
|
||||
s.integer(rtc.hour);
|
||||
s.integer(rtc.minute);
|
||||
s.integer(rtc.second);
|
||||
s.integer(rtc.meridian);
|
||||
s.integer(rtc.leapYear);
|
||||
s.integer(rtc.hourMode);
|
||||
s.integer(rtc.test);
|
||||
}
|
33
higan/gb/cartridge/tama/tama.hpp
Normal file
33
higan/gb/cartridge/tama/tama.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
struct TAMA : Mapper {
|
||||
auto second() -> void;
|
||||
auto read(uint16 address) -> uint8;
|
||||
auto write(uint16 address, uint8 data) -> void;
|
||||
auto power() -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
struct IO {
|
||||
uint1 ready;
|
||||
uint4 select;
|
||||
uint3 mode;
|
||||
uint5 index;
|
||||
uint8 input;
|
||||
uint8 output;
|
||||
struct ROM {
|
||||
uint5 bank;
|
||||
} rom;
|
||||
} io;
|
||||
|
||||
struct RTC {
|
||||
uint8 year; //0 - 99
|
||||
uint8 month; //1 - 12
|
||||
uint8 day; //1 - 31
|
||||
uint8 hour; //0 - 23
|
||||
uint8 minute; //0 - 59
|
||||
uint8 second; //0 - 59
|
||||
uint1 meridian; //0 = AM; 1 = PM
|
||||
uint2 leapYear; //0 = leap year; 1-3 = non-leap year
|
||||
uint1 hourMode; //0 = 12-hour; 1 = 24-hour
|
||||
uint4 test;
|
||||
uint8 index;
|
||||
} rtc;
|
||||
} tama;
|
@@ -123,13 +123,6 @@ auto CPU::power() -> void {
|
||||
for(auto& n : wram) n = 0x00;
|
||||
for(auto& n : hram) n = 0x00;
|
||||
|
||||
r[PC] = 0x0000;
|
||||
r[SP] = 0x0000;
|
||||
r[AF] = 0x0000;
|
||||
r[BC] = 0x0000;
|
||||
r[DE] = 0x0000;
|
||||
r[HL] = 0x0000;
|
||||
|
||||
memory::fill(&status, sizeof(Status));
|
||||
status.dmaCompleted = true;
|
||||
status.wramBank = 1;
|
||||
|
@@ -17,13 +17,13 @@ struct CPU : Processor::LR35902, Thread, MMIO {
|
||||
auto writeIO(uint16 addr, uint8 data) -> void;
|
||||
|
||||
//memory.cpp
|
||||
auto io() -> void override;
|
||||
auto idle() -> void override;
|
||||
auto read(uint16 addr) -> uint8 override;
|
||||
auto write(uint16 addr, uint8 data) -> void override;
|
||||
auto cycleEdge() -> void;
|
||||
auto readDMA(uint16 addr) -> uint8;
|
||||
auto writeDMA(uint16 addr, uint8 data) -> void;
|
||||
auto readDebugger(uint16 addr) -> uint8;
|
||||
auto readDebugger(uint16 addr) -> uint8 override;
|
||||
|
||||
//timing.cpp
|
||||
auto step(uint clocks) -> void;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
auto CPU::io() -> void {
|
||||
auto CPU::idle() -> void {
|
||||
cycleEdge();
|
||||
step(4);
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@
|
||||
auto CPU::step(uint clocks) -> void {
|
||||
for(auto n : range(clocks)) {
|
||||
if(++status.clock == 0) {
|
||||
cartridge.mbc3.second();
|
||||
cartridge.second();
|
||||
}
|
||||
|
||||
//4MHz / N(hz) - 1 = mask
|
||||
@@ -76,8 +76,8 @@ auto CPU::hblank() -> void {
|
||||
if(status.dmaMode == 1 && status.dmaLength && ppu.status.ly < 144) {
|
||||
for(auto n : range(16)) {
|
||||
writeDMA(status.dmaTarget++, readDMA(status.dmaSource++));
|
||||
status.dmaLength--;
|
||||
if(n & 1) step(1 << status.speedDouble);
|
||||
}
|
||||
step(8 << status.speedDouble);
|
||||
status.dmaLength -= 16;
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ GameBoyColorInterface::GameBoyColorInterface() {
|
||||
information.name = "Game Boy Color";
|
||||
information.overscan = false;
|
||||
|
||||
media.append({ID::GameBoyColor, "Game Boy Color", "gb"});
|
||||
media.append({ID::GameBoyColor, "Game Boy Color", "gbc"});
|
||||
}
|
||||
|
||||
auto GameBoyColorInterface::videoColors() -> uint32 {
|
||||
|
@@ -19,6 +19,9 @@ Interface::Interface() {
|
||||
device.inputs.append({0, "A" });
|
||||
device.inputs.append({0, "Select"});
|
||||
device.inputs.append({0, "Start" });
|
||||
device.inputs.append({1, "X-axis"});
|
||||
device.inputs.append({1, "Y-axis"});
|
||||
device.inputs.append({2, "Rumble"});
|
||||
hardwarePort.devices.append(device);
|
||||
}
|
||||
|
||||
@@ -33,15 +36,8 @@ auto Interface::title() -> string {
|
||||
return cartridge.title();
|
||||
}
|
||||
|
||||
auto Interface::videoResolution() -> VideoSize {
|
||||
return {160, 144};
|
||||
}
|
||||
|
||||
auto Interface::videoSize(uint width, uint height, bool arc) -> VideoSize {
|
||||
uint w = 160;
|
||||
uint h = 144;
|
||||
uint m = min(width / w, height / h);
|
||||
return {w * m, h * m};
|
||||
auto Interface::videoResolution() -> VideoResolution {
|
||||
return {160, 144, 160, 144, 1.0};
|
||||
}
|
||||
|
||||
auto Interface::loaded() -> bool {
|
||||
|
@@ -23,8 +23,7 @@ struct Interface : Emulator::Interface {
|
||||
auto manifest() -> string override;
|
||||
auto title() -> string override;
|
||||
|
||||
auto videoResolution() -> VideoSize override;
|
||||
auto videoSize(uint width, uint height, bool arc) -> VideoSize override;
|
||||
auto videoResolution() -> VideoResolution override;
|
||||
|
||||
auto loaded() -> bool override;
|
||||
auto sha256() -> string override;
|
||||
|
@@ -37,7 +37,6 @@ auto PPU::readTileCGB(bool select, uint x, uint y, uint& attr, uint& data) -> vo
|
||||
|
||||
auto PPU::scanlineCGB() -> void {
|
||||
px = 0;
|
||||
if(!enabled()) return;
|
||||
|
||||
const uint Height = (status.obSize == 0 ? 8 : 16);
|
||||
sprites = 0;
|
||||
@@ -69,24 +68,22 @@ auto PPU::runCGB() -> void {
|
||||
ob.priority = 0;
|
||||
|
||||
uint color = 0x7fff;
|
||||
if(enabled()) {
|
||||
runBackgroundCGB();
|
||||
if(status.windowDisplayEnable) runWindowCGB();
|
||||
if(status.obEnable) runObjectsCGB();
|
||||
runBackgroundCGB();
|
||||
if(status.windowDisplayEnable) runWindowCGB();
|
||||
if(status.obEnable) runObjectsCGB();
|
||||
|
||||
if(ob.palette == 0) {
|
||||
color = bg.color;
|
||||
} else if(bg.palette == 0) {
|
||||
color = ob.color;
|
||||
} else if(status.bgEnable == 0) {
|
||||
color = ob.color;
|
||||
} else if(bg.priority) {
|
||||
color = bg.color;
|
||||
} else if(ob.priority) {
|
||||
color = ob.color;
|
||||
} else {
|
||||
color = bg.color;
|
||||
}
|
||||
if(ob.palette == 0) {
|
||||
color = bg.color;
|
||||
} else if(bg.palette == 0) {
|
||||
color = ob.color;
|
||||
} else if(status.bgEnable == 0) {
|
||||
color = ob.color;
|
||||
} else if(bg.priority) {
|
||||
color = bg.color;
|
||||
} else if(ob.priority) {
|
||||
color = ob.color;
|
||||
} else {
|
||||
color = bg.color;
|
||||
}
|
||||
|
||||
uint32* output = screen + status.ly * 160 + px++;
|
||||
|
@@ -19,7 +19,6 @@ auto PPU::readTileDMG(bool select, uint x, uint y, uint& data) -> void {
|
||||
|
||||
auto PPU::scanlineDMG() -> void {
|
||||
px = 0;
|
||||
if(!enabled()) return;
|
||||
|
||||
const uint Height = (status.obSize == 0 ? 8 : 16);
|
||||
sprites = 0;
|
||||
@@ -60,20 +59,18 @@ auto PPU::runDMG() -> void {
|
||||
ob.palette = 0;
|
||||
|
||||
uint color = 0;
|
||||
if(enabled()) {
|
||||
if(status.bgEnable) runBackgroundDMG();
|
||||
if(status.windowDisplayEnable) runWindowDMG();
|
||||
if(status.obEnable) runObjectsDMG();
|
||||
if(status.bgEnable) runBackgroundDMG();
|
||||
if(status.windowDisplayEnable) runWindowDMG();
|
||||
if(status.obEnable) runObjectsDMG();
|
||||
|
||||
if(ob.palette == 0) {
|
||||
color = bg.color;
|
||||
} else if(bg.palette == 0) {
|
||||
color = ob.color;
|
||||
} else if(ob.priority) {
|
||||
color = ob.color;
|
||||
} else {
|
||||
color = bg.color;
|
||||
}
|
||||
if(ob.palette == 0) {
|
||||
color = bg.color;
|
||||
} else if(bg.palette == 0) {
|
||||
color = ob.color;
|
||||
} else if(ob.priority) {
|
||||
color = ob.color;
|
||||
} else {
|
||||
color = bg.color;
|
||||
}
|
||||
|
||||
uint32* output = screen + status.ly * 160 + px++;
|
||||
|
@@ -113,7 +113,8 @@ auto PPU::writeIO(uint16 addr, uint8 data) -> void {
|
||||
}
|
||||
|
||||
if(addr == 0xff40) { //LCDC
|
||||
if(!status.displayEnable && (data & 0x80)) {
|
||||
if(status.displayEnable && !data.bit(7)) {
|
||||
status.mode = 0;
|
||||
status.ly = 0;
|
||||
status.lx = 0;
|
||||
|
||||
|
@@ -8,39 +8,45 @@ PPU ppu;
|
||||
#include "cgb.cpp"
|
||||
#include "serialization.cpp"
|
||||
|
||||
auto PPU::enabled() const -> bool { return status.displayEnable; }
|
||||
|
||||
auto PPU::Enter() -> void {
|
||||
while(true) scheduler.synchronize(), ppu.main();
|
||||
}
|
||||
|
||||
auto PPU::main() -> void {
|
||||
if(!status.displayEnable) {
|
||||
for(uint n : range(160 * 144)) screen[n] = Model::GameBoy() ? 0 : 0x7fff;
|
||||
Thread::step(154 * 456);
|
||||
synchronize(cpu);
|
||||
scheduler.exit(Scheduler::Event::Frame);
|
||||
return;
|
||||
}
|
||||
|
||||
status.lx = 0;
|
||||
if(Model::SuperGameBoy()) superGameBoy->lcdScanline();
|
||||
|
||||
if(status.ly <= 143) {
|
||||
mode(2);
|
||||
status.mode = 2;
|
||||
scanline();
|
||||
step(92);
|
||||
|
||||
mode(3);
|
||||
status.mode = 3;
|
||||
for(auto n : range(160)) {
|
||||
run();
|
||||
step(1);
|
||||
}
|
||||
|
||||
mode(0);
|
||||
if(enabled()) cpu.hblank();
|
||||
status.mode = 0;
|
||||
cpu.hblank();
|
||||
step(204);
|
||||
} else {
|
||||
mode(1);
|
||||
status.mode = 1;
|
||||
step(456);
|
||||
}
|
||||
|
||||
status.ly++;
|
||||
|
||||
if(status.ly == 144) {
|
||||
if(enabled()) cpu.raise(CPU::Interrupt::Vblank);
|
||||
cpu.raise(CPU::Interrupt::Vblank);
|
||||
scheduler.exit(Scheduler::Event::Frame);
|
||||
}
|
||||
|
||||
@@ -49,10 +55,6 @@ auto PPU::main() -> void {
|
||||
}
|
||||
}
|
||||
|
||||
auto PPU::mode(uint mode) -> void {
|
||||
status.mode = mode;
|
||||
}
|
||||
|
||||
auto PPU::stat() -> void {
|
||||
bool irq = status.irq;
|
||||
|
||||
|
@@ -1,9 +1,6 @@
|
||||
struct PPU : Thread, MMIO {
|
||||
auto enabled() const -> bool;
|
||||
|
||||
static auto Enter() -> void;
|
||||
auto main() -> void;
|
||||
auto mode(uint) -> void;
|
||||
auto stat() -> void;
|
||||
auto coincidence() -> bool;
|
||||
auto refresh() -> void;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
processors += arm
|
||||
processors += arm7tdmi
|
||||
|
||||
objects += gba-memory gba-interface gba-system
|
||||
objects += gba-cartridge gba-player
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace GameBoyAdvance {
|
||||
|
||||
APU apu;
|
||||
#include "io.cpp"
|
||||
#include "square.cpp"
|
||||
#include "square1.cpp"
|
||||
@@ -11,7 +12,6 @@ namespace GameBoyAdvance {
|
||||
#include "sequencer.cpp"
|
||||
#include "fifo.cpp"
|
||||
#include "serialization.cpp"
|
||||
APU apu;
|
||||
|
||||
auto APU::Enter() -> void {
|
||||
while(true) scheduler.synchronize(), apu.main();
|
||||
@@ -65,7 +65,7 @@ auto APU::main() -> void {
|
||||
if(regs.bias.amplitude == 2) lsample &= ~3, rsample &= ~3; //7-bit
|
||||
if(regs.bias.amplitude == 3) lsample &= ~7, rsample &= ~7; //6-bit
|
||||
|
||||
if(cpu.regs.mode == CPU::Registers::Mode::Stop) lsample = 0, rsample = 0;
|
||||
if(cpu.stopped()) lsample = 0, rsample = 0;
|
||||
stream->sample((lsample << 5) / 32768.0, (rsample << 5) / 32768.0);
|
||||
}
|
||||
|
||||
@@ -75,10 +75,10 @@ auto APU::step(uint clocks) -> void {
|
||||
}
|
||||
|
||||
auto APU::power() -> void {
|
||||
create(APU::Enter, 16'777'216);
|
||||
create(APU::Enter, system.frequency());
|
||||
stream = Emulator::audio.createStream(2, frequency() / 64.0);
|
||||
stream->addLowPassFilter(20000.0, 3);
|
||||
stream->addHighPassFilter(20.0, 3);
|
||||
stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 20.0);
|
||||
stream->addFilter(Emulator::Filter::Order::Second, Emulator::Filter::Type::LowPass, 20000.0, 3);
|
||||
|
||||
clock = 0;
|
||||
square1.power();
|
||||
|
@@ -1,8 +1,6 @@
|
||||
struct APU : Thread, IO {
|
||||
shared_pointer<Emulator::Stream> stream;
|
||||
|
||||
#include "registers.hpp"
|
||||
|
||||
static auto Enter() -> void;
|
||||
auto main() -> void;
|
||||
auto step(uint clocks) -> void;
|
||||
@@ -16,6 +14,165 @@ struct APU : Thread, IO {
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
uint clock;
|
||||
|
||||
struct Registers {
|
||||
struct SoundBias {
|
||||
uint10 level;
|
||||
uint2 amplitude;
|
||||
} bias;
|
||||
} regs;
|
||||
|
||||
struct Sweep {
|
||||
uint3 shift;
|
||||
uint1 direction;
|
||||
uint3 frequency;
|
||||
|
||||
uint1 enable;
|
||||
uint1 negate;
|
||||
uint3 period;
|
||||
};
|
||||
|
||||
struct Envelope {
|
||||
uint3 frequency;
|
||||
uint1 direction;
|
||||
uint4 volume;
|
||||
|
||||
uint3 period;
|
||||
|
||||
auto dacEnable() const -> bool { return volume || direction; }
|
||||
};
|
||||
|
||||
struct Square {
|
||||
Envelope envelope;
|
||||
uint1 enable;
|
||||
uint6 length;
|
||||
uint2 duty;
|
||||
uint11 frequency;
|
||||
uint1 counter;
|
||||
uint1 initialize;
|
||||
|
||||
int shadowfrequency;
|
||||
uint1 signal;
|
||||
uint4 output;
|
||||
uint period;
|
||||
uint3 phase;
|
||||
uint4 volume;
|
||||
|
||||
auto run() -> void;
|
||||
auto clocklength() -> void;
|
||||
auto clockenvelope() -> void;
|
||||
};
|
||||
|
||||
struct Square1 : Square {
|
||||
Sweep sweep;
|
||||
|
||||
auto runsweep(bool update) -> void;
|
||||
auto clocksweep() -> void;
|
||||
auto read(uint addr) const -> uint8;
|
||||
auto write(uint addr, uint8 byte) -> void;
|
||||
auto power() -> void;
|
||||
} square1;
|
||||
|
||||
struct Square2 : Square {
|
||||
auto read(uint addr) const -> uint8;
|
||||
auto write(uint addr, uint8 byte) -> void;
|
||||
auto power() -> void;
|
||||
} square2;
|
||||
|
||||
struct Wave {
|
||||
uint1 mode;
|
||||
uint1 bank;
|
||||
uint1 dacenable;
|
||||
uint8 length;
|
||||
uint3 volume;
|
||||
uint11 frequency;
|
||||
uint1 counter;
|
||||
uint1 initialize;
|
||||
uint4 pattern[2 * 32];
|
||||
|
||||
uint1 enable;
|
||||
uint4 output;
|
||||
uint5 patternaddr;
|
||||
uint1 patternbank;
|
||||
uint4 patternsample;
|
||||
uint period;
|
||||
|
||||
auto run() -> void;
|
||||
auto clocklength() -> void;
|
||||
auto read(uint addr) const -> uint8;
|
||||
auto write(uint addr, uint8 byte) -> void;
|
||||
auto readram(uint addr) const -> uint8;
|
||||
auto writeram(uint addr, uint8 byte) -> void;
|
||||
auto power() -> void;
|
||||
} wave;
|
||||
|
||||
struct Noise {
|
||||
Envelope envelope;
|
||||
uint6 length;
|
||||
uint3 divisor;
|
||||
uint1 narrowlfsr;
|
||||
uint4 frequency;
|
||||
uint1 counter;
|
||||
uint1 initialize;
|
||||
|
||||
uint1 enable;
|
||||
uint15 lfsr;
|
||||
uint4 output;
|
||||
uint period;
|
||||
uint4 volume;
|
||||
|
||||
auto divider() const -> uint;
|
||||
auto run() -> void;
|
||||
auto clocklength() -> void;
|
||||
auto clockenvelope() -> void;
|
||||
auto read(uint addr) const -> uint8;
|
||||
auto write(uint addr, uint8 byte) -> void;
|
||||
auto power() -> void;
|
||||
} noise;
|
||||
|
||||
struct Sequencer {
|
||||
uint2 volume;
|
||||
uint3 lvolume;
|
||||
uint3 rvolume;
|
||||
uint1 lenable[4];
|
||||
uint1 renable[4];
|
||||
uint1 masterenable;
|
||||
|
||||
uint12 base;
|
||||
uint3 step;
|
||||
int16 lsample;
|
||||
int16 rsample;
|
||||
|
||||
uint10 loutput;
|
||||
uint10 routput;
|
||||
|
||||
auto sample() -> void;
|
||||
|
||||
auto read(uint addr) const -> uint8;
|
||||
auto write(uint addr, uint8 byte) -> void;
|
||||
auto power() -> void;
|
||||
} sequencer;
|
||||
|
||||
struct FIFO {
|
||||
int8 samples[32];
|
||||
int8 active;
|
||||
int8 output;
|
||||
|
||||
uint5 rdoffset;
|
||||
uint5 wroffset;
|
||||
uint6 size;
|
||||
|
||||
uint1 volume; //0 = 50%, 1 = 100%
|
||||
uint1 lenable;
|
||||
uint1 renable;
|
||||
uint1 timer;
|
||||
|
||||
auto sample() -> void;
|
||||
auto read() -> void;
|
||||
auto write(int8 byte) -> void;
|
||||
auto reset() -> void;
|
||||
auto power() -> void;
|
||||
} fifo[2];
|
||||
};
|
||||
|
||||
extern APU apu;
|
||||
|
@@ -1,158 +0,0 @@
|
||||
struct Registers {
|
||||
struct SoundBias {
|
||||
uint10 level;
|
||||
uint2 amplitude;
|
||||
} bias;
|
||||
} regs;
|
||||
|
||||
struct Sweep {
|
||||
uint3 shift;
|
||||
uint1 direction;
|
||||
uint3 frequency;
|
||||
|
||||
uint1 enable;
|
||||
uint1 negate;
|
||||
uint3 period;
|
||||
};
|
||||
|
||||
struct Envelope {
|
||||
uint3 frequency;
|
||||
uint1 direction;
|
||||
uint4 volume;
|
||||
|
||||
uint3 period;
|
||||
|
||||
auto dacEnable() const -> bool { return volume || direction; }
|
||||
};
|
||||
|
||||
struct Square {
|
||||
Envelope envelope;
|
||||
uint1 enable;
|
||||
uint6 length;
|
||||
uint2 duty;
|
||||
uint11 frequency;
|
||||
uint1 counter;
|
||||
uint1 initialize;
|
||||
|
||||
int shadowfrequency;
|
||||
uint1 signal;
|
||||
uint4 output;
|
||||
uint period;
|
||||
uint3 phase;
|
||||
uint4 volume;
|
||||
|
||||
auto run() -> void;
|
||||
auto clocklength() -> void;
|
||||
auto clockenvelope() -> void;
|
||||
};
|
||||
|
||||
struct Square1 : Square {
|
||||
Sweep sweep;
|
||||
|
||||
auto runsweep(bool update) -> void;
|
||||
auto clocksweep() -> void;
|
||||
auto read(uint addr) const -> uint8;
|
||||
auto write(uint addr, uint8 byte) -> void;
|
||||
auto power() -> void;
|
||||
} square1;
|
||||
|
||||
struct Square2 : Square {
|
||||
auto read(uint addr) const -> uint8;
|
||||
auto write(uint addr, uint8 byte) -> void;
|
||||
auto power() -> void;
|
||||
} square2;
|
||||
|
||||
struct Wave {
|
||||
uint1 mode;
|
||||
uint1 bank;
|
||||
uint1 dacenable;
|
||||
uint8 length;
|
||||
uint3 volume;
|
||||
uint11 frequency;
|
||||
uint1 counter;
|
||||
uint1 initialize;
|
||||
uint4 pattern[32];
|
||||
|
||||
uint1 enable;
|
||||
uint4 output;
|
||||
uint4 patternaddr;
|
||||
uint1 patternbank;
|
||||
uint4 patternsample;
|
||||
uint period;
|
||||
|
||||
auto run() -> void;
|
||||
auto clocklength() -> void;
|
||||
auto read(uint addr) const -> uint8;
|
||||
auto write(uint addr, uint8 byte) -> void;
|
||||
auto readram(uint addr) const -> uint8;
|
||||
auto writeram(uint addr, uint8 byte) -> void;
|
||||
auto power() -> void;
|
||||
} wave;
|
||||
|
||||
struct Noise {
|
||||
Envelope envelope;
|
||||
uint6 length;
|
||||
uint3 divisor;
|
||||
uint1 narrowlfsr;
|
||||
uint4 frequency;
|
||||
uint1 counter;
|
||||
uint1 initialize;
|
||||
|
||||
uint1 enable;
|
||||
uint15 lfsr;
|
||||
uint4 output;
|
||||
uint period;
|
||||
uint4 volume;
|
||||
|
||||
auto divider() const -> uint;
|
||||
auto run() -> void;
|
||||
auto clocklength() -> void;
|
||||
auto clockenvelope() -> void;
|
||||
auto read(uint addr) const -> uint8;
|
||||
auto write(uint addr, uint8 byte) -> void;
|
||||
auto power() -> void;
|
||||
} noise;
|
||||
|
||||
struct Sequencer {
|
||||
uint2 volume;
|
||||
uint3 lvolume;
|
||||
uint3 rvolume;
|
||||
uint1 lenable[4];
|
||||
uint1 renable[4];
|
||||
uint1 masterenable;
|
||||
|
||||
uint12 base;
|
||||
uint3 step;
|
||||
int16 lsample;
|
||||
int16 rsample;
|
||||
|
||||
uint10 loutput;
|
||||
uint10 routput;
|
||||
|
||||
auto sample() -> void;
|
||||
|
||||
auto read(uint addr) const -> uint8;
|
||||
auto write(uint addr, uint8 byte) -> void;
|
||||
auto power() -> void;
|
||||
} sequencer;
|
||||
|
||||
struct FIFO {
|
||||
int8 samples[32];
|
||||
int8 active;
|
||||
int8 output;
|
||||
|
||||
uint5 rdoffset;
|
||||
uint5 wroffset;
|
||||
uint6 size;
|
||||
|
||||
uint1 volume; //0 = 50%, 1 = 100%
|
||||
uint1 lenable;
|
||||
uint1 renable;
|
||||
uint1 timer;
|
||||
|
||||
auto sample() -> void;
|
||||
auto read() -> void;
|
||||
auto write(int8 byte) -> void;
|
||||
auto reset() -> void;
|
||||
auto power() -> void;
|
||||
} fifo[2];
|
@@ -1,7 +1,7 @@
|
||||
auto APU::Wave::run() -> void {
|
||||
if(period && --period == 0) {
|
||||
period = 1 * (2048 - frequency);
|
||||
patternsample = pattern[patternbank * 16 + patternaddr++];
|
||||
patternsample = pattern[patternbank << 5 | patternaddr++];
|
||||
if(patternaddr == 0) patternbank ^= mode;
|
||||
}
|
||||
|
||||
@@ -66,14 +66,14 @@ auto APU::Wave::write(uint addr, uint8 byte) -> void {
|
||||
|
||||
auto APU::Wave::readram(uint addr) const -> uint8 {
|
||||
uint8 byte = 0;
|
||||
byte |= pattern[addr * 2 + 0] << 0;
|
||||
byte |= pattern[addr * 2 + 1] << 4;
|
||||
byte |= pattern[!bank << 5 | addr << 1 | 0] << 4;
|
||||
byte |= pattern[!bank << 5 | addr << 1 | 1] << 0;
|
||||
return byte;
|
||||
}
|
||||
|
||||
auto APU::Wave::writeram(uint addr, uint8 byte) -> void {
|
||||
pattern[addr * 2 + 0] = byte >> 0;
|
||||
pattern[addr * 2 + 1] = byte >> 4;
|
||||
pattern[!bank << 5 | addr << 1 | 0] = byte >> 4;
|
||||
pattern[!bank << 5 | addr << 1 | 1] = byte >> 0;
|
||||
}
|
||||
|
||||
auto APU::Wave::power() -> void {
|
||||
|
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace GameBoyAdvance {
|
||||
|
||||
Cartridge cartridge;
|
||||
#include "mrom.cpp"
|
||||
#include "sram.cpp"
|
||||
#include "eeprom.cpp"
|
||||
#include "flash.cpp"
|
||||
#include "serialization.cpp"
|
||||
Cartridge cartridge;
|
||||
|
||||
Cartridge::Cartridge() {
|
||||
mrom.data = new uint8[mrom.size = 32 * 1024 * 1024];
|
||||
|
@@ -1,26 +1,26 @@
|
||||
auto CPU::_idle() -> void {
|
||||
auto CPU::sleep() -> void {
|
||||
prefetchStep(1);
|
||||
}
|
||||
|
||||
auto CPU::_read(uint mode, uint32 addr) -> uint32 {
|
||||
uint wait = this->wait(mode, addr);
|
||||
auto CPU::get(uint mode, uint32 addr) -> uint32 {
|
||||
uint clocks = _wait(mode, addr);
|
||||
uint word = pipeline.fetch.instruction;
|
||||
|
||||
if(addr >= 0x1000'0000) {
|
||||
prefetchStep(wait);
|
||||
prefetchStep(clocks);
|
||||
} else if(addr & 0x0800'0000) {
|
||||
if(mode & Prefetch && regs.wait.control.prefetch) {
|
||||
if(mode & Prefetch && wait.prefetch) {
|
||||
prefetchSync(addr);
|
||||
word = prefetchRead();
|
||||
if(mode & Word) word |= prefetchRead() << 16;
|
||||
} else {
|
||||
if(!active.dma) prefetchWait();
|
||||
step(wait - 1);
|
||||
if(!context.dmaActive) prefetchWait();
|
||||
step(clocks - 1);
|
||||
word = cartridge.read(mode, addr);
|
||||
step(1);
|
||||
}
|
||||
} else {
|
||||
prefetchStep(wait - 1);
|
||||
prefetchStep(clocks - 1);
|
||||
if(addr < 0x0200'0000) word = bios.read(mode, addr);
|
||||
else if(addr < 0x0300'0000) word = readEWRAM(mode, addr);
|
||||
else if(addr < 0x0400'0000) word = readIWRAM(mode, addr);
|
||||
@@ -35,17 +35,17 @@ auto CPU::_read(uint mode, uint32 addr) -> uint32 {
|
||||
return word;
|
||||
}
|
||||
|
||||
auto CPU::_write(uint mode, uint32 addr, uint32 word) -> void {
|
||||
uint wait = this->wait(mode, addr);
|
||||
auto CPU::set(uint mode, uint32 addr, uint32 word) -> void {
|
||||
uint clocks = _wait(mode, addr);
|
||||
|
||||
if(addr >= 0x1000'0000) {
|
||||
prefetchStep(wait);
|
||||
prefetchStep(clocks);
|
||||
} else if(addr & 0x0800'0000) {
|
||||
if(!active.dma) prefetchWait();
|
||||
step(wait);
|
||||
if(!context.dmaActive) prefetchWait();
|
||||
step(clocks);
|
||||
cartridge.write(mode, addr, word);
|
||||
} else {
|
||||
prefetchStep(wait);
|
||||
prefetchStep(clocks);
|
||||
if(addr < 0x0200'0000);
|
||||
else if(addr < 0x0300'0000) writeEWRAM(mode, addr, word);
|
||||
else if(addr < 0x0400'0000) writeIWRAM(mode, addr, word);
|
||||
@@ -57,17 +57,17 @@ auto CPU::_write(uint mode, uint32 addr, uint32 word) -> void {
|
||||
}
|
||||
}
|
||||
|
||||
auto CPU::wait(uint mode, uint32 addr) -> uint {
|
||||
auto CPU::_wait(uint mode, uint32 addr) -> uint {
|
||||
if(addr >= 0x1000'0000) return 1; //unmapped
|
||||
if(addr < 0x0200'0000) return 1;
|
||||
if(addr < 0x0300'0000) return (16 - regs.memory.control.ewramwait) * (mode & Word ? 2 : 1);
|
||||
if(addr < 0x0300'0000) return (16 - memory.ewramWait) * (mode & Word ? 2 : 1);
|
||||
if(addr < 0x0500'0000) return 1;
|
||||
if(addr < 0x0700'0000) return mode & Word ? 2 : 1;
|
||||
if(addr < 0x0800'0000) return 1;
|
||||
|
||||
static uint timings[] = {5, 4, 3, 9};
|
||||
uint n = timings[regs.wait.control.nwait[addr >> 25 & 3]];
|
||||
uint s = regs.wait.control.swait[addr >> 25 & 3];
|
||||
uint n = timings[wait.nwait[addr >> 25 & 3]];
|
||||
uint s = wait.swait[addr >> 25 & 3];
|
||||
|
||||
switch(addr & 0x0e00'0000) {
|
||||
case 0x0800'0000: s = s ? 2 : 3; break;
|
||||
|
@@ -2,172 +2,100 @@
|
||||
|
||||
namespace GameBoyAdvance {
|
||||
|
||||
CPU cpu;
|
||||
#include "prefetch.cpp"
|
||||
#include "bus.cpp"
|
||||
#include "io.cpp"
|
||||
#include "memory.cpp"
|
||||
#include "dma.cpp"
|
||||
#include "timer.cpp"
|
||||
#include "keypad.cpp"
|
||||
#include "serialization.cpp"
|
||||
CPU cpu;
|
||||
|
||||
CPU::CPU() {
|
||||
iwram = new uint8[ 32 * 1024];
|
||||
ewram = new uint8[256 * 1024];
|
||||
|
||||
regs.dma[0].source.resize(27); regs.dma[0].run.source.resize(27);
|
||||
regs.dma[0].target.resize(27); regs.dma[0].run.target.resize(27);
|
||||
regs.dma[0].length.resize(14); regs.dma[0].run.length.resize(14);
|
||||
|
||||
regs.dma[1].source.resize(28); regs.dma[1].run.source.resize(28);
|
||||
regs.dma[1].target.resize(27); regs.dma[1].run.target.resize(27);
|
||||
regs.dma[1].length.resize(14); regs.dma[1].run.length.resize(14);
|
||||
|
||||
regs.dma[2].source.resize(28); regs.dma[2].run.source.resize(28);
|
||||
regs.dma[2].target.resize(27); regs.dma[2].run.target.resize(27);
|
||||
regs.dma[2].length.resize(14); regs.dma[2].run.length.resize(14);
|
||||
|
||||
regs.dma[3].source.resize(28); regs.dma[3].run.source.resize(28);
|
||||
regs.dma[3].target.resize(28); regs.dma[3].run.target.resize(28);
|
||||
regs.dma[3].length.resize(16); regs.dma[3].run.length.resize(16);
|
||||
}
|
||||
|
||||
CPU::~CPU() {
|
||||
delete[] iwram;
|
||||
delete[] ewram;
|
||||
}
|
||||
|
||||
auto CPU::Enter() -> void {
|
||||
while(true) scheduler.synchronize(), cpu.main();
|
||||
}
|
||||
|
||||
auto CPU::main() -> void {
|
||||
#if defined(DEBUG)
|
||||
if(crash) {
|
||||
print(cpsr().t ? disassemble_thumb_instruction(pipeline.execute.address)
|
||||
: disassemble_arm_instruction(pipeline.execute.address), "\n");
|
||||
print(disassemble_registers(), "\n");
|
||||
print("Executed: ", instructions, "\n");
|
||||
while(true) step(frequency);
|
||||
}
|
||||
#endif
|
||||
ARM7TDMI::irq = irq.ime && (irq.enable & irq.flag);
|
||||
|
||||
processor.irqline = regs.ime && (regs.irq.enable & regs.irq.flag);
|
||||
|
||||
if(regs.mode == Registers::Mode::Stop) {
|
||||
if(!(regs.irq.enable & regs.irq.flag & Interrupt::Keypad)) {
|
||||
syncStep(16); //STOP does not advance timers
|
||||
} else {
|
||||
regs.mode = Registers::Mode::Normal;
|
||||
if(stopped()) {
|
||||
if(!(irq.enable & irq.flag & Interrupt::Keypad)) {
|
||||
Thread::step(16);
|
||||
synchronize(cpu);
|
||||
synchronize(apu);
|
||||
}
|
||||
return;
|
||||
context.stopped = false;
|
||||
}
|
||||
|
||||
dmaRun();
|
||||
|
||||
if(regs.mode == Registers::Mode::Halt) {
|
||||
if(!(regs.irq.enable & regs.irq.flag)) {
|
||||
step(16);
|
||||
} else {
|
||||
regs.mode = Registers::Mode::Normal;
|
||||
if(halted()) {
|
||||
if(!(irq.enable & irq.flag)) {
|
||||
return step(16);
|
||||
}
|
||||
return;
|
||||
context.halted = false;
|
||||
}
|
||||
|
||||
exec();
|
||||
instruction();
|
||||
}
|
||||
|
||||
auto CPU::step(uint clocks) -> void {
|
||||
timerStep(clocks);
|
||||
syncStep(clocks);
|
||||
}
|
||||
dma[0].waiting = max(0, dma[0].waiting - (int)clocks);
|
||||
dma[1].waiting = max(0, dma[1].waiting - (int)clocks);
|
||||
dma[2].waiting = max(0, dma[2].waiting - (int)clocks);
|
||||
dma[3].waiting = max(0, dma[3].waiting - (int)clocks);
|
||||
|
||||
if(!context.dmaActive) {
|
||||
context.dmaActive = true;
|
||||
while(dma[0].run() | dma[1].run() | dma[2].run() | dma[3].run());
|
||||
context.dmaActive = false;
|
||||
}
|
||||
|
||||
for(auto _ : range(clocks)) {
|
||||
timer[0].run();
|
||||
timer[1].run();
|
||||
timer[2].run();
|
||||
timer[3].run();
|
||||
context.clock++;
|
||||
}
|
||||
|
||||
auto CPU::syncStep(uint clocks) -> void {
|
||||
Thread::step(clocks);
|
||||
synchronize(ppu);
|
||||
synchronize(apu);
|
||||
}
|
||||
|
||||
auto CPU::keypadRun() -> void {
|
||||
//lookup table to convert button indexes to Emulator::Interface indexes
|
||||
static const uint lookup[] = {5, 4, 8, 9, 3, 2, 0, 1, 7, 6};
|
||||
|
||||
if(!regs.keypad.control.enable) return;
|
||||
|
||||
bool test = regs.keypad.control.condition; //0 = OR, 1 = AND
|
||||
for(auto n : range(10)) {
|
||||
if(!regs.keypad.control.flag[n]) continue;
|
||||
bool input = platform->inputPoll(0, 0, lookup[n]);
|
||||
if(regs.keypad.control.condition == 0) test |= input;
|
||||
if(regs.keypad.control.condition == 1) test &= input;
|
||||
}
|
||||
if(test) regs.irq.flag |= Interrupt::Keypad;
|
||||
}
|
||||
|
||||
auto CPU::power() -> void {
|
||||
create(CPU::Enter, 16'777'216);
|
||||
ARM7TDMI::power();
|
||||
create(CPU::Enter, system.frequency());
|
||||
|
||||
ARM::power();
|
||||
for(auto n : range( 32 * 1024)) iwram[n] = 0;
|
||||
for(auto n : range(256 * 1024)) ewram[n] = 0;
|
||||
|
||||
for(auto& dma : regs.dma) {
|
||||
dma.source = 0;
|
||||
dma.target = 0;
|
||||
dma.length = 0;
|
||||
dma.data = 0;
|
||||
dma.control.targetmode = 0;
|
||||
dma.control.sourcemode = 0;
|
||||
dma.control.repeat = 0;
|
||||
dma.control.size = 0;
|
||||
dma.control.drq = 0;
|
||||
dma.control.timingmode = 0;
|
||||
dma.control.irq = 0;
|
||||
dma.control.enable = 0;
|
||||
dma.pending = 0;
|
||||
dma.run.target = 0;
|
||||
dma.run.source = 0;
|
||||
dma.run.length = 0;
|
||||
}
|
||||
for(auto& timer : regs.timer) {
|
||||
timer.period = 0;
|
||||
timer.reload = 0;
|
||||
timer.pending = false;
|
||||
timer.control.frequency = 0;
|
||||
timer.control.cascade = 0;
|
||||
timer.control.irq = 0;
|
||||
timer.control.enable = 0;
|
||||
}
|
||||
regs.serial = {};
|
||||
for(auto& flag : regs.keypad.control.flag) flag = 0;
|
||||
regs.keypad.control.enable = 0;
|
||||
regs.keypad.control.condition = 0;
|
||||
regs.joybus = {};
|
||||
regs.ime = 0;
|
||||
regs.irq.enable = 0;
|
||||
regs.irq.flag = 0;
|
||||
for(auto& nwait : regs.wait.control.nwait) nwait = 0;
|
||||
for(auto& swait : regs.wait.control.swait) swait = 0;
|
||||
regs.wait.control.phi = 0;
|
||||
regs.wait.control.prefetch = 0;
|
||||
regs.wait.control.gametype = 0; //0 = GBA, 1 = GBC
|
||||
regs.memory.control.disable = 0;
|
||||
regs.memory.control.unknown1 = 0;
|
||||
regs.memory.control.ewram = 1;
|
||||
regs.memory.control.ewramwait = 13;
|
||||
regs.memory.control.unknown2 = 0;
|
||||
regs.postboot = 0;
|
||||
regs.mode = Registers::Mode::Normal;
|
||||
regs.clock = 0;
|
||||
for(auto& byte : iwram) byte = 0x00;
|
||||
for(auto& byte : ewram) byte = 0x00;
|
||||
|
||||
for(auto n : range(4)) dma[n] = {n};
|
||||
for(auto n : range(4)) timer[n] = {n};
|
||||
serial = {};
|
||||
keypad = {};
|
||||
joybus = {};
|
||||
irq = {};
|
||||
wait = {};
|
||||
memory = {};
|
||||
prefetch = {};
|
||||
prefetch.wait = 1;
|
||||
context = {};
|
||||
|
||||
pending.dma.vblank = 0;
|
||||
pending.dma.hblank = 0;
|
||||
pending.dma.hdma = 0;
|
||||
dma[0].source.resize(27); dma[0].latch.source.resize(27);
|
||||
dma[0].target.resize(27); dma[0].latch.target.resize(27);
|
||||
dma[0].length.resize(14); dma[0].latch.length.resize(14);
|
||||
|
||||
active.dma = false;
|
||||
dma[1].source.resize(28); dma[1].latch.source.resize(28);
|
||||
dma[1].target.resize(27); dma[1].latch.target.resize(27);
|
||||
dma[1].length.resize(14); dma[1].latch.length.resize(14);
|
||||
|
||||
dma[2].source.resize(28); dma[2].latch.source.resize(28);
|
||||
dma[2].target.resize(27); dma[2].latch.target.resize(27);
|
||||
dma[2].length.resize(14); dma[2].latch.length.resize(14);
|
||||
|
||||
dma[3].source.resize(28); dma[3].latch.source.resize(28);
|
||||
dma[3].target.resize(28); dma[3].latch.target.resize(28);
|
||||
dma[3].length.resize(16); dma[3].latch.length.resize(16);
|
||||
|
||||
for(uint n = 0x0b0; n <= 0x0df; n++) bus.io[n] = this; //DMA
|
||||
for(uint n = 0x100; n <= 0x10f; n++) bus.io[n] = this; //Timers
|
||||
@@ -176,7 +104,7 @@ auto CPU::power() -> void {
|
||||
for(uint n = 0x134; n <= 0x159; n++) bus.io[n] = this; //Serial
|
||||
for(uint n = 0x200; n <= 0x209; n++) bus.io[n] = this; //System
|
||||
for(uint n = 0x300; n <= 0x301; n++) bus.io[n] = this; //System
|
||||
//0x080-0x083 mirrored via gba/memory/memory.cpp //System
|
||||
//0x080-0x083 mirrored via gba/memory/memory.cpp //System
|
||||
}
|
||||
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user