Compare commits

..

149 Commits

Author SHA1 Message Date
8a352ca807 chore: prevent bitrot 2024-10-13 14:59:24 -05:00
068e4124a6 chore: update dependencies 2024-10-13 14:46:46 -05:00
fedae65cc8 chore: update dependencies 2023-03-17 23:42:51 -05:00
41586d8cc4 chore: update gilrs 2023-02-03 15:20:38 -06:00
79740ac245 chore: update default presenting mode 2022-11-09 08:19:15 -04:00
d6bfde081e feat(ppu): implement double buffering 2022-11-05 02:02:41 -03:00
d2f57ee66b chore: update clap to 4.0 2022-11-05 01:57:44 -03:00
3841d3a98c chore(readme): add build instructions
closes #1
2022-10-26 21:05:00 -03:00
85940c8744 fix: actually paint the gui 2022-10-21 01:17:02 -03:00
c7adf54976 chore: run cargo fmt 2022-10-21 01:13:44 -03:00
f0f9003247 chore: update wgpu 2022-10-21 00:46:14 -03:00
5eba097ad4 chore: update recommended vscode extensions 2022-10-21 00:14:10 -03:00
756c806c2a chore: satisfy clippy 2022-09-12 07:06:09 -03:00
77af5453f0 chore: refactor gui code 2022-09-12 07:00:50 -03:00
41bc4f7afe chore: update dependencies 2022-09-12 06:01:34 -03:00
36e46d3780 chore: update dependencies 2022-04-26 11:45:57 -05:00
926c66746d chore: update some dependencies 2022-04-07 15:30:46 -03:00
cf2357f917 feat: add disassembler that is aware of immediate values 2021-12-16 12:16:58 -04:00
3fb7edd333 chore: add some missing comments to instruction decoding 2021-12-16 12:16:36 -04:00
3ab512d663 fix(ppu): implement proper PPU reset behaviour 2021-12-09 10:46:27 -04:00
61156ca8a8 fix(cart): rename enum variant and enable MBC3 w/ battery 2021-12-09 09:30:47 -04:00
2c411d5792 chore(cart): add test for all-whitespace title 2021-12-09 07:27:52 -04:00
4c657c9832 chore: update dependencies 2021-12-09 06:23:32 -04:00
e72b11f946 feat: add more debug tools to gui 2021-12-09 05:21:05 -04:00
bbcbbd8ce3 chore: fix dependency conflict 2021-12-06 13:36:23 -04:00
c4226e6e76 Merge branch 'main' of ssh://git.musuka.dev:2222/paoda/gb into main 2021-12-06 13:34:14 -04:00
409314a4e5 fix(dbg): don't crash when attempting to read data as executable code 2021-12-06 13:33:22 -04:00
952bf68bd0 chore: add extension to recommended 2021-12-06 13:24:05 -04:00
865c11d53c chore: update dependencies 2021-12-03 16:34:12 -04:00
90d2da9272 chore(gui): implement basic disassembler
Also implement CPU and Interrupt debug information.

Of note:
1. IE and IRQ status boxes are slightly misaligned
2. Whenever the disassembler accidentally reads into game data rather
   than executable code the emulator crashes
   * Thus I should turn Instruction decoding into a Result<> rather than
     panic on failure
2021-11-30 10:23:06 -04:00
4a1a21a08f chore: satisfy clippy 2021-11-30 04:13:12 -04:00
9d23b571fb chore: update cargo.toml 2021-11-30 04:00:34 -04:00
70e0b5868d chore: code cleanup 2021-11-30 03:59:44 -04:00
a0e3c7c602 chore: remove useless default implementations 2021-11-25 04:07:45 -04:00
ef4f3d9ec6 chore(cart): refactor sections of cartridge code 2021-11-25 03:32:22 -04:00
7f872a554b chore(cart): code cleanup 2021-11-25 03:32:18 -04:00
dda0257655 fix(cart): support cartridge headers w/ manufactor codes 2021-11-25 00:12:35 -04:00
c10816c048 chore: imrpove private and public APIs 2021-11-24 22:12:26 -04:00
2405fd027f chore: remove Emulator struct 2021-11-21 05:27:04 -04:00
939c25ce1a chore: update comment 2021-11-15 22:05:26 -04:00
6f5e863645 chore: drop pixels-rs and add egui w/ wgpu backend 2021-11-15 21:34:24 -04:00
ce5d58e1d2 chore(ppu): rename some structs 2021-11-07 14:37:40 -06:00
ebcd5bec5c fix(cart): reset sub-cycle counting in RTC
specifically when RTC S is written to
2021-11-07 13:05:41 -06:00
f2c49b398c chore(cart): improve code quality of RtcClock 2021-11-05 23:05:36 -03:00
6f76571d6c chore: update README 2021-11-05 22:40:31 -03:00
55da5a29d8 chore: remove more getters 2021-11-01 18:04:22 +09:00
7dfcd44820 chore(emu): remove bus and bus_mut from cpu 2021-11-01 17:56:42 +09:00
80a15039e0 chore(apu): improve accuracy of Channel 1 2021-10-30 21:53:08 +09:00
32405c0734 fix(apu): implement some obscure behaviour for ch1,2 & 3 2021-10-30 20:04:16 +09:00
2ef8fefbb7 chore(apu): implement more obscure behaviour
implement capacitor
2021-10-30 18:41:31 +09:00
548945a4ee chore: remove imports of tracing macros 2021-10-30 16:41:06 +09:00
1526b4a09d fix(cart): improve title parsin
Now both Tokimeki Memorial titles are read properly
2021-10-30 15:48:47 +09:00
4125ea5c74 fix(instr): fix timing issues with select instructions
instr_timing now runs (and fails on everything)
2021-10-30 14:02:28 +09:00
b9519d9b7a chore(bus): stub some CGB IO registers 2021-10-30 14:02:05 +09:00
01278ca83f chore: reduce size of public interface 2021-10-30 10:28:20 +09:00
2bb8bd6d8f chore: update dependencies 2021-10-30 09:37:24 +09:00
080c1e7518 fix(bus): don't panic on non-existent cartridge 2021-10-28 22:19:38 -03:00
765f9d8288 Merge branch 'main' of ssh://musuka.dev:2222/paoda/gb 2021-10-28 22:08:04 -03:00
8780c4a59b chore(emu): remove and replace EmulatorBuilder 2021-10-28 22:07:36 -03:00
e19a540650 chore: upgrade to rust 2021 2021-10-22 01:42:40 -03:00
4cfd951ede chore(ppu): improve bg-sprite priority functionality 2021-10-20 20:08:09 -03:00
de8fe1a367 chore(ppu): remove object buffer iter_mut method 2021-10-20 19:41:27 -03:00
c5fa41b20d chore(ppu): refactor FetcherState 2021-10-20 19:40:29 -03:00
552cfd4a18 chore(ppu): sort OAM Memory 2021-10-20 17:36:38 -03:00
4c516804e4 fix(ppu): refactor and improve accuracy of background fetcher 2021-10-20 16:58:19 -03:00
9b3ab73bb1 fix(ppu): incremental improvements to accuracy of ppu fetcher 2021-10-20 05:21:49 -03:00
d9f1d661ae chore(ppu): refactor behaviour w.r.t window enabling 2021-10-20 04:48:20 -03:00
da83032e24 chore(ppu): use "dot" intead of "cycle" 2021-10-20 03:34:09 -03:00
67e2447d3f chore(ppu): rename window_stat to win_stat 2021-10-20 03:16:37 -03:00
37cf3d92e4 chore(ppu): refactor OAM Scan implementation 2021-10-20 03:14:30 -03:00
293e5762c3 chore: small code-cleanup changes 2021-10-20 02:48:44 -03:00
999f661e6b chore: update cargo.lock 2021-10-14 14:58:25 -03:00
e573411890 chore: update dependencies 2021-10-11 00:06:51 -03:00
352a65b705 chore(apu): update spsc depenency 2021-10-07 15:27:51 -03:00
64230973f1 chore: suggest inline for hot code 2021-09-24 16:16:14 -03:00
dbbf87af52 chore: add a creative-commons boot rom for compat 2021-09-24 16:15:55 -03:00
1440cd1fc7 chore: update README.md 2021-09-24 12:34:48 -03:00
9964b49ce1 fix(ppu): improve accuracy of timer 2021-09-21 12:50:31 -03:00
142231d355 chore(ppu): remoe unnecessary brackets 2021-09-21 12:13:55 -03:00
227928e8ca Revert "fix(ppu): explicity choose to use sign extension"
This reverts commit 1001b0b124.
2021-09-21 12:09:35 -03:00
1001b0b124 fix(ppu): explicity choose to use sign extension 2021-09-21 09:52:12 -03:00
71ce3f43e0 chore: satisfy clippy 2021-09-20 04:15:05 -03:00
ce121864d2 feat: implement cartridge saving
Implemented for MBC1, MBC2, MBC3 and MBC5
2021-09-20 04:13:25 -03:00
e1fe00ab64 fix: have start and select keybinds match other emulators 2021-09-20 03:26:06 -03:00
5882678bc5 chore: inline some functions
Some checks reported errors
continuous-integration/drone/push Build was killed
2021-09-20 01:34:41 -03:00
bcd67cb317 chore: begin refactor of public api 2021-09-20 01:34:21 -03:00
4516ca8477 chore: run cargo fix
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-12 04:56:39 -03:00
6087e3b20b chore: remove Cycle struct and begin scheduler design 2021-09-12 04:56:34 -03:00
10ac579c40 fix(main): Use LogicalSize and PhysicalSize properly 2021-09-11 22:56:40 -03:00
ee5504111b Merge branch 'main' of ssh://git.musuka.dev:2222/paoda/gb into main
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-11 20:48:05 -03:00
a628f64d28 chore: update dependencies 2021-09-11 20:47:45 -03:00
318a6e0386 fix(emu): remove GAMEPAD_ENABLED const flag
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-09 11:12:50 -03:00
db012c7f4b fix(main): remove code unrelated to audio sync
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-07 02:09:02 -03:00
e42c87aeb7 fix(apu): quiet gameboy APU 2021-09-07 01:52:02 -03:00
9113e95fa0 fix(apu): pass blargg apu sweep tests 2021-09-07 01:17:01 -03:00
9973dc8714 fix(cartridge): don't read from RAM that doesn't exist
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-06 23:45:04 -03:00
e128025208 fix(cartridge): improve accuracy of MBC1 with large file sizes 2021-09-06 23:37:55 -03:00
44ac0c8ebd feat(cartridge): implement MBC2
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-06 18:13:34 -03:00
01064bab69 chore(cpu): comment out blargg-specific code
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-22 01:49:01 -05:00
634bc2d2c0 fix(apu): remove redundant code 2021-08-22 01:48:34 -05:00
d794a94b68 fix(timer): increase accuracy of timer
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-20 00:17:28 -05:00
b87e31d3f4 fix(cartridge): remove unnecessary dbg statement 2021-08-20 00:17:05 -05:00
3c2456611e fix(cartridge): trim whitespace from cartridge title
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-19 21:56:07 -05:00
b829f05a34 chore(cartridge): clean-up code
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-19 20:39:04 -05:00
afd2d16371 chore(cartridge): re-rename RamInfo and RomRinfo 2021-08-19 20:05:48 -05:00
1f8fa48168 chore: add reccomended vscode extensions
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-19 17:05:36 -05:00
c6fbb79189 chore(cartridge): reognanize code 2021-08-19 16:10:39 -05:00
8b78b2943e fix(cartridge): specify intentional overflow in MBC5
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-19 16:09:58 -05:00
0af95a1dd3 feat(cartridge): implement MBC5 and cleanup code
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-18 22:40:15 -05:00
9fa40f8584 fix(main): do not drop output stream 2021-08-18 22:39:55 -05:00
b10bc7b4fd chore(main): add flag that enables audio 2021-08-18 16:34:26 -05:00
4658a0d106 fix(apu): ch4 set_len should set timer to 64 - len
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-18 02:32:48 -05:00
f92b9d61ef chore(apu_gen): cosmetic changes to sample generation
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-18 01:43:38 -05:00
360a9a7b65 chore(apu): improve code organization
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-18 00:04:27 -05:00
5d64e539a7 fix(apu): resolve off-by-one error 2021-08-17 23:14:56 -05:00
22f96a10e7 fix(apu): increase accuracy of apu emulation
Reintroduce the Frame Sequencer and it's state enum (needed so that we
can reset the FS on NR52 enable)
2021-08-17 22:18:40 -05:00
8fea8eb1ff fix(apu): better emulate behaviour of apu channel DACs 2021-08-17 21:26:22 -05:00
9b2c91445a chore: enable audio by default
even if it sounds really bad right now
2021-08-17 21:25:55 -05:00
b9046bceba fix(apu): disable channel if DAC is disabled 2021-08-17 20:42:41 -05:00
aa22e93049 chore: clean up TODO messages
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-15 23:31:27 -05:00
6215eccb2f chore(cpu): merge halted and state properties 2021-08-15 23:26:01 -05:00
a77d0a0f62 fix(apu): clock frame sequencer at correct Hz
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-15 02:46:20 -05:00
c2f2e2194b chore(ppu): improve accuracy of pixel fifo 2021-08-14 23:47:16 -05:00
d68257bb29 fix(ppu): improve accuracy of SCX discard 2021-08-14 22:03:01 -05:00
e27d6dc25b chore(ppu): rename discriminants of fetcher state 2021-08-14 22:02:41 -05:00
1acb5de19d fix(main): GB frametime should be 59.73 Hz 2021-08-14 17:59:59 -05:00
1b78b248a3 chore: minor edits to documentation 2021-08-14 17:51:09 -05:00
5d6df46a2d fix(cpu): reimplement instruction handling
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-14 17:23:45 -05:00
7e65d82fef chore(cpu): document fetch, decode, execute 2021-08-14 16:42:38 -05:00
8c9567b610 chore(cpu): rename discriminants of ImeState enum 2021-08-14 16:42:15 -05:00
53dfaf0de2 fix(apu): increase size of the audio buffer
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-14 15:29:35 -05:00
16c2dd81fc fix(bus): remove dead code 2021-08-14 15:02:25 -05:00
79be38a1e6 fix(main): rename constant SCALE to WINDOW_SCALE
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-14 01:02:18 -05:00
8625bec059 feat: clock bus on instruction read-write
Some checks failed
continuous-integration/drone/push Build is failing
Commit also includes general work towards passing mem-timings.

