From 95f44a6c2ea148328123afced9cdf88490834c9d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com>
Date: Thu, 18 May 2023 22:50:47 +0100
Subject: [PATCH] feat: support for auto GB mode selection This makes it
 possible to select CBG or DMG mode taking into consideration the CGB flag in
 the ROM file.

---
 frontends/sdl/src/main.rs                     | 22 +++++++++---
 .../serial-section/serial-section.tsx         |  4 +--
 frontends/web/ts/gb.ts                        | 34 +++++++++++++++----
 src/gb.rs                                     |  5 +++
 src/rom.rs                                    | 14 +++++++-
 5 files changed, 65 insertions(+), 14 deletions(-)

diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs
index c8227f12..139b8a22 100644
--- a/frontends/sdl/src/main.rs
+++ b/frontends/sdl/src/main.rs
@@ -10,6 +10,7 @@ use boytacean::{
     gb::{AudioProvider, GameBoy, GameBoyMode},
     pad::PadKey,
     ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH},
+    rom::Cartridge,
     serial::{NullDevice, SerialDevice},
 };
 use chrono::Utc;
@@ -49,6 +50,7 @@ impl Default for Benchmark {
 
 pub struct Emulator {
     system: GameBoy,
+    auto_mode: bool,
     graphics: Option<Graphics>,
     audio: Option<Audio>,
     title: &'static str,
@@ -63,9 +65,10 @@ pub struct Emulator {
 }
 
 impl Emulator {
-    pub fn new(system: GameBoy) -> Self {
+    pub fn new(system: GameBoy, auto_mode: bool) -> Self {
         Self {
             system,
+            auto_mode,
             graphics: None,
             audio: None,
             title: TITLE,
@@ -183,7 +186,7 @@ impl Emulator {
 
     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);
+        let rom: &boytacean::rom::Cartridge = self.system.load_rom_file(path_res);
         println!(
             "========= Cartridge =========\n{}\n=============================",
             rom
@@ -329,6 +332,10 @@ impl Emulator {
                         }
                     }
                     Event::DropFile { filename, .. } => {
+                        if self.auto_mode {
+                            let mode = Cartridge::from_file(&filename).gb_mode();
+                            self.system.set_mode(mode);
+                        }
                         self.system.reset();
                         self.system.load(true);
                         self.load_rom(Some(&filename));
@@ -461,7 +468,7 @@ impl Emulator {
 #[derive(Parser, Debug)]
 #[command(author, version, about, long_about = None)]
 struct Args {
-    #[arg(short, long, default_value_t = String::from("cgb"))]
+    #[arg(short, long, default_value_t = String::from("auto"))]
     mode: String,
 
     #[arg(short, long, default_value_t = String::from("printer"))]
@@ -487,7 +494,12 @@ fn main() {
     // parses the provided command line arguments and uses them to
     // obtain structured values
     let args = Args::parse();
-    let mode: GameBoyMode = GameBoyMode::from_string(&args.mode);
+    let mode: GameBoyMode = if args.mode == "auto" {
+        GameBoyMode::Dmg
+    } else {
+        GameBoyMode::from_string(&args.mode)
+    };
+    let auto_mode = args.mode == "auto";
 
     // creates a new Game Boy instance and loads both the boot ROM
     // and the initial game ROM to "start the engine"
@@ -506,7 +518,7 @@ fn main() {
     // 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);
+    let mut emulator = Emulator::new(game_boy, auto_mode);
     emulator.start(SCREEN_SCALE);
     emulator.load_rom(Some(&args.rom_path));
     emulator.toggle_palette();
diff --git a/frontends/web/react/components/serial-section/serial-section.tsx b/frontends/web/react/components/serial-section/serial-section.tsx
index 03d2ffb5..2b2f8406 100644
--- a/frontends/web/react/components/serial-section/serial-section.tsx
+++ b/frontends/web/react/components/serial-section/serial-section.tsx
@@ -58,7 +58,7 @@ export const SerialSection: FC<SerialSectionProps> = ({
         };
     }, []);
 
-    const onEngineChange = (option: string) => {
+    const onDeviceChange = (option: string) => {
         emulator.loadSerialDevice(option as SerialDevice);
         const optionIcon = DEVICE_ICON[option] ?? "";
         emulator.handlers.showToast?.(
@@ -79,7 +79,7 @@ export const SerialSection: FC<SerialSectionProps> = ({
                             uppercase={true}
                             size={"large"}
                             style={["simple"]}
-                            onChange={onEngineChange}
+                            onChange={onDeviceChange}
                         />
                     }
                 />
diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts
index 2e5af350..20a58b9a 100644
--- a/frontends/web/ts/gb.ts
+++ b/frontends/web/ts/gb.ts
@@ -119,9 +119,18 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
      */
     private _engine: string | null = null;
 
-    private logicFrequency: number = LOGIC_HZ;
-    private visualFrequency: number = VISUAL_HZ;
-    private idleFrequency: number = IDLE_HZ;
+    /**
+     * If the GB running mode should be automatically inferred
+     * from the GBC flag in the cartridge. Meaning that if the
+     * cartridge is a GBC compatible or GBC only the GBC emulation
+     * mode is going to be used, otherwise the DMG mode is used
+     * instead. This should provide an optimal usage experience.
+     */
+    private autoMode = false;
+
+    private logicFrequency = LOGIC_HZ;
+    private visualFrequency = VISUAL_HZ;
+    private idleFrequency = IDLE_HZ;
 
     private paused = false;
     private nextTickTime = 0;
@@ -361,7 +370,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
      * the emulator engine to use.
      */
     async boot({
-        engine = "cgb",
+        engine = "auto",
         restore = true,
         loadRom = false,
         romPath = ROM_PATH,
@@ -394,11 +403,17 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         // selects the proper engine for execution
         // and builds a new instance of it
         switch (engine) {
+            case "auto":
+                this.gameBoy = new GameBoy(GameBoyMode.Dmg);
+                this.autoMode = true;
+                break;
             case "cgb":
                 this.gameBoy = new GameBoy(GameBoyMode.Cgb);
+                this.autoMode = false;
                 break;
             case "dmg":
                 this.gameBoy = new GameBoy(GameBoyMode.Dmg);
+                this.autoMode = false;
                 break;
             default:
                 if (!this.gameBoy) {
@@ -412,6 +427,13 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         // selected one
         this.updatePalette();
 
+        // in case the auto emulation mode is enabled runs the
+        // inference logic to try to infer the best mode from the
+        // GBC header in the cartridge data
+        if (this.autoMode) {
+            this.gameBoy.infer_mode_ws(romData);
+        }
+
         // resets the Game Boy engine to restore it into
         // a valid state ready to be used
         this.gameBoy.reset();
@@ -541,11 +563,11 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
     }
 
     get engines(): string[] {
-        return ["cgb", "dmg"];
+        return ["auto", "cgb", "dmg"];
     }
 
     get engine(): string {
-        return this._engine || "cgb";
+        return this._engine || "auto";
     }
 
     get romExts(): string[] {
diff --git a/src/gb.rs b/src/gb.rs
index 67244e46..6f09ab21 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -936,6 +936,11 @@ impl GameBoy {
         }));
     }
 
+    pub fn infer_mode_ws(&mut self, data: &[u8]) {
+        let mode = Cartridge::from_data(data).gb_mode();
+        self.set_mode(mode);
+    }
+
     pub fn load_rom_ws(&mut self, data: &[u8]) -> Cartridge {
         self.load_rom(data).clone()
     }
diff --git a/src/rom.rs b/src/rom.rs
index ca26e57a..1ab7975f 100644
--- a/src/rom.rs
+++ b/src/rom.rs
@@ -4,7 +4,7 @@ use std::{
     fmt::{Display, Formatter},
 };
 
-use crate::{debugln, warnln};
+use crate::{debugln, gb::GameBoyMode, util::read_file, warnln};
 
 #[cfg(feature = "wasm")]
 use wasm_bindgen::prelude::*;
@@ -276,6 +276,11 @@ impl Cartridge {
         cartridge
     }
 
+    pub fn from_file(path: &str) -> Self {
+        let data = read_file(path);
+        Self::from_data(&data)
+    }
+
     pub fn read(&self, addr: u16) -> u8 {
         match addr & 0xf000 {
             0x0000 | 0x1000 | 0x2000 | 0x3000 | 0x4000 | 0x5000 | 0x6000 | 0x7000 => {
@@ -404,6 +409,13 @@ impl Cartridge {
         }
     }
 
+    pub fn gb_mode(&self) -> GameBoyMode {
+        match self.cgb_flag() {
+            CgbMode::CgbCompatible | CgbMode::CgbOnly => GameBoyMode::Cgb,
+            _ => GameBoyMode::Dmg,
+        }
+    }
+
     /// A cartridge is considered legacy if it does
     /// not have a CGB flag bit (bit 7 of 0x0143) set.
     /// These are the monochromatic only Cartridges built
-- 
GitLab