mirror of
https://github.com/D4C1-Labs/Flipper-ARF.git
synced 2026-07-03 19:11:36 +00:00
Compare commits
1 Commits
dev-492cee43
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d598b854c |
@@ -0,0 +1,202 @@
|
||||
# FlipGB — Game Boy Emulator for Flipper Zero
|
||||
|
||||
A Game Boy (DMG) emulator for the Flipper Zero, based on the
|
||||
[jgilchrist/gbemu](https://github.com/jgilchrist/gbemu) core, heavily adapted
|
||||
for microcontrollers. Loads `.gb` ROMs from the SD card, streams ROM banks,
|
||||
supports battery saves and adaptive frameskip.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Copy `dist/flipgb.fap` to your SD card under `SD/apps/Games/`
|
||||
(or run `ufbt launch` with the Flipper connected via USB).
|
||||
2. Copy your `.gb` ROM files anywhere on the SD card (e.g. `SD/gb_roms/`).
|
||||
3. On the Flipper: **Apps → Games → FlipGB**.
|
||||
4. Pick a ROM with the file browser. The game starts immediately.
|
||||
5. To open the emulator menu at any time: **press Up + Down together**.
|
||||
|
||||
> Only use ROMs you legally own or free homebrew (e.g. µCity, Tobu Tobu Girl).
|
||||
|
||||
---
|
||||
|
||||
## Controls
|
||||
|
||||
### In game
|
||||
|
||||
| Flipper button | Game Boy button |
|
||||
|---|---|
|
||||
| **Up / Down / Left / Right** | D-pad |
|
||||
| **OK (center)** | **A** |
|
||||
| **Back** | **B** |
|
||||
| **Up + Down pressed together** | Open the emulator menu |
|
||||
|
||||
There is no direct Start/Select button on the Flipper — they are sent from
|
||||
the emulator menu (see below).
|
||||
|
||||
**Why Up+Down for the menu?** A real Game Boy d-pad physically cannot press
|
||||
opposite directions at the same time, so no game ever reads that combination
|
||||
— it can never conflict with gameplay. (A long-press on Back was rejected
|
||||
because Back is B, and many games hold B continuously — e.g. running in
|
||||
platformers — which would keep popping the menu open.)
|
||||
|
||||
### Emulator menu (Up + Down)
|
||||
|
||||
Navigate with **Up/Down**, activate with **OK**, close with **Back**.
|
||||
|
||||
| Item | What it does |
|
||||
|---|---|
|
||||
| **Continue** | Close the menu and resume the game |
|
||||
| **Press START** | Sends a Start press to the game (pause menus, "PRESS START" screens) and resumes |
|
||||
| **Press SELECT** | Sends a Select press to the game and resumes |
|
||||
| **Frameskip** | Change with **Left/Right**: `auto` (recommended), or fixed `0–4`. `auto` shows the skip level currently in use |
|
||||
| **Sound** | Toggle piezo sound on/off (`n/a` if the speaker is in use by another app) |
|
||||
| **Save SRAM** | Writes the cartridge battery save (`.sav`) to the SD card immediately |
|
||||
| **Exit** | Saves SRAM (if the cartridge has a battery) and quits the app |
|
||||
|
||||
The game is paused while the menu is open; all buttons are released for the
|
||||
game so nothing stays "stuck".
|
||||
|
||||
---
|
||||
|
||||
## Saves
|
||||
|
||||
- Games with battery-backed cartridge RAM (Zelda, Pokémon, etc.) are saved to
|
||||
a file **next to the ROM**: `MyGame.gb` → `MyGame.gb.sav`.
|
||||
- Saving happens **automatically on Exit**, and manually via **Save SRAM** in
|
||||
the menu (recommended before pulling the battery/USB, since a hard power
|
||||
loss cannot auto-save).
|
||||
- Save states are **not** supported — only real in-game saving, like original
|
||||
hardware.
|
||||
|
||||
---
|
||||
|
||||
## Display
|
||||
|
||||
The Game Boy screen (160×144, 4 shades of gray) is downscaled to the
|
||||
Flipper's 128×64 1-bit LCD:
|
||||
|
||||
- Full screen is always visible (no cropping), slightly squashed vertically.
|
||||
- The 4 shades become ordered-dither patterns: white → lit, light gray → 3/4
|
||||
lit, dark gray → 1/4 lit, black → off.
|
||||
|
||||
## Sound
|
||||
|
||||
The Flipper's speaker is a single-tone piezo — it plays exactly one
|
||||
frequency at one volume at a time, so the real 4-channel Game Boy mix
|
||||
cannot be reproduced. Instead, FlipGB emulates the APU **at register level**
|
||||
(frequencies, CH1 frequency sweep, volume envelopes, length counters,
|
||||
NR52 power/status) and every frame sends the **dominant voice** to the
|
||||
piezo:
|
||||
|
||||
1. the louder of the two pulse channels (they carry the melody in almost
|
||||
every GB soundtrack); the most recently triggered wins ties,
|
||||
2. otherwise the wave channel (bass lines),
|
||||
3. otherwise the noise channel, mapped to a short low buzz (percussion).
|
||||
|
||||
The result is a monophonic ringtone-style rendition of the game's music
|
||||
and sound effects (sweeps like Mario's jump work). It can be toggled in
|
||||
the emulator menu. Since no waveforms are synthesized, the CPU cost is
|
||||
negligible (one counter per emulated instruction) and RAM cost is ~120
|
||||
bytes.
|
||||
|
||||
## Performance
|
||||
|
||||
- The upstream core had a cycle-domain mismatch: the CPU tables count
|
||||
machine cycles but the PPU counted them against T-cycle constants, so
|
||||
**every frame emulated 4x the hardware-correct amount of CPU work**
|
||||
(70224 M-cycles instead of 17556). Unnoticeable on a desktop, a slideshow
|
||||
on a 64 MHz Cortex-M4. Fixed — this alone made everything ~4.5x faster.
|
||||
- While halted (games spend most of each frame in HALT waiting for vblank),
|
||||
the CPU steps 4 M-cycles at a time instead of 1, making the idle part of
|
||||
the frame cheap.
|
||||
- **Frameskip `auto`** (default) measures the real cost of each emulated frame
|
||||
and skips *rendering* (never emulation) to keep the game running at correct
|
||||
speed. Games stay full-speed logically; visible FPS drops instead.
|
||||
- Fixed frameskip `0–4` is available in the menu if you prefer consistency.
|
||||
- The PPU only renders the 64 scanlines (out of 144) that survive the
|
||||
downscale to the Flipper LCD — ~55% of the per-frame rendering work is
|
||||
skipped with zero visual difference.
|
||||
- Bank-switch heavy games may micro-stutter when a 16 KB bank has to be
|
||||
streamed from the SD card (only happens when the ROM doesn't fit in RAM).
|
||||
- The emulator menu shows the free heap (`NNk free`) so you can see the
|
||||
memory headroom of the current game at a glance.
|
||||
|
||||
## Compatibility
|
||||
|
||||
| Feature | Status |
|
||||
|---|---|
|
||||
| Mappers | ROM-only, MBC1, MBC2, MBC3 (no RTC), MBC5 |
|
||||
| Game Boy Color | Not supported. CGB-only ROMs are rejected with a message; dual-mode ROMs run in DMG mode |
|
||||
| MBC3 real-time clock | Not emulated (Pokémon Gold/Silver run without the clock) |
|
||||
| Audio | Monophonic: register-level APU + dominant voice on the piezo (see *Sound* above). No waveform mixing — the hardware physically can't play it |
|
||||
| Link cable | Not supported |
|
||||
| ROM size | Any (streamed from SD; small ROMs are fully loaded to RAM) |
|
||||
|
||||
---
|
||||
|
||||
## Building from source
|
||||
|
||||
```sh
|
||||
pip install ufbt
|
||||
cd FlipperGB
|
||||
ufbt # produces dist/flipgb.fap
|
||||
ufbt launch # builds, installs and runs on a connected Flipper
|
||||
```
|
||||
|
||||
### Verifying the emulator core on your PC
|
||||
|
||||
The exact core that ships in the FAP can be compiled and tested on a desktop:
|
||||
|
||||
```sh
|
||||
g++ -std=c++17 -O2 -fno-exceptions -fno-rtti -I gb \
|
||||
-o hosttest/hosttest hosttest/main.cpp gb/*.cc
|
||||
|
||||
# Blargg CPU tests (print Passed/Failed via the serial port):
|
||||
./hosttest/hosttest path/to/01-special.gb 4000
|
||||
|
||||
# ASCII dump of a game frame:
|
||||
./hosttest/hosttest game.gb 600 --dump-frame
|
||||
|
||||
# Trace of what the piezo would play (dominant APU voice per frame):
|
||||
./hosttest/hosttest game.gb 1500 --dump-audio
|
||||
```
|
||||
|
||||
Current status: **Blargg `cpu_instrs` 11/11 PASS**.
|
||||
|
||||
---
|
||||
|
||||
## Technical notes (PC core → MCU adaptations)
|
||||
|
||||
| Upstream (PC) | This port (Flipper) |
|
||||
|---|---|
|
||||
| Whole ROM in RAM (with several transient copies) | 16 KB bank streaming from SD with an adaptive LRU cache; bank 0 resident; when every bank fits, the whole ROM is preloaded into individual 16 KB slots (O(1) switching, SD file closed) |
|
||||
| — | All ROM-dependent allocations are 16 KB or smaller and are checked against the largest free heap block first: heap fragmentation can never crash the firmware, the app degrades to streaming or shows "Not enough RAM" instead |
|
||||
| Renders all 144 scanlines | Renders only the 64 scanlines that are actually displayed after the 144→64 downscale (row mask, ~2x faster rendering) |
|
||||
| PPU counts M-cycles against T-cycle constants (4x too much CPU emulation per frame) | Hardware-correct M-cycle constants (114 per scanline): ~4.5x faster overall |
|
||||
| DIV register incremented every M-cycle (64x too fast) | Correct 16384 Hz rate (games use DIV for delays and randomness) |
|
||||
| Framebuffer stores all 144 rows | Flipper build stores only the 64 displayed rows (2.5 KB instead of 5.7 KB) |
|
||||
| 92 KB framebuffer of 4-byte enums + unused 256 KB background map | Packed 2bpp framebuffer (5.7 KB); dead buffer removed |
|
||||
| 32 KB WRAM / 16 KB VRAM (CGB provision) | Real DMG sizes: 8 KB / 8 KB |
|
||||
| Virtual methods on every CPU register access | Devirtualized, inlined registers |
|
||||
| Heap allocation per tile per scanline in the PPU | Zero-alloc per-tile rendering |
|
||||
| `std::function` / `std::string` / `ifstream` / exceptions | Function pointers + Flipper Storage API, builds with `-fno-exceptions -fno-rtti` |
|
||||
| Nintendo boot ROM embedded | Removed; documented post-boot register state instead |
|
||||
| MBC1 partial (bugs, 512 KB max), no MBC2/MBC5 | MBC1 complete, MBC2/MBC3/MBC5 implemented |
|
||||
| No joypad interrupt | Added (wakes games waiting in HALT/STOP) |
|
||||
| No APU at all | Register-level APU (sweep/envelope/length/NR52, proper read-back masks) driving the piezo with the dominant voice |
|
||||
|
||||
RAM budget on device (256 KB total, ~140 KB heap; the app binary itself
|
||||
loads into ~32 KB of that heap): ~19.5 KB emulation state, 16 KB bank 0,
|
||||
adaptive bank cache (10 KB heap kept in reserve for the system), 0–32 KB
|
||||
cartridge RAM per game, 4 KB stack. The bank cache is allocated greedily in
|
||||
independent 16 KB blocks until the reserve would be touched, so any `.gb`
|
||||
ROM size works: small ROMs end up fully resident, large ones stream through
|
||||
however many slots fit. Worst case (1 MB ROM + 32 KB battery RAM, e.g.
|
||||
Pokémon Red/Blue) needs ~78 KB before the first cache slot, which fits the
|
||||
post-launch heap with room for 1–2 streaming slots.
|
||||
|
||||
## License
|
||||
|
||||
The emulator core derives from jgilchrist/gbemu — see its upstream license.
|
||||
No Nintendo code or assets are included in this repository.
|
||||
@@ -0,0 +1,15 @@
|
||||
App(
|
||||
appid="flipgb",
|
||||
name="FlipGB",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="flipgb_app",
|
||||
sources=["*.c*", "!hosttest"],
|
||||
requires=["gui", "dialogs", "storage"],
|
||||
stack_size=8 * 1024,
|
||||
cdefines=[("GB_FB_ROWS", "64")],
|
||||
fap_category="Games",
|
||||
fap_icon="flipgb_icon.png",
|
||||
fap_author="user",
|
||||
fap_version="1.0",
|
||||
fap_description="Game Boy (DMG) emulator. Loads .gb ROMs from the SD card, streams ROM banks, battery saves, adaptive frameskip.",
|
||||
)
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 96 B |
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "definitions.h"
|
||||
#include "register.h"
|
||||
|
||||
class Address {
|
||||
public:
|
||||
Address(u16 location) : addr(location) {}
|
||||
explicit Address(const RegisterPair& from) : addr(from.value()) {}
|
||||
explicit Address(const WordRegister& from) : addr(from.value()) {}
|
||||
|
||||
auto value() const -> u16 { return addr; }
|
||||
|
||||
auto in_range(Address low, Address high) const -> bool {
|
||||
return low.value() <= value() && value() <= high.value();
|
||||
}
|
||||
|
||||
auto operator==(u16 other) const -> bool { return addr == other; }
|
||||
auto operator+(uint other) const -> Address {
|
||||
return Address(static_cast<u16>(addr + other));
|
||||
}
|
||||
auto operator-(uint other) const -> Address {
|
||||
return Address(static_cast<u16>(addr - other));
|
||||
}
|
||||
|
||||
private:
|
||||
u16 addr = 0x0;
|
||||
};
|
||||
@@ -0,0 +1,242 @@
|
||||
#include "apu.h"
|
||||
|
||||
/* One frame-sequencer step every 2048 M-cycles (8192 T-cycles = 512 Hz),
|
||||
* in the same M-cycle domain the CPU/PPU/timer now share. */
|
||||
static const uint FRAME_SEQ_PERIOD = 2048;
|
||||
|
||||
void Apu::tick(uint cycles) {
|
||||
if(!power) return;
|
||||
|
||||
seq_counter += cycles;
|
||||
while(seq_counter >= FRAME_SEQ_PERIOD) {
|
||||
seq_counter -= FRAME_SEQ_PERIOD;
|
||||
seq_step = (u8)((seq_step + 1) & 7);
|
||||
|
||||
if((seq_step & 1) == 0) clock_lengths(); /* 256 Hz */
|
||||
if(seq_step == 2 || seq_step == 6) clock_sweep(); /* 128 Hz */
|
||||
if(seq_step == 7) clock_envelopes(); /* 64 Hz */
|
||||
}
|
||||
}
|
||||
|
||||
void Apu::clock_lengths() {
|
||||
for(uint n = 0; n < 4; n++) {
|
||||
Ch& c = ch[n];
|
||||
if((c.nr4 & 0x40) && c.length > 0) {
|
||||
c.length--;
|
||||
if(c.length == 0) c.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Apu::clock_envelopes() {
|
||||
static const u8 env_channels[3] = {0, 1, 3}; /* wave has no envelope */
|
||||
for(uint i = 0; i < 3; i++) {
|
||||
Ch& c = ch[env_channels[i]];
|
||||
u8 period = c.nr2 & 0x07;
|
||||
if(!period || !c.enabled) continue;
|
||||
if(c.env_timer > 0) c.env_timer--;
|
||||
if(c.env_timer == 0) {
|
||||
c.env_timer = period;
|
||||
if(c.nr2 & 0x08) {
|
||||
if(c.env_volume < 15) c.env_volume++;
|
||||
} else {
|
||||
if(c.env_volume > 0) c.env_volume--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Apu::sweep_calc() const -> uint {
|
||||
uint delta = sweep_shadow >> (ch[0].nr0 & 0x07);
|
||||
return (ch[0].nr0 & 0x08) ? sweep_shadow - delta : sweep_shadow + delta;
|
||||
}
|
||||
|
||||
void Apu::clock_sweep() {
|
||||
Ch& c = ch[0];
|
||||
if(!c.enabled || !sweep_enabled) return;
|
||||
|
||||
if(sweep_timer > 0) sweep_timer--;
|
||||
if(sweep_timer != 0) return;
|
||||
|
||||
u8 period = (c.nr0 >> 4) & 0x07;
|
||||
sweep_timer = period ? period : 8;
|
||||
if(!period) return;
|
||||
|
||||
uint nf = sweep_calc();
|
||||
if(nf > 2047) {
|
||||
c.enabled = false;
|
||||
} else if(c.nr0 & 0x07) {
|
||||
sweep_shadow = nf;
|
||||
set_ch_freq(0, nf);
|
||||
if(sweep_calc() > 2047) c.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Apu::trigger(uint n) {
|
||||
Ch& c = ch[n];
|
||||
|
||||
c.enabled = dac_on(n);
|
||||
if(c.length == 0) c.length = (n == 2) ? 256 : 64;
|
||||
c.env_volume = c.nr2 >> 4;
|
||||
u8 period = c.nr2 & 0x07;
|
||||
c.env_timer = period ? period : 8;
|
||||
c.order = ++trigger_counter;
|
||||
|
||||
if(n == 0) {
|
||||
sweep_shadow = ch_freq(0);
|
||||
u8 sw_period = (c.nr0 >> 4) & 0x07;
|
||||
u8 sw_shift = c.nr0 & 0x07;
|
||||
sweep_timer = sw_period ? sw_period : 8;
|
||||
sweep_enabled = (sw_period != 0) || (sw_shift != 0);
|
||||
if(sw_shift && sweep_calc() > 2047) c.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Apu::power_off() {
|
||||
for(uint n = 0; n < 4; n++) {
|
||||
ch[n] = Ch(); /* wave RAM survives power-off, registers do not */
|
||||
}
|
||||
nr50 = 0;
|
||||
nr51 = 0;
|
||||
sweep_shadow = 0;
|
||||
sweep_timer = 0;
|
||||
sweep_enabled = false;
|
||||
seq_counter = 0;
|
||||
seq_step = 0;
|
||||
}
|
||||
|
||||
void Apu::write(u16 addr, u8 value) {
|
||||
/* wave RAM is accessible regardless of power */
|
||||
if(addr >= 0xFF30 && addr <= 0xFF3F) {
|
||||
wave_ram[addr - 0xFF30] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if(addr == 0xFF26) { /* NR52: only the power bit is writable */
|
||||
bool new_power = (value & 0x80) != 0;
|
||||
if(power && !new_power) power_off();
|
||||
if(!power && new_power) {
|
||||
seq_counter = 0;
|
||||
seq_step = 0;
|
||||
}
|
||||
power = new_power;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!power) return; /* all other registers are dead while powered off */
|
||||
|
||||
if(addr >= 0xFF10 && addr <= 0xFF23) {
|
||||
uint idx = addr - 0xFF10;
|
||||
uint n = idx / 5; /* channel */
|
||||
Ch& c = ch[n];
|
||||
switch(idx % 5) {
|
||||
case 0: /* NRx0: CH1 sweep / CH3 DAC enable */
|
||||
c.nr0 = value;
|
||||
if(n == 2 && !dac_on(2)) c.enabled = false;
|
||||
break;
|
||||
case 1: /* NRx1: duty/length load */
|
||||
c.nr1 = value;
|
||||
c.length = (n == 2) ? 256u - value : 64u - (value & 0x3F);
|
||||
break;
|
||||
case 2: /* NRx2: envelope (CH3: output level) */
|
||||
c.nr2 = value;
|
||||
if(n != 2 && !dac_on(n)) c.enabled = false;
|
||||
break;
|
||||
case 3: /* NRx3: frequency low (CH4: polynomial counter) */
|
||||
c.nr3 = value;
|
||||
break;
|
||||
case 4: /* NRx4: frequency high / length enable / trigger */
|
||||
c.nr4 = value;
|
||||
if(value & 0x80) trigger(n);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(addr == 0xFF24) {
|
||||
nr50 = value;
|
||||
return;
|
||||
}
|
||||
if(addr == 0xFF25) {
|
||||
nr51 = value;
|
||||
return;
|
||||
}
|
||||
/* 0xFF27 - 0xFF2F: unmapped */
|
||||
}
|
||||
|
||||
auto Apu::read(u16 addr) const -> u8 {
|
||||
if(addr >= 0xFF30 && addr <= 0xFF3F) return wave_ram[addr - 0xFF30];
|
||||
|
||||
/* unused bits read back as 1 (hardware OR masks) */
|
||||
static const u8 masks[0x17] = {
|
||||
0x80, 0x3F, 0x00, 0xFF, 0xBF, /* NR10-NR14 */
|
||||
0xFF, 0x3F, 0x00, 0xFF, 0xBF, /* ----, NR21-NR24 */
|
||||
0x7F, 0xFF, 0x9F, 0xFF, 0xBF, /* NR30-NR34 */
|
||||
0xFF, 0xFF, 0x00, 0x00, 0xBF, /* ----, NR41-NR44 */
|
||||
0x00, 0x00, 0x70, /* NR50, NR51, NR52 */
|
||||
};
|
||||
|
||||
if(addr >= 0xFF10 && addr <= 0xFF23) {
|
||||
uint idx = addr - 0xFF10;
|
||||
const Ch& c = ch[idx / 5];
|
||||
u8 raw;
|
||||
switch(idx % 5) {
|
||||
case 0: raw = c.nr0; break;
|
||||
case 1: raw = c.nr1; break;
|
||||
case 2: raw = c.nr2; break;
|
||||
case 3: raw = c.nr3; break;
|
||||
default: raw = c.nr4; break;
|
||||
}
|
||||
return raw | masks[idx];
|
||||
}
|
||||
|
||||
switch(addr) {
|
||||
case 0xFF24:
|
||||
return nr50;
|
||||
case 0xFF25:
|
||||
return nr51;
|
||||
case 0xFF26: {
|
||||
u8 v = (u8)(power ? 0x80 : 0x00) | 0x70;
|
||||
for(uint n = 0; n < 4; n++)
|
||||
if(ch[n].enabled) v |= (u8)(1 << n);
|
||||
return v;
|
||||
}
|
||||
default:
|
||||
return 0xFF; /* 0xFF27 - 0xFF2F */
|
||||
}
|
||||
}
|
||||
|
||||
void Apu::get_voice(uint n, ApuVoice* out) const {
|
||||
const Ch& c = ch[n];
|
||||
|
||||
out->order = c.order;
|
||||
|
||||
bool routed = ((nr51 >> n) & 1) || ((nr51 >> (n + 4)) & 1);
|
||||
|
||||
u8 vol;
|
||||
if(n == 2) {
|
||||
/* wave output level: mute / 100% / 50% / 25% */
|
||||
switch((c.nr2 >> 5) & 3) {
|
||||
case 0: vol = 0; break;
|
||||
case 1: vol = 15; break;
|
||||
case 2: vol = 7; break;
|
||||
default: vol = 3; break;
|
||||
}
|
||||
} else {
|
||||
vol = c.env_volume;
|
||||
}
|
||||
out->volume = vol;
|
||||
out->active = power && c.enabled && dac_on(n) && routed && vol > 0;
|
||||
|
||||
if(n == 3) {
|
||||
/* noise: LFSR clock rate (the frontend maps it to a percussive
|
||||
* buzz; a piezo cannot reproduce real noise) */
|
||||
u8 shift = c.nr3 >> 4;
|
||||
u8 r = c.nr3 & 0x07;
|
||||
u32 divisor = r ? ((u32)r << 4) : 8u;
|
||||
out->freq_hz = (524288u / divisor) >> (shift + 1);
|
||||
} else {
|
||||
uint x = ch_freq(n);
|
||||
out->freq_hz = ((n == 2) ? 65536u : 131072u) / (2048u - x);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include "definitions.h"
|
||||
|
||||
/* Register-level APU (no waveform synthesis).
|
||||
*
|
||||
* The Flipper Zero speaker is a single-tone piezo: it can play exactly one
|
||||
* frequency at one volume at a time. Synthesizing the real 4-channel GB mix
|
||||
* would be wasted CPU (there is no DAC to play it on), so this APU only
|
||||
* models what games actually program into the sound registers:
|
||||
*
|
||||
* - channel frequencies (including the CH1 frequency sweep)
|
||||
* - volume envelopes (64 Hz), length counters (256 Hz), sweep (128 Hz)
|
||||
* - trigger / DAC-enable / NR52 power semantics and register read-back
|
||||
* masks (some games poll NR52 channel-status bits)
|
||||
*
|
||||
* The frontend queries the per-channel state once per frame and decides
|
||||
* which voice to send to the piezo. Cost per emulated instruction: one
|
||||
* counter add + compare. Extra RAM: ~120 bytes.
|
||||
*/
|
||||
|
||||
struct ApuVoice {
|
||||
bool active; /* audible now: triggered, DAC on, length alive, routed */
|
||||
u32 freq_hz; /* square/wave: tone frequency. noise: LFSR clock rate */
|
||||
u8 volume; /* current volume 0..15 (wave level mapped to 0/3/7/15) */
|
||||
u32 order; /* trigger recency; higher = more recently triggered */
|
||||
};
|
||||
|
||||
class Apu {
|
||||
public:
|
||||
void tick(uint cycles);
|
||||
|
||||
/* 0xFF10 - 0xFF3F (sound registers + wave RAM) */
|
||||
auto read(u16 addr) const -> u8;
|
||||
void write(u16 addr, u8 value);
|
||||
|
||||
/* n: 0 = pulse 1, 1 = pulse 2, 2 = wave, 3 = noise */
|
||||
void get_voice(uint n, ApuVoice* out) const;
|
||||
|
||||
/* Master volume 0..7 (louder of the two NR50 output terminals) */
|
||||
auto master_volume() const -> u8 {
|
||||
u8 l = (nr50 >> 4) & 7;
|
||||
u8 r = nr50 & 7;
|
||||
return l > r ? l : r;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Ch {
|
||||
u8 nr0 = 0, nr1 = 0, nr2 = 0, nr3 = 0, nr4 = 0;
|
||||
bool enabled = false;
|
||||
uint length = 0;
|
||||
u8 env_volume = 0;
|
||||
u8 env_timer = 0;
|
||||
u32 order = 0;
|
||||
};
|
||||
Ch ch[4]; /* 0 = pulse1, 1 = pulse2, 2 = wave, 3 = noise */
|
||||
|
||||
/* channel 1 sweep unit */
|
||||
uint sweep_shadow = 0;
|
||||
u8 sweep_timer = 0;
|
||||
bool sweep_enabled = false;
|
||||
|
||||
u8 nr50 = 0, nr51 = 0;
|
||||
bool power = false;
|
||||
u8 wave_ram[16] = {};
|
||||
|
||||
uint seq_counter = 0;
|
||||
u8 seq_step = 0;
|
||||
u32 trigger_counter = 0;
|
||||
|
||||
auto dac_on(uint n) const -> bool {
|
||||
if(n == 2) return (ch[2].nr0 & 0x80) != 0;
|
||||
return (ch[n].nr2 & 0xF8) != 0;
|
||||
}
|
||||
auto ch_freq(uint n) const -> uint {
|
||||
return ((uint)(ch[n].nr4 & 0x07) << 8) | ch[n].nr3;
|
||||
}
|
||||
void set_ch_freq(uint n, uint f) {
|
||||
ch[n].nr3 = (u8)(f & 0xFF);
|
||||
ch[n].nr4 = (u8)((ch[n].nr4 & ~0x07) | ((f >> 8) & 0x07));
|
||||
}
|
||||
|
||||
void trigger(uint n);
|
||||
void clock_lengths();
|
||||
void clock_envelopes();
|
||||
void clock_sweep();
|
||||
auto sweep_calc() const -> uint;
|
||||
void power_off();
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "definitions.h"
|
||||
|
||||
namespace bitwise {
|
||||
|
||||
inline auto compose_bits(const u8 high, const u8 low) -> u8 {
|
||||
return static_cast<u8>(high << 1 | low);
|
||||
}
|
||||
|
||||
inline auto compose_nibbles(const u8 high, const u8 low) -> u8 {
|
||||
return static_cast<u8>((high << 4) | low);
|
||||
}
|
||||
|
||||
inline auto compose_bytes(const u8 high, const u8 low) -> u16 {
|
||||
return static_cast<u16>((high << 8) | low);
|
||||
}
|
||||
|
||||
inline auto check_bit(const u8 value, const u8 bit) -> bool { return (value & (1 << bit)) != 0; }
|
||||
|
||||
inline auto bit_value(const u8 value, const u8 bit) -> u8 { return (value >> bit) & 1; }
|
||||
|
||||
inline auto set_bit(const u8 value, const u8 bit) -> u8 {
|
||||
auto value_set = value | (1 << bit);
|
||||
return static_cast<u8>(value_set);
|
||||
}
|
||||
|
||||
inline auto clear_bit(const u8 value, const u8 bit) -> u8 {
|
||||
auto value_cleared = value & ~(1 << bit);
|
||||
return static_cast<u8>(value_cleared);
|
||||
}
|
||||
|
||||
inline auto set_bit_to(const u8 value, const u8 bit, bool bit_on) -> u8 {
|
||||
return bit_on
|
||||
? set_bit(value, bit)
|
||||
: clear_bit(value, bit);
|
||||
}
|
||||
|
||||
} // namespace bitwise
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "cartridge.h"
|
||||
|
||||
void Cartridge::init(
|
||||
const u8* bank0_data,
|
||||
uint rom_bank_count,
|
||||
MBCType mbc_type,
|
||||
u8* cart_ram,
|
||||
u32 cart_ram_size,
|
||||
RomBankProvider rom_provider,
|
||||
void* rom_provider_ctx) {
|
||||
bank0 = bank0_data;
|
||||
bank_count = rom_bank_count > 0 ? rom_bank_count : 2;
|
||||
mbc = mbc_type;
|
||||
ram = cart_ram;
|
||||
ram_size = cart_ram_size;
|
||||
provider = rom_provider;
|
||||
provider_ctx = rom_provider_ctx;
|
||||
|
||||
ram_enabled = false;
|
||||
advanced_banking_mode = false;
|
||||
bank_low = 1;
|
||||
bank_high = 0;
|
||||
ram_bank = 0;
|
||||
|
||||
update_rom_bank();
|
||||
}
|
||||
|
||||
void Cartridge::update_rom_bank() {
|
||||
uint bank;
|
||||
|
||||
switch(mbc) {
|
||||
case MBCType::None:
|
||||
bank = 1;
|
||||
break;
|
||||
case MBCType::MBC1:
|
||||
bank = (bank_high << 5) | bank_low;
|
||||
/* bank_low == 0 is translated to 1 at write time */
|
||||
break;
|
||||
case MBCType::MBC2:
|
||||
case MBCType::MBC3:
|
||||
bank = bank_low;
|
||||
break;
|
||||
case MBCType::MBC5:
|
||||
/* MBC5 genuinely allows bank 0 in the switchable slot */
|
||||
bank = (bank_high << 8) | bank_low;
|
||||
break;
|
||||
default:
|
||||
bank = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if(bank_count) bank %= bank_count;
|
||||
|
||||
bankN = provider(provider_ctx, bank);
|
||||
}
|
||||
|
||||
void Cartridge::write(u16 addr, u8 value) {
|
||||
if(addr >= 0xA000) {
|
||||
write_ram(addr, value);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(mbc) {
|
||||
case MBCType::None:
|
||||
return;
|
||||
|
||||
case MBCType::MBC1:
|
||||
if(addr < 0x2000) {
|
||||
ram_enabled = (value & 0x0F) == 0x0A;
|
||||
} else if(addr < 0x4000) {
|
||||
bank_low = value & 0x1F;
|
||||
if(bank_low == 0) bank_low = 1;
|
||||
update_rom_bank();
|
||||
} else if(addr < 0x6000) {
|
||||
bank_high = value & 0x03;
|
||||
if(advanced_banking_mode) {
|
||||
ram_bank = value & 0x03;
|
||||
}
|
||||
update_rom_bank();
|
||||
} else {
|
||||
advanced_banking_mode = (value & 0x01) != 0;
|
||||
ram_bank = advanced_banking_mode ? (bank_high & 0x03) : 0;
|
||||
update_rom_bank();
|
||||
}
|
||||
return;
|
||||
|
||||
case MBCType::MBC2:
|
||||
if(addr < 0x4000) {
|
||||
/* bit 8 of the address selects RAM-enable vs ROM-bank */
|
||||
if(addr & 0x0100) {
|
||||
bank_low = value & 0x0F;
|
||||
if(bank_low == 0) bank_low = 1;
|
||||
update_rom_bank();
|
||||
} else {
|
||||
ram_enabled = (value & 0x0F) == 0x0A;
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
case MBCType::MBC3:
|
||||
if(addr < 0x2000) {
|
||||
ram_enabled = (value & 0x0F) == 0x0A;
|
||||
} else if(addr < 0x4000) {
|
||||
bank_low = value & 0x7F;
|
||||
if(bank_low == 0) bank_low = 1;
|
||||
update_rom_bank();
|
||||
} else if(addr < 0x6000) {
|
||||
/* 0x00-0x03: RAM bank. 0x08-0x0C: RTC register (unsupported,
|
||||
* reads return 0xFF via ram_bank marker) */
|
||||
ram_bank = value;
|
||||
} else {
|
||||
/* RTC latch: unsupported */
|
||||
}
|
||||
return;
|
||||
|
||||
case MBCType::MBC5:
|
||||
if(addr < 0x2000) {
|
||||
ram_enabled = (value & 0x0F) == 0x0A;
|
||||
} else if(addr < 0x3000) {
|
||||
bank_low = value;
|
||||
update_rom_bank();
|
||||
} else if(addr < 0x4000) {
|
||||
bank_high = value & 0x01;
|
||||
update_rom_bank();
|
||||
} else if(addr < 0x6000) {
|
||||
ram_bank = value & 0x0F;
|
||||
}
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::read_ram(u16 addr) const -> u8 {
|
||||
if(!ram || !ram_enabled) return 0xFF;
|
||||
|
||||
if(mbc == MBCType::MBC2) {
|
||||
/* 512 half-bytes, mirrored */
|
||||
return static_cast<u8>(ram[(addr - 0xA000) & 0x1FF] | 0xF0);
|
||||
}
|
||||
|
||||
if(mbc == MBCType::MBC3 && ram_bank > 0x03) return 0xFF; /* RTC regs */
|
||||
|
||||
u32 idx = static_cast<u32>(addr - 0xA000) + static_cast<u32>(ram_bank & 0x0F) * 0x2000;
|
||||
if(idx >= ram_size) idx %= ram_size;
|
||||
return ram[idx];
|
||||
}
|
||||
|
||||
void Cartridge::write_ram(u16 addr, u8 value) {
|
||||
if(!ram || !ram_enabled) return;
|
||||
|
||||
if(mbc == MBCType::MBC2) {
|
||||
ram[(addr - 0xA000) & 0x1FF] = value & 0x0F;
|
||||
return;
|
||||
}
|
||||
|
||||
if(mbc == MBCType::MBC3 && ram_bank > 0x03) return; /* RTC regs */
|
||||
|
||||
u32 idx = static_cast<u32>(addr - 0xA000) + static_cast<u32>(ram_bank & 0x0F) * 0x2000;
|
||||
if(idx >= ram_size) idx %= ram_size;
|
||||
ram[idx] = value;
|
||||
}
|
||||
|
||||
auto Cartridge::parse_mbc(u8 t) -> MBCType {
|
||||
switch(t) {
|
||||
case 0x00:
|
||||
case 0x08:
|
||||
case 0x09:
|
||||
return MBCType::None;
|
||||
case 0x01:
|
||||
case 0x02:
|
||||
case 0x03:
|
||||
return MBCType::MBC1;
|
||||
case 0x05:
|
||||
case 0x06:
|
||||
return MBCType::MBC2;
|
||||
case 0x0F:
|
||||
case 0x10:
|
||||
case 0x11:
|
||||
case 0x12:
|
||||
case 0x13:
|
||||
return MBCType::MBC3;
|
||||
case 0x19:
|
||||
case 0x1A:
|
||||
case 0x1B:
|
||||
case 0x1C:
|
||||
case 0x1D:
|
||||
case 0x1E:
|
||||
return MBCType::MBC5;
|
||||
default:
|
||||
return MBCType::Unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::has_battery(u8 t) -> bool {
|
||||
switch(t) {
|
||||
case 0x03: /* MBC1+RAM+BATTERY */
|
||||
case 0x06: /* MBC2+BATTERY */
|
||||
case 0x09: /* ROM+RAM+BATTERY */
|
||||
case 0x0F: /* MBC3+TIMER+BATTERY */
|
||||
case 0x10: /* MBC3+TIMER+RAM+BATTERY */
|
||||
case 0x13: /* MBC3+RAM+BATTERY */
|
||||
case 0x1B: /* MBC5+RAM+BATTERY */
|
||||
case 0x1E: /* MBC5+RUMBLE+RAM+BATTERY */
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto Cartridge::rom_bank_count_from_header(u8 rom_size_byte) -> uint {
|
||||
/* 0x00 = 32KB (2 banks), each step doubles */
|
||||
if(rom_size_byte <= 0x08) return 2u << rom_size_byte;
|
||||
return 2;
|
||||
}
|
||||
|
||||
auto Cartridge::ram_size_from_header(u8 ram_size_byte, MBCType mbc) -> u32 {
|
||||
if(mbc == MBCType::MBC2) return 512; /* built-in, not in header */
|
||||
switch(ram_size_byte) {
|
||||
case 0x00:
|
||||
return 0;
|
||||
case 0x01:
|
||||
return 0x800; /* 2 KB */
|
||||
case 0x02:
|
||||
return 0x2000; /* 8 KB */
|
||||
case 0x03:
|
||||
return 0x8000; /* 32 KB */
|
||||
case 0x04:
|
||||
return 0x20000; /* 128 KB */
|
||||
case 0x05:
|
||||
return 0x10000; /* 64 KB */
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include "definitions.h"
|
||||
|
||||
/* Cartridge with pluggable ROM bank provider.
|
||||
*
|
||||
* Instead of holding the whole ROM in RAM (impossible on Flipper Zero for
|
||||
* anything above 32 KB), the cartridge asks the platform for a pointer to a
|
||||
* 16 KB bank whenever the game switches banks. On the desktop test build the
|
||||
* provider just returns `rom + bank * 0x4000`; on the Flipper it is backed
|
||||
* by an LRU cache streaming from the SD card.
|
||||
*
|
||||
* Supported mappers: ROM only, MBC1 (incl. upper bits / mode select),
|
||||
* MBC2 (built-in 512x4 RAM), MBC3 (no RTC), MBC5.
|
||||
*/
|
||||
|
||||
using RomBankProvider = const u8* (*)(void* ctx, uint bank);
|
||||
|
||||
enum class MBCType : u8 {
|
||||
None,
|
||||
MBC1,
|
||||
MBC2,
|
||||
MBC3,
|
||||
MBC5,
|
||||
Unsupported,
|
||||
};
|
||||
|
||||
class Cartridge {
|
||||
public:
|
||||
/* bank0 must stay valid for the lifetime of the cartridge */
|
||||
void init(
|
||||
const u8* bank0_data,
|
||||
uint rom_bank_count,
|
||||
MBCType mbc_type,
|
||||
u8* cart_ram,
|
||||
u32 cart_ram_size,
|
||||
RomBankProvider provider,
|
||||
void* provider_ctx);
|
||||
|
||||
auto read(u16 addr) const -> u8 {
|
||||
if(addr < 0x4000) return bank0[addr];
|
||||
if(addr < 0x8000) return bankN[addr - 0x4000];
|
||||
/* 0xA000 - 0xBFFF: cartridge RAM */
|
||||
return read_ram(addr);
|
||||
}
|
||||
|
||||
void write(u16 addr, u8 value);
|
||||
|
||||
auto get_ram() -> u8* { return ram; }
|
||||
auto get_ram_size() const -> u32 { return ram_size; }
|
||||
|
||||
/* Header helpers (operate on the first bank) */
|
||||
static auto parse_mbc(u8 cartridge_type_byte) -> MBCType;
|
||||
static auto has_battery(u8 cartridge_type_byte) -> bool;
|
||||
static auto rom_bank_count_from_header(u8 rom_size_byte) -> uint;
|
||||
static auto ram_size_from_header(u8 ram_size_byte, MBCType mbc) -> u32;
|
||||
|
||||
private:
|
||||
auto read_ram(u16 addr) const -> u8;
|
||||
void write_ram(u16 addr, u8 value);
|
||||
void update_rom_bank();
|
||||
|
||||
const u8* bank0 = nullptr;
|
||||
const u8* bankN = nullptr;
|
||||
|
||||
u8* ram = nullptr;
|
||||
u32 ram_size = 0;
|
||||
|
||||
RomBankProvider provider = nullptr;
|
||||
void* provider_ctx = nullptr;
|
||||
|
||||
MBCType mbc = MBCType::None;
|
||||
uint bank_count = 2;
|
||||
|
||||
bool ram_enabled = false;
|
||||
bool advanced_banking_mode = false; /* MBC1 mode 1 */
|
||||
uint bank_low = 1; /* MBC1: 5 bits, MBC3: 7 bits, MBC5: 8 bits */
|
||||
uint bank_high = 0; /* MBC1: 2 bits, MBC5: 9th bit */
|
||||
uint ram_bank = 0;
|
||||
};
|
||||
@@ -0,0 +1,225 @@
|
||||
#include "cpu.h"
|
||||
|
||||
#include "gameboy.h"
|
||||
#include "opcode_cycles.h"
|
||||
#include "bitwise.h"
|
||||
|
||||
using bitwise::compose_bytes;
|
||||
|
||||
CPU::CPU(Gameboy& inGb) :
|
||||
gb(inGb),
|
||||
af(a, f, 0xF0),
|
||||
bc(b, c),
|
||||
de(d, e),
|
||||
hl(h, l)
|
||||
{
|
||||
}
|
||||
|
||||
void CPU::init_post_boot() {
|
||||
af.set(0x01B0);
|
||||
bc.set(0x0013);
|
||||
de.set(0x00D8);
|
||||
hl.set(0x014D);
|
||||
sp.set(0xFFFE);
|
||||
pc.set(0x0100);
|
||||
interrupt_flag.set(0xE1);
|
||||
interrupt_enabled.set(0x00);
|
||||
interrupts_enabled = false;
|
||||
halted = false;
|
||||
}
|
||||
|
||||
auto CPU::tick() -> Cycles {
|
||||
handle_interrupts();
|
||||
|
||||
/* Halted: batch 4 M-cycles per iteration. Games spend most of every
|
||||
* frame in HALT waiting for vblank; stepping 1 cycle at a time made
|
||||
* the idle part of the frame as expensive to emulate as the busy part.
|
||||
* Interrupt recognition is delayed by at most 3 M-cycles (12 T-cycles),
|
||||
* well within what real hardware tolerates. */
|
||||
if (halted) { return 4; }
|
||||
|
||||
u16 opcode_pc = pc.value();
|
||||
auto opcode = get_byte_from_pc();
|
||||
auto cycles = execute_opcode(opcode, opcode_pc);
|
||||
return cycles;
|
||||
}
|
||||
|
||||
auto CPU::execute_opcode(const u8 opcode, u16 opcode_pc) -> Cycles {
|
||||
branch_taken = false;
|
||||
|
||||
if (opcode == 0xCB) {
|
||||
u8 cb_opcode = get_byte_from_pc();
|
||||
return execute_cb_opcode(cb_opcode, opcode_pc);
|
||||
}
|
||||
|
||||
return execute_normal_opcode(opcode, opcode_pc);
|
||||
}
|
||||
|
||||
void CPU::handle_interrupts() {
|
||||
u8 fired_interrupts = interrupt_flag.value() & interrupt_enabled.value();
|
||||
if (!fired_interrupts) { return; }
|
||||
|
||||
if (halted && fired_interrupts != 0x0) {
|
||||
// TODO: Handle halt bug
|
||||
halted = false;
|
||||
}
|
||||
|
||||
if (!interrupts_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
stack_push(pc);
|
||||
|
||||
bool handled_interrupt = false;
|
||||
|
||||
handled_interrupt = handle_interrupt(0, interrupts::vblank, fired_interrupts);
|
||||
if (handled_interrupt) { return; }
|
||||
|
||||
handled_interrupt = handle_interrupt(1, interrupts::lcdc_status, fired_interrupts);
|
||||
if (handled_interrupt) { return; }
|
||||
|
||||
handled_interrupt = handle_interrupt(2, interrupts::timer, fired_interrupts);
|
||||
if (handled_interrupt) { return; }
|
||||
|
||||
handled_interrupt = handle_interrupt(3, interrupts::serial, fired_interrupts);
|
||||
if (handled_interrupt) { return; }
|
||||
|
||||
handled_interrupt = handle_interrupt(4, interrupts::joypad, fired_interrupts);
|
||||
if (handled_interrupt) { return; }
|
||||
}
|
||||
|
||||
auto CPU::handle_interrupt(u8 interrupt_bit, u16 interrupt_vector, u8 fired_interrupts) -> bool {
|
||||
using bitwise::check_bit;
|
||||
|
||||
if (!check_bit(fired_interrupts, interrupt_bit)) { return false; }
|
||||
|
||||
interrupt_flag.set_bit_to(interrupt_bit, false);
|
||||
pc.set(interrupt_vector);
|
||||
interrupts_enabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto CPU::get_byte_from_pc() -> u8 {
|
||||
u8 byte = gb.mmu.read(Address(pc));
|
||||
pc.increment();
|
||||
|
||||
return byte;
|
||||
}
|
||||
|
||||
auto CPU::get_signed_byte_from_pc() -> s8 {
|
||||
u8 byte = get_byte_from_pc();
|
||||
return static_cast<s8>(byte);
|
||||
}
|
||||
|
||||
auto CPU::get_word_from_pc() -> u16 {
|
||||
u8 low_byte = get_byte_from_pc();
|
||||
u8 high_byte = get_byte_from_pc();
|
||||
|
||||
return compose_bytes(high_byte, low_byte);
|
||||
}
|
||||
|
||||
void CPU::set_flag_zero(bool set) { f.set_flag_zero(set); }
|
||||
void CPU::set_flag_subtract(bool set) { f.set_flag_subtract(set); }
|
||||
void CPU::set_flag_half_carry(bool set) { f.set_flag_half_carry(set); }
|
||||
void CPU::set_flag_carry(bool set) { f.set_flag_carry(set); }
|
||||
|
||||
auto CPU::is_condition(Condition condition) -> bool {
|
||||
bool should_branch = false;
|
||||
|
||||
switch (condition) {
|
||||
case Condition::C:
|
||||
should_branch = f.flag_carry();
|
||||
break;
|
||||
case Condition::NC:
|
||||
should_branch = !f.flag_carry();
|
||||
break;
|
||||
case Condition::Z:
|
||||
should_branch = f.flag_zero();
|
||||
break;
|
||||
case Condition::NZ:
|
||||
should_branch = !f.flag_zero();
|
||||
break;
|
||||
}
|
||||
|
||||
/* If the branch is taken, remember so that the correct processor cycles
|
||||
* can be used */
|
||||
branch_taken = should_branch;
|
||||
return should_branch;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void CPU::stack_push(const T& reg) {
|
||||
sp.decrement();
|
||||
gb.mmu.write(Address(sp), reg.high());
|
||||
sp.decrement();
|
||||
gb.mmu.write(Address(sp), reg.low());
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void CPU::stack_pop(T& reg) {
|
||||
u8 low_byte = gb.mmu.read(Address(sp));
|
||||
sp.increment();
|
||||
u8 high_byte = gb.mmu.read(Address(sp));
|
||||
sp.increment();
|
||||
|
||||
u16 value = compose_bytes(high_byte, low_byte);
|
||||
reg.set(value);
|
||||
}
|
||||
|
||||
template void CPU::stack_push<WordRegister>(const WordRegister&);
|
||||
template void CPU::stack_push<RegisterPair>(const RegisterPair&);
|
||||
template void CPU::stack_pop<WordRegister>(WordRegister&);
|
||||
template void CPU::stack_pop<RegisterPair>(RegisterPair&);
|
||||
|
||||
/* clang-format off */
|
||||
auto CPU::execute_normal_opcode(const u8 opcode, u16 opcode_pc) -> Cycles {
|
||||
(void)opcode_pc;
|
||||
|
||||
switch (opcode) {
|
||||
case 0x00: opcode_00(); break; case 0x01: opcode_01(); break; case 0x02: opcode_02(); break; case 0x03: opcode_03(); break; case 0x04: opcode_04(); break; case 0x05: opcode_05(); break; case 0x06: opcode_06(); break; case 0x07: opcode_07(); break; case 0x08: opcode_08(); break; case 0x09: opcode_09(); break; case 0x0A: opcode_0A(); break; case 0x0B: opcode_0B(); break; case 0x0C: opcode_0C(); break; case 0x0D: opcode_0D(); break; case 0x0E: opcode_0E(); break; case 0x0F: opcode_0F(); break;
|
||||
case 0x10: opcode_10(); break; case 0x11: opcode_11(); break; case 0x12: opcode_12(); break; case 0x13: opcode_13(); break; case 0x14: opcode_14(); break; case 0x15: opcode_15(); break; case 0x16: opcode_16(); break; case 0x17: opcode_17(); break; case 0x18: opcode_18(); break; case 0x19: opcode_19(); break; case 0x1A: opcode_1A(); break; case 0x1B: opcode_1B(); break; case 0x1C: opcode_1C(); break; case 0x1D: opcode_1D(); break; case 0x1E: opcode_1E(); break; case 0x1F: opcode_1F(); break;
|
||||
case 0x20: opcode_20(); break; case 0x21: opcode_21(); break; case 0x22: opcode_22(); break; case 0x23: opcode_23(); break; case 0x24: opcode_24(); break; case 0x25: opcode_25(); break; case 0x26: opcode_26(); break; case 0x27: opcode_27(); break; case 0x28: opcode_28(); break; case 0x29: opcode_29(); break; case 0x2A: opcode_2A(); break; case 0x2B: opcode_2B(); break; case 0x2C: opcode_2C(); break; case 0x2D: opcode_2D(); break; case 0x2E: opcode_2E(); break; case 0x2F: opcode_2F(); break;
|
||||
case 0x30: opcode_30(); break; case 0x31: opcode_31(); break; case 0x32: opcode_32(); break; case 0x33: opcode_33(); break; case 0x34: opcode_34(); break; case 0x35: opcode_35(); break; case 0x36: opcode_36(); break; case 0x37: opcode_37(); break; case 0x38: opcode_38(); break; case 0x39: opcode_39(); break; case 0x3A: opcode_3A(); break; case 0x3B: opcode_3B(); break; case 0x3C: opcode_3C(); break; case 0x3D: opcode_3D(); break; case 0x3E: opcode_3E(); break; case 0x3F: opcode_3F(); break;
|
||||
case 0x40: opcode_40(); break; case 0x41: opcode_41(); break; case 0x42: opcode_42(); break; case 0x43: opcode_43(); break; case 0x44: opcode_44(); break; case 0x45: opcode_45(); break; case 0x46: opcode_46(); break; case 0x47: opcode_47(); break; case 0x48: opcode_48(); break; case 0x49: opcode_49(); break; case 0x4A: opcode_4A(); break; case 0x4B: opcode_4B(); break; case 0x4C: opcode_4C(); break; case 0x4D: opcode_4D(); break; case 0x4E: opcode_4E(); break; case 0x4F: opcode_4F(); break;
|
||||
case 0x50: opcode_50(); break; case 0x51: opcode_51(); break; case 0x52: opcode_52(); break; case 0x53: opcode_53(); break; case 0x54: opcode_54(); break; case 0x55: opcode_55(); break; case 0x56: opcode_56(); break; case 0x57: opcode_57(); break; case 0x58: opcode_58(); break; case 0x59: opcode_59(); break; case 0x5A: opcode_5A(); break; case 0x5B: opcode_5B(); break; case 0x5C: opcode_5C(); break; case 0x5D: opcode_5D(); break; case 0x5E: opcode_5E(); break; case 0x5F: opcode_5F(); break;
|
||||
case 0x60: opcode_60(); break; case 0x61: opcode_61(); break; case 0x62: opcode_62(); break; case 0x63: opcode_63(); break; case 0x64: opcode_64(); break; case 0x65: opcode_65(); break; case 0x66: opcode_66(); break; case 0x67: opcode_67(); break; case 0x68: opcode_68(); break; case 0x69: opcode_69(); break; case 0x6A: opcode_6A(); break; case 0x6B: opcode_6B(); break; case 0x6C: opcode_6C(); break; case 0x6D: opcode_6D(); break; case 0x6E: opcode_6E(); break; case 0x6F: opcode_6F(); break;
|
||||
case 0x70: opcode_70(); break; case 0x71: opcode_71(); break; case 0x72: opcode_72(); break; case 0x73: opcode_73(); break; case 0x74: opcode_74(); break; case 0x75: opcode_75(); break; case 0x76: opcode_76(); break; case 0x77: opcode_77(); break; case 0x78: opcode_78(); break; case 0x79: opcode_79(); break; case 0x7A: opcode_7A(); break; case 0x7B: opcode_7B(); break; case 0x7C: opcode_7C(); break; case 0x7D: opcode_7D(); break; case 0x7E: opcode_7E(); break; case 0x7F: opcode_7F(); break;
|
||||
case 0x80: opcode_80(); break; case 0x81: opcode_81(); break; case 0x82: opcode_82(); break; case 0x83: opcode_83(); break; case 0x84: opcode_84(); break; case 0x85: opcode_85(); break; case 0x86: opcode_86(); break; case 0x87: opcode_87(); break; case 0x88: opcode_88(); break; case 0x89: opcode_89(); break; case 0x8A: opcode_8A(); break; case 0x8B: opcode_8B(); break; case 0x8C: opcode_8C(); break; case 0x8D: opcode_8D(); break; case 0x8E: opcode_8E(); break; case 0x8F: opcode_8F(); break;
|
||||
case 0x90: opcode_90(); break; case 0x91: opcode_91(); break; case 0x92: opcode_92(); break; case 0x93: opcode_93(); break; case 0x94: opcode_94(); break; case 0x95: opcode_95(); break; case 0x96: opcode_96(); break; case 0x97: opcode_97(); break; case 0x98: opcode_98(); break; case 0x99: opcode_99(); break; case 0x9A: opcode_9A(); break; case 0x9B: opcode_9B(); break; case 0x9C: opcode_9C(); break; case 0x9D: opcode_9D(); break; case 0x9E: opcode_9E(); break; case 0x9F: opcode_9F(); break;
|
||||
case 0xA0: opcode_A0(); break; case 0xA1: opcode_A1(); break; case 0xA2: opcode_A2(); break; case 0xA3: opcode_A3(); break; case 0xA4: opcode_A4(); break; case 0xA5: opcode_A5(); break; case 0xA6: opcode_A6(); break; case 0xA7: opcode_A7(); break; case 0xA8: opcode_A8(); break; case 0xA9: opcode_A9(); break; case 0xAA: opcode_AA(); break; case 0xAB: opcode_AB(); break; case 0xAC: opcode_AC(); break; case 0xAD: opcode_AD(); break; case 0xAE: opcode_AE(); break; case 0xAF: opcode_AF(); break;
|
||||
case 0xB0: opcode_B0(); break; case 0xB1: opcode_B1(); break; case 0xB2: opcode_B2(); break; case 0xB3: opcode_B3(); break; case 0xB4: opcode_B4(); break; case 0xB5: opcode_B5(); break; case 0xB6: opcode_B6(); break; case 0xB7: opcode_B7(); break; case 0xB8: opcode_B8(); break; case 0xB9: opcode_B9(); break; case 0xBA: opcode_BA(); break; case 0xBB: opcode_BB(); break; case 0xBC: opcode_BC(); break; case 0xBD: opcode_BD(); break; case 0xBE: opcode_BE(); break; case 0xBF: opcode_BF(); break;
|
||||
case 0xC0: opcode_C0(); break; case 0xC1: opcode_C1(); break; case 0xC2: opcode_C2(); break; case 0xC3: opcode_C3(); break; case 0xC4: opcode_C4(); break; case 0xC5: opcode_C5(); break; case 0xC6: opcode_C6(); break; case 0xC7: opcode_C7(); break; case 0xC8: opcode_C8(); break; case 0xC9: opcode_C9(); break; case 0xCA: opcode_CA(); break; case 0xCB: opcode_CB(); break; case 0xCC: opcode_CC(); break; case 0xCD: opcode_CD(); break; case 0xCE: opcode_CE(); break; case 0xCF: opcode_CF(); break;
|
||||
case 0xD0: opcode_D0(); break; case 0xD1: opcode_D1(); break; case 0xD2: opcode_D2(); break; case 0xD3: opcode_D3(); break; case 0xD4: opcode_D4(); break; case 0xD5: opcode_D5(); break; case 0xD6: opcode_D6(); break; case 0xD7: opcode_D7(); break; case 0xD8: opcode_D8(); break; case 0xD9: opcode_D9(); break; case 0xDA: opcode_DA(); break; case 0xDB: opcode_DB(); break; case 0xDC: opcode_DC(); break; case 0xDD: opcode_DD(); break; case 0xDE: opcode_DE(); break; case 0xDF: opcode_DF(); break;
|
||||
case 0xE0: opcode_E0(); break; case 0xE1: opcode_E1(); break; case 0xE2: opcode_E2(); break; case 0xE3: opcode_E3(); break; case 0xE4: opcode_E4(); break; case 0xE5: opcode_E5(); break; case 0xE6: opcode_E6(); break; case 0xE7: opcode_E7(); break; case 0xE8: opcode_E8(); break; case 0xE9: opcode_E9(); break; case 0xEA: opcode_EA(); break; case 0xEB: opcode_EB(); break; case 0xEC: opcode_EC(); break; case 0xED: opcode_ED(); break; case 0xEE: opcode_EE(); break; case 0xEF: opcode_EF(); break;
|
||||
case 0xF0: opcode_F0(); break; case 0xF1: opcode_F1(); break; case 0xF2: opcode_F2(); break; case 0xF3: opcode_F3(); break; case 0xF4: opcode_F4(); break; case 0xF5: opcode_F5(); break; case 0xF6: opcode_F6(); break; case 0xF7: opcode_F7(); break; case 0xF8: opcode_F8(); break; case 0xF9: opcode_F9(); break; case 0xFA: opcode_FA(); break; case 0xFB: opcode_FB(); break; case 0xFC: opcode_FC(); break; case 0xFD: opcode_FD(); break; case 0xFE: opcode_FE(); break; case 0xFF: opcode_FF(); break;
|
||||
}
|
||||
|
||||
return !branch_taken
|
||||
? opcode_cycles[opcode]
|
||||
: opcode_cycles_branched[opcode];
|
||||
}
|
||||
|
||||
auto CPU::execute_cb_opcode(const u8 opcode, u16 opcode_pc) -> Cycles {
|
||||
(void)opcode_pc;
|
||||
|
||||
switch (opcode) {
|
||||
case 0x00: opcode_CB_00(); break; case 0x01: opcode_CB_01(); break; case 0x02: opcode_CB_02(); break; case 0x03: opcode_CB_03(); break; case 0x04: opcode_CB_04(); break; case 0x05: opcode_CB_05(); break; case 0x06: opcode_CB_06(); break; case 0x07: opcode_CB_07(); break; case 0x08: opcode_CB_08(); break; case 0x09: opcode_CB_09(); break; case 0x0A: opcode_CB_0A(); break; case 0x0B: opcode_CB_0B(); break; case 0x0C: opcode_CB_0C(); break; case 0x0D: opcode_CB_0D(); break; case 0x0E: opcode_CB_0E(); break; case 0x0F: opcode_CB_0F(); break;
|
||||
case 0x10: opcode_CB_10(); break; case 0x11: opcode_CB_11(); break; case 0x12: opcode_CB_12(); break; case 0x13: opcode_CB_13(); break; case 0x14: opcode_CB_14(); break; case 0x15: opcode_CB_15(); break; case 0x16: opcode_CB_16(); break; case 0x17: opcode_CB_17(); break; case 0x18: opcode_CB_18(); break; case 0x19: opcode_CB_19(); break; case 0x1A: opcode_CB_1A(); break; case 0x1B: opcode_CB_1B(); break; case 0x1C: opcode_CB_1C(); break; case 0x1D: opcode_CB_1D(); break; case 0x1E: opcode_CB_1E(); break; case 0x1F: opcode_CB_1F(); break;
|
||||
case 0x20: opcode_CB_20(); break; case 0x21: opcode_CB_21(); break; case 0x22: opcode_CB_22(); break; case 0x23: opcode_CB_23(); break; case 0x24: opcode_CB_24(); break; case 0x25: opcode_CB_25(); break; case 0x26: opcode_CB_26(); break; case 0x27: opcode_CB_27(); break; case 0x28: opcode_CB_28(); break; case 0x29: opcode_CB_29(); break; case 0x2A: opcode_CB_2A(); break; case 0x2B: opcode_CB_2B(); break; case 0x2C: opcode_CB_2C(); break; case 0x2D: opcode_CB_2D(); break; case 0x2E: opcode_CB_2E(); break; case 0x2F: opcode_CB_2F(); break;
|
||||
case 0x30: opcode_CB_30(); break; case 0x31: opcode_CB_31(); break; case 0x32: opcode_CB_32(); break; case 0x33: opcode_CB_33(); break; case 0x34: opcode_CB_34(); break; case 0x35: opcode_CB_35(); break; case 0x36: opcode_CB_36(); break; case 0x37: opcode_CB_37(); break; case 0x38: opcode_CB_38(); break; case 0x39: opcode_CB_39(); break; case 0x3A: opcode_CB_3A(); break; case 0x3B: opcode_CB_3B(); break; case 0x3C: opcode_CB_3C(); break; case 0x3D: opcode_CB_3D(); break; case 0x3E: opcode_CB_3E(); break; case 0x3F: opcode_CB_3F(); break;
|
||||
case 0x40: opcode_CB_40(); break; case 0x41: opcode_CB_41(); break; case 0x42: opcode_CB_42(); break; case 0x43: opcode_CB_43(); break; case 0x44: opcode_CB_44(); break; case 0x45: opcode_CB_45(); break; case 0x46: opcode_CB_46(); break; case 0x47: opcode_CB_47(); break; case 0x48: opcode_CB_48(); break; case 0x49: opcode_CB_49(); break; case 0x4A: opcode_CB_4A(); break; case 0x4B: opcode_CB_4B(); break; case 0x4C: opcode_CB_4C(); break; case 0x4D: opcode_CB_4D(); break; case 0x4E: opcode_CB_4E(); break; case 0x4F: opcode_CB_4F(); break;
|
||||
case 0x50: opcode_CB_50(); break; case 0x51: opcode_CB_51(); break; case 0x52: opcode_CB_52(); break; case 0x53: opcode_CB_53(); break; case 0x54: opcode_CB_54(); break; case 0x55: opcode_CB_55(); break; case 0x56: opcode_CB_56(); break; case 0x57: opcode_CB_57(); break; case 0x58: opcode_CB_58(); break; case 0x59: opcode_CB_59(); break; case 0x5A: opcode_CB_5A(); break; case 0x5B: opcode_CB_5B(); break; case 0x5C: opcode_CB_5C(); break; case 0x5D: opcode_CB_5D(); break; case 0x5E: opcode_CB_5E(); break; case 0x5F: opcode_CB_5F(); break;
|
||||
case 0x60: opcode_CB_60(); break; case 0x61: opcode_CB_61(); break; case 0x62: opcode_CB_62(); break; case 0x63: opcode_CB_63(); break; case 0x64: opcode_CB_64(); break; case 0x65: opcode_CB_65(); break; case 0x66: opcode_CB_66(); break; case 0x67: opcode_CB_67(); break; case 0x68: opcode_CB_68(); break; case 0x69: opcode_CB_69(); break; case 0x6A: opcode_CB_6A(); break; case 0x6B: opcode_CB_6B(); break; case 0x6C: opcode_CB_6C(); break; case 0x6D: opcode_CB_6D(); break; case 0x6E: opcode_CB_6E(); break; case 0x6F: opcode_CB_6F(); break;
|
||||
case 0x70: opcode_CB_70(); break; case 0x71: opcode_CB_71(); break; case 0x72: opcode_CB_72(); break; case 0x73: opcode_CB_73(); break; case 0x74: opcode_CB_74(); break; case 0x75: opcode_CB_75(); break; case 0x76: opcode_CB_76(); break; case 0x77: opcode_CB_77(); break; case 0x78: opcode_CB_78(); break; case 0x79: opcode_CB_79(); break; case 0x7A: opcode_CB_7A(); break; case 0x7B: opcode_CB_7B(); break; case 0x7C: opcode_CB_7C(); break; case 0x7D: opcode_CB_7D(); break; case 0x7E: opcode_CB_7E(); break; case 0x7F: opcode_CB_7F(); break;
|
||||
case 0x80: opcode_CB_80(); break; case 0x81: opcode_CB_81(); break; case 0x82: opcode_CB_82(); break; case 0x83: opcode_CB_83(); break; case 0x84: opcode_CB_84(); break; case 0x85: opcode_CB_85(); break; case 0x86: opcode_CB_86(); break; case 0x87: opcode_CB_87(); break; case 0x88: opcode_CB_88(); break; case 0x89: opcode_CB_89(); break; case 0x8A: opcode_CB_8A(); break; case 0x8B: opcode_CB_8B(); break; case 0x8C: opcode_CB_8C(); break; case 0x8D: opcode_CB_8D(); break; case 0x8E: opcode_CB_8E(); break; case 0x8F: opcode_CB_8F(); break;
|
||||
case 0x90: opcode_CB_90(); break; case 0x91: opcode_CB_91(); break; case 0x92: opcode_CB_92(); break; case 0x93: opcode_CB_93(); break; case 0x94: opcode_CB_94(); break; case 0x95: opcode_CB_95(); break; case 0x96: opcode_CB_96(); break; case 0x97: opcode_CB_97(); break; case 0x98: opcode_CB_98(); break; case 0x99: opcode_CB_99(); break; case 0x9A: opcode_CB_9A(); break; case 0x9B: opcode_CB_9B(); break; case 0x9C: opcode_CB_9C(); break; case 0x9D: opcode_CB_9D(); break; case 0x9E: opcode_CB_9E(); break; case 0x9F: opcode_CB_9F(); break;
|
||||
case 0xA0: opcode_CB_A0(); break; case 0xA1: opcode_CB_A1(); break; case 0xA2: opcode_CB_A2(); break; case 0xA3: opcode_CB_A3(); break; case 0xA4: opcode_CB_A4(); break; case 0xA5: opcode_CB_A5(); break; case 0xA6: opcode_CB_A6(); break; case 0xA7: opcode_CB_A7(); break; case 0xA8: opcode_CB_A8(); break; case 0xA9: opcode_CB_A9(); break; case 0xAA: opcode_CB_AA(); break; case 0xAB: opcode_CB_AB(); break; case 0xAC: opcode_CB_AC(); break; case 0xAD: opcode_CB_AD(); break; case 0xAE: opcode_CB_AE(); break; case 0xAF: opcode_CB_AF(); break;
|
||||
case 0xB0: opcode_CB_B0(); break; case 0xB1: opcode_CB_B1(); break; case 0xB2: opcode_CB_B2(); break; case 0xB3: opcode_CB_B3(); break; case 0xB4: opcode_CB_B4(); break; case 0xB5: opcode_CB_B5(); break; case 0xB6: opcode_CB_B6(); break; case 0xB7: opcode_CB_B7(); break; case 0xB8: opcode_CB_B8(); break; case 0xB9: opcode_CB_B9(); break; case 0xBA: opcode_CB_BA(); break; case 0xBB: opcode_CB_BB(); break; case 0xBC: opcode_CB_BC(); break; case 0xBD: opcode_CB_BD(); break; case 0xBE: opcode_CB_BE(); break; case 0xBF: opcode_CB_BF(); break;
|
||||
case 0xC0: opcode_CB_C0(); break; case 0xC1: opcode_CB_C1(); break; case 0xC2: opcode_CB_C2(); break; case 0xC3: opcode_CB_C3(); break; case 0xC4: opcode_CB_C4(); break; case 0xC5: opcode_CB_C5(); break; case 0xC6: opcode_CB_C6(); break; case 0xC7: opcode_CB_C7(); break; case 0xC8: opcode_CB_C8(); break; case 0xC9: opcode_CB_C9(); break; case 0xCA: opcode_CB_CA(); break; case 0xCB: opcode_CB_CB(); break; case 0xCC: opcode_CB_CC(); break; case 0xCD: opcode_CB_CD(); break; case 0xCE: opcode_CB_CE(); break; case 0xCF: opcode_CB_CF(); break;
|
||||
case 0xD0: opcode_CB_D0(); break; case 0xD1: opcode_CB_D1(); break; case 0xD2: opcode_CB_D2(); break; case 0xD3: opcode_CB_D3(); break; case 0xD4: opcode_CB_D4(); break; case 0xD5: opcode_CB_D5(); break; case 0xD6: opcode_CB_D6(); break; case 0xD7: opcode_CB_D7(); break; case 0xD8: opcode_CB_D8(); break; case 0xD9: opcode_CB_D9(); break; case 0xDA: opcode_CB_DA(); break; case 0xDB: opcode_CB_DB(); break; case 0xDC: opcode_CB_DC(); break; case 0xDD: opcode_CB_DD(); break; case 0xDE: opcode_CB_DE(); break; case 0xDF: opcode_CB_DF(); break;
|
||||
case 0xE0: opcode_CB_E0(); break; case 0xE1: opcode_CB_E1(); break; case 0xE2: opcode_CB_E2(); break; case 0xE3: opcode_CB_E3(); break; case 0xE4: opcode_CB_E4(); break; case 0xE5: opcode_CB_E5(); break; case 0xE6: opcode_CB_E6(); break; case 0xE7: opcode_CB_E7(); break; case 0xE8: opcode_CB_E8(); break; case 0xE9: opcode_CB_E9(); break; case 0xEA: opcode_CB_EA(); break; case 0xEB: opcode_CB_EB(); break; case 0xEC: opcode_CB_EC(); break; case 0xED: opcode_CB_ED(); break; case 0xEE: opcode_CB_EE(); break; case 0xEF: opcode_CB_EF(); break;
|
||||
case 0xF0: opcode_CB_F0(); break; case 0xF1: opcode_CB_F1(); break; case 0xF2: opcode_CB_F2(); break; case 0xF3: opcode_CB_F3(); break; case 0xF4: opcode_CB_F4(); break; case 0xF5: opcode_CB_F5(); break; case 0xF6: opcode_CB_F6(); break; case 0xF7: opcode_CB_F7(); break; case 0xF8: opcode_CB_F8(); break; case 0xF9: opcode_CB_F9(); break; case 0xFA: opcode_CB_FA(); break; case 0xFB: opcode_CB_FB(); break; case 0xFC: opcode_CB_FC(); break; case 0xFD: opcode_CB_FD(); break; case 0xFE: opcode_CB_FE(); break; case 0xFF: opcode_CB_FF(); break;
|
||||
}
|
||||
|
||||
return opcode_cycles_cb[opcode];
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
#pragma once
|
||||
|
||||
#include "address.h"
|
||||
#include "register.h"
|
||||
#include "definitions.h"
|
||||
|
||||
class Gameboy;
|
||||
|
||||
enum class Condition {
|
||||
NZ,
|
||||
Z,
|
||||
NC,
|
||||
C,
|
||||
};
|
||||
|
||||
namespace rst {
|
||||
const u16 rst1 = 0x00;
|
||||
const u16 rst2 = 0x08;
|
||||
const u16 rst3 = 0x10;
|
||||
const u16 rst4 = 0x18;
|
||||
const u16 rst5 = 0x20;
|
||||
const u16 rst6 = 0x28;
|
||||
const u16 rst7 = 0x30;
|
||||
const u16 rst8 = 0x38;
|
||||
} // namespace rst
|
||||
|
||||
namespace interrupts {
|
||||
const u16 vblank = 0x40;
|
||||
const u16 lcdc_status = 0x48;
|
||||
const u16 timer = 0x50;
|
||||
const u16 serial = 0x58;
|
||||
const u16 joypad = 0x60;
|
||||
} // namespace interrupts
|
||||
|
||||
|
||||
class CPU {
|
||||
public:
|
||||
CPU(Gameboy& inGb);
|
||||
|
||||
/* Initialise registers to the documented DMG post-boot state
|
||||
* (the Nintendo boot ROM is not shipped nor emulated) */
|
||||
void init_post_boot();
|
||||
|
||||
auto tick() -> Cycles;
|
||||
|
||||
auto execute_opcode(u8 opcode, u16 opcode_pc) -> Cycles;
|
||||
|
||||
auto execute_normal_opcode(u8 opcode, u16 opcode_pc) -> Cycles;
|
||||
auto execute_cb_opcode(u8 opcode, u16 opcode_pc) -> Cycles;
|
||||
|
||||
ByteRegister interrupt_flag;
|
||||
ByteRegister interrupt_enabled;
|
||||
|
||||
private:
|
||||
void handle_interrupts();
|
||||
auto handle_interrupt(u8 interrupt_bit, u16 interrupt_vector, u8 fired_interrupts) -> bool;
|
||||
|
||||
Gameboy& gb;
|
||||
|
||||
bool interrupts_enabled = false;
|
||||
bool halted = false;
|
||||
|
||||
bool branch_taken = false;
|
||||
|
||||
/* Basic registers */
|
||||
ByteRegister a, b, c, d, e, h, l;
|
||||
|
||||
/* 'Group' registers for operations which use two registers as a word */
|
||||
RegisterPair af;
|
||||
RegisterPair bc;
|
||||
RegisterPair de;
|
||||
RegisterPair hl;
|
||||
|
||||
/*
|
||||
* Flags set dependant on the result of the last operation
|
||||
* 0x80 - produced 0
|
||||
* 0x40 - was a subtraction
|
||||
* 0x20 - lower half of the byte overflowed 15
|
||||
* 0x10 - overflowed 255 or underflowed 0 for additions/subtractions
|
||||
*/
|
||||
FlagRegister f;
|
||||
|
||||
void set_flag_zero(bool set);
|
||||
void set_flag_subtract(bool set);
|
||||
void set_flag_half_carry(bool set);
|
||||
void set_flag_carry(bool set);
|
||||
|
||||
/* Note: Not const because this also sets the 'branch_taken' member
|
||||
* variable if a branch is taken. This allows the correct cycle
|
||||
* count to be used */
|
||||
auto is_condition(Condition condition) -> bool;
|
||||
|
||||
/* Program counter */
|
||||
WordRegister pc;
|
||||
|
||||
/* Stack pointer */
|
||||
WordRegister sp;
|
||||
|
||||
auto get_byte_from_pc() -> u8;
|
||||
auto get_signed_byte_from_pc() -> s8;
|
||||
auto get_word_from_pc() -> u16;
|
||||
|
||||
template <typename T> void stack_push(const T& reg);
|
||||
template <typename T> void stack_pop(T& reg);
|
||||
|
||||
/* Opcode Helper Functions */
|
||||
|
||||
/* ADC */
|
||||
void _opcode_adc(u8 value);
|
||||
|
||||
void opcode_adc();
|
||||
void opcode_adc(const ByteRegister& reg);
|
||||
void opcode_adc(const Address&& addr);
|
||||
|
||||
/* ADD */
|
||||
void _opcode_add(u8 reg, u8 value);
|
||||
|
||||
void opcode_add_a();
|
||||
void opcode_add_a(const ByteRegister& reg);
|
||||
void opcode_add_a(const Address& addr);
|
||||
|
||||
void _opcode_add_hl(u16 value);
|
||||
void opcode_add_hl(const RegisterPair& reg_pair);
|
||||
void opcode_add_hl(const WordRegister& word_reg);
|
||||
|
||||
void opcode_add_sp();
|
||||
|
||||
void opcode_add_signed();
|
||||
|
||||
/* AND */
|
||||
void _opcode_and(u8 value);
|
||||
|
||||
void opcode_and();
|
||||
void opcode_and(ByteRegister& reg);
|
||||
void opcode_and(Address&& addr);
|
||||
|
||||
/* BIT */
|
||||
void _opcode_bit(u8 bit, u8 value);
|
||||
|
||||
void opcode_bit(u8 bit, ByteRegister& reg);
|
||||
void opcode_bit(u8 bit, Address&& addr);
|
||||
|
||||
/* CALL */
|
||||
void opcode_call();
|
||||
void opcode_call(Condition condition);
|
||||
|
||||
/* CCF */
|
||||
void opcode_ccf();
|
||||
|
||||
/* CP */
|
||||
void _opcode_cp(u8 value);
|
||||
|
||||
void opcode_cp();
|
||||
void opcode_cp(const ByteRegister& reg);
|
||||
void opcode_cp(const Address& addr);
|
||||
|
||||
/* CPL */
|
||||
void opcode_cpl();
|
||||
|
||||
/* DAA */
|
||||
void opcode_daa();
|
||||
|
||||
/* DEC */
|
||||
void opcode_dec(ByteRegister& reg);
|
||||
void opcode_dec(RegisterPair& reg);
|
||||
void opcode_dec(WordRegister& reg);
|
||||
void opcode_dec(Address&& addr);
|
||||
|
||||
/* DI */
|
||||
void opcode_di();
|
||||
|
||||
/* EI */
|
||||
void opcode_ei();
|
||||
|
||||
/* INC */
|
||||
void opcode_inc(ByteRegister& reg);
|
||||
void opcode_inc(RegisterPair& reg);
|
||||
void opcode_inc(WordRegister& reg);
|
||||
void opcode_inc(Address&& addr);
|
||||
|
||||
/* JP */
|
||||
void opcode_jp();
|
||||
void opcode_jp(Condition condition);
|
||||
void opcode_jp(const Address& addr);
|
||||
|
||||
/* JR */
|
||||
void opcode_jr();
|
||||
void opcode_jr(Condition condition);
|
||||
|
||||
/* HALT */
|
||||
void opcode_halt();
|
||||
|
||||
/* LD */
|
||||
void opcode_ld(ByteRegister& reg);
|
||||
void opcode_ld(ByteRegister& reg, const ByteRegister& byte_reg);
|
||||
void opcode_ld(ByteRegister& reg, const Address& address);
|
||||
|
||||
void opcode_ld(RegisterPair& reg);
|
||||
|
||||
void opcode_ld(WordRegister& reg);
|
||||
void opcode_ld(WordRegister& reg, const RegisterPair& reg_pair);
|
||||
|
||||
void opcode_ld(const Address& address);
|
||||
void opcode_ld(const Address& address, const ByteRegister& byte_reg);
|
||||
void opcode_ld(const Address& address, const WordRegister& word_reg);
|
||||
|
||||
// (nn), A
|
||||
void opcode_ld_to_addr(const ByteRegister& reg);
|
||||
void opcode_ld_from_addr(ByteRegister& reg);
|
||||
|
||||
/* LDD */
|
||||
auto _opcode_ldd(u8 value) -> u8;
|
||||
|
||||
void opcode_ldd(ByteRegister& reg, const Address& address);
|
||||
void opcode_ldd(const Address& address, const ByteRegister& reg);
|
||||
|
||||
/* LDH */
|
||||
// A, (n)
|
||||
void opcode_ldh_into_a();
|
||||
// (n), A
|
||||
void opcode_ldh_into_data();
|
||||
// (reg), A
|
||||
void opcode_ldh_into_c();
|
||||
// A, (reg)
|
||||
void opcode_ldh_c_into_a();
|
||||
|
||||
/* LDHL */
|
||||
void opcode_ldhl();
|
||||
|
||||
/* LDI */
|
||||
void opcode_ldi(ByteRegister& reg, const Address& address);
|
||||
void opcode_ldi(const Address& address, const ByteRegister& reg);
|
||||
|
||||
/* NOP */
|
||||
void opcode_nop();
|
||||
|
||||
/* OR */
|
||||
void _opcode_or(u8 value);
|
||||
|
||||
void opcode_or();
|
||||
void opcode_or(const ByteRegister& reg);
|
||||
void opcode_or(const Address& addr);
|
||||
|
||||
/* POP */
|
||||
void opcode_pop(RegisterPair& reg);
|
||||
|
||||
/* PUSH */
|
||||
void opcode_push(const RegisterPair& reg);
|
||||
|
||||
/* RES */
|
||||
void opcode_res(u8 bit, ByteRegister& reg);
|
||||
void opcode_res(u8 bit, Address&& addr);
|
||||
|
||||
/* RET */
|
||||
void opcode_ret();
|
||||
void opcode_ret(Condition condition);
|
||||
|
||||
/* RETI */
|
||||
void opcode_reti();
|
||||
|
||||
/* RL */
|
||||
auto _opcode_rl(u8 value) -> u8;
|
||||
|
||||
void opcode_rla();
|
||||
void opcode_rl(ByteRegister& reg);
|
||||
void opcode_rl(Address&& addr);
|
||||
|
||||
/* RLC */
|
||||
auto _opcode_rlc(u8 value) -> u8;
|
||||
|
||||
void opcode_rlca();
|
||||
void opcode_rlc(ByteRegister& reg);
|
||||
void opcode_rlc(Address&& addr);
|
||||
|
||||
/* RR */
|
||||
auto _opcode_rr(u8 value) -> u8;
|
||||
|
||||
void opcode_rra();
|
||||
void opcode_rr(ByteRegister& reg);
|
||||
void opcode_rr(Address&& addr);
|
||||
|
||||
/* RRC */
|
||||
auto _opcode_rrc(u8 value) -> u8;
|
||||
|
||||
void opcode_rrca();
|
||||
void opcode_rrc(ByteRegister& reg);
|
||||
void opcode_rrc(Address&& addr);
|
||||
|
||||
/* RST */
|
||||
void opcode_rst(u8 offset);
|
||||
|
||||
/* SBC */
|
||||
void _opcode_sbc(u8 value);
|
||||
|
||||
void opcode_sbc();
|
||||
void opcode_sbc(ByteRegister& reg);
|
||||
void opcode_sbc(Address&& addr);
|
||||
|
||||
/* SCF */
|
||||
void opcode_scf();
|
||||
|
||||
/* SET */
|
||||
void opcode_set(u8 bit, ByteRegister& reg);
|
||||
void opcode_set(u8 bit, Address&& addr);
|
||||
|
||||
/* SLA */
|
||||
auto _opcode_sla(u8 value) -> u8;
|
||||
|
||||
void opcode_sla(ByteRegister& reg);
|
||||
void opcode_sla(Address&& addr);
|
||||
|
||||
/* SRA */
|
||||
auto _opcode_sra(u8 value) -> u8;
|
||||
|
||||
void opcode_sra(ByteRegister& reg);
|
||||
void opcode_sra(Address&& addr);
|
||||
|
||||
/* SRL */
|
||||
auto _opcode_srl(u8 value) -> u8;
|
||||
|
||||
void opcode_srl(ByteRegister& reg);
|
||||
void opcode_srl(Address&& addr);
|
||||
|
||||
/* STOP */
|
||||
void opcode_stop();
|
||||
|
||||
/* SUB */
|
||||
void _opcode_sub(u8 value);
|
||||
|
||||
void opcode_sub();
|
||||
void opcode_sub(ByteRegister& reg);
|
||||
void opcode_sub(Address&& addr);
|
||||
|
||||
/* SWAP */
|
||||
auto _opcode_swap(u8 value) -> u8;
|
||||
|
||||
void opcode_swap(ByteRegister& reg);
|
||||
void opcode_swap(Address&& addr);
|
||||
|
||||
/* XOR */
|
||||
void _opcode_xor(u8 value);
|
||||
|
||||
void opcode_xor();
|
||||
void opcode_xor(const ByteRegister& reg);
|
||||
void opcode_xor(const Address& addr);
|
||||
|
||||
|
||||
/* clang-format off */
|
||||
/* Opcodes */
|
||||
void opcode_00(); void opcode_01(); void opcode_02(); void opcode_03(); void opcode_04(); void opcode_05(); void opcode_06(); void opcode_07(); void opcode_08(); void opcode_09(); void opcode_0A(); void opcode_0B(); void opcode_0C(); void opcode_0D(); void opcode_0E(); void opcode_0F();
|
||||
void opcode_10(); void opcode_11(); void opcode_12(); void opcode_13(); void opcode_14(); void opcode_15(); void opcode_16(); void opcode_17(); void opcode_18(); void opcode_19(); void opcode_1A(); void opcode_1B(); void opcode_1C(); void opcode_1D(); void opcode_1E(); void opcode_1F();
|
||||
void opcode_20(); void opcode_21(); void opcode_22(); void opcode_23(); void opcode_24(); void opcode_25(); void opcode_26(); void opcode_27(); void opcode_28(); void opcode_29(); void opcode_2A(); void opcode_2B(); void opcode_2C(); void opcode_2D(); void opcode_2E(); void opcode_2F();
|
||||
void opcode_30(); void opcode_31(); void opcode_32(); void opcode_33(); void opcode_34(); void opcode_35(); void opcode_36(); void opcode_37(); void opcode_38(); void opcode_39(); void opcode_3A(); void opcode_3B(); void opcode_3C(); void opcode_3D(); void opcode_3E(); void opcode_3F();
|
||||
void opcode_40(); void opcode_41(); void opcode_42(); void opcode_43(); void opcode_44(); void opcode_45(); void opcode_46(); void opcode_47(); void opcode_48(); void opcode_49(); void opcode_4A(); void opcode_4B(); void opcode_4C(); void opcode_4D(); void opcode_4E(); void opcode_4F();
|
||||
void opcode_50(); void opcode_51(); void opcode_52(); void opcode_53(); void opcode_54(); void opcode_55(); void opcode_56(); void opcode_57(); void opcode_58(); void opcode_59(); void opcode_5A(); void opcode_5B(); void opcode_5C(); void opcode_5D(); void opcode_5E(); void opcode_5F();
|
||||
void opcode_60(); void opcode_61(); void opcode_62(); void opcode_63(); void opcode_64(); void opcode_65(); void opcode_66(); void opcode_67(); void opcode_68(); void opcode_69(); void opcode_6A(); void opcode_6B(); void opcode_6C(); void opcode_6D(); void opcode_6E(); void opcode_6F();
|
||||
void opcode_70(); void opcode_71(); void opcode_72(); void opcode_73(); void opcode_74(); void opcode_75(); void opcode_76(); void opcode_77(); void opcode_78(); void opcode_79(); void opcode_7A(); void opcode_7B(); void opcode_7C(); void opcode_7D(); void opcode_7E(); void opcode_7F();
|
||||
void opcode_80(); void opcode_81(); void opcode_82(); void opcode_83(); void opcode_84(); void opcode_85(); void opcode_86(); void opcode_87(); void opcode_88(); void opcode_89(); void opcode_8A(); void opcode_8B(); void opcode_8C(); void opcode_8D(); void opcode_8E(); void opcode_8F();
|
||||
void opcode_90(); void opcode_91(); void opcode_92(); void opcode_93(); void opcode_94(); void opcode_95(); void opcode_96(); void opcode_97(); void opcode_98(); void opcode_99(); void opcode_9A(); void opcode_9B(); void opcode_9C(); void opcode_9D(); void opcode_9E(); void opcode_9F();
|
||||
void opcode_A0(); void opcode_A1(); void opcode_A2(); void opcode_A3(); void opcode_A4(); void opcode_A5(); void opcode_A6(); void opcode_A7(); void opcode_A8(); void opcode_A9(); void opcode_AA(); void opcode_AB(); void opcode_AC(); void opcode_AD(); void opcode_AE(); void opcode_AF();
|
||||
void opcode_B0(); void opcode_B1(); void opcode_B2(); void opcode_B3(); void opcode_B4(); void opcode_B5(); void opcode_B6(); void opcode_B7(); void opcode_B8(); void opcode_B9(); void opcode_BA(); void opcode_BB(); void opcode_BC(); void opcode_BD(); void opcode_BE(); void opcode_BF();
|
||||
void opcode_C0(); void opcode_C1(); void opcode_C2(); void opcode_C3(); void opcode_C4(); void opcode_C5(); void opcode_C6(); void opcode_C7(); void opcode_C8(); void opcode_C9(); void opcode_CA(); void opcode_CB(); void opcode_CC(); void opcode_CD(); void opcode_CE(); void opcode_CF();
|
||||
void opcode_D0(); void opcode_D1(); void opcode_D2(); void opcode_D3(); void opcode_D4(); void opcode_D5(); void opcode_D6(); void opcode_D7(); void opcode_D8(); void opcode_D9(); void opcode_DA(); void opcode_DB(); void opcode_DC(); void opcode_DD(); void opcode_DE(); void opcode_DF();
|
||||
void opcode_E0(); void opcode_E1(); void opcode_E2(); void opcode_E3(); void opcode_E4(); void opcode_E5(); void opcode_E6(); void opcode_E7(); void opcode_E8(); void opcode_E9(); void opcode_EA(); void opcode_EB(); void opcode_EC(); void opcode_ED(); void opcode_EE(); void opcode_EF();
|
||||
void opcode_F0(); void opcode_F1(); void opcode_F2(); void opcode_F3(); void opcode_F4(); void opcode_F5(); void opcode_F6(); void opcode_F7(); void opcode_F8(); void opcode_F9(); void opcode_FA(); void opcode_FB(); void opcode_FC(); void opcode_FD(); void opcode_FE(); void opcode_FF();
|
||||
|
||||
/* CB Opcodes */
|
||||
void opcode_CB_00(); void opcode_CB_01(); void opcode_CB_02(); void opcode_CB_03(); void opcode_CB_04(); void opcode_CB_05(); void opcode_CB_06(); void opcode_CB_07(); void opcode_CB_08(); void opcode_CB_09(); void opcode_CB_0A(); void opcode_CB_0B(); void opcode_CB_0C(); void opcode_CB_0D(); void opcode_CB_0E(); void opcode_CB_0F();
|
||||
void opcode_CB_10(); void opcode_CB_11(); void opcode_CB_12(); void opcode_CB_13(); void opcode_CB_14(); void opcode_CB_15(); void opcode_CB_16(); void opcode_CB_17(); void opcode_CB_18(); void opcode_CB_19(); void opcode_CB_1A(); void opcode_CB_1B(); void opcode_CB_1C(); void opcode_CB_1D(); void opcode_CB_1E(); void opcode_CB_1F();
|
||||
void opcode_CB_20(); void opcode_CB_21(); void opcode_CB_22(); void opcode_CB_23(); void opcode_CB_24(); void opcode_CB_25(); void opcode_CB_26(); void opcode_CB_27(); void opcode_CB_28(); void opcode_CB_29(); void opcode_CB_2A(); void opcode_CB_2B(); void opcode_CB_2C(); void opcode_CB_2D(); void opcode_CB_2E(); void opcode_CB_2F();
|
||||
void opcode_CB_30(); void opcode_CB_31(); void opcode_CB_32(); void opcode_CB_33(); void opcode_CB_34(); void opcode_CB_35(); void opcode_CB_36(); void opcode_CB_37(); void opcode_CB_38(); void opcode_CB_39(); void opcode_CB_3A(); void opcode_CB_3B(); void opcode_CB_3C(); void opcode_CB_3D(); void opcode_CB_3E(); void opcode_CB_3F();
|
||||
void opcode_CB_40(); void opcode_CB_41(); void opcode_CB_42(); void opcode_CB_43(); void opcode_CB_44(); void opcode_CB_45(); void opcode_CB_46(); void opcode_CB_47(); void opcode_CB_48(); void opcode_CB_49(); void opcode_CB_4A(); void opcode_CB_4B(); void opcode_CB_4C(); void opcode_CB_4D(); void opcode_CB_4E(); void opcode_CB_4F();
|
||||
void opcode_CB_50(); void opcode_CB_51(); void opcode_CB_52(); void opcode_CB_53(); void opcode_CB_54(); void opcode_CB_55(); void opcode_CB_56(); void opcode_CB_57(); void opcode_CB_58(); void opcode_CB_59(); void opcode_CB_5A(); void opcode_CB_5B(); void opcode_CB_5C(); void opcode_CB_5D(); void opcode_CB_5E(); void opcode_CB_5F();
|
||||
void opcode_CB_60(); void opcode_CB_61(); void opcode_CB_62(); void opcode_CB_63(); void opcode_CB_64(); void opcode_CB_65(); void opcode_CB_66(); void opcode_CB_67(); void opcode_CB_68(); void opcode_CB_69(); void opcode_CB_6A(); void opcode_CB_6B(); void opcode_CB_6C(); void opcode_CB_6D(); void opcode_CB_6E(); void opcode_CB_6F();
|
||||
void opcode_CB_70(); void opcode_CB_71(); void opcode_CB_72(); void opcode_CB_73(); void opcode_CB_74(); void opcode_CB_75(); void opcode_CB_76(); void opcode_CB_77(); void opcode_CB_78(); void opcode_CB_79(); void opcode_CB_7A(); void opcode_CB_7B(); void opcode_CB_7C(); void opcode_CB_7D(); void opcode_CB_7E(); void opcode_CB_7F();
|
||||
void opcode_CB_80(); void opcode_CB_81(); void opcode_CB_82(); void opcode_CB_83(); void opcode_CB_84(); void opcode_CB_85(); void opcode_CB_86(); void opcode_CB_87(); void opcode_CB_88(); void opcode_CB_89(); void opcode_CB_8A(); void opcode_CB_8B(); void opcode_CB_8C(); void opcode_CB_8D(); void opcode_CB_8E(); void opcode_CB_8F();
|
||||
void opcode_CB_90(); void opcode_CB_91(); void opcode_CB_92(); void opcode_CB_93(); void opcode_CB_94(); void opcode_CB_95(); void opcode_CB_96(); void opcode_CB_97(); void opcode_CB_98(); void opcode_CB_99(); void opcode_CB_9A(); void opcode_CB_9B(); void opcode_CB_9C(); void opcode_CB_9D(); void opcode_CB_9E(); void opcode_CB_9F();
|
||||
void opcode_CB_A0(); void opcode_CB_A1(); void opcode_CB_A2(); void opcode_CB_A3(); void opcode_CB_A4(); void opcode_CB_A5(); void opcode_CB_A6(); void opcode_CB_A7(); void opcode_CB_A8(); void opcode_CB_A9(); void opcode_CB_AA(); void opcode_CB_AB(); void opcode_CB_AC(); void opcode_CB_AD(); void opcode_CB_AE(); void opcode_CB_AF();
|
||||
void opcode_CB_B0(); void opcode_CB_B1(); void opcode_CB_B2(); void opcode_CB_B3(); void opcode_CB_B4(); void opcode_CB_B5(); void opcode_CB_B6(); void opcode_CB_B7(); void opcode_CB_B8(); void opcode_CB_B9(); void opcode_CB_BA(); void opcode_CB_BB(); void opcode_CB_BC(); void opcode_CB_BD(); void opcode_CB_BE(); void opcode_CB_BF();
|
||||
void opcode_CB_C0(); void opcode_CB_C1(); void opcode_CB_C2(); void opcode_CB_C3(); void opcode_CB_C4(); void opcode_CB_C5(); void opcode_CB_C6(); void opcode_CB_C7(); void opcode_CB_C8(); void opcode_CB_C9(); void opcode_CB_CA(); void opcode_CB_CB(); void opcode_CB_CC(); void opcode_CB_CD(); void opcode_CB_CE(); void opcode_CB_CF();
|
||||
void opcode_CB_D0(); void opcode_CB_D1(); void opcode_CB_D2(); void opcode_CB_D3(); void opcode_CB_D4(); void opcode_CB_D5(); void opcode_CB_D6(); void opcode_CB_D7(); void opcode_CB_D8(); void opcode_CB_D9(); void opcode_CB_DA(); void opcode_CB_DB(); void opcode_CB_DC(); void opcode_CB_DD(); void opcode_CB_DE(); void opcode_CB_DF();
|
||||
void opcode_CB_E0(); void opcode_CB_E1(); void opcode_CB_E2(); void opcode_CB_E3(); void opcode_CB_E4(); void opcode_CB_E5(); void opcode_CB_E6(); void opcode_CB_E7(); void opcode_CB_E8(); void opcode_CB_E9(); void opcode_CB_EA(); void opcode_CB_EB(); void opcode_CB_EC(); void opcode_CB_ED(); void opcode_CB_EE(); void opcode_CB_EF();
|
||||
void opcode_CB_F0(); void opcode_CB_F1(); void opcode_CB_F2(); void opcode_CB_F3(); void opcode_CB_F4(); void opcode_CB_F5(); void opcode_CB_F6(); void opcode_CB_F7(); void opcode_CB_F8(); void opcode_CB_F9(); void opcode_CB_FA(); void opcode_CB_FB(); void opcode_CB_FC(); void opcode_CB_FD(); void opcode_CB_FE(); void opcode_CB_FF();
|
||||
/* clang-format on */
|
||||
|
||||
friend class Debugger;
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
using uint = unsigned int;
|
||||
|
||||
using u8 = uint8_t;
|
||||
using u16 = uint16_t;
|
||||
using u32 = uint32_t;
|
||||
using s8 = int8_t;
|
||||
using s16 = uint16_t; /* kept as upstream (unused alias) */
|
||||
|
||||
struct Noncopyable {
|
||||
auto operator=(const Noncopyable&) -> Noncopyable& = delete;
|
||||
Noncopyable(const Noncopyable&) = delete;
|
||||
Noncopyable() = default;
|
||||
~Noncopyable() = default;
|
||||
};
|
||||
|
||||
template <typename... T> inline void unused(T&&...) {}
|
||||
|
||||
/* Logging compiled out for the embedded target */
|
||||
#define log_error(...) ((void)0)
|
||||
#define log_warn(...) ((void)0)
|
||||
#define log_info(...) ((void)0)
|
||||
#define log_debug(...) ((void)0)
|
||||
#define log_trace(...) ((void)0)
|
||||
#define log_unimplemented(...) ((void)0)
|
||||
|
||||
/* Fatal errors: platform provides the handler (never returns) */
|
||||
extern "C" [[noreturn]] void gb_fatal(const char* msg);
|
||||
#define fatal_error(...) gb_fatal("gb core fatal error")
|
||||
|
||||
const uint GAMEBOY_WIDTH = 160;
|
||||
const uint GAMEBOY_HEIGHT = 144;
|
||||
|
||||
const int CLOCK_RATE = 4194304;
|
||||
|
||||
/* Shades are plain bytes now: 0=White .. 3=Black */
|
||||
using Shade = u8;
|
||||
const Shade SHADE_WHITE = 0;
|
||||
const Shade SHADE_LIGHT = 1;
|
||||
const Shade SHADE_DARK = 2;
|
||||
const Shade SHADE_BLACK = 3;
|
||||
|
||||
struct Palette {
|
||||
Shade color0 = 0;
|
||||
Shade color1 = 1;
|
||||
Shade color2 = 2;
|
||||
Shade color3 = 3;
|
||||
};
|
||||
|
||||
class Cycles {
|
||||
public:
|
||||
Cycles(uint nCycles) : cycles(nCycles) {}
|
||||
const uint cycles;
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "definitions.h"
|
||||
|
||||
/* Packed 2 bits-per-pixel framebuffer (the original stored one 4-byte enum
|
||||
* per pixel = 92 KB).
|
||||
*
|
||||
* GB_FB_ROWS storage rows are kept (default: all 144 = 5.7 KB). The Flipper
|
||||
* build compiles with GB_FB_ROWS=64: only the 64 scanlines that survive the
|
||||
* 144 -> 64 downscale are stored (2.5 KB, saving 3.2 KB of always-resident
|
||||
* RAM). row_slot[] maps screen y -> storage slot; rows without a slot are
|
||||
* write-ignored / read-as-white, and the render row mask already prevents
|
||||
* the PPU from touching them anyway. */
|
||||
#ifndef GB_FB_ROWS
|
||||
#define GB_FB_ROWS GAMEBOY_HEIGHT
|
||||
#endif
|
||||
|
||||
class FrameBuffer {
|
||||
public:
|
||||
FrameBuffer() {
|
||||
for(uint y = 0; y < GAMEBOY_HEIGHT; y++)
|
||||
row_slot[y] = (y < GB_FB_ROWS) ? static_cast<u8>(y) : NO_ROW;
|
||||
}
|
||||
|
||||
/* Compact storage to exactly the rows enabled in mask. Storage slots
|
||||
* are assigned in ascending y order, so the frontend's n-th displayed
|
||||
* row lives in slot n. No-op when every row fits (host build). */
|
||||
void set_row_map(const u8* mask) {
|
||||
if(!mask || GB_FB_ROWS >= GAMEBOY_HEIGHT) return;
|
||||
uint slot = 0;
|
||||
for(uint y = 0; y < GAMEBOY_HEIGHT; y++) {
|
||||
if(mask[y] && slot < GB_FB_ROWS)
|
||||
row_slot[y] = static_cast<u8>(slot++);
|
||||
else
|
||||
row_slot[y] = NO_ROW;
|
||||
}
|
||||
}
|
||||
|
||||
void set_pixel(uint x, uint y, Shade shade) {
|
||||
uint slot = row_slot[y];
|
||||
if(slot == NO_ROW) return;
|
||||
uint i = slot * GAMEBOY_WIDTH + x;
|
||||
uint byte = i >> 2;
|
||||
uint shift = (i & 3) * 2;
|
||||
buf[byte] = static_cast<u8>((buf[byte] & ~(0x3 << shift)) | (shade << shift));
|
||||
}
|
||||
|
||||
auto get_pixel(uint x, uint y) const -> Shade {
|
||||
uint slot = row_slot[y];
|
||||
if(slot == NO_ROW) return 0;
|
||||
uint i = slot * GAMEBOY_WIDTH + x;
|
||||
return static_cast<Shade>((buf[i >> 2] >> ((i & 3) * 2)) & 0x3);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
for(uint i = 0; i < sizeof(buf); i++)
|
||||
buf[i] = 0;
|
||||
}
|
||||
|
||||
/* Raw packed 2bpp storage (4 pixels per byte, LSB first), slot-major:
|
||||
* storage slot s starts at bit offset s * GAMEBOY_WIDTH * 2. */
|
||||
auto raw() const -> const u8* { return buf; }
|
||||
|
||||
private:
|
||||
static const u8 NO_ROW = 0xFF;
|
||||
|
||||
u8 buf[GAMEBOY_WIDTH * GB_FB_ROWS / 4] = {};
|
||||
u8 row_slot[GAMEBOY_HEIGHT];
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "gameboy.h"
|
||||
|
||||
Gameboy::Gameboy(
|
||||
const u8* bank0,
|
||||
uint rom_bank_count,
|
||||
MBCType mbc,
|
||||
u8* cart_ram,
|
||||
u32 cart_ram_size,
|
||||
RomBankProvider provider,
|
||||
void* provider_ctx)
|
||||
: cpu(*this)
|
||||
, video(*this)
|
||||
, mmu(*this)
|
||||
, timer(*this) {
|
||||
cartridge.init(bank0, rom_bank_count, mbc, cart_ram, cart_ram_size, provider, provider_ctx);
|
||||
|
||||
video.register_vblank_callback(&Gameboy::vblank_trampoline, this);
|
||||
|
||||
/* The DMG boot ROM is not shipped with this emulator. The CPU and IO
|
||||
* registers are initialised directly to the well-documented post-boot
|
||||
* state instead. */
|
||||
cpu.init_post_boot();
|
||||
|
||||
mmu.write(0xFF26, 0x80); /* NR52: APU powered on (post-boot state) */
|
||||
mmu.write(0xFF25, 0xF3); /* NR51: all channels routed */
|
||||
mmu.write(0xFF24, 0x77); /* NR50: max master volume */
|
||||
mmu.write(0xFF40, 0x91); /* LCDC */
|
||||
mmu.write(0xFF42, 0x00); /* SCY */
|
||||
mmu.write(0xFF43, 0x00); /* SCX */
|
||||
mmu.write(0xFF45, 0x00); /* LYC */
|
||||
mmu.write(0xFF47, 0xFC); /* BGP */
|
||||
mmu.write(0xFF48, 0xFF); /* OBP0 */
|
||||
mmu.write(0xFF49, 0xFF); /* OBP1 */
|
||||
mmu.write(0xFF4A, 0x00); /* WY */
|
||||
mmu.write(0xFF4B, 0x00); /* WX */
|
||||
}
|
||||
|
||||
void Gameboy::vblank_trampoline(void* ctx) {
|
||||
Gameboy* self = static_cast<Gameboy*>(ctx);
|
||||
if(self->user_frame_cb) self->user_frame_cb(self->user_frame_ctx);
|
||||
self->frame_done = true;
|
||||
}
|
||||
|
||||
void Gameboy::button_pressed(GbButton button) {
|
||||
input.button_pressed(button);
|
||||
/* Request the joypad interrupt (missing upstream); mainly wakes
|
||||
* games waiting in HALT/STOP for input. */
|
||||
cpu.interrupt_flag.set_bit_to(4, true);
|
||||
}
|
||||
|
||||
void Gameboy::button_released(GbButton button) {
|
||||
input.button_released(button);
|
||||
}
|
||||
|
||||
void Gameboy::run_to_vblank() {
|
||||
frame_done = false;
|
||||
while(!frame_done) {
|
||||
tick();
|
||||
}
|
||||
}
|
||||
|
||||
void Gameboy::tick() {
|
||||
auto cycles = cpu.tick();
|
||||
video.tick(cycles);
|
||||
timer.tick(cycles.cycles);
|
||||
apu.tick(cycles.cycles);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "input.h"
|
||||
#include "cpu.h"
|
||||
#include "video.h"
|
||||
#include "timer.h"
|
||||
#include "mmu.h"
|
||||
#include "cartridge.h"
|
||||
#include "apu.h"
|
||||
|
||||
class Gameboy {
|
||||
public:
|
||||
/* bank0: pointer to the first 16 KB of ROM (stays resident).
|
||||
* provider: returns pointers to 16 KB switchable banks. */
|
||||
Gameboy(
|
||||
const u8* bank0,
|
||||
uint rom_bank_count,
|
||||
MBCType mbc,
|
||||
u8* cart_ram,
|
||||
u32 cart_ram_size,
|
||||
RomBankProvider provider,
|
||||
void* provider_ctx);
|
||||
|
||||
/* Runs the emulator until the next vblank (one full frame). */
|
||||
void run_to_vblank();
|
||||
|
||||
/* Called on every vblank BEFORE the framebuffer is cleared for the next
|
||||
* frame: this is where the frontend must convert/copy the image. */
|
||||
void set_frame_callback(void (*cb)(void*), void* ctx) {
|
||||
user_frame_cb = cb;
|
||||
user_frame_ctx = ctx;
|
||||
}
|
||||
|
||||
void button_pressed(GbButton button);
|
||||
void button_released(GbButton button);
|
||||
|
||||
void set_skip_render(bool skip) { video.skip_render = skip; }
|
||||
|
||||
/* Optional display-line mask (see Video::row_mask). The array must stay
|
||||
* valid for the lifetime of the emulator. */
|
||||
void set_row_mask(const u8* mask) { video.set_row_mask(mask); }
|
||||
auto get_framebuffer() const -> const FrameBuffer& { return video.get_framebuffer(); }
|
||||
|
||||
auto get_cartridge_ram() -> u8* { return cartridge.get_ram(); }
|
||||
auto get_cartridge_ram_size() const -> u32 { return cartridge.get_ram_size(); }
|
||||
|
||||
Cartridge cartridge;
|
||||
|
||||
CPU cpu;
|
||||
friend class CPU;
|
||||
|
||||
Video video;
|
||||
friend class Video;
|
||||
|
||||
MMU mmu;
|
||||
friend class MMU;
|
||||
|
||||
Timer timer;
|
||||
friend class Timer;
|
||||
|
||||
Apu apu;
|
||||
|
||||
Input input;
|
||||
|
||||
private:
|
||||
void tick();
|
||||
|
||||
static void vblank_trampoline(void* ctx);
|
||||
volatile bool frame_done = false;
|
||||
|
||||
void (*user_frame_cb)(void*) = nullptr;
|
||||
void* user_frame_ctx = nullptr;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "input.h"
|
||||
|
||||
#include "bitwise.h"
|
||||
|
||||
void Input::button_pressed(GbButton button) {
|
||||
set_button(button, true);
|
||||
}
|
||||
|
||||
void Input::button_released(GbButton button) {
|
||||
set_button(button, false);
|
||||
}
|
||||
|
||||
void Input::set_button(GbButton button, bool set) {
|
||||
if (button == GbButton::Up) { up = set; }
|
||||
if (button == GbButton::Down) { down = set; }
|
||||
if (button == GbButton::Left) { left = set; }
|
||||
if (button == GbButton::Right) { right = set; }
|
||||
if (button == GbButton::A) { a = set; }
|
||||
if (button == GbButton::B) { b = set; }
|
||||
if (button == GbButton::Select) { select = set; }
|
||||
if (button == GbButton::Start) { start = set; }
|
||||
}
|
||||
|
||||
void Input::write(u8 set) {
|
||||
using bitwise::check_bit;
|
||||
|
||||
direction_switch = !check_bit(set, 4);
|
||||
button_switch = !check_bit(set, 5);
|
||||
}
|
||||
|
||||
auto Input::get_input() const -> u8 {
|
||||
using bitwise::set_bit_to;
|
||||
|
||||
u8 buttons = 0b1111;
|
||||
|
||||
if (direction_switch) {
|
||||
buttons = set_bit_to(buttons, 0, !right);
|
||||
buttons = set_bit_to(buttons, 1, !left);
|
||||
buttons = set_bit_to(buttons, 2, !up);
|
||||
buttons = set_bit_to(buttons, 3, !down);
|
||||
}
|
||||
|
||||
if (button_switch) {
|
||||
buttons = set_bit_to(buttons, 0, !a);
|
||||
buttons = set_bit_to(buttons, 1, !b);
|
||||
buttons = set_bit_to(buttons, 2, !select);
|
||||
buttons = set_bit_to(buttons, 3, !start);
|
||||
}
|
||||
|
||||
buttons = set_bit_to(buttons, 4, !direction_switch);
|
||||
buttons = set_bit_to(buttons, 5, !button_switch);
|
||||
|
||||
return buttons;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "definitions.h"
|
||||
|
||||
enum class GbButton {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
A,
|
||||
B,
|
||||
Select,
|
||||
Start,
|
||||
};
|
||||
|
||||
class Input {
|
||||
public:
|
||||
void button_pressed(GbButton button);
|
||||
void button_released(GbButton button);
|
||||
void write(u8 set);
|
||||
|
||||
auto get_input() const -> u8;
|
||||
|
||||
private:
|
||||
void set_button(GbButton button, bool set);
|
||||
|
||||
bool up = false;
|
||||
bool down = false;
|
||||
bool left = false;
|
||||
bool right = false;
|
||||
bool a = false;
|
||||
bool b = false;
|
||||
bool select = false;
|
||||
bool start = false;
|
||||
|
||||
bool button_switch = false;
|
||||
bool direction_switch = false;
|
||||
};
|
||||
@@ -0,0 +1,274 @@
|
||||
#include "mmu.h"
|
||||
#include "gameboy.h"
|
||||
#include "input.h"
|
||||
#include "timer.h"
|
||||
#include "cpu.h"
|
||||
#include "video.h"
|
||||
|
||||
void (*gb_serial_hook)(u8 byte) = nullptr;
|
||||
|
||||
MMU::MMU(Gameboy& inGb)
|
||||
: gb(inGb) {
|
||||
}
|
||||
|
||||
auto MMU::read(const Address& address) const -> u8 {
|
||||
u16 a = address.value();
|
||||
|
||||
/* Cartridge ROM (boot ROM is skipped: CPU starts with post-boot state) */
|
||||
if(a < 0x8000) return gb.cartridge.read(a);
|
||||
|
||||
/* VRAM */
|
||||
if(a < 0xA000) return gb.video.vram_read(static_cast<u16>(a - 0x8000));
|
||||
|
||||
/* External (cartridge) RAM */
|
||||
if(a < 0xC000) return gb.cartridge.read(a);
|
||||
|
||||
/* Internal work RAM */
|
||||
if(a < 0xE000) return work_ram[a - 0xC000];
|
||||
|
||||
/* Echo RAM */
|
||||
if(a < 0xFE00) return work_ram[a - 0xE000];
|
||||
|
||||
/* OAM */
|
||||
if(a < 0xFEA0) return oam_ram[a - 0xFE00];
|
||||
|
||||
/* Unusable region */
|
||||
if(a < 0xFF00) return 0xFF;
|
||||
|
||||
/* Mapped IO */
|
||||
if(a < 0xFF80) return read_io(address);
|
||||
|
||||
/* Zero page RAM */
|
||||
if(a < 0xFFFF) return high_ram[a - 0xFF80];
|
||||
|
||||
/* Interrupt enable register */
|
||||
return gb.cpu.interrupt_enabled.value();
|
||||
}
|
||||
|
||||
void MMU::write(const Address& address, u8 byte) {
|
||||
u16 a = address.value();
|
||||
|
||||
if(a < 0x8000) {
|
||||
gb.cartridge.write(a, byte);
|
||||
return;
|
||||
}
|
||||
|
||||
if(a < 0xA000) {
|
||||
gb.video.vram_write(static_cast<u16>(a - 0x8000), byte);
|
||||
return;
|
||||
}
|
||||
|
||||
if(a < 0xC000) {
|
||||
gb.cartridge.write(a, byte);
|
||||
return;
|
||||
}
|
||||
|
||||
if(a < 0xE000) {
|
||||
work_ram[a - 0xC000] = byte;
|
||||
return;
|
||||
}
|
||||
|
||||
if(a < 0xFE00) {
|
||||
work_ram[a - 0xE000] = byte;
|
||||
return;
|
||||
}
|
||||
|
||||
if(a < 0xFEA0) {
|
||||
oam_ram[a - 0xFE00] = byte;
|
||||
return;
|
||||
}
|
||||
|
||||
if(a < 0xFF00) return; /* unusable */
|
||||
|
||||
if(a < 0xFF80) {
|
||||
write_io(address, byte);
|
||||
return;
|
||||
}
|
||||
|
||||
if(a < 0xFFFF) {
|
||||
high_ram[a - 0xFF80] = byte;
|
||||
return;
|
||||
}
|
||||
|
||||
gb.cpu.interrupt_enabled.set(byte);
|
||||
}
|
||||
|
||||
auto MMU::read_io(const Address& address) const -> u8 {
|
||||
u16 a = address.value();
|
||||
|
||||
/* Sound registers + wave RAM */
|
||||
if(a >= 0xFF10 && a <= 0xFF3F) return gb.apu.read(a);
|
||||
|
||||
switch(address.value()) {
|
||||
case 0xFF00:
|
||||
return gb.input.get_input();
|
||||
|
||||
case 0xFF01:
|
||||
return serial_data;
|
||||
|
||||
case 0xFF02:
|
||||
return 0xFF;
|
||||
|
||||
case 0xFF04:
|
||||
return gb.timer.get_divider();
|
||||
|
||||
case 0xFF05:
|
||||
return gb.timer.get_timer();
|
||||
|
||||
case 0xFF06:
|
||||
return gb.timer.get_timer_modulo();
|
||||
|
||||
case 0xFF07:
|
||||
return gb.timer.get_timer_control();
|
||||
|
||||
case 0xFF0F:
|
||||
return gb.cpu.interrupt_flag.value();
|
||||
|
||||
case 0xFF40:
|
||||
return gb.video.control_byte;
|
||||
|
||||
case 0xFF41:
|
||||
return gb.video.lcd_status.value();
|
||||
|
||||
case 0xFF42:
|
||||
return gb.video.scroll_y.value();
|
||||
|
||||
case 0xFF43:
|
||||
return gb.video.scroll_x.value();
|
||||
|
||||
case 0xFF44:
|
||||
return gb.video.line.value();
|
||||
|
||||
case 0xFF45:
|
||||
return gb.video.ly_compare.value();
|
||||
|
||||
case 0xFF47:
|
||||
return gb.video.bg_palette.value();
|
||||
|
||||
case 0xFF48:
|
||||
return gb.video.sprite_palette_0.value();
|
||||
|
||||
case 0xFF49:
|
||||
return gb.video.sprite_palette_1.value();
|
||||
|
||||
case 0xFF4A:
|
||||
return gb.video.window_y.value();
|
||||
|
||||
case 0xFF4B:
|
||||
return gb.video.window_x.value();
|
||||
|
||||
case 0xFF4D: /* CGB speed switch: report normal speed */
|
||||
return 0x00;
|
||||
|
||||
default:
|
||||
/* Audio registers, CGB registers and unmapped IO */
|
||||
return 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
void MMU::write_io(const Address& address, u8 byte) {
|
||||
u16 a = address.value();
|
||||
|
||||
/* Sound registers + wave RAM */
|
||||
if(a >= 0xFF10 && a <= 0xFF3F) {
|
||||
gb.apu.write(a, byte);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(address.value()) {
|
||||
case 0xFF00:
|
||||
gb.input.write(byte);
|
||||
return;
|
||||
|
||||
case 0xFF01:
|
||||
serial_data = byte;
|
||||
return;
|
||||
|
||||
case 0xFF02:
|
||||
/* Serial control: transfer start with internal clock -> deliver
|
||||
* the byte immediately (enough for link-less games and for the
|
||||
* Blargg test ROMs which print through the serial port) */
|
||||
if((byte & 0x81) == 0x81 && gb_serial_hook) gb_serial_hook(serial_data);
|
||||
return;
|
||||
|
||||
case 0xFF04:
|
||||
gb.timer.reset_divider();
|
||||
return;
|
||||
|
||||
case 0xFF05:
|
||||
gb.timer.set_timer(byte);
|
||||
return;
|
||||
|
||||
case 0xFF06:
|
||||
gb.timer.set_timer_modulo(byte);
|
||||
return;
|
||||
|
||||
case 0xFF07:
|
||||
gb.timer.set_timer_control(byte);
|
||||
return;
|
||||
|
||||
case 0xFF0F:
|
||||
gb.cpu.interrupt_flag.set(byte);
|
||||
return;
|
||||
|
||||
case 0xFF40:
|
||||
gb.video.control_byte = byte;
|
||||
return;
|
||||
|
||||
case 0xFF41:
|
||||
gb.video.lcd_status.set(byte);
|
||||
return;
|
||||
|
||||
case 0xFF42:
|
||||
gb.video.scroll_y.set(byte);
|
||||
return;
|
||||
|
||||
case 0xFF43:
|
||||
gb.video.scroll_x.set(byte);
|
||||
return;
|
||||
|
||||
case 0xFF44:
|
||||
gb.video.line.set(0x0);
|
||||
return;
|
||||
|
||||
case 0xFF45:
|
||||
gb.video.ly_compare.set(byte);
|
||||
return;
|
||||
|
||||
case 0xFF46:
|
||||
dma_transfer(byte);
|
||||
return;
|
||||
|
||||
case 0xFF47:
|
||||
gb.video.bg_palette.set(byte);
|
||||
return;
|
||||
|
||||
case 0xFF48:
|
||||
gb.video.sprite_palette_0.set(byte);
|
||||
return;
|
||||
|
||||
case 0xFF49:
|
||||
gb.video.sprite_palette_1.set(byte);
|
||||
return;
|
||||
|
||||
case 0xFF4A:
|
||||
gb.video.window_y.set(byte);
|
||||
return;
|
||||
|
||||
case 0xFF4B:
|
||||
gb.video.window_x.set(byte);
|
||||
return;
|
||||
|
||||
default:
|
||||
/* Audio registers, CGB registers and unmapped IO: ignored */
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MMU::dma_transfer(u8 byte) {
|
||||
u16 start_address = static_cast<u16>(byte) * 0x100;
|
||||
|
||||
for(u8 i = 0x0; i <= 0x9F; i++) {
|
||||
oam_ram[i] = read(static_cast<u16>(start_address + i));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "address.h"
|
||||
#include "definitions.h"
|
||||
|
||||
class Gameboy;
|
||||
|
||||
/* Serial output hook, used by the host test harness to capture Blargg test
|
||||
* ROM output. Null on the Flipper build. */
|
||||
extern "C" {
|
||||
extern void (*gb_serial_hook)(u8 byte);
|
||||
}
|
||||
|
||||
class MMU {
|
||||
public:
|
||||
MMU(Gameboy& inGb);
|
||||
|
||||
auto read(const Address& address) const -> u8;
|
||||
void write(const Address& address, u8 byte);
|
||||
|
||||
private:
|
||||
auto read_io(const Address& address) const -> u8;
|
||||
void write_io(const Address& address, u8 byte);
|
||||
void dma_transfer(u8 byte);
|
||||
|
||||
Gameboy& gb;
|
||||
|
||||
u8 work_ram[0x2000] = {}; /* DMG: 8 KB (was 32 KB upstream) */
|
||||
u8 oam_ram[0xA0] = {};
|
||||
u8 high_ram[0x80] = {};
|
||||
|
||||
u8 serial_data = 0;
|
||||
|
||||
friend class Video;
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
/* clang-format off */
|
||||
|
||||
#include <array>
|
||||
|
||||
const std::array<u8, 256> opcode_cycles = {
|
||||
1, 3, 2, 2, 1, 1, 2, 1, 5, 2, 2, 2, 1, 1, 2, 1,
|
||||
1, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
|
||||
2, 3, 2, 2, 1, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 1,
|
||||
2, 3, 2, 2, 3, 3, 3, 1, 2, 2, 2, 2, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
2, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
2, 3, 3, 4, 3, 4, 2, 4, 2, 4, 3, 0, 3, 6, 2, 4,
|
||||
2, 3, 3, 0, 3, 4, 2, 4, 2, 4, 3, 0, 3, 0, 2, 4,
|
||||
3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4,
|
||||
3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4
|
||||
};
|
||||
|
||||
const std::array<u8, 256> opcode_cycles_branched = {
|
||||
1, 3, 2, 2, 1, 1, 2, 1, 5, 2, 2, 2, 1, 1, 2, 1,
|
||||
1, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
|
||||
3, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
|
||||
3, 3, 2, 2, 3, 3, 3, 1, 3, 2, 2, 2, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
2, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
5, 3, 4, 4, 6, 4, 2, 4, 5, 4, 4, 0, 6, 6, 2, 4,
|
||||
5, 3, 4, 0, 6, 4, 2, 4, 5, 4, 4, 0, 6, 0, 2, 4,
|
||||
3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4,
|
||||
3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4
|
||||
};
|
||||
|
||||
const std::array<u8, 256> opcode_cycles_cb = {
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
|
||||
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
|
||||
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
|
||||
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
|
||||
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2
|
||||
};
|
||||
@@ -0,0 +1,526 @@
|
||||
#include "cpu.h"
|
||||
/* clang-format off */
|
||||
|
||||
/**
|
||||
* This section contains functions which map to actual opcodes which are executed
|
||||
* by the Gameboy's processor.
|
||||
*/
|
||||
|
||||
void CPU::opcode_00() { opcode_nop(); }
|
||||
void CPU::opcode_01() { opcode_ld(bc); }
|
||||
void CPU::opcode_02() { opcode_ld(Address(bc), a); }
|
||||
void CPU::opcode_03() { opcode_inc(bc); }
|
||||
void CPU::opcode_04() { opcode_inc(b); }
|
||||
void CPU::opcode_05() { opcode_dec(b); }
|
||||
void CPU::opcode_06() { opcode_ld(b); }
|
||||
void CPU::opcode_07() { opcode_rlca(); }
|
||||
void CPU::opcode_08() { u16 nn = get_word_from_pc(); opcode_ld(Address(nn), sp); }
|
||||
void CPU::opcode_09() { opcode_add_hl(bc); }
|
||||
void CPU::opcode_0A() { opcode_ld(a, Address(bc)); }
|
||||
void CPU::opcode_0B() { opcode_dec(bc); }
|
||||
void CPU::opcode_0C() { opcode_inc(c); }
|
||||
void CPU::opcode_0D() { opcode_dec(c); }
|
||||
void CPU::opcode_0E() { opcode_ld(c); }
|
||||
void CPU::opcode_0F() { opcode_rrca(); }
|
||||
void CPU::opcode_10() { opcode_stop(); }
|
||||
void CPU::opcode_11() { opcode_ld(de); }
|
||||
void CPU::opcode_12() { opcode_ld(Address(de), a); }
|
||||
void CPU::opcode_13() { opcode_inc(de); }
|
||||
void CPU::opcode_14() { opcode_inc(d); }
|
||||
void CPU::opcode_15() { opcode_dec(d); }
|
||||
void CPU::opcode_16() { opcode_ld(d); }
|
||||
void CPU::opcode_17() { opcode_rla(); }
|
||||
void CPU::opcode_18() { opcode_jr(); }
|
||||
void CPU::opcode_19() { opcode_add_hl(de); }
|
||||
void CPU::opcode_1A() { opcode_ld(a, Address(de)); }
|
||||
void CPU::opcode_1B() { opcode_dec(de); }
|
||||
void CPU::opcode_1C() { opcode_inc(e); }
|
||||
void CPU::opcode_1D() { opcode_dec(e); }
|
||||
void CPU::opcode_1E() { opcode_ld(e); }
|
||||
void CPU::opcode_1F() { opcode_rra(); }
|
||||
void CPU::opcode_20() { opcode_jr(Condition::NZ); }
|
||||
void CPU::opcode_21() { opcode_ld(hl); }
|
||||
void CPU::opcode_22() { opcode_ldi(Address(hl), a); }
|
||||
void CPU::opcode_23() { opcode_inc(hl); }
|
||||
void CPU::opcode_24() { opcode_inc(h); }
|
||||
void CPU::opcode_25() { opcode_dec(h); }
|
||||
void CPU::opcode_26() { opcode_ld(h); }
|
||||
void CPU::opcode_27() { opcode_daa(); }
|
||||
void CPU::opcode_28() { opcode_jr(Condition::Z); }
|
||||
void CPU::opcode_29() { opcode_add_hl(hl); }
|
||||
void CPU::opcode_2A() { opcode_ldi(a, Address(hl)); }
|
||||
void CPU::opcode_2B() { opcode_dec(hl); }
|
||||
void CPU::opcode_2C() { opcode_inc(l); }
|
||||
void CPU::opcode_2D() { opcode_dec(l); }
|
||||
void CPU::opcode_2E() { opcode_ld(l); }
|
||||
void CPU::opcode_2F() { opcode_cpl(); }
|
||||
void CPU::opcode_30() { opcode_jr(Condition::NC); }
|
||||
void CPU::opcode_31() { opcode_ld(sp); }
|
||||
void CPU::opcode_32() { opcode_ldd(Address(hl), a); }
|
||||
void CPU::opcode_33() { opcode_inc(sp); }
|
||||
void CPU::opcode_34() { opcode_inc(Address(hl)); }
|
||||
void CPU::opcode_35() { opcode_dec(Address(hl)); }
|
||||
void CPU::opcode_36() { opcode_ld(Address(hl)); }
|
||||
void CPU::opcode_37() { opcode_scf(); }
|
||||
void CPU::opcode_38() { opcode_jr(Condition::C); }
|
||||
void CPU::opcode_39() { opcode_add_hl(sp); }
|
||||
void CPU::opcode_3A() { opcode_ldd(a, Address(hl)); }
|
||||
void CPU::opcode_3B() { opcode_dec(sp); }
|
||||
void CPU::opcode_3C() { opcode_inc(a); }
|
||||
void CPU::opcode_3D() { opcode_dec(a); }
|
||||
void CPU::opcode_3E() { opcode_ld(a); }
|
||||
void CPU::opcode_3F() { opcode_ccf(); }
|
||||
void CPU::opcode_40() { opcode_ld(b, b); }
|
||||
void CPU::opcode_41() { opcode_ld(b, c); }
|
||||
void CPU::opcode_42() { opcode_ld(b, d); }
|
||||
void CPU::opcode_43() { opcode_ld(b, e); }
|
||||
void CPU::opcode_44() { opcode_ld(b, h); }
|
||||
void CPU::opcode_45() { opcode_ld(b, l); }
|
||||
void CPU::opcode_46() { opcode_ld(b, Address(hl)); }
|
||||
void CPU::opcode_47() { opcode_ld(b, a); }
|
||||
void CPU::opcode_48() { opcode_ld(c, b); }
|
||||
void CPU::opcode_49() { opcode_ld(c, c); }
|
||||
void CPU::opcode_4A() { opcode_ld(c, d); }
|
||||
void CPU::opcode_4B() { opcode_ld(c, e); }
|
||||
void CPU::opcode_4C() { opcode_ld(c, h); }
|
||||
void CPU::opcode_4D() { opcode_ld(c, l); }
|
||||
void CPU::opcode_4E() { opcode_ld(c, Address(hl)); }
|
||||
void CPU::opcode_4F() { opcode_ld(c, a); }
|
||||
void CPU::opcode_50() { opcode_ld(d, b); }
|
||||
void CPU::opcode_51() { opcode_ld(d, c); }
|
||||
void CPU::opcode_52() { opcode_ld(d, d); }
|
||||
void CPU::opcode_53() { opcode_ld(d, e); }
|
||||
void CPU::opcode_54() { opcode_ld(d, h); }
|
||||
void CPU::opcode_55() { opcode_ld(d, l); }
|
||||
void CPU::opcode_56() { opcode_ld(d, Address(hl)); }
|
||||
void CPU::opcode_57() { opcode_ld(d, a); }
|
||||
void CPU::opcode_58() { opcode_ld(e, b); }
|
||||
void CPU::opcode_59() { opcode_ld(e, c); }
|
||||
void CPU::opcode_5A() { opcode_ld(e, d); }
|
||||
void CPU::opcode_5B() { opcode_ld(e, e); }
|
||||
void CPU::opcode_5C() { opcode_ld(e, h); }
|
||||
void CPU::opcode_5D() { opcode_ld(e, l); }
|
||||
void CPU::opcode_5E() { opcode_ld(e, Address(hl)); }
|
||||
void CPU::opcode_5F() { opcode_ld(e, a); }
|
||||
void CPU::opcode_60() { opcode_ld(h, b); }
|
||||
void CPU::opcode_61() { opcode_ld(h, c); }
|
||||
void CPU::opcode_62() { opcode_ld(h, d); }
|
||||
void CPU::opcode_63() { opcode_ld(h, e); }
|
||||
void CPU::opcode_64() { opcode_ld(h, h); }
|
||||
void CPU::opcode_65() { opcode_ld(h, l); }
|
||||
void CPU::opcode_66() { opcode_ld(h, Address(hl)); }
|
||||
void CPU::opcode_67() { opcode_ld(h, a); }
|
||||
void CPU::opcode_68() { opcode_ld(l, b); }
|
||||
void CPU::opcode_69() { opcode_ld(l, c); }
|
||||
void CPU::opcode_6A() { opcode_ld(l, d); }
|
||||
void CPU::opcode_6B() { opcode_ld(l, e); }
|
||||
void CPU::opcode_6C() { opcode_ld(l, h); }
|
||||
void CPU::opcode_6D() { opcode_ld(l, l); }
|
||||
void CPU::opcode_6E() { opcode_ld(l, Address(hl)); }
|
||||
void CPU::opcode_6F() { opcode_ld(l, a); }
|
||||
void CPU::opcode_70() { opcode_ld(Address(hl), b); }
|
||||
void CPU::opcode_71() { opcode_ld(Address(hl), c); }
|
||||
void CPU::opcode_72() { opcode_ld(Address(hl), d); }
|
||||
void CPU::opcode_73() { opcode_ld(Address(hl), e); }
|
||||
void CPU::opcode_74() { opcode_ld(Address(hl), h); }
|
||||
void CPU::opcode_75() { opcode_ld(Address(hl), l); }
|
||||
void CPU::opcode_76() { opcode_halt(); }
|
||||
void CPU::opcode_77() { opcode_ld(Address(hl), a); }
|
||||
void CPU::opcode_78() { opcode_ld(a, b); }
|
||||
void CPU::opcode_79() { opcode_ld(a, c); }
|
||||
void CPU::opcode_7A() { opcode_ld(a, d); }
|
||||
void CPU::opcode_7B() { opcode_ld(a, e); }
|
||||
void CPU::opcode_7C() { opcode_ld(a, h); }
|
||||
void CPU::opcode_7D() { opcode_ld(a, l); }
|
||||
void CPU::opcode_7E() { opcode_ld(a, Address(hl)); }
|
||||
void CPU::opcode_7F() { opcode_ld(a, a); }
|
||||
void CPU::opcode_80() { opcode_add_a(b); }
|
||||
void CPU::opcode_81() { opcode_add_a(c); }
|
||||
void CPU::opcode_82() { opcode_add_a(d); }
|
||||
void CPU::opcode_83() { opcode_add_a(e); }
|
||||
void CPU::opcode_84() { opcode_add_a(h); }
|
||||
void CPU::opcode_85() { opcode_add_a(l); }
|
||||
void CPU::opcode_86() { opcode_add_a(Address(hl)); }
|
||||
void CPU::opcode_87() { opcode_add_a(a); }
|
||||
void CPU::opcode_88() { opcode_adc(b); }
|
||||
void CPU::opcode_89() { opcode_adc(c); }
|
||||
void CPU::opcode_8A() { opcode_adc(d); }
|
||||
void CPU::opcode_8B() { opcode_adc(e); }
|
||||
void CPU::opcode_8C() { opcode_adc(h); }
|
||||
void CPU::opcode_8D() { opcode_adc(l); }
|
||||
void CPU::opcode_8E() { opcode_adc(Address(hl)); }
|
||||
void CPU::opcode_8F() { opcode_adc(a); }
|
||||
void CPU::opcode_90() { opcode_sub(b); }
|
||||
void CPU::opcode_91() { opcode_sub(c); }
|
||||
void CPU::opcode_92() { opcode_sub(d); }
|
||||
void CPU::opcode_93() { opcode_sub(e); }
|
||||
void CPU::opcode_94() { opcode_sub(h); }
|
||||
void CPU::opcode_95() { opcode_sub(l); }
|
||||
void CPU::opcode_96() { opcode_sub(Address(hl)); }
|
||||
void CPU::opcode_97() { opcode_sub(a); }
|
||||
void CPU::opcode_98() { opcode_sbc(b); }
|
||||
void CPU::opcode_99() { opcode_sbc(c); }
|
||||
void CPU::opcode_9A() { opcode_sbc(d); }
|
||||
void CPU::opcode_9B() { opcode_sbc(e); }
|
||||
void CPU::opcode_9C() { opcode_sbc(h); }
|
||||
void CPU::opcode_9D() { opcode_sbc(l); }
|
||||
void CPU::opcode_9E() { opcode_sbc(Address(hl)); }
|
||||
void CPU::opcode_9F() { opcode_sbc(a); }
|
||||
void CPU::opcode_A0() { opcode_and(b); }
|
||||
void CPU::opcode_A1() { opcode_and(c); }
|
||||
void CPU::opcode_A2() { opcode_and(d); }
|
||||
void CPU::opcode_A3() { opcode_and(e); }
|
||||
void CPU::opcode_A4() { opcode_and(h); }
|
||||
void CPU::opcode_A5() { opcode_and(l); }
|
||||
void CPU::opcode_A6() { opcode_and(Address(hl)); }
|
||||
void CPU::opcode_A7() { opcode_and(a); }
|
||||
void CPU::opcode_A8() { opcode_xor(b); }
|
||||
void CPU::opcode_A9() { opcode_xor(c); }
|
||||
void CPU::opcode_AA() { opcode_xor(d); }
|
||||
void CPU::opcode_AB() { opcode_xor(e); }
|
||||
void CPU::opcode_AC() { opcode_xor(h); }
|
||||
void CPU::opcode_AD() { opcode_xor(l); }
|
||||
void CPU::opcode_AE() { opcode_xor(Address(hl)); }
|
||||
void CPU::opcode_AF() { opcode_xor(a); }
|
||||
void CPU::opcode_B0() { opcode_or(b); }
|
||||
void CPU::opcode_B1() { opcode_or(c); }
|
||||
void CPU::opcode_B2() { opcode_or(d); }
|
||||
void CPU::opcode_B3() { opcode_or(e); }
|
||||
void CPU::opcode_B4() { opcode_or(h); }
|
||||
void CPU::opcode_B5() { opcode_or(l); }
|
||||
void CPU::opcode_B6() { opcode_or(Address(hl)); }
|
||||
void CPU::opcode_B7() { opcode_or(a); }
|
||||
void CPU::opcode_B8() { opcode_cp(b); }
|
||||
void CPU::opcode_B9() { opcode_cp(c); }
|
||||
void CPU::opcode_BA() { opcode_cp(d); }
|
||||
void CPU::opcode_BB() { opcode_cp(e); }
|
||||
void CPU::opcode_BC() { opcode_cp(h); }
|
||||
void CPU::opcode_BD() { opcode_cp(l); }
|
||||
void CPU::opcode_BE() { opcode_cp(Address(hl)); }
|
||||
void CPU::opcode_BF() { opcode_cp(a); }
|
||||
void CPU::opcode_C0() { opcode_ret(Condition::NZ); }
|
||||
void CPU::opcode_C1() { opcode_pop(bc); }
|
||||
void CPU::opcode_C2() { opcode_jp(Condition::NZ); }
|
||||
void CPU::opcode_C3() { opcode_jp(); }
|
||||
void CPU::opcode_C4() { opcode_call(Condition::NZ); }
|
||||
void CPU::opcode_C5() { opcode_push(bc); }
|
||||
void CPU::opcode_C6() { opcode_add_a(); }
|
||||
void CPU::opcode_C7() { opcode_rst(rst::rst1); }
|
||||
void CPU::opcode_C8() { opcode_ret(Condition::Z); }
|
||||
void CPU::opcode_C9() { opcode_ret(); }
|
||||
void CPU::opcode_CA() { opcode_jp(Condition::Z); }
|
||||
void CPU::opcode_CB() { /* External Ops */ }
|
||||
void CPU::opcode_CC() { opcode_call(Condition::Z); }
|
||||
void CPU::opcode_CD() { opcode_call(); }
|
||||
void CPU::opcode_CE() { opcode_adc(); }
|
||||
void CPU::opcode_CF() { opcode_rst(rst::rst2); }
|
||||
void CPU::opcode_D0() { opcode_ret(Condition::NC); }
|
||||
void CPU::opcode_D1() { opcode_pop(de); }
|
||||
void CPU::opcode_D2() { opcode_jp(Condition::NC); }
|
||||
void CPU::opcode_D3() { /* Undefined */ }
|
||||
void CPU::opcode_D4() { opcode_call(Condition::NC); }
|
||||
void CPU::opcode_D5() { opcode_push(de); }
|
||||
void CPU::opcode_D6() { opcode_sub(); }
|
||||
void CPU::opcode_D7() { opcode_rst(rst::rst3); }
|
||||
void CPU::opcode_D8() { opcode_ret(Condition::C); }
|
||||
void CPU::opcode_D9() { opcode_reti(); }
|
||||
void CPU::opcode_DA() { opcode_jp(Condition::C); }
|
||||
void CPU::opcode_DB() { /* Undefined */ }
|
||||
void CPU::opcode_DC() { opcode_call(Condition::C); }
|
||||
void CPU::opcode_DD() { /* Undefined */ }
|
||||
void CPU::opcode_DE() { opcode_sbc(); }
|
||||
void CPU::opcode_DF() { opcode_rst(rst::rst4); }
|
||||
void CPU::opcode_E0() { opcode_ldh_into_data(); }
|
||||
void CPU::opcode_E1() { opcode_pop(hl); }
|
||||
void CPU::opcode_E2() { opcode_ldh_into_c(); }
|
||||
void CPU::opcode_E3() { /* Undefined */ }
|
||||
void CPU::opcode_E4() { /* Undefined */ }
|
||||
void CPU::opcode_E5() { opcode_push(hl); }
|
||||
void CPU::opcode_E6() { opcode_and(); }
|
||||
void CPU::opcode_E7() { opcode_rst(rst::rst5); }
|
||||
void CPU::opcode_E8() { opcode_add_sp(); }
|
||||
void CPU::opcode_E9() { opcode_jp(Address(hl)); }
|
||||
void CPU::opcode_EA() { opcode_ld_to_addr(a); }
|
||||
void CPU::opcode_EB() { /* Undefined */ }
|
||||
void CPU::opcode_EC() { /* Undefined */ }
|
||||
void CPU::opcode_ED() { /* Undefined */ }
|
||||
void CPU::opcode_EE() { opcode_xor(); }
|
||||
void CPU::opcode_EF() { opcode_rst(rst::rst6); }
|
||||
void CPU::opcode_F0() { opcode_ldh_into_a(); }
|
||||
void CPU::opcode_F1() { opcode_pop(af); }
|
||||
void CPU::opcode_F2() { opcode_ldh_c_into_a(); }
|
||||
void CPU::opcode_F3() { opcode_di(); }
|
||||
void CPU::opcode_F4() { /* Undefined */ }
|
||||
void CPU::opcode_F5() { opcode_push(af); }
|
||||
void CPU::opcode_F6() { opcode_or(); }
|
||||
void CPU::opcode_F7() { opcode_rst(rst::rst7); }
|
||||
void CPU::opcode_F8() { opcode_ldhl(); }
|
||||
void CPU::opcode_F9() { opcode_ld(sp, hl); }
|
||||
void CPU::opcode_FA() { opcode_ld_from_addr(a); }
|
||||
void CPU::opcode_FB() { opcode_ei(); }
|
||||
void CPU::opcode_FC() { /* Undefined */ }
|
||||
void CPU::opcode_FD() { /* Undefined */ }
|
||||
void CPU::opcode_FE() { opcode_cp(); }
|
||||
void CPU::opcode_FF() { opcode_rst(rst::rst8); }
|
||||
|
||||
/**
|
||||
* This section contains two-byte opcodes, which are triggered when prefixed with
|
||||
* the CB instruction above.
|
||||
*/
|
||||
|
||||
void CPU::opcode_CB_00() { opcode_rlc(b); }
|
||||
void CPU::opcode_CB_01() { opcode_rlc(c); }
|
||||
void CPU::opcode_CB_02() { opcode_rlc(d); }
|
||||
void CPU::opcode_CB_03() { opcode_rlc(e); }
|
||||
void CPU::opcode_CB_04() { opcode_rlc(h); }
|
||||
void CPU::opcode_CB_05() { opcode_rlc(l); }
|
||||
void CPU::opcode_CB_06() { opcode_rlc(Address(hl)); }
|
||||
void CPU::opcode_CB_07() { opcode_rlc(a); }
|
||||
void CPU::opcode_CB_08() { opcode_rrc(b); }
|
||||
void CPU::opcode_CB_09() { opcode_rrc(c); }
|
||||
void CPU::opcode_CB_0A() { opcode_rrc(d); }
|
||||
void CPU::opcode_CB_0B() { opcode_rrc(e); }
|
||||
void CPU::opcode_CB_0C() { opcode_rrc(h); }
|
||||
void CPU::opcode_CB_0D() { opcode_rrc(l); }
|
||||
void CPU::opcode_CB_0E() { opcode_rrc(Address(hl)); }
|
||||
void CPU::opcode_CB_0F() { opcode_rrc(a); }
|
||||
void CPU::opcode_CB_10() { opcode_rl(b); }
|
||||
void CPU::opcode_CB_11() { opcode_rl(c); }
|
||||
void CPU::opcode_CB_12() { opcode_rl(d); }
|
||||
void CPU::opcode_CB_13() { opcode_rl(e); }
|
||||
void CPU::opcode_CB_14() { opcode_rl(h); }
|
||||
void CPU::opcode_CB_15() { opcode_rl(l); }
|
||||
void CPU::opcode_CB_16() { opcode_rl(Address(hl)); }
|
||||
void CPU::opcode_CB_17() { opcode_rl(a); }
|
||||
void CPU::opcode_CB_18() { opcode_rr(b); }
|
||||
void CPU::opcode_CB_19() { opcode_rr(c); }
|
||||
void CPU::opcode_CB_1A() { opcode_rr(d); }
|
||||
void CPU::opcode_CB_1B() { opcode_rr(e); }
|
||||
void CPU::opcode_CB_1C() { opcode_rr(h); }
|
||||
void CPU::opcode_CB_1D() { opcode_rr(l); }
|
||||
void CPU::opcode_CB_1E() { opcode_rr(Address(hl)); }
|
||||
void CPU::opcode_CB_1F() { opcode_rr(a); }
|
||||
void CPU::opcode_CB_20() { opcode_sla(b); }
|
||||
void CPU::opcode_CB_21() { opcode_sla(c); }
|
||||
void CPU::opcode_CB_22() { opcode_sla(d); }
|
||||
void CPU::opcode_CB_23() { opcode_sla(e); }
|
||||
void CPU::opcode_CB_24() { opcode_sla(h); }
|
||||
void CPU::opcode_CB_25() { opcode_sla(l); }
|
||||
void CPU::opcode_CB_26() { opcode_sla(Address(hl)); }
|
||||
void CPU::opcode_CB_27() { opcode_sla(a); }
|
||||
void CPU::opcode_CB_28() { opcode_sra(b); }
|
||||
void CPU::opcode_CB_29() { opcode_sra(c); }
|
||||
void CPU::opcode_CB_2A() { opcode_sra(d); }
|
||||
void CPU::opcode_CB_2B() { opcode_sra(e); }
|
||||
void CPU::opcode_CB_2C() { opcode_sra(h); }
|
||||
void CPU::opcode_CB_2D() { opcode_sra(l); }
|
||||
void CPU::opcode_CB_2E() { opcode_sra(Address(hl)); }
|
||||
void CPU::opcode_CB_2F() { opcode_sra(a); }
|
||||
void CPU::opcode_CB_30() { opcode_swap(b); }
|
||||
void CPU::opcode_CB_31() { opcode_swap(c); }
|
||||
void CPU::opcode_CB_32() { opcode_swap(d); }
|
||||
void CPU::opcode_CB_33() { opcode_swap(e); }
|
||||
void CPU::opcode_CB_34() { opcode_swap(h); }
|
||||
void CPU::opcode_CB_35() { opcode_swap(l); }
|
||||
void CPU::opcode_CB_36() { opcode_swap(Address(hl)); }
|
||||
void CPU::opcode_CB_37() { opcode_swap(a); }
|
||||
void CPU::opcode_CB_38() { opcode_srl(b); }
|
||||
void CPU::opcode_CB_39() { opcode_srl(c); }
|
||||
void CPU::opcode_CB_3A() { opcode_srl(d); }
|
||||
void CPU::opcode_CB_3B() { opcode_srl(e); }
|
||||
void CPU::opcode_CB_3C() { opcode_srl(h); }
|
||||
void CPU::opcode_CB_3D() { opcode_srl(l); }
|
||||
void CPU::opcode_CB_3E() { opcode_srl(Address(hl)); }
|
||||
void CPU::opcode_CB_3F() { opcode_srl(a); }
|
||||
void CPU::opcode_CB_40() { opcode_bit(0, b); }
|
||||
void CPU::opcode_CB_41() { opcode_bit(0, c); }
|
||||
void CPU::opcode_CB_42() { opcode_bit(0, d); }
|
||||
void CPU::opcode_CB_43() { opcode_bit(0, e); }
|
||||
void CPU::opcode_CB_44() { opcode_bit(0, h); }
|
||||
void CPU::opcode_CB_45() { opcode_bit(0, l); }
|
||||
void CPU::opcode_CB_46() { opcode_bit(0, Address(hl)); }
|
||||
void CPU::opcode_CB_47() { opcode_bit(0, a); }
|
||||
void CPU::opcode_CB_48() { opcode_bit(1, b); }
|
||||
void CPU::opcode_CB_49() { opcode_bit(1, c); }
|
||||
void CPU::opcode_CB_4A() { opcode_bit(1, d); }
|
||||
void CPU::opcode_CB_4B() { opcode_bit(1, e); }
|
||||
void CPU::opcode_CB_4C() { opcode_bit(1, h); }
|
||||
void CPU::opcode_CB_4D() { opcode_bit(1, l); }
|
||||
void CPU::opcode_CB_4E() { opcode_bit(1, Address(hl)); }
|
||||
void CPU::opcode_CB_4F() { opcode_bit(1, a); }
|
||||
void CPU::opcode_CB_50() { opcode_bit(2, b); }
|
||||
void CPU::opcode_CB_51() { opcode_bit(2, c); }
|
||||
void CPU::opcode_CB_52() { opcode_bit(2, d); }
|
||||
void CPU::opcode_CB_53() { opcode_bit(2, e); }
|
||||
void CPU::opcode_CB_54() { opcode_bit(2, h); }
|
||||
void CPU::opcode_CB_55() { opcode_bit(2, l); }
|
||||
void CPU::opcode_CB_56() { opcode_bit(2, Address(hl)); }
|
||||
void CPU::opcode_CB_57() { opcode_bit(2, a); }
|
||||
void CPU::opcode_CB_58() { opcode_bit(3, b); }
|
||||
void CPU::opcode_CB_59() { opcode_bit(3, c); }
|
||||
void CPU::opcode_CB_5A() { opcode_bit(3, d); }
|
||||
void CPU::opcode_CB_5B() { opcode_bit(3, e); }
|
||||
void CPU::opcode_CB_5C() { opcode_bit(3, h); }
|
||||
void CPU::opcode_CB_5D() { opcode_bit(3, l); }
|
||||
void CPU::opcode_CB_5E() { opcode_bit(3, Address(hl)); }
|
||||
void CPU::opcode_CB_5F() { opcode_bit(3, a); }
|
||||
void CPU::opcode_CB_60() { opcode_bit(4, b); }
|
||||
void CPU::opcode_CB_61() { opcode_bit(4, c); }
|
||||
void CPU::opcode_CB_62() { opcode_bit(4, d); }
|
||||
void CPU::opcode_CB_63() { opcode_bit(4, e); }
|
||||
void CPU::opcode_CB_64() { opcode_bit(4, h); }
|
||||
void CPU::opcode_CB_65() { opcode_bit(4, l); }
|
||||
void CPU::opcode_CB_66() { opcode_bit(4, Address(hl)); }
|
||||
void CPU::opcode_CB_67() { opcode_bit(4, a); }
|
||||
void CPU::opcode_CB_68() { opcode_bit(5, b); }
|
||||
void CPU::opcode_CB_69() { opcode_bit(5, c); }
|
||||
void CPU::opcode_CB_6A() { opcode_bit(5, d); }
|
||||
void CPU::opcode_CB_6B() { opcode_bit(5, e); }
|
||||
void CPU::opcode_CB_6C() { opcode_bit(5, h); }
|
||||
void CPU::opcode_CB_6D() { opcode_bit(5, l); }
|
||||
void CPU::opcode_CB_6E() { opcode_bit(5, Address(hl)); }
|
||||
void CPU::opcode_CB_6F() { opcode_bit(5, a); }
|
||||
void CPU::opcode_CB_70() { opcode_bit(6, b); }
|
||||
void CPU::opcode_CB_71() { opcode_bit(6, c); }
|
||||
void CPU::opcode_CB_72() { opcode_bit(6, d); }
|
||||
void CPU::opcode_CB_73() { opcode_bit(6, e); }
|
||||
void CPU::opcode_CB_74() { opcode_bit(6, h); }
|
||||
void CPU::opcode_CB_75() { opcode_bit(6, l); }
|
||||
void CPU::opcode_CB_76() { opcode_bit(6, Address(hl)); }
|
||||
void CPU::opcode_CB_77() { opcode_bit(6, a); }
|
||||
void CPU::opcode_CB_78() { opcode_bit(7, b); }
|
||||
void CPU::opcode_CB_79() { opcode_bit(7, c); }
|
||||
void CPU::opcode_CB_7A() { opcode_bit(7, d); }
|
||||
void CPU::opcode_CB_7B() { opcode_bit(7, e); }
|
||||
void CPU::opcode_CB_7C() { opcode_bit(7, h); }
|
||||
void CPU::opcode_CB_7D() { opcode_bit(7, l); }
|
||||
void CPU::opcode_CB_7E() { opcode_bit(7, Address(hl)); }
|
||||
void CPU::opcode_CB_7F() { opcode_bit(7, a); }
|
||||
void CPU::opcode_CB_80() { opcode_res(0, b); }
|
||||
void CPU::opcode_CB_81() { opcode_res(0, c); }
|
||||
void CPU::opcode_CB_82() { opcode_res(0, d); }
|
||||
void CPU::opcode_CB_83() { opcode_res(0, e); }
|
||||
void CPU::opcode_CB_84() { opcode_res(0, h); }
|
||||
void CPU::opcode_CB_85() { opcode_res(0, l); }
|
||||
void CPU::opcode_CB_86() { opcode_res(0, Address(hl)); }
|
||||
void CPU::opcode_CB_87() { opcode_res(0, a); }
|
||||
void CPU::opcode_CB_88() { opcode_res(1, b); }
|
||||
void CPU::opcode_CB_89() { opcode_res(1, c); }
|
||||
void CPU::opcode_CB_8A() { opcode_res(1, d); }
|
||||
void CPU::opcode_CB_8B() { opcode_res(1, e); }
|
||||
void CPU::opcode_CB_8C() { opcode_res(1, h); }
|
||||
void CPU::opcode_CB_8D() { opcode_res(1, l); }
|
||||
void CPU::opcode_CB_8E() { opcode_res(1, Address(hl)); }
|
||||
void CPU::opcode_CB_8F() { opcode_res(1, a); }
|
||||
void CPU::opcode_CB_90() { opcode_res(2, b); }
|
||||
void CPU::opcode_CB_91() { opcode_res(2, c); }
|
||||
void CPU::opcode_CB_92() { opcode_res(2, d); }
|
||||
void CPU::opcode_CB_93() { opcode_res(2, e); }
|
||||
void CPU::opcode_CB_94() { opcode_res(2, h); }
|
||||
void CPU::opcode_CB_95() { opcode_res(2, l); }
|
||||
void CPU::opcode_CB_96() { opcode_res(2, Address(hl)); }
|
||||
void CPU::opcode_CB_97() { opcode_res(2, a); }
|
||||
void CPU::opcode_CB_98() { opcode_res(3, b); }
|
||||
void CPU::opcode_CB_99() { opcode_res(3, c); }
|
||||
void CPU::opcode_CB_9A() { opcode_res(3, d); }
|
||||
void CPU::opcode_CB_9B() { opcode_res(3, e); }
|
||||
void CPU::opcode_CB_9C() { opcode_res(3, h); }
|
||||
void CPU::opcode_CB_9D() { opcode_res(3, l); }
|
||||
void CPU::opcode_CB_9E() { opcode_res(3, Address(hl)); }
|
||||
void CPU::opcode_CB_9F() { opcode_res(3, a); }
|
||||
void CPU::opcode_CB_A0() { opcode_res(4, b); }
|
||||
void CPU::opcode_CB_A1() { opcode_res(4, c); }
|
||||
void CPU::opcode_CB_A2() { opcode_res(4, d); }
|
||||
void CPU::opcode_CB_A3() { opcode_res(4, e); }
|
||||
void CPU::opcode_CB_A4() { opcode_res(4, h); }
|
||||
void CPU::opcode_CB_A5() { opcode_res(4, l); }
|
||||
void CPU::opcode_CB_A6() { opcode_res(4, Address(hl)); }
|
||||
void CPU::opcode_CB_A7() { opcode_res(4, a); }
|
||||
void CPU::opcode_CB_A8() { opcode_res(5, b); }
|
||||
void CPU::opcode_CB_A9() { opcode_res(5, c); }
|
||||
void CPU::opcode_CB_AA() { opcode_res(5, d); }
|
||||
void CPU::opcode_CB_AB() { opcode_res(5, e); }
|
||||
void CPU::opcode_CB_AC() { opcode_res(5, h); }
|
||||
void CPU::opcode_CB_AD() { opcode_res(5, l); }
|
||||
void CPU::opcode_CB_AE() { opcode_res(5, Address(hl)); }
|
||||
void CPU::opcode_CB_AF() { opcode_res(5, a); }
|
||||
void CPU::opcode_CB_B0() { opcode_res(6, b); }
|
||||
void CPU::opcode_CB_B1() { opcode_res(6, c); }
|
||||
void CPU::opcode_CB_B2() { opcode_res(6, d); }
|
||||
void CPU::opcode_CB_B3() { opcode_res(6, e); }
|
||||
void CPU::opcode_CB_B4() { opcode_res(6, h); }
|
||||
void CPU::opcode_CB_B5() { opcode_res(6, l); }
|
||||
void CPU::opcode_CB_B6() { opcode_res(6, Address(hl)); }
|
||||
void CPU::opcode_CB_B7() { opcode_res(6, a); }
|
||||
void CPU::opcode_CB_B8() { opcode_res(7, b); }
|
||||
void CPU::opcode_CB_B9() { opcode_res(7, c); }
|
||||
void CPU::opcode_CB_BA() { opcode_res(7, d); }
|
||||
void CPU::opcode_CB_BB() { opcode_res(7, e); }
|
||||
void CPU::opcode_CB_BC() { opcode_res(7, h); }
|
||||
void CPU::opcode_CB_BD() { opcode_res(7, l); }
|
||||
void CPU::opcode_CB_BE() { opcode_res(7, Address(hl)); }
|
||||
void CPU::opcode_CB_BF() { opcode_res(7, a); }
|
||||
void CPU::opcode_CB_C0() { opcode_set(0, b); }
|
||||
void CPU::opcode_CB_C1() { opcode_set(0, c); }
|
||||
void CPU::opcode_CB_C2() { opcode_set(0, d); }
|
||||
void CPU::opcode_CB_C3() { opcode_set(0, e); }
|
||||
void CPU::opcode_CB_C4() { opcode_set(0, h); }
|
||||
void CPU::opcode_CB_C5() { opcode_set(0, l); }
|
||||
void CPU::opcode_CB_C6() { opcode_set(0, Address(hl)); }
|
||||
void CPU::opcode_CB_C7() { opcode_set(0, a); }
|
||||
void CPU::opcode_CB_C8() { opcode_set(1, b); }
|
||||
void CPU::opcode_CB_C9() { opcode_set(1, c); }
|
||||
void CPU::opcode_CB_CA() { opcode_set(1, d); }
|
||||
void CPU::opcode_CB_CB() { opcode_set(1, e); }
|
||||
void CPU::opcode_CB_CC() { opcode_set(1, h); }
|
||||
void CPU::opcode_CB_CD() { opcode_set(1, l); }
|
||||
void CPU::opcode_CB_CE() { opcode_set(1, Address(hl)); }
|
||||
void CPU::opcode_CB_CF() { opcode_set(1, a); }
|
||||
void CPU::opcode_CB_D0() { opcode_set(2, b); }
|
||||
void CPU::opcode_CB_D1() { opcode_set(2, c); }
|
||||
void CPU::opcode_CB_D2() { opcode_set(2, d); }
|
||||
void CPU::opcode_CB_D3() { opcode_set(2, e); }
|
||||
void CPU::opcode_CB_D4() { opcode_set(2, h); }
|
||||
void CPU::opcode_CB_D5() { opcode_set(2, l); }
|
||||
void CPU::opcode_CB_D6() { opcode_set(2, Address(hl)); }
|
||||
void CPU::opcode_CB_D7() { opcode_set(2, a); }
|
||||
void CPU::opcode_CB_D8() { opcode_set(3, b); }
|
||||
void CPU::opcode_CB_D9() { opcode_set(3, c); }
|
||||
void CPU::opcode_CB_DA() { opcode_set(3, d); }
|
||||
void CPU::opcode_CB_DB() { opcode_set(3, e); }
|
||||
void CPU::opcode_CB_DC() { opcode_set(3, h); }
|
||||
void CPU::opcode_CB_DD() { opcode_set(3, l); }
|
||||
void CPU::opcode_CB_DE() { opcode_set(3, Address(hl)); }
|
||||
void CPU::opcode_CB_DF() { opcode_set(3, a); }
|
||||
void CPU::opcode_CB_E0() { opcode_set(4, b); }
|
||||
void CPU::opcode_CB_E1() { opcode_set(4, c); }
|
||||
void CPU::opcode_CB_E2() { opcode_set(4, d); }
|
||||
void CPU::opcode_CB_E3() { opcode_set(4, e); }
|
||||
void CPU::opcode_CB_E4() { opcode_set(4, h); }
|
||||
void CPU::opcode_CB_E5() { opcode_set(4, l); }
|
||||
void CPU::opcode_CB_E6() { opcode_set(4, Address(hl)); }
|
||||
void CPU::opcode_CB_E7() { opcode_set(4, a); }
|
||||
void CPU::opcode_CB_E8() { opcode_set(5, b); }
|
||||
void CPU::opcode_CB_E9() { opcode_set(5, c); }
|
||||
void CPU::opcode_CB_EA() { opcode_set(5, d); }
|
||||
void CPU::opcode_CB_EB() { opcode_set(5, e); }
|
||||
void CPU::opcode_CB_EC() { opcode_set(5, h); }
|
||||
void CPU::opcode_CB_ED() { opcode_set(5, l); }
|
||||
void CPU::opcode_CB_EE() { opcode_set(5, Address(hl)); }
|
||||
void CPU::opcode_CB_EF() { opcode_set(5, a); }
|
||||
void CPU::opcode_CB_F0() { opcode_set(6, b); }
|
||||
void CPU::opcode_CB_F1() { opcode_set(6, c); }
|
||||
void CPU::opcode_CB_F2() { opcode_set(6, d); }
|
||||
void CPU::opcode_CB_F3() { opcode_set(6, e); }
|
||||
void CPU::opcode_CB_F4() { opcode_set(6, h); }
|
||||
void CPU::opcode_CB_F5() { opcode_set(6, l); }
|
||||
void CPU::opcode_CB_F6() { opcode_set(6, Address(hl)); }
|
||||
void CPU::opcode_CB_F7() { opcode_set(6, a); }
|
||||
void CPU::opcode_CB_F8() { opcode_set(7, b); }
|
||||
void CPU::opcode_CB_F9() { opcode_set(7, c); }
|
||||
void CPU::opcode_CB_FA() { opcode_set(7, d); }
|
||||
void CPU::opcode_CB_FB() { opcode_set(7, e); }
|
||||
void CPU::opcode_CB_FC() { opcode_set(7, h); }
|
||||
void CPU::opcode_CB_FD() { opcode_set(7, l); }
|
||||
void CPU::opcode_CB_FE() { opcode_set(7, Address(hl)); }
|
||||
void CPU::opcode_CB_FF() { opcode_set(7, a); }
|
||||
@@ -0,0 +1,897 @@
|
||||
#include "cpu.h"
|
||||
|
||||
#include "gameboy.h"
|
||||
#include "bitwise.h"
|
||||
|
||||
using bitwise::check_bit;
|
||||
using bitwise::clear_bit;
|
||||
using bitwise::set_bit;
|
||||
|
||||
/* ADC */
|
||||
void CPU::_opcode_adc(u8 value) {
|
||||
u8 reg = a.value();
|
||||
u8 carry = f.flag_carry_value();
|
||||
|
||||
uint result_full = reg + value + carry;
|
||||
u8 result = static_cast<u8>(result_full);
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry(((reg & 0xf) + (value & 0xf) + carry) > 0xf);
|
||||
set_flag_carry(result_full > 0xff);
|
||||
|
||||
a.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_adc() {
|
||||
_opcode_adc(get_byte_from_pc());
|
||||
}
|
||||
|
||||
void CPU::opcode_adc(const ByteRegister& reg) {
|
||||
_opcode_adc(reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_adc(const Address&& addr) {
|
||||
_opcode_adc(gb.mmu.read(addr));
|
||||
}
|
||||
|
||||
|
||||
/* ADD */
|
||||
void CPU::_opcode_add(u8 reg, u8 value) {
|
||||
uint result = reg + value;
|
||||
|
||||
a.set(static_cast<u8>(result));
|
||||
|
||||
set_flag_zero(a.value() == 0);
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry((reg & 0xf) + (value & 0xf) > 0xf);
|
||||
set_flag_carry((result & 0x100) != 0);
|
||||
}
|
||||
|
||||
void CPU::opcode_add_a() {
|
||||
_opcode_add(a.value(), get_byte_from_pc());
|
||||
}
|
||||
|
||||
void CPU::opcode_add_a(const ByteRegister& reg) {
|
||||
_opcode_add(a.value(), reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_add_a(const Address& addr) {
|
||||
_opcode_add(a.value(), gb.mmu.read(addr));
|
||||
}
|
||||
|
||||
|
||||
void CPU::_opcode_add_hl(u16 value) {
|
||||
u16 reg = hl.value();
|
||||
|
||||
uint result = reg + value;
|
||||
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry((reg & 0xfff) + (value & 0xfff) > 0xfff);
|
||||
set_flag_carry((result & 0x10000) != 0);
|
||||
|
||||
hl.set(static_cast<u16>(result));
|
||||
}
|
||||
|
||||
void CPU::opcode_add_hl(const RegisterPair& reg_pair) {
|
||||
_opcode_add_hl(reg_pair.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_add_hl(const WordRegister& word_reg) {
|
||||
_opcode_add_hl(word_reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_add_sp() {
|
||||
u16 reg = sp.value();
|
||||
s8 value = get_signed_byte_from_pc();
|
||||
|
||||
int result = static_cast<int>(reg + value);
|
||||
|
||||
set_flag_zero(false);
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry(((reg ^ value ^ (result & 0xFFFF)) & 0x10) == 0x10);
|
||||
set_flag_carry(((reg ^ value ^ (result & 0xFFFF)) & 0x100) == 0x100);
|
||||
|
||||
sp.set(static_cast<u16>(result));
|
||||
}
|
||||
|
||||
|
||||
/* AND */
|
||||
void CPU::_opcode_and(u8 value) {
|
||||
u8 reg = a.value();
|
||||
u8 result = reg & value;
|
||||
|
||||
a.set(result);
|
||||
|
||||
set_flag_zero(a.value() == 0);
|
||||
set_flag_half_carry(true);
|
||||
set_flag_carry(false);
|
||||
set_flag_subtract(false);
|
||||
}
|
||||
|
||||
void CPU::opcode_and() {
|
||||
_opcode_and(get_byte_from_pc());
|
||||
}
|
||||
|
||||
void CPU::opcode_and(ByteRegister& reg) {
|
||||
_opcode_and(reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_and(Address&& addr) {
|
||||
_opcode_and(gb.mmu.read(addr));
|
||||
}
|
||||
|
||||
|
||||
/* BIT */
|
||||
void CPU::_opcode_bit(const u8 bit, const u8 value) {
|
||||
set_flag_zero(!check_bit(value, bit));
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry(true);
|
||||
}
|
||||
|
||||
void CPU::opcode_bit(const u8 bit, ByteRegister& reg) {
|
||||
_opcode_bit(bit, reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_bit(const u8 bit, Address&& addr) {
|
||||
_opcode_bit(bit, gb.mmu.read(addr));
|
||||
}
|
||||
|
||||
|
||||
/* CALL */
|
||||
void CPU::opcode_call() {
|
||||
u16 address = get_word_from_pc();
|
||||
stack_push(pc);
|
||||
pc.set(address);
|
||||
}
|
||||
|
||||
void CPU::opcode_call(Condition condition) {
|
||||
if (is_condition(condition)) {
|
||||
opcode_call();
|
||||
} else {
|
||||
/* Consume unused word argument */
|
||||
get_word_from_pc();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* CCF */
|
||||
void CPU::opcode_ccf() {
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry(false);
|
||||
set_flag_carry(!f.flag_carry());
|
||||
}
|
||||
|
||||
|
||||
/* CP */
|
||||
void CPU::_opcode_cp(const u8 value) {
|
||||
u8 reg = a.value();
|
||||
u8 result = static_cast<u8>(reg - value);
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_subtract(true);
|
||||
set_flag_half_carry(((reg & 0xf) - (value & 0xf)) < 0);
|
||||
set_flag_carry(reg < value);
|
||||
}
|
||||
|
||||
void CPU::opcode_cp() {
|
||||
_opcode_cp(get_byte_from_pc());
|
||||
}
|
||||
|
||||
void CPU::opcode_cp(const ByteRegister& reg) {
|
||||
_opcode_cp(reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_cp(const Address& addr) {
|
||||
_opcode_cp(gb.mmu.read(addr));
|
||||
}
|
||||
|
||||
|
||||
/* CPL */
|
||||
void CPU::opcode_cpl() {
|
||||
u8 reg = a.value();
|
||||
u8 result = ~reg;
|
||||
a.set(result);
|
||||
|
||||
set_flag_subtract(true);
|
||||
set_flag_half_carry(true);
|
||||
}
|
||||
|
||||
|
||||
/* DAA */
|
||||
void CPU::opcode_daa() {
|
||||
u8 reg = a.value();
|
||||
|
||||
u16 correction = f.flag_carry()
|
||||
? 0x60
|
||||
: 0x00;
|
||||
|
||||
if (f.flag_half_carry() || (!f.flag_subtract() && ((reg & 0x0F) > 9))) {
|
||||
correction |= 0x06;
|
||||
}
|
||||
|
||||
if (f.flag_carry() || (!f.flag_subtract() && (reg > 0x99))) {
|
||||
correction |= 0x60;
|
||||
}
|
||||
|
||||
if (f.flag_subtract()) {
|
||||
reg = static_cast<u8>(reg - correction);
|
||||
} else {
|
||||
reg = static_cast<u8>(reg + correction);
|
||||
}
|
||||
|
||||
if (((correction << 2) & 0x100) != 0) {
|
||||
set_flag_carry(true);
|
||||
}
|
||||
|
||||
set_flag_half_carry(false);
|
||||
set_flag_zero(reg == 0);
|
||||
|
||||
a.set(static_cast<u8>(reg));
|
||||
}
|
||||
|
||||
|
||||
/* DEC */
|
||||
void CPU::opcode_dec(ByteRegister& reg) {
|
||||
reg.decrement();
|
||||
|
||||
set_flag_zero(reg.value() == 0);
|
||||
set_flag_subtract(true);
|
||||
set_flag_half_carry((reg.value() & 0x0F) == 0x0F);
|
||||
}
|
||||
|
||||
void CPU::opcode_dec(RegisterPair& reg) {
|
||||
reg.decrement();
|
||||
}
|
||||
|
||||
void CPU::opcode_dec(WordRegister& reg) {
|
||||
reg.decrement();
|
||||
}
|
||||
|
||||
void CPU::opcode_dec(Address&& addr) {
|
||||
u8 value = gb.mmu.read(addr);
|
||||
u8 result = static_cast<u8>(value - 1);
|
||||
gb.mmu.write(addr, result);
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_subtract(true);
|
||||
set_flag_half_carry((result & 0x0F) == 0x0F);
|
||||
}
|
||||
|
||||
|
||||
/* DI */
|
||||
void CPU::opcode_di() {
|
||||
interrupts_enabled = false;
|
||||
}
|
||||
|
||||
|
||||
/* EI */
|
||||
void CPU::opcode_ei() {
|
||||
interrupts_enabled = true;
|
||||
}
|
||||
|
||||
|
||||
/* INC */
|
||||
void CPU::opcode_inc(ByteRegister& reg) {
|
||||
reg.increment();
|
||||
|
||||
set_flag_zero(reg.value() == 0);
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry((reg.value() & 0x0F) == 0x00);
|
||||
}
|
||||
|
||||
void CPU::opcode_inc(RegisterPair& reg) {
|
||||
reg.increment();
|
||||
}
|
||||
|
||||
void CPU::opcode_inc(WordRegister& reg) {
|
||||
reg.increment();
|
||||
}
|
||||
|
||||
void CPU::opcode_inc(Address&& addr) {
|
||||
u8 value = gb.mmu.read(addr);
|
||||
u8 result = static_cast<u8>(value + 1);
|
||||
gb.mmu.write(addr, result);
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry((result & 0x0F) == 0x00);
|
||||
}
|
||||
|
||||
|
||||
/* JP */
|
||||
void CPU::opcode_jp() {
|
||||
u16 address = get_word_from_pc();
|
||||
pc.set(address);
|
||||
}
|
||||
|
||||
void CPU::opcode_jp(Condition condition) {
|
||||
if (is_condition(condition)) {
|
||||
opcode_jp();
|
||||
} else {
|
||||
/* Consume unused word argument */
|
||||
get_word_from_pc();
|
||||
}
|
||||
}
|
||||
|
||||
void CPU::opcode_jp(const Address& addr) {
|
||||
unused(addr);
|
||||
pc.set(hl.value());
|
||||
}
|
||||
|
||||
|
||||
/* JR */
|
||||
void CPU::opcode_jr() {
|
||||
s8 offset = get_signed_byte_from_pc();
|
||||
|
||||
|
||||
u16 old_pc = pc.value();
|
||||
|
||||
u16 new_pc = static_cast<u16>(old_pc + offset);
|
||||
pc.set(new_pc);
|
||||
}
|
||||
|
||||
void CPU::opcode_jr(Condition condition) {
|
||||
if (is_condition(condition)) {
|
||||
opcode_jr();
|
||||
} else {
|
||||
/* Consume unused argument */
|
||||
get_signed_byte_from_pc();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* HALT */
|
||||
void CPU::opcode_halt() {
|
||||
halted = true;
|
||||
}
|
||||
|
||||
|
||||
/* LD */
|
||||
void CPU::opcode_ld(ByteRegister& reg) {
|
||||
u8 n = get_byte_from_pc();
|
||||
reg.set(n);
|
||||
}
|
||||
|
||||
void CPU::opcode_ld(ByteRegister& reg, const ByteRegister& byte_reg) {
|
||||
reg.set(byte_reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_ld(ByteRegister& reg, const Address& address) {
|
||||
reg.set(gb.mmu.read(address));
|
||||
}
|
||||
|
||||
void CPU::opcode_ld_from_addr(ByteRegister& reg) {
|
||||
u16 nn = get_word_from_pc();
|
||||
reg.set(gb.mmu.read(nn));
|
||||
}
|
||||
|
||||
void CPU::opcode_ld(RegisterPair& reg) {
|
||||
u16 nn = get_word_from_pc();
|
||||
reg.set(nn);
|
||||
}
|
||||
|
||||
|
||||
void CPU::opcode_ld(WordRegister& reg) {
|
||||
u16 nn = get_word_from_pc();
|
||||
reg.set(nn);
|
||||
}
|
||||
|
||||
void CPU::opcode_ld(WordRegister& reg, const RegisterPair& reg_pair) {
|
||||
reg.set(reg_pair.value());
|
||||
}
|
||||
|
||||
|
||||
void CPU::opcode_ld(const Address& address) {
|
||||
u8 n = get_byte_from_pc();
|
||||
gb.mmu.write(address, n);
|
||||
}
|
||||
|
||||
void CPU::opcode_ld(const Address& address, const ByteRegister& byte_reg) {
|
||||
gb.mmu.write(address, byte_reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_ld(const Address& address, const WordRegister& word_reg) {
|
||||
gb.mmu.write(address, word_reg.low());
|
||||
gb.mmu.write(address + 1, word_reg.high());
|
||||
}
|
||||
|
||||
|
||||
void CPU::opcode_ld_to_addr(const ByteRegister ®) {
|
||||
u16 address = get_word_from_pc();
|
||||
gb.mmu.write(Address(address), reg.value());
|
||||
}
|
||||
|
||||
|
||||
/* LDD */
|
||||
void CPU::opcode_ldd(ByteRegister& reg, const Address& address) {
|
||||
reg.set(gb.mmu.read(address));
|
||||
hl.decrement();
|
||||
}
|
||||
|
||||
void CPU::opcode_ldd(const Address& address, const ByteRegister& reg) {
|
||||
gb.mmu.write(address, reg.value());
|
||||
hl.decrement();
|
||||
}
|
||||
|
||||
|
||||
/* LDH */
|
||||
void CPU::opcode_ldh_into_a() {
|
||||
u8 offset = get_byte_from_pc();
|
||||
auto address = Address(0xFF00 + offset);
|
||||
|
||||
u8 value = gb.mmu.read(address);
|
||||
a.set(value);
|
||||
}
|
||||
|
||||
void CPU::opcode_ldh_into_data() {
|
||||
u8 offset = get_byte_from_pc();
|
||||
auto address = Address(0xFF00 + offset);
|
||||
|
||||
gb.mmu.write(address, a.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_ldh_into_c() {
|
||||
u8 offset = c.value();
|
||||
auto address = Address(0xFF00 + offset);
|
||||
|
||||
gb.mmu.write(address, a.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_ldh_c_into_a() {
|
||||
auto address = Address(0xFF00 + c.value());
|
||||
|
||||
a.set(gb.mmu.read(address));
|
||||
}
|
||||
|
||||
|
||||
/* LDHL */
|
||||
void CPU::opcode_ldhl() {
|
||||
u16 reg = sp.value();
|
||||
s8 value = get_signed_byte_from_pc();
|
||||
|
||||
int result = static_cast<int>(reg + value);
|
||||
|
||||
set_flag_zero(false);
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry(((reg ^ value ^ (result & 0xFFFF)) & 0x10) == 0x10);
|
||||
set_flag_carry(((reg ^ value ^ (result & 0xFFFF)) & 0x100) == 0x100);
|
||||
|
||||
hl.set(static_cast<u16>(result));
|
||||
}
|
||||
|
||||
|
||||
/* LDI */
|
||||
void CPU::opcode_ldi(ByteRegister& reg, const Address& address) {
|
||||
reg.set(gb.mmu.read(address));
|
||||
hl.increment();
|
||||
}
|
||||
|
||||
void CPU::opcode_ldi(const Address& address, const ByteRegister& reg) {
|
||||
gb.mmu.write(address, reg.value());
|
||||
hl.increment();
|
||||
}
|
||||
|
||||
|
||||
/* NOP */
|
||||
void CPU::opcode_nop() {
|
||||
/* Do nothing */
|
||||
}
|
||||
|
||||
|
||||
/* OR */
|
||||
void CPU::_opcode_or(u8 value) {
|
||||
u8 reg = a.value();
|
||||
u8 result = reg | value;
|
||||
|
||||
a.set(result);
|
||||
|
||||
set_flag_zero(a.value() == 0);
|
||||
set_flag_half_carry(false);
|
||||
set_flag_carry(false);
|
||||
set_flag_subtract(false);
|
||||
|
||||
}
|
||||
|
||||
void CPU::opcode_or() {
|
||||
_opcode_or(get_byte_from_pc());
|
||||
}
|
||||
|
||||
void CPU::opcode_or(const ByteRegister& reg) {
|
||||
_opcode_or(reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_or(const Address& addr) {
|
||||
_opcode_or(gb.mmu.read(addr));
|
||||
}
|
||||
|
||||
|
||||
/* POP */
|
||||
void CPU::opcode_pop(RegisterPair& reg) {
|
||||
stack_pop(reg);
|
||||
}
|
||||
|
||||
|
||||
/* PUSH */
|
||||
void CPU::opcode_push(const RegisterPair& reg) {
|
||||
stack_push(reg);
|
||||
}
|
||||
|
||||
|
||||
/* RES */
|
||||
void CPU::opcode_res(const u8 bit, ByteRegister& reg) {
|
||||
u8 result = clear_bit(reg.value(), bit);
|
||||
reg.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_res(const u8 bit, Address&& addr) {
|
||||
u8 value = gb.mmu.read(addr);
|
||||
u8 result = clear_bit(value, bit);
|
||||
gb.mmu.write(addr, result);
|
||||
}
|
||||
|
||||
|
||||
/* RET */
|
||||
void CPU::opcode_ret() {
|
||||
stack_pop(pc);
|
||||
}
|
||||
|
||||
void CPU::opcode_ret(Condition condition) {
|
||||
if (is_condition(condition)) {
|
||||
opcode_ret();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* RETI */
|
||||
void CPU::opcode_reti() {
|
||||
opcode_ret();
|
||||
opcode_ei();
|
||||
}
|
||||
|
||||
|
||||
/* RL */
|
||||
auto CPU::_opcode_rl(u8 value) -> u8 {
|
||||
u8 carry = f.flag_carry_value();
|
||||
|
||||
bool will_carry = check_bit(value, 7);
|
||||
set_flag_carry(will_carry);
|
||||
|
||||
u8 result = static_cast<u8>(value << 1);
|
||||
result |= carry;
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CPU::opcode_rla() {
|
||||
opcode_rl(a);
|
||||
set_flag_zero(false);
|
||||
}
|
||||
|
||||
void CPU::opcode_rl(ByteRegister& reg) {
|
||||
u8 result = _opcode_rl(reg.value());
|
||||
reg.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_rl(Address&& addr) {
|
||||
u8 result = _opcode_rl(gb.mmu.read(addr));
|
||||
gb.mmu.write(addr, result);
|
||||
}
|
||||
|
||||
|
||||
/* RLC */
|
||||
auto CPU::_opcode_rlc(u8 value) -> u8 {
|
||||
u8 carry_flag = check_bit(value, 7);
|
||||
u8 truncated_bit = check_bit(value, 7);
|
||||
u8 result = static_cast<u8>((value << 1) | truncated_bit);
|
||||
|
||||
set_flag_carry(carry_flag);
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_half_carry(false);
|
||||
set_flag_subtract(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CPU::opcode_rlca() {
|
||||
opcode_rlc(a);
|
||||
set_flag_zero(false);
|
||||
}
|
||||
|
||||
void CPU::opcode_rlc(ByteRegister& reg) {
|
||||
u8 result = _opcode_rlc(reg.value());
|
||||
reg.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_rlc(Address&& addr) {
|
||||
u8 result = _opcode_rlc(gb.mmu.read(addr));
|
||||
gb.mmu.write(addr, result);
|
||||
}
|
||||
|
||||
|
||||
/* RR */
|
||||
auto CPU::_opcode_rr(u8 value) -> u8 {
|
||||
u8 carry = f.flag_carry_value();
|
||||
|
||||
bool will_carry = check_bit(value, 0);
|
||||
set_flag_carry(will_carry);
|
||||
|
||||
u8 result = static_cast<u8>(value >> 1);
|
||||
result |= (carry << 7);
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CPU::opcode_rra() {
|
||||
opcode_rr(a);
|
||||
set_flag_zero(false);
|
||||
}
|
||||
|
||||
void CPU::opcode_rr(ByteRegister& reg) {
|
||||
u8 result = _opcode_rr(reg.value());
|
||||
reg.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_rr(Address&& addr) {
|
||||
u8 result = _opcode_rr(gb.mmu.read(addr));
|
||||
gb.mmu.write(addr, result);
|
||||
}
|
||||
|
||||
|
||||
/* RRC */
|
||||
auto CPU::_opcode_rrc(u8 value) -> u8 {
|
||||
u8 carry_flag = check_bit(value, 0);
|
||||
u8 truncated_bit = check_bit(value, 0);
|
||||
u8 result = static_cast<u8>((value >> 1) | (truncated_bit << 7));
|
||||
|
||||
set_flag_carry(carry_flag);
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_half_carry(false);
|
||||
set_flag_subtract(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CPU::opcode_rrca() {
|
||||
opcode_rrc(a);
|
||||
set_flag_zero(false);
|
||||
}
|
||||
|
||||
void CPU::opcode_rrc(ByteRegister& reg) {
|
||||
u8 result = _opcode_rrc(reg.value());
|
||||
reg.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_rrc(Address&& addr) {
|
||||
u8 result = _opcode_rrc(gb.mmu.read(addr));
|
||||
gb.mmu.write(addr, result);
|
||||
}
|
||||
|
||||
|
||||
/* RST */
|
||||
void CPU::opcode_rst(const u8 offset) {
|
||||
stack_push(pc);
|
||||
pc.set(offset);
|
||||
}
|
||||
|
||||
|
||||
/* SBC */
|
||||
void CPU::_opcode_sbc(const u8 value) {
|
||||
u8 carry = f.flag_carry_value();
|
||||
u8 reg = a.value();
|
||||
|
||||
int result_full = reg - value - carry;
|
||||
u8 result = static_cast<u8>(result_full);
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_subtract(true);
|
||||
set_flag_carry(result_full < 0);
|
||||
set_flag_half_carry(((reg & 0xf) - (value & 0xf) - carry) < 0);
|
||||
|
||||
a.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_sbc() {
|
||||
_opcode_sbc(get_byte_from_pc());
|
||||
}
|
||||
|
||||
void CPU::opcode_sbc(ByteRegister& reg) {
|
||||
_opcode_sbc(reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_sbc(Address&& addr) {
|
||||
_opcode_sbc(gb.mmu.read(addr));
|
||||
}
|
||||
|
||||
|
||||
/* SCF */
|
||||
void CPU::opcode_scf() {
|
||||
set_flag_carry(true);
|
||||
set_flag_half_carry(false);
|
||||
set_flag_subtract(false);
|
||||
}
|
||||
|
||||
|
||||
/* SET */
|
||||
void CPU::opcode_set(const u8 bit, ByteRegister& reg) {
|
||||
u8 result = set_bit(reg.value(), bit);
|
||||
reg.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_set(const u8 bit, Address&& addr) {
|
||||
u8 value = gb.mmu.read(addr);
|
||||
u8 result = set_bit(value, bit);
|
||||
gb.mmu.write(addr, result);
|
||||
}
|
||||
|
||||
|
||||
/* SLA */
|
||||
auto CPU::_opcode_sla(u8 value) -> u8 {
|
||||
u8 carry_bit = check_bit(value, 7);
|
||||
|
||||
u8 result = static_cast<u8>(value << 1);
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_carry(carry_bit);
|
||||
set_flag_half_carry(false);
|
||||
set_flag_subtract(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CPU::opcode_sla(ByteRegister& reg) {
|
||||
u8 result = _opcode_sla(reg.value());
|
||||
reg.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_sla(Address&& addr) {
|
||||
u8 result = _opcode_sla(gb.mmu.read(addr));
|
||||
gb.mmu.write(addr, result);
|
||||
}
|
||||
|
||||
|
||||
/* SRA */
|
||||
auto CPU::_opcode_sra(u8 value) -> u8 {
|
||||
u8 carry_bit = check_bit(value, 0);
|
||||
u8 top_bit = check_bit(value, 7);
|
||||
|
||||
u8 result = static_cast<u8>(value >> 1);
|
||||
result = bitwise::set_bit_to(result, 7, top_bit);
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_carry(carry_bit);
|
||||
set_flag_half_carry(false);
|
||||
set_flag_subtract(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CPU::opcode_sra(ByteRegister& reg) {
|
||||
u8 result = _opcode_sra(reg.value());
|
||||
reg.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_sra(Address&& addr) {
|
||||
u8 result = _opcode_sra(gb.mmu.read(addr));
|
||||
gb.mmu.write(addr, result);
|
||||
}
|
||||
|
||||
|
||||
/* SRL */
|
||||
auto CPU::_opcode_srl(u8 value) -> u8 {
|
||||
bool least_bit_set = check_bit(value, 0);
|
||||
|
||||
u8 result = (value >> 1);
|
||||
set_flag_carry(least_bit_set);
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_half_carry(false);
|
||||
set_flag_subtract(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CPU::opcode_srl(ByteRegister& reg) {
|
||||
u8 result = _opcode_srl(reg.value());
|
||||
reg.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_srl(Address&& addr) {
|
||||
u8 result = _opcode_srl(gb.mmu.read(addr));
|
||||
gb.mmu.write(addr, result);
|
||||
}
|
||||
|
||||
|
||||
/* STOP */
|
||||
void CPU::opcode_stop() {
|
||||
/* halted = true; */
|
||||
}
|
||||
|
||||
|
||||
/* SUB */
|
||||
void CPU::_opcode_sub(u8 value) {
|
||||
u8 reg = a.value();
|
||||
u8 result = static_cast<u8>(reg - value);
|
||||
|
||||
a.set(result);
|
||||
|
||||
set_flag_zero(a.value() == 0);
|
||||
set_flag_subtract(true);
|
||||
set_flag_half_carry(((reg & 0xf) - (value & 0xf)) < 0);
|
||||
set_flag_carry(reg < value);
|
||||
}
|
||||
|
||||
void CPU::opcode_sub() {
|
||||
_opcode_sub(get_byte_from_pc());
|
||||
}
|
||||
|
||||
void CPU::opcode_sub(ByteRegister& reg) {
|
||||
_opcode_sub(reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_sub(Address&& addr) {
|
||||
_opcode_sub(gb.mmu.read(addr));
|
||||
}
|
||||
|
||||
|
||||
/* SWAP */
|
||||
auto CPU::_opcode_swap(u8 value) -> u8 {
|
||||
using bitwise::compose_nibbles;
|
||||
|
||||
u8 lower_nibble = value & 0x0F;
|
||||
u8 upper_nibble = (value & 0xF0) >> 4;
|
||||
|
||||
u8 result = compose_nibbles(lower_nibble, upper_nibble);
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry(false);
|
||||
set_flag_carry(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CPU::opcode_swap(ByteRegister& reg) {
|
||||
u8 result = _opcode_swap(reg.value());
|
||||
reg.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_swap(Address&& addr) {
|
||||
u8 result = _opcode_swap(gb.mmu.read(addr));
|
||||
gb.mmu.write(addr, result);
|
||||
}
|
||||
|
||||
|
||||
/* XOR */
|
||||
void CPU::_opcode_xor(u8 value) {
|
||||
u8 reg = a.value();
|
||||
|
||||
u8 result = reg ^ value;
|
||||
|
||||
set_flag_zero(result == 0);
|
||||
set_flag_subtract(false);
|
||||
set_flag_half_carry(false);
|
||||
set_flag_carry(false);
|
||||
|
||||
a.set(result);
|
||||
}
|
||||
|
||||
void CPU::opcode_xor() {
|
||||
_opcode_xor(get_byte_from_pc());
|
||||
}
|
||||
|
||||
void CPU::opcode_xor(const ByteRegister& reg) {
|
||||
_opcode_xor(reg.value());
|
||||
}
|
||||
|
||||
void CPU::opcode_xor(const Address& addr) {
|
||||
_opcode_xor(gb.mmu.read(addr));
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
#include "definitions.h"
|
||||
|
||||
/* Devirtualized registers: on the original design every register access was
|
||||
* a virtual call and every 1-byte register carried an 8-byte vtable pointer.
|
||||
* On a 64 MHz Cortex-M4 that overhead matters, so everything here is plain
|
||||
* inline code. The flag register masking (lower nibble always 0) is handled
|
||||
* by FlagRegister's shadowing set() and by RegisterPair's low_mask. */
|
||||
|
||||
class ByteRegister {
|
||||
public:
|
||||
ByteRegister() = default;
|
||||
|
||||
void set(u8 new_value) { val = new_value; }
|
||||
void reset() { val = 0; }
|
||||
auto value() const -> u8 { return val; }
|
||||
|
||||
auto check_bit(u8 bit) const -> bool { return (val & (1 << bit)) != 0; }
|
||||
void set_bit_to(u8 bit, bool set) {
|
||||
if(set)
|
||||
val = static_cast<u8>(val | (1 << bit));
|
||||
else
|
||||
val = static_cast<u8>(val & ~(1 << bit));
|
||||
}
|
||||
|
||||
void increment() { val++; }
|
||||
void decrement() { val--; }
|
||||
|
||||
auto operator==(u8 other) const -> bool { return val == other; }
|
||||
|
||||
protected:
|
||||
u8 val = 0x0;
|
||||
};
|
||||
|
||||
class FlagRegister : public ByteRegister {
|
||||
public:
|
||||
FlagRegister() = default;
|
||||
|
||||
/* lower nibble of F is always 0 */
|
||||
void set(u8 new_value) { val = static_cast<u8>(new_value & 0xF0); }
|
||||
|
||||
void set_flag_zero(bool set) { set_bit_to(7, set); }
|
||||
void set_flag_subtract(bool set) { set_bit_to(6, set); }
|
||||
void set_flag_half_carry(bool set) { set_bit_to(5, set); }
|
||||
void set_flag_carry(bool set) { set_bit_to(4, set); }
|
||||
|
||||
auto flag_zero() const -> bool { return check_bit(7); }
|
||||
auto flag_subtract() const -> bool { return check_bit(6); }
|
||||
auto flag_half_carry() const -> bool { return check_bit(5); }
|
||||
auto flag_carry() const -> bool { return check_bit(4); }
|
||||
|
||||
auto flag_zero_value() const -> u8 { return static_cast<u8>((val >> 7) & 1); }
|
||||
auto flag_subtract_value() const -> u8 { return static_cast<u8>((val >> 6) & 1); }
|
||||
auto flag_half_carry_value() const -> u8 { return static_cast<u8>((val >> 5) & 1); }
|
||||
auto flag_carry_value() const -> u8 { return static_cast<u8>((val >> 4) & 1); }
|
||||
};
|
||||
|
||||
class WordRegister {
|
||||
public:
|
||||
WordRegister() = default;
|
||||
|
||||
void set(u16 new_value) { val = new_value; }
|
||||
|
||||
auto value() const -> u16 { return val; }
|
||||
|
||||
auto low() const -> u8 { return static_cast<u8>(val); }
|
||||
auto high() const -> u8 { return static_cast<u8>(val >> 8); }
|
||||
|
||||
void increment() { val++; }
|
||||
void decrement() { val--; }
|
||||
|
||||
private:
|
||||
u16 val = 0x0;
|
||||
};
|
||||
|
||||
class RegisterPair {
|
||||
public:
|
||||
/* mask_low is 0xF0 for AF (F's lower nibble reads/writes as 0) */
|
||||
RegisterPair(ByteRegister& high, ByteRegister& low, u8 mask_low = 0xFF)
|
||||
: low_byte(low)
|
||||
, high_byte(high)
|
||||
, low_mask(mask_low) {}
|
||||
|
||||
void set(u16 word) {
|
||||
low_byte.set(static_cast<u8>(word & low_mask));
|
||||
high_byte.set(static_cast<u8>(word >> 8));
|
||||
}
|
||||
|
||||
auto value() const -> u16 {
|
||||
return static_cast<u16>((high_byte.value() << 8) | (low_byte.value() & low_mask));
|
||||
}
|
||||
|
||||
auto low() const -> u8 { return static_cast<u8>(low_byte.value() & low_mask); }
|
||||
auto high() const -> u8 { return high_byte.value(); }
|
||||
|
||||
void increment() { set(static_cast<u16>(value() + 1)); }
|
||||
void decrement() { set(static_cast<u16>(value() - 1)); }
|
||||
|
||||
private:
|
||||
ByteRegister& low_byte;
|
||||
ByteRegister& high_byte;
|
||||
u8 low_mask;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "timer.h"
|
||||
|
||||
#include "definitions.h"
|
||||
#include "gameboy.h"
|
||||
#include "cpu.h"
|
||||
#include "bitwise.h"
|
||||
|
||||
const uint CLOCKS_PER_CYCLE = 4;
|
||||
|
||||
Timer::Timer(Gameboy& _gb) : gb(_gb) {}
|
||||
|
||||
void Timer::tick(uint cycles) {
|
||||
/* DIV increments at 16384 Hz = every 64 M-cycles (upstream incremented
|
||||
* it once per M-cycle: 64x too fast, breaking games that use DIV for
|
||||
* delays or randomness) */
|
||||
div_clocks += cycles;
|
||||
if(div_clocks >= 64) {
|
||||
divider.set(static_cast<u8>(divider.value() + (div_clocks >> 6)));
|
||||
div_clocks &= 63;
|
||||
}
|
||||
|
||||
clocks += cycles * CLOCKS_PER_CYCLE;
|
||||
|
||||
auto timer_is_on = timer_control.check_bit(2);
|
||||
if (timer_is_on == 0) { return; }
|
||||
|
||||
auto clock_limit = clocks_needed_to_increment();
|
||||
|
||||
if (clocks >= clock_limit) {
|
||||
clocks = clocks % clock_limit;
|
||||
|
||||
u8 old_timer_counter = timer_counter.value();
|
||||
timer_counter.increment();
|
||||
|
||||
if (timer_counter.value() < old_timer_counter) {
|
||||
gb.cpu.interrupt_flag.set_bit_to(2, true);
|
||||
timer_counter.set(timer_modulo.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Timer::get_divider() const -> u8 { return divider.value(); }
|
||||
|
||||
auto Timer::get_timer() const -> u8 { return timer_counter.value(); }
|
||||
|
||||
auto Timer::get_timer_modulo() const -> u8 { return timer_modulo.value(); }
|
||||
|
||||
// Only the bottom three bits of this register are usable
|
||||
auto Timer::get_timer_control() const -> u8 { return timer_control.value() & 0x3; }
|
||||
|
||||
void Timer::reset_divider() {
|
||||
divider.set(0x0);
|
||||
}
|
||||
|
||||
void Timer::set_timer(u8 value) {
|
||||
timer_counter.set(value);
|
||||
}
|
||||
|
||||
void Timer::set_timer_modulo(u8 value) {
|
||||
timer_modulo.set(value);
|
||||
}
|
||||
|
||||
void Timer::set_timer_control(u8 value) {
|
||||
timer_control.set(value);
|
||||
}
|
||||
|
||||
uint Timer::clocks_needed_to_increment() {
|
||||
using bitwise::check_bit;
|
||||
|
||||
switch (get_timer_control()) {
|
||||
case 0: return CLOCK_RATE / 4096;
|
||||
case 1: return CLOCK_RATE / 262144;
|
||||
case 2: return CLOCK_RATE / 65536;
|
||||
case 3: return CLOCK_RATE / 16384;
|
||||
default: return CLOCK_RATE / 4096; /* unreachable */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "definitions.h"
|
||||
#include "register.h"
|
||||
|
||||
class Gameboy;
|
||||
|
||||
class Timer {
|
||||
public:
|
||||
Timer(Gameboy& inGb);
|
||||
|
||||
void tick(uint cycles);
|
||||
|
||||
auto get_divider() const -> u8;
|
||||
auto get_timer() const -> u8;
|
||||
auto get_timer_modulo() const -> u8;
|
||||
auto get_timer_control() const -> u8;
|
||||
|
||||
void reset_divider();
|
||||
void set_timer(u8 value);
|
||||
void set_timer_modulo(u8 value);
|
||||
void set_timer_control(u8 value);
|
||||
|
||||
private:
|
||||
uint clocks_needed_to_increment();
|
||||
|
||||
uint clocks = 0;
|
||||
uint div_clocks = 0;
|
||||
|
||||
Gameboy& gb;
|
||||
|
||||
ByteRegister divider;
|
||||
ByteRegister timer_counter;
|
||||
|
||||
ByteRegister timer_modulo;
|
||||
ByteRegister timer_control;
|
||||
};
|
||||
@@ -0,0 +1,353 @@
|
||||
#include "video.h"
|
||||
|
||||
#include "gameboy.h"
|
||||
#include "cpu.h"
|
||||
#include "bitwise.h"
|
||||
|
||||
using bitwise::check_bit;
|
||||
|
||||
Video::Video(Gameboy& inGb)
|
||||
: gb(inGb) {
|
||||
}
|
||||
|
||||
void Video::tick(Cycles cycles) {
|
||||
cycle_counter += cycles.cycles;
|
||||
|
||||
switch(current_mode) {
|
||||
case VideoMode::ACCESS_OAM:
|
||||
if(cycle_counter >= CLOCKS_PER_SCANLINE_OAM) {
|
||||
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE_OAM;
|
||||
lcd_status.set_bit_to(1, true);
|
||||
lcd_status.set_bit_to(0, true);
|
||||
current_mode = VideoMode::ACCESS_VRAM;
|
||||
}
|
||||
break;
|
||||
case VideoMode::ACCESS_VRAM:
|
||||
if(cycle_counter >= CLOCKS_PER_SCANLINE_VRAM) {
|
||||
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE_VRAM;
|
||||
current_mode = VideoMode::HBLANK;
|
||||
|
||||
bool hblank_interrupt = check_bit(lcd_status.value(), 3);
|
||||
|
||||
if(hblank_interrupt) {
|
||||
gb.cpu.interrupt_flag.set_bit_to(1, true);
|
||||
}
|
||||
|
||||
bool ly_coincidence_interrupt = check_bit(lcd_status.value(), 6);
|
||||
bool ly_coincidence = ly_compare.value() == line.value();
|
||||
if(ly_coincidence_interrupt && ly_coincidence) {
|
||||
gb.cpu.interrupt_flag.set_bit_to(1, true);
|
||||
}
|
||||
lcd_status.set_bit_to(2, ly_coincidence);
|
||||
|
||||
lcd_status.set_bit_to(1, false);
|
||||
lcd_status.set_bit_to(0, false);
|
||||
}
|
||||
break;
|
||||
case VideoMode::HBLANK:
|
||||
if(cycle_counter >= CLOCKS_PER_HBLANK) {
|
||||
if(!skip_render) write_scanline(line.value());
|
||||
line.increment();
|
||||
|
||||
cycle_counter = cycle_counter % CLOCKS_PER_HBLANK;
|
||||
|
||||
/* Line 145 (index 144) is the first line of VBLANK */
|
||||
if(line == 144) {
|
||||
current_mode = VideoMode::VBLANK;
|
||||
lcd_status.set_bit_to(1, false);
|
||||
lcd_status.set_bit_to(0, true);
|
||||
gb.cpu.interrupt_flag.set_bit_to(0, true);
|
||||
} else {
|
||||
lcd_status.set_bit_to(1, true);
|
||||
lcd_status.set_bit_to(0, false);
|
||||
current_mode = VideoMode::ACCESS_OAM;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case VideoMode::VBLANK:
|
||||
if(cycle_counter >= CLOCKS_PER_SCANLINE) {
|
||||
line.increment();
|
||||
|
||||
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE;
|
||||
|
||||
/* Line 155 (index 154) is the last line */
|
||||
if(line == 154) {
|
||||
if(!skip_render) {
|
||||
write_sprites();
|
||||
draw();
|
||||
buffer.reset();
|
||||
} else {
|
||||
draw(); /* still notify the frontend for pacing */
|
||||
}
|
||||
line.reset();
|
||||
current_mode = VideoMode::ACCESS_OAM;
|
||||
lcd_status.set_bit_to(1, true);
|
||||
lcd_status.set_bit_to(0, false);
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto Video::display_enabled() const -> bool {
|
||||
return check_bit(control_byte, 7);
|
||||
}
|
||||
auto Video::window_tile_map() const -> bool {
|
||||
return check_bit(control_byte, 6);
|
||||
}
|
||||
auto Video::window_enabled() const -> bool {
|
||||
return check_bit(control_byte, 5);
|
||||
}
|
||||
auto Video::bg_window_tile_data() const -> bool {
|
||||
return check_bit(control_byte, 4);
|
||||
}
|
||||
auto Video::bg_tile_map_display() const -> bool {
|
||||
return check_bit(control_byte, 3);
|
||||
}
|
||||
auto Video::sprite_size() const -> bool {
|
||||
return check_bit(control_byte, 2);
|
||||
}
|
||||
auto Video::sprites_enabled() const -> bool {
|
||||
return check_bit(control_byte, 1);
|
||||
}
|
||||
auto Video::bg_enabled() const -> bool {
|
||||
return check_bit(control_byte, 0);
|
||||
}
|
||||
|
||||
void Video::write_scanline(u8 current_line) {
|
||||
if(!display_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Lines the frontend never displays are not worth rendering */
|
||||
if(row_mask && current_line < GAMEBOY_HEIGHT && !row_mask[current_line]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(bg_enabled()) {
|
||||
draw_bg_line(current_line);
|
||||
}
|
||||
|
||||
if(window_enabled()) {
|
||||
draw_window_line(current_line);
|
||||
}
|
||||
}
|
||||
|
||||
void Video::write_sprites() {
|
||||
if(!sprites_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(uint sprite_n = 0; sprite_n < 40; sprite_n++) {
|
||||
draw_sprite(sprite_n);
|
||||
}
|
||||
}
|
||||
|
||||
void Video::draw_bg_line(uint current_line) {
|
||||
/* Note: tileset two uses signed numbering to share half the tiles with
|
||||
* tileset one */
|
||||
bool use_tile_set_zero = bg_window_tile_data();
|
||||
bool use_tile_map_zero = !bg_tile_map_display();
|
||||
|
||||
Palette palette = load_palette(bg_palette);
|
||||
|
||||
u16 tile_set_address = use_tile_set_zero ? TILE_SET_ZERO_ADDRESS : TILE_SET_ONE_ADDRESS;
|
||||
u16 tile_map_address = use_tile_map_zero ? TILE_MAP_ZERO_ADDRESS : TILE_MAP_ONE_ADDRESS;
|
||||
|
||||
uint screen_y = current_line;
|
||||
uint scrolled_y = (screen_y + scroll_y.value()) % BG_MAP_SIZE;
|
||||
uint tile_y = scrolled_y / TILE_HEIGHT_PX;
|
||||
uint tile_pixel_y = scrolled_y % TILE_HEIGHT_PX;
|
||||
uint tile_data_line_offset = tile_pixel_y * 2;
|
||||
|
||||
/* Render tile-by-tile instead of refetching the tile data for every
|
||||
* pixel like upstream did */
|
||||
uint scroll_x_val = scroll_x.value();
|
||||
|
||||
uint screen_x = 0;
|
||||
while(screen_x < GAMEBOY_WIDTH) {
|
||||
uint scrolled_x = (screen_x + scroll_x_val) % BG_MAP_SIZE;
|
||||
uint tile_x = scrolled_x / TILE_WIDTH_PX;
|
||||
uint tile_pixel_x = scrolled_x % TILE_WIDTH_PX;
|
||||
|
||||
uint tile_index = tile_y * TILES_PER_LINE + tile_x;
|
||||
u8 tile_id = video_ram[tile_map_address - 0x8000 + tile_index];
|
||||
|
||||
uint tile_data_mem_offset = use_tile_set_zero ?
|
||||
tile_id * TILE_BYTES :
|
||||
static_cast<uint>(
|
||||
(static_cast<s8>(tile_id) + 128)) *
|
||||
TILE_BYTES;
|
||||
|
||||
uint line_addr = (tile_set_address - 0x8000) + tile_data_mem_offset +
|
||||
tile_data_line_offset;
|
||||
|
||||
u8 pixels_1 = video_ram[line_addr];
|
||||
u8 pixels_2 = video_ram[line_addr + 1];
|
||||
|
||||
/* Draw the remainder of this tile's row */
|
||||
for(uint px = tile_pixel_x; px < TILE_WIDTH_PX && screen_x < GAMEBOY_WIDTH;
|
||||
px++, screen_x++) {
|
||||
u8 pixel_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(px));
|
||||
buffer.set_pixel(screen_x, screen_y, get_shade_from_palette(pixel_color, palette));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Video::draw_window_line(uint current_line) {
|
||||
bool use_tile_set_zero = bg_window_tile_data();
|
||||
bool use_tile_map_zero = !window_tile_map();
|
||||
|
||||
Palette palette = load_palette(bg_palette);
|
||||
|
||||
u16 tile_set_address = use_tile_set_zero ? TILE_SET_ZERO_ADDRESS : TILE_SET_ONE_ADDRESS;
|
||||
u16 tile_map_address = use_tile_map_zero ? TILE_MAP_ZERO_ADDRESS : TILE_MAP_ONE_ADDRESS;
|
||||
|
||||
uint screen_y = current_line;
|
||||
uint scrolled_y = screen_y - window_y.value();
|
||||
|
||||
if(scrolled_y >= GAMEBOY_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint tile_y = scrolled_y / TILE_HEIGHT_PX;
|
||||
uint tile_pixel_y = scrolled_y % TILE_HEIGHT_PX;
|
||||
uint tile_data_line_offset = tile_pixel_y * 2;
|
||||
|
||||
for(uint screen_x = 0; screen_x < GAMEBOY_WIDTH; screen_x++) {
|
||||
uint scrolled_x = screen_x + window_x.value() - 7;
|
||||
|
||||
uint tile_x = scrolled_x / TILE_WIDTH_PX;
|
||||
uint tile_pixel_x = scrolled_x % TILE_WIDTH_PX;
|
||||
|
||||
uint tile_index = tile_y * TILES_PER_LINE + tile_x;
|
||||
if(tile_index >= 32 * 32) continue;
|
||||
|
||||
u8 tile_id = video_ram[tile_map_address - 0x8000 + tile_index];
|
||||
|
||||
uint tile_data_mem_offset = use_tile_set_zero ?
|
||||
tile_id * TILE_BYTES :
|
||||
static_cast<uint>(
|
||||
(static_cast<s8>(tile_id) + 128)) *
|
||||
TILE_BYTES;
|
||||
|
||||
uint line_addr = (tile_set_address - 0x8000) + tile_data_mem_offset +
|
||||
tile_data_line_offset;
|
||||
|
||||
u8 pixels_1 = video_ram[line_addr];
|
||||
u8 pixels_2 = video_ram[line_addr + 1];
|
||||
|
||||
u8 pixel_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(tile_pixel_x));
|
||||
buffer.set_pixel(screen_x, screen_y, get_shade_from_palette(pixel_color, palette));
|
||||
}
|
||||
}
|
||||
|
||||
void Video::draw_sprite(const uint sprite_n) {
|
||||
/* Each sprite is represented by 4 bytes */
|
||||
u16 oam_start = static_cast<u16>(sprite_n * SPRITE_BYTES);
|
||||
|
||||
u8 sprite_y = gb.mmu.oam_ram[oam_start];
|
||||
u8 sprite_x = gb.mmu.oam_ram[oam_start + 1];
|
||||
|
||||
/* Offscreen sprites are not drawn */
|
||||
if(sprite_y == 0 || sprite_y >= 160) {
|
||||
return;
|
||||
}
|
||||
if(sprite_x == 0 || sprite_x >= 168) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint sprite_height = sprite_size() ? 16 : 8;
|
||||
|
||||
u8 pattern_n = gb.mmu.oam_ram[oam_start + 2];
|
||||
u8 sprite_attrs = gb.mmu.oam_ram[oam_start + 3];
|
||||
|
||||
/* Bits 0-3 are used only for CGB */
|
||||
bool use_palette_1 = check_bit(sprite_attrs, 4);
|
||||
bool flip_x = check_bit(sprite_attrs, 5);
|
||||
bool flip_y = check_bit(sprite_attrs, 6);
|
||||
bool obj_behind_bg = check_bit(sprite_attrs, 7);
|
||||
|
||||
Palette palette = use_palette_1 ? load_palette(sprite_palette_1) :
|
||||
load_palette(sprite_palette_0);
|
||||
|
||||
uint tile_offset = pattern_n * TILE_BYTES;
|
||||
|
||||
int start_y = sprite_y - 16;
|
||||
int start_x = sprite_x - 8;
|
||||
|
||||
for(uint y = 0; y < sprite_height; y++) {
|
||||
int screen_y = start_y + static_cast<int>(y);
|
||||
if(screen_y < 0 || screen_y >= static_cast<int>(GAMEBOY_HEIGHT)) continue;
|
||||
if(row_mask && !row_mask[screen_y]) continue;
|
||||
|
||||
uint src_y = !flip_y ? y : sprite_height - y - 1;
|
||||
|
||||
uint line_addr = tile_offset + src_y * 2; /* relative to tile set zero */
|
||||
u8 pixels_1 = video_ram[line_addr];
|
||||
u8 pixels_2 = video_ram[line_addr + 1];
|
||||
|
||||
for(uint x = 0; x < TILE_WIDTH_PX; x++) {
|
||||
int screen_x = start_x + static_cast<int>(x);
|
||||
if(screen_x < 0 || screen_x >= static_cast<int>(GAMEBOY_WIDTH)) continue;
|
||||
|
||||
uint src_x = !flip_x ? x : TILE_WIDTH_PX - x - 1;
|
||||
|
||||
u8 gb_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(src_x));
|
||||
|
||||
/* Color 0 is transparent */
|
||||
if(gb_color == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Shade existing_pixel = buffer.get_pixel(
|
||||
static_cast<uint>(screen_x), static_cast<uint>(screen_y));
|
||||
|
||||
/* Note: same behaviour as upstream - compares the final shade
|
||||
* rather than the logical color 0 */
|
||||
if(obj_behind_bg && existing_pixel != SHADE_WHITE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.set_pixel(
|
||||
static_cast<uint>(screen_x),
|
||||
static_cast<uint>(screen_y),
|
||||
get_shade_from_palette(gb_color, palette));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Video::get_pixel_from_line(u8 byte1, u8 byte2, u8 pixel_index) -> u8 {
|
||||
using bitwise::bit_value;
|
||||
|
||||
return static_cast<u8>(
|
||||
(bit_value(byte2, 7 - pixel_index) << 1) | bit_value(byte1, 7 - pixel_index));
|
||||
}
|
||||
|
||||
auto Video::load_palette(const ByteRegister& palette_register) -> Palette {
|
||||
u8 v = palette_register.value();
|
||||
|
||||
Palette palette;
|
||||
palette.color0 = static_cast<Shade>(v & 0x3);
|
||||
palette.color1 = static_cast<Shade>((v >> 2) & 0x3);
|
||||
palette.color2 = static_cast<Shade>((v >> 4) & 0x3);
|
||||
palette.color3 = static_cast<Shade>((v >> 6) & 0x3);
|
||||
return palette;
|
||||
}
|
||||
|
||||
auto Video::get_shade_from_palette(u8 color, const Palette& palette) -> Shade {
|
||||
switch(color) {
|
||||
case 0:
|
||||
return palette.color0;
|
||||
case 1:
|
||||
return palette.color1;
|
||||
case 2:
|
||||
return palette.color2;
|
||||
default:
|
||||
return palette.color3;
|
||||
}
|
||||
}
|
||||
|
||||
void Video::draw() {
|
||||
if(vblank_callback) vblank_callback(vblank_ctx);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
#pragma once
|
||||
|
||||
#include "framebuffer.h"
|
||||
#include "address.h"
|
||||
#include "register.h"
|
||||
#include "definitions.h"
|
||||
|
||||
class Gameboy;
|
||||
|
||||
using vblank_callback_t = void (*)(void* ctx);
|
||||
|
||||
enum class VideoMode {
|
||||
ACCESS_OAM,
|
||||
ACCESS_VRAM,
|
||||
HBLANK,
|
||||
VBLANK,
|
||||
};
|
||||
|
||||
class Video {
|
||||
public:
|
||||
Video(Gameboy& inGb);
|
||||
|
||||
void tick(Cycles cycles);
|
||||
|
||||
void register_vblank_callback(vblank_callback_t cb, void* ctx) {
|
||||
vblank_callback = cb;
|
||||
vblank_ctx = ctx;
|
||||
}
|
||||
|
||||
auto vram_read(u16 offset) const -> u8 { return video_ram[offset]; }
|
||||
void vram_write(u16 offset, u8 value) { video_ram[offset] = value; }
|
||||
|
||||
/* When true, scanline/sprite rendering work is skipped (frame skip);
|
||||
* timing, interrupts and register behaviour are unaffected. */
|
||||
bool skip_render = false;
|
||||
|
||||
/* Optional per-scanline render mask (GAMEBOY_HEIGHT entries, 0 = the
|
||||
* frontend never displays this line so its pixels are not rendered).
|
||||
* Purely a display optimization: timing/interrupts are unaffected.
|
||||
* nullptr (default) renders every line. On the Flipper only 64 of the
|
||||
* 144 lines survive the downscale, so ~55% of PPU work is skipped. */
|
||||
const u8* row_mask = nullptr;
|
||||
|
||||
void set_row_mask(const u8* mask) {
|
||||
row_mask = mask;
|
||||
buffer.set_row_map(mask); /* compact storage to the visible rows */
|
||||
}
|
||||
|
||||
auto get_framebuffer() const -> const FrameBuffer& { return buffer; }
|
||||
|
||||
u8 control_byte = 0;
|
||||
|
||||
ByteRegister lcd_control;
|
||||
ByteRegister lcd_status;
|
||||
|
||||
ByteRegister scroll_y;
|
||||
ByteRegister scroll_x;
|
||||
|
||||
ByteRegister line; /* LY */
|
||||
ByteRegister ly_compare;
|
||||
|
||||
ByteRegister window_y;
|
||||
ByteRegister window_x; /* Note: x - 7 */
|
||||
|
||||
ByteRegister bg_palette;
|
||||
ByteRegister sprite_palette_0; /* OBP0 */
|
||||
ByteRegister sprite_palette_1; /* OBP1 */
|
||||
|
||||
ByteRegister dma_transfer; /* DMA */
|
||||
|
||||
private:
|
||||
void write_scanline(u8 current_line);
|
||||
void write_sprites();
|
||||
void draw();
|
||||
void draw_bg_line(uint current_line);
|
||||
void draw_window_line(uint current_line);
|
||||
void draw_sprite(uint sprite_n);
|
||||
static auto get_pixel_from_line(u8 byte1, u8 byte2, u8 pixel_index) -> u8;
|
||||
|
||||
static auto is_on_screen(int x, int y) -> bool {
|
||||
return x >= 0 && y >= 0 && x < static_cast<int>(GAMEBOY_WIDTH) &&
|
||||
y < static_cast<int>(GAMEBOY_HEIGHT);
|
||||
}
|
||||
|
||||
auto display_enabled() const -> bool;
|
||||
auto window_tile_map() const -> bool;
|
||||
auto window_enabled() const -> bool;
|
||||
auto bg_window_tile_data() const -> bool;
|
||||
auto bg_tile_map_display() const -> bool;
|
||||
auto sprite_size() const -> bool;
|
||||
auto sprites_enabled() const -> bool;
|
||||
auto bg_enabled() const -> bool;
|
||||
|
||||
static auto load_palette(const ByteRegister& palette_register) -> Palette;
|
||||
static auto get_shade_from_palette(u8 color, const Palette& palette) -> Shade;
|
||||
|
||||
Gameboy& gb;
|
||||
|
||||
FrameBuffer buffer;
|
||||
|
||||
u8 video_ram[0x2000] = {}; /* DMG: 8 KB (was 16 KB upstream) */
|
||||
|
||||
VideoMode current_mode = VideoMode::ACCESS_OAM;
|
||||
uint cycle_counter = 0;
|
||||
|
||||
vblank_callback_t vblank_callback = nullptr;
|
||||
void* vblank_ctx = nullptr;
|
||||
};
|
||||
|
||||
const uint TILES_PER_LINE = 32;
|
||||
const uint TILE_HEIGHT_PX = 8;
|
||||
const uint TILE_WIDTH_PX = 8;
|
||||
const uint TILE_BYTES = 2 * 8;
|
||||
const uint SPRITE_BYTES = 4;
|
||||
const uint BG_MAP_SIZE = 256;
|
||||
|
||||
const u16 TILE_SET_ZERO_ADDRESS = 0x8000;
|
||||
const u16 TILE_SET_ONE_ADDRESS = 0x8800;
|
||||
const u16 TILE_MAP_ZERO_ADDRESS = 0x9800;
|
||||
const u16 TILE_MAP_ONE_ADDRESS = 0x9C00;
|
||||
|
||||
/* All in machine cycles (M-cycles), matching the CPU cycle tables.
|
||||
*
|
||||
* IMPORTANT: upstream had these in T-cycles (204/80/172, 456 per scanline)
|
||||
* while the opcode tables count M-cycles (NOP = 1). The PPU counted M-cycles
|
||||
* against T-cycle constants, so every emulated frame burned 70224 M-cycles
|
||||
* of CPU emulation instead of the hardware-correct 17556: literally 4x the
|
||||
* work per frame (and 4x the timer interrupts per frame). A desktop CPU
|
||||
* hides that; on a 64 MHz Cortex-M4 it was the difference between slideshow
|
||||
* and playable. Real hardware: scanline = 456 T-cycles = 114 M-cycles. */
|
||||
const uint CLOCKS_PER_HBLANK = 51; /* Mode 0: 204 T-cycles */
|
||||
const uint CLOCKS_PER_SCANLINE_OAM = 20; /* Mode 2: 80 T-cycles */
|
||||
const uint CLOCKS_PER_SCANLINE_VRAM = 43; /* Mode 3: 172 T-cycles */
|
||||
const uint CLOCKS_PER_SCANLINE =
|
||||
(CLOCKS_PER_SCANLINE_OAM + CLOCKS_PER_SCANLINE_VRAM + CLOCKS_PER_HBLANK);
|
||||
|
||||
const uint CLOCKS_PER_VBLANK = 1140; /* Mode 1: 4560 T-cycles */
|
||||
const uint SCANLINES_PER_FRAME = 144;
|
||||
const uint CLOCKS_PER_FRAME = (CLOCKS_PER_SCANLINE * SCANLINES_PER_FRAME) + CLOCKS_PER_VBLANK;
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,188 @@
|
||||
/* Host test harness: runs a ROM headless on the PC and captures the serial
|
||||
* output (Blargg's test ROMs print their results through the serial port).
|
||||
* Usage: hosttest <rom.gb> [max_frames] [--dump-frame N]
|
||||
*/
|
||||
|
||||
#include "../gb/gameboy.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
static u8* g_rom = nullptr;
|
||||
static long g_rom_size = 0;
|
||||
|
||||
static char g_serial[4096];
|
||||
static unsigned g_serial_len = 0;
|
||||
|
||||
extern "C" void gb_fatal(const char* msg) {
|
||||
fprintf(stderr, "FATAL: %s\n", msg);
|
||||
exit(2);
|
||||
}
|
||||
|
||||
static void serial_hook(u8 byte) {
|
||||
if(g_serial_len < sizeof(g_serial) - 1) {
|
||||
g_serial[g_serial_len++] = static_cast<char>(byte);
|
||||
g_serial[g_serial_len] = 0;
|
||||
fputc(byte, stdout);
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
static const u8* bank_provider(void* /*ctx*/, uint bank) {
|
||||
long offset = static_cast<long>(bank) * 0x4000;
|
||||
if(offset + 0x4000 > g_rom_size) offset = 0;
|
||||
return g_rom + offset;
|
||||
}
|
||||
|
||||
static FrameBuffer g_last_frame;
|
||||
|
||||
static void frame_hook(void* ctx) {
|
||||
g_last_frame = static_cast<Gameboy*>(ctx)->get_framebuffer();
|
||||
}
|
||||
|
||||
/* Same dominant-voice heuristic the Flipper frontend uses for the piezo */
|
||||
static bool pick_voice(Gameboy* gb, ApuVoice* out, int* out_ch) {
|
||||
ApuVoice v;
|
||||
bool have = false;
|
||||
for(uint n = 0; n < 2; n++) {
|
||||
gb->apu.get_voice(n, &v);
|
||||
if(!v.active) continue;
|
||||
if(!have || v.volume > out->volume ||
|
||||
(v.volume == out->volume && v.order > out->order)) {
|
||||
*out = v;
|
||||
*out_ch = (int)n;
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
if(!have) {
|
||||
gb->apu.get_voice(2, &v);
|
||||
if(v.active) {
|
||||
*out = v;
|
||||
*out_ch = 2;
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
if(!have) {
|
||||
gb->apu.get_voice(3, &v);
|
||||
if(v.active) {
|
||||
*out = v;
|
||||
*out_ch = 3;
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
return have;
|
||||
}
|
||||
|
||||
static void dump_frame_ascii(const FrameBuffer& fb) {
|
||||
const char* shades = " .*#";
|
||||
/* downsample x2 for terminal readability */
|
||||
for(uint y = 0; y < GAMEBOY_HEIGHT; y += 2) {
|
||||
for(uint x = 0; x < GAMEBOY_WIDTH; x += 2) {
|
||||
printf("%c", shades[fb.get_pixel(x, y)]);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if(argc < 2) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"usage: %s <rom.gb> [max_frames] [--dump-frame] [--rowmask] [--dump-audio]\n",
|
||||
argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
long max_frames = argc > 2 ? atol(argv[2]) : 2000;
|
||||
bool dump = false;
|
||||
bool rowmask = false;
|
||||
bool dump_audio = false;
|
||||
for(int i = 1; i < argc; i++) {
|
||||
if(!strcmp(argv[i], "--dump-frame")) dump = true;
|
||||
if(!strcmp(argv[i], "--rowmask")) rowmask = true;
|
||||
if(!strcmp(argv[i], "--dump-audio")) dump_audio = true;
|
||||
}
|
||||
|
||||
/* Same 144 -> 64 line subsampling the Flipper frontend uses */
|
||||
static u8 mask[GAMEBOY_HEIGHT];
|
||||
for(uint y = 0; y < 64; y++)
|
||||
mask[(y * GAMEBOY_HEIGHT) / 64] = 1;
|
||||
|
||||
FILE* f = fopen(argv[1], "rb");
|
||||
if(!f) {
|
||||
fprintf(stderr, "cannot open %s\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
g_rom_size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
g_rom = static_cast<u8*>(malloc(static_cast<size_t>(g_rom_size)));
|
||||
if(fread(g_rom, 1, static_cast<size_t>(g_rom_size), f) != static_cast<size_t>(g_rom_size)) {
|
||||
fprintf(stderr, "short read\n");
|
||||
return 1;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
gb_serial_hook = serial_hook;
|
||||
|
||||
MBCType mbc = Cartridge::parse_mbc(g_rom[0x147]);
|
||||
if(mbc == MBCType::Unsupported) {
|
||||
fprintf(stderr, "unsupported mapper 0x%02X\n", g_rom[0x147]);
|
||||
return 1;
|
||||
}
|
||||
uint banks = Cartridge::rom_bank_count_from_header(g_rom[0x148]);
|
||||
u32 ram_size = Cartridge::ram_size_from_header(g_rom[0x149], mbc);
|
||||
u8* cart_ram = ram_size ? static_cast<u8*>(calloc(1, ram_size)) : nullptr;
|
||||
|
||||
fprintf(
|
||||
stderr,
|
||||
"rom: %ld bytes, mapper=%d, banks=%u, cart_ram=%u\n",
|
||||
g_rom_size,
|
||||
static_cast<int>(mbc),
|
||||
banks,
|
||||
ram_size);
|
||||
|
||||
auto* gb = new Gameboy(g_rom, banks, mbc, cart_ram, ram_size, bank_provider, nullptr);
|
||||
gb->set_frame_callback(frame_hook, gb);
|
||||
if(rowmask) gb->set_row_mask(mask);
|
||||
|
||||
u32 last_freq = 0;
|
||||
int last_ch = -1;
|
||||
u8 last_vol = 0;
|
||||
|
||||
for(long frame = 0; frame < max_frames; frame++) {
|
||||
gb->run_to_vblank();
|
||||
|
||||
if(dump_audio) {
|
||||
ApuVoice v;
|
||||
int ch = -1;
|
||||
bool have = pick_voice(gb, &v, &ch);
|
||||
u32 f = have ? v.freq_hz : 0;
|
||||
u8 vol = have ? v.volume : 0;
|
||||
if(f != last_freq || ch != last_ch || vol != last_vol) {
|
||||
if(have)
|
||||
printf("f=%05ld ch%d %5u Hz vol=%2u\n", frame, ch, f, vol);
|
||||
else
|
||||
printf("f=%05ld silence\n", frame);
|
||||
last_freq = f;
|
||||
last_ch = ch;
|
||||
last_vol = vol;
|
||||
}
|
||||
}
|
||||
|
||||
if(strstr(g_serial, "Passed") || strstr(g_serial, "Failed")) {
|
||||
/* let it print the tail */
|
||||
for(int i = 0; i < 30; i++)
|
||||
gb->run_to_vblank();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(dump) dump_frame_ascii(g_last_frame);
|
||||
|
||||
printf("\n");
|
||||
if(strstr(g_serial, "Passed")) return 0;
|
||||
if(strstr(g_serial, "Failed")) return 3;
|
||||
return 4; /* no verdict */
|
||||
}
|
||||
@@ -0,0 +1,942 @@
|
||||
/* FlipGB - Game Boy (DMG) emulator frontend for Flipper Zero.
|
||||
*
|
||||
* Frontend responsibilities:
|
||||
* - ROM picker (system file browser), ROM header parsing
|
||||
* - ROM bank streaming from SD with an LRU cache (whole ROM loaded to RAM
|
||||
* when it fits)
|
||||
* - 160x144 2bpp -> 128x64 1bpp ordered-dither downscale
|
||||
* - input mapping (OK=A, Back=B, Up+Down together = emulator menu)
|
||||
* - emulator menu: Start/Select injection, frameskip, save SRAM, exit
|
||||
* - battery save (.sav) persistence next to the ROM
|
||||
*/
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <new>
|
||||
|
||||
#include "gb/gameboy.h"
|
||||
|
||||
#define SCREEN_W 128
|
||||
#define SCREEN_H 64
|
||||
#define BUFFER_SIZE (SCREEN_W * SCREEN_H / 8)
|
||||
|
||||
#define GB_FRAME_US 16742 /* 59.73 Hz */
|
||||
#define BANK_SIZE 0x4000u
|
||||
/* Heap kept free for the GUI/system while playing. 10 KB is enough for the
|
||||
* direct-draw takeover + input subscription + background services; the old
|
||||
* 24 KB reserve was more than one whole ROM bank of wasted headroom and
|
||||
* pushed big cartridges (1 MB MBC3 + 32 KB SRAM) into "Not enough RAM". */
|
||||
#define HEAP_RESERVE (10u * 1024u)
|
||||
#define ALLOC_MARGIN 1024u /* never take the very last heap block */
|
||||
|
||||
/* On the Flipper malloc() does NOT return NULL on failure: it crashes the
|
||||
* whole firmware with "out of memory". Every allocation whose size depends
|
||||
* on the ROM must go through this wrapper, which checks the largest free
|
||||
* heap block first (total free heap is not enough: fragmentation can make
|
||||
* a big contiguous allocation impossible even with plenty of free bytes). */
|
||||
static void* safe_malloc(size_t size) {
|
||||
if(memmgr_heap_get_max_free_block() < size + ALLOC_MARGIN) return NULL;
|
||||
return malloc(size);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ state */
|
||||
|
||||
/* GB button bits used between the input callback and the main loop */
|
||||
enum {
|
||||
KBIT_UP = 1 << 0,
|
||||
KBIT_DOWN = 1 << 1,
|
||||
KBIT_LEFT = 1 << 2,
|
||||
KBIT_RIGHT = 1 << 3,
|
||||
KBIT_A = 1 << 4,
|
||||
KBIT_B = 1 << 5,
|
||||
};
|
||||
|
||||
/* ROM bank cache.
|
||||
*
|
||||
* Every switchable bank lives in its own 16 KB heap block: there is no
|
||||
* "whole ROM in one contiguous malloc" fast path any more, because a large
|
||||
* contiguous allocation is exactly what fails (and used to crash the
|
||||
* firmware) on a fragmented heap. Instead:
|
||||
*
|
||||
* - as many 16 KB slots as the heap safely affords are allocated up front;
|
||||
* - if every switchable bank got a slot, the ROM is fully resident: bank
|
||||
* lookup is a direct O(1) index and the SD file is closed;
|
||||
* - otherwise the slots form an LRU cache streaming banks from SD.
|
||||
*/
|
||||
typedef struct {
|
||||
File* file; /* NULL once the ROM is fully resident */
|
||||
u8* bank0;
|
||||
u8** slots; /* num_slots pointers to 16 KB blocks */
|
||||
u16* slot_bank; /* which bank each slot holds (0 = empty) */
|
||||
u32* slot_use; /* LRU stamps (streaming mode only) */
|
||||
u32 use_counter;
|
||||
u16 num_slots;
|
||||
u16 banks;
|
||||
bool fully_loaded; /* slots[i] permanently holds bank i+1 */
|
||||
} RomCache;
|
||||
|
||||
typedef struct {
|
||||
uint8_t screen[BUFFER_SIZE]; /* 1bpp page-format, bit set = light */
|
||||
|
||||
Gui* gui;
|
||||
Canvas* canvas;
|
||||
FuriMutex* fb_mutex;
|
||||
|
||||
volatile uint8_t keys; /* KBIT_* currently pressed */
|
||||
volatile bool menu_requested;
|
||||
volatile bool menu_active;
|
||||
volatile bool exit_requested;
|
||||
|
||||
Gameboy* gb;
|
||||
RomCache rom;
|
||||
u8* cart_ram;
|
||||
u32 cart_ram_size;
|
||||
bool has_battery;
|
||||
|
||||
/* sound (single-tone piezo fed with the dominant APU voice) */
|
||||
bool sound_enabled;
|
||||
bool speaker_acquired;
|
||||
uint32_t tone_freq; /* currently playing tone, 0 = silent */
|
||||
uint8_t tone_vol; /* 0..15, scaled by master volume */
|
||||
|
||||
/* menu */
|
||||
int menu_cursor;
|
||||
uint8_t menu_last_keys;
|
||||
int frameskip_setting; /* -1 = auto, 0..4 fixed */
|
||||
int inject_start_frames;
|
||||
int inject_select_frames;
|
||||
char rom_title[17];
|
||||
char status_msg[32];
|
||||
|
||||
/* pacing */
|
||||
uint32_t emu_ms_ema; /* x16 fixed point */
|
||||
int auto_skip;
|
||||
int skip_phase;
|
||||
} AppState;
|
||||
|
||||
static AppState* g_app = NULL;
|
||||
|
||||
static volatile uint32_t s_input_cb_inflight = 0;
|
||||
static volatile uint32_t s_fb_cb_inflight = 0;
|
||||
|
||||
static inline void wait_inflight_zero(volatile uint32_t* counter) {
|
||||
while(__atomic_load_n(counter, __ATOMIC_ACQUIRE) != 0) {
|
||||
furi_delay_ms(1);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" [[noreturn]] void gb_fatal(const char* msg) {
|
||||
furi_crash(msg);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------- rom bank provider */
|
||||
|
||||
static const u8* rom_bank_provider(void* ctx, uint bank) {
|
||||
RomCache* rc = (RomCache*)ctx;
|
||||
|
||||
if(bank == 0) return rc->bank0;
|
||||
|
||||
if(rc->fully_loaded) {
|
||||
return rc->slots[bank - 1]; /* O(1), the whole ROM is resident */
|
||||
}
|
||||
|
||||
/* cache lookup */
|
||||
u16 lru = 0;
|
||||
u32 lru_use = 0xFFFFFFFFu;
|
||||
for(u16 i = 0; i < rc->num_slots; i++) {
|
||||
if(rc->slot_bank[i] == bank) {
|
||||
rc->slot_use[i] = ++rc->use_counter;
|
||||
return rc->slots[i];
|
||||
}
|
||||
if(rc->slot_use[i] < lru_use) {
|
||||
lru_use = rc->slot_use[i];
|
||||
lru = i;
|
||||
}
|
||||
}
|
||||
|
||||
/* miss: stream the bank from SD into the LRU slot */
|
||||
storage_file_seek(rc->file, (u32)bank * BANK_SIZE, true);
|
||||
size_t got = storage_file_read(rc->file, rc->slots[lru], BANK_SIZE);
|
||||
if(got < BANK_SIZE) {
|
||||
/* short read (SD hiccup / truncated ROM): open-bus instead of
|
||||
* stale data from whatever bank lived here before */
|
||||
memset(rc->slots[lru] + got, 0xFF, BANK_SIZE - got);
|
||||
}
|
||||
rc->slot_bank[lru] = (u16)bank;
|
||||
rc->slot_use[lru] = ++rc->use_counter;
|
||||
return rc->slots[lru];
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- rendering */
|
||||
|
||||
/* Destination -> source coordinate maps (128 <- 160, 64 <- 144) */
|
||||
static uint8_t s_xmap[SCREEN_W];
|
||||
static uint8_t s_ymap[SCREEN_H];
|
||||
|
||||
/* Scanlines that actually survive the 144 -> 64 downscale. Handed to the
|
||||
* emulator core so the PPU skips the other 80 lines entirely (~55% less
|
||||
* rendering work per frame). */
|
||||
static u8 s_rowmask[GAMEBOY_HEIGHT];
|
||||
|
||||
/* light/dark decision per (y-parity, x-parity, shade): 2x2 ordered dither.
|
||||
* white -> lit, light gray -> 3/4 lit, dark gray -> 1/4 lit, black -> off */
|
||||
static uint8_t s_dither[2][2][4];
|
||||
|
||||
static void init_scale_maps(void) {
|
||||
for(int x = 0; x < SCREEN_W; x++)
|
||||
s_xmap[x] = (uint8_t)((x * (int)GAMEBOY_WIDTH) / SCREEN_W);
|
||||
for(int y = 0; y < SCREEN_H; y++)
|
||||
s_ymap[y] = (uint8_t)((y * (int)GAMEBOY_HEIGHT) / SCREEN_H);
|
||||
|
||||
memset(s_rowmask, 0, sizeof(s_rowmask));
|
||||
for(int y = 0; y < SCREEN_H; y++)
|
||||
s_rowmask[s_ymap[y]] = 1;
|
||||
|
||||
for(int py = 0; py < 2; py++) {
|
||||
for(int px = 0; px < 2; px++) {
|
||||
s_dither[py][px][0] = 1;
|
||||
s_dither[py][px][1] = (px == 1 && py == 1) ? 0 : 1;
|
||||
s_dither[py][px][2] = (px == 0 && py == 0) ? 1 : 0;
|
||||
s_dither[py][px][3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Called by the core on every vblank, before the framebuffer is cleared.
|
||||
* Converts 4 shades -> 1 bit with a 2x2 ordered dither. */
|
||||
static void frame_callback(void* ctx) {
|
||||
AppState* app = (AppState*)ctx;
|
||||
const u8* raw = app->gb->get_framebuffer().raw(); /* packed 2bpp */
|
||||
|
||||
furi_mutex_acquire(app->fb_mutex, FuriWaitForever);
|
||||
|
||||
uint8_t* dst = app->screen;
|
||||
for(int y = 0; y < SCREEN_H; y++) {
|
||||
/* storage is compacted to the 64 displayed rows in ascending order
|
||||
* (GB_FB_ROWS=64 + row mask), so displayed row y == storage slot y */
|
||||
uint base = (uint)y * GAMEBOY_WIDTH;
|
||||
uint8_t bit = (uint8_t)(1u << (y & 7));
|
||||
uint8_t nbit = (uint8_t)~bit;
|
||||
uint8_t* row = dst + (y >> 3) * SCREEN_W;
|
||||
const uint8_t(*dither)[4] = s_dither[y & 1];
|
||||
|
||||
for(int x = 0; x < SCREEN_W; x++) {
|
||||
uint i = base + s_xmap[x];
|
||||
uint s = (raw[i >> 2] >> ((i & 3) << 1)) & 0x3;
|
||||
if(dither[x & 1][s])
|
||||
row[x] |= bit;
|
||||
else
|
||||
row[x] &= nbit;
|
||||
}
|
||||
}
|
||||
|
||||
furi_mutex_release(app->fb_mutex);
|
||||
}
|
||||
|
||||
static void framebuffer_commit_callback(
|
||||
uint8_t* data,
|
||||
size_t size,
|
||||
CanvasOrientation orientation,
|
||||
void* context) {
|
||||
__atomic_fetch_add(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
|
||||
AppState* app = (AppState*)context;
|
||||
(void)orientation;
|
||||
|
||||
if(!app || !data || size < BUFFER_SIZE || app->menu_active) {
|
||||
/* in menu mode the canvas content (drawn with canvas_*) passes
|
||||
* through untouched */
|
||||
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
return;
|
||||
}
|
||||
|
||||
if(furi_mutex_acquire(app->fb_mutex, 0) != FuriStatusOk) {
|
||||
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
return;
|
||||
}
|
||||
|
||||
/* screen buffer: bit=1 means light; display buffer: bit=1 means dark */
|
||||
const uint8_t* src = app->screen;
|
||||
for(size_t i = 0; i < BUFFER_SIZE; i++) {
|
||||
data[i] = (uint8_t)(src[i] ^ 0xFF);
|
||||
}
|
||||
|
||||
furi_mutex_release(app->fb_mutex);
|
||||
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ input */
|
||||
|
||||
static void input_events_callback(const void* value, void* ctx) {
|
||||
if(!value || !ctx) return;
|
||||
|
||||
__atomic_fetch_add(&s_input_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
|
||||
AppState* app = (AppState*)ctx;
|
||||
const InputEvent* event = (const InputEvent*)value;
|
||||
|
||||
uint8_t bit = 0;
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
bit = KBIT_UP;
|
||||
break;
|
||||
case InputKeyDown:
|
||||
bit = KBIT_DOWN;
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
bit = KBIT_LEFT;
|
||||
break;
|
||||
case InputKeyRight:
|
||||
bit = KBIT_RIGHT;
|
||||
break;
|
||||
case InputKeyOk:
|
||||
bit = KBIT_A;
|
||||
break;
|
||||
case InputKeyBack:
|
||||
bit = KBIT_B;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(bit) {
|
||||
if((event->type == InputTypePress) || (event->type == InputTypeRepeat)) {
|
||||
uint8_t keys =
|
||||
(uint8_t)__atomic_or_fetch((uint8_t*)&app->keys, bit, __ATOMIC_RELAXED);
|
||||
/* Up+Down together: physically impossible on a real GB d-pad,
|
||||
* so it is our reserved menu gesture */
|
||||
if((keys & (KBIT_UP | KBIT_DOWN)) == (KBIT_UP | KBIT_DOWN)) {
|
||||
app->menu_requested = true;
|
||||
}
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
(void)__atomic_fetch_and((uint8_t*)&app->keys, (uint8_t)~bit, __ATOMIC_RELAXED);
|
||||
}
|
||||
}
|
||||
|
||||
__atomic_fetch_sub(&s_input_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
/* Apply the current key snapshot to the emulated joypad (edge based) */
|
||||
static void apply_input(AppState* app, uint8_t* last_applied) {
|
||||
uint8_t now = app->menu_active ? 0 : app->keys;
|
||||
uint8_t changed = (uint8_t)(now ^ *last_applied);
|
||||
if(!changed && !app->inject_start_frames && !app->inject_select_frames) return;
|
||||
|
||||
struct {
|
||||
uint8_t bit;
|
||||
GbButton btn;
|
||||
} map[] = {
|
||||
{KBIT_UP, GbButton::Up},
|
||||
{KBIT_DOWN, GbButton::Down},
|
||||
{KBIT_LEFT, GbButton::Left},
|
||||
{KBIT_RIGHT, GbButton::Right},
|
||||
{KBIT_A, GbButton::A},
|
||||
{KBIT_B, GbButton::B},
|
||||
};
|
||||
|
||||
for(auto& m : map) {
|
||||
if(!(changed & m.bit)) continue;
|
||||
if(now & m.bit)
|
||||
app->gb->button_pressed(m.btn);
|
||||
else
|
||||
app->gb->button_released(m.btn);
|
||||
}
|
||||
|
||||
/* menu-injected Start/Select presses (held for a few frames) */
|
||||
if(app->inject_start_frames > 0) {
|
||||
app->inject_start_frames--;
|
||||
if(app->inject_start_frames == 0)
|
||||
app->gb->button_released(GbButton::Start);
|
||||
}
|
||||
if(app->inject_select_frames > 0) {
|
||||
app->inject_select_frames--;
|
||||
if(app->inject_select_frames == 0)
|
||||
app->gb->button_released(GbButton::Select);
|
||||
}
|
||||
|
||||
*last_applied = now;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ sound */
|
||||
|
||||
/* The Flipper piezo plays one frequency at one volume at a time, so the
|
||||
* 4 APU channels are reduced to the "dominant voice":
|
||||
* 1. the louder of the two pulse channels (they carry the melody in
|
||||
* almost every GB soundtrack); newer trigger wins ties (lead line)
|
||||
* 2. otherwise the wave channel (bass/secondary melody)
|
||||
* 3. otherwise noise (percussion), mapped to a short low buzz
|
||||
* Updated once per emulated frame (~60 Hz), like a tracker row. */
|
||||
static void sound_update(AppState* app, bool force_silent) {
|
||||
if(!app->speaker_acquired) return;
|
||||
|
||||
ApuVoice best = {false, 0, 0, 0};
|
||||
bool have = false;
|
||||
|
||||
if(!force_silent && app->sound_enabled && !app->menu_active) {
|
||||
ApuVoice v;
|
||||
/* pulse 1 / pulse 2 */
|
||||
for(uint n = 0; n < 2; n++) {
|
||||
app->gb->apu.get_voice(n, &v);
|
||||
if(!v.active) continue;
|
||||
if(!have || v.volume > best.volume ||
|
||||
(v.volume == best.volume && v.order > best.order)) {
|
||||
best = v;
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
/* wave */
|
||||
if(!have) {
|
||||
app->gb->apu.get_voice(2, &v);
|
||||
if(v.active) {
|
||||
best = v;
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
/* noise: LFSR clock -> percussive buzz in the piezo's low range */
|
||||
if(!have) {
|
||||
app->gb->apu.get_voice(3, &v);
|
||||
if(v.active) {
|
||||
uint32_t f = v.freq_hz >> 5;
|
||||
if(f < 80) f = 80;
|
||||
if(f > 400) f = 400;
|
||||
v.freq_hz = f;
|
||||
best = v;
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(have) {
|
||||
uint32_t f = best.freq_hz;
|
||||
if(f < 40 || f > 12000) {
|
||||
have = false; /* outside anything the piezo can render */
|
||||
} else {
|
||||
uint8_t master = app->gb->apu.master_volume(); /* 0..7 */
|
||||
uint8_t vol = (uint8_t)((best.volume * (master + 1)) >> 3); /* 0..15 */
|
||||
if(vol == 0) {
|
||||
have = false;
|
||||
} else if(f != app->tone_freq || vol != app->tone_vol) {
|
||||
furi_hal_speaker_start((float)f, (float)vol / 15.0f);
|
||||
app->tone_freq = f;
|
||||
app->tone_vol = vol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!have && app->tone_freq) {
|
||||
furi_hal_speaker_stop();
|
||||
app->tone_freq = 0;
|
||||
app->tone_vol = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------- save */
|
||||
|
||||
static void save_path_for_rom(FuriString* rom_path, FuriString* out) {
|
||||
/* parentheses bypass the C11 _Generic macro (not usable from C++) */
|
||||
(furi_string_set)(out, rom_path);
|
||||
furi_string_cat_str(out, ".sav");
|
||||
}
|
||||
|
||||
static bool save_sram(AppState* app, Storage* storage, FuriString* sav_path) {
|
||||
if(!app->cart_ram || !app->cart_ram_size || !app->has_battery) return false;
|
||||
|
||||
File* f = storage_file_alloc(storage);
|
||||
bool ok = false;
|
||||
if(storage_file_open(f, furi_string_get_cstr(sav_path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
||||
ok = storage_file_write(f, app->cart_ram, app->cart_ram_size) == app->cart_ram_size;
|
||||
storage_file_close(f);
|
||||
}
|
||||
storage_file_free(f);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void load_sram(AppState* app, Storage* storage, FuriString* sav_path) {
|
||||
if(!app->cart_ram || !app->cart_ram_size) return;
|
||||
|
||||
File* f = storage_file_alloc(storage);
|
||||
if(storage_file_open(f, furi_string_get_cstr(sav_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
|
||||
storage_file_read(f, app->cart_ram, app->cart_ram_size);
|
||||
storage_file_close(f);
|
||||
}
|
||||
storage_file_free(f);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------- menu */
|
||||
|
||||
#define MENU_ITEMS 7
|
||||
|
||||
static void menu_draw(AppState* app) {
|
||||
Canvas* c = app->canvas;
|
||||
canvas_reset(c);
|
||||
canvas_clear(c);
|
||||
|
||||
canvas_set_font(c, FontPrimary);
|
||||
canvas_draw_str(c, 2, 10, app->rom_title[0] ? app->rom_title : "FlipGB");
|
||||
canvas_draw_line(c, 0, 12, 127, 12);
|
||||
|
||||
static const char* labels[MENU_ITEMS] = {
|
||||
"Continue",
|
||||
"Press START",
|
||||
"Press SELECT",
|
||||
"Frameskip",
|
||||
"Sound",
|
||||
"Save SRAM",
|
||||
"Exit",
|
||||
};
|
||||
|
||||
canvas_set_font(c, FontSecondary);
|
||||
|
||||
for(int i = 0; i < MENU_ITEMS; i++) {
|
||||
/* 7 px pitch keeps all 7 items on the 64 px screen */
|
||||
int y = 20 + i * 7;
|
||||
if(i == app->menu_cursor) {
|
||||
canvas_draw_str(c, 2, y, ">");
|
||||
}
|
||||
canvas_draw_str(c, 10, y, labels[i]);
|
||||
|
||||
if(i == 3) {
|
||||
char buf[12];
|
||||
if(app->frameskip_setting < 0)
|
||||
snprintf(buf, sizeof(buf), "auto(%d)", app->auto_skip);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%d", app->frameskip_setting);
|
||||
canvas_draw_str(c, 72, y, buf);
|
||||
}
|
||||
if(i == 4) {
|
||||
canvas_draw_str(
|
||||
c, 72, y, !app->speaker_acquired ? "n/a" : (app->sound_enabled ? "on" : "off"));
|
||||
}
|
||||
}
|
||||
|
||||
if(app->status_msg[0]) {
|
||||
canvas_draw_str(c, 70, 10, app->status_msg);
|
||||
} else {
|
||||
/* free heap indicator: helps spotting memory pressure on device */
|
||||
char rambuf[16];
|
||||
snprintf(rambuf, sizeof(rambuf), "%uk free", (unsigned)(memmgr_get_free_heap() / 1024u));
|
||||
canvas_draw_str_aligned(c, 126, 10, AlignRight, AlignBottom, rambuf);
|
||||
}
|
||||
|
||||
canvas_commit(c);
|
||||
}
|
||||
|
||||
/* returns true while the menu stays open */
|
||||
static bool menu_tick(AppState* app, Storage* storage, FuriString* sav_path) {
|
||||
uint8_t keys = app->keys;
|
||||
uint8_t pressed = (uint8_t)(keys & ~app->menu_last_keys);
|
||||
app->menu_last_keys = keys;
|
||||
|
||||
if(pressed & KBIT_UP) {
|
||||
app->menu_cursor = (app->menu_cursor + MENU_ITEMS - 1) % MENU_ITEMS;
|
||||
app->status_msg[0] = 0;
|
||||
}
|
||||
if(pressed & KBIT_DOWN) {
|
||||
app->menu_cursor = (app->menu_cursor + 1) % MENU_ITEMS;
|
||||
app->status_msg[0] = 0;
|
||||
}
|
||||
|
||||
if(pressed & (KBIT_LEFT | KBIT_RIGHT)) {
|
||||
if(app->menu_cursor == 3) {
|
||||
/* cycle: auto, 0, 1, 2, 3, 4 */
|
||||
int v = app->frameskip_setting;
|
||||
if(pressed & KBIT_RIGHT)
|
||||
v = (v >= 4) ? -1 : v + 1;
|
||||
else
|
||||
v = (v <= -1) ? 4 : v - 1;
|
||||
app->frameskip_setting = v;
|
||||
}
|
||||
if(app->menu_cursor == 4 && app->speaker_acquired) {
|
||||
app->sound_enabled = !app->sound_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
if(pressed & KBIT_B) return false; /* Back closes the menu */
|
||||
|
||||
if(pressed & KBIT_A) {
|
||||
switch(app->menu_cursor) {
|
||||
case 0:
|
||||
return false;
|
||||
case 1:
|
||||
app->gb->button_pressed(GbButton::Start);
|
||||
app->inject_start_frames = 8;
|
||||
return false;
|
||||
case 2:
|
||||
app->gb->button_pressed(GbButton::Select);
|
||||
app->inject_select_frames = 8;
|
||||
return false;
|
||||
case 3:
|
||||
break;
|
||||
case 4:
|
||||
if(app->speaker_acquired) app->sound_enabled = !app->sound_enabled;
|
||||
break;
|
||||
case 5:
|
||||
if(app->has_battery) {
|
||||
bool ok = save_sram(app, storage, sav_path);
|
||||
snprintf(app->status_msg, sizeof(app->status_msg), ok ? "saved!" : "error");
|
||||
} else {
|
||||
snprintf(app->status_msg, sizeof(app->status_msg), "no battery");
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
app->exit_requested = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
menu_draw(app);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- rom setup */
|
||||
|
||||
typedef enum {
|
||||
RomLoadOk,
|
||||
RomLoadIoError,
|
||||
RomLoadCgbOnly,
|
||||
RomLoadBadMapper,
|
||||
RomLoadNoMem,
|
||||
} RomLoadResult;
|
||||
|
||||
static RomLoadResult
|
||||
rom_load(AppState* app, Storage* storage, const char* path, MBCType* out_mbc) {
|
||||
RomCache* rc = &app->rom;
|
||||
|
||||
rc->file = storage_file_alloc(storage);
|
||||
if(!storage_file_open(rc->file, path, FSAM_READ, FSOM_OPEN_EXISTING)) {
|
||||
return RomLoadIoError;
|
||||
}
|
||||
|
||||
/* bank 0 stays resident and contains the header */
|
||||
rc->bank0 = (u8*)safe_malloc(BANK_SIZE);
|
||||
if(!rc->bank0) return RomLoadNoMem;
|
||||
memset(rc->bank0, 0xFF, BANK_SIZE);
|
||||
if(storage_file_read(rc->file, rc->bank0, BANK_SIZE) < 0x150) {
|
||||
return RomLoadIoError;
|
||||
}
|
||||
|
||||
/* header */
|
||||
u8 cgb_flag = rc->bank0[0x143];
|
||||
if(cgb_flag == 0xC0) return RomLoadCgbOnly;
|
||||
|
||||
MBCType mbc = Cartridge::parse_mbc(rc->bank0[0x147]);
|
||||
if(mbc == MBCType::Unsupported) return RomLoadBadMapper;
|
||||
*out_mbc = mbc;
|
||||
|
||||
memcpy(app->rom_title, &rc->bank0[0x134], 16);
|
||||
app->rom_title[16] = 0;
|
||||
for(int i = 0; i < 16; i++) {
|
||||
char ch = app->rom_title[i];
|
||||
if(ch != 0 && (ch < 0x20 || ch > 0x7E)) app->rom_title[i] = 0;
|
||||
}
|
||||
|
||||
rc->banks = (u16)Cartridge::rom_bank_count_from_header(rc->bank0[0x148]);
|
||||
app->has_battery = Cartridge::has_battery(rc->bank0[0x147]);
|
||||
|
||||
/* cartridge RAM */
|
||||
app->cart_ram_size = Cartridge::ram_size_from_header(rc->bank0[0x149], mbc);
|
||||
if(app->cart_ram_size) {
|
||||
app->cart_ram = (u8*)safe_malloc(app->cart_ram_size);
|
||||
if(!app->cart_ram) return RomLoadNoMem;
|
||||
memset(app->cart_ram, 0, app->cart_ram_size);
|
||||
}
|
||||
|
||||
/* Allocate as many 16 KB bank slots as the heap safely affords.
|
||||
*
|
||||
* IMPORTANT: the emulator core (Gameboy: 8K VRAM + 8K WRAM + packed
|
||||
* framebuffer + CPU state, ~22 KB) plus the GUI takeover (mutex, canvas,
|
||||
* input subscription) are allocated AFTER the ROM cache, so room for
|
||||
* them is reserved up front. Each slot is its own allocation: no huge
|
||||
* contiguous block is ever requested, which makes the loader immune to
|
||||
* heap fragmentation (the old single-malloc full-ROM path could crash
|
||||
* the firmware even when the total free heap looked sufficient). */
|
||||
u32 switchable = (u32)(rc->banks - 1);
|
||||
const size_t reserve = HEAP_RESERVE + sizeof(Gameboy) + 1024 /* alloc slack */;
|
||||
|
||||
/* slot bookkeeping arrays (a few bytes per slot) */
|
||||
size_t free_heap = memmgr_get_free_heap();
|
||||
u32 max_slots = (u32)(free_heap / BANK_SIZE) + 1;
|
||||
if(max_slots > switchable) max_slots = switchable;
|
||||
if(max_slots < 1) max_slots = 1;
|
||||
|
||||
rc->slots = (u8**)safe_malloc(max_slots * sizeof(u8*));
|
||||
rc->slot_bank = (u16*)safe_malloc(max_slots * sizeof(u16));
|
||||
rc->slot_use = (u32*)safe_malloc(max_slots * sizeof(u32));
|
||||
if(!rc->slots || !rc->slot_bank || !rc->slot_use) return RomLoadNoMem;
|
||||
|
||||
rc->num_slots = 0;
|
||||
while((u32)rc->num_slots < max_slots) {
|
||||
if(memmgr_get_free_heap() < reserve + BANK_SIZE + ALLOC_MARGIN) break;
|
||||
u8* slot = (u8*)safe_malloc(BANK_SIZE);
|
||||
if(!slot) break;
|
||||
rc->slots[rc->num_slots] = slot;
|
||||
rc->slot_bank[rc->num_slots] = 0; /* bank 0 never lives in a slot */
|
||||
rc->slot_use[rc->num_slots] = 0;
|
||||
rc->num_slots++;
|
||||
}
|
||||
|
||||
/* not even one 16 KB slot fits: fail gracefully with the
|
||||
* "Not enough RAM" dialog instead of crashing inside malloc() */
|
||||
if(rc->num_slots == 0) return RomLoadNoMem;
|
||||
|
||||
if((u32)rc->num_slots >= switchable) {
|
||||
/* every switchable bank fits: preload the whole ROM and close the
|
||||
* SD file (O(1) bank switching, zero stutter, frees the handle) */
|
||||
storage_file_seek(rc->file, BANK_SIZE, true);
|
||||
for(u32 i = 0; i < switchable; i++) {
|
||||
size_t got = storage_file_read(rc->file, rc->slots[i], BANK_SIZE);
|
||||
if(got < BANK_SIZE) memset(rc->slots[i] + got, 0xFF, BANK_SIZE - got);
|
||||
rc->slot_bank[i] = (u16)(i + 1);
|
||||
}
|
||||
rc->fully_loaded = true;
|
||||
storage_file_close(rc->file);
|
||||
storage_file_free(rc->file);
|
||||
rc->file = NULL;
|
||||
}
|
||||
|
||||
return RomLoadOk;
|
||||
}
|
||||
|
||||
static void rom_free(AppState* app) {
|
||||
RomCache* rc = &app->rom;
|
||||
for(u16 i = 0; i < rc->num_slots; i++)
|
||||
if(rc->slots[i]) free(rc->slots[i]);
|
||||
if(rc->slots) free(rc->slots);
|
||||
if(rc->slot_bank) free(rc->slot_bank);
|
||||
if(rc->slot_use) free(rc->slot_use);
|
||||
if(rc->bank0) free(rc->bank0);
|
||||
if(rc->file) {
|
||||
storage_file_close(rc->file);
|
||||
storage_file_free(rc->file);
|
||||
}
|
||||
memset(rc, 0, sizeof(*rc));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------- main */
|
||||
|
||||
extern "C" int32_t flipgb_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
AppState* app = (AppState*)malloc(sizeof(AppState));
|
||||
if(!app) return -1;
|
||||
memset(app, 0, sizeof(AppState));
|
||||
app->frameskip_setting = -1; /* auto */
|
||||
g_app = app;
|
||||
|
||||
init_scale_maps();
|
||||
|
||||
Storage* storage = (Storage*)furi_record_open(RECORD_STORAGE);
|
||||
DialogsApp* dialogs = (DialogsApp*)furi_record_open(RECORD_DIALOGS);
|
||||
|
||||
FuriString* rom_path = furi_string_alloc_set_str("/ext");
|
||||
FuriString* sav_path = furi_string_alloc();
|
||||
|
||||
Gui* gui = NULL;
|
||||
Canvas* canvas = NULL;
|
||||
FuriPubSub* input_events = NULL;
|
||||
FuriPubSubSubscription* input_sub = NULL;
|
||||
bool fb_cb_added = false;
|
||||
|
||||
do {
|
||||
/* --- pick a ROM with the stock file browser (normal GUI mode) --- */
|
||||
DialogsFileBrowserOptions browser_options;
|
||||
dialog_file_browser_set_basic_options(&browser_options, ".gb", NULL);
|
||||
browser_options.base_path = "/ext";
|
||||
browser_options.hide_ext = false;
|
||||
|
||||
if(!dialog_file_browser_show(dialogs, rom_path, rom_path, &browser_options)) {
|
||||
break; /* cancelled */
|
||||
}
|
||||
|
||||
MBCType mbc = MBCType::None;
|
||||
RomLoadResult res = rom_load(app, storage, furi_string_get_cstr(rom_path), &mbc);
|
||||
|
||||
if(res != RomLoadOk) {
|
||||
DialogMessage* msg = dialog_message_alloc();
|
||||
const char* text = "ROM load failed";
|
||||
if(res == RomLoadCgbOnly) text = "Game Boy Color only\nROM: not supported";
|
||||
if(res == RomLoadBadMapper) text = "Unsupported mapper";
|
||||
if(res == RomLoadNoMem) text = "Not enough RAM";
|
||||
dialog_message_set_text(msg, text, 64, 30, AlignCenter, AlignCenter);
|
||||
dialog_message_set_buttons(msg, NULL, "OK", NULL);
|
||||
dialog_message_show(dialogs, msg);
|
||||
dialog_message_free(msg);
|
||||
break;
|
||||
}
|
||||
|
||||
save_path_for_rom(rom_path, sav_path);
|
||||
load_sram(app, storage, sav_path);
|
||||
|
||||
/* --- construct the emulator --- */
|
||||
app->fb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
if(!app->fb_mutex) break;
|
||||
|
||||
/* rom_load reserved room for this, but double-check anyway: on the
|
||||
* Flipper an unchecked new/malloc crashes the firmware on OOM */
|
||||
void* gb_mem = safe_malloc(sizeof(Gameboy));
|
||||
if(!gb_mem) {
|
||||
DialogMessage* msg = dialog_message_alloc();
|
||||
dialog_message_set_text(msg, "Not enough RAM", 64, 30, AlignCenter, AlignCenter);
|
||||
dialog_message_set_buttons(msg, NULL, "OK", NULL);
|
||||
dialog_message_show(dialogs, msg);
|
||||
dialog_message_free(msg);
|
||||
break;
|
||||
}
|
||||
app->gb = new(gb_mem) Gameboy(
|
||||
app->rom.bank0,
|
||||
app->rom.banks,
|
||||
mbc,
|
||||
app->cart_ram,
|
||||
app->cart_ram_size,
|
||||
rom_bank_provider,
|
||||
&app->rom);
|
||||
app->gb->set_frame_callback(frame_callback, app);
|
||||
app->gb->set_row_mask(s_rowmask);
|
||||
|
||||
/* --- sound: piezo plays the dominant APU voice --- */
|
||||
app->speaker_acquired = furi_hal_speaker_acquire(50);
|
||||
app->sound_enabled = app->speaker_acquired;
|
||||
|
||||
/* --- take over the display --- */
|
||||
gui = (Gui*)furi_record_open(RECORD_GUI);
|
||||
if(!gui) break;
|
||||
app->gui = gui;
|
||||
|
||||
gui_add_framebuffer_callback(gui, framebuffer_commit_callback, app);
|
||||
fb_cb_added = true;
|
||||
|
||||
canvas = gui_direct_draw_acquire(gui);
|
||||
if(!canvas) break;
|
||||
app->canvas = canvas;
|
||||
|
||||
input_events = (FuriPubSub*)furi_record_open(RECORD_INPUT_EVENTS);
|
||||
if(!input_events) break;
|
||||
input_sub = furi_pubsub_subscribe(input_events, input_events_callback, app);
|
||||
if(!input_sub) break;
|
||||
|
||||
/* --- main loop --- */
|
||||
uint8_t last_applied_keys = 0;
|
||||
uint32_t frame_deadline_us = 0;
|
||||
uint32_t last_now_ms = furi_get_tick();
|
||||
app->emu_ms_ema = 16 << 4;
|
||||
|
||||
while(!app->exit_requested) {
|
||||
/* ------ menu mode ------ */
|
||||
if(app->menu_requested) {
|
||||
app->menu_requested = false;
|
||||
app->menu_active = true;
|
||||
app->menu_cursor = 0;
|
||||
app->menu_last_keys = app->keys; /* no spurious edges from held keys */
|
||||
app->status_msg[0] = 0;
|
||||
/* lift all buttons for the game, mute while paused */
|
||||
apply_input(app, &last_applied_keys);
|
||||
sound_update(app, true);
|
||||
menu_draw(app);
|
||||
|
||||
while(app->menu_active && !app->exit_requested) {
|
||||
if(!menu_tick(app, storage, sav_path)) {
|
||||
app->menu_active = false;
|
||||
}
|
||||
furi_delay_ms(33);
|
||||
}
|
||||
frame_deadline_us = 0;
|
||||
last_now_ms = furi_get_tick();
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ------ decide frameskip ------ */
|
||||
int skip_n = app->frameskip_setting >= 0 ? app->frameskip_setting : app->auto_skip;
|
||||
bool render_this = app->skip_phase == 0;
|
||||
app->skip_phase = (app->skip_phase + 1) % (skip_n + 1);
|
||||
|
||||
/* ------ run one emulated frame ------ */
|
||||
apply_input(app, &last_applied_keys);
|
||||
app->gb->set_skip_render(!render_this);
|
||||
|
||||
uint32_t t0 = furi_get_tick();
|
||||
app->gb->run_to_vblank();
|
||||
uint32_t emu_ms = furi_get_tick() - t0;
|
||||
|
||||
/* refresh the piezo with this frame's dominant APU voice */
|
||||
sound_update(app, false);
|
||||
|
||||
/* EMA of the cost of one emulated frame (x16 fixed point) */
|
||||
app->emu_ms_ema += ((emu_ms << 4) - app->emu_ms_ema) / 8;
|
||||
uint32_t ema_ms = app->emu_ms_ema >> 4;
|
||||
app->auto_skip = ema_ms <= 17 ? 0 : (int)((ema_ms - 1) / 17);
|
||||
if(app->auto_skip > 4) app->auto_skip = 4;
|
||||
|
||||
if(render_this && !app->menu_active) {
|
||||
canvas_commit(canvas);
|
||||
}
|
||||
|
||||
/* ------ pacing: aim for 59.73 Hz wall time ------ */
|
||||
uint32_t now = furi_get_tick();
|
||||
uint32_t elapsed_us = (now - last_now_ms) * 1000u;
|
||||
last_now_ms = now;
|
||||
|
||||
if(frame_deadline_us > elapsed_us) {
|
||||
frame_deadline_us -= elapsed_us;
|
||||
} else {
|
||||
frame_deadline_us = 0;
|
||||
}
|
||||
frame_deadline_us += GB_FRAME_US;
|
||||
|
||||
/* cap the backlog so a long stall doesn't fast-forward */
|
||||
if(frame_deadline_us > 4 * GB_FRAME_US) frame_deadline_us = 4 * GB_FRAME_US;
|
||||
|
||||
if(frame_deadline_us > GB_FRAME_US + 2000) {
|
||||
uint32_t sleep_ms = (frame_deadline_us - GB_FRAME_US) / 1000u;
|
||||
furi_delay_ms(sleep_ms);
|
||||
} else if(emu_ms == 0) {
|
||||
furi_delay_ms(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* auto-save battery RAM on exit */
|
||||
if(app->has_battery) save_sram(app, storage, sav_path);
|
||||
} while(false);
|
||||
|
||||
/* --- teardown --- */
|
||||
if(app->speaker_acquired) {
|
||||
furi_hal_speaker_stop();
|
||||
furi_hal_speaker_release();
|
||||
app->speaker_acquired = false;
|
||||
}
|
||||
if(input_sub && input_events) furi_pubsub_unsubscribe(input_events, input_sub);
|
||||
wait_inflight_zero(&s_input_cb_inflight);
|
||||
if(input_events) furi_record_close(RECORD_INPUT_EVENTS);
|
||||
|
||||
if(fb_cb_added) gui_remove_framebuffer_callback(gui, framebuffer_commit_callback, app);
|
||||
wait_inflight_zero(&s_fb_cb_inflight);
|
||||
|
||||
if(gui) {
|
||||
if(canvas) gui_direct_draw_release(gui);
|
||||
furi_record_close(RECORD_GUI);
|
||||
}
|
||||
|
||||
if(app->gb) {
|
||||
app->gb->~Gameboy(); /* placement-new counterpart */
|
||||
free(app->gb);
|
||||
}
|
||||
rom_free(app);
|
||||
if(app->cart_ram) free(app->cart_ram);
|
||||
if(app->fb_mutex) furi_mutex_free(app->fb_mutex);
|
||||
|
||||
furi_string_free(rom_path);
|
||||
furi_string_free(sav_path);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
free(app);
|
||||
g_app = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user