Note: while cpu_instrs.gb passes, instr_timing.gb and mem_timing.gb both
are stuck in infinite loops (Currently, it seems like a timing issue).
This is a major regression that hopefully shouldn't last for too long.
2021-08-14 00:10:51 -05:00
0637b771e3 chore(instr): implement copy and clone on instruction enum
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-05 20:04:39 -05:00
0107fa04c9 chore(apu): remove implemtation of register that always returns 0xFF 2021-08-05 16:39:04 -05:00
6265c8af04 chore(joypad): poll input every frame instead of every instruction
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-03 20:36:55 -05:00
5482a8e75f fix(apu): fix out of bounds error with channel 3 wave ram 2021-08-03 20:35:22 -05:00
002dae6826 fix(joypad): improve handling of keyboard input 2021-08-03 20:23:43 -05:00
c863dc835c fix(apu): fix index out of bounds error on channel 3 write 2021-08-03 20:23:08 -05:00
d4407cf849 fix(apu): implement NR50 volume controls 2021-08-03 19:33:27 -05:00
de0d147685 fix(cartridge): put a bit more detail into the MBC3 RTC stub
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-03 15:19:08 -05:00
dd8339e8de chore(cartridge): consistently capitalize MBC 2021-08-03 15:06:06 -05:00
05d6475015 fix(cartridge): Use default title instead of empty string 2021-08-03 14:53:30 -05:00
32b597a328 fix(apu): incremental improvements to APU accuracy
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-03 14:10:08 -05:00
33be2e0e83 fix(main): tie emulation to 60Hz on host machine 2021-08-03 14:09:16 -05:00
832e1b7633 fix(apu): implement WAVE RAM blocking 2021-08-02 22:38:00 -05:00
a549b9feef chore(bus): suppress warning 2021-08-02 21:55:07 -05:00
4d6fc95130 chore: remove premature optimizations 2021-08-02 21:52:12 -05:00
25 changed files with 5247 additions and 3483 deletions

9
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"recommendations": [
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"serayuzgur.crates",
"vadimcn.vscode-lldb",
"donaldhays.rgbds-z80"
]
}

2910
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,20 +2,27 @@
name = "gb" name = "gb"
version = "0.1.0" version = "0.1.0"
authors = ["Rekai Musuka <rekai@musuka.dev>"] authors = ["Rekai Musuka <rekai@musuka.dev>"]
edition = "2018" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "^1.0" anyhow = "1.0"
bitfield = "^0.13" bitfield = "0.17"
clap = "^2.33" clap = { version = "4.0", features = ["cargo"] }
gilrs = "^0.8" gilrs = "0.11"
pixels = "^0.5" winit = "0.28"
winit = "^0.25" egui = "0.21"
winit_input_helper = "^0.10" egui_wgpu_backend = "0.22"
rodio = "^0.14" egui_winit_platform = "0.18"
rtrb = "^0.1.4" pollster = "0.3"
rodio = "0.19"
rtrb = "0.3"
directories-next = "2.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] }
thiserror = "1.0"
once_cell = "1.10"
[profile.release] [profile.release]
debug = true debug = true

View File

