Compare commits
149 Commits
7112cd15e3
...
main
Author | SHA1 | Date | |
---|---|---|---|
8a352ca807 | |||
068e4124a6 | |||
fedae65cc8 | |||
41586d8cc4 | |||
79740ac245 | |||
d6bfde081e | |||
d2f57ee66b | |||
3841d3a98c | |||
85940c8744 | |||
c7adf54976 | |||
f0f9003247 | |||
5eba097ad4 | |||
756c806c2a | |||
77af5453f0 | |||
41bc4f7afe | |||
36e46d3780 | |||
926c66746d | |||
cf2357f917 | |||
3fb7edd333 | |||
3ab512d663 | |||
61156ca8a8 | |||
2c411d5792 | |||
4c657c9832 | |||
e72b11f946 | |||
bbcbbd8ce3 | |||
c4226e6e76 | |||
409314a4e5 | |||
952bf68bd0 | |||
865c11d53c | |||
90d2da9272 | |||
4a1a21a08f | |||
9d23b571fb | |||
70e0b5868d | |||
a0e3c7c602 | |||
ef4f3d9ec6 | |||
7f872a554b | |||
dda0257655 | |||
c10816c048 | |||
2405fd027f | |||
939c25ce1a | |||
6f5e863645 | |||
ce5d58e1d2 | |||
ebcd5bec5c | |||
f2c49b398c | |||
6f76571d6c | |||
55da5a29d8 | |||
7dfcd44820 | |||
80a15039e0 | |||
32405c0734 | |||
2ef8fefbb7 | |||
548945a4ee | |||
1526b4a09d | |||
4125ea5c74 | |||
b9519d9b7a | |||
01278ca83f | |||
2bb8bd6d8f | |||
080c1e7518 | |||
765f9d8288 | |||
8780c4a59b | |||
e19a540650 | |||
4cfd951ede | |||
de8fe1a367 | |||
c5fa41b20d | |||
552cfd4a18 | |||
4c516804e4 | |||
9b3ab73bb1 | |||
d9f1d661ae | |||
da83032e24 | |||
67e2447d3f | |||
37cf3d92e4 | |||
293e5762c3 | |||
999f661e6b | |||
e573411890 | |||
352a65b705 | |||
64230973f1 | |||
dbbf87af52 | |||
1440cd1fc7 | |||
9964b49ce1 | |||
142231d355 | |||
227928e8ca | |||
1001b0b124 | |||
71ce3f43e0 | |||
ce121864d2 | |||
e1fe00ab64 | |||
5882678bc5 | |||
bcd67cb317 | |||
4516ca8477 | |||
6087e3b20b | |||
10ac579c40 | |||
ee5504111b | |||
a628f64d28 | |||
318a6e0386 | |||
db012c7f4b | |||
e42c87aeb7 | |||
9113e95fa0 | |||
9973dc8714 | |||
e128025208 | |||
44ac0c8ebd | |||
01064bab69 | |||
634bc2d2c0 | |||
d794a94b68 | |||
b87e31d3f4 | |||
3c2456611e | |||
b829f05a34 | |||
afd2d16371 | |||
1f8fa48168 | |||
c6fbb79189 | |||
8b78b2943e | |||
0af95a1dd3 | |||
9fa40f8584 | |||
b10bc7b4fd | |||
4658a0d106 | |||
f92b9d61ef | |||
360a9a7b65 | |||
5d64e539a7 | |||
22f96a10e7 | |||
8fea8eb1ff | |||
9b2c91445a | |||
b9046bceba | |||
aa22e93049 | |||
6215eccb2f | |||
a77d0a0f62 | |||
c2f2e2194b | |||
d68257bb29 | |||
e27d6dc25b | |||
1acb5de19d | |||
1b78b248a3 | |||
5d6df46a2d | |||
7e65d82fef | |||
8c9567b610 | |||
53dfaf0de2 | |||
16c2dd81fc | |||
79be38a1e6 | |||
8625bec059 | |||
0637b771e3 | |||
0107fa04c9 | |||
6265c8af04 | |||
5482a8e75f | |||
002dae6826 | |||
c863dc835c | |||
d4407cf849 | |||
de0d147685 | |||
dd8339e8de | |||
05d6475015 | |||
32b597a328 | |||
33be2e0e83 | |||
832e1b7633 | |||
a549b9feef | |||
4d6fc95130 |
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal 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
2910
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@@ -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
|
||||||
|
47
README.md
47
README.md
@@ -1,14 +1,43 @@
|
|||||||
# Rekai's Gameboy Emulator
|
# Rekai's Gameboy Emulator
|
||||||
[](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
BIN
bin/bootix_dmg.bin
Normal file
Binary file not shown.
732
src/apu.rs
732
src/apu.rs
File diff suppressed because it is too large
Load Diff
@@ -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> {
|
|
||||||
inner: RingBuffer<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Default for AudioSPSC<T> {
|
|
||||||
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();
|
|
||||||
|
|
||||||
|
pub fn init<T>() -> (SampleProducer<T>, SampleConsumer<T>) {
|
||||||
|
let (prod, cons) = RingBuffer::new(BUFFER_CAPACITY);
|
||||||
(
|
(
|
||||||
SampleProducer { inner: prod },
|
SampleProducer { inner: prod },
|
||||||
SampleConsumer { inner: cons },
|
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> {
|
||||||
|
203
src/apu/types.rs
203
src/apu/types.rs
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
141
src/bus.rs
141
src/bus.rs
@@ -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);
|
||||||
|
1007
src/cartridge.rs
1007
src/cartridge.rs
File diff suppressed because it is too large
Load Diff
360
src/cpu.rs
360
src/cpu.rs
@@ -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<()> {
|
|
||||||
self.bus.load_cartridge(path)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rom_title(&self) -> Option<&str> {
|
impl Default for Cpu {
|
||||||
self.bus.rom_title()
|
fn default() -> Self {
|
||||||
|
Cpu::new(*include_bytes!("../bin/bootix_dmg.bin"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
if let Some(kind) = self.halt_kind() {
|
||||||
let cycles = self.execute(instr);
|
use HaltKind::*;
|
||||||
|
|
||||||
self.check_ime();
|
|
||||||
|
|
||||||
cycles
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let pending: u32 = cycles.into();
|
|
||||||
for _ in 0..pending {
|
|
||||||
self.bus.clock();
|
self.bus.clock();
|
||||||
|
return match kind {
|
||||||
|
ImeEnabled | NonePending => 4,
|
||||||
|
SomePending => todo!("Implement HALT bug"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is in the wrong place
|
let opcode = self.fetch();
|
||||||
self.handle_interrupts();
|
let instr = self.decode(opcode);
|
||||||
|
let elapsed = self.execute(instr);
|
||||||
|
self.handle_ei();
|
||||||
|
|
||||||
cycles
|
// 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,87 +141,72 @@ 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)
|
||||||
@@ -248,17 +214,19 @@ impl Cpu {
|
|||||||
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 => Cycle::new(0), // NO Interrupts were enabled and / or requested
|
None => None,
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
_ => 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
193
src/emu.rs
193
src/emu.rs
@@ -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(
|
|
||||||
game_boy: &mut SM83,
|
|
||||||
gamepad: &mut Gilrs,
|
|
||||||
input: &WinitInputHelper,
|
|
||||||
pending: Cycle,
|
|
||||||
) -> Cycle {
|
|
||||||
let mut elapsed = Cycle::new(0);
|
|
||||||
|
|
||||||
while elapsed < pending {
|
|
||||||
elapsed += run_unsynced(game_boy, gamepad, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsed
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_unsynced(game_boy: &mut SM83, gamepad: &mut Gilrs, input: &WinitInputHelper) -> Cycle {
|
|
||||||
if GAMEPAD_ENABLED {
|
|
||||||
if let Some(event) = gamepad.next_event() {
|
if let Some(event) = gamepad.next_event() {
|
||||||
joypad::handle_gamepad_input(game_boy.joypad_mut(), event);
|
crate::joypad::handle_gamepad_input(&mut cpu.bus.joyp, event);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
crate::joypad::handle_keyboard_input(&mut cpu.bus.joyp, key);
|
||||||
|
|
||||||
joypad::handle_keyboard_input(game_boy.joypad_mut(), input);
|
while elapsed < cycles {
|
||||||
game_boy.step()
|
elapsed += cpu.step();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(ppu: &Ppu, frame: &mut [u8]) {
|
pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) {
|
||||||
ppu.copy_to_gui(frame);
|
write_save(cpu);
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_save_from_file(cart: &mut Cartridge) -> Result<PathBuf, SaveError> {
|
||||||
|
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
472
src/gui.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1047
src/instruction.rs
1047
src/instruction.rs
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||||
|
@@ -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::*;
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
211
src/main.rs
211
src/main.rs
@@ -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);
|
|
||||||
|
|
||||||
|
if AUDIO_ENABLED {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
emu::set_audio_prod(&mut cpu, prod);
|
||||||
|
|
||||||
|
tracing::info!("Spawn Audio Thread");
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
sink.sleep_until_end();
|
sink.sleep_until_end();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let mut now = Instant::now();
|
// Set up state for the Immediate-mode GUI
|
||||||
let mut cycle_count: Cycle = Default::default();
|
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);
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
if input.update(&event) {
|
// Input has been consumed, reset it
|
||||||
if input.key_pressed(VirtualKeyCode::Escape) || input.quit() {
|
last_key = gui::unused_key();
|
||||||
*control_flow = ControlFlow::Exit;
|
|
||||||
return;
|
app.request_redraw();
|
||||||
}
|
}
|
||||||
|
Event::RedrawRequested(..) => {
|
||||||
if let Some(size) = input.window_resized() {
|
app.update_time(start_time.elapsed().as_secs_f64());
|
||||||
pixels.resize_surface(size.width, size.height);
|
app.paint(&cpu);
|
||||||
}
|
|
||||||
|
|
||||||
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)?)
|
|
||||||
}
|
|
||||||
|
755
src/ppu.rs
755
src/ppu.rs
File diff suppressed because it is too large
Load Diff
@@ -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>);
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 {
|
||||||
|
94
src/timer.rs
94
src/timer.rs
@@ -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);
|
||||||
}
|
|
||||||
|
|
||||||
fn increment_tima(&mut self) {
|
match self.state {
|
||||||
let (result, did_overflow) = self.counter.overflowing_add(1);
|
TIMAOverflow(_) | AbortedTIMAOverflow(_) => self.next(),
|
||||||
|
LoadTIMA => {
|
||||||
self.counter = if did_overflow {
|
self.counter = self.modulo;
|
||||||
self.interrupt = true;
|
self.interrupt = true;
|
||||||
self.modulo
|
|
||||||
} else {
|
self.next();
|
||||||
result
|
}
|
||||||
|
Normal => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 0xFF05 | TIMA - Timer Counter
|
||||||
|
pub(crate) fn tima(&self) -> u8 {
|
||||||
|
self.counter
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 0xFF05 | TIMA - Timer Counter
|
||||||
|
pub(crate) fn set_tima(&mut self, byte: u8) {
|
||||||
|
use State::*;
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user