From c766704267c83dc7bad3ef315b0005e6f65d1070 Mon Sep 17 00:00:00 2001 From: Lior Halphon Date: Fri, 21 Apr 2017 16:00:53 +0300 Subject: [PATCH] More accurate FPS capping that tracks time correctly even when the screen is off. Should also support restarting the LCD during blank to increase FPS to 63. --- Core/display.c | 58 ++++--------------------------------------- Core/gb.c | 1 - Core/gb.h | 11 ++++++--- Core/memory.c | 4 +++ Core/timing.c | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ Core/timing.h | 3 +++ 6 files changed, 87 insertions(+), 57 deletions(-) diff --git a/Core/display.c b/Core/display.c index 0855cb5..a3985d3 100755 --- a/Core/display.c +++ b/Core/display.c @@ -1,14 +1,9 @@ #include #include -#include #include #include #include #include "gb.h" -#ifdef _WIN32 -#define _WIN32_WINNT 0x0500 -#include -#endif /* Each line is 456 cycles, approximately: @@ -198,46 +193,12 @@ static uint32_t get_pixel(GB_gameboy_t *gb, uint8_t x, uint8_t y) return gb->background_palettes_rgb[(attributes & 7) * 4 + background_pixel]; } -static int64_t get_nanoseconds(void) +static void display_vblank(GB_gameboy_t *gb) { -#ifndef _WIN32 - struct timeval now; - gettimeofday(&now, NULL); - return (now.tv_usec) * 1000 + now.tv_sec * 1000000000L; -#else - FILETIME time; - GetSystemTimeAsFileTime(&time); - return (((int64_t)time.dwHighDateTime << 32) | time.dwLowDateTime) * 100L; -#endif -} - -static void nsleep(uint64_t nanoseconds) -{ -#ifndef _WIN32 - struct timespec sleep = {0, nanoseconds}; - nanosleep(&sleep, NULL); -#else - HANDLE timer; - LARGE_INTEGER time; - timer = CreateWaitableTimer(NULL, true, NULL); - time.QuadPart = -(nanoseconds / 100L); - SetWaitableTimer(timer, &time, 0, NULL, NULL, false); - WaitForSingleObject(timer, INFINITE); - CloseHandle(timer); -#endif -} - -// Todo: FPS capping should not be related to vblank, as the display is not always on, and this causes "jumps" -// when switching the display on and off. -void display_vblank(GB_gameboy_t *gb) -{ - /* Called every Gameboy vblank. Does FPS-capping and calls user's vblank callback if Turbo Mode allows. */ - if (gb->turbo && !gb->turbo_dont_skip) { - int64_t nanoseconds = get_nanoseconds(); - if (nanoseconds <= gb->last_vblank + FRAME_LENGTH) { + if (gb->turbo) { + if (GB_timing_sync_turbo(gb)) { return; } - gb->last_vblank = nanoseconds; } if (!gb->disable_rendering && (!(gb->io_registers[GB_IO_LCDC] & 0x80) || gb->stopped)) { @@ -247,16 +208,7 @@ void display_vblank(GB_gameboy_t *gb) } gb->vblank_callback(gb); - if (!gb->turbo) { - int64_t nanoseconds = get_nanoseconds(); - if (labs((signed long)(nanoseconds - gb->last_vblank)) < FRAME_LENGTH ) { - nsleep(FRAME_LENGTH + gb->last_vblank - nanoseconds); - gb->last_vblank += FRAME_LENGTH; - } - else { - gb->last_vblank = nanoseconds; - } - } + GB_timing_sync(gb); gb->vblank_just_occured = true; } @@ -352,7 +304,6 @@ static void update_display_state(GB_gameboy_t *gb, uint8_t cycles) /* Reset window rendering state */ gb->effective_window_enabled = false; gb->effective_window_y = 0xFF; - display_vblank(gb); } /* Entered VBlank state, update STAT and IF */ @@ -365,6 +316,7 @@ static void update_display_state(GB_gameboy_t *gb, uint8_t cycles) if (gb->io_registers[GB_IO_STAT] & 0x20) { gb->stat_interrupt_line = true; } + display_vblank(gb); } /* Handle STAT changes for lines 0-143 */ diff --git a/Core/gb.c b/Core/gb.c index 1497573..8eb280d 100755 --- a/Core/gb.c +++ b/Core/gb.c @@ -574,7 +574,6 @@ void GB_reset(GB_gameboy_t *gb) gb->mbc_rom_bank = 1; gb->last_rtc_second = time(NULL); - gb->last_vblank = clock(); gb->cgb_ram_bank = 1; gb->io_registers[GB_IO_JOYP] = 0xF; gb->io_registers[GB_IO_OBP0] = gb->io_registers[GB_IO_OBP1] = 0xFF; diff --git a/Core/gb.h b/Core/gb.h index c185a48..dd7c426 100644 --- a/Core/gb.h +++ b/Core/gb.h @@ -325,7 +325,7 @@ struct GB_gameboy_internal_s { /* Timing */ GB_SECTION(timing, - int64_t last_vblank; + GB_PADDING(int64_t, last_vblank); uint32_t display_cycles; uint32_t div_cycles; GB_PADDING(uint32_t, tima_cycles); @@ -394,12 +394,16 @@ struct GB_gameboy_internal_s { uint32_t *screen; GB_sample_t *audio_buffer; bool keys[GB_KEY_MAX]; + + /* Timing */ + uint64_t last_sync; + uint64_t cycles_since_last_sync; - /* Audio Specific */ + /* Audio */ unsigned int buffer_size; unsigned int sample_rate; unsigned int audio_position; - bool audio_stream_started; // detects first copy request to minimize lag + bool audio_stream_started; /* detects first copy request to minimize lag */ volatile bool audio_copy_in_progress; volatile bool apu_lock; @@ -416,6 +420,7 @@ struct GB_gameboy_internal_s { GB_rumble_callback_t rumble_callback; GB_serial_transfer_start_callback_t serial_transfer_start_callback; GB_serial_transfer_end_callback_t serial_transfer_end_callback; + /* IR */ long cycles_since_ir_change; long cycles_since_input_ir_change; diff --git a/Core/memory.c b/Core/memory.c index 70635a8..da9f0e5 100644 --- a/Core/memory.c +++ b/Core/memory.c @@ -440,6 +440,10 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) /* Todo: verify this. */ gb->display_cycles = gb->cgb_double_speed? -2 : -4; } + else if (!(value & 0x80) && (gb->io_registers[GB_IO_LCDC] & 0x80)) { + /* Sync after turning off LCD */ + GB_timing_sync(gb); + } gb->io_registers[GB_IO_LCDC] = value; return; diff --git a/Core/timing.c b/Core/timing.c index d08006b..6ace186 100644 --- a/Core/timing.c +++ b/Core/timing.c @@ -1,4 +1,70 @@ #include "gb.h" +#ifdef _WIN32 +#define _WIN32_WINNT 0x0500 +#include +#else +#include +#endif + +static int64_t get_nanoseconds(void) +{ +#ifndef _WIN32 + struct timeval now; + gettimeofday(&now, NULL); + return (now.tv_usec) * 1000 + now.tv_sec * 1000000000L; +#else + FILETIME time; + GetSystemTimeAsFileTime(&time); + return (((int64_t)time.dwHighDateTime << 32) | time.dwLowDateTime) * 100L; +#endif +} + +static void nsleep(uint64_t nanoseconds) +{ +#ifndef _WIN32 + struct timespec sleep = {0, nanoseconds}; + nanosleep(&sleep, NULL); +#else + HANDLE timer; + LARGE_INTEGER time; + timer = CreateWaitableTimer(NULL, true, NULL); + time.QuadPart = -(nanoseconds / 100L); + SetWaitableTimer(timer, &time, 0, NULL, NULL, false); + WaitForSingleObject(timer, INFINITE); + CloseHandle(timer); +#endif +} + +bool GB_timing_sync_turbo(GB_gameboy_t *gb) +{ + if (!gb->turbo_dont_skip) { + int64_t nanoseconds = get_nanoseconds(); + if (nanoseconds <= gb->last_sync + FRAME_LENGTH) { + return true; + } + gb->last_sync = nanoseconds; + } + return false; +} + +void GB_timing_sync(GB_gameboy_t *gb) +{ + if (gb->turbo) return; + /* Prevent syncing if not enough time has passed.*/ + if (gb->cycles_since_last_sync < LCDC_PERIOD / 4) return; + + uint64_t target_nanoseconds = gb->cycles_since_last_sync * FRAME_LENGTH / LCDC_PERIOD; + int64_t nanoseconds = get_nanoseconds(); + if (labs((signed long)(nanoseconds - gb->last_sync)) < target_nanoseconds ) { + nsleep(target_nanoseconds + gb->last_sync - nanoseconds); + gb->last_sync += target_nanoseconds; + } + else { + gb->last_sync = nanoseconds; + } + + gb->cycles_since_last_sync = 0; +} static void GB_ir_run(GB_gameboy_t *gb) { @@ -69,6 +135,7 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) gb->apu.apu_cycles += cycles; gb->cycles_since_ir_change += cycles; gb->cycles_since_input_ir_change += cycles; + gb->cycles_since_last_sync += cycles; GB_dma_run(gb); GB_hdma_run(gb); GB_apu_run(gb); diff --git a/Core/timing.h b/Core/timing.h index bd3b2f2..ed9e15a 100644 --- a/Core/timing.h +++ b/Core/timing.h @@ -7,6 +7,9 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles); void GB_set_internal_div_counter(GB_gameboy_t *gb, uint32_t value); void GB_rtc_run(GB_gameboy_t *gb); void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac); +bool GB_timing_sync_turbo(GB_gameboy_t *gb); /* Returns true if should skip frame */ +void GB_timing_sync(GB_gameboy_t *gb); + enum { GB_TIMA_RUNNING = 0,