@@ -1,14 +1,43 @@
# Rekai's Gameboy Emulator # Rekai's Gameboy Emulator
[![Build Status](https://ci.paoda.moe/api/badges/paoda/gb/status.svg)](https://ci.paoda.moe/paoda/gb)
### Status ### Status
* Passes Blargg's cpu_instrs Test * From [Blargg Test ROMs](https://github.com/L-P/blargg-test-roms/)
* Renders Background & Window Tiles * [x] cpu_instrs
* Implements a PPU FIFO * [x] instr_timing
* [x] mem_timing
* [x] mem_timing-2
* [ ] dmg_sound (partial)
* [x] [dmg-acid2](https://github.com/mattcurrie/dmg-acid2)
* From [mooneye-gb](https://github.com/Gekkio/mooneye-gb):
* Cartridges:
* [x] MBC1
* [ ] MBC1M
* [x] MBC2
* [x] MBC5
* Implements a cycle-accurate PPU FIFO
* Doesn't \*exactly\* work just yet
Supports: ROM-only, MBC1, MBC2, MBC3 and MBC5 games.
### Notes ### Compiling
* [gameboy-logs](https://github.com/wheremyfoodat/Gameboy-logs) suggests that there are still some underlying problems with the cpu implementation This project was last successfully built on [Rust 1.64.0](https://github.com/rust-lang/rust/tree/1.64.0)
* The Sprite FIFO does not work as expected yet
* Sound is neither emulated nor stubbed. Upon writing / reading to a APU related register the emulator will panic. 1. `git clone https://github.com/paoda/gb`
* Code cleanup is pending completion of some minimum viable product of the emulator 2. `cd gb`
3. `cargo run --release`
### Controls
Controls are defined [here](https://github.com/paoda/gb/blob/85940c874460b9cb31bf9b8211bf7dda596d114a/src/joypad.rs#L114)
Key | Button
--- | ---
<kbd>X</kbd> | B
<kbd>Z</kbd> | A
<kbd>Enter</kbd> | START
<kbd>Shift</kbd> | SELECT
Then use the Arrow keys for the D-Pad
### Credits
The Boot ROM found in the `bin/` directory was made by [Optix](https://github.com/Hacktix) over [here](https://github.com/Hacktix/Bootix)

BIN
bin/bootix_dmg.bin Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -3,35 +3,14 @@ use rtrb::{Consumer, Producer, PushError, RingBuffer};
pub(crate) const SAMPLE_RATE: u32 = 48000; // Hz pub(crate) const SAMPLE_RATE: u32 = 48000; // Hz
const CHANNEL_COUNT: usize = 2; const CHANNEL_COUNT: usize = 2;
const BUFFER_CAPACITY: usize = 4096 * CHANNEL_COUNT; // # of samples * the # of channels const BUFFER_CAPACITY: usize = 2048 * CHANNEL_COUNT; // # of samples * the # of channels
pub struct AudioSPSC<T> { pub fn init<T>() -> (SampleProducer<T>, SampleConsumer<T>) {
inner: RingBuffer<T>, let (prod, cons) = RingBuffer::new(BUFFER_CAPACITY);
} (
SampleProducer { inner: prod },
impl<T> Default for AudioSPSC<T> { SampleConsumer { inner: cons },
fn default() -> Self { )
Self {
inner: RingBuffer::new(BUFFER_CAPACITY),
}
}
}
impl<T> AudioSPSC<T> {
pub fn new(capacity: usize) -> Self {
Self {
inner: RingBuffer::new(capacity),
}
}
pub fn init(self) -> (SampleProducer<T>, SampleConsumer<T>) {
let (prod, cons) = self.inner.split();
(
SampleProducer { inner: prod },
SampleConsumer { inner: cons },
)
}
} }
pub struct SampleProducer<T> { pub struct SampleProducer<T> {
@@ -43,10 +22,18 @@ impl<T> SampleProducer<T> {
self.inner.push(value) self.inner.push(value)
} }
#[inline] #[allow(dead_code)]
pub(crate) fn two_available(&self) -> bool { pub(crate) fn available(&self) -> bool {
self.inner.slots() > 2 self.inner.slots() > 2
} }
pub(crate) fn available_blocking(&self) -> bool {
loop {
if self.inner.slots() > 2 {
break true;
}
}
}
} }
impl<T> std::fmt::Debug for SampleProducer<T> { impl<T> std::fmt::Debug for SampleProducer<T> {

View File

@@ -1,5 +1,11 @@
use bitfield::bitfield; use bitfield::bitfield;
pub(crate) trait NRx4 {
fn trigger(&self) -> bool;
fn length_enable(&self) -> bool;
fn set_length_enable(&mut self, value: bool);
}
pub(crate) mod ch1 { pub(crate) mod ch1 {
use super::bitfield; use super::bitfield;
@@ -73,7 +79,7 @@ pub(crate) mod ch1 {
} }
} }
pub(crate) mod ch3 { pub(super) mod ch3 {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) enum Volume { pub(crate) enum Volume {
Mute = 0, Mute = 0,
@@ -102,8 +108,8 @@ pub(crate) mod ch3 {
} }
} }
pub(crate) mod ch4 { pub(super) mod ch4 {
use super::bitfield; use super::{bitfield, NRx4};
bitfield! { bitfield! {
pub struct PolynomialCounter(u8); pub struct PolynomialCounter(u8);
@@ -163,7 +169,7 @@ pub(crate) mod ch4 {
match byte & 0x01 { match byte & 0x01 {
0b00 => Self::Short, 0b00 => Self::Short,
0b01 => Self::Long, 0b01 => Self::Long,
_ => unreachable!("{:#04X} is not a valid value for CounterWidth"), _ => unreachable!("{:#04X} is not a valid value for CounterWidth", byte),
} }
} }
} }
@@ -177,17 +183,21 @@ pub(crate) mod ch4 {
bitfield! { bitfield! {
pub struct Frequency(u8); pub struct Frequency(u8);
impl Debug; impl Debug;
_initial, _: 7; _trigger, _: 7;
_idk, _: 6; // TODO: same as FrequencyHigh, figure out what this is _length_enable, _set_length_enable: 6;
} }
impl Frequency { impl NRx4 for Frequency {
pub(crate) fn idk(&self) -> bool { fn trigger(&self) -> bool {
self._idk() self._trigger()
} }
pub(crate) fn initial(&self) -> bool { fn length_enable(&self) -> bool {
self._initial() self._length_enable()
}
fn set_length_enable(&mut self, value: bool) {
self._set_length_enable(value);
} }
} }
@@ -217,24 +227,28 @@ pub(crate) mod ch4 {
} }
} }
pub(crate) mod common { pub(super) mod common {
use super::bitfield; use super::{bitfield, NRx4};
bitfield! { bitfield! {
pub struct FrequencyHigh(u8); pub struct FrequencyHigh(u8);
impl Debug; impl Debug;
_initial, _: 7; _trigger, _: 7;
_idk, _: 6; // TODO: Figure out what the hell this is _length_enable, _set_length_enable: 6;
pub freq_bits, set_freq_bits: 2, 0; pub freq_bits, set_freq_bits: 2, 0;
} }
impl FrequencyHigh { impl NRx4 for FrequencyHigh {
pub(crate) fn initial(&self) -> bool { fn trigger(&self) -> bool {
self._initial() self._trigger()
} }
pub(crate) fn idk(&self) -> bool { fn length_enable(&self) -> bool {
self._idk() self._length_enable()
}
fn set_length_enable(&mut self, value: bool) {
self._set_length_enable(value);
} }
} }
@@ -342,7 +356,7 @@ pub(crate) mod common {
pub struct SoundDuty(u8); pub struct SoundDuty(u8);
impl Debug; impl Debug;
from into WavePattern, _wave_pattern, _: 7, 6; from into WavePattern, _wave_pattern, _: 7, 6;
_sound_length, _: 5, 0; // TODO: Getter only used if bit 6 in NR14 is set _sound_length, _: 5, 0;
} }
impl SoundDuty { impl SoundDuty {
@@ -402,12 +416,6 @@ pub(crate) mod common {
} }
} }
impl Default for WavePattern {
fn default() -> Self {
Self::OneEighth // Rationale: OneEighth is 0x00
}
}
impl From<WavePattern> for u8 { impl From<WavePattern> for u8 {
fn from(pattern: WavePattern) -> Self { fn from(pattern: WavePattern) -> Self {
pattern as Self pattern as Self
@@ -427,41 +435,6 @@ pub(crate) mod common {
} }
} }
#[derive(Debug, Clone, Copy)]
pub(crate) enum FrameSequencerState {
Step0Length,
Step1Nothing,
Step2LengthAndSweep,
Step3Nothing,
Step4Length,
Step5Nothing,
Step6LengthAndSweep,
Step7VolumeEnvelope,
}
impl FrameSequencerState {
pub(crate) fn step(&mut self) {
use FrameSequencerState::*;
*self = match *self {
Step0Length => Step1Nothing,
Step1Nothing => Step2LengthAndSweep,
Step2LengthAndSweep => Step3Nothing,
Step3Nothing => Step4Length,
Step4Length => Step5Nothing,
Step5Nothing => Step6LengthAndSweep,
Step6LengthAndSweep => Step7VolumeEnvelope,
Step7VolumeEnvelope => Step0Length,
};
}
}
impl Default for FrameSequencerState {
fn default() -> Self {
Self::Step0Length
}
}
bitfield! { bitfield! {
pub struct SoundOutput(u8); pub struct SoundOutput(u8);
impl Debug; impl Debug;
@@ -475,6 +448,24 @@ bitfield! {
pub ch1_right, _: 0; pub ch1_right, _: 0;
} }
impl SoundOutput {
pub(super) fn ch1(&self) -> (bool, bool) {
(self.ch1_left(), self.ch1_right())
}
pub(super) fn ch2(&self) -> (bool, bool) {
(self.ch2_left(), self.ch2_right())
}
pub(super) fn ch3(&self) -> (bool, bool) {
(self.ch3_left(), self.ch3_right())
}
pub(super) fn ch4(&self) -> (bool, bool) {
(self.ch4_left(), self.ch4_right())
}
}
impl Copy for SoundOutput {} impl Copy for SoundOutput {}
impl Clone for SoundOutput { impl Clone for SoundOutput {
fn clone(&self) -> Self { fn clone(&self) -> Self {
@@ -482,12 +473,6 @@ impl Clone for SoundOutput {
} }
} }
impl Default for SoundOutput {
fn default() -> Self {
Self(0)
}
}
impl From<u8> for SoundOutput { impl From<u8> for SoundOutput {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
Self(byte) Self(byte)
@@ -503,10 +488,20 @@ impl From<SoundOutput> for u8 {
bitfield! { bitfield! {
pub struct ChannelControl(u8); pub struct ChannelControl(u8);
impl Debug; impl Debug;
vin_so2, _: 7; vin_left, _: 7;
so2_level, _: 6, 4; _left_volume, _: 6, 4;
vin_so1, _: 3; vin_right, _: 3;
so1_level, _: 2, 0; _right_volume, _: 2, 0;
}
impl ChannelControl {
pub(crate) fn left_volume(&self) -> f32 {
self._left_volume() as f32
}
pub(crate) fn right_volume(&self) -> f32 {
self._right_volume() as f32
}
} }
impl Copy for ChannelControl {} impl Copy for ChannelControl {}
@@ -516,12 +511,6 @@ impl Clone for ChannelControl {
} }
} }
impl Default for ChannelControl {
fn default() -> Self {
Self(0)
}
}
impl From<u8> for ChannelControl { impl From<u8> for ChannelControl {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
Self(byte) Self(byte)
@@ -533,3 +522,61 @@ impl From<ChannelControl> for u8 {
ctrl.0 ctrl.0
} }
} }
pub(super) mod fs {
#[derive(Debug)]
pub(crate) struct FrameSequencer {
step: u8,
state: State,
}
impl Default for FrameSequencer {
fn default() -> Self {
Self {
step: Default::default(),
state: State::Length,
}
}
}
impl FrameSequencer {
pub(crate) fn next(&mut self) {
use State::*;
self.step = (self.step + 1) % 8;
self.state = match self.step {
1 | 3 | 5 => Nothing,
0 | 4 => Length,
2 | 6 => LengthAndSweep,
7 => Envelope,
_ => unreachable!("Step {} is invalid for the Frame Sequencer", self.step),
};
}
pub(crate) fn next_clocks_length(&self) -> bool {
use State::*;
match self.state {
Length | LengthAndSweep => true,
Nothing | Envelope => false,
}
}
pub(crate) fn state(&self) -> State {
self.state
}
pub(crate) fn reset(&mut self) {
self.step = Default::default();
self.state = State::Length;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum State {
Length,
Nothing,
LengthAndSweep,
Envelope,
}
}

View File

@@ -7,78 +7,58 @@ use crate::ppu::{Ppu, PpuMode};
use crate::serial::Serial; use crate::serial::Serial;
use crate::timer::Timer; use crate::timer::Timer;
use crate::work_ram::{VariableWorkRam, WorkRam}; use crate::work_ram::{VariableWorkRam, WorkRam};
use std::{fs::File, io::Read};
const BOOT_ROM_SIZE: usize = 0x100; pub(crate) const BOOT_SIZE: usize = 0x100;
#[derive(Debug)] #[derive(Debug, Default)]
pub struct Bus { pub struct Bus {
boot: Option<[u8; BOOT_ROM_SIZE]>, // Boot ROM is 256b long boot: Option<[u8; BOOT_SIZE]>, // Boot ROM is 256b long
cartridge: Option<Cartridge>, pub(crate) cart: Option<Cartridge>,
pub(crate) ppu: Ppu, pub(crate) ppu: Ppu,
work_ram: WorkRam, work_ram: WorkRam,
var_ram: VariableWorkRam, var_ram: VariableWorkRam,
pub(crate) timer: Timer, timer: Timer,
int: Interrupt, int: Interrupt,
pub(crate) apu: Apu, pub(crate) apu: Apu,
high_ram: HighRam, high_ram: HighRam,
serial: Serial, serial: Serial,
pub(crate) joypad: Joypad, pub(crate) joyp: Joypad,
}
impl Default for Bus {
fn default() -> Self {
Self {
boot: None,
cartridge: None,
ppu: Default::default(),
work_ram: Default::default(),
var_ram: Default::default(),
timer: Default::default(),
int: Default::default(),
apu: Default::default(),
high_ram: Default::default(),
serial: Default::default(),
joypad: Default::default(),
}
}
} }
impl Bus { impl Bus {
pub(crate) fn with_boot(path: &str) -> anyhow::Result<Self> { pub(crate) fn with_boot(rom: [u8; 256]) -> Self {
let mut file = File::open(path)?; Self {
let mut boot_rom = [0u8; 256]; boot: Some(rom),
file.read_exact(&mut boot_rom)?;
Ok(Self {
boot: Some(boot_rom),
..Default::default() ..Default::default()
}) }
}
pub(crate) fn load_cartridge(&mut self, path: &str) -> std::io::Result<()> {
self.cartridge = Some(Cartridge::new(path)?);
Ok(())
}
pub(crate) fn rom_title(&self) -> Option<&str> {
self.cartridge.as_ref()?.title()
} }
#[allow(dead_code)]
pub(crate) fn boot_mapped(&self) -> bool { pub(crate) fn boot_mapped(&self) -> bool {
self.boot.is_some() self.boot.is_some()
} }
#[inline]
pub(crate) fn clock(&mut self) { pub(crate) fn clock(&mut self) {
self.ppu.clock(); self.tick(4);
self.timer.clock();
self.apu.clock(self.timer.divider);
self.clock_dma();
} }
fn clock_dma(&mut self) { #[inline]
if let Some((src_addr, dest_addr)) = self.ppu.dma.clock() { fn tick(&mut self, limit: u8) {
for _ in 0..limit {
self.timer.tick();
if let Some(c) = self.cart.as_mut() {
c.tick()
}
self.ppu.tick();
self.apu.tick(self.timer.divider);
self.dma_tick()
}
}
fn dma_tick(&mut self) {
if let Some((src_addr, dest_addr)) = self.ppu.dma.tick() {
let byte = self.oam_read_byte(src_addr); let byte = self.oam_read_byte(src_addr);
self.oam_write_byte(dest_addr, byte); self.oam_write_byte(dest_addr, byte);
} }
@@ -86,7 +66,7 @@ impl Bus {
} }
impl Bus { impl Bus {
pub fn oam_read_byte(&self, addr: u16) -> u8 { pub(crate) fn oam_read_byte(&self, addr: u16) -> u8 {
match addr { match addr {
0x0000..=0x7FFF => { 0x0000..=0x7FFF => {
// 16KB ROM bank 00 (ends at 0x3FFF) // 16KB ROM bank 00 (ends at 0x3FFF)
@@ -97,16 +77,16 @@ impl Bus {
} }
} }
match self.cartridge.as_ref() { match self.cart.as_ref() {
Some(cart) => cart.read_byte(addr), Some(cart) => cart.read_byte(addr),
None => panic!("Tried to read from a non-existent cartridge"), None => 0xFF,
} }
} }
0x8000..=0x9FFF => self.ppu.read_byte(addr), // 8KB Video RAM 0x8000..=0x9FFF => self.ppu.read_byte(addr), // 8KB Video RAM
0xA000..=0xBFFF => match self.cartridge.as_ref() { 0xA000..=0xBFFF => match self.cart.as_ref() {
// 8KB External RAM // 8KB External RAM
Some(cart) => cart.read_byte(addr), Some(cart) => cart.read_byte(addr),
None => panic!("Tried to read from a non-existent cartridge"), None => 0xFF,
}, },
0xC000..=0xCFFF => self.work_ram.read_byte(addr), // 4KB Work RAM Bank 0 0xC000..=0xCFFF => self.work_ram.read_byte(addr), // 4KB Work RAM Bank 0
0xD000..=0xDFFF => self.var_ram.read_byte(addr), // 4KB Work RAM Bank 1 -> N 0xD000..=0xDFFF => self.var_ram.read_byte(addr), // 4KB Work RAM Bank 1 -> N
@@ -133,7 +113,7 @@ impl Bus {
} }
} }
pub fn oam_write_byte(&mut self, addr: u16, byte: u8) { pub(crate) fn oam_write_byte(&mut self, addr: u16, byte: u8) {
self.ppu.oam.write_byte(addr, byte); self.ppu.oam.write_byte(addr, byte);
} }
} }
@@ -150,9 +130,9 @@ impl BusIo for Bus {
} }
} }
match self.cartridge.as_ref() { match self.cart.as_ref() {
Some(cart) => cart.read_byte(addr), Some(cart) => cart.read_byte(addr),
None => panic!("Tried to read from a non-existent cartridge"), None => 0xFF,
} }
} }
0x8000..=0x9FFF => { 0x8000..=0x9FFF => {
@@ -162,10 +142,10 @@ impl BusIo for Bus {
_ => self.ppu.read_byte(addr), _ => self.ppu.read_byte(addr),
} }
} }
0xA000..=0xBFFF => match self.cartridge.as_ref() { 0xA000..=0xBFFF => match self.cart.as_ref() {
// 8KB External RAM // 8KB External RAM
Some(cart) => cart.read_byte(addr), Some(cart) => cart.read_byte(addr),
None => panic!("Tried to read from a non-existent cartridge"), None => 0xFF,
}, },
0xC000..=0xCFFF => self.work_ram.read_byte(addr), // 4KB Work RAM Bank 0 0xC000..=0xCFFF => self.work_ram.read_byte(addr), // 4KB Work RAM Bank 0
0xD000..=0xDFFF => self.var_ram.read_byte(addr), // 4KB Work RAM Bank 1 -> N 0xD000..=0xDFFF => self.var_ram.read_byte(addr), // 4KB Work RAM Bank 1 -> N
@@ -212,11 +192,11 @@ impl BusIo for Bus {
// Every address here starts with 0xFF so we can just check the // Every address here starts with 0xFF so we can just check the
// low byte to figure out which register it is // low byte to figure out which register it is
match addr & 0x00FF { match addr & 0x00FF {
0x00 => self.joypad.p1, 0x00 => self.joyp.p1,
0x01 => self.serial.next, 0x01 => self.serial.next,
0x02 => self.serial.ctrl.into(), 0x02 => self.serial.ctrl.into(),
0x04 => (self.timer.divider >> 8) as u8, 0x04 => (self.timer.divider >> 8) as u8,
0x05 => self.timer.counter, 0x05 => self.timer.tima(),
0x06 => self.timer.modulo, 0x06 => self.timer.modulo,
0x07 => self.timer.ctrl.into(), 0x07 => self.timer.ctrl.into(),
0x0F => self.interrupt_flag().into(), 0x0F => self.interrupt_flag().into(),
@@ -233,9 +213,9 @@ impl BusIo for Bus {
0x49 => self.ppu.monochrome.obj_palette_1.into(), 0x49 => self.ppu.monochrome.obj_palette_1.into(),
0x4A => self.ppu.pos.window_y, 0x4A => self.ppu.pos.window_y,
0x4B => self.ppu.pos.window_x, 0x4B => self.ppu.pos.window_x,
0x4D => 0xFF, // TODO: CGB Specific Register 0x4F => 0xFF, // CGB VRAM Bank Select
_ => { _ => {
eprintln!("Read 0xFF from unused IO register {:#06X}.", addr); tracing::warn!("Attempted read from {:#06X} on IO", addr);
0xFF 0xFF
} }
} }
@@ -256,9 +236,8 @@ impl BusIo for Bus {
0x0000..=0x7FFF => { 0x0000..=0x7FFF => {
// 16KB ROM bank 00 (ends at 0x3FFF) // 16KB ROM bank 00 (ends at 0x3FFF)
// and 16KB ROM Bank 01 -> NN (switchable via MB) // and 16KB ROM Bank 01 -> NN (switchable via MB)
match self.cartridge.as_mut() { if let Some(cart) = self.cart.as_mut() {
Some(cart) => cart.write_byte(addr, byte), cart.write_byte(addr, byte);
None => panic!("Tried to write into non-existent cartridge"),
} }
} }
0x8000..=0x9FFF => { 0x8000..=0x9FFF => {
@@ -270,9 +249,8 @@ impl BusIo for Bus {
} }
0xA000..=0xBFFF => { 0xA000..=0xBFFF => {
// 8KB External RAM // 8KB External RAM
match self.cartridge.as_mut() { if let Some(cart) = self.cart.as_mut() {
Some(cart) => cart.write_byte(addr, byte), cart.write_byte(addr, byte);
None => panic!("Tried to write into non-existent cartridge"),
} }
} }
0xC000..=0xCFFF => self.work_ram.write_byte(addr, byte), // 4KB Work RAM Bank 0 0xC000..=0xCFFF => self.work_ram.write_byte(addr, byte), // 4KB Work RAM Bank 0
@@ -305,18 +283,18 @@ impl BusIo for Bus {
_ => {} _ => {}
} }
} }
0xFEA0..=0xFEFF => {} // TODO: As far as I know, writes to here do nothing. 0xFEA0..=0xFEFF => {} // FIXME: As far as I know, writes to here do nothing.
0xFF00..=0xFF7F => { 0xFF00..=0xFF7F => {
// IO Registers // IO Registers
// Every address here starts with 0xFF so we can just check the // Every address here starts with 0xFF so we can just check the
// low byte to figure out which register it is // low byte to figure out which register it is
match addr & 0x00FF { match addr & 0x00FF {
0x00 => self.joypad.update(byte), 0x00 => self.joyp.update(byte),
0x01 => self.serial.next = byte, 0x01 => self.serial.next = byte,
0x02 => self.serial.ctrl = byte.into(), 0x02 => self.serial.ctrl = byte.into(),
0x04 => self.timer.divider = 0x0000, 0x04 => self.timer.divider = 0x0000,
0x05 => self.timer.counter = byte, 0x05 => self.timer.set_tima(byte),
0x06 => self.timer.modulo = byte, 0x06 => self.timer.modulo = byte,
0x07 => self.timer.ctrl = byte.into(), 0x07 => self.timer.ctrl = byte.into(),
0x0F => self.set_interrupt_flag(byte), 0x0F => self.set_interrupt_flag(byte),
@@ -346,13 +324,15 @@ impl BusIo for Bus {
0x4A => self.ppu.pos.window_y = byte, 0x4A => self.ppu.pos.window_y = byte,
0x4B => self.ppu.pos.window_x = byte, 0x4B => self.ppu.pos.window_x = byte,
0x4D => {} // CGB Specific Register 0x4D => {} // CGB Specific Register
0x4F => {} // CGB VRAM Bank Select
0x50 => { 0x50 => {
// Disable Boot ROM // Disable Boot ROM
if byte != 0 { if byte != 0 {
self.boot = None; self.boot = None;
} }
} }
_ => eprintln!("Wrote {:#04X} to unused IO register {:#06X}.", byte, addr), 0x70 => {} // CGB WRAM Bank Select
_ => tracing::warn!("Attempted write of {:#04X} to {:#06X} on IO", byte, addr),
}; };
} }
0xFF80..=0xFFFE => { 0xFF80..=0xFFFE => {
@@ -367,17 +347,6 @@ impl BusIo for Bus {
} }
} }
impl Bus {
pub(crate) fn read_word(&self, addr: u16) -> u16 {
(self.read_byte(addr + 1) as u16) << 8 | self.read_byte(addr) as u16
}
pub(crate) fn write_word(&mut self, addr: u16, word: u16) {
self.write_byte(addr + 1, (word >> 8) as u8);
self.write_byte(addr, (word & 0x00FF) as u8);
}
}
impl Bus { impl Bus {
fn interrupt_flag(&self) -> InterruptFlag { fn interrupt_flag(&self) -> InterruptFlag {
// Read the current interrupt information from the PPU // Read the current interrupt information from the PPU
@@ -385,7 +354,7 @@ impl Bus {
let lcd_stat = self.ppu.int.lcd_stat(); let lcd_stat = self.ppu.int.lcd_stat();
// Read the current interrupt information from the Joypad // Read the current interrupt information from the Joypad
let joypad = self.joypad.interrupt(); let joypad = self.joyp.interrupt();
// Read the current interrupt information from the Timer // Read the current interrupt information from the Timer
let timer = self.timer.interrupt(); let timer = self.timer.interrupt();
@@ -415,7 +384,7 @@ impl Bus {
self.ppu.int.set_lcd_stat(lcd_stat); self.ppu.int.set_lcd_stat(lcd_stat);
// Update the Joypad's instance of the following interrupts // Update the Joypad's instance of the following interrupts
self.joypad.set_interrupt(joypad); self.joyp.set_interrupt(joypad);
// Update the Timer's instance of the following interrupts // Update the Timer's instance of the following interrupts
self.timer.set_interrupt(timer); self.timer.set_interrupt(timer);

File diff suppressed because it is too large Load Diff

View File

@@ -1,151 +1,132 @@
use crate::apu::Apu; use crate::bus::{Bus, BusIo, BOOT_SIZE};
use crate::bus::{Bus, BusIo};
use crate::instruction::cycle::Cycle;
use crate::instruction::Instruction; use crate::instruction::Instruction;
use crate::interrupt::{InterruptEnable, InterruptFlag}; use crate::interrupt::{InterruptEnable, InterruptFlag};
use crate::joypad::Joypad; use crate::Cycle;
use crate::ppu::Ppu;
use bitfield::bitfield; use bitfield::bitfield;
use std::fmt::{Display, Formatter, Result as FmtResult}; use std::fmt::{Display, Formatter, Result as FmtResult};
#[derive(Debug, Default)] #[derive(Debug)]
pub struct Cpu { pub struct Cpu {
pub bus: Bus, pub(crate) bus: Bus,
reg: Registers, reg: Registers,
flags: Flags, flags: Flags,
ime: ImeState, ime: ImeState,
// TODO: Merge halted and state properties
halted: Option<HaltState>,
state: State, state: State,
} }
impl Cpu { impl Cpu {
pub fn new() -> Self { pub(crate) fn new(rom: [u8; BOOT_SIZE]) -> Self {
Self { Self {
reg: Registers { bus: Bus::with_boot(rom),
a: 0x01, reg: Default::default(),
b: 0x00, flags: Flags(0),
c: 0x13, ime: ImeState::Disabled,
d: 0x00, state: State::Execute,
e: 0xD8,
h: 0x01,
l: 0x4D,
sp: 0xFFFE,
pc: 0x0100,
},
flags: 0xb0.into(),
..Default::default()
} }
} }
pub fn boot_new(path: &str) -> anyhow::Result<Self> { pub(crate) fn ime(&self) -> ImeState {
Ok(Self { self.ime
bus: Bus::with_boot(path)?,
..Default::default()
})
}
pub(crate) fn ime(&self) -> &ImeState {
&self.ime
} }
pub(crate) fn set_ime(&mut self, state: ImeState) { pub(crate) fn set_ime(&mut self, state: ImeState) {
self.ime = state; self.ime = state;
} }
pub(crate) fn halt(&mut self, state: HaltState) { pub(crate) fn halt_cpu(&mut self, kind: HaltKind) {
self.halted = Some(state); self.state = State::Halt(kind);
} }
fn resume(&mut self) { fn resume_execution(&mut self) {
self.halted = None; self.state = State::Execute;
} }
pub(crate) fn halted(&self) -> Option<&HaltState> { pub(crate) fn is_halted(&self) -> bool {
self.halted.as_ref() matches!(self.state, State::Halt(_))
} }
fn inc_pc(&mut self) { pub(crate) fn halt_kind(&self) -> Option<HaltKind> {
self.reg.pc += 1; match self.state {
State::Halt(kind) => Some(kind),
_ => None,
}
} }
}
pub fn load_cartridge(&mut self, path: &str) -> std::io::Result<()> { impl Default for Cpu {
self.bus.load_cartridge(path) fn default() -> Self {
} Cpu::new(*include_bytes!("../bin/bootix_dmg.bin"))
pub fn rom_title(&self) -> Option<&str> {
self.bus.rom_title()
} }
} }
impl Cpu { impl Cpu {
fn fetch(&self) -> u8 { /// Fetch an [Instruction] from the memory bus
self.bus.read_byte(self.reg.pc) /// (4 cycles)
} fn fetch(&mut self) -> u8 {
let byte = self.read_byte(self.reg.pc);
pub(crate) fn imm_byte(&mut self) -> u8 { self.bus.clock();
let byte = self.bus.read_byte(self.reg.pc);
self.reg.pc += 1; self.reg.pc += 1;
byte byte
} }
pub(crate) fn imm_word(&mut self) -> u16 { /// Decode a byte into an [SM83](Cpu) [Instruction]
let word = self.bus.read_word(self.reg.pc); ///
self.reg.pc += 2; /// If opcode == 0xCB, then decoding costs 4 cycles.
word /// Otherwise, decoding is free
}
pub(crate) fn decode(&mut self, opcode: u8) -> Instruction { pub(crate) fn decode(&mut self, opcode: u8) -> Instruction {
if opcode == 0xCB { if opcode == 0xCB {
Instruction::decode(self.imm_byte(), true) Instruction::decode(self.fetch(), true)
} else { } else {
Instruction::decode(opcode, false) Instruction::decode(opcode, false)
} }
} }
/// Execute an [Instruction].
///
/// The amount of cycles necessary to execute an instruction range from
/// 0 to 20 T-cycles
fn execute(&mut self, instruction: Instruction) -> Cycle { fn execute(&mut self, instruction: Instruction) -> Cycle {
Instruction::execute(self, instruction) Instruction::execute(self, instruction)
} }
pub fn step(&mut self) -> Cycle { /// Perform the [`Cpu::fetch()`] [`Cpu::decode()`] [`Cpu::execute()`]
/// routine.
///
/// Handle HALT and interrupts.
pub(crate) fn step(&mut self) -> Cycle {
// Log instructions // Log instructions
// if !self.bus.boot_mapped() { // if self.reg.pc > 0xFF {
// let out = std::io::stdout(); // let out = std::io::stdout();
// let _ = self._log_state(out.lock()); // let _ = self._print_logs(out.lock());
// } // }
// FIXME: The Halt instruction takes less cycles than it should in Blargg's 2nd cpu_instrs test if let Some(elapsed) = self.handle_interrupt() {
let cycles = match self.halted() { return elapsed;
Some(state) => {
use HaltState::*;
match state {
ImeEnabled | NonePending => Cycle::new(4),
SomePending => todo!("Implement HALT bug"),
}
}
None => {
let opcode = self.fetch();
self.inc_pc();
let instr = self.decode(opcode);
let cycles = self.execute(instr);
self.check_ime();
cycles
}
};
let pending: u32 = cycles.into();
for _ in 0..pending {
self.bus.clock();
} }
// TODO: This is in the wrong place if let Some(kind) = self.halt_kind() {
self.handle_interrupts(); use HaltKind::*;
cycles self.bus.clock();
return match kind {
ImeEnabled | NonePending => 4,
SomePending => todo!("Implement HALT bug"),
};
}
let opcode = self.fetch();
let instr = self.decode(opcode);
let elapsed = self.execute(instr);
self.handle_ei();
// For use in Blargg's Test ROMs
// if self.read_byte(0xFF02) == 0x81 {
// let c = self.read_byte(0xFF01) as char;
// self.write_byte(0xFF02, 0x00);
// eprint!("{}", c);
// }
elapsed
} }
} }
@@ -160,105 +141,92 @@ impl BusIo for Cpu {
} }
impl Cpu { impl Cpu {
pub(crate) fn write_word(&mut self, addr: u16, word: u16) { fn handle_ei(&mut self) {
self.bus.write_word(addr, word)
}
}
impl Cpu {
pub fn ppu(&mut self) -> &Ppu {
&self.bus.ppu
}
pub fn apu_mut(&mut self) -> &mut Apu {
&mut self.bus.apu
}
pub(crate) fn joypad_mut(&mut self) -> &mut Joypad {
&mut self.bus.joypad
}
fn check_ime(&mut self) {
match self.ime { match self.ime {
ImeState::Pending => { ImeState::EiExecuted => self.ime = ImeState::Pending,
// This is within the context of the EI instruction, we need to not update EI until the end of the ImeState::Pending => self.ime = ImeState::Enabled,
// next executed Instruction ImeState::Disabled | ImeState::Enabled => {}
self.ime = ImeState::PendingEnd;
}
ImeState::PendingEnd => {
// The Instruction after EI has now been executed, so we want to enable the IME flag here
self.ime = ImeState::Enabled;
}
ImeState::Disabled | ImeState::Enabled => {} // Do Nothing
} }
} }
fn handle_interrupts(&mut self) { pub(crate) fn int_request(&self) -> u8 {
let req = self.read_byte(0xFF0F); self.read_byte(0xFF0F)
let enabled = self.read_byte(0xFFFF); }
if self.halted.is_some() { pub(crate) fn int_enable(&self) -> u8 {
self.read_byte(0xFFFF)
}
fn handle_interrupt(&mut self) -> Option<Cycle> {
let irq = self.int_request();
let enable = self.int_enable();
// TODO: Ensure that this behaviour is correct
if self.is_halted() {
// When we're here either a HALT with IME set or // When we're here either a HALT with IME set or
// a HALT with IME not set and No pending Interrupts was called // a HALT with IME not set and No pending Interrupts was called
if req & enabled != 0 { if irq & enable != 0 {
// The if self.ime() below correctly follows the "resuming from HALT" behaviour so // The if self.ime() below correctly follows the "resuming from HALT" behaviour so
// nothing actually needs to be added here. This is just documentation // nothing actually needs to be added here. This is just documentation
// since it's a bit weird why nothing is being done // since it's a bit weird why nothing is being done
self.resume() self.resume_execution();
} }
} }
if let ImeState::Enabled = self.ime() { match self.ime() {
let mut req: InterruptFlag = req.into(); ImeState::Enabled => {
let enabled: InterruptEnable = enabled.into(); let mut irq: InterruptFlag = irq.into();
let enable: InterruptEnable = enable.into();
let vector = if req.vblank() && enabled.vblank() { let rst_vector = if irq.vblank() && enable.vblank() {
// Handle VBlank Interrupt // Handle VBlank Interrupt
req.set_vblank(false); irq.set_vblank(false);
// INT 40h // INT 40h
Some(0x40) Some(0x40)
} else if req.lcd_stat() && enabled.lcd_stat() { } else if irq.lcd_stat() && enable.lcd_stat() {
// Handle LCD STAT Interrupt // Handle LCD STAT Interrupt
req.set_lcd_stat(false); irq.set_lcd_stat(false);
// INT 48h // INT 48h
Some(0x48) Some(0x48)
} else if req.timer() && enabled.timer() { } else if irq.timer() && enable.timer() {
// Handle Timer Interrupt // Handle Timer Interrupt
req.set_timer(false); irq.set_timer(false);
// INT 50h // INT 50h
Some(0x50) Some(0x50)
} else if req.serial() && enabled.serial() { } else if irq.serial() && enable.serial() {
// Handle Serial Interrupt // Handle Serial Interrupt
req.set_serial(false); irq.set_serial(false);
// INT 58h // INT 58h
Some(0x58) Some(0x58)
} else if req.joypad() && enabled.joypad() { } else if irq.joypad() && enable.joypad() {
// Handle Joypad Interrupt // Handle Joypad Interrupt
req.set_joypad(false); irq.set_joypad(false);
// INT 60h // INT 60h
Some(0x60) Some(0x60)
} else { } else {
None None
}; };
let _ = match vector { match rst_vector {
Some(address) => { Some(vector) => {
// Write the Changes to 0xFF0F and 0xFFFF registers // Write the Changes to 0xFF0F and 0xFFFF registers
self.write_byte(0xFF0F, req.into()); self.write_byte(0xFF0F, irq.into());
// Disable all future interrupts // Disable all future interrupts
self.set_ime(ImeState::Disabled); self.set_ime(ImeState::Disabled);
Instruction::reset(self, address) Some(Instruction::reset(self, vector))
}
None => None,
} }
None => Cycle::new(0), // NO Interrupts were enabled and / or requested }
}; _ => None,
} }
} }
} }
@@ -266,16 +234,10 @@ impl Cpu {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum State { enum State {
Execute, Execute,
// Halt, Halt(HaltKind),
// Stop, // Stop,
} }
impl Default for State {
fn default() -> Self {
Self::Execute
}
}
impl Cpu { impl Cpu {
pub(crate) fn set_register(&mut self, register: Register, value: u8) { pub(crate) fn set_register(&mut self, register: Register, value: u8) {
use Register::*; use Register::*;
@@ -363,10 +325,9 @@ impl Cpu {
} }
impl Cpu { impl Cpu {
fn _debug_log(&self, mut w: impl std::io::Write, instr: &Instruction) -> std::io::Result<()> { fn _print_debug(&self, mut w: impl std::io::Write) -> std::io::Result<()> {
let pc = self.reg.pc - 1;
write!(w, "A: {:02X} ", self.reg.a)?; write!(w, "A: {:02X} ", self.reg.a)?;
write!(w, "F: {:04b} ", u8::from(self.flags) >> 4)?; write!(w, "F: {:02X} ", u8::from(self.flags))?;
write!(w, "B: {:02X} ", self.reg.b)?; write!(w, "B: {:02X} ", self.reg.b)?;
write!(w, "C: {:02X} ", self.reg.c)?; write!(w, "C: {:02X} ", self.reg.c)?;
write!(w, "D: {:02X} ", self.reg.d)?; write!(w, "D: {:02X} ", self.reg.d)?;
@@ -374,31 +335,40 @@ impl Cpu {
write!(w, "H: {:02X} ", self.reg.h)?; write!(w, "H: {:02X} ", self.reg.h)?;
write!(w, "L: {:02X} ", self.reg.l)?; write!(w, "L: {:02X} ", self.reg.l)?;
write!(w, "SP: {:04X} ", self.reg.sp)?; write!(w, "SP: {:04X} ", self.reg.sp)?;
write!(w, "PC: 00:{:04X} ", pc)?; write!(w, "PC: 00:{:04X} ", self.reg.pc)?;
write!(w, "({:02X} ", self.read_byte(pc))?; write!(w, "({:02X} ", self.read_byte(self.reg.pc))?;
write!(w, "{:02X} ", self.read_byte(pc + 1))?; write!(w, "{:02X} ", self.read_byte(self.reg.pc + 1))?;
write!(w, "{:02X} ", self.read_byte(pc + 2))?; write!(w, "{:02X} ", self.read_byte(self.reg.pc + 2))?;
write!(w, "{:02X}) ", self.read_byte(pc + 3))?; write!(w, "{:02X})", self.read_byte(self.reg.pc + 3))?;
writeln!(w, "| {:?}", instr)?; writeln!(w, "| {:?}", self._dbg_instr())?;
w.flush() w.flush()
} }
fn _log_state(&self, mut writer: impl std::io::Write) -> std::io::Result<()> { fn _print_logs(&self, mut w: impl std::io::Write) -> std::io::Result<()> {
write!(writer, "A: {:02X} ", self.reg.a)?; write!(w, "A: {:02X} ", self.reg.a)?;
write!(writer, "F: {:02X} ", u8::from(self.flags))?; write!(w, "F: {:02X} ", u8::from(self.flags))?;
write!(writer, "B: {:02X} ", self.reg.b)?; write!(w, "B: {:02X} ", self.reg.b)?;
write!(writer, "C: {:02X} ", self.reg.c)?; write!(w, "C: {:02X} ", self.reg.c)?;
write!(writer, "D: {:02X} ", self.reg.d)?; write!(w, "D: {:02X} ", self.reg.d)?;
write!(writer, "E: {:02X} ", self.reg.e)?; write!(w, "E: {:02X} ", self.reg.e)?;
write!(writer, "H: {:02X} ", self.reg.h)?; write!(w, "H: {:02X} ", self.reg.h)?;
write!(writer, "L: {:02X} ", self.reg.l)?; write!(w, "L: {:02X} ", self.reg.l)?;
write!(writer, "SP: {:04X} ", self.reg.sp)?; write!(w, "SP: {:04X} ", self.reg.sp)?;
write!(writer, "PC: 00:{:04X} ", self.reg.pc)?; write!(w, "PC: 00:{:04X} ", self.reg.pc)?;
write!(writer, "({:02X} ", self.read_byte(self.reg.pc))?; write!(w, "({:02X} ", self.read_byte(self.reg.pc))?;
write!(writer, "{:02X} ", self.read_byte(self.reg.pc + 1))?; write!(w, "{:02X} ", self.read_byte(self.reg.pc + 1))?;
write!(writer, "{:02X} ", self.read_byte(self.reg.pc + 2))?; write!(w, "{:02X} ", self.read_byte(self.reg.pc + 2))?;
writeln!(writer, "{:02X})", self.read_byte(self.reg.pc + 3))?; writeln!(w, "{:02X})", self.read_byte(self.reg.pc + 3))?;
writer.flush() w.flush()
}
fn _dbg_instr(&self) -> Instruction {
let byte = self.read_byte(self.reg.pc);
if byte == 0xCB {
Instruction::decode(self.read_byte(self.reg.pc + 1), true)
} else {
Instruction::decode(byte, false)
}
} }
} }
@@ -461,12 +431,6 @@ impl Clone for Flags {
} }
} }
impl Default for Flags {
fn default() -> Self {
Self(0)
}
}
impl Display for Flags { impl Display for Flags {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if self.z() { if self.z() {
@@ -508,7 +472,7 @@ impl From<u8> for Flags {
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) enum HaltState { pub(crate) enum HaltKind {
ImeEnabled, ImeEnabled,
NonePending, NonePending,
SomePending, SomePending,
@@ -517,13 +481,43 @@ pub(crate) enum HaltState {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) enum ImeState { pub(crate) enum ImeState {
Disabled, Disabled,
EiExecuted,
Pending, Pending,
PendingEnd,
Enabled, Enabled,
} }
impl Default for ImeState { pub(crate) mod dbg {
fn default() -> Self { use super::{Cpu, ImeState, RegisterPair};
Self::Disabled
pub(crate) fn flags(cpu: &Cpu) -> u8 {
cpu.flags.into()
}
pub(crate) fn af(cpu: &Cpu) -> u16 {
cpu.register_pair(RegisterPair::AF)
}
pub(crate) fn bc(cpu: &Cpu) -> u16 {
cpu.register_pair(RegisterPair::BC)
}
pub(crate) fn de(cpu: &Cpu) -> u16 {
cpu.register_pair(RegisterPair::DE)
}
pub(crate) fn hl(cpu: &Cpu) -> u16 {
cpu.register_pair(RegisterPair::HL)
}
pub(crate) fn sp(cpu: &Cpu) -> u16 {
cpu.register_pair(RegisterPair::SP)
}
pub(crate) fn pc(cpu: &Cpu) -> u16 {
cpu.register_pair(RegisterPair::PC)
}
pub(crate) fn ime(cpu: &Cpu) -> bool {
matches!(cpu.ime, ImeState::Enabled)
} }
} }

View File

@@ -1,60 +1,163 @@
use crate::cpu::Cpu as SM83; use crate::apu::gen::SampleProducer;
use crate::instruction::cycle::Cycle; use crate::bus::BOOT_SIZE;
use crate::joypad; use crate::cartridge::Cartridge;
use crate::ppu::Ppu; use crate::cpu::Cpu;
use anyhow::Result; use crate::{Cycle, GB_HEIGHT, GB_WIDTH};
use clap::crate_name;
use gilrs::Gilrs; use gilrs::Gilrs;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use winit_input_helper::WinitInputHelper; use thiserror::Error;
use winit::event::KeyboardInput;
use winit::event_loop::ControlFlow;
pub const SM83_CYCLE_TIME: Duration = Duration::from_nanos(1_000_000_000 / SM83_CLOCK_SPEED); pub const SM83_CYCLE_TIME: Duration = Duration::from_nanos(1_000_000_000 / SM83_CLOCK_SPEED);
pub const CYCLES_IN_FRAME: Cycle = Cycle::new(456 * 154); // 456 Cycles times 154 scanlines pub const CYCLES_IN_FRAME: Cycle = 456 * 154; // 456 Cycles times 154 scanlines
pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz
const DEFAULT_TITLE: &str = "DMG-01 Emulator"; const DEFAULT_TITLE: &str = "Game Boy Screen";
const GAMEPAD_ENABLED: bool = false;
pub fn init(boot_path: Option<&str>, rom_path: &str) -> Result<SM83> { #[inline]
let mut cpu = match boot_path { pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) {
Some(path) => SM83::boot_new(path)?, run(cpu, gamepad, key, CYCLES_IN_FRAME)
None => SM83::new(),
};
eprintln!("Initialized GB Emulator");
cpu.load_cartridge(rom_path)?;
Ok(cpu)
} }
pub fn rom_title(game_boy: &SM83) -> &str { #[inline]
game_boy.rom_title().unwrap_or(DEFAULT_TITLE) pub fn run(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput, cycles: Cycle) {
} let mut elapsed = 0;
pub fn run( if let Some(event) = gamepad.next_event() {
game_boy: &mut SM83, crate::joypad::handle_gamepad_input(&mut cpu.bus.joyp, event);
gamepad: &mut Gilrs,
input: &WinitInputHelper,
pending: Cycle,
) -> Cycle {
let mut elapsed = Cycle::new(0);
while elapsed < pending {
elapsed += run_unsynced(game_boy, gamepad, input);
} }
crate::joypad::handle_keyboard_input(&mut cpu.bus.joyp, key);
elapsed while elapsed < cycles {
elapsed += cpu.step();
}
} }
pub fn run_unsynced(game_boy: &mut SM83, gamepad: &mut Gilrs, input: &WinitInputHelper) -> Cycle { pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) {
if GAMEPAD_ENABLED { write_save(cpu);
if let Some(event) = gamepad.next_event() { *control_flow = ControlFlow::Exit;
joypad::handle_gamepad_input(game_boy.joypad_mut(), event); }
#[inline]
pub fn pixel_buf(cpu: &Cpu) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] {
use crate::ppu::Device;
cpu.bus.ppu.frame_buf.get(Device::Host)
}
pub fn from_boot_rom<P: AsRef<Path>>(path: P) -> std::io::Result<Cpu> {
Ok(Cpu::new(read_boot(path)?))
}
pub fn read_game_rom<P: AsRef<Path>>(cpu: &mut Cpu, path: P) -> std::io::Result<()> {
cpu.bus.cart = Some(Cartridge::new(std::fs::read(path.as_ref())?));
Ok(())
}
pub fn set_audio_prod(cpu: &mut Cpu, prod: SampleProducer<f32>) {
cpu.bus.apu.prod = Some(prod);
}
pub fn rom_title(cpu: &Cpu) -> &str {
cpu.bus
.cart
.as_ref()
.and_then(|c| c.title.as_deref())
.unwrap_or(DEFAULT_TITLE)
}
pub fn write_save(cpu: &Cpu) {
match cpu.bus.cart.as_ref() {
Some(cart) => match write_save_to_file(cart) {
Ok(path) => tracing::info!("Wrote to save at {:?}", path),
Err(err @ SaveError::NotApplicable) => tracing::warn!("Unable to Save: {:?}", err),
Err(SaveError::DiffSize) => unreachable!(),
Err(SaveError::Io(err)) => tracing::error!("{:?}", err),
},
None => tracing::error!("No cartridge is currently present"),
}
}
pub fn load_save(cpu: &mut Cpu) {
match cpu.bus.cart.as_mut() {
Some(cart) => match read_save_from_file(cart) {
Ok(path) => tracing::info!("Loaded save from {:?}", path),
Err(err @ SaveError::NotApplicable) => tracing::warn!("Unable to load save: {}", err),
Err(err @ SaveError::DiffSize) => tracing::error!("Unable to load save: {}", err),
Err(SaveError::Io(err)) => match err.kind() {
std::io::ErrorKind::NotFound => tracing::warn!("Save not found"),
_ => tracing::error!("{:?}", err),
},
},
None => tracing::error!("No cartridge is currently present"),
}
}
fn write_save_to_file(cart: &Cartridge) -> Result<PathBuf, SaveError> {
match cart.title.as_ref().zip(cart.ext_ram()) {
Some((title, ram)) => {
let mut save_path = data_path().unwrap_or_else(|| PathBuf::from("."));
save_path.push(title);
save_path.set_extension("sav");
let mut file = File::create(&save_path)?;
file.write_all(ram)?;
Ok(save_path)
} }
None => Err(SaveError::NotApplicable),
} }
joypad::handle_keyboard_input(game_boy.joypad_mut(), input);
game_boy.step()
} }
pub fn draw(ppu: &Ppu, frame: &mut [u8]) { fn read_save_from_file(cart: &mut Cartridge) -> Result<PathBuf, SaveError> {
ppu.copy_to_gui(frame); match cart.title.clone().zip(cart.ext_ram_mut()) {
Some((title, ext_ram)) => {
let mut save_path = data_path().unwrap_or_else(|| PathBuf::from("."));
save_path.push(title);
save_path.set_extension("sav");
let mut file = File::open(&save_path)?;
let mut memory = Vec::new();
file.read_to_end(&mut memory)?;
if ext_ram.len() != memory.len() {
return Err(SaveError::DiffSize);
}
ext_ram.copy_from_slice(&memory);
Ok(save_path)
}
None => Err(SaveError::NotApplicable),
}
}
fn read_boot<P: AsRef<Path>>(path: P) -> std::io::Result<[u8; BOOT_SIZE]> {
let mut buf = [0; BOOT_SIZE];
let mut file = File::open(path.as_ref())?;
file.read_exact(&mut buf)?;
Ok(buf)
}
fn data_path() -> Option<PathBuf> {
match directories_next::ProjectDirs::from("dev", "musuka", crate_name!()) {
Some(dirs) => {
let data_local = dirs.data_local_dir();
std::fs::create_dir_all(data_local).ok()?;
Some(data_local.to_path_buf())
}
None => None,
}
}
#[derive(Debug, Error)]
pub enum SaveError {
#[error("cartridge lacks title and/or external ram")]
NotApplicable,
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("save file size differs from external ram")]
DiffSize,
} }

472
src/gui.rs Normal file
View File

@@ -0,0 +1,472 @@
use egui::{Context, TextureId};
use egui_wgpu_backend::{wgpu, RenderPass, ScreenDescriptor};
use egui_winit_platform::Platform;
use wgpu::{
Adapter, CommandEncoder, Device, Extent3d, FilterMode, Instance, Queue, RequestDeviceError,
Surface, SurfaceCapabilities, SurfaceConfiguration, Texture, TextureView,
TextureViewDescriptor,
};
use winit::dpi::PhysicalSize;
use winit::error::OsError;
use winit::event::{ElementState, Event, KeyboardInput};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::Window;
use crate::cpu::Cpu;
use crate::{emu, GB_HEIGHT, GB_WIDTH};
const EGUI_DIMENSIONS: (usize, usize) = (1280, 720);
const FILTER_MODE: FilterMode = FilterMode::Nearest;
const WINDOW_TITLE: &str = "DMG-01 Emulator";
const SCALE: f32 = 3.0;
pub struct Gui {
pub should_quit: bool,
pub title: String,
pub mode: EmuMode,
window: Window,
platform: Platform,
surface: Surface,
device: Device,
queue: Queue,
surface_config: SurfaceConfiguration,
render_pass: RenderPass,
texture: Texture,
texture_size: Extent3d,
texture_id: TextureId,
}
impl Gui {
pub fn new<T>(title: String, event_loop: &EventLoop<T>, cpu: &Cpu) -> Self {
use wgpu::InstanceDescriptor;
let window = build_window(event_loop).expect("build window");
let instance = Instance::new(InstanceDescriptor::default());
let surface = unsafe {
instance
.create_surface(&window)
.expect("create wgpu instance surface")
};
let adapter = request_adapter(&instance, &surface).expect("request adaptor");
let (device, queue) = request_device(&adapter).expect("request device");
let capabilities = surface.get_capabilities(&adapter);
let surface_config = surface_config(&window, capabilities);
surface.configure(&device, &surface_config);
let platform = platform(&window);
let mut render_pass = RenderPass::new(&device, surface_config.format, 1);
let texture_size = texture_size();
let texture = create_texture(&device, texture_size);
write_to_texture(&queue, &texture, emu::pixel_buf(cpu), texture_size);
let view = texture.create_view(&TextureViewDescriptor::default());
let texture_id = expose_texture_to_egui(&mut render_pass, &device, &view);
Self {
should_quit: Default::default(),
title,
mode: EmuMode::Running,
window,
platform,
surface,
device,
queue,
surface_config,
render_pass,
texture,
texture_size,
texture_id,
}
}
pub fn maybe_quit(&self, cpu: &Cpu, control_flow: &mut ControlFlow) {
if self.should_quit {
emu::save_and_exit(cpu, control_flow);
}
}
pub fn request_redraw(&self) {
self.window.request_redraw();
}
pub fn handle_event<T>(&mut self, event: &Event<T>) {
self.platform.handle_event(event);
}
pub fn update_time(&mut self, elapsed_seconds: f64) {
self.platform.update_time(elapsed_seconds);
}
pub fn resize(&mut self, size: PhysicalSize<u32>) {
// See: https://github.com/rust-windowing/winit/issues/208
if size.width > 0 && size.height > 0 {
self.surface_config.width = size.width;
self.surface_config.height = size.height;
self.surface.configure(&self.device, &self.surface_config);
}
}
pub fn paint(&mut self, cpu: &Cpu) {
use wgpu::{Color, SurfaceError};
let data = emu::pixel_buf(cpu);
write_to_texture(&self.queue, &self.texture, data, self.texture_size);
let output_frame = match self.surface.get_current_texture() {
Ok(frame) => frame,
Err(SurfaceError::Outdated) => return, // Occurs on minimization on Windows
Err(e) => {
eprintln!("Dropped frame with error: {}", e);
return;
}
};
let output_view = output_frame
.texture
.create_view(&TextureViewDescriptor::default());
// Begin to draw Egui components
self.platform.begin_frame();
self.draw_egui(cpu, &self.platform.context(), self.texture_id);
// End the UI frame. We could now handle the output and draw the UI with the backend.
let full_output = self.platform.end_frame(Some(&self.window));
let paint_jobs = self.platform.context().tessellate(full_output.shapes);
let mut encoder = create_command_encoder(&self.device);
let screen_descriptor = create_screen_descriptor(&self.window, &self.surface_config);
let tdelta = full_output.textures_delta;
// Upload all resources for the GPU.
self.render_pass
.add_textures(&self.device, &self.queue, &tdelta)
.expect("add texture ok");
self.render_pass
.update_buffers(&self.device, &self.queue, &paint_jobs, &screen_descriptor);
// Record all render passes.
self.render_pass
.execute(
&mut encoder,
&output_view,
&paint_jobs,
&screen_descriptor,
Some(Color::BLACK),
)
.expect("execute render pass");
// Submit the commands.
self.queue.submit(std::iter::once(encoder.finish()));
// Redraw egui
output_frame.present();
self.render_pass
.remove_textures(tdelta)
.expect("remove texture delta");
}
#[inline]
pub fn draw_egui(&mut self, cpu: &Cpu, ctx: &Context, texture_id: TextureId) {
use crate::{cpu, instruction, ppu};
fn selectable_text(ui: &mut egui::Ui, mut text: &str) -> egui::Response {
ui.add(egui::TextEdit::multiline(&mut text).code_editor())
}
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Quit").clicked() {
self.should_quit = true;
}
});
egui::Window::new(&self.title).show(ctx, |ui| {
ui.image(
texture_id,
[GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE],
);
});
egui::Window::new("Disassembly").show(ctx, |ui| {
selectable_text(ui, &instruction::dbg::tmp_disasm(cpu, 20));
});
egui::Window::new("Settings").show(ctx, |ui| {
egui::ComboBox::from_label("Emulation Mode")
.selected_text(format!("{:?}", self.mode))
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.mode, EmuMode::Running, "Running");
ui.selectable_value(&mut self.mode, EmuMode::Stopped, "Stopped");
ui.selectable_value(&mut self.mode, EmuMode::StepFrame, "Step Frame");
ui.selectable_value(&mut self.mode, EmuMode::Step, "Step");
})
});
egui::Window::new("GB Info").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.heading("CPU");
ui.monospace(format!("AF: {:#06X}", cpu::dbg::af(cpu)));
ui.monospace(format!("BC: {:#06X}", cpu::dbg::bc(cpu)));
ui.monospace(format!("DE: {:#06X}", cpu::dbg::de(cpu)));
ui.monospace(format!("HL: {:#06X}", cpu::dbg::hl(cpu)));
ui.add_space(10.0);
ui.monospace(format!("SP: {:#06X}", cpu::dbg::sp(cpu)));
ui.monospace(format!("PC: {:#06X}", cpu::dbg::pc(cpu)));
});
ui.vertical(|ui| {
let ppu = &cpu.bus.ppu;
ui.heading("PPU");
ui.monospace(format!("LY: {}", ppu::dbg::ly(ppu)));
ui.horizontal(|ui| {
ui.monospace(format!("SCX: {}", ppu::dbg::scx(ppu)));
ui.monospace(format!("SCY: {}", ppu::dbg::scy(ppu)));
});
ui.horizontal(|ui| {
ui.monospace(format!("WX: {}", ppu::dbg::wx(ppu)));
ui.monospace(format!("WY: {}", ppu::dbg::wy(ppu)));
});
ui.monospace(format!(
"Mode: {:?} {}",
ppu::dbg::mode(ppu),
ppu::dbg::dot(ppu)
))
});
});
ui.add_space(10.0);
let _ = ui.selectable_label(cpu::dbg::ime(cpu), "IME");
ui.horizontal(|ui| {
let irq = cpu.int_request();
ui.label("IRQ:");
let _ = ui.selectable_label(irq & 0b01 == 0x01, "VBlank");
let _ = ui.selectable_label(irq >> 1 & 0x01 == 0x01, "LCD STAT");
let _ = ui.selectable_label(irq >> 2 & 0x01 == 0x01, "Timer");
let _ = ui.selectable_label(irq >> 3 & 0x01 == 0x01, "Serial");
let _ = ui.selectable_label(irq >> 4 & 0x01 == 0x01, "Joypad");
});
ui.horizontal(|ui| {
let ie = cpu.int_enable();
// TODO: Reimplement this
// let r_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'R');
// let e_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'E');
// let q_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'Q');
ui.label("IE:");
// ui.add_space(q_len - (e_len - r_len));
let _ = ui.selectable_label(ie & 0b01 == 0x01, "VBlank");
let _ = ui.selectable_label(ie >> 1 & 0x01 == 0x01, "LCD STAT");
let _ = ui.selectable_label(ie >> 2 & 0x01 == 0x01, "Timer");
let _ = ui.selectable_label(ie >> 3 & 0x01 == 0x01, "Serial");
let _ = ui.selectable_label(ie >> 4 & 0x01 == 0x01, "Joypad");
});
ui.add_space(10.0);
ui.horizontal(|ui| {
let flags = cpu::dbg::flags(cpu);
let _ = ui.selectable_label((flags >> 7 & 0x01) == 0x01, "Zero");
let _ = ui.selectable_label((flags >> 6 & 0x01) == 0x01, "Negative");
let _ = ui.selectable_label((flags >> 5 & 0x01) == 0x01, "Half-Carry");
let _ = ui.selectable_label((flags >> 4 & 0x01) == 0x01, "Carry");
})
})
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmuMode {
Running,
StepFrame,
Stopped,
Step,
}
/// To avoid using an [Option<KeyboardInput>] to keep track of user input from winit,
/// we can use a "default" value. However, in order for this to work the chosen "default"
/// value must be an **unused** key, so that it is ignored by the emulator.
pub fn unused_key() -> KeyboardInput {
#![allow(deprecated)]
KeyboardInput {
scancode: Default::default(),
state: ElementState::Released,
virtual_keycode: Default::default(),
modifiers: Default::default(), // this argument is deprecated
}
}
fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> {
use winit::window::WindowBuilder;
WindowBuilder::new()
.with_decorations(true)
.with_resizable(true)
.with_transparent(false)
.with_title(WINDOW_TITLE)
.with_inner_size(PhysicalSize {
width: EGUI_DIMENSIONS.0 as f32,
height: EGUI_DIMENSIONS.1 as f32,
})
.build(event_loop)
}
fn request_adapter(instance: &Instance, surface: &Surface) -> Option<Adapter> {
use wgpu::{PowerPreference, RequestAdapterOptions};
pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
power_preference: PowerPreference::HighPerformance,
compatible_surface: Some(surface),
force_fallback_adapter: false, // TODO: What do I want to do with this?
}))
}
fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> {
use wgpu::{DeviceDescriptor, Features, Limits};
pollster::block_on(adapter.request_device(
&DeviceDescriptor {
features: Features::default(),
limits: Limits::default(),
label: None,
},
None,
))
}
fn surface_config(window: &Window, capabilities: SurfaceCapabilities) -> SurfaceConfiguration {
use egui_wgpu_backend::wgpu::{PresentMode, TextureFormat, TextureUsages};
let size = window.inner_size();
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: capabilities.formats[0],
width: size.width as u32,
height: size.height as u32,
present_mode: PresentMode::Fifo,
alpha_mode: capabilities.alpha_modes[0],
view_formats: vec![TextureFormat::Rgba8UnormSrgb],
}
}
fn platform(window: &Window) -> Platform {
use egui::FontDefinitions;
use egui_winit_platform::PlatformDescriptor;
let size = window.inner_size();
Platform::new(PlatformDescriptor {
physical_width: size.width as u32,
physical_height: size.height as u32,
scale_factor: window.scale_factor(),
font_definitions: FontDefinitions::default(),
..Default::default()
})
}
fn texture_size() -> Extent3d {
Extent3d {
width: GB_WIDTH as u32,
height: GB_HEIGHT as u32,
..Default::default()
}
}
fn create_texture(device: &Device, size: Extent3d) -> Texture {
use wgpu::{TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
device.create_texture(&TextureDescriptor {
size,
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Bgra8Unorm,
usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
label: Some("gb_pixel_buffer"),
view_formats: &[TextureFormat::Bgra8Unorm],
})
}
#[inline]
fn write_to_texture(
queue: &Queue,
texture: &Texture,
data: &[u8; GB_WIDTH * 4 * GB_HEIGHT],
size: Extent3d,
) {
use std::num::NonZeroU32;
use wgpu::{ImageCopyTexture, ImageDataLayout, Origin3d, TextureAspect};
queue.write_texture(
ImageCopyTexture {
texture,
mip_level: 0,
origin: Origin3d::ZERO,
aspect: TextureAspect::All,
},
data,
ImageDataLayout {
offset: 0,
bytes_per_row: NonZeroU32::new(4 * GB_WIDTH as u32),
rows_per_image: NonZeroU32::new(GB_HEIGHT as u32),
},
size,
);
}
fn expose_texture_to_egui(
render_pass: &mut RenderPass,
device: &Device,
view: &TextureView,
) -> TextureId {
render_pass.egui_texture_from_wgpu_texture(device, view, FILTER_MODE)
}
#[inline]
fn create_command_encoder(device: &Device) -> CommandEncoder {
use wgpu::CommandEncoderDescriptor;
device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("encoder"),
})
}
#[inline]
fn create_screen_descriptor(window: &Window, config: &SurfaceConfiguration) -> ScreenDescriptor {
ScreenDescriptor {
physical_width: config.width,
physical_height: config.height,
scale_factor: window.scale_factor() as f32,
}
}
pub mod kbd {
use winit::event::{ElementState, KeyboardInput, VirtualKeyCode};
pub fn space_released(input: &KeyboardInput) -> bool {
let keycode = input.virtual_keycode;
matches!(input.state, ElementState::Released if keycode == Some(VirtualKeyCode::Space))
}
}

