From e5b15376baa09ed55fc7ae333b2f036cba9d36a8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com>
Date: Tue, 25 Jul 2023 10:29:32 +0100
Subject: [PATCH] chore: better dirty frame redraw

---
 frontends/libretro/src/lib.rs | 176 ++++++++++++++++++----------------
 frontends/sdl/src/main.rs     |  16 ++--
 frontends/web/ts/gb.ts        |  12 +--
 3 files changed, 105 insertions(+), 99 deletions(-)

diff --git a/frontends/libretro/src/lib.rs b/frontends/libretro/src/lib.rs
index 2f99b7da..d196b293 100644
--- a/frontends/libretro/src/lib.rs
+++ b/frontends/libretro/src/lib.rs
@@ -12,7 +12,7 @@ use std::{
 use boytacean::{
     gb::{AudioProvider, GameBoy},
     pad::PadKey,
-    ppu::{DISPLAY_HEIGHT, DISPLAY_WIDTH, FRAME_BUFFER_RGB155_SIZE, RGB1555_SIZE},
+    ppu::{PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH, FRAME_BUFFER_RGB155_SIZE, RGB1555_SIZE},
     rom::Cartridge,
 };
 use consts::{
@@ -39,6 +39,17 @@ static mut INPUT_STATE_CALLBACK: Option<
     extern "C" fn(port: u32, device: u32, index: u32, id: u32) -> i16,
 > = None;
 
+const KEYS: [RetroJoypad; 8] = [
+    RetroJoypad::RetroDeviceIdJoypadUp,
+    RetroJoypad::RetroDeviceIdJoypadDown,
+    RetroJoypad::RetroDeviceIdJoypadLeft,
+    RetroJoypad::RetroDeviceIdJoypadRight,
+    RetroJoypad::RetroDeviceIdJoypadStart,
+    RetroJoypad::RetroDeviceIdJoypadSelect,
+    RetroJoypad::RetroDeviceIdJoypadA,
+    RetroJoypad::RetroDeviceIdJoypadB,
+];
+
 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
 pub enum RetroJoypad {
     RetroDeviceIdJoypadB = RETRO_DEVICE_ID_JOYPAD_B,
@@ -88,17 +99,6 @@ impl Display for RetroJoypad {
     }
 }
 
-const KEYS: [RetroJoypad; 8] = [
-    RetroJoypad::RetroDeviceIdJoypadUp,
-    RetroJoypad::RetroDeviceIdJoypadDown,
-    RetroJoypad::RetroDeviceIdJoypadLeft,
-    RetroJoypad::RetroDeviceIdJoypadRight,
-    RetroJoypad::RetroDeviceIdJoypadStart,
-    RetroJoypad::RetroDeviceIdJoypadSelect,
-    RetroJoypad::RetroDeviceIdJoypadA,
-    RetroJoypad::RetroDeviceIdJoypadB,
-];
-
 #[repr(C)]
 pub struct RetroGameInfo {
     pub path: *const c_char,
@@ -137,6 +137,12 @@ pub struct RetroSystemTiming {
     sample_rate: f64,
 }
 
+#[no_mangle]
+pub extern "C" fn retro_api_version() -> c_uint {
+    println!("retro_api_version()");
+    RETRO_API_VERSION
+}
+
 #[no_mangle]
 pub extern "C" fn retro_init() {
     println!("retro_init()");
@@ -158,12 +164,6 @@ pub extern "C" fn retro_reset() {
     emulator.reload();
 }
 
-#[no_mangle]
-pub extern "C" fn retro_api_version() -> c_uint {
-    println!("retro_api_version()");
-    RETRO_API_VERSION
-}
-
 /// # Safety
 ///
 /// This function should not be called only within Lib Retro context.
@@ -202,60 +202,6 @@ pub extern "C" fn retro_set_environment(
     }
 }
 
-#[no_mangle]
-pub extern "C" fn retro_set_video_refresh(
-    callback: Option<extern "C" fn(*const u8, c_uint, c_uint, usize)>,
-) {
-    println!("retro_set_video_refresh()");
-    unsafe {
-        VIDEO_REFRESH_CALLBACK = callback;
-    }
-}
-
-#[no_mangle]
-pub extern "C" fn retro_set_audio_sample(callback: Option<extern "C" fn(i16, i16)>) {
-    println!("retro_set_audio_sample()");
-    unsafe {
-        AUDIO_SAMPLE_CALLBACK = callback;
-    }
-}
-
-#[no_mangle]
-pub extern "C" fn retro_set_audio_sample_batch(callback: Option<extern "C" fn(*const i16, usize)>) {
-    println!("retro_set_audio_sample_batch()");
-    unsafe {
-        AUDIO_SAMPLE_BATCH_CALLBACK = callback;
-    }
-}
-
-#[no_mangle]
-pub extern "C" fn retro_set_input_poll(callback: Option<extern "C" fn()>) {
-    println!("retro_set_input_poll()");
-    unsafe {
-        INPUT_POLL_CALLBACK = callback;
-    }
-}
-
-#[no_mangle]
-pub extern "C" fn retro_set_input_state(
-    callback: Option<extern "C" fn(port: u32, device: u32, index: u32, id: u32) -> i16>,
-) {
-    println!("retro_set_input_state()");
-    unsafe {
-        INPUT_STATE_CALLBACK = callback;
-    }
-}
-
-#[no_mangle]
-pub extern "C" fn retro_load_game_special(
-    _system: u32,
-    _info: *const RetroGameInfo,
-    _num_info: usize,
-) -> bool {
-    println!("retro_load_game_special()");
-    false
-}
-
 #[no_mangle]
 pub extern "C" fn retro_set_controller_port_device() {
     println!("retro_set_controller_port_device()");
@@ -271,6 +217,8 @@ pub extern "C" fn retro_run() {
     let key_states = unsafe { KEY_STATES.as_mut().unwrap() };
     let channels = emulator.audio_channels();
 
+    let mut last_frame = emulator.ppu_frame();
+
     let mut counter_cycles = 0_u32;
     let cycle_limit = (GameBoy::CPU_FREQ as f32 * emulator.multiplier() as f32
         / GameBoy::VISUAL_FREQ)
@@ -289,6 +237,25 @@ pub extern "C" fn retro_run() {
         // and any other frequency based component of the system
         counter_cycles += emulator.clock() as u32;
 
+        // in case a new frame is available in the emulator
+        // then the frame must be pushed into display
+        if emulator.ppu_frame() != last_frame {
+            let frame_buffer = emulator.frame_buffer_rgb1555();
+            unsafe {
+                FRAME_BUFFER.copy_from_slice(&frame_buffer);
+                video_refresh_cb(
+                    FRAME_BUFFER.as_ptr(),
+                    DISPLAY_WIDTH as u32,
+                    DISPLAY_HEIGHT as u32,
+                    DISPLAY_WIDTH * RGB1555_SIZE,
+                );
+            }
+
+            // obtains the index of the current PPU frame, this value
+            // is going to be used to detect for new frame presence
+            last_frame = emulator.ppu_frame();
+        }
+
         // obtains the audio buffer reference and queues it
         // in a batch manner using the audio callback at the
         // the end of the operation clears the buffer
@@ -320,17 +287,6 @@ pub extern "C" fn retro_run() {
         }
         key_states.insert(key, current);
     }
-
-    let frame_buffer = emulator.frame_buffer_rgb1555();
-    unsafe {
-        FRAME_BUFFER.copy_from_slice(&frame_buffer);
-        video_refresh_cb(
-            FRAME_BUFFER.as_ptr(),
-            DISPLAY_WIDTH as u32,
-            DISPLAY_HEIGHT as u32,
-            DISPLAY_WIDTH * RGB1555_SIZE,
-        );
-    }
 }
 
 #[no_mangle]
@@ -356,6 +312,16 @@ pub unsafe extern "C" fn retro_load_game(game: *const RetroGameInfo) -> bool {
     true
 }
 
+#[no_mangle]
+pub extern "C" fn retro_load_game_special(
+    _system: u32,
+    _info: *const RetroGameInfo,
+    _num_info: usize,
+) -> bool {
+    println!("retro_load_game_special()");
+    false
+}
+
 #[no_mangle]
 pub extern "C" fn retro_unload_game() {
     println!("retro_unload_game()");
@@ -398,6 +364,50 @@ pub extern "C" fn retro_cheat_set() {
     println!("retro_cheat_set()");
 }
 
+#[no_mangle]
+pub extern "C" fn retro_set_video_refresh(
+    callback: Option<extern "C" fn(*const u8, c_uint, c_uint, usize)>,
+) {
+    println!("retro_set_video_refresh()");
+    unsafe {
+        VIDEO_REFRESH_CALLBACK = callback;
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn retro_set_audio_sample(callback: Option<extern "C" fn(i16, i16)>) {
+    println!("retro_set_audio_sample()");
+    unsafe {
+        AUDIO_SAMPLE_CALLBACK = callback;
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn retro_set_audio_sample_batch(callback: Option<extern "C" fn(*const i16, usize)>) {
+    println!("retro_set_audio_sample_batch()");
+    unsafe {
+        AUDIO_SAMPLE_BATCH_CALLBACK = callback;
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn retro_set_input_poll(callback: Option<extern "C" fn()>) {
+    println!("retro_set_input_poll()");
+    unsafe {
+        INPUT_POLL_CALLBACK = callback;
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn retro_set_input_state(
+    callback: Option<extern "C" fn(port: u32, device: u32, index: u32, id: u32) -> i16>,
+) {
+    println!("retro_set_input_state()");
+    unsafe {
+        INPUT_STATE_CALLBACK = callback;
+    }
+}
+
 fn retro_key_to_pad(retro_key: RetroJoypad) -> Option<PadKey> {
     match retro_key {
         RetroJoypad::RetroDeviceIdJoypadUp => Some(PadKey::Up),
diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs
index 01e3e2cb..1faf3570 100644
--- a/frontends/sdl/src/main.rs
+++ b/frontends/sdl/src/main.rs
@@ -423,11 +423,12 @@ impl Emulator {
 
             if current_time >= self.next_tick_time_i {
                 // re-starts the counter cycles with the number of pending cycles
-                // from the previous tick and the last frame with a dummy value
-                // meant to be overridden in case there's at least one new frame
+                // from the previous tick and the last frame with the system PPU
+                // frame index to be overridden in case there's at least one new frame
                 // being drawn in the current tick
                 let mut counter_cycles = pending_cycles;
-                let mut last_frame = 0xffffu16;
+                let mut last_frame = self.system.ppu_frame();
+                let mut frame_dirty = false;
 
                 // calculates the number of cycles that are meant to be the target
                 // for the current "tick" operation this is basically the current
@@ -450,11 +451,9 @@ impl Emulator {
                     // and any other frequency based component of the system
                     counter_cycles += self.system.clock() as u32;
 
-                    // in case a V-Blank state has been reached a new frame is available
+                    // in case a new frame is available from the emulator
                     // then the frame must be pushed into SDL for display
-                    if self.system.ppu_mode() == PpuMode::VBlank
-                        && self.system.ppu_frame() != last_frame
-                    {
+                    if self.system.ppu_frame() != last_frame {
                         // obtains the frame buffer of the Game Boy PPU and uses it
                         // to update the stream texture, that will latter be copied
                         // to the canvas
@@ -464,6 +463,7 @@ impl Emulator {
                         // obtains the index of the current PPU frame, this value
                         // is going to be used to detect for new frame presence
                         last_frame = self.system.ppu_frame();
+                        frame_dirty = true;
                     }
 
                     // in case the audio subsystem is enabled, then the audio buffer
@@ -488,7 +488,7 @@ impl Emulator {
                 // this separation between texture creation and canvas flush prevents
                 // resources from being over-used in situations where multiple frames
                 // are generated during the same tick cycle
-                if last_frame != 0xffffu16 {
+                if frame_dirty {
                     // clears the graphics canvas, making sure that no garbage
                     // pixel data remaining in the pixel buffer, not doing this would
                     // create visual glitches in OSs like Mac OS X
diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts
index f36ccc09..5a96259f 100644
--- a/frontends/web/ts/gb.ts
+++ b/frontends/web/ts/gb.ts
@@ -274,7 +274,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         // of cycles coming from the previous tick
         let counterCycles = pending;
 
-        let lastFrame = -1;
+        let lastFrame = this.gameBoy.ppu_frame();
 
         while (true) {
             // limits the number of cycles to the provided
@@ -288,13 +288,9 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
             const tickCycles = this.gameBoy.clock();
             counterCycles += tickCycles;
 
-            // in case the current PPU mode is VBlank and the
-            // frame is different from the previously rendered
-            // one then it's time to update the canvas
-            if (
-                this.gameBoy.ppu_mode() === PpuMode.VBlank &&
-                this.gameBoy.ppu_frame() !== lastFrame
-            ) {
+            // in case the frame is different from the previously
+            // rendered one then it's time to update the canvas
+            if (this.gameBoy.ppu_frame() !== lastFrame) {
                 // updates the reference to the last frame index
                 // to be used for comparison in the next tick
                 lastFrame = this.gameBoy.ppu_frame();
-- 
GitLab