diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 399b116dcedec391eec972a8efd4202c2ecb9670..0a11b8d400b86630ace923b54ad29ea545e70387 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,6 +12,7 @@ jobs: rust-version: [ "1.56.1", "1.60.0", + "1.61.0", "1.62.0", "1.63.0", "1.64.0", @@ -45,9 +46,6 @@ jobs: strategy: matrix: rust-version: [ - "1.56.1", - "1.60.0", - "1.62.0", "1.63.0", "1.64.0", "1.65.0", diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 56a8f2aa890e62f916b7e29224942fdc9eda76f1..715499972b48f242eb26c1566e7b6c916544632a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,7 +42,7 @@ build-wasm: stage: build parallel: matrix: - - RUST_VERSION: ["1.60.0"] + - RUST_VERSION: ["1.63.0"] script: - rustup toolchain install $RUST_VERSION - rustup override set $RUST_VERSION diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee5b5a22c1ee39b1263dda8ae75e0d2faf83ecf..3a7ac6225b842c8f5002717f6e0405405e1c01f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Support for WASM engine version printing +* ### Changed @@ -19,6 +19,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * +## [0.7.1] - 2023-03-02 + +### Changed + +* Bumped emukit, fixing a lot of bugs + +## [0.7.0] - 2023-03-01 + +### Added + +* Support for Audio 🔈!!! - [#12](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/12) +* Support for WASM engine version printing + ## [0.6.12] - 2023-02-21 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 65a09a8928d7cd4ff834287642e7e77677890399..ab7d0cc5425e3371bb6c18eec6366e2b82813e63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "boytacean" description = "A Game Boy emulator that is written in Rust." -version = "0.6.12" +version = "0.7.1" authors = ["João Magalhães <joamag@gmail.com>"] license = "Apache-2.0" repository = "https://github.com/joamag/boytacean" diff --git a/README.md b/README.md index dc449caa07a4a76afd5b754904794ae391c47c38..5506448669d561bbf8d6f9487df007f96683e5ef 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A Game Boy emulator that is written in Rust 🦀. * Game Boy (DMG) emulation * Simple navigable source-code * Web and SDL front-ends +* Audio, with a pretty accurate APU * Support for multiple MBCs: MBC1, MBC2, MBC3, and MBC5 * Variable CPU clock speed * Accurate PPU - passes [dmg-acid2](https://github.com/mattcurrie/dmg-acid2) tests @@ -27,7 +28,6 @@ For the Web front-end... What's missing... -* Audio emulation APU * Game Boy Color (GBC) emulation ## Deployments diff --git a/frontends/sdl/Cargo.toml b/frontends/sdl/Cargo.toml index d762d0807012fc37495fef8fbc43c9ee729a7a3b..03177352b9e3ff885fb6eeba816ff0e0608531ff 100644 --- a/frontends/sdl/Cargo.toml +++ b/frontends/sdl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boytacean-sdl" -version = "0.6.12" +version = "0.7.1" authors = ["João Magalhães <joamag@gmail.com>"] description = "An SDL frontend for Boytacen" license = "Apache-2.0" @@ -14,6 +14,10 @@ path = "../.." version = "0.35" features = ["ttf", "image", "gfx", "mixer", "static-link", "use-vcpkg"] +# If the vcpkg version of SDL2 does not work (eg: display not found error) +# then try dynamic linking SDL2 using the following features +# features = ["ttf", "image", "gfx", "mixer", "bundled"] + [package.metadata.vcpkg] dependencies = ["sdl2", "sdl2-image[libjpeg-turbo,tiff,libwebp]", "sdl2-ttf", "sdl2-gfx", "sdl2-mixer"] git = "https://github.com/microsoft/vcpkg" diff --git a/frontends/sdl/README.md b/frontends/sdl/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ac9ab6a5d8636e34a8274ef37750aedebac25738 --- /dev/null +++ b/frontends/sdl/README.md @@ -0,0 +1,18 @@ +# Boytacean SDL + +## Build + +To be able to run the `cargo build` one must first install a local version of `vcpkg` using: + +```bash +cargo install cargo-vcpkg +cargo vcpkg build +cargo build +``` + +Then you can use the following command to build and run Boytacean SDL: + +```bash +cargo build +cargo run +``` diff --git a/frontends/sdl/src/audio.rs b/frontends/sdl/src/audio.rs new file mode 100644 index 0000000000000000000000000000000000000000..c5c113c0b64aa6b1b465445e5570743a9cefcefc --- /dev/null +++ b/frontends/sdl/src/audio.rs @@ -0,0 +1,34 @@ +use sdl2::{ + audio::{AudioQueue, AudioSpecDesired}, + AudioSubsystem, Sdl, +}; + +pub struct Audio { + pub device: AudioQueue<f32>, + pub audio_subsystem: AudioSubsystem, +} + +impl Audio { + pub fn new(sdl: &Sdl) -> Self { + let audio_subsystem = sdl.audio().unwrap(); + + let desired_spec = AudioSpecDesired { + freq: Some(44100), + channels: Some(2), + samples: Some(4096), + }; + + // creates the queue that is going to be used to update the + // audio stream with new values during the main loop + let device = audio_subsystem.open_queue(None, &desired_spec).unwrap(); + + // starts the playback by resuming the audio + // device's activity + device.resume(); + + Self { + device, + audio_subsystem, + } + } +} diff --git a/frontends/sdl/src/util.rs b/frontends/sdl/src/graphics.rs similarity index 79% rename from frontends/sdl/src/util.rs rename to frontends/sdl/src/graphics.rs index df6a550f781e486ccdf4b4b914b1b6501ee131c4..62941ddb9b1af3fa63681236347acfa921b68c50 100644 --- a/frontends/sdl/src/util.rs +++ b/frontends/sdl/src/graphics.rs @@ -1,6 +1,6 @@ use sdl2::{ render::Canvas, rwops::RWops, surface::Surface, sys::image, ttf::Sdl2TtfContext, video::Window, - AudioSubsystem, EventPump, TimerSubsystem, VideoSubsystem, + AudioSubsystem, EventPump, Sdl, TimerSubsystem, VideoSubsystem, }; /// Structure that provides the complete set of Graphics @@ -19,10 +19,17 @@ impl Graphics { /// Start the SDL sub-system and all of its structure and returns /// a structure with all the needed stuff to handle SDL graphics /// and sound. - pub fn new(title: &str, width: u32, height: u32, scale: f32) -> Self { + pub fn new( + sdl: &Sdl, + title: &str, + width: u32, + height: u32, + scale: f32, + accelerated: bool, + vsync: bool, + ) -> Self { // initializes the SDL sub-system, making it ready to be // used for display of graphics and audio - let sdl = sdl2::init().unwrap(); let video_subsystem = sdl.video().unwrap(); let timer_subsystem = sdl.timer().unwrap(); let audio_subsystem = sdl.audio().unwrap(); @@ -42,14 +49,16 @@ impl Graphics { .build() .unwrap(); - // creates an accelerated canvas to be used in the drawing + // creates a canvas (according to spec) to be used in the drawing // then clears it so that is can be presented empty initially - let mut canvas = window - .into_canvas() - .accelerated() - .present_vsync() - .build() - .unwrap(); + let mut canvas_builder = window.into_canvas(); + if accelerated { + canvas_builder = canvas_builder.accelerated(); + } + if vsync { + canvas_builder = canvas_builder.present_vsync(); + } + let mut canvas = canvas_builder.build().unwrap(); canvas.set_logical_size(width, height).unwrap(); canvas.clear(); diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs index da532325b882bd72e864848c4cb2fe573837cd72..471e84f59dfafef77fd46fa5563b238f78a51b49 100644 --- a/frontends/sdl/src/main.rs +++ b/frontends/sdl/src/main.rs @@ -1,18 +1,18 @@ #![allow(clippy::uninlined_format_args)] +pub mod audio; pub mod data; -pub mod util; +pub mod graphics; +use audio::Audio; use boytacean::{ - gb::GameBoy, + gb::{AudioProvider, GameBoy}, pad::PadKey, ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH}, }; -use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum}; +use graphics::{surface_from_bytes, Graphics}; +use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum, Sdl}; use std::{cmp::max, time::SystemTime}; -use util::Graphics; - -use crate::util::surface_from_bytes; /// The scale at which the screen is going to be drawn /// meaning the ratio between Game Boy resolution and @@ -22,6 +22,10 @@ const SCREEN_SCALE: f32 = 2.0; /// The base title to be used in the window. static TITLE: &str = "Boytacean"; +/// Base audio volume to be used as the basis of the +/// amplification level of the volume +static VOLUME: f32 = 64.0; + pub struct Benchmark { count: usize, } @@ -40,29 +44,30 @@ impl Default for Benchmark { pub struct Emulator { system: GameBoy, - graphics: Graphics, + graphics: Option<Graphics>, + audio: Option<Audio>, + title: &'static str, logic_frequency: u32, visual_frequency: f32, next_tick_time: f32, next_tick_time_i: u32, + features: Vec<&'static str>, palettes: [PaletteInfo; 3], palette_index: usize, } impl Emulator { - pub fn new(system: GameBoy, screen_scale: f32) -> Self { + pub fn new(system: GameBoy) -> Self { Self { system, - graphics: Graphics::new( - TITLE, - DISPLAY_WIDTH as u32, - DISPLAY_HEIGHT as u32, - screen_scale, - ), + graphics: None, + audio: None, + title: TITLE, logic_frequency: GameBoy::CPU_FREQ, visual_frequency: GameBoy::VISUAL_FREQ, next_tick_time: 0.0, next_tick_time_i: 0, + features: vec!["video", "audio", "no-vsync"], palettes: [ PaletteInfo::new( "basic", @@ -96,6 +101,32 @@ impl Emulator { } } + pub fn start(&mut self, screen_scale: f32) { + let sdl = sdl2::init().unwrap(); + if self.features.contains(&"video") { + self.start_graphics(&sdl, screen_scale); + } + if self.features.contains(&"audio") { + self.start_audio(&sdl); + } + } + + pub fn start_graphics(&mut self, sdl: &Sdl, screen_scale: f32) { + self.graphics = Some(Graphics::new( + sdl, + self.title, + DISPLAY_WIDTH as u32, + DISPLAY_HEIGHT as u32, + screen_scale, + !self.features.contains(&"no-accelerated"), + !self.features.contains(&"no-vsync"), + )); + } + + pub fn start_audio(&mut self, sdl: &Sdl) { + self.audio = Some(Audio::new(sdl)); + } + pub fn load_rom(&mut self, path: &str) { let rom = self.system.load_rom_file(path); println!( @@ -103,8 +134,10 @@ impl Emulator { rom ); self.graphics + .as_mut() + .unwrap() .window_mut() - .set_title(format!("{} [{}]", TITLE, rom.title()).as_str()) + .set_title(format!("{} [{}]", self.title, rom.title()).as_str()) .unwrap(); } @@ -129,6 +162,11 @@ impl Emulator { ); } + pub fn toggle_audio(&mut self) { + let apu_enabled = self.system.get_apu_enabled(); + self.system.set_apu_enabled(!apu_enabled); + } + pub fn toggle_palette(&mut self) { self.system .ppu() @@ -140,15 +178,19 @@ impl Emulator { // updates the icon of the window to reflect the image // and style of the emulator let surface = surface_from_bytes(&data::ICON); - self.graphics.window_mut().set_icon(&surface); + self.graphics + .as_mut() + .unwrap() + .window_mut() + .set_icon(&surface); // creates an accelerated canvas to be used in the drawing // then clears it and presents it - self.graphics.canvas.present(); + self.graphics.as_mut().unwrap().canvas.present(); // creates a texture creator for the current canvas, required // for the creation of dynamic and static textures - let texture_creator = self.graphics.canvas.texture_creator(); + let texture_creator = self.graphics.as_mut().unwrap().canvas.texture_creator(); // creates the texture streaming that is going to be used // as the target for the pixel buffer @@ -177,7 +219,7 @@ impl Emulator { // obtains an event from the SDL sub-system to be // processed under the current emulation context - while let Some(event) = self.graphics.event_pump.poll_event() { + while let Some(event) = self.graphics.as_mut().unwrap().event_pump.poll_event() { match event { Event::Quit { .. } => break 'main, Event::KeyDown { @@ -188,6 +230,10 @@ impl Emulator { keycode: Some(Keycode::B), .. } => self.benchmark(Benchmark::default()), + Event::KeyDown { + keycode: Some(Keycode::T), + .. + } => self.toggle_audio(), Event::KeyDown { keycode: Some(Keycode::P), .. @@ -225,7 +271,7 @@ impl Emulator { } } - let current_time = self.graphics.timer_subsystem.ticks(); + let current_time = self.graphics.as_mut().unwrap().timer_subsystem.ticks(); if current_time >= self.next_tick_time_i { // re-starts the counter cycles with the number of pending cycles @@ -249,10 +295,13 @@ impl Emulator { break; } - // runs the Game Boy clock, this operations should - // include the advance of both the CPU and the PPU + // runs the Game Boy clock, this operation should + // include the advance of both the CPU, PPU, APU + // and any other frequency based component of the system counter_cycles += self.system.clock() as u32; + // in case a V-Blank state has been reached a new frame is available + // then the frame must be pushed into SDL for display if self.system.ppu_mode() == PpuMode::VBlank && self.system.ppu_frame() != last_frame { @@ -268,6 +317,22 @@ impl Emulator { // is going to be used to detect for new frame presence last_frame = self.system.ppu_frame(); } + + if let Some(audio) = self.audio.as_mut() { + // obtains the new audio buffer and queues it into the audio + // subsystem ready to be processed + let audio_buffer = self + .system + .audio_buffer() + .iter() + .map(|v| *v as f32 / VOLUME) + .collect::<Vec<f32>>(); + audio.device.queue_audio(&audio_buffer).unwrap(); + } + + // clears the audio buffer to prevent it from + // "exploding" in size + self.system.clear_audio_buffer(); } // in case there's at least one new frame that was drawn during @@ -279,15 +344,20 @@ impl Emulator { // clears the graphics canvas, making sure that no garbage // pixel data remaining in the pixel buffer, not doing this would // create visual glitches in OSs like Mac OS X - self.graphics.canvas.clear(); + self.graphics.as_mut().unwrap().canvas.clear(); // copies the texture that was created for the frame (during // the loop part of the tick) to the canvas - self.graphics.canvas.copy(&texture, None, None).unwrap(); + self.graphics + .as_mut() + .unwrap() + .canvas + .copy(&texture, None, None) + .unwrap(); // presents the canvas effectively updating the screen // information presented to the user - self.graphics.canvas.present(); + self.graphics.as_mut().unwrap().canvas.present(); } // calculates the number of ticks that have elapsed since the @@ -309,9 +379,13 @@ impl Emulator { self.next_tick_time_i = self.next_tick_time.ceil() as u32; } - let current_time = self.graphics.timer_subsystem.ticks(); + let current_time = self.graphics.as_mut().unwrap().timer_subsystem.ticks(); let pending_time = self.next_tick_time_i.saturating_sub(current_time); - self.graphics.timer_subsystem.delay(pending_time); + self.graphics + .as_mut() + .unwrap() + .timer_subsystem + .delay(pending_time); } } } @@ -322,9 +396,11 @@ fn main() { let mut game_boy = GameBoy::new(); game_boy.load_cgb(true); - // creates a new generic emulator structure loads the default + // creates a new generic emulator structure then starts + // both the video and audio sub-systems, loads default // ROM file and starts running it - let mut emulator = Emulator::new(game_boy, SCREEN_SCALE); + let mut emulator = Emulator::new(game_boy); + emulator.start(SCREEN_SCALE); emulator.load_rom("../../res/roms/pocket.gb"); emulator.toggle_palette(); emulator.run(); diff --git a/frontends/web/package.json b/frontends/web/package.json index e949cc0eaa70a18b3a037165c9a1979628f23493..45278e03938956f263aa6fffd3a58fcf899e2d96 100644 --- a/frontends/web/package.json +++ b/frontends/web/package.json @@ -1,6 +1,6 @@ { "name": "boytacean-web", - "version": "0.6.12", + "version": "0.7.1", "description": "The web version of Boytacean", "repository": { "type": "git", @@ -21,10 +21,10 @@ "@parcel/transformer-typescript-tsc": "^2.8.3", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "@typescript-eslint/eslint-plugin": "^5.53.0", - "@typescript-eslint/parser": "^5.53.0", - "emukit": "^0.6.5", - "eslint": "^8.34.0", + "@typescript-eslint/eslint-plugin": "^5.54.0", + "@typescript-eslint/parser": "^5.54.0", + "emukit": "^0.7.1", + "eslint": "^8.35.0", "nodemon": "^2.0.20", "parcel": "^2.8.3", "prettier": "^2.8.4", diff --git a/frontends/web/react/components/help/help.tsx b/frontends/web/react/components/help/help.tsx index 42ea60c648b1ba260e1f9f6047485e595cbaae47..c3b0de50c5ffcd7ebe6a0046016847ac9ad1d999 100644 --- a/frontends/web/react/components/help/help.tsx +++ b/frontends/web/react/components/help/help.tsx @@ -67,6 +67,12 @@ export const HelpKeyboard: FC = () => ( </span> Toggle on-screen keyboard </li> + <li> + <span className="key-container"> + <span className="key">Ctrl + P</span> + </span> + Change screen palette + </li> </ul> ); @@ -79,7 +85,15 @@ export const HelpFaqs: FC = () => ( </p> <h3>Why there's no sound?</h3> <p> - It's under development, I'm hopping to have it before end of 2023. + You need to click or touch the screen to start the{" "} + <Link + href="https://developer.mozilla.org/docs/Web/API/Web_Audio_API" + target="_blank" + > + Web Audio API + </Link>{" "} + support. That's just the way browsers work these days, security + first 😄. </p> <h3>Can I use my Xbox One/PS4/PS5 Gamepad?</h3> <p> diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts index f376320f4ff09599c039b7d1075c7fba84d34eb4..dcde0e67798a246778a73e203c21ef4afb77f7a8 100644 --- a/frontends/web/ts/gb.ts +++ b/frontends/web/ts/gb.ts @@ -1,4 +1,5 @@ import { + AudioSpecs, BenchmarkResult, Compilation, Compiler, @@ -105,7 +106,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { private paused = false; private nextTickTime = 0; private fps = 0; - private frameStart: number = new Date().getTime(); + private frameStart: number = EmulatorBase.now(); private frameCount = 0; private paletteIndex = 0; private storeCycles: number = LOGIC_HZ * STORE_RATE; @@ -163,7 +164,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { // obtains the current time, this value is going // to be used to compute the need for tick computation - let currentTime = new Date().getTime(); + let currentTime = EmulatorBase.now(); try { pending = this.tick( @@ -214,7 +215,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { // calculates the amount of time until the next draw operation // this is the amount of time that is going to be pending - currentTime = new Date().getTime(); + currentTime = EmulatorBase.now(); const pendingTime = Math.max(this.nextTickTime - currentTime, 0); // waits a little bit for the next frame to be draw, @@ -226,6 +227,10 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { } tick(currentTime: number, pending: number, cycles = 70224) { + // in case the reference to the system is not set then + // returns the control flow immediately (not possible to tick) + if (!this.gameBoy) return pending; + // in case the time to draw the next frame has not been // reached the flush of the "tick" logic is skipped if (currentTime < this.nextTickTime) return pending; @@ -245,19 +250,19 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { // runs the Game Boy clock, this operations should // include the advance of both the CPU and the PPU - const tickCycles = this.gameBoy?.clock() ?? 0; + const tickCycles = this.gameBoy.clock(); counterCycles += tickCycles; // in case the current PPU mode is VBlank and the // frame is different from the previously rendered // one then it's time to update the canvas if ( - this.gameBoy?.ppu_mode() === PpuMode.VBlank && - this.gameBoy?.ppu_frame() !== lastFrame + this.gameBoy.ppu_mode() === PpuMode.VBlank && + this.gameBoy.ppu_frame() !== lastFrame ) { // updates the reference to the last frame index // to be used for comparison in the next tick - lastFrame = this.gameBoy?.ppu_frame(); + lastFrame = this.gameBoy.ppu_frame(); // triggers the frame event indicating that // a new frame is now available for drawing @@ -276,6 +281,11 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { } } + // triggers the audio event, meaning that the audio should be + // processed for the current emulator, effectively emptying + // the audio buffer that is pending processing + this.trigger("audio"); + // increments the number of frames rendered in the current // section, this value is going to be used to calculate FPS this.frameCount += 1; @@ -284,7 +294,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { // has been reached calculates the number of FPS and // flushes the value to the screen if (this.frameCount >= this.visualFrequency * FPS_SAMPLE_RATE) { - const currentTime = new Date().getTime(); + const currentTime = EmulatorBase.now(); const deltaTime = (currentTime - this.frameStart) / 1000; const fps = Math.round(this.frameCount / deltaTime); this.fps = fps; @@ -500,6 +510,24 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { return this.gameBoy?.frame_buffer_eager() ?? new Uint8Array(); } + get audioSpecs(): AudioSpecs { + return { + samplingRate: 44100, + channels: 2 + }; + } + + get audioBuffer(): Float32Array[] { + const internalBuffer = this.gameBoy?.audio_buffer_eager(true) ?? []; + const leftStream = new Float32Array(internalBuffer.length / 2); + const rightStream = new Float32Array(internalBuffer.length / 2); + for (let index = 0; index < internalBuffer.length; index += 2) { + leftStream[index / 2] = internalBuffer[index] / 100.0; + rightStream[index / 2] = internalBuffer[index + 1] / 100.0; + } + return [leftStream, rightStream]; + } + get romInfo(): RomInfo { return { name: this.romName ?? undefined, @@ -604,7 +632,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { resume() { this.paused = false; - this.nextTickTime = new Date().getTime(); + this.nextTickTime = EmulatorBase.now(); } reset() { @@ -623,6 +651,30 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { this.gameBoy?.key_lift(keyCode); } + pauseVideo() { + this.gameBoy?.set_ppu_enabled(false); + } + + resumeVideo() { + this.gameBoy?.set_ppu_enabled(true); + } + + getVideoState(): boolean { + return this.gameBoy?.get_ppu_enabled() ?? false; + } + + pauseAudio() { + this.gameBoy?.set_apu_enabled(false); + } + + resumeAudio() { + this.gameBoy?.set_apu_enabled(true); + } + + getAudioState(): boolean { + return this.gameBoy?.get_apu_enabled() ?? false; + } + getTile(index: number): Uint8Array { return this.gameBoy?.get_tile_buffer(index) ?? new Uint8Array(); } @@ -638,11 +690,11 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { let cycles = 0; this.pause(); try { - const initial = Date.now(); + const initial = EmulatorBase.now(); for (let i = 0; i < count; i++) { cycles += this.gameBoy?.clock() ?? 0; } - const delta = (Date.now() - initial) / 1000; + const delta = (EmulatorBase.now() - initial) / 1000; const frequency_mhz = cycles / delta / 1000 / 1000; return { delta: delta, diff --git a/src/apu.rs b/src/apu.rs new file mode 100644 index 0000000000000000000000000000000000000000..8fb3fd7bac5978ae00ab692e312cfee0472ef56a --- /dev/null +++ b/src/apu.rs @@ -0,0 +1,564 @@ +use std::collections::VecDeque; + +use crate::warnln; + +const DUTY_TABLE: [[u8; 8]; 4] = [ + [0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 0], +]; + +pub enum Channel { + Ch1, + Ch2, + Ch3, + Ch4, +} + +pub struct Apu { + ch1_timer: u16, + ch1_sequence: u8, + ch1_envelope_sequence: u8, + ch1_envelope_enabled: bool, + ch1_sweep_sequence: u8, + ch1_output: u8, + ch1_sweep_slope: u8, + ch1_sweep_increase: bool, + ch1_sweep_pace: u8, + ch1_length_timer: u8, + ch1_wave_duty: u8, + ch1_pace: u8, + ch1_direction: u8, + ch1_volume: u8, + ch1_wave_length: u16, + ch1_length_stop: bool, + ch1_enabled: bool, + + ch2_timer: u16, + ch2_sequence: u8, + ch2_envelope_sequence: u8, + ch2_envelope_enabled: bool, + ch2_output: u8, + ch2_length_timer: u8, + ch2_wave_duty: u8, + ch2_pace: u8, + ch2_direction: u8, + ch2_volume: u8, + ch2_wave_length: u16, + ch2_length_stop: bool, + ch2_enabled: bool, + + ch3_timer: u16, + ch3_position: u8, + ch3_output: u8, + ch3_dac: bool, + ch3_length_timer: u8, + ch3_output_level: u8, + ch3_wave_length: u16, + ch3_length_stop: bool, + ch3_enabled: bool, + + right_enabled: bool, + left_enabled: bool, + + wave_ram: [u8; 16], + + sampling_rate: u16, + sequencer: u16, + sequencer_step: u8, + output_timer: u16, + audio_buffer: VecDeque<u8>, + audio_buffer_max: usize, +} + +impl Apu { + pub fn new(sampling_rate: u16, buffer_size: f32) -> Self { + Self { + ch1_timer: 0, + ch1_sequence: 0, + ch1_envelope_sequence: 0, + ch1_envelope_enabled: false, + ch1_sweep_sequence: 0, + ch1_output: 0, + ch1_sweep_slope: 0x0, + ch1_sweep_increase: false, + ch1_sweep_pace: 0x0, + ch1_length_timer: 0x0, + ch1_wave_duty: 0x0, + ch1_pace: 0x0, + ch1_direction: 0x0, + ch1_volume: 0x0, + ch1_wave_length: 0x0, + ch1_length_stop: false, + ch1_enabled: false, + + ch2_timer: 0, + ch2_sequence: 0, + ch2_envelope_sequence: 0, + ch2_envelope_enabled: false, + ch2_output: 0, + ch2_length_timer: 0x0, + ch2_wave_duty: 0x0, + ch2_pace: 0x0, + ch2_direction: 0x0, + ch2_volume: 0x0, + ch2_wave_length: 0x0, + ch2_length_stop: false, + ch2_enabled: false, + + ch3_timer: 0, + ch3_position: 0, + ch3_output: 0, + ch3_dac: false, + ch3_length_timer: 0x0, + ch3_output_level: 0x0, + ch3_wave_length: 0x0, + ch3_length_stop: false, + ch3_enabled: false, + + left_enabled: true, + right_enabled: true, + + wave_ram: [0u8; 16], + + sampling_rate, + + /// Internal sequencer counter that runs at 512Hz + /// used for the activation of the tick actions. + sequencer: 0, + sequencer_step: 0, + output_timer: 0, + audio_buffer: VecDeque::with_capacity( + (sampling_rate as f32 * buffer_size as f32 * 2.0) as usize, + ), + audio_buffer_max: (sampling_rate as f32 * buffer_size as f32 * 2.0) as usize, + } + } + + pub fn reset(&mut self) { + self.ch1_timer = 0; + self.ch1_sequence = 0; + self.ch1_envelope_sequence = 0; + self.ch1_envelope_enabled = false; + self.ch1_sweep_sequence = 0; + self.ch1_output = 0; + self.ch1_sweep_slope = 0x0; + self.ch1_sweep_increase = false; + self.ch1_sweep_pace = 0x0; + self.ch1_length_timer = 0x0; + self.ch1_wave_duty = 0x0; + self.ch1_pace = 0x0; + self.ch1_direction = 0x0; + self.ch1_volume = 0x0; + self.ch1_wave_length = 0x0; + self.ch1_length_stop = false; + self.ch1_enabled = false; + + self.ch2_timer = 0; + self.ch2_sequence = 0; + self.ch2_envelope_sequence = 0; + self.ch2_envelope_enabled = false; + self.ch2_output = 0; + self.ch2_length_timer = 0x0; + self.ch2_wave_duty = 0x0; + self.ch2_pace = 0x0; + self.ch2_direction = 0x0; + self.ch2_volume = 0x0; + self.ch2_wave_length = 0x0; + self.ch2_length_stop = false; + self.ch2_enabled = false; + + self.ch3_timer = 0; + self.ch3_position = 0; + self.ch3_output = 0; + self.ch3_dac = false; + self.ch3_length_timer = 0x0; + self.ch3_output_level = 0x0; + self.ch3_wave_length = 0x0; + self.ch3_length_stop = false; + self.ch3_enabled = false; + + self.left_enabled = true; + self.right_enabled = true; + + self.sequencer = 0; + self.sequencer_step = 0; + self.output_timer = 0; + + self.clear_audio_buffer() + } + + pub fn clock(&mut self, cycles: u8) { + // @TODO the performance here requires improvement + for _ in 0..cycles { + self.tick(); + } + } + + pub fn read(&mut self, addr: u16) -> u8 { + { + warnln!("Reading from unknown APU location 0x{:04x}", addr); + 0xff + } + } + + pub fn write(&mut self, addr: u16, value: u8) { + match addr { + // 0xFF10 — NR10: Channel 1 sweep + 0xff10 => { + self.ch1_sweep_slope = value & 0x07; + self.ch1_sweep_increase = value & 0x08 == 0x00; + self.ch1_sweep_pace = (value & 0x70) >> 4; + self.ch1_sweep_sequence = 0; + } + // 0xFF11 — NR11: Channel 1 length timer & duty cycle + 0xff11 => { + self.ch1_length_timer = value & 0x3f; + self.ch1_wave_duty = (value & 0xc0) >> 6; + } + // 0xFF12 — NR12: Channel 1 volume & envelope + 0xff12 => { + self.ch1_pace = value & 0x07; + self.ch1_direction = (value & 0x08) >> 3; + self.ch1_volume = (value & 0xf0) >> 4; + self.ch1_envelope_enabled = self.ch1_pace > 0; + self.ch1_envelope_sequence = 0; + } + // 0xFF13 — NR13: Channel 1 wavelength low + 0xff13 => { + self.ch1_wave_length = (self.ch1_wave_length & 0xff00) | value as u16; + } + // 0xFF14 — NR14: Channel 1 wavelength high & control + 0xff14 => { + self.ch1_wave_length = + (self.ch1_wave_length & 0x00ff) | (((value & 0x07) as u16) << 8); + self.ch1_length_stop |= value & 0x40 == 0x40; + self.ch1_enabled |= value & 0x80 == 0x80; + } + + // 0xFF16 — NR21: Channel 2 length timer & duty cycle + 0xff16 => { + self.ch2_length_timer = value & 0x3f; + self.ch2_wave_duty = (value & 0xc0) >> 6; + } + // 0xFF17 — NR22: Channel 2 volume & envelope + 0xff17 => { + self.ch2_pace = value & 0x07; + self.ch2_direction = (value & 0x08) >> 3; + self.ch2_volume = (value & 0xf0) >> 4; + } + // 0xFF18 — NR23: Channel 2 wavelength low + 0xff18 => { + self.ch2_wave_length = (self.ch2_wave_length & 0xff00) | value as u16; + } + // 0xFF19 — NR24: Channel 2 wavelength high & control + 0xff19 => { + self.ch2_wave_length = + (self.ch2_wave_length & 0x00ff) | (((value & 0x07) as u16) << 8); + self.ch2_length_stop |= value & 0x40 == 0x40; + self.ch2_enabled |= value & 0x80 == 0x80; + } + + // 0xFF1A — NR30: Channel 3 DAC enable + 0xff1a => { + self.ch3_dac = value & 0x80 == 0x80; + } + // 0xFF1B — NR31: Channel 3 length timer + 0xff1b => { + self.ch3_length_timer = value; + } + // 0xFF1C — NR32: Channel 3 output level + 0xff1c => { + self.ch3_output_level = (value & 0x60) >> 5; + } + // 0xFF1D — NR33: Channel 3 wavelength low [write-only] + 0xff1d => { + self.ch3_wave_length = (self.ch3_wave_length & 0xff00) | value as u16; + } + // 0xFF1E — NR34: Channel 3 wavelength high & control + 0xff1e => { + self.ch3_wave_length = + (self.ch3_wave_length & 0x00ff) | (((value & 0x07) as u16) << 8); + self.ch3_length_stop |= value & 0x40 == 0x40; + self.ch3_enabled |= value & 0x80 == 0x80; + } + + // 0xFF30-0xFF3F — Wave pattern RAM + 0xff30..=0xff3f => { + self.wave_ram[addr as usize & 0x000f] = value; + } + + _ => warnln!("Writing in unknown APU location 0x{:04x}", addr), + } + } + + pub fn output(&self) -> u8 { + self.ch1_output + self.ch2_output + self.ch3_output + } + + pub fn audio_buffer(&self) -> &VecDeque<u8> { + &self.audio_buffer + } + + pub fn audio_buffer_mut(&mut self) -> &mut VecDeque<u8> { + &mut self.audio_buffer + } + + pub fn clear_audio_buffer(&mut self) { + self.audio_buffer.clear(); + } + + #[inline(always)] + fn tick(&mut self) { + self.sequencer += 1; + if self.sequencer >= 8192 { + // each of these steps runs at 512/8 Hz = 64Hz, + // meaning a complete loop runs at 512 Hz + match self.sequencer_step { + 0 => { + self.tick_length_all(); + } + 1 => (), + 2 => { + self.tick_ch1_sweep(); + self.tick_length_all(); + } + 3 => (), + 4 => { + self.tick_length_all(); + } + 5 => (), + 6 => { + self.tick_ch1_sweep(); + self.tick_length_all(); + } + 7 => { + self.tick_envelope_all(); + } + _ => (), + } + + self.sequencer = 0; + self.sequencer_step = (self.sequencer_step + 1) & 7; + } + + self.tick_ch_all(); + + self.output_timer = self.output_timer.saturating_sub(1); + if self.output_timer == 0 { + // verifies if we've reached the maximum allowed size for the + // audio buffer and if that's the case an item is removed from + // the buffer (avoiding overflow) and then then the new audio + // volume item is added to the queue + if self.audio_buffer.len() >= self.audio_buffer_max { + self.audio_buffer.pop_front(); + self.audio_buffer.pop_front(); + } + if self.left_enabled { + self.audio_buffer.push_back(self.output()); + } + if self.right_enabled { + self.audio_buffer.push_back(self.output()); + } + + // @TODO the CPU clock is hardcoded here, we must handle situations + // where there's some kind of overclock + self.output_timer = (4194304.0 / self.sampling_rate as f32) as u16; + } + } + + #[inline(always)] + fn tick_length_all(&mut self) { + self.tick_length(Channel::Ch1); + self.tick_length(Channel::Ch2); + self.tick_length(Channel::Ch3); + self.tick_length(Channel::Ch4); + } + + #[inline(always)] + fn tick_length(&mut self, channel: Channel) { + match channel { + Channel::Ch1 => { + if !self.ch1_enabled { + return; + } + self.ch1_length_timer = self.ch1_length_timer.saturating_add(1); + if self.ch1_length_timer >= 64 { + self.ch1_enabled = !self.ch1_length_stop; + self.ch1_length_timer = 0; + } + } + Channel::Ch2 => { + self.ch2_length_timer = self.ch2_length_timer.saturating_add(1); + if self.ch2_length_timer >= 64 { + self.ch2_enabled = !self.ch2_length_stop; + self.ch2_length_timer = 0; + } + } + Channel::Ch3 => { + self.ch3_length_timer = self.ch3_length_timer.saturating_add(1); + if self.ch3_length_timer >= 64 { + self.ch3_enabled = !self.ch3_length_stop; + self.ch3_length_timer = 0; + } + } + Channel::Ch4 => (), + } + } + + #[inline(always)] + fn tick_envelope_all(&mut self) { + self.tick_envelope(Channel::Ch1); + } + + #[inline(always)] + fn tick_envelope(&mut self, channel: Channel) { + match channel { + Channel::Ch1 => { + if !self.ch1_enabled || !self.ch1_envelope_enabled { + return; + } + self.ch1_envelope_sequence += 1; + if self.ch1_envelope_sequence >= self.ch1_pace { + if self.ch1_direction == 0x01 { + self.ch1_volume = self.ch1_volume.saturating_add(1); + } else { + self.ch1_volume = self.ch1_volume.saturating_sub(1); + } + if self.ch1_volume == 0 || self.ch1_volume == 15 { + self.ch1_envelope_enabled = false; + } + self.ch1_envelope_sequence = 0; + } + } + Channel::Ch2 => { + if !self.ch2_enabled || !self.ch2_envelope_enabled { + return; + } + self.ch2_envelope_sequence += 1; + if self.ch2_envelope_sequence >= self.ch2_pace { + if self.ch2_direction == 0x01 { + self.ch2_volume = self.ch2_volume.saturating_add(1); + } else { + self.ch2_volume = self.ch2_volume.saturating_sub(1); + } + if self.ch2_volume == 0 || self.ch2_volume == 15 { + self.ch2_envelope_enabled = false; + } + self.ch2_envelope_sequence = 0; + } + } + Channel::Ch3 => (), + Channel::Ch4 => (), + } + } + + #[inline(always)] + fn tick_ch1_sweep(&mut self) { + if self.ch1_sweep_pace == 0x0 { + return; + } + self.ch1_sweep_sequence += 1; + if self.ch1_sweep_sequence >= self.ch1_sweep_pace { + let divisor = 1u16 << self.ch1_sweep_slope as u16; + let delta = (self.ch1_wave_length as f32 / divisor as f32) as u16; + if self.ch1_sweep_increase { + self.ch1_wave_length = self.ch1_wave_length.saturating_add(delta); + } else { + self.ch1_wave_length = self.ch1_wave_length.saturating_sub(delta); + } + if self.ch1_wave_length > 0x07ff { + self.ch1_enabled = false; + self.ch1_wave_length = 0x07ff; + } + self.ch1_sweep_sequence = 0; + } + } + + #[inline(always)] + fn tick_ch_all(&mut self) { + self.tick_ch1(); + self.tick_ch2(); + self.tick_ch3(); + } + + #[inline(always)] + fn tick_ch1(&mut self) { + self.ch1_timer = self.ch1_timer.saturating_sub(1); + if self.ch1_timer > 0 { + return; + } + + if self.ch1_enabled { + self.ch1_output = + if DUTY_TABLE[self.ch1_wave_duty as usize][self.ch1_sequence as usize] == 1 { + self.ch1_volume + } else { + 0 + }; + } else { + self.ch1_output = 0; + } + + self.ch1_timer = (2048 - self.ch1_wave_length) << 2; + self.ch1_sequence = (self.ch1_sequence + 1) & 7; + } + + #[inline(always)] + fn tick_ch2(&mut self) { + self.ch2_timer = self.ch2_timer.saturating_sub(1); + if self.ch2_timer > 0 { + return; + } + + if self.ch2_enabled { + self.ch2_output = + if DUTY_TABLE[self.ch2_wave_duty as usize][self.ch2_sequence as usize] == 1 { + self.ch2_volume + } else { + 0 + }; + } else { + self.ch2_output = 0; + } + + self.ch2_timer = (2048 - self.ch2_wave_length) << 2; + self.ch2_sequence = (self.ch2_sequence + 1) & 7; + } + + #[inline(always)] + fn tick_ch3(&mut self) { + self.ch3_timer = self.ch3_timer.saturating_sub(1); + if self.ch3_timer > 0 { + return; + } + + if self.ch3_enabled { + let wave_index = self.ch3_position >> 1; + let mut output = self.wave_ram[wave_index as usize]; + output = if (self.ch3_position & 0x01) == 0x01 { + output & 0x0f + } else { + (output & 0xf0) >> 4 + }; + if self.ch3_output_level > 0 { + output >>= self.ch3_output_level - 1; + } else { + output = 0; + } + self.ch3_output = output; + } else { + self.ch3_output = 0; + } + + self.ch3_timer = (2048 - self.ch3_wave_length) << 1; + self.ch3_position = (self.ch3_position + 1) & 31; + } +} + +impl Default for Apu { + fn default() -> Self { + Self::new(44100, 1.0) + } +} diff --git a/src/cpu.rs b/src/cpu.rs index 629691bb69ad76d7eea9300c6a61b69947a618a2..237ef622b82478e4fc6f4433c1eaa051cfd5153c 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,6 +1,7 @@ use core::panic; use crate::{ + apu::Apu, debugln, inst::{EXTENDED, INSTRUCTIONS}, mmu::Mmu, @@ -266,11 +267,26 @@ impl Cpu { &mut self.mmu } + #[inline(always)] + pub fn mmu_i(&self) -> &Mmu { + &self.mmu + } + #[inline(always)] pub fn ppu(&mut self) -> &mut Ppu { self.mmu().ppu() } + #[inline(always)] + pub fn apu(&mut self) -> &mut Apu { + self.mmu().apu() + } + + #[inline(always)] + pub fn apu_i(&self) -> &Apu { + self.mmu_i().apu_i() + } + #[inline(always)] pub fn pad(&mut self) -> &mut Pad { self.mmu().pad() diff --git a/src/gb.rs b/src/gb.rs index 47cf0ea2bf744aadbb57745479b9f72044c7da9a..399c386ebe3d8286af1378a11c71034ba537003e 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -1,4 +1,5 @@ use crate::{ + apu::Apu, cpu::Cpu, data::{BootRom, CGB_BOOT, DMG_BOOT, DMG_BOOTIX, MGB_BOOTIX, SGB_BOOT}, gen::{COMPILATION_DATE, COMPILATION_TIME, COMPILER, COMPILER_VERSION}, @@ -10,6 +11,8 @@ use crate::{ util::read_file, }; +use std::collections::VecDeque; + #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; @@ -31,6 +34,9 @@ use std::{ #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct GameBoy { cpu: Cpu, + ppu_enabled: bool, + apu_enabled: bool, + timer_enabled: bool, } #[cfg_attr(feature = "wasm", wasm_bindgen)] @@ -52,28 +58,48 @@ pub struct Registers { pub lyc: u8, } +pub trait AudioProvider { + fn audio_output(&self) -> u8; + fn audio_buffer(&self) -> &VecDeque<u8>; + fn clear_audio_buffer(&mut self); +} + #[cfg_attr(feature = "wasm", wasm_bindgen)] impl GameBoy { #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))] pub fn new() -> Self { - let ppu = Ppu::new(); - let pad = Pad::new(); - let timer = Timer::new(); - let mmu = Mmu::new(ppu, pad, timer); + let ppu = Ppu::default(); + let apu = Apu::default(); + let pad = Pad::default(); + let timer = Timer::default(); + let mmu = Mmu::new(ppu, apu, pad, timer); let cpu = Cpu::new(mmu); - Self { cpu } + Self { + cpu, + ppu_enabled: true, + apu_enabled: true, + timer_enabled: true, + } } pub fn reset(&mut self) { self.ppu().reset(); + self.apu().reset(); self.mmu().reset(); self.cpu.reset(); } pub fn clock(&mut self) -> u8 { let cycles = self.cpu_clock(); - self.ppu_clock(cycles); - self.timer_clock(cycles); + if self.ppu_enabled { + self.ppu_clock(cycles); + } + if self.apu_enabled { + self.apu_clock(cycles); + } + if self.timer_enabled { + self.timer_clock(cycles); + } cycles } @@ -93,6 +119,10 @@ impl GameBoy { self.ppu().clock(cycles) } + pub fn apu_clock(&mut self, cycles: u8) { + self.apu().clock(cycles) + } + pub fn timer_clock(&mut self, cycles: u8) { self.timer().clock(cycles) } @@ -172,6 +202,14 @@ impl GameBoy { self.frame_buffer().to_vec() } + pub fn audio_buffer_eager(&mut self, clear: bool) -> Vec<u8> { + let buffer = Vec::from(self.audio_buffer().clone()); + if clear { + self.clear_audio_buffer(); + } + buffer + } + pub fn cartridge_eager(&mut self) -> Cartridge { self.mmu().rom().clone() } @@ -238,6 +276,30 @@ impl GameBoy { pub fn get_compilation_time(&self) -> String { String::from(COMPILATION_TIME) } + + pub fn get_ppu_enabled(&self) -> bool { + self.ppu_enabled + } + + pub fn set_ppu_enabled(&mut self, value: bool) { + self.ppu_enabled = value; + } + + pub fn get_apu_enabled(&self) -> bool { + self.apu_enabled + } + + pub fn set_apu_enabled(&mut self, value: bool) { + self.apu_enabled = value; + } + + pub fn get_timer_enabled(&self) -> bool { + self.apu_enabled + } + + pub fn set_timer_enabled(&mut self, value: bool) { + self.timer_enabled = value; + } } /// Gameboy implementations that are meant with performance @@ -267,6 +329,14 @@ impl GameBoy { self.cpu.ppu() } + pub fn apu(&mut self) -> &mut Apu { + self.cpu.apu() + } + + pub fn apu_i(&self) -> &Apu { + self.cpu.apu_i() + } + pub fn pad(&mut self) -> &mut Pad { self.cpu.pad() } @@ -279,6 +349,10 @@ impl GameBoy { &(self.ppu().frame_buffer) } + pub fn audio_buffer(&mut self) -> &VecDeque<u8> { + self.apu().audio_buffer() + } + pub fn load_boot_path(&mut self, path: &str) { let data = read_file(path); self.load_boot(&data); @@ -382,6 +456,20 @@ pub fn hook_impl(info: &PanicInfo) { panic(message.as_str()); } +impl AudioProvider for GameBoy { + fn audio_output(&self) -> u8 { + self.apu_i().output() + } + + fn audio_buffer(&self) -> &VecDeque<u8> { + self.apu_i().audio_buffer() + } + + fn clear_audio_buffer(&mut self) { + self.apu().clear_audio_buffer() + } +} + impl Default for GameBoy { fn default() -> Self { Self::new() diff --git a/src/lib.rs b/src/lib.rs index 0bc2f37c3e2d1185f3ecec18c9d1d306b39da464..7decfab2b80948c67ae0c8c3765191dda99b7743 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ #![allow(clippy::uninlined_format_args)] +pub mod apu; pub mod cpu; pub mod data; pub mod gb; diff --git a/src/mmu.rs b/src/mmu.rs index 08399aad8c6848a4f5a9dae0c6ec00ecaffd762a..17de8cb88a7a1d743c7baf291135337bf781d261 100644 --- a/src/mmu.rs +++ b/src/mmu.rs @@ -1,4 +1,4 @@ -use crate::{debugln, pad::Pad, ppu::Ppu, rom::Cartridge, timer::Timer}; +use crate::{apu::Apu, debugln, pad::Pad, ppu::Ppu, rom::Cartridge, timer::Timer}; pub const BOOT_SIZE_DMG: usize = 256; pub const BOOT_SIZE_CGB: usize = 2304; @@ -16,6 +16,11 @@ pub struct Mmu { /// some of the access operations. ppu: Ppu, + /// Reference to the APU (Audio Processing Unit) that is going + /// to be used both for register reading/writing and to forward + /// some of the access operations. + apu: Apu, + /// Reference to the Gamepad structure that is going to control /// the I/O access to this device. pad: Pad, @@ -53,9 +58,10 @@ pub struct Mmu { } impl Mmu { - pub fn new(ppu: Ppu, pad: Pad, timer: Timer) -> Self { + pub fn new(ppu: Ppu, apu: Apu, pad: Pad, timer: Timer) -> Self { Self { ppu, + apu, pad, timer, rom: Cartridge::new(), @@ -96,6 +102,14 @@ impl Mmu { &mut self.ppu } + pub fn apu(&mut self) -> &mut Apu { + &mut self.apu + } + + pub fn apu_i(&self) -> &Apu { + &self.apu + } + pub fn pad(&mut self) -> &mut Pad { &mut self.pad } @@ -186,6 +200,7 @@ impl Mmu { 0x00 } }, + 0x10..=26 | 0x30..=0x37 => self.apu.read(addr), 0x40 | 0x50 | 0x60 | 0x70 => self.ppu.read(addr), _ => { debugln!("Reading from unknown IO control 0x{:04x}", addr); @@ -266,6 +281,7 @@ impl Mmu { 0x04..=0x07 => self.timer.write(addr, value), _ => debugln!("Writing to unknown IO control 0x{:04x}", addr), }, + 0x10..=26 | 0x30..=0x37 => self.apu.write(addr, value), 0x40 | 0x60 | 0x70 => { match addr & 0x00ff { // 0xFF46 — DMA: OAM DMA source address & start diff --git a/src/timer.rs b/src/timer.rs index e1fb4a1a397180b52fde04e14336b6be66cc60fc..537c14c6714406aa2045ab6a578a8f121f59a119 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -27,6 +27,18 @@ impl Timer { } } + pub fn reset(&mut self) { + self.div = 0; + self.tima = 0; + self.tma = 0; + self.tac = 0x0; + self.div_clock = 0; + self.tima_clock = 0; + self.tima_enabled = false; + self.tima_ratio = 1024; + self.int_tima = false; + } + pub fn clock(&mut self, cycles: u8) { self.div_clock += cycles as u16; while self.div_clock >= 256 {