View File

@@ -17,11 +17,11 @@ impl Default for HighRam {
} }
impl BusIo for HighRam { impl BusIo for HighRam {
fn write_byte(&mut self, addr: u16, byte: u8) {
self.buf[addr as usize - HIGH_RAM_START_ADDRESS] = byte;
}
fn read_byte(&self, addr: u16) -> u8 { fn read_byte(&self, addr: u16) -> u8 {
self.buf[addr as usize - HIGH_RAM_START_ADDRESS] self.buf[addr as usize - HIGH_RAM_START_ADDRESS]
} }
fn write_byte(&mut self, addr: u16, byte: u8) {
self.buf[addr as usize - HIGH_RAM_START_ADDRESS] = byte;
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,20 @@
use bitfield::bitfield; use bitfield::bitfield;
#[derive(Debug, Default)] #[derive(Debug)]
pub(crate) struct Interrupt { pub(crate) struct Interrupt {
pub(crate) flag: InterruptFlag, pub(crate) flag: InterruptFlag,
pub(crate) enable: InterruptEnable, pub(crate) enable: InterruptEnable,
} }
impl Default for Interrupt {
fn default() -> Self {
Self {
flag: InterruptFlag(0),
enable: InterruptEnable(0),
}
}
}
bitfield! { bitfield! {
pub struct InterruptEnable(u8); pub struct InterruptEnable(u8);
impl Debug; impl Debug;
@@ -23,12 +32,6 @@ impl Clone for InterruptEnable {
} }
} }
impl Default for InterruptEnable {
fn default() -> Self {
Self(0)
}
}
impl From<u8> for InterruptEnable { impl From<u8> for InterruptEnable {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
Self(byte) Self(byte)
@@ -58,12 +61,6 @@ impl Clone for InterruptFlag {
} }
} }
impl Default for InterruptFlag {
fn default() -> Self {
Self(0)
}
}
impl From<u8> for InterruptFlag { impl From<u8> for InterruptFlag {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
Self(byte) Self(byte)

View File

@@ -1,5 +1,5 @@
use gilrs::{Button, Event as GamepadEvent, EventType as GamepadEventType}; use gilrs::{Button, Event as GamepadEvent, EventType as GamepadEventType};
use winit_input_helper::WinitInputHelper; use winit::event::{ElementState, KeyboardInput, VirtualKeyCode};
#[derive(Debug)] #[derive(Debug)]
pub struct Joypad { pub struct Joypad {
@@ -110,64 +110,39 @@ impl ButtonEvent {
} }
} }
pub fn handle_keyboard_input(pad: &mut Joypad, input: &WinitInputHelper) { #[inline]
use winit::event::VirtualKeyCode; pub(crate) fn handle_keyboard_input(pad: &mut Joypad, key: KeyboardInput) {
// TODO: What do I have to do to get a match statement here?
let state = &mut pad.ext; let state = &mut pad.ext;
let irq = &mut pad.interrupt; let irq = &mut pad.interrupt;
if input.key_pressed(VirtualKeyCode::Down) { match key.state {
state.dpad_down.update(true, irq); ElementState::Pressed => match key.virtual_keycode {
} else if input.key_released(VirtualKeyCode::Down) { Some(VirtualKeyCode::Down) => state.dpad_down.update(true, irq),
state.dpad_down.update(false, irq); Some(VirtualKeyCode::Up) => state.dpad_up.update(true, irq),
} Some(VirtualKeyCode::Left) => state.dpad_left.update(true, irq),
Some(VirtualKeyCode::Right) => state.dpad_right.update(true, irq),
if input.key_pressed(VirtualKeyCode::Up) { Some(VirtualKeyCode::Return) => state.start.update(true, irq),
state.dpad_up.update(true, irq); Some(VirtualKeyCode::RShift) => state.select.update(true, irq),
} else if input.key_released(VirtualKeyCode::Up) { Some(VirtualKeyCode::Z) => state.south.update(true, irq),
state.dpad_up.update(false, irq); Some(VirtualKeyCode::X) => state.east.update(true, irq),
} None | Some(_) => {}
},
if input.key_pressed(VirtualKeyCode::Left) { ElementState::Released => match key.virtual_keycode {
state.dpad_left.update(true, irq); Some(VirtualKeyCode::Down) => state.dpad_down.update(false, irq),
} else if input.key_released(VirtualKeyCode::Left) { Some(VirtualKeyCode::Up) => state.dpad_up.update(false, irq),
state.dpad_left.update(false, irq); Some(VirtualKeyCode::Left) => state.dpad_left.update(false, irq),
} Some(VirtualKeyCode::Right) => state.dpad_right.update(false, irq),
Some(VirtualKeyCode::Return) => state.start.update(false, irq),
if input.key_pressed(VirtualKeyCode::Right) { Some(VirtualKeyCode::RShift) => state.select.update(false, irq),
state.dpad_right.update(true, irq); Some(VirtualKeyCode::Z) => state.south.update(false, irq),
} else if input.key_released(VirtualKeyCode::Right) { Some(VirtualKeyCode::X) => state.east.update(false, irq),
state.dpad_right.update(false, irq); None | Some(_) => {}
} },
if input.key_pressed(VirtualKeyCode::T) {
state.start.update(true, irq);
} else if input.key_released(VirtualKeyCode::T) {
state.start.update(false, irq);
}
if input.key_pressed(VirtualKeyCode::Y) {
state.select.update(true, irq);
} else if input.key_released(VirtualKeyCode::Y) {
state.select.update(false, irq);
}
if input.key_pressed(VirtualKeyCode::Z) {
state.south.update(true, irq);
} else if input.key_released(VirtualKeyCode::Z) {
state.south.update(false, irq);
}
if input.key_pressed(VirtualKeyCode::X) {
state.east.update(true, irq);
} else if input.key_released(VirtualKeyCode::X) {
state.east.update(false, irq);
} }
} }
pub fn handle_gamepad_input(pad: &mut Joypad, event: GamepadEvent) { #[inline]
pub(crate) fn handle_gamepad_input(pad: &mut Joypad, event: GamepadEvent) {
use Button::*; use Button::*;
use GamepadEventType::*; use GamepadEventType::*;

View File

@@ -1,5 +1,7 @@
pub use apu::gen::AudioSPSC; #![allow(clippy::derivable_impls)] // Will remove this if bitfield-rs allows default impls
pub use instruction::cycle::Cycle;
pub use apu::gen::init as spsc_init;
pub type Cycle = u64;
pub const GB_WIDTH: usize = 160; pub const GB_WIDTH: usize = 160;
pub const GB_HEIGHT: usize = 144; pub const GB_HEIGHT: usize = 144;
@@ -9,6 +11,7 @@ mod bus;
mod cartridge; mod cartridge;
mod cpu; mod cpu;
pub mod emu; pub mod emu;
pub mod gui;
mod high_ram; mod high_ram;
mod instruction; mod instruction;
mod interrupt; mod interrupt;

View File

@@ -1,151 +1,126 @@
use anyhow::{anyhow, Result}; use std::path::PathBuf;
use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg};
use gb::{AudioSPSC, Cycle, GB_HEIGHT, GB_WIDTH};
use gilrs::Gilrs;
use pixels::{PixelsBuilder, SurfaceTexture};
use rodio::{OutputStream, Sink};
use std::time::Instant; use std::time::Instant;
use winit::dpi::LogicalSize;
use winit::event::{Event, VirtualKeyCode};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::{Window, WindowBuilder};
use winit_input_helper::WinitInputHelper;
const SCALE: f64 = 2.0; use clap::{arg, command, value_parser};
use gb::gui::{EmuMode, Gui};
use gb::{emu, gui};
use gilrs::Gilrs;
use rodio::{OutputStream, Sink};
use tracing_subscriber::EnvFilter;
use winit::event::{Event, WindowEvent};
use winit::event_loop::{EventLoop, EventLoopBuilder};
fn main() -> Result<()> { const AUDIO_ENABLED: bool = true;
let app = App::new(crate_name!())
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!());
let m = app fn main() {
let m = command!()
.arg( .arg(
Arg::with_name("rom") arg!(-b --boot <FILE> "path to boot ROM")
.value_name("ROM_FILE") .required(false)
.takes_value(true) .value_parser(value_parser!(PathBuf)),
.required(true)
.index(1)
.help("Path to the Game ROM"),
) )
.arg( .arg(
Arg::with_name("boot") arg!([ROM_FILE] "path to game ROM")
.short("b") .required(true)
.long("boot") .value_parser(value_parser!(PathBuf)),
.value_name("FILE")
.takes_value(true)
.help("Path to Boot ROM"),
) )
.get_matches(); .get_matches();
// `rom` is a required value in every situation so this will // Set up subscriber
// always exist. if std::env::var("RUST_LOG").is_err() {
let rom_path = m std::env::set_var("RUST_LOG", "gb=info");
.value_of("rom") }
.expect("Required value 'rom' was provided");
let mut game_boy = tracing_subscriber::fmt::fmt()
gb::emu::init(m.value_of("boot"), rom_path).expect("Initialized DMG-01 Emulator"); .with_env_filter(EnvFilter::from_default_env())
let cartridge_title = gb::emu::rom_title(&game_boy); .init();
// Initialize Gamepad Support // Init CPU
let mut gamepad = Gilrs::new().expect("Initialized Gilrs for Controller Input"); let mut cpu = match m.get_one::<PathBuf>("boot") {
Some(path) => {
// Initialize GUI tracing::info!("User-provided boot ROM");
let event_loop = EventLoop::new(); emu::from_boot_rom(path).expect("initialize emulator with custom boot rom")
let mut input = WinitInputHelper::new(); }
let window = create_window(&event_loop, cartridge_title)?; None => {
tracing::info!("Built-in boot ROM");
let mut pixels = { Default::default()
let size = window.inner_size(); }
let surface_texture = SurfaceTexture::new(size.width, size.height, &window);
PixelsBuilder::new(GB_WIDTH as u32, GB_HEIGHT as u32, surface_texture)
.enable_vsync(false)
.build()?
}; };
let spsc: AudioSPSC<f32> = Default::default(); // Load ROM if filepath was provided
let (prod, cons) = spsc.init(); if let Some(path) = m.get_one::<PathBuf>("ROM_FILE") {
tracing::info!("User-provided cartridge ROM");
emu::read_game_rom(&mut cpu, path).expect("read game rom from path");
}
emu::load_save(&mut cpu);
let rom_title = emu::rom_title(&cpu).to_string();
game_boy.apu_mut().set_producer(prod); tracing::info!("Initialize Gamepad");
let mut gamepad = Gilrs::new().expect("Initialize Controller Support");
// Initialize Audio // Initialize Audio
let (_stream, stream_handle) = OutputStream::try_default().expect("Initialized Audio"); let (_stream, stream_handle) = OutputStream::try_default().expect("Initialized Audio");
let sink = Sink::try_new(&stream_handle)?;
sink.append(cons);
std::thread::spawn(move || { if AUDIO_ENABLED {
sink.sleep_until_end(); let (prod, cons) = gb::spsc_init();
}); let sink = {
let s = Sink::try_new(&stream_handle).expect("create sink from audio stream handle");
s.append(cons);
s.set_volume(0.1);
s
};
let mut now = Instant::now(); emu::set_audio_prod(&mut cpu, prod);
let mut cycle_count: Cycle = Default::default();
tracing::info!("Spawn Audio Thread");
std::thread::spawn(move || {
sink.sleep_until_end();
});
}
// Set up state for the Immediate-mode GUI
let event_loop: EventLoop<Event<()>> = EventLoopBuilder::with_user_event().build();
let mut app = Gui::new(rom_title, &event_loop, &cpu);
let mut last_key = gui::unused_key(); // TODO: Fix this awful impl
// used for egui animations
let start_time = Instant::now();
event_loop.run(move |event, _, control_flow| { event_loop.run(move |event, _, control_flow| {
if let Event::RedrawRequested(_) = event { app.handle_event(&event);
if pixels
.render() match event {
.map_err(|e| anyhow!("pixels.render() failed: {}", e)) Event::MainEventsCleared => {
.is_err() app.maybe_quit(&cpu, control_flow);
{
*control_flow = ControlFlow::Exit; match app.mode {
return; EmuMode::Running => emu::run_frame(&mut cpu, &mut gamepad, last_key),
EmuMode::StepFrame if gui::kbd::space_released(&last_key) => {
emu::run_frame(&mut cpu, &mut gamepad, last_key)
}
EmuMode::Step if gui::kbd::space_released(&last_key) => {
emu::run(&mut cpu, &mut gamepad, last_key, 4);
}
_ => {}
};
// Input has been consumed, reset it
last_key = gui::unused_key();
app.request_redraw();
} }
} Event::RedrawRequested(..) => {
app.update_time(start_time.elapsed().as_secs_f64());
if input.update(&event) { app.paint(&cpu);
if input.key_pressed(VirtualKeyCode::Escape) || input.quit() {
*control_flow = ControlFlow::Exit;
return;
}
if let Some(size) = input.window_resized() {
pixels.resize_surface(size.width, size.height);
}
let delta = now.elapsed().subsec_nanos();
now = Instant::now();
let pending = Cycle::new(delta / gb::emu::SM83_CYCLE_TIME.subsec_nanos());
cycle_count += gb::emu::run(&mut game_boy, &mut gamepad, &input, pending);
if cycle_count >= gb::emu::CYCLES_IN_FRAME {
// Draw Frame
cycle_count %= gb::emu::CYCLES_IN_FRAME;
gb::emu::draw(game_boy.ppu(), pixels.get_frame());
window.request_redraw();
} }
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(size) => app.resize(size),
WindowEvent::CloseRequested => emu::save_and_exit(&cpu, control_flow),
WindowEvent::KeyboardInput { input, .. } => last_key = input,
_ => {}
},
_ => {}
} }
}); });
} }
#[cfg(not(windows))]
fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result<Window> {
let size = LogicalSize::new((GB_WIDTH as f64) * SCALE, (GB_HEIGHT as f64) * SCALE);
Ok(WindowBuilder::new()
.with_title(title)
.with_inner_size(size)
.with_min_inner_size(size)
.with_resizable(true)
.with_decorations(true)
.with_transparent(false)
.build(event_loop)?)
}
#[cfg(windows)]
fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result<Window> {
use winit::platform::windows::WindowBuilderExtWindows;
let size = LogicalSize::new((GB_WIDTH as f64) * SCALE, (GB_HEIGHT as f64) * SCALE);
Ok(WindowBuilder::new()
.with_title(title)
.with_inner_size(size)
.with_min_inner_size(size)
.with_resizable(true)
.with_decorations(true)
.with_transparent(false)
.with_drag_and_drop(false)
.build(event_loop)?)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use crate::instruction::cycle::Cycle; use crate::Cycle;
#[derive(Debug, Default)] #[derive(Debug)]
pub(crate) struct DirectMemoryAccess { pub(crate) struct DirectMemoryAccess {
pub(crate) state: DmaState, pub(crate) state: DmaState,
cycle: Cycle, cycle: Cycle,
@@ -8,8 +8,18 @@ pub(crate) struct DirectMemoryAccess {
pub(crate) start: DmaAddress, pub(crate) start: DmaAddress,
} }
impl Default for DirectMemoryAccess {
fn default() -> Self {
Self {
state: DmaState::Disabled,
cycle: Default::default(),
start: Default::default(),
}
}
}
impl DirectMemoryAccess { impl DirectMemoryAccess {
pub(crate) fn clock(&mut self) -> Option<(u16, u16)> { pub(crate) fn tick(&mut self) -> Option<(u16, u16)> {
match self.state { match self.state {
DmaState::Pending => { DmaState::Pending => {
self.cycle += 1; self.cycle += 1;
@@ -56,7 +66,7 @@ impl DirectMemoryAccess {
} }
fn reset(&mut self) { fn reset(&mut self) {
self.cycle = Cycle::new(0); self.cycle = 0;
self.state = DmaState::Disabled; self.state = DmaState::Disabled;
self.start.0 = None; self.start.0 = None;
} }
@@ -69,12 +79,6 @@ pub(crate) enum DmaState {
Transferring, Transferring,
} }
impl Default for DmaState {
fn default() -> Self {
Self::Disabled
}
}
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub(crate) struct DmaAddress(Option<u16>); pub(crate) struct DmaAddress(Option<u16>);

View File

@@ -29,12 +29,6 @@ impl Clone for LCDStatus {
} }
} }
impl Default for LCDStatus {
fn default() -> Self {
Self(0x80) // bit 7 is always 1
}
}
impl From<LCDStatus> for u8 { impl From<LCDStatus> for u8 {
fn from(status: LCDStatus) -> Self { fn from(status: LCDStatus) -> Self {
status.0 status.0
@@ -67,12 +61,6 @@ impl From<PpuMode> for u8 {
} }
} }
impl Default for PpuMode {
fn default() -> Self {
Self::HBlank
}
}
bitfield! { bitfield! {
pub struct LCDControl(u8); pub struct LCDControl(u8);
impl Debug; impl Debug;
@@ -93,12 +81,6 @@ impl Clone for LCDControl {
} }
} }
impl Default for LCDControl {
fn default() -> Self {
Self(0)
}
}
impl From<u8> for LCDControl { impl From<u8> for LCDControl {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
Self(byte) Self(byte)
@@ -142,12 +124,6 @@ impl From<TileMapAddress> for u8 {
} }
} }
impl Default for TileMapAddress {
fn default() -> Self {
Self::X9800
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum TileDataAddress { pub enum TileDataAddress {
X8800 = 0, X8800 = 0,
@@ -170,12 +146,6 @@ impl From<TileDataAddress> for u8 {
} }
} }
impl Default for TileDataAddress {
fn default() -> Self {
Self::X8800
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum ObjectSize { pub enum ObjectSize {
Eight = 0, Eight = 0,
@@ -183,7 +153,7 @@ pub enum ObjectSize {
} }
impl ObjectSize { impl ObjectSize {
pub(crate) fn as_u8(&self) -> u8 { pub(crate) fn size(&self) -> u8 {
use ObjectSize::*; use ObjectSize::*;
match self { match self {
@@ -209,12 +179,6 @@ impl From<ObjectSize> for u8 {
} }
} }
impl Default for ObjectSize {
fn default() -> Self {
Self::Eight
}
}
bitfield! { bitfield! {
pub struct BackgroundPalette(u8); pub struct BackgroundPalette(u8);
impl Debug; impl Debug;
@@ -243,12 +207,6 @@ impl Clone for BackgroundPalette {
} }
} }
impl Default for BackgroundPalette {
fn default() -> Self {
Self(0)
}
}
impl From<u8> for BackgroundPalette { impl From<u8> for BackgroundPalette {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
Self(byte) Self(byte)
@@ -288,12 +246,6 @@ impl Clone for ObjectPalette {
} }
} }
impl Default for ObjectPalette {
fn default() -> Self {
Self(0)
}
}
impl From<u8> for ObjectPalette { impl From<u8> for ObjectPalette {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
Self(byte) Self(byte)
@@ -361,24 +313,12 @@ impl From<ObjectFlags> for u8 {
} }
} }
impl Default for ObjectFlags {
fn default() -> Self {
Self(0)
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum ObjectPaletteKind { pub enum ObjectPaletteKind {
Zero = 0, Zero = 0,
One = 1, One = 1,
} }
impl Default for ObjectPaletteKind {
fn default() -> Self {
Self::Zero
}
}
impl From<u8> for ObjectPaletteKind { impl From<u8> for ObjectPaletteKind {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
match byte & 0b01 { match byte & 0b01 {
@@ -395,7 +335,7 @@ impl From<ObjectPaletteKind> for u8 {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum RenderPriority { pub enum RenderPriority {
Object = 0, Object = 0,
BackgroundAndWindow = 1, BackgroundAndWindow = 1,
@@ -417,12 +357,6 @@ impl From<RenderPriority> for u8 {
} }
} }
impl Default for RenderPriority {
fn default() -> Self {
Self::Object
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum GrayShade { pub enum GrayShade {
White = 0, White = 0,
@@ -442,12 +376,6 @@ impl GrayShade {
} }
} }
impl Default for GrayShade {
fn default() -> Self {
Self::White
}
}
impl From<u8> for GrayShade { impl From<u8> for GrayShade {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
match byte & 0b11 { match byte & 0b11 {

View File

@@ -1,6 +1,6 @@
use bitfield::bitfield; use bitfield::bitfield;
#[derive(Debug, Default)] #[derive(Debug)]
pub(crate) struct Serial { pub(crate) struct Serial {
/// 0xFF01 | SB - Serial Transfer Data /// 0xFF01 | SB - Serial Transfer Data
pub(crate) next: u8, pub(crate) next: u8,
@@ -8,6 +8,15 @@ pub(crate) struct Serial {
pub(crate) ctrl: SerialControl, pub(crate) ctrl: SerialControl,
} }
impl Default for Serial {
fn default() -> Self {
Self {
next: Default::default(),
ctrl: SerialControl(0),
}
}
}
bitfield! { bitfield! {
pub struct SerialControl(u8); pub struct SerialControl(u8);
impl Debug; impl Debug;
@@ -23,12 +32,6 @@ impl Clone for SerialControl {
} }
} }
impl Default for SerialControl {
fn default() -> Self {
Self(0)
}
}
impl From<u8> for SerialControl { impl From<u8> for SerialControl {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
Self(byte) Self(byte)
@@ -47,12 +50,6 @@ enum ShiftClock {
Internal = 1, Internal = 1,
} }
impl Default for ShiftClock {
fn default() -> Self {
Self::External
}
}
impl From<u8> for ShiftClock { impl From<u8> for ShiftClock {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
match byte & 0b01 { match byte & 0b01 {
@@ -69,12 +66,6 @@ enum ClockSpeed {
Fast = 1, Fast = 1,
} }
impl Default for ClockSpeed {
fn default() -> Self {
Self::Normal
}
}
impl From<u8> for ClockSpeed { impl From<u8> for ClockSpeed {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
match byte & 0b01 { match byte & 0b01 {

View File

@@ -5,17 +5,20 @@ pub(crate) struct Timer {
/// 0xFF07 | TAC - Timer Control /// 0xFF07 | TAC - Timer Control
pub(crate) ctrl: TimerControl, pub(crate) ctrl: TimerControl,
/// 0xFF05 | TIMA - Timer Counter /// 0xFF05 | TIMA - Timer Counter
pub(crate) counter: u8, counter: u8,
/// 0xFF06 | TMA - Timer Modulo /// 0xFF06 | TMA - Timer Modulo
pub(crate) modulo: u8, pub(crate) modulo: u8,
/// 0xFF04 | DIV - Divider Register /// 0xFF04 | DIV - Divider Register
pub(crate) divider: u16, pub(crate) divider: u16,
prev_and_result: Option<u8>,
and_result: Option<u8>,
interrupt: bool, interrupt: bool,
state: State,
} }
impl Timer { impl Timer {
pub(crate) fn clock(&mut self) { pub(crate) fn tick(&mut self) {
use State::*;
use TimerSpeed::*; use TimerSpeed::*;
self.divider = self.divider.wrapping_add(1); self.divider = self.divider.wrapping_add(1);
@@ -29,27 +32,45 @@ impl Timer {
}; };
let bit = (self.divider >> bit) as u8 & 0x01; let bit = (self.divider >> bit) as u8 & 0x01;
let timer_enable = self.ctrl.enabled() as u8; let new_result = bit & self.ctrl.enabled() as u8;
let and_result = bit & timer_enable;
if let Some(0x01) = self.prev_and_result { if let Some(0x01) = self.and_result {
if and_result == 0x00 { if new_result == 0x00 {
// Falling Edge, increase TIMA Register // Falling Edge, increase TIMA Register
self.increment_tima(); self.inc_counter();
} }
} }
self.prev_and_result = Some(and_result); self.and_result = Some(new_result);
match self.state {
TIMAOverflow(_) | AbortedTIMAOverflow(_) => self.next(),
LoadTIMA => {
self.counter = self.modulo;
self.interrupt = true;
self.next();
}
Normal => {}
}
} }
fn increment_tima(&mut self) { /// 0xFF05 | TIMA - Timer Counter
let (result, did_overflow) = self.counter.overflowing_add(1); pub(crate) fn tima(&self) -> u8 {
self.counter
}
self.counter = if did_overflow { /// 0xFF05 | TIMA - Timer Counter
self.interrupt = true; pub(crate) fn set_tima(&mut self, byte: u8) {
self.modulo use State::*;
} else {
result match self.state {
Normal | AbortedTIMAOverflow(_) => self.counter = byte,
TIMAOverflow(step) => {
self.counter = byte;
self.state = AbortedTIMAOverflow(step);
}
LoadTIMA => { /* Ignored */ }
} }
} }
@@ -60,17 +81,38 @@ impl Timer {
pub(crate) fn set_interrupt(&mut self, value: bool) { pub(crate) fn set_interrupt(&mut self, value: bool) {
self.interrupt = value; self.interrupt = value;
} }
fn inc_counter(&mut self) {
let (sum, did_overflow) = self.counter.overflowing_add(1);
self.counter = if did_overflow { 0 } else { sum };
if did_overflow {
self.state = State::TIMAOverflow(0);
}
}
fn next(&mut self) {
use State::*;
self.state = match self.state {
Normal | LoadTIMA | AbortedTIMAOverflow(3) => Normal,
TIMAOverflow(3) => LoadTIMA,
AbortedTIMAOverflow(step) => AbortedTIMAOverflow(step + 1),
TIMAOverflow(step) => TIMAOverflow(step + 1),
}
}
} }
impl Default for Timer { impl Default for Timer {
fn default() -> Self { fn default() -> Self {
Self { Self {
ctrl: Default::default(), ctrl: TimerControl(0),
counter: 0, counter: 0,
modulo: 0, modulo: 0,
divider: 0, divider: 0,
interrupt: false, interrupt: false,
prev_and_result: None, and_result: None,
state: State::Normal,
} }
} }
} }
@@ -115,12 +157,6 @@ impl Clone for TimerControl {
} }
} }
impl Default for TimerControl {
fn default() -> Self {
Self(0)
}
}
impl From<u8> for TimerControl { impl From<u8> for TimerControl {
fn from(byte: u8) -> Self { fn from(byte: u8) -> Self {
Self(byte) Self(byte)
@@ -132,3 +168,11 @@ impl From<TimerControl> for u8 {
ctrl.0 ctrl.0
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
TIMAOverflow(u8),
AbortedTIMAOverflow(u8),
Normal,
LoadTIMA,
}

View File

@@ -11,13 +11,13 @@ pub(crate) struct WorkRam {
} }
impl BusIo for WorkRam { impl BusIo for WorkRam {
fn write_byte(&mut self, addr: u16, byte: u8) {
self.bank[addr as usize - WORK_RAM_START_ADDRESS] = byte;
}
fn read_byte(&self, addr: u16) -> u8 { fn read_byte(&self, addr: u16) -> u8 {
self.bank[addr as usize - WORK_RAM_START_ADDRESS] self.bank[addr as usize - WORK_RAM_START_ADDRESS]
} }
fn write_byte(&mut self, addr: u16, byte: u8) {
self.bank[addr as usize - WORK_RAM_START_ADDRESS] = byte;
}
} }
impl Default for WorkRam { impl Default for WorkRam {
@@ -28,48 +28,25 @@ impl Default for WorkRam {
} }
} }
#[derive(Debug, Clone, Copy)]
enum BankNumber {
One = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6,
Seven = 7,
}
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct VariableWorkRam { pub(crate) struct VariableWorkRam {
current: BankNumber, buf: Box<[u8; VARIABLE_WORK_RAM_SIZE]>, // 4K for Variable amount of Banks (Banks 1 -> 7) in Game Boy Colour
bank_n: Box<[[u8; VARIABLE_WORK_RAM_SIZE]; 7]>, // 4K for Variable amount of Banks (Banks 1 -> 7) in Game Boy Colour
} }
impl Default for VariableWorkRam { impl Default for VariableWorkRam {
fn default() -> Self { fn default() -> Self {
Self { Self {
current: BankNumber::One, buf: Box::new([0u8; VARIABLE_WORK_RAM_SIZE]),
bank_n: Box::new([[0u8; VARIABLE_WORK_RAM_SIZE]; 7]),
} }
} }
} }
impl VariableWorkRam {
fn set_current_bank(&mut self, bank: BankNumber) {
self.current = bank;
}
fn get_current_bank(&self) -> &BankNumber {
&self.current
}
}
impl BusIo for VariableWorkRam { impl BusIo for VariableWorkRam {
fn write_byte(&mut self, addr: u16, byte: u8) { fn read_byte(&self, addr: u16) -> u8 {
self.bank_n[self.current as usize][addr as usize - VARIABLE_WORK_RAM_START_ADDRESS] = byte; self.buf[addr as usize - VARIABLE_WORK_RAM_START_ADDRESS]
} }
fn read_byte(&self, addr: u16) -> u8 { fn write_byte(&mut self, addr: u16, byte: u8) {
self.bank_n[self.current as usize][addr as usize - VARIABLE_WORK_RAM_START_ADDRESS] self.buf[addr as usize - VARIABLE_WORK_RAM_START_ADDRESS] = byte;
} }
} }