diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a11b8d400b86630ace923b54ad29ea545e70387..cfbfd15789725e1aa1947fc2e6f09eabecacf177 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,7 @@ jobs: "1.65.0", "1.66.0", "1.67.0", + "1.68.0", "latest" ] runs-on: ubuntu-latest @@ -46,11 +47,11 @@ jobs: strategy: matrix: rust-version: [ - "1.63.0", "1.64.0", "1.65.0", "1.66.0", "1.67.0", + "1.68.0", "latest" ] node-version: ["16"] @@ -79,3 +80,44 @@ jobs: node-version: ${{ matrix.node-version }} - name: Build and lint Web code run: cd frontends/web && npm install && npm run build && npm run lint + build-sdl: + name: Build SDL + timeout-minutes: 30 + strategy: + matrix: + rust-version: [ + "1.61.0", + "1.62.0", + "1.63.0", + "1.64.0", + "1.65.0", + "1.66.0", + "1.67.0", + "1.68.0", + "latest" + ] + runs-on: ubuntu-latest + container: rust:${{ matrix.rust-version }} + steps: + - name: Checkout code from repository + uses: actions/checkout@v3 + - name: Install Dependencies + run: | + apt-get update + apt-get install -y -q zip + - name: Install Rust components + run: | + rustup component add rustfmt + rustup component add clippy + - name: Print Rust information + run: rustc --version + - name: Install SDL dependencies + run: cd frontends/sdl && cargo install cargo-vcpkg && cargo vcpkg build + - name: Verify Rust code format + run: cd frontends/sdl && cargo fmt --all -- --check + - name: Verify Rust code linting + run: cd frontends/sdl && cargo clippy -- -D warnings -A unknown-lints + - name: Build development version + run: cd frontends/sdl && cargo build + - name: Build release version + run: cd frontends/sdl && cargo build --release diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 715499972b48f242eb26c1566e7b6c916544632a..daff0a9883bdb764f0ae44a17460bba4f954639a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,7 +42,7 @@ build-wasm: stage: build parallel: matrix: - - RUST_VERSION: ["1.63.0"] + - RUST_VERSION: ["1.64.0"] script: - rustup toolchain install $RUST_VERSION - rustup override set $RUST_VERSION @@ -117,6 +117,8 @@ deploy-cloudfare-prod: - cd frontends/web/dist - cp -rp ../static/* . - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch prod + - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch production + - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch main dependencies: - build-wasm only: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a7ac6225b842c8f5002717f6e0405405e1c01f4..11565a5c5e815edfaab2ec79f14844e30bb5b18f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * +## [0.8.0] - 2023-04-20 + +### Added + +* Support for serial data transfer - [#19](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/19) +* Support for printing of images using Printer emulation - [#19](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/19) +* Support for display of logger and printer in Web panels +* Converted serial-sections strategy to event driven + +### Fixed + +* `ButtonSwitch` issues by updating the value strategy nad bumping `emukit` +* `AudioGB` with display of canvas with no visibility + +## [0.7.5] - 2023-04-11 + +### Added + +* Support for variable clock speed for APU, means variable audio speed +* Moved debug into the base emulator (from emukit) + +## [0.7.4] - 2023-04-08 + +### Added + +* Support for audio channel 4 (noise) 🔈 +* Better trigger support for audio channels 🔈 + +### Changed + +* Added CH4 public API method for WASM + +### Fixed + +* Envelope support for both channel 2 and 4 🔈 +* Issue related to the wave length stop flag 🔈 + +## [0.7.3] - 2023-04-02 + +### Added + +* Support for CGB flag parsing +* Waveform plotting support + +### Fixed + +* Major JoyPad issue with Action/Select read in register +* Small issue with channel 3 audio and DAC disable + +## [0.7.2] - 2023-03-04 + +### Added + +* Support for stereo sound 🔊 + +### Changed + +* APU `clock()` method with `cycles` parameter, improving performance by an order of magnitude 💪 + +### Fixed + +* Added reset of APU, which fixes annoying "garbage" data in buffer when restarting the state of the emulator + ## [0.7.1] - 2023-03-02 ### Changed diff --git a/Cargo.toml b/Cargo.toml index ab7d0cc5425e3371bb6c18eec6366e2b82813e63..ef4b65745deac225ab54cd0532b8f681eae467d3 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.7.1" +version = "0.8.0" 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 5506448669d561bbf8d6f9487df007f96683e5ef..e0b5316e569c16368e3752f378ab355ee2143cdc 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ A Game Boy emulator that is written in Rust 🦀. * Simple navigable source-code * Web and SDL front-ends * Audio, with a pretty accurate APU +* Serial Data Transfer (Link Cable) support +* Game Boy Printer emulation * 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 diff --git a/doc/inspiration.md b/doc/inspiration.md index 5a9583e989ce17d9208b8c8ef171418d5ace0c90..b097ac1a6a98ae3624712ddae1b57f93dbf7624e 100644 --- a/doc/inspiration.md +++ b/doc/inspiration.md @@ -20,21 +20,29 @@ * [GitHub - LIJI32/SameBoy (C)](https://github.com/LIJI32/SameBoy) * [GitHub - binji/binjgb (C)](https://github.com/binji/binjgb) +* [GitHub - 7thSamurai/Azayaka (C++)](https://github.com/7thSamurai/Azayaka) * [GitHub - feo-boy/feo-boy (Rust)](https://github.com/feo-boy/feo-boy) * [GitHub - Rodrigodd/gameroy (Rust)](https://github.com/Rodrigodd/gameroy) * [GitHub - simias/gb-rs (Rust)](https://github.com/simias/gb-rs) * [GitHub - RubenG123/frosty (Rust)](https://github.com/RubenG123/frosty) * [GitHub - calvinbaart/gameboy (TypeScript)](https://github.com/calvinbaart/gameboy) +* [GitHub - HFO4/gameboy.live (Go)](https://github.com/HFO4/gameboy.live) * [GitHub - djhworld/gomeboycolor-wasm (Go)](https://github.com/djhworld/gomeboycolor-wasm) +* [GitHub - torch2424/wasmboy (WASM)](https://github.com/torch2424/wasmboy) ## Videos * [YouTube - The Ultimate Game Boy Talk (33c3)](https://www.youtube.com/watch?v=HyzD8pNlpwI) +## Peripherals + +* [In Depth: The Game Boy Printer](https://shonumi.github.io/articles/art2.html) + ## Other * [GitHub - gbdev/awesome-gbdev](https://github.com/gbdev/awesome-gbdev) * [GitHub - Hacktix/Bootix](https://github.com/Hacktix/Bootix) +* [GitHub - gbdk-2020/gbdk-2020 (Best GB C SDK)](https://github.com/gbdk-2020/gbdk-2020) * [GitHub - fcambus/jsemu (Emulators written in JavaScript)](https://github.com/fcambus/jsemu) * [Gameboy Doctor: debug and fix your gameboy emulator](https://robertheaton.com/gameboy-doctor) * [Game Boy Boot ROMs](https://gbdev.gg8.se/files/roms/bootroms) diff --git a/frontends/sdl/Cargo.toml b/frontends/sdl/Cargo.toml index 03177352b9e3ff885fb6eeba816ff0e0608531ff..0a07c0b6c22341ef812dfa436a70959ccf3b4463 100644 --- a/frontends/sdl/Cargo.toml +++ b/frontends/sdl/Cargo.toml @@ -1,15 +1,24 @@ [package] name = "boytacean-sdl" -version = "0.7.1" +version = "0.8.0" authors = ["João Magalhães <joamag@gmail.com>"] description = "An SDL frontend for Boytacen" license = "Apache-2.0" keywords = ["gameboy", "emulator", "rust", "sdl"] edition = "2018" +[features] +debug = ["boytacean/debug"] + [dependencies.boytacean] path = "../.." +[dependencies.image] +version = "0.24" + +[dependencies.chrono] +version = "0.4" + [dependencies.sdl2] version = "0.35" features = ["ttf", "image", "gfx", "mixer", "static-link", "use-vcpkg"] diff --git a/frontends/sdl/README.md b/frontends/sdl/README.md index ac9ab6a5d8636e34a8274ef37750aedebac25738..163d1b2e9ff09f07c11d104e7a28e0beea1101c3 100644 --- a/frontends/sdl/README.md +++ b/frontends/sdl/README.md @@ -16,3 +16,16 @@ Then you can use the following command to build and run Boytacean SDL: cargo build cargo run ``` + +To reload the code continuously use the cargo watch tool: + +```bash +cargo install cargo-watch +cargo watch -x run +``` + +There are some feature flags that control the verbosity of the emulator to run in debug mode use: + +```bash +cargo run --features debug +``` diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs index 471e84f59dfafef77fd46fa5563b238f78a51b49..aae9f363b55a7b6a3727e24bb3d6809050bed4dd 100644 --- a/frontends/sdl/src/main.rs +++ b/frontends/sdl/src/main.rs @@ -6,13 +6,16 @@ pub mod graphics; use audio::Audio; use boytacean::{ + devices::printer::PrinterDevice, gb::{AudioProvider, GameBoy}, pad::PadKey, ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH}, }; +use chrono::Utc; use graphics::{surface_from_bytes, Graphics}; +use image::ColorType; use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum, Sdl}; -use std::{cmp::max, time::SystemTime}; +use std::{cmp::max, path::Path, time::SystemTime}; /// The scale at which the screen is going to be drawn /// meaning the ratio between Game Boy resolution and @@ -20,11 +23,11 @@ use std::{cmp::max, time::SystemTime}; const SCREEN_SCALE: f32 = 2.0; /// The base title to be used in the window. -static TITLE: &str = "Boytacean"; +const TITLE: &str = "Boytacean"; /// Base audio volume to be used as the basis of the /// amplification level of the volume -static VOLUME: f32 = 64.0; +const VOLUME: f32 = 64.0; pub struct Benchmark { count: usize, @@ -47,12 +50,13 @@ pub struct Emulator { graphics: Option<Graphics>, audio: Option<Audio>, title: &'static str, + rom_path: String, logic_frequency: u32, visual_frequency: f32, next_tick_time: f32, next_tick_time_i: u32, features: Vec<&'static str>, - palettes: [PaletteInfo; 3], + palettes: [PaletteInfo; 7], palette_index: usize, } @@ -63,6 +67,7 @@ impl Emulator { graphics: None, audio: None, title: TITLE, + rom_path: String::from("invalid"), logic_frequency: GameBoy::CPU_FREQ, visual_frequency: GameBoy::VISUAL_FREQ, next_tick_time: 0.0, @@ -96,6 +101,42 @@ impl Emulator { [0x53, 0x4d, 0x57], ], ), + PaletteInfo::new( + "goldsilver", + [ + [0xc5, 0xc6, 0x6d], + [0x97, 0xa1, 0xb0], + [0x58, 0x5e, 0x67], + [0x23, 0x52, 0x29], + ], + ), + PaletteInfo::new( + "pacman", + [ + [0xff, 0xff, 0x00], + [0xff, 0xb8, 0x97], + [0x37, 0x32, 0xff], + [0x00, 0x00, 0x00], + ], + ), + PaletteInfo::new( + "mariobros", + [ + [0xf7, 0xce, 0xc3], + [0xcc, 0x9e, 0x22], + [0x92, 0x34, 0x04], + [0x00, 0x00, 0x00], + ], + ), + PaletteInfo::new( + "pokemon", + [ + [0xf8, 0x78, 0x00], + [0xb8, 0x60, 0x00], + [0x78, 0x38, 0x00], + [0x00, 0x00, 0x00], + ], + ), ], palette_index: 0, } @@ -127,8 +168,9 @@ impl Emulator { self.audio = Some(Audio::new(sdl)); } - pub fn load_rom(&mut self, path: &str) { - let rom = self.system.load_rom_file(path); + pub fn load_rom(&mut self, path: Option<&str>) { + let path_res = path.unwrap_or(&self.rom_path); + let rom = self.system.load_rom_file(path_res); println!( "========= Cartridge =========\n{}\n=============================", rom @@ -139,6 +181,13 @@ impl Emulator { .window_mut() .set_title(format!("{} [{}]", self.title, rom.title()).as_str()) .unwrap(); + self.rom_path = String::from(path_res); + } + + pub fn reset(&mut self) { + self.system.reset(); + self.system.load_boot_default(); + self.load_rom(None); } pub fn benchmark(&mut self, params: Benchmark) { @@ -163,7 +212,7 @@ impl Emulator { } pub fn toggle_audio(&mut self) { - let apu_enabled = self.system.get_apu_enabled(); + let apu_enabled = self.system.apu_enabled(); self.system.set_apu_enabled(!apu_enabled); } @@ -226,6 +275,10 @@ impl Emulator { keycode: Some(Keycode::Escape), .. } => break 'main, + Event::KeyDown { + keycode: Some(Keycode::R), + .. + } => self.reset(), Event::KeyDown { keycode: Some(Keycode::B), .. @@ -264,8 +317,8 @@ impl Emulator { } Event::DropFile { filename, .. } => { self.system.reset(); - self.system.load_cgb(true); - self.load_rom(&filename); + self.system.load_boot_cgb(); + self.load_rom(Some(&filename)); } _ => (), } @@ -318,9 +371,9 @@ impl Emulator { last_frame = self.system.ppu_frame(); } + // in case the audio subsystem is enabled, then the audio buffer + // must be queued into the SDL audio subsystem 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() @@ -331,7 +384,7 @@ impl Emulator { } // clears the audio buffer to prevent it from - // "exploding" in size + // "exploding" in size, this is required GC operation self.system.clear_audio_buffer(); } @@ -394,14 +447,27 @@ fn main() { // creates a new Game Boy instance and loads both the boot ROM // and the initial game ROM to "start the engine" let mut game_boy = GameBoy::new(); - game_boy.load_cgb(true); + let mut printer = Box::<PrinterDevice>::default(); + printer.set_callback(|image_buffer| { + let file_name = format!("printer-{}.png", Utc::now().format("%Y%m%d-%H%M%S")); + image::save_buffer( + Path::new(&file_name), + image_buffer, + 160, + (image_buffer.len() / 4 / 160) as u32, + ColorType::Rgba8, + ) + .unwrap(); + }); + game_boy.attach_serial(printer); + game_boy.load_boot_cgb(); // 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); emulator.start(SCREEN_SCALE); - emulator.load_rom("../../res/roms/pocket.gb"); + emulator.load_rom(Some("../../res/roms/demo/pocket.gb")); emulator.toggle_palette(); emulator.run(); } diff --git a/frontends/web/.parcelrc b/frontends/web/.parcelrc index 1017d972ae95a264db5e230a4dc04e5cee3fc6ed..4fd2b68587dab082a05be3735294f53a5862b638 100644 --- a/frontends/web/.parcelrc +++ b/frontends/web/.parcelrc @@ -2,6 +2,6 @@ "extends": "@parcel/config-default", "transformers": { "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"], - "*.gb": ["@parcel/transformer-raw"] + "*.{gb,gbc}": ["@parcel/transformer-raw"] } } diff --git a/frontends/web/index.ts b/frontends/web/index.ts index 560d43e9424be72ac4b43973ccf29fbc8a637d00..bf4afbd1c5c7592b58c5b014907d69cabae2a583 100644 --- a/frontends/web/index.ts +++ b/frontends/web/index.ts @@ -50,5 +50,12 @@ const BACKGROUNDS = [ background: background, backgrounds: BACKGROUNDS }); + + // sets the emulator in the global scope this is useful + // to be able to access the emulator from global functions + window.emulator = emulator; + + // starts the emulator with the provided ROM URL, this is + // going to run the main emulator (infinite) loop await emulator.main({ romUrl: romUrl }); })(); diff --git a/frontends/web/package.json b/frontends/web/package.json index 45278e03938956f263aa6fffd3a58fcf899e2d96..4e83628ab7980c0fff801e6e84cc6f6098bfb6f6 100644 --- a/frontends/web/package.json +++ b/frontends/web/package.json @@ -1,6 +1,6 @@ { "name": "boytacean-web", - "version": "0.7.1", + "version": "0.8.0", "description": "The web version of Boytacean", "repository": { "type": "git", @@ -19,18 +19,19 @@ "source": "index.ts", "devDependencies": { "@parcel/transformer-typescript-tsc": "^2.8.3", - "@types/react": "^18.0.28", + "@types/react": "^18.0.37", "@types/react-dom": "^18.0.11", - "@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", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", + "emukit": "^0.8.6", + "eslint": "^8.38.0", + "nodemon": "^2.0.22", "parcel": "^2.8.3", - "prettier": "^2.8.4", + "prettier": "^2.8.7", "process": "^0.11.10", "react": "^18.2.0", "react-dom": "^18.2.0", - "typescript": "^4.9.5" + "typescript": "^5.0.4", + "webgl-plot": "^0.7.0" } } diff --git a/frontends/web/react/components/audio-gb/audio-gb.css b/frontends/web/react/components/audio-gb/audio-gb.css new file mode 100644 index 0000000000000000000000000000000000000000..bcfefa0fcf22d37e0257724c993b14d239e6d4b5 --- /dev/null +++ b/frontends/web/react/components/audio-gb/audio-gb.css @@ -0,0 +1,13 @@ +.audio-gb > .section { + display: inline-block; + vertical-align: top; +} + +.audio-gb > .section > .audio-wave { + display: inline-block; + margin-right: 5px; +} + +.audio-gb > .section > .audio-wave > h4 { + margin: 4px 0px 4px 0px; +} diff --git a/frontends/web/react/components/audio-gb/audio-gb.tsx b/frontends/web/react/components/audio-gb/audio-gb.tsx new file mode 100644 index 0000000000000000000000000000000000000000..03a4854ca2f71d0ac414722f2ae093a9cf49fb99 --- /dev/null +++ b/frontends/web/react/components/audio-gb/audio-gb.tsx @@ -0,0 +1,195 @@ +import React, { FC, useEffect, useRef, useState } from "react"; +import { Canvas, CanvasStructure, PixelFormat } from "emukit"; +import { WebglPlot, WebglLine, ColorRGBA } from "webgl-plot"; + +import "./audio-gb.css"; + +type AudioGBProps = { + getAudioOutput: () => Record<string, number>; + interval?: number; + drawInterval?: number; + color?: number; + range?: number; + rangeVolume?: number; + engine?: "webgl" | "canvas"; + style?: string[]; + renderWave?: (name: string, key: string, styles?: string[]) => JSX.Element; +}; + +export const AudioGB: FC<AudioGBProps> = ({ + getAudioOutput, + interval = 1, + drawInterval = 1000 / 60, + color = 0x50cb93ff, + range = 128, + rangeVolume = 32, + engine = "webgl", + style = [], + renderWave +}) => { + const classes = () => ["audio-gb", ...style].join(" "); + const [audioOutput, setAudioOutput] = useState<Record<string, number[]>>( + {} + ); + const intervalsRef = useRef<number>(); + const intervalsExtraRef = useRef<number>(); + + useEffect(() => { + const updateAudioOutput = () => { + const _audioOutput = getAudioOutput(); + for (const [key, value] of Object.entries(_audioOutput)) { + const values = audioOutput[key] ?? new Array(range).fill(0); + values.push(value); + if (values.length > range) { + values.shift(); + } + audioOutput[key] = values; + } + setAudioOutput(audioOutput); + }; + setInterval(() => updateAudioOutput(), interval); + updateAudioOutput(); + return () => { + if (intervalsRef.current) { + clearInterval(intervalsRef.current); + } + if (intervalsExtraRef.current) { + clearInterval(intervalsExtraRef.current); + } + }; + }, []); + const renderAudioWave = ( + name: string, + key: string, + styles: string[] = [] + ) => { + const classes = ["audio-wave", ...styles].join(" "); + const onCanvas = (structure: CanvasStructure) => { + const drawWave = () => { + const values = audioOutput[key]; + if (!values) { + return; + } + structure.canvasImage.data.fill(0); + values.forEach((value, index) => { + const valueN = Math.min(value, rangeVolume - 1); + const line = rangeVolume - 1 - valueN; + const offset = (line * range + index) * PixelFormat.RGBA; + structure.canvasBuffer.setUint32(offset, color); + }); + structure.canvasOffScreenContext.putImageData( + structure.canvasImage, + 0, + 0 + ); + structure.canvasContext.clearRect(0, 0, range, rangeVolume); + structure.canvasContext.drawImage( + structure.canvasOffScreen, + 0, + 0 + ); + }; + drawWave(); + intervalsExtraRef.current = setInterval( + () => drawWave(), + drawInterval + ); + }; + return ( + <div className={classes}> + <h4>{name}</h4> + <Canvas + width={range} + height={rangeVolume} + onCanvas={onCanvas} + /> + </div> + ); + }; + const renderAudioWaveWgl = ( + name: string, + key: string, + styles: string[] = [] + ) => { + const canvasRef = useRef<HTMLCanvasElement>(null); + const classes = ["audio-wave", ...styles].join(" "); + useEffect(() => { + if (!canvasRef.current) return; + + // converts the canvas to the expected size according + // to the device pixel ratio value + const devicePixelRatio = window.devicePixelRatio || 1; + canvasRef.current.width = range * devicePixelRatio; + canvasRef.current.height = rangeVolume * devicePixelRatio; + + // creates the WGL Plot object with the canvas element + // that is associated with the current audio wave + const wglPlot = new WebglPlot(canvasRef.current); + + const colorRgba = new ColorRGBA(...intToColor2(color)); + const line = new WebglLine(colorRgba, range); + + line.arrangeX(); + wglPlot.addLine(line); + + const drawWave = () => { + const values = audioOutput[key]; + if (!values) { + return; + } + + values.forEach((value, index) => { + const valueN = Math.min(value, rangeVolume - 1); + line.setY(index, valueN / rangeVolume - 1); + }); + + wglPlot.update(); + }; + drawWave(); + intervalsExtraRef.current = setInterval( + () => drawWave(), + drawInterval + ); + }, [canvasRef]); + return ( + <div className={classes}> + <h4>{name}</h4> + <Canvas + canvasRef={canvasRef} + width={range} + height={rangeVolume} + init={false} + /> + </div> + ); + }; + let renderMethod = + engine === "webgl" ? renderAudioWaveWgl : renderAudioWave; + renderMethod = renderWave ?? renderMethod; + return ( + <div className={classes()}> + <div className="section"> + {renderMethod("Master", "master")} + {renderMethod("CH1", "ch1")} + {renderMethod("CH2", "ch2")} + {renderMethod("CH3", "ch3")} + {renderMethod("CH4", "ch4")} + </div> + </div> + ); +}; + +const intToColor = (int: number): [number, number, number, number] => { + const r = (int >> 24) & 0xff; + const g = (int >> 16) & 0xff; + const b = (int >> 8) & 0xff; + const a = int & 0xff; + return [r, g, b, a]; +}; + +const intToColor2 = (int: number): [number, number, number, number] => { + const color = intToColor(int); + return color.map((v) => v / 255) as [number, number, number, number]; +}; + +export default AudioGB; diff --git a/frontends/web/react/components/debug/debug.css b/frontends/web/react/components/debug/debug.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/frontends/web/react/components/debug/debug.tsx b/frontends/web/react/components/debug/debug.tsx new file mode 100644 index 0000000000000000000000000000000000000000..312de1c6ca15de640d83b7f7742c3d06d4227389 --- /dev/null +++ b/frontends/web/react/components/debug/debug.tsx @@ -0,0 +1,65 @@ +import React, { FC } from "react"; +import { AudioGB } from "../audio-gb/audio-gb"; +import { RegistersGB } from "../registers-gb/registers-gb"; +import { TilesGB } from "../tiles-gb/tiles-gb"; +import { GameboyEmulator } from "../../../ts"; + +import "./debug.css"; + +type EmulatorProps = { + emulator: GameboyEmulator; +}; + +export const DebugVideo: FC<EmulatorProps> = ({ emulator }) => { + return ( + <> + {emulator.getTile && ( + <div + style={{ + display: "inline-block", + verticalAlign: "top", + marginRight: 32, + width: 256 + }} + > + <h3>VRAM Tiles</h3> + <TilesGB + getTile={(index) => + emulator.getTile + ? emulator.getTile(index) + : new Uint8Array() + } + tileCount={384} + width={"100%"} + contentBox={false} + /> + </div> + )} + <div + style={{ + display: "inline-block", + verticalAlign: "top" + }} + > + <h3>Registers</h3> + <RegistersGB getRegisters={() => emulator.registers} /> + </div> + </> + ); +}; + +export const DebugAudio: FC<EmulatorProps> = ({ emulator }) => { + return ( + <> + <div + style={{ + display: "inline-block", + verticalAlign: "top" + }} + > + <h3>Audio Waveform</h3> + <AudioGB getAudioOutput={() => emulator.audioOutput} /> + </div> + </> + ); +}; diff --git a/frontends/web/react/components/index.ts b/frontends/web/react/components/index.ts index 5e2bfe9fe05e7cdc3f3e4804b4afec5968397e86..6ec9010a9a7d8099e4c715093e38cbf130dfcf2b 100644 --- a/frontends/web/react/components/index.ts +++ b/frontends/web/react/components/index.ts @@ -1 +1,6 @@ +export * from "./audio-gb/audio-gb"; +export * from "./debug/debug"; export * from "./help/help"; +export * from "./registers-gb/registers-gb"; +export * from "./serial-section/serial-section"; +export * from "./tiles-gb/tiles-gb"; diff --git a/frontends/web/react/components/registers-gb/registers-gb.css b/frontends/web/react/components/registers-gb/registers-gb.css new file mode 100644 index 0000000000000000000000000000000000000000..0801939b049a3955f55675c88db3af75cb271045 --- /dev/null +++ b/frontends/web/react/components/registers-gb/registers-gb.css @@ -0,0 +1,32 @@ +.registers-gb > .section { + display: inline-block; + margin-right: 32px; + vertical-align: top; +} + +.registers-gb > .section:last-child { + margin-right: 0px; +} + +.registers-gb > .section > h4 { + font-size: 22px; + margin: 0px 0px 8px 0px; +} + +.registers-gb > .section > .register { + font-size: 0px; + line-height: 22px; +} + +.registers-gb > .section > .register > .register-key { + display: inline-block; + font-size: 20px; + width: 40px; +} + +.registers-gb > .section > .register > .register-value { + display: inline-block; + font-size: 20px; + text-align: right; + width: 66px; +} diff --git a/frontends/web/react/components/registers-gb/registers-gb.tsx b/frontends/web/react/components/registers-gb/registers-gb.tsx new file mode 100644 index 0000000000000000000000000000000000000000..988ba20ea37839ee3fa392000f66b1f6a532774f --- /dev/null +++ b/frontends/web/react/components/registers-gb/registers-gb.tsx @@ -0,0 +1,79 @@ +import React, { FC, useEffect, useRef, useState } from "react"; + +import "./registers-gb.css"; + +type RegistersGBProps = { + getRegisters: () => Record<string, string | number>; + interval?: number; + style?: string[]; +}; + +export const RegistersGB: FC<RegistersGBProps> = ({ + getRegisters, + interval = 50, + style = [] +}) => { + const classes = () => ["registers-gb", ...style].join(" "); + const [registers, setRegisters] = useState<Record<string, string | number>>( + {} + ); + const intervalsRef = useRef<number>(); + useEffect(() => { + const updateRegisters = () => { + const registers = getRegisters(); + setRegisters(registers); + }; + setInterval(() => updateRegisters(), interval); + updateRegisters(); + return () => { + if (intervalsRef.current) { + clearInterval(intervalsRef.current); + } + }; + }, []); + const renderRegister = ( + key: string, + value?: number, + size = 2, + styles: string[] = [] + ) => { + const classes = ["register", ...styles].join(" "); + const valueS = + value?.toString(16).toUpperCase().padStart(size, "0") ?? value; + return ( + <div className={classes}> + <span className="register-key">{key}</span> + <span className="register-value"> + {valueS ? `0x${valueS}` : "-"} + </span> + </div> + ); + }; + return ( + <div className={classes()}> + <div className="section"> + <h4>CPU</h4> + {renderRegister("PC", registers.pc as number, 4)} + {renderRegister("SP", registers.sp as number, 4)} + {renderRegister("A", registers.a as number)} + {renderRegister("B", registers.b as number)} + {renderRegister("C", registers.c as number)} + {renderRegister("D", registers.d as number)} + {renderRegister("E", registers.e as number)} + {renderRegister("H", registers.h as number)} + {renderRegister("L", registers.l as number)} + </div> + <div className="section"> + <h4>PPU</h4> + {renderRegister("SCY", registers.scy as number)} + {renderRegister("SCX", registers.scx as number)} + {renderRegister("WY", registers.wy as number)} + {renderRegister("WX", registers.wx as number)} + {renderRegister("LY", registers.ly as number)} + {renderRegister("LYC", registers.lyc as number)} + </div> + </div> + ); +}; + +export default RegistersGB; diff --git a/frontends/web/react/components/serial-section/serial-section.css b/frontends/web/react/components/serial-section/serial-section.css new file mode 100644 index 0000000000000000000000000000000000000000..ba1a95ad35c4da654c3324919935439d4c38e1ce --- /dev/null +++ b/frontends/web/react/components/serial-section/serial-section.css @@ -0,0 +1,11 @@ +.serial-section .printer > .printer-lines { + font-size: 0px; +} + +.serial-section .printer > .printer-lines > .printer-line { + display: block; +} + +.serial-section .printer > .printer-lines > .placeholder { + font-size: initial; +} diff --git a/frontends/web/react/components/serial-section/serial-section.tsx b/frontends/web/react/components/serial-section/serial-section.tsx new file mode 100644 index 0000000000000000000000000000000000000000..03d2ffb5416b66cd8eb8758666fa5527f3ca704f --- /dev/null +++ b/frontends/web/react/components/serial-section/serial-section.tsx @@ -0,0 +1,126 @@ +import React, { FC, useEffect, useRef, useState } from "react"; +import { ButtonSwitch, Emulator, Info, Pair, PanelTab } from "emukit"; +import { GameboyEmulator, SerialDevice, bufferToDataUrl } from "../../../ts"; + +import "./serial-section.css"; + +const DEVICE_ICON: { [key: string]: string } = { + null: "🛑", + logger: "📜", + printer: "🖨ï¸" +}; + +type SerialSectionProps = { + emulator: GameboyEmulator; + style?: string[]; +}; + +export const SerialSection: FC<SerialSectionProps> = ({ + emulator, + style = [] +}) => { + const classes = () => ["serial-section", ...style].join(" "); + const [loggerData, setLoggerData] = useState<string>(); + const [printerImageUrls, setPrinterImageUrls] = useState<string[]>(); + const loggerDataRef = useRef<string[]>([]); + const printerDataRef = useRef<string[]>([]); + const loggerRef = useRef<HTMLDivElement>(null); + const imagesRef = useRef<HTMLDivElement>(null); + + useEffect(() => { + const onLoggerData = (data: Uint8Array) => { + const byte = data[0]; + const charByte = String.fromCharCode(byte); + loggerDataRef.current.push(charByte); + setLoggerData(loggerDataRef.current.join("")); + }; + const onPrinterData = (imageBuffer: Uint8Array) => { + const imageUrl = bufferToDataUrl(imageBuffer, 160); + printerDataRef.current.unshift(imageUrl); + setPrinterImageUrls([...printerDataRef.current]); + }; + + const onLogger = (emulator: Emulator, _params: unknown = {}) => { + const params = _params as Record<string, unknown>; + onLoggerData(params.data as Uint8Array); + }; + const onPrinter = (emulator: Emulator, _params: unknown = {}) => { + const params = _params as Record<string, unknown>; + onPrinterData(params.imageBuffer as Uint8Array); + }; + + emulator.bind("logger", onLogger); + emulator.bind("printer", onPrinter); + + return () => { + emulator.unbind("logger", onLogger); + emulator.unbind("printer", onPrinter); + }; + }, []); + + const onEngineChange = (option: string) => { + emulator.loadSerialDevice(option as SerialDevice); + const optionIcon = DEVICE_ICON[option] ?? ""; + emulator.handlers.showToast?.( + `${optionIcon} ${option} attached to the serial port & active` + ); + }; + + const getTabs = () => { + return [ + <Info> + <Pair + key="button-device" + name={"Device"} + valueNode={ + <ButtonSwitch + options={["null", "logger", "printer"]} + value={emulator.serialDevice} + uppercase={true} + size={"large"} + style={["simple"]} + onChange={onEngineChange} + /> + } + /> + <Pair key="baud-rate" name={"Baud Rate"} value={"1 KB/s"} /> + </Info>, + <div className="logger" ref={loggerRef}> + <div className="logger-data"> + {loggerData || "Logger contents are empty."} + </div> + </div>, + <div className="printer" ref={imagesRef}> + <div className="printer-lines"> + {printerImageUrls ? ( + printerImageUrls.map((url, index) => ( + <img + key={index} + className="printer-line" + src={url} + /> + )) + ) : ( + <span className="placeholder"> + Printer contents are empty. + </span> + )} + </div> + </div> + ]; + }; + const getTabNames = () => { + return ["Settings", "Logger", "Printer"]; + }; + return ( + <div className={classes()}> + <PanelTab + tabs={getTabs()} + tabNames={getTabNames()} + selectors={true} + /> + </div> + ); +}; + +export default SerialSection; diff --git a/frontends/web/react/components/tiles-gb/tiles-gb.css b/frontends/web/react/components/tiles-gb/tiles-gb.css new file mode 100644 index 0000000000000000000000000000000000000000..28c5a4f09e98069aab32e108f5aa8a06e41b3ebb --- /dev/null +++ b/frontends/web/react/components/tiles-gb/tiles-gb.css @@ -0,0 +1,14 @@ +.tiles-gb > .canvas { + background-color: #1b1a17; + border: 2px solid #50cb93; + padding: 8px 8px 8px 8px; +} + +.tiles-gb > .canvas.content-box { + box-sizing: content-box; + -o-box-sizing: content-box; + -ms-box-sizing: content-box; + -moz-box-sizing: content-box; + -khtml-box-sizing: content-box; + -webkit-box-sizing: content-box; +} diff --git a/frontends/web/react/components/tiles-gb/tiles-gb.tsx b/frontends/web/react/components/tiles-gb/tiles-gb.tsx new file mode 100644 index 0000000000000000000000000000000000000000..826ee1a4309ff98f0679a1788f01b4be46a3902e --- /dev/null +++ b/frontends/web/react/components/tiles-gb/tiles-gb.tsx @@ -0,0 +1,99 @@ +import React, { FC, useEffect, useRef } from "react"; +import { Canvas, CanvasStructure, PixelFormat } from "emukit"; + +import "./tiles-gb.css"; + +type TilesGBProps = { + getTile: (index: number) => Uint8Array; + tileCount: number; + width?: number | string; + contentBox?: boolean; + interval?: number; + style?: string[]; +}; + +export const TilesGB: FC<TilesGBProps> = ({ + getTile, + tileCount, + width, + contentBox = true, + interval = 1000, + style = [] +}) => { + const classes = () => + ["tiles-gb", contentBox ? "content-box" : "", ...style].join(" "); + const intervalsRef = useRef<number>(); + useEffect(() => { + return () => { + if (intervalsRef.current) { + clearInterval(intervalsRef.current); + } + }; + }, []); + const onCanvas = (structure: CanvasStructure) => { + const drawTiles = () => { + for (let index = 0; index < tileCount; index++) { + const pixels = getTile(index); + drawTile(index, pixels, structure); + } + }; + drawTiles(); + intervalsRef.current = setInterval(() => drawTiles(), interval); + }; + return ( + <div className={classes()}> + <Canvas + width={128} + height={192} + scale={2} + scaledWidth={width} + onCanvas={onCanvas} + /> + </div> + ); +}; + +/** + * Draws the tile at the given index to the proper vertical + * offset in the given context and buffer. + * + * @param index The index of the sprite to be drawn. + * @param pixels Buffer of pixels that contains the RGB data + * that is going to be drawn. + * @param structure The canvas context to which the tile is + * growing to be drawn. + * @param format The pixel format of the sprite. + */ +const drawTile = ( + index: number, + pixels: Uint8Array, + structure: CanvasStructure, + format: PixelFormat = PixelFormat.RGB +) => { + const line = Math.floor(index / 16); + const column = index % 16; + let offset = + (line * structure.canvasOffScreen.width * 8 + column * 8) * + PixelFormat.RGBA; + let counter = 0; + for (let i = 0; i < pixels.length; i += format) { + const color = + (pixels[i] << 24) | + (pixels[i + 1] << 16) | + (pixels[i + 2] << 8) | + (format === PixelFormat.RGBA ? pixels[i + 3] : 0xff); + structure.canvasBuffer.setUint32(offset, color); + + counter++; + if (counter === 8) { + counter = 0; + offset += (structure.canvasOffScreen.width - 7) * PixelFormat.RGBA; + } else { + offset += PixelFormat.RGBA; + } + } + structure.canvasOffScreenContext.putImageData(structure.canvasImage, 0, 0); + structure.canvasContext.drawImage(structure.canvasOffScreen, 0, 0); +}; + +export default TilesGB; diff --git a/frontends/web/res/serial.svg b/frontends/web/res/serial.svg new file mode 100644 index 0000000000000000000000000000000000000000..578d6cb83ce6ee6cb657df805dfbb43e22df1f01 --- /dev/null +++ b/frontends/web/res/serial.svg @@ -0,0 +1 @@ +<svg width="48px" height="48px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-labelledby="swapHorizontalIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="swapHorizontalIconTitle">Swap items (horizontally)</title> <path d="M16 4L19 7L16 10"/> <path d="M4 7L18 7"/> <path d="M7 20L4 17L7 14"/> <path d="M19 17L5 17"/> </svg> \ No newline at end of file diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts index dcde0e67798a246778a73e203c21ef4afb77f7a8..f223f000b5e9b2c4e2a6ad7c7019d68242c8ab90 100644 --- a/frontends/web/ts/gb.ts +++ b/frontends/web/ts/gb.ts @@ -1,8 +1,10 @@ import { AudioSpecs, BenchmarkResult, + SectionInfo, Compilation, Compiler, + DebugPanel, Emulator, EmulatorBase, Entry, @@ -16,7 +18,13 @@ import { } from "emukit"; import { PALETTES, PALETTES_MAP } from "./palettes"; import { base64ToBuffer, bufferToBase64 } from "./util"; -import { HelpFaqs, HelpKeyboard } from "../react"; +import { + DebugAudio, + DebugVideo, + HelpFaqs, + HelpKeyboard, + SerialSection +} from "../react"; import { Cartridge, @@ -79,7 +87,17 @@ const KEYS_NAME: Record<string, number> = { B: PadKey.B }; -const ROM_PATH = require("../../../res/roms/pocket.gb"); +const ROM_PATH = require("../../../res/roms/demo/pocket.gb"); + +/** + * Enumeration with the values for the complete set of available + * serial devices that can be used in the emulator. + */ +export enum SerialDevice { + Null = "null", + Logger = "logger", + Printer = "printer" +} /** * Top level class that controls the emulator behaviour @@ -116,6 +134,8 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { private romSize = 0; private cartridge: Cartridge | null = null; + private _serialDevice: SerialDevice = SerialDevice.Null; + /** * Associative map for extra settings to be used in * opaque local storage operations, associated setting @@ -389,6 +409,10 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { this.gameBoy.load_boot_default(); const cartridge = this.gameBoy.load_rom_ws(romData); + // in case there's a serial device involved tries to load + // it and initialize for the current Game Boy machine + this.loadSerialDevice(); + // updates the name of the currently selected engine // to the one that has been provided (logic change) if (engine) this._engine = engine; @@ -463,6 +487,18 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { ]; } + get sections(): SectionInfo[] { + return [ + { + name: "Serial", + icon: require("../res/serial.svg"), + node: SerialSection({ + emulator: this + }) + } + ]; + } + get help(): HelpPanel[] { return [ { @@ -476,6 +512,19 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { ]; } + get debug(): DebugPanel[] { + return [ + { + name: "Video", + node: DebugVideo({ emulator: this }) + }, + { + name: "Audio", + node: DebugAudio({ emulator: this }) + } + ]; + } + get engines(): string[] { return ["neo"]; } @@ -485,7 +534,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { } get romExts(): string[] { - return ["gb"]; + return ["gb", "gbc"]; } get pixelFormat(): PixelFormat { @@ -548,6 +597,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { set frequency(value: number) { value = Math.max(value, 0); this.logicFrequency = value; + this.gameBoy?.set_clock_freq(value); this.trigger("frequency", value); } @@ -562,22 +612,22 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { get compiler(): Compiler | null { if (!this.gameBoy) return null; return { - name: this.gameBoy.get_compiler(), - version: this.gameBoy.get_compiler_version() + name: this.gameBoy.compiler(), + version: this.gameBoy.compiler_version() }; } get compilation(): Compilation | null { if (!this.gameBoy) return null; return { - date: this.gameBoy.get_compilation_date(), - time: this.gameBoy.get_compilation_time() + date: this.gameBoy.compilation_date(), + time: this.gameBoy.compilation_time() }; } get wasmEngine(): string | null { if (!this.gameBoy) return null; - return this.gameBoy.get_wasm_engine_ws() ?? null; + return this.gameBoy.wasm_engine_ws() ?? null; } get framerate(): number { @@ -606,6 +656,18 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { }; } + get audioOutput(): Record<string, number> { + const output = this.gameBoy?.audio_all_output(); + if (!output) return {}; + return { + master: output[0], + ch1: output[1], + ch2: output[2], + ch3: output[3], + ch4: output[4] + }; + } + get palette(): string | undefined { const paletteObj = PALETTES[this.paletteIndex]; return paletteObj.name; @@ -618,6 +680,14 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { this.updatePalette(); } + get serialDevice(): SerialDevice { + return this._serialDevice; + } + + set serialDevice(value: SerialDevice) { + this._serialDevice = value; + } + toggleRunning() { if (this.paused) { this.resume(); @@ -660,7 +730,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { } getVideoState(): boolean { - return this.gameBoy?.get_ppu_enabled() ?? false; + return this.gameBoy?.ppu_enabled() ?? false; } pauseAudio() { @@ -672,7 +742,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { } getAudioState(): boolean { - return this.gameBoy?.get_apu_enabled() ?? false; + return this.gameBoy?.apu_enabled() ?? false; } getTile(index: number): Uint8Array { @@ -712,6 +782,46 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { this.storeSettings(); } + loadSerialDevice(device?: SerialDevice) { + device = device ?? this.serialDevice; + switch (device) { + case SerialDevice.Null: + this.loadNullDevice(); + break; + + case SerialDevice.Logger: + this.loadLoggerDevice(); + break; + + case SerialDevice.Printer: + this.loadPrinterDevice(); + break; + } + } + + loadNullDevice(set = true) { + this.gameBoy?.load_null_ws(); + if (set) this.serialDevice = SerialDevice.Null; + } + + loadLoggerDevice(set = true) { + this.gameBoy?.load_logger_ws(); + if (set) this.serialDevice = SerialDevice.Logger; + } + + loadPrinterDevice(set = true) { + this.gameBoy?.load_printer_ws(); + if (set) this.serialDevice = SerialDevice.Printer; + } + + onLoggerDevice(data: Uint8Array) { + this.trigger("logger", { data: data }); + } + + onPrinterDevice(imageBuffer: Uint8Array) { + this.trigger("printer", { imageBuffer: imageBuffer }); + } + /** * Tries to load game RAM from the `localStorage` using the * current cartridge title as the name of the item and @@ -780,7 +890,14 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { declare global { interface Window { + emulator: GameboyEmulator; panic: (message: string) => void; + loggerCallback: (data: Uint8Array) => void; + printerCallback: (imageBuffer: Uint8Array) => void; + } + + interface Console { + image(url: string, size?: number): void; } } @@ -788,6 +905,19 @@ window.panic = (message: string) => { console.error(message); }; +window.loggerCallback = (data: Uint8Array) => { + window.emulator.onLoggerDevice(data); +}; + +window.printerCallback = (imageBuffer: Uint8Array) => { + window.emulator.onPrinterDevice(imageBuffer); +}; + +console.image = (url: string, size = 80) => { + const style = `font-size: ${size}px; background-image: url("${url}"); background-size: contain; background-repeat: no-repeat;`; + console.log("%c ", style); +}; + const wasm = async () => { await _wasm(); GameBoy.set_panic_hook_ws(); diff --git a/frontends/web/ts/util.ts b/frontends/web/ts/util.ts index 37a00f53f35e731290ed7797421d93b7b6e12b81..c07d7ea99c1723db265bfab2bb2b0bb6d91e6e3d 100644 --- a/frontends/web/ts/util.ts +++ b/frontends/web/ts/util.ts @@ -15,3 +15,30 @@ export const base64ToBuffer = (base64: string) => { const buffer = new Uint8Array(array); return buffer; }; + +export const bufferToImageData = (buffer: Uint8Array, width: number) => { + const clampedBuffer = new Uint8ClampedArray(buffer.length); + + for (let index = 0; index < clampedBuffer.length; index += 4) { + clampedBuffer[index + 0] = buffer[index]; + clampedBuffer[index + 1] = buffer[index + 1]; + clampedBuffer[index + 2] = buffer[index + 2]; + clampedBuffer[index + 3] = buffer[index + 3]; + } + + return new ImageData(clampedBuffer, width); +}; + +export const bufferToDataUrl = (buffer: Uint8Array, width: number) => { + const imageData = bufferToImageData(buffer, width); + + const canvas = document.createElement("canvas"); + canvas.width = imageData.width; + canvas.height = imageData.height; + + const context = canvas.getContext("2d"); + context?.putImageData(imageData, 0, 0); + + const dataUrl = canvas.toDataURL(); + return dataUrl; +}; diff --git a/res/roms/_headers b/res/roms/_headers deleted file mode 100644 index a292ad706e3c35f1cae544006acd43d1e72c119b..0000000000000000000000000000000000000000 --- a/res/roms/_headers +++ /dev/null @@ -1,4 +0,0 @@ - -/* - X-Robots-Tag: all - Access-Control-Allow-Origin: * diff --git a/res/roms/20y.gb b/res/roms/demo/20y.gb similarity index 100% rename from res/roms/20y.gb rename to res/roms/demo/20y.gb diff --git a/res/roms/gejmboj.gb b/res/roms/demo/gejmboj.gb similarity index 100% rename from res/roms/gejmboj.gb rename to res/roms/demo/gejmboj.gb diff --git a/res/roms/pocket.gb b/res/roms/demo/pocket.gb similarity index 100% rename from res/roms/pocket.gb rename to res/roms/demo/pocket.gb diff --git a/res/roms/opus5.gb b/res/roms/game/opus5.gb similarity index 100% rename from res/roms/opus5.gb rename to res/roms/game/opus5.gb diff --git a/res/roms/shocklobster.gb b/res/roms/game/shocklobster.gb similarity index 100% rename from res/roms/shocklobster.gb rename to res/roms/game/shocklobster.gb diff --git a/res/roms/thebouncingball.gb b/res/roms/game/thebouncingball.gb similarity index 100% rename from res/roms/thebouncingball.gb rename to res/roms/game/thebouncingball.gb diff --git a/res/roms/robots.txt b/res/roms/robots.txt deleted file mode 100644 index a78466b3d66c7386c0ccc2293a67bccf4b6c85ff..0000000000000000000000000000000000000000 --- a/res/roms/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Allow: / diff --git a/res/roms/test/blargg/cgb_sound/cgb_sound.gb b/res/roms/test/blargg/cgb_sound/cgb_sound.gb new file mode 100644 index 0000000000000000000000000000000000000000..dc50471b00315edade129fc438517975a7036465 Binary files /dev/null and b/res/roms/test/blargg/cgb_sound/cgb_sound.gb differ diff --git a/res/roms/paradius/cpu/01-special.gb b/res/roms/test/blargg/cpu/01-special.gb similarity index 100% rename from res/roms/paradius/cpu/01-special.gb rename to res/roms/test/blargg/cpu/01-special.gb diff --git a/res/roms/paradius/cpu/02-interrupts.gb b/res/roms/test/blargg/cpu/02-interrupts.gb similarity index 100% rename from res/roms/paradius/cpu/02-interrupts.gb rename to res/roms/test/blargg/cpu/02-interrupts.gb diff --git a/res/roms/paradius/cpu/03-op sp,hl.gb b/res/roms/test/blargg/cpu/03-op sp,hl.gb similarity index 100% rename from res/roms/paradius/cpu/03-op sp,hl.gb rename to res/roms/test/blargg/cpu/03-op sp,hl.gb diff --git a/res/roms/paradius/cpu/04-op r,imm.gb b/res/roms/test/blargg/cpu/04-op r,imm.gb similarity index 100% rename from res/roms/paradius/cpu/04-op r,imm.gb rename to res/roms/test/blargg/cpu/04-op r,imm.gb diff --git a/res/roms/paradius/cpu/05-op rp.gb b/res/roms/test/blargg/cpu/05-op rp.gb similarity index 100% rename from res/roms/paradius/cpu/05-op rp.gb rename to res/roms/test/blargg/cpu/05-op rp.gb diff --git a/res/roms/paradius/cpu/06-ld r,r.gb b/res/roms/test/blargg/cpu/06-ld r,r.gb similarity index 100% rename from res/roms/paradius/cpu/06-ld r,r.gb rename to res/roms/test/blargg/cpu/06-ld r,r.gb diff --git a/res/roms/paradius/cpu/07-jr,jp,call,ret,rst.gb b/res/roms/test/blargg/cpu/07-jr,jp,call,ret,rst.gb similarity index 100% rename from res/roms/paradius/cpu/07-jr,jp,call,ret,rst.gb rename to res/roms/test/blargg/cpu/07-jr,jp,call,ret,rst.gb diff --git a/res/roms/paradius/cpu/08-misc instrs.gb b/res/roms/test/blargg/cpu/08-misc instrs.gb similarity index 100% rename from res/roms/paradius/cpu/08-misc instrs.gb rename to res/roms/test/blargg/cpu/08-misc instrs.gb diff --git a/res/roms/paradius/cpu/09-op r,r.gb b/res/roms/test/blargg/cpu/09-op r,r.gb similarity index 100% rename from res/roms/paradius/cpu/09-op r,r.gb rename to res/roms/test/blargg/cpu/09-op r,r.gb diff --git a/res/roms/paradius/cpu/10-bit ops.gb b/res/roms/test/blargg/cpu/10-bit ops.gb similarity index 100% rename from res/roms/paradius/cpu/10-bit ops.gb rename to res/roms/test/blargg/cpu/10-bit ops.gb diff --git a/res/roms/paradius/cpu/11-op a,(hl).gb b/res/roms/test/blargg/cpu/11-op a,(hl).gb similarity index 100% rename from res/roms/paradius/cpu/11-op a,(hl).gb rename to res/roms/test/blargg/cpu/11-op a,(hl).gb diff --git a/res/roms/paradius/cpu/cpu_instrs.gb b/res/roms/test/blargg/cpu/cpu_instrs.gb similarity index 100% rename from res/roms/paradius/cpu/cpu_instrs.gb rename to res/roms/test/blargg/cpu/cpu_instrs.gb diff --git a/res/roms/test/blargg/dmg_sound/dmg_sound.gb b/res/roms/test/blargg/dmg_sound/dmg_sound.gb new file mode 100644 index 0000000000000000000000000000000000000000..fe9131044031c45f404642f5dc8a9ec43ecd2065 Binary files /dev/null and b/res/roms/test/blargg/dmg_sound/dmg_sound.gb differ diff --git a/res/roms/test/blargg/halt_bug/halt_bug.gb b/res/roms/test/blargg/halt_bug/halt_bug.gb new file mode 100644 index 0000000000000000000000000000000000000000..38e36625d805e68cb3b8cbf562d6dff500060bdd Binary files /dev/null and b/res/roms/test/blargg/halt_bug/halt_bug.gb differ diff --git a/res/roms/paradius/instr_timing/instr_timing.gb b/res/roms/test/blargg/instr_timing/instr_timing.gb similarity index 100% rename from res/roms/paradius/instr_timing/instr_timing.gb rename to res/roms/test/blargg/instr_timing/instr_timing.gb diff --git a/res/roms/paradius/interrupt_time/interrupt_time.gb b/res/roms/test/blargg/interrupt_time/interrupt_time.gb similarity index 100% rename from res/roms/paradius/interrupt_time/interrupt_time.gb rename to res/roms/test/blargg/interrupt_time/interrupt_time.gb diff --git a/res/roms/test/blargg/mem_timing-2/mem_timing.gb b/res/roms/test/blargg/mem_timing-2/mem_timing.gb new file mode 100644 index 0000000000000000000000000000000000000000..2665aa2d4f2b613b79eceef1a3365d238f919bb8 Binary files /dev/null and b/res/roms/test/blargg/mem_timing-2/mem_timing.gb differ diff --git a/res/roms/paradius/mem_timing/mem_timing.gb b/res/roms/test/blargg/mem_timing/mem_timing.gb similarity index 100% rename from res/roms/paradius/mem_timing/mem_timing.gb rename to res/roms/test/blargg/mem_timing/mem_timing.gb diff --git a/res/roms/paradius/oam_bug/oam_bug.gb b/res/roms/test/blargg/oam_bug/oam_bug.gb similarity index 100% rename from res/roms/paradius/oam_bug/oam_bug.gb rename to res/roms/test/blargg/oam_bug/oam_bug.gb diff --git a/res/roms/dmg_acid2.gb b/res/roms/test/dmg_acid2.gb similarity index 100% rename from res/roms/dmg_acid2.gb rename to res/roms/test/dmg_acid2.gb diff --git a/res/roms/firstwhite.gb b/res/roms/test/firstwhite.gb similarity index 100% rename from res/roms/firstwhite.gb rename to res/roms/test/firstwhite.gb diff --git a/res/roms/test/gbprinter.gb b/res/roms/test/gbprinter.gb new file mode 100644 index 0000000000000000000000000000000000000000..aa8c6d2b565f7808ddf0eec31765436bc2dca61c Binary files /dev/null and b/res/roms/test/gbprinter.gb differ diff --git a/res/roms/test/jayro.gb b/res/roms/test/jayro.gb new file mode 100644 index 0000000000000000000000000000000000000000..23960ad207779c4a2879f22277b844bdf6fc8325 Binary files /dev/null and b/res/roms/test/jayro.gb differ diff --git a/res/roms/rtc3test.gb b/res/roms/test/rtc3test.gb similarity index 100% rename from res/roms/rtc3test.gb rename to res/roms/test/rtc3test.gb diff --git a/res/roms/sprite_priority.gb b/res/roms/test/sprite_priority.gb similarity index 100% rename from res/roms/sprite_priority.gb rename to res/roms/test/sprite_priority.gb diff --git a/res/videos/003-boot.gif b/res/videos/003-boot.gif new file mode 100644 index 0000000000000000000000000000000000000000..0c1fbe45b4de73dd44c2950c52194506aeef026a Binary files /dev/null and b/res/videos/003-boot.gif differ diff --git a/res/videos/004-tetris.gif b/res/videos/004-tetris.gif new file mode 100644 index 0000000000000000000000000000000000000000..befb6a950d9c3898b4e685f45df50d89233b71ce Binary files /dev/null and b/res/videos/004-tetris.gif differ diff --git a/src/apu.rs b/src/apu.rs index deb08753c310f2060c07e8b1b0d3e9c2fa75c67c..07dc230b2ee3fda256092154b4047468e0eff7c6 100644 --- a/src/apu.rs +++ b/src/apu.rs @@ -1,6 +1,6 @@ use std::collections::VecDeque; -use crate::warnln; +use crate::{gb::GameBoy, warnln}; const DUTY_TABLE: [[u8; 8]; 4] = [ [0, 0, 0, 0, 0, 0, 0, 1], @@ -9,6 +9,8 @@ const DUTY_TABLE: [[u8; 8]; 4] = [ [0, 1, 1, 1, 1, 1, 1, 0], ]; +const CH4_DIVISORS: [u8; 8] = [8, 16, 32, 48, 64, 80, 96, 112]; + pub enum Channel { Ch1, Ch2, @@ -17,7 +19,7 @@ pub enum Channel { } pub struct Apu { - ch1_timer: u16, + ch1_timer: i16, ch1_sequence: u8, ch1_envelope_sequence: u8, ch1_envelope_enabled: bool, @@ -35,7 +37,7 @@ pub struct Apu { ch1_length_stop: bool, ch1_enabled: bool, - ch2_timer: u16, + ch2_timer: i16, ch2_sequence: u8, ch2_envelope_sequence: u8, ch2_envelope_enabled: bool, @@ -49,7 +51,7 @@ pub struct Apu { ch2_length_stop: bool, ch2_enabled: bool, - ch3_timer: u16, + ch3_timer: i16, ch3_position: u8, ch3_output: u8, ch3_dac: bool, @@ -59,21 +61,44 @@ pub struct Apu { ch3_length_stop: bool, ch3_enabled: bool, + ch4_timer: i16, + ch4_envelope_sequence: u8, + ch4_envelope_enabled: bool, + ch4_output: u8, + ch4_length_timer: u8, + ch4_pace: u8, + ch4_direction: u8, + ch4_volume: u8, + ch4_divisor: u8, + ch4_width_mode: bool, + ch4_clock_shift: u8, + ch4_lfsr: u16, + ch4_length_stop: bool, + ch4_enabled: bool, + + glob_panning: u8, + right_enabled: bool, left_enabled: bool, + ch1_out_enabled: bool, + ch2_out_enabled: bool, + ch3_out_enabled: bool, + ch4_out_enabled: bool, wave_ram: [u8; 16], sampling_rate: u16, sequencer: u16, sequencer_step: u8, - output_timer: u16, + output_timer: i16, audio_buffer: VecDeque<u8>, audio_buffer_max: usize, + + clock_freq: u32, } impl Apu { - pub fn new(sampling_rate: u16, buffer_size: f32) -> Self { + pub fn new(sampling_rate: u16, buffer_size: f32, clock_freq: u32) -> Self { Self { ch1_timer: 0, ch1_sequence: 0, @@ -117,11 +142,39 @@ impl Apu { ch3_length_stop: false, ch3_enabled: false, + ch4_timer: 0, + ch4_envelope_sequence: 0, + ch4_envelope_enabled: false, + ch4_output: 0, + ch4_length_timer: 0x0, + ch4_pace: 0x0, + ch4_direction: 0x0, + ch4_volume: 0x0, + ch4_divisor: 0x0, + ch4_width_mode: false, + ch4_clock_shift: 0x0, + ch4_lfsr: 0x0, + ch4_length_stop: false, + ch4_enabled: false, + + glob_panning: 0x0, + left_enabled: true, right_enabled: true, + ch1_out_enabled: true, + ch2_out_enabled: true, + ch3_out_enabled: true, + ch4_out_enabled: true, + + /// The RAM that is used to sore the wave information + /// to be used in channel 3 audio wave_ram: [0u8; 16], + /// The rate at which audio samples are going to be + /// taken, ideally this value should be aligned with + /// the sampling rate of the output device. A typical + /// sampling rate would be of 44.1kHz. sampling_rate, /// Internal sequencer counter that runs at 512Hz @@ -133,6 +186,7 @@ impl Apu { (sampling_rate as f32 * buffer_size) as usize * 2, ), audio_buffer_max: (sampling_rate as f32 * buffer_size) as usize * 2, + clock_freq, } } @@ -179,6 +233,23 @@ impl Apu { self.ch3_length_stop = false; self.ch3_enabled = false; + self.ch4_timer = 0; + self.ch4_envelope_sequence = 0; + self.ch4_envelope_enabled = false; + self.ch4_output = 0; + self.ch4_length_timer = 0x0; + self.ch4_pace = 0x0; + self.ch4_direction = 0x0; + self.ch4_volume = 0x0; + self.ch4_divisor = 0x0; + self.ch4_width_mode = false; + self.ch4_clock_shift = 0x0; + self.ch4_lfsr = 0x0; + self.ch4_length_stop = false; + self.ch4_enabled = false; + + self.glob_panning = 0x0; + self.left_enabled = true; self.right_enabled = true; @@ -190,16 +261,73 @@ impl Apu { } pub fn clock(&mut self, cycles: u8) { - // @TODO the performance here requires improvement - for _ in 0..cycles { - self.tick(); + self.sequencer += cycles as u16; + 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 -= 8192; + self.sequencer_step = (self.sequencer_step + 1) & 7; + } + + self.tick_ch_all(cycles); + + self.output_timer = self.output_timer.saturating_sub(cycles as i16); + 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()); + } + + // calculates the rate at which a new audio sample should be + // created based on the (base/CPU) clock frequency and the + // sampling rate, this is basically the amount of APU clock + // calls that should be performed until an audio sample is created + self.output_timer += (self.clock_freq as f32 / self.sampling_rate as f32) as i16; } } pub fn read(&mut self, addr: u16) -> u8 { - { - warnln!("Reading from unknown APU location 0x{:04x}", addr); - 0xff + match addr { + // 0xFF25 — NR51: Sound panning + 0xff25 => self.glob_panning, + _ => { + warnln!("Reading from unknown APU location 0x{:04x}", addr); + 0xff + } } } @@ -231,10 +359,18 @@ impl Apu { } // 0xFF14 — NR14: Channel 1 wavelength high & control 0xff14 => { + let length_trigger = value & 0x40 == 0x40; + let trigger = value & 0x80 == 0x80; self.ch1_wave_length = (self.ch1_wave_length & 0x00ff) | (((value & 0x07) as u16) << 8); - self.ch1_length_stop |= value & 0x40 == 0x40; + self.ch1_length_stop = value & 0x40 == 0x40; self.ch1_enabled |= value & 0x80 == 0x80; + if trigger { + self.trigger_ch1(); + } + if (length_trigger || trigger) && self.ch1_length_timer == 0 { + self.ch1_length_timer = 0; + } } // 0xFF16 — NR21: Channel 2 length timer & duty cycle @@ -254,10 +390,18 @@ impl Apu { } // 0xFF19 — NR24: Channel 2 wavelength high & control 0xff19 => { + let length_trigger = value & 0x40 == 0x40; + let trigger = value & 0x80 == 0x80; 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; + self.ch2_length_stop = length_trigger; + self.ch2_enabled |= trigger; + if trigger { + self.trigger_ch2(); + } + if (length_trigger || trigger) && self.ch2_length_timer == 0 { + self.ch2_length_timer = 0; + } } // 0xFF1A — NR30: Channel 3 DAC enable @@ -278,10 +422,63 @@ impl Apu { } // 0xFF1E — NR34: Channel 3 wavelength high & control 0xff1e => { + let length_trigger = value & 0x40 == 0x40; + let trigger = value & 0x80 == 0x80; 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; + self.ch3_length_stop = length_trigger; + self.ch3_enabled |= trigger; + if trigger { + self.trigger_ch3(); + } + if (length_trigger || trigger) && self.ch3_length_timer == 0 { + self.ch3_length_timer = 0; + } + } + + // 0xFF20 — NR41: Channel 4 length timer + 0xff20 => { + self.ch4_length_timer = value & 0x3f; + } + // 0xFF21 — NR42: Channel 4 volume & envelope + 0xff21 => { + self.ch4_pace = value & 0x07; + self.ch4_direction = (value & 0x08) >> 3; + self.ch4_volume = (value & 0xf0) >> 4; + self.ch4_envelope_enabled = self.ch4_pace > 0; + self.ch4_envelope_sequence = 0; + } + // 0xFF22 — NR43: Channel 4 frequency & randomness + 0xff22 => { + self.ch4_divisor = value & 0x07; + self.ch4_width_mode = value & 0x08 == 0x08; + self.ch4_clock_shift = (value & 0xf0) >> 4; + } + // 0xFF23 — NR44: Channel 4 control + 0xff23 => { + let length_trigger = value & 0x40 == 0x40; + let trigger = value & 0x80 == 0x80; + self.ch4_length_stop = length_trigger; + self.ch4_enabled |= trigger; + if trigger { + self.trigger_ch4(); + } + if (length_trigger || trigger) && self.ch4_length_timer == 0 { + self.ch4_length_timer = 0; + } + } + + // 0xFF24 — NR50: Master volume & VIN panning + 0xff24 => { + //@TODO: Implement master volume & VIN panning + } + // 0xFF25 — NR51: Sound panning + 0xff25 => { + self.glob_panning = value; + } + // 0xFF26 — NR52: Sound on/off + 0xff26 => { + //@TODO: Implement sound on/off } // 0xFF30-0xFF3F — Wave pattern RAM @@ -293,8 +490,61 @@ impl Apu { } } + #[inline(always)] pub fn output(&self) -> u8 { - self.ch1_output + self.ch2_output + self.ch3_output + self.ch1_output() + self.ch2_output() + self.ch3_output() + self.ch4_output() + } + + #[inline(always)] + pub fn ch1_output(&self) -> u8 { + if self.ch1_out_enabled { + self.ch1_output + } else { + 0 + } + } + + #[inline(always)] + pub fn ch2_output(&self) -> u8 { + if self.ch2_out_enabled { + self.ch2_output + } else { + 0 + } + } + + #[inline(always)] + pub fn ch3_output(&self) -> u8 { + if self.ch3_out_enabled { + self.ch3_output + } else { + 0 + } + } + + #[inline(always)] + pub fn ch4_output(&self) -> u8 { + if self.ch4_out_enabled { + self.ch4_output + } else { + 0 + } + } + + pub fn set_ch1_enabled(&mut self, enabled: bool) { + self.ch1_out_enabled = enabled; + } + + pub fn set_ch2_enabled(&mut self, enabled: bool) { + self.ch2_out_enabled = enabled; + } + + pub fn set_ch3_enabled(&mut self, enabled: bool) { + self.ch3_out_enabled = enabled; + } + + pub fn set_ch4_enabled(&mut self, enabled: bool) { + self.ch4_out_enabled = enabled; } pub fn audio_buffer(&self) -> &VecDeque<u8> { @@ -309,63 +559,12 @@ impl Apu { 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()); - } + pub fn clock_freq(&self) -> u32 { + self.clock_freq + } - // @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; - } + pub fn set_clock_freq(&mut self, value: u32) { + self.clock_freq = value; } #[inline(always)] @@ -403,13 +602,21 @@ impl Apu { self.ch3_length_timer = 0; } } - Channel::Ch4 => (), + Channel::Ch4 => { + self.ch4_length_timer = self.ch4_length_timer.saturating_add(1); + if self.ch4_length_timer >= 64 { + self.ch4_enabled = !self.ch4_length_stop; + self.ch4_length_timer = 0; + } + } } } #[inline(always)] fn tick_envelope_all(&mut self) { self.tick_envelope(Channel::Ch1); + self.tick_envelope(Channel::Ch2); + self.tick_envelope(Channel::Ch4); } #[inline(always)] @@ -450,7 +657,23 @@ impl Apu { } } Channel::Ch3 => (), - Channel::Ch4 => (), + Channel::Ch4 => { + if !self.ch4_enabled || !self.ch4_envelope_enabled { + return; + } + self.ch4_envelope_sequence += 1; + if self.ch4_envelope_sequence >= self.ch4_pace { + if self.ch4_direction == 0x01 { + self.ch4_volume = self.ch4_volume.saturating_add(1); + } else { + self.ch4_volume = self.ch4_volume.saturating_sub(1); + } + if self.ch4_volume == 0 || self.ch4_volume == 15 { + self.ch4_envelope_enabled = false; + } + self.ch4_envelope_sequence = 0; + } + } } } @@ -477,15 +700,16 @@ impl Apu { } #[inline(always)] - fn tick_ch_all(&mut self) { - self.tick_ch1(); - self.tick_ch2(); - self.tick_ch3(); + fn tick_ch_all(&mut self, cycles: u8) { + self.tick_ch1(cycles); + self.tick_ch2(cycles); + self.tick_ch3(cycles); + self.tick_ch4(cycles); } #[inline(always)] - fn tick_ch1(&mut self) { - self.ch1_timer = self.ch1_timer.saturating_sub(1); + fn tick_ch1(&mut self, cycles: u8) { + self.ch1_timer = self.ch1_timer.saturating_sub(cycles as i16); if self.ch1_timer > 0 { return; } @@ -501,13 +725,13 @@ impl Apu { self.ch1_output = 0; } - self.ch1_timer = (2048 - self.ch1_wave_length) << 2; + self.ch1_timer += ((2048 - self.ch1_wave_length) << 2) as i16; self.ch1_sequence = (self.ch1_sequence + 1) & 7; } #[inline(always)] - fn tick_ch2(&mut self) { - self.ch2_timer = self.ch2_timer.saturating_sub(1); + fn tick_ch2(&mut self, cycles: u8) { + self.ch2_timer = self.ch2_timer.saturating_sub(cycles as i16); if self.ch2_timer > 0 { return; } @@ -523,18 +747,18 @@ impl Apu { self.ch2_output = 0; } - self.ch2_timer = (2048 - self.ch2_wave_length) << 2; + self.ch2_timer += ((2048 - self.ch2_wave_length) << 2) as i16; self.ch2_sequence = (self.ch2_sequence + 1) & 7; } #[inline(always)] - fn tick_ch3(&mut self) { - self.ch3_timer = self.ch3_timer.saturating_sub(1); + fn tick_ch3(&mut self, cycles: u8) { + self.ch3_timer = self.ch3_timer.saturating_sub(cycles as i16); if self.ch3_timer > 0 { return; } - if self.ch3_enabled { + if self.ch3_enabled && self.ch3_dac { let wave_index = self.ch3_position >> 1; let mut output = self.wave_ram[wave_index as usize]; output = if (self.ch3_position & 0x01) == 0x01 { @@ -552,13 +776,73 @@ impl Apu { self.ch3_output = 0; } - self.ch3_timer = (2048 - self.ch3_wave_length) << 1; + self.ch3_timer += ((2048 - self.ch3_wave_length) << 1) as i16; self.ch3_position = (self.ch3_position + 1) & 31; } + + #[inline(always)] + fn tick_ch4(&mut self, cycles: u8) { + self.ch4_timer = self.ch4_timer.saturating_sub(cycles as i16); + if self.ch4_timer > 0 { + return; + } + + if self.ch4_enabled { + // obtains the current value of the LFSR based as + // the XOR of the 1st and 2nd bit of the LFSR + let result = ((self.ch4_lfsr & 0x0001) ^ ((self.ch4_lfsr >> 1) & 0x0001)) == 0x0001; + + // shifts the LFSR to the right and in case the + // value is positive sets the 15th bit to 1 + self.ch4_lfsr >>= 1; + self.ch4_lfsr |= if result { 0x0001 << 14 } else { 0x0 }; + + // in case the short width mode (7 bits) is set then + // the 6th bit will be set to value of the 15th bit + if self.ch4_width_mode { + self.ch4_lfsr &= 0xbf; + self.ch4_lfsr |= if result { 0x40 } else { 0x00 }; + } + + self.ch4_output = if result { self.ch4_volume } else { 0 }; + } else { + self.ch4_output = 0; + } + + self.ch4_timer += + ((CH4_DIVISORS[self.ch4_divisor as usize] as u16) << self.ch4_clock_shift) as i16; + } + + #[inline(always)] + fn trigger_ch1(&mut self) { + self.ch1_timer = ((2048 - self.ch1_wave_length) << 2) as i16; + self.ch1_envelope_sequence = 0; + self.ch1_sweep_sequence = 0; + } + + #[inline(always)] + fn trigger_ch2(&mut self) { + self.ch2_timer = ((2048 - self.ch2_wave_length) << 2) as i16; + self.ch2_envelope_sequence = 0; + } + + #[inline(always)] + fn trigger_ch3(&mut self) { + self.ch3_timer = 3; + self.ch3_position = 0; + } + + #[inline(always)] + fn trigger_ch4(&mut self) { + self.ch4_timer = + ((CH4_DIVISORS[self.ch4_divisor as usize] as u16) << self.ch4_clock_shift) as i16; + self.ch4_lfsr = 0x7ff1; + self.ch4_envelope_sequence = 0; + } } impl Default for Apu { fn default() -> Self { - Self::new(44100, 1.0) + Self::new(44100, 1.0, GameBoy::CPU_FREQ) } } diff --git a/src/cpu.rs b/src/cpu.rs index 237ef622b82478e4fc6f4433c1eaa051cfd5153c..ba115c2e54d6ffa5077b0db04d48539d976701d7 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -7,6 +7,7 @@ use crate::{ mmu::Mmu, pad::Pad, ppu::Ppu, + serial::Serial, timer::Timer, }; @@ -127,7 +128,7 @@ impl Cpu { self.halted = false; } - if self.ime { + if self.ime && self.mmu.ie != 0x00 { // @TODO aggregate all of this interrupts in the MMU, as there's // a lot of redundant code involved in here which complicates the // readability and maybe performance of this code @@ -191,6 +192,26 @@ impl Cpu { return 24; } // @TODO aggregate the handling of these interrupts + else if (self.mmu.ie & 0x08 == 0x08) && self.mmu.serial().int_serial() { + debugln!("Going to run Serial interrupt handler (0x58)"); + + self.disable_int(); + self.push_word(pc); + self.pc = 0x58; + + // acknowledges that the serial interrupt has been + // properly handled + self.mmu.serial().ack_serial(); + + // in case the CPU is currently halted waiting + // for an interrupt, releases it + if self.halted { + self.halted = false; + } + + return 24; + } + // @TODO aggregate the handling of these interrupts else if (self.mmu.ie & 0x10 == 0x10) && self.mmu.pad().int_pad() { debugln!("Going to run JoyPad interrupt handler (0x60)"); @@ -297,6 +318,11 @@ impl Cpu { self.mmu().timer() } + #[inline(always)] + pub fn serial(&mut self) -> &mut Serial { + self.mmu().serial() + } + #[inline(always)] pub fn halted(&self) -> bool { self.halted @@ -427,7 +453,7 @@ impl Cpu { } #[inline(always)] - pub fn get_zero(&self) -> bool { + pub fn zero(&self) -> bool { self.zero } @@ -437,7 +463,7 @@ impl Cpu { } #[inline(always)] - pub fn get_sub(&self) -> bool { + pub fn sub(&self) -> bool { self.sub } @@ -447,7 +473,7 @@ impl Cpu { } #[inline(always)] - pub fn get_half_carry(&self) -> bool { + pub fn half_carry(&self) -> bool { self.half_carry } @@ -457,7 +483,7 @@ impl Cpu { } #[inline(always)] - pub fn get_carry(&self) -> bool { + pub fn carry(&self) -> bool { self.carry } diff --git a/src/devices/mod.rs b/src/devices/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..ada19f9138884470af7746f6fa424080b2601f8a --- /dev/null +++ b/src/devices/mod.rs @@ -0,0 +1,2 @@ +pub mod printer; +pub mod stdout; diff --git a/src/devices/printer.rs b/src/devices/printer.rs new file mode 100644 index 0000000000000000000000000000000000000000..23c46b4e0c8b2d03d85826217f634289934d03f1 --- /dev/null +++ b/src/devices/printer.rs @@ -0,0 +1,321 @@ +use std::fmt::{self, Display, Formatter}; + +use crate::{ppu::PaletteAlpha, serial::SerialDevice, warnln}; + +const PRINTER_PALETTE: PaletteAlpha = [ + [0xff, 0xff, 0xff, 0xff], + [0xaa, 0xaa, 0xaa, 0xff], + [0x55, 0x55, 0x55, 0xff], + [0x00, 0x00, 0x00, 0xff], +]; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum PrinterState { + MagicBytes1 = 0x00, + MagicBytes2 = 0x01, + Identification = 0x02, + Compression = 0x03, + LengthLow = 0x04, + LengthHigh = 0x05, + Data = 0x06, + ChecksumLow = 0x07, + ChecksumHigh = 0x08, + KeepAlive = 0x09, + Status = 0x0a, + Other = 0xff, +} + +impl PrinterState { + pub fn description(&self) -> &'static str { + match self { + PrinterState::MagicBytes1 => "Magic Bytes 1", + PrinterState::MagicBytes2 => "Magic Bytes 2", + PrinterState::Identification => "Identification", + PrinterState::Compression => "Compression", + PrinterState::LengthLow => "Length Low", + PrinterState::LengthHigh => "Length High", + PrinterState::Data => "Data", + PrinterState::ChecksumLow => "Checksum Low", + PrinterState::ChecksumHigh => "Checksum High", + PrinterState::KeepAlive => "Keep Alive", + PrinterState::Status => "Status", + PrinterState::Other => "Other", + } + } + + fn from_u8(value: u8) -> Self { + match value { + 0x00 => PrinterState::MagicBytes1, + 0x01 => PrinterState::MagicBytes2, + 0x02 => PrinterState::Identification, + 0x03 => PrinterState::Compression, + 0x04 => PrinterState::LengthLow, + 0x05 => PrinterState::LengthHigh, + 0x06 => PrinterState::Data, + 0x07 => PrinterState::ChecksumLow, + 0x08 => PrinterState::ChecksumHigh, + 0x09 => PrinterState::KeepAlive, + 0x0a => PrinterState::Status, + _ => PrinterState::Other, + } + } +} + +impl Display for PrinterState { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.description()) + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum PrinterCommand { + Init = 0x01, + Print = 0x02, + Data = 0x04, + Status = 0x0f, + Other = 0xff, +} + +impl PrinterCommand { + pub fn description(&self) -> &'static str { + match self { + PrinterCommand::Init => "Init", + PrinterCommand::Print => "Print", + PrinterCommand::Data => "Data", + PrinterCommand::Status => "Status", + PrinterCommand::Other => "Other", + } + } + + fn from_u8(value: u8) -> Self { + match value { + 0x01 => PrinterCommand::Init, + 0x02 => PrinterCommand::Print, + 0x04 => PrinterCommand::Data, + 0x0f => PrinterCommand::Status, + _ => PrinterCommand::Other, + } + } +} + +impl Display for PrinterCommand { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.description()) + } +} + +pub struct PrinterDevice { + state: PrinterState, + command: PrinterCommand, + compression: bool, + command_length: u16, + length_left: u16, + checksum: u16, + status: u8, + byte_out: u8, + data: [u8; 0x280], + image: [u8; 160 * 200], + image_offset: u16, + callback: fn(image_buffer: &Vec<u8>), +} + +impl PrinterDevice { + pub fn new() -> Self { + Self { + state: PrinterState::MagicBytes1, + command: PrinterCommand::Other, + compression: false, + command_length: 0, + length_left: 0, + checksum: 0x0, + status: 0x0, + byte_out: 0x0, + data: [0x00; 0x280], + image: [0x00; 160 * 200], + image_offset: 0, + callback: |_| {}, + } + } + + pub fn reset(&mut self) { + self.state = PrinterState::MagicBytes1; + self.command = PrinterCommand::Other; + self.compression = false; + self.command_length = 0; + self.length_left = 0; + self.checksum = 0x0; + self.status = 0x0; + self.byte_out = 0x0; + self.data = [0x00; 0x280]; + self.image = [0x00; 160 * 200]; + self.image_offset = 0; + } + + pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) { + self.callback = callback; + } + + fn run_command(&mut self, command: PrinterCommand) { + match command { + PrinterCommand::Init => { + self.status = 0x00; + self.byte_out = self.status; + self.image_offset = 0; + } + PrinterCommand::Print => { + let mut image_buffer = Vec::new(); + let palette_index = self.data[2]; + + for index in 0..self.image_offset { + let value = self.image[index as usize]; + let pixel_offset = (palette_index >> (value << 1)) & 0x03; + let pixel = PRINTER_PALETTE[pixel_offset as usize]; + image_buffer.push(pixel[0]); + image_buffer.push(pixel[1]); + image_buffer.push(pixel[2]); + image_buffer.push(pixel[3]); + } + + (self.callback)(&image_buffer); + + self.byte_out = self.status; + self.status = 0x06; + } + PrinterCommand::Data => { + if self.command_length == 0x280 { + self.flush_image(); + } + // in case the command is of size 0 we assume this is + // an EOF and we ignore this data operation + else if self.command_length == 0x0 { + } else { + warnln!( + "Printer: Wrong size for data: {:04x} bytes", + self.command_length + ); + } + self.status = 0x08; + self.byte_out = self.status; + } + PrinterCommand::Status => { + self.byte_out = self.status; + + // in case the current status is printing let's + // mark it as done, resetting the status back to + // the original value + if self.status == 0x06 { + self.status = 0x00; + } + } + PrinterCommand::Other => { + warnln!("Printer: Invalid command: {:02x}", self.state as u8); + } + } + } + + fn flush_image(&mut self) { + // sets the initial value of the index that will point to + // the data that is going to be copied to the image buffer + let mut index = 0; + + // iterates over the two rows that are going to be printed + // keep in mind that the printer only allows 2 lines at each + // time to be printed + for _ in 0..2 { + for col in 0..20 { + for y in 0..8 { + let mut first = self.data[index]; + let mut second = self.data[index + 1]; + for x in 0..8 { + let offset = self.image_offset as usize + (col * 8) + (y * 160) + x; + self.image[offset] = (first >> 7) | ((second >> 6) & 0x02); + + first <<= 1; + second <<= 1; + } + index += 2 + } + } + + self.image_offset += 160 * 8; + } + } +} + +impl SerialDevice for PrinterDevice { + fn send(&mut self) -> u8 { + self.byte_out + } + + fn receive(&mut self, byte: u8) { + self.byte_out = 0x00; + + match self.state { + PrinterState::MagicBytes1 => { + if byte != 0x88 { + warnln!("Printer: Invalid magic byte 1: {:02x}", byte); + return; + } + self.command = PrinterCommand::Other; + self.command_length = 0; + } + PrinterState::MagicBytes2 => { + if byte != 0x33 { + if byte != 0x88 { + self.state = PrinterState::MagicBytes1; + } + warnln!("Printer: Invalid magic byte 2: {:02x}", byte); + return; + } + } + PrinterState::Identification => self.command = PrinterCommand::from_u8(byte), + PrinterState::Compression => { + self.compression = byte & 0x01 == 0x01; + if self.compression { + warnln!("Printer: Using compressed data, currently unsupported"); + } + } + PrinterState::LengthLow => self.length_left = byte as u16, + PrinterState::LengthHigh => self.length_left |= (byte as u16) << 8, + PrinterState::Data => { + self.data[self.command_length as usize] = byte; + self.command_length += 1; + self.length_left -= 1; + } + PrinterState::ChecksumLow => self.checksum = byte as u16, + PrinterState::ChecksumHigh => { + self.checksum |= (byte as u16) << 8; + self.byte_out = 0x81; + } + PrinterState::KeepAlive => { + self.run_command(self.command); + } + PrinterState::Status => { + self.state = PrinterState::MagicBytes1; + return; + } + PrinterState::Other => { + warnln!("Printer: Invalid state: {:02x}", self.state as u8); + return; + } + } + + if self.state != PrinterState::Data { + self.state = PrinterState::from_u8(self.state as u8 + 1); + } + + if self.state == PrinterState::Data && self.length_left == 0 { + self.state = PrinterState::from_u8(self.state as u8 + 1); + } + } + + fn allow_slave(&self) -> bool { + false + } +} + +impl Default for PrinterDevice { + fn default() -> Self { + Self::new() + } +} diff --git a/src/devices/stdout.rs b/src/devices/stdout.rs new file mode 100644 index 0000000000000000000000000000000000000000..048a83f7a78e61df5245d8aa03cd04524d8c8ba0 --- /dev/null +++ b/src/devices/stdout.rs @@ -0,0 +1,46 @@ +use std::io::{stdout, Write}; + +use crate::serial::SerialDevice; + +pub struct StdoutDevice { + flush: bool, + callback: fn(image_buffer: &Vec<u8>), +} + +impl StdoutDevice { + pub fn new(flush: bool) -> Self { + Self { + flush, + callback: |_| {}, + } + } + + pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) { + self.callback = callback; + } +} + +impl SerialDevice for StdoutDevice { + fn send(&mut self) -> u8 { + 0xff + } + + fn receive(&mut self, byte: u8) { + print!("{}", byte as char); + if self.flush { + stdout().flush().unwrap(); + } + let data = vec![byte]; + (self.callback)(&data); + } + + fn allow_slave(&self) -> bool { + false + } +} + +impl Default for StdoutDevice { + fn default() -> Self { + Self::new(true) + } +} diff --git a/src/gb.rs b/src/gb.rs index 399c386ebe3d8286af1378a11c71034ba537003e..1e970d4e6c356793385df1582f088061a424073b 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -2,11 +2,13 @@ use crate::{ apu::Apu, cpu::Cpu, data::{BootRom, CGB_BOOT, DMG_BOOT, DMG_BOOTIX, MGB_BOOTIX, SGB_BOOT}, + devices::{printer::PrinterDevice, stdout::StdoutDevice}, gen::{COMPILATION_DATE, COMPILATION_TIME, COMPILER, COMPILER_VERSION}, mmu::Mmu, pad::{Pad, PadKey}, ppu::{Ppu, PpuMode, Tile, FRAME_BUFFER_SIZE}, rom::Cartridge, + serial::{NullDevice, Serial, SerialDevice}, timer::Timer, util::read_file, }; @@ -33,10 +35,31 @@ use std::{ /// Should serve as the main entry-point API. #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct GameBoy { + /// Reference to the Game Boy CPU component to be + /// used as the main element of the system, when + /// clocked, the amount of ticks from it will be + /// used as reference or the rest of the components. cpu: Cpu, + + /// If the PPU is enabled, it will be clocked. ppu_enabled: bool, + + /// If the APU is enabled, it will be clocked. apu_enabled: bool, + + /// If the timer is enabled, it will be clocked. timer_enabled: bool, + + /// If the serial is enabled, it will be clocked. + serial_enabled: bool, + + /// The current frequency at which the Game Boy + /// emulator is being handled. This is a "hint" that + /// may help components to adjust their internal + /// logic to match the current frequency. For example + /// the APU will adjust its internal clock to match + /// this hint. + clock_freq: u32, } #[cfg_attr(feature = "wasm", wasm_bindgen)] @@ -72,19 +95,24 @@ impl GameBoy { let apu = Apu::default(); let pad = Pad::default(); let timer = Timer::default(); - let mmu = Mmu::new(ppu, apu, pad, timer); + let serial = Serial::default(); + let mmu = Mmu::new(ppu, apu, pad, timer, serial); let cpu = Cpu::new(mmu); Self { cpu, ppu_enabled: true, apu_enabled: true, timer_enabled: true, + serial_enabled: true, + clock_freq: GameBoy::CPU_FREQ, } } pub fn reset(&mut self) { self.ppu().reset(); self.apu().reset(); + self.timer().reset(); + self.serial().reset(); self.mmu().reset(); self.cpu.reset(); } @@ -100,6 +128,9 @@ impl GameBoy { if self.timer_enabled { self.timer_clock(cycles); } + if self.serial_enabled { + self.serial_clock(cycles); + } cycles } @@ -127,6 +158,10 @@ impl GameBoy { self.timer().clock(cycles) } + pub fn serial_clock(&mut self, cycles: u8) { + self.serial().clock(cycles) + } + pub fn ppu_ly(&mut self) -> u8 { self.ppu().ly() } @@ -210,6 +245,36 @@ impl GameBoy { buffer } + pub fn audio_output(&self) -> u8 { + self.apu_i().output() + } + + pub fn audio_all_output(&self) -> Vec<u8> { + vec![ + self.audio_output(), + self.audio_ch1_output(), + self.audio_ch2_output(), + self.audio_ch3_output(), + self.audio_ch4_output(), + ] + } + + pub fn audio_ch1_output(&self) -> u8 { + self.apu_i().ch1_output() + } + + pub fn audio_ch2_output(&self) -> u8 { + self.apu_i().ch2_output() + } + + pub fn audio_ch3_output(&self) -> u8 { + self.apu_i().ch3_output() + } + + pub fn audio_ch4_output(&self) -> u8 { + self.apu_i().ch4_output() + } + pub fn cartridge_eager(&mut self) -> Cartridge { self.mmu().rom().clone() } @@ -261,23 +326,23 @@ impl GameBoy { /// Obtains the name of the compiler that has been /// used in the compilation of the base Boytacean /// library. Can be used for diagnostics. - pub fn get_compiler(&self) -> String { + pub fn compiler(&self) -> String { String::from(COMPILER) } - pub fn get_compiler_version(&self) -> String { + pub fn compiler_version(&self) -> String { String::from(COMPILER_VERSION) } - pub fn get_compilation_date(&self) -> String { + pub fn compilation_date(&self) -> String { String::from(COMPILATION_DATE) } - pub fn get_compilation_time(&self) -> String { + pub fn compilation_time(&self) -> String { String::from(COMPILATION_TIME) } - pub fn get_ppu_enabled(&self) -> bool { + pub fn ppu_enabled(&self) -> bool { self.ppu_enabled } @@ -285,7 +350,7 @@ impl GameBoy { self.ppu_enabled = value; } - pub fn get_apu_enabled(&self) -> bool { + pub fn apu_enabled(&self) -> bool { self.apu_enabled } @@ -293,13 +358,42 @@ impl GameBoy { self.apu_enabled = value; } - pub fn get_timer_enabled(&self) -> bool { - self.apu_enabled + pub fn timer_enabled(&self) -> bool { + self.timer_enabled } pub fn set_timer_enabled(&mut self, value: bool) { self.timer_enabled = value; } + + pub fn serial_enabled(&self) -> bool { + self.serial_enabled + } + + pub fn set_serial_enabled(&mut self, value: bool) { + self.serial_enabled = value; + } + + pub fn clock_freq(&self) -> u32 { + self.clock_freq + } + + pub fn set_clock_freq(&mut self, value: u32) { + self.clock_freq = value; + self.apu().set_clock_freq(value); + } + + pub fn attach_null_serial(&mut self) { + self.attach_serial(Box::<NullDevice>::default()); + } + + pub fn attach_stdout_serial(&mut self) { + self.attach_serial(Box::<StdoutDevice>::default()); + } + + pub fn attach_printer_serial(&mut self) { + self.attach_serial(Box::<PrinterDevice>::default()); + } } /// Gameboy implementations that are meant with performance @@ -345,6 +439,10 @@ impl GameBoy { self.cpu.timer() } + pub fn serial(&mut self) -> &mut Serial { + self.cpu.serial() + } + pub fn frame_buffer(&mut self) -> &[u8; FRAME_BUFFER_SIZE] { &(self.ppu().frame_buffer) } @@ -390,6 +488,10 @@ impl GameBoy { let data = read_file(path); self.load_rom(&data) } + + pub fn attach_serial(&mut self, device: Box<dyn SerialDevice>) { + self.serial().set_device(device); + } } #[cfg(feature = "wasm")] @@ -407,6 +509,27 @@ impl GameBoy { self.load_rom(data).clone() } + pub fn load_null_ws(&mut self) { + let null = Box::<NullDevice>::default(); + self.attach_serial(null); + } + + pub fn load_logger_ws(&mut self) { + let mut logger = Box::<StdoutDevice>::default(); + logger.set_callback(|data| { + logger_callback(data.to_vec()); + }); + self.attach_serial(logger); + } + + pub fn load_printer_ws(&mut self) { + let mut printer = Box::<PrinterDevice>::default(); + printer.set_callback(|image_buffer| { + printer_callback(image_buffer.to_vec()); + }); + self.attach_serial(printer); + } + pub fn set_palette_colors_ws(&mut self, value: Vec<JsValue>) { let palette: Palette = value .into_iter() @@ -417,7 +540,7 @@ impl GameBoy { self.ppu().set_palette_colors(&palette); } - pub fn get_wasm_engine_ws(&self) -> Option<String> { + pub fn wasm_engine_ws(&self) -> Option<String> { let dependencies = dependencies_map(); if !dependencies.contains_key("wasm-bindgen") { return None; @@ -448,6 +571,12 @@ impl GameBoy { extern "C" { #[wasm_bindgen(js_namespace = window)] fn panic(message: &str); + + #[wasm_bindgen(js_namespace = window, js_name = loggerCallback)] + fn logger_callback(data: Vec<u8>); + + #[wasm_bindgen(js_namespace = window, js_name = printerCallback)] + fn printer_callback(image_buffer: Vec<u8>); } #[cfg(feature = "wasm")] diff --git a/src/inst.rs b/src/inst.rs index b19487d5e681617e27ce0bb7e7de53389d3d4ab7..8b059a595d8a894afacebf4cbb24e369096ef15a 100644 --- a/src/inst.rs +++ b/src/inst.rs @@ -711,7 +711,7 @@ fn ld_d_u8(cpu: &mut Cpu) { } fn rla(cpu: &mut Cpu) { - let carry = cpu.get_carry(); + let carry = cpu.carry(); cpu.set_carry(cpu.a & 0x80 == 0x80); @@ -769,7 +769,7 @@ fn ld_e_u8(cpu: &mut Cpu) { } fn rra(cpu: &mut Cpu) { - let carry = cpu.get_carry(); + let carry = cpu.carry(); cpu.set_carry((cpu.a & 0x1) == 0x1); @@ -783,7 +783,7 @@ fn rra(cpu: &mut Cpu) { fn jr_nz_i8(cpu: &mut Cpu) { let byte = cpu.read_u8() as i8; - if cpu.get_zero() { + if cpu.zero() { return; } @@ -836,16 +836,16 @@ fn daa(cpu: &mut Cpu) { let a = cpu.a; let mut adjust = 0; - if cpu.get_half_carry() { + if cpu.half_carry() { adjust |= 0x06; } - if cpu.get_carry() { + if cpu.carry() { // Yes, we have to adjust it. adjust |= 0x60; } - let res = if cpu.get_sub() { + let res = if cpu.sub() { a.wrapping_sub(adjust) } else { if a & 0x0f > 0x09 { @@ -869,7 +869,7 @@ fn daa(cpu: &mut Cpu) { fn jr_z_i8(cpu: &mut Cpu) { let byte = cpu.read_u8() as i8; - if !cpu.get_zero() { + if !cpu.zero() { return; } @@ -933,7 +933,7 @@ fn ld_sp_u16(cpu: &mut Cpu) { fn jr_nc_i8(cpu: &mut Cpu) { let byte = cpu.read_u8() as i8; - if cpu.get_carry() { + if cpu.carry() { return; } @@ -986,7 +986,7 @@ fn scf(cpu: &mut Cpu) { fn jr_c_i8(cpu: &mut Cpu) { let byte = cpu.read_u8() as i8; - if !cpu.get_carry() { + if !cpu.carry() { return; } @@ -1039,7 +1039,7 @@ fn ld_a_u8(cpu: &mut Cpu) { fn ccf(cpu: &mut Cpu) { cpu.set_sub(false); cpu.set_half_carry(false); - cpu.set_carry(!cpu.get_carry()); + cpu.set_carry(!cpu.carry()); } fn ld_b_b(_cpu: &mut Cpu) {} @@ -1676,7 +1676,7 @@ fn cp_a_a(cpu: &mut Cpu) { } fn ret_nz(cpu: &mut Cpu) { - if cpu.get_zero() { + if cpu.zero() { return; } @@ -1692,7 +1692,7 @@ fn pop_bc(cpu: &mut Cpu) { fn jp_nz_u16(cpu: &mut Cpu) { let word = cpu.read_u16(); - if cpu.get_zero() { + if cpu.zero() { return; } @@ -1708,7 +1708,7 @@ fn jp_u16(cpu: &mut Cpu) { fn call_nz_u16(cpu: &mut Cpu) { let word = cpu.read_u16(); - if cpu.get_zero() { + if cpu.zero() { return; } @@ -1731,7 +1731,7 @@ fn rst_00h(cpu: &mut Cpu) { } fn ret_z(cpu: &mut Cpu) { - if !cpu.get_zero() { + if !cpu.zero() { return; } @@ -1746,7 +1746,7 @@ fn ret(cpu: &mut Cpu) { fn jp_z_u16(cpu: &mut Cpu) { let word = cpu.read_u16(); - if !cpu.get_zero() { + if !cpu.zero() { return; } @@ -1757,7 +1757,7 @@ fn jp_z_u16(cpu: &mut Cpu) { fn call_z_u16(cpu: &mut Cpu) { let word = cpu.read_u16(); - if !cpu.get_zero() { + if !cpu.zero() { return; } @@ -1782,7 +1782,7 @@ fn rst_08h(cpu: &mut Cpu) { } fn ret_nc(cpu: &mut Cpu) { - if cpu.get_carry() { + if cpu.carry() { return; } @@ -1798,7 +1798,7 @@ fn pop_de(cpu: &mut Cpu) { fn jp_nc_u16(cpu: &mut Cpu) { let word = cpu.read_u16(); - if cpu.get_carry() { + if cpu.carry() { return; } @@ -1809,7 +1809,7 @@ fn jp_nc_u16(cpu: &mut Cpu) { fn call_nc_u16(cpu: &mut Cpu) { let word = cpu.read_u16(); - if cpu.get_carry() { + if cpu.carry() { return; } @@ -1832,7 +1832,7 @@ fn rst_10h(cpu: &mut Cpu) { } fn ret_c(cpu: &mut Cpu) { - if !cpu.get_carry() { + if !cpu.carry() { return; } @@ -1848,7 +1848,7 @@ fn reti(cpu: &mut Cpu) { fn jp_c_u16(cpu: &mut Cpu) { let word = cpu.read_u16(); - if !cpu.get_carry() { + if !cpu.carry() { return; } @@ -1859,7 +1859,7 @@ fn jp_c_u16(cpu: &mut Cpu) { fn call_c_u16(cpu: &mut Cpu) { let word = cpu.read_u16(); - if !cpu.get_carry() { + if !cpu.carry() { return; } @@ -3132,7 +3132,7 @@ fn res(value: u8, bit: u8) -> u8 { /// byte (probably from a register) and updates the /// proper flag registers. fn rl(cpu: &mut Cpu, value: u8) -> u8 { - let carry = cpu.get_carry(); + let carry = cpu.carry(); cpu.set_carry((value & 0x80) == 0x80); @@ -3161,7 +3161,7 @@ fn rlc(cpu: &mut Cpu, value: u8) -> u8 { /// byte (probably from a register) and updates the /// proper flag registers. fn rr(cpu: &mut Cpu, value: u8) -> u8 { - let carry = cpu.get_carry(); + let carry = cpu.carry(); cpu.set_carry((value & 0x1) == 0x1); @@ -3259,7 +3259,7 @@ fn add_set_flags(cpu: &mut Cpu, first: u8, second: u8) -> u8 { fn add_carry_set_flags(cpu: &mut Cpu, first: u8, second: u8) -> u8 { let first = first as u32; let second = second as u32; - let carry = cpu.get_carry() as u32; + let carry = cpu.carry() as u32; let result = first.wrapping_add(second).wrapping_add(carry); let result_b = result as u8; @@ -3290,7 +3290,7 @@ fn sub_set_flags(cpu: &mut Cpu, first: u8, second: u8) -> u8 { fn sub_carry_set_flags(cpu: &mut Cpu, first: u8, second: u8) -> u8 { let first = first as u32; let second = second as u32; - let carry = cpu.get_carry() as u32; + let carry = cpu.carry() as u32; let result = first.wrapping_sub(second).wrapping_sub(carry); let result_b = result as u8; diff --git a/src/lib.rs b/src/lib.rs index 7decfab2b80948c67ae0c8c3765191dda99b7743..80f4f5b8e995c1081e8319a0c9b396a73503375a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod apu; pub mod cpu; pub mod data; +pub mod devices; pub mod gb; pub mod gen; pub mod inst; @@ -11,5 +12,6 @@ pub mod mmu; pub mod pad; pub mod ppu; pub mod rom; +pub mod serial; pub mod timer; pub mod util; diff --git a/src/macros.rs b/src/macros.rs index f1831675cc6702887dc6ee90b627410c82ababac..8180aa3a90626e50730dd29353ac902f4dba0afe 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -2,8 +2,10 @@ #[macro_export] macro_rules! debugln { ($($rest:tt)*) => { - std::print!("[DEBUG] "); - std::println!($($rest)*) + { + std::print!("[DEBUG] "); + std::println!($($rest)*); + } } } diff --git a/src/mmu.rs b/src/mmu.rs index 17de8cb88a7a1d743c7baf291135337bf781d261..c659df2a8098f19d9c30e3e409cf90f24a876958 100644 --- a/src/mmu.rs +++ b/src/mmu.rs @@ -1,4 +1,4 @@ -use crate::{apu::Apu, debugln, pad::Pad, ppu::Ppu, rom::Cartridge, timer::Timer}; +use crate::{apu::Apu, debugln, pad::Pad, ppu::Ppu, rom::Cartridge, serial::Serial, timer::Timer}; pub const BOOT_SIZE_DMG: usize = 256; pub const BOOT_SIZE_CGB: usize = 2304; @@ -29,6 +29,10 @@ pub struct Mmu { /// that is memory mapped. timer: Timer, + /// The serial data transfer controller to be used to control the + /// link cable connection, this component is memory mapped. + serial: Serial, + /// The cartridge ROM that is currently loaded into the system, /// going to be used to access ROM and external RAM banks. rom: Cartridge, @@ -58,12 +62,13 @@ pub struct Mmu { } impl Mmu { - pub fn new(ppu: Ppu, apu: Apu, pad: Pad, timer: Timer) -> Self { + pub fn new(ppu: Ppu, apu: Apu, pad: Pad, timer: Timer, serial: Serial) -> Self { Self { ppu, apu, pad, timer, + serial, rom: Cartridge::new(), boot_active: true, boot: vec![], @@ -118,6 +123,10 @@ impl Mmu { &mut self.timer } + pub fn serial(&mut self) -> &mut Serial { + &mut self.serial + } + pub fn boot_active(&self) -> bool { self.boot_active } @@ -171,6 +180,9 @@ impl Mmu { | 0xa00 | 0xb00 | 0xc00 | 0xd00 => self.ram[(addr & 0x1fff) as usize], 0xe00 => self.ppu.read(addr), 0xf00 => match addr & 0x00ff { + // 0xFF01-0xFF02 - Serial data transfer + 0x01..=0x02 => self.serial.read(addr), + // 0xFF0F — IF: Interrupt flag 0x0f => { @@ -178,6 +190,7 @@ impl Mmu { (if self.ppu.int_vblank() { 0x01 } else { 0x00 } | if self.ppu.int_stat() { 0x02 } else { 0x00 } | if self.timer.int_tima() { 0x04 } else { 0x00 } + | if self.serial.int_serial() { 0x08 } else { 0x00 } | if self.pad.int_pad() { 0x10 } else { 0x00 }) } @@ -200,7 +213,7 @@ impl Mmu { 0x00 } }, - 0x10..=26 | 0x30..=0x37 => self.apu.read(addr), + 0x10..=0x26 | 0x30..=0x37 => self.apu.read(addr), 0x40 | 0x50 | 0x60 | 0x70 => self.ppu.read(addr), _ => { debugln!("Reading from unknown IO control 0x{:04x}", addr); @@ -246,11 +259,15 @@ impl Mmu { } 0xe00 => self.ppu.write(addr, value), 0xf00 => match addr & 0x00ff { + // 0xFF01-0xFF02 - Serial data transfer + 0x01..=0x02 => self.serial.write(addr, value), + // 0xFF0F — IF: Interrupt flag 0x0f => { self.ppu.set_int_vblank(value & 0x01 == 0x01); self.ppu.set_int_stat(value & 0x02 == 0x02); self.timer.set_int_tima(value & 0x04 == 0x04); + self.serial.set_int_serial(value & 0x08 == 0x08); self.pad.set_int_pad(value & 0x10 == 0x10); } @@ -281,7 +298,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), + 0x10..=0x26 | 0x30..=0x37 => self.apu.write(addr, value), 0x40 | 0x60 | 0x70 => { match addr & 0x00ff { // 0xFF46 — DMA: OAM DMA source address & start diff --git a/src/pad.rs b/src/pad.rs index f99e0a85a2b43b5705845039b9908bbc780609f7..f9e6ef97d3b6ab49edec7ea9f1a8f778958a6597 100644 --- a/src/pad.rs +++ b/src/pad.rs @@ -5,6 +5,7 @@ use crate::warnln; #[derive(Clone, Copy, PartialEq, Eq)] pub enum PadSelection { + None, Action, Direction, } @@ -45,7 +46,7 @@ impl Pad { select: false, b: false, a: false, - selection: PadSelection::Action, + selection: PadSelection::None, int_pad: false, } } @@ -70,15 +71,12 @@ impl Pad { | if self.up { 0x00 } else { 0x04 } | if self.down { 0x00 } else { 0x08 }) } + PadSelection::None => 0x0f, }; - value |= if self.selection == PadSelection::Direction { - 0x10 - } else { - 0x00 - } | if self.selection == PadSelection::Action { - 0x20 - } else { - 0x00 + value |= match self.selection { + PadSelection::Action => 0x10, + PadSelection::Direction => 0x20, + PadSelection::None => 0x30, }; value } @@ -92,11 +90,12 @@ impl Pad { pub fn write(&mut self, addr: u16, value: u8) { match addr & 0x00ff { 0x0000 => { - self.selection = if value & 0x10 == 0x00 { - PadSelection::Direction - } else { - PadSelection::Action - } + self.selection = match value & 0x30 { + 0x10 => PadSelection::Action, + 0x20 => PadSelection::Direction, + 0x30 => PadSelection::None, + _ => PadSelection::None, + }; } _ => warnln!("Writing to unknown Pad location 0x{:04x}", addr), } @@ -115,7 +114,7 @@ impl Pad { } // signals that a JoyPad interrupt is pending to be - // handled as a key pressed has been done + // handled as a key press has been performed self.int_pad = true; } @@ -132,14 +131,17 @@ impl Pad { } } + #[inline(always)] pub fn int_pad(&self) -> bool { self.int_pad } + #[inline(always)] pub fn set_int_pad(&mut self, value: bool) { self.int_pad = value; } + #[inline(always)] pub fn ack_pad(&mut self) { self.set_int_pad(false); } diff --git a/src/ppu.rs b/src/ppu.rs index af904c779421cfaed25074b128e296183e85dfec..99f6042cb14dd7b8fe9df3deb1e2bab20b3efa6c 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -14,6 +14,7 @@ pub const HRAM_SIZE: usize = 128; pub const OAM_SIZE: usize = 260; pub const PALETTE_SIZE: usize = 4; pub const RGB_SIZE: usize = 3; +pub const RGBA_SIZE: usize = 4; pub const TILE_WIDTH: usize = 8; pub const TILE_HEIGHT: usize = 8; pub const TILE_DOUBLE_HEIGHT: usize = 16; @@ -48,10 +49,18 @@ pub const PALETTE_COLORS: Palette = [[255, 255, 255], [192, 192, 192], [96, 96, /// with the size of RGB (3 bytes). pub type Pixel = [u8; RGB_SIZE]; +/// Defines a transparent Game Boy pixel type as a buffer +/// with the size of RGBA (4 bytes). +pub type PixelAlpha = [u8; RGBA_SIZE]; + /// Defines a type that represents a color palette /// within the Game Boy context. pub type Palette = [Pixel; PALETTE_SIZE]; +/// Defines a type that represents a color palette +/// with alpha within the Game Boy context. +pub type PaletteAlpha = [PixelAlpha; PALETTE_SIZE]; + /// Represents a palette with the metadata that is /// associated with it. #[cfg_attr(feature = "wasm", wasm_bindgen)] @@ -642,26 +651,32 @@ impl Ppu { self.frame_index } + #[inline(always)] pub fn int_vblank(&self) -> bool { self.int_vblank } + #[inline(always)] pub fn set_int_vblank(&mut self, value: bool) { self.int_vblank = value; } + #[inline(always)] pub fn ack_vblank(&mut self) { self.set_int_vblank(false); } + #[inline(always)] pub fn int_stat(&self) -> bool { self.int_stat } + #[inline(always)] pub fn set_int_stat(&mut self, value: bool) { self.int_stat = value; } + #[inline(always)] pub fn ack_stat(&mut self) { self.set_int_stat(false); } @@ -873,7 +888,7 @@ impl Ppu { let mut index_buffer = [-256i16; DISPLAY_WIDTH]; for index in 0..OBJ_COUNT { - // in case the limit on number of object to be draw per + // in case the limit on the number of objects to be draw per // line has been reached breaks the loop avoiding more draws if draw_count == 10 { break; diff --git a/src/rom.rs b/src/rom.rs index 04ccbec44555a6b8bb3ef701c9350187b95dc71e..bdad478dfbb7cdfcefaa53e31cf02945436af22a 100644 --- a/src/rom.rs +++ b/src/rom.rs @@ -182,6 +182,29 @@ impl Display for RamSize { } } +#[cfg_attr(feature = "wasm", wasm_bindgen)] +pub enum CgbMode { + NoCgb = 0x00, + CgbCompatible = 0x80, + CgbOnly = 0xc0, +} + +impl CgbMode { + pub fn description(&self) -> &'static str { + match self { + CgbMode::NoCgb => "No CGB support", + CgbMode::CgbCompatible => "CGB backwards compatible", + CgbMode::CgbOnly => "CGB only", + } + } +} + +impl Display for CgbMode { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.description()) + } +} + /// Structure that defines the ROM and ROM contents /// of a Game Boy cartridge. Should correctly address /// the specifics of all the major MBCs (Memory Bank @@ -339,6 +362,16 @@ impl Cartridge { if *byte == 0u8 { break; } + + // in we're at the final byte of the title and the value + // is one that is reserved for CGB compatibility testing + // then we must ignore it for title processing purposes + if offset > 14 + && (*byte == CgbMode::CgbCompatible as u8 || *byte == CgbMode::CgbOnly as u8) + { + break; + } + offset += 1; } self.title_offset = 0x0134 + offset; @@ -360,6 +393,14 @@ impl Cartridge { ) } + pub fn cgb_flag(&self) -> CgbMode { + match self.rom_data[0x0143] { + 0x80 => CgbMode::CgbCompatible, + 0xc0 => CgbMode::CgbOnly, + _ => CgbMode::NoCgb, + } + } + pub fn rom_type(&self) -> RomType { match self.rom_data[0x0147] { 0x00 => RomType::RomOnly, @@ -469,11 +510,12 @@ impl Display for Cartridge { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!( f, - "Name => {}\nType => {}\nROM Size => {}\nRAM Size => {}", + "Name => {}\nType => {}\nROM Size => {}\nRAM Size => {}\nCGB Mode => {}", self.title(), self.rom_type(), self.rom_size(), - self.ram_size() + self.ram_size(), + self.cgb_flag() ) } } diff --git a/src/serial.rs b/src/serial.rs new file mode 100644 index 0000000000000000000000000000000000000000..dd6388a3c65daddda4800dabbb2ead2851184322 --- /dev/null +++ b/src/serial.rs @@ -0,0 +1,211 @@ +use crate::warnln; + +pub trait SerialDevice { + fn send(&mut self) -> u8; + fn receive(&mut self, byte: u8); + + /// Whether the serial device "driver" supports slave mode + /// simulating an external clock source. Or if instead the + /// clock should always be generated by the running device. + fn allow_slave(&self) -> bool; +} + +pub struct Serial { + data: u8, + control: u8, + shift_clock: bool, + clock_speed: bool, + transferring: bool, + timer: i16, + length: u16, + bit_count: u8, + byte_receive: u8, + int_serial: bool, + device: Box<dyn SerialDevice>, +} + +impl Serial { + pub fn new() -> Self { + Self { + data: 0x0, + control: 0x0, + shift_clock: false, + clock_speed: false, + transferring: false, + timer: 0, + length: 512, + bit_count: 0, + byte_receive: 0x0, + int_serial: false, + device: Box::<NullDevice>::default(), + } + } + + pub fn reset(&mut self) { + self.data = 0x0; + self.control = 0x0; + self.shift_clock = false; + self.clock_speed = false; + self.transferring = false; + self.timer = 0; + self.length = 512; + self.bit_count = 0; + self.byte_receive = 0x0; + self.int_serial = false; + } + + pub fn clock(&mut self, cycles: u8) { + if !self.transferring { + return; + } + + self.timer = self.timer.saturating_sub(cycles as i16); + if self.timer <= 0 { + let bit = (self.byte_receive >> (7 - self.bit_count)) & 0x01; + self.data = (self.data << 1) | bit; + + self.tick_transfer(); + + self.timer = self.length as i16; + } + } + + pub fn read(&mut self, addr: u16) -> u8 { + match addr & 0x00ff { + 0x01 => self.data, + 0x02 => + { + #[allow(clippy::bool_to_int_with_if)] + (if self.shift_clock { 0x01 } else { 0x00 } + | if self.clock_speed { 0x02 } else { 0x00 } + | if self.transferring { 0x80 } else { 0x00 }) + } + _ => { + warnln!("Reding from unknown Serial location 0x{:04x}", addr); + 0xff + } + } + } + + pub fn write(&mut self, addr: u16, value: u8) { + match addr & 0x00ff { + 0x01 => self.data = value, + 0x02 => { + self.shift_clock = value & 0x01 == 0x01; + self.clock_speed = value & 0x02 == 0x02; + self.transferring = value & 0x80 == 0x80; + + // in case the clock is meant to be set by the attached device + // and the current Game Boy is meant to be running in slave mode + // then checks if the attached device "driver" allows clock set + // by external device and if not then ignores the transfer request + // by immediately disabling the transferring flag + if !self.shift_clock && !self.device.allow_slave() { + self.transferring = false; + } + + // in case a transfer of byte has been requested and + // this is the then we need to start the transfer setup + if self.transferring { + // @TODO: if the GBC mode exists there should + // be special check logic here + //self.length = if self.gb.is_cgb() && self.clock_speed { 16 } else { 512 }; + self.length = 512; + self.bit_count = 0; + self.timer = self.length as i16; + + // executes the send and receive operation immediately + // this is considered an operational optimization with + // no real effect on the emulation (ex: not timing issues) + self.byte_receive = self.device.send(); + self.device.receive(self.data); + } + } + _ => warnln!("Writing to unknown Serial location 0x{:04x}", addr), + } + } + + pub fn send(&self) -> bool { + if self.shift_clock { + true + } else { + self.data & 0x80 == 0x80 + } + } + + pub fn receive(&mut self, bit: bool) { + if !self.shift_clock { + self.data = (self.data << 1) | bit as u8; + self.tick_transfer(); + } + } + + #[inline(always)] + pub fn int_serial(&self) -> bool { + self.int_serial + } + + #[inline(always)] + pub fn set_int_serial(&mut self, value: bool) { + self.int_serial = value; + } + + #[inline(always)] + pub fn ack_serial(&mut self) { + self.set_int_serial(false); + } + + pub fn device(&self) -> &dyn SerialDevice { + self.device.as_ref() + } + + pub fn set_device(&mut self, device: Box<dyn SerialDevice>) { + self.device = device; + } + + fn tick_transfer(&mut self) { + self.bit_count += 1; + if self.bit_count == 8 { + self.transferring = false; + self.length = 0; + self.bit_count = 0; + + // signals the interrupt for the serial + // transfer completion, indicating that + // a new byte is ready to be read + self.int_serial = true; + } + } +} + +impl Default for Serial { + fn default() -> Self { + Self::new() + } +} + +pub struct NullDevice {} + +impl NullDevice { + pub fn new() -> Self { + Self {} + } +} + +impl SerialDevice for NullDevice { + fn send(&mut self) -> u8 { + 0xff + } + + fn receive(&mut self, _: u8) {} + + fn allow_slave(&self) -> bool { + false + } +} + +impl Default for NullDevice { + fn default() -> Self { + Self::new() + } +} diff --git a/src/timer.rs b/src/timer.rs index 537c14c6714406aa2045ab6a578a8f121f59a119..e423663620d9d65a02d48345a681b45a27d9c4fe 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -100,14 +100,17 @@ impl Timer { } } + #[inline(always)] pub fn int_tima(&self) -> bool { self.int_tima } + #[inline(always)] pub fn set_int_tima(&mut self, value: bool) { self.int_tima = value; } + #[inline(always)] pub fn ack_tima(&mut self) { self.set_int_tima(false); }