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.
This commit is contained in:
parent
fb55c35f87
commit
c766704267
@ -1,14 +1,9 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/time.h>
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "gb.h"
|
#include "gb.h"
|
||||||
#ifdef _WIN32
|
|
||||||
#define _WIN32_WINNT 0x0500
|
|
||||||
#include <Windows.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Each line is 456 cycles, approximately:
|
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];
|
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
|
if (gb->turbo) {
|
||||||
struct timeval now;
|
if (GB_timing_sync_turbo(gb)) {
|
||||||
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) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gb->last_vblank = nanoseconds;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gb->disable_rendering && (!(gb->io_registers[GB_IO_LCDC] & 0x80) || gb->stopped)) {
|
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);
|
gb->vblank_callback(gb);
|
||||||
if (!gb->turbo) {
|
GB_timing_sync(gb);
|
||||||
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->vblank_just_occured = true;
|
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 */
|
/* Reset window rendering state */
|
||||||
gb->effective_window_enabled = false;
|
gb->effective_window_enabled = false;
|
||||||
gb->effective_window_y = 0xFF;
|
gb->effective_window_y = 0xFF;
|
||||||
display_vblank(gb);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Entered VBlank state, update STAT and IF */
|
/* 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) {
|
if (gb->io_registers[GB_IO_STAT] & 0x20) {
|
||||||
gb->stat_interrupt_line = true;
|
gb->stat_interrupt_line = true;
|
||||||
}
|
}
|
||||||
|
display_vblank(gb);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle STAT changes for lines 0-143 */
|
/* Handle STAT changes for lines 0-143 */
|
||||||
|
@ -574,7 +574,6 @@ void GB_reset(GB_gameboy_t *gb)
|
|||||||
|
|
||||||
gb->mbc_rom_bank = 1;
|
gb->mbc_rom_bank = 1;
|
||||||
gb->last_rtc_second = time(NULL);
|
gb->last_rtc_second = time(NULL);
|
||||||
gb->last_vblank = clock();
|
|
||||||
gb->cgb_ram_bank = 1;
|
gb->cgb_ram_bank = 1;
|
||||||
gb->io_registers[GB_IO_JOYP] = 0xF;
|
gb->io_registers[GB_IO_JOYP] = 0xF;
|
||||||
gb->io_registers[GB_IO_OBP0] = gb->io_registers[GB_IO_OBP1] = 0xFF;
|
gb->io_registers[GB_IO_OBP0] = gb->io_registers[GB_IO_OBP1] = 0xFF;
|
||||||
|
11
Core/gb.h
11
Core/gb.h
@ -325,7 +325,7 @@ struct GB_gameboy_internal_s {
|
|||||||
|
|
||||||
/* Timing */
|
/* Timing */
|
||||||
GB_SECTION(timing,
|
GB_SECTION(timing,
|
||||||
int64_t last_vblank;
|
GB_PADDING(int64_t, last_vblank);
|
||||||
uint32_t display_cycles;
|
uint32_t display_cycles;
|
||||||
uint32_t div_cycles;
|
uint32_t div_cycles;
|
||||||
GB_PADDING(uint32_t, tima_cycles);
|
GB_PADDING(uint32_t, tima_cycles);
|
||||||
@ -395,11 +395,15 @@ struct GB_gameboy_internal_s {
|
|||||||
GB_sample_t *audio_buffer;
|
GB_sample_t *audio_buffer;
|
||||||
bool keys[GB_KEY_MAX];
|
bool keys[GB_KEY_MAX];
|
||||||
|
|
||||||
/* Audio Specific */
|
/* Timing */
|
||||||
|
uint64_t last_sync;
|
||||||
|
uint64_t cycles_since_last_sync;
|
||||||
|
|
||||||
|
/* Audio */
|
||||||
unsigned int buffer_size;
|
unsigned int buffer_size;
|
||||||
unsigned int sample_rate;
|
unsigned int sample_rate;
|
||||||
unsigned int audio_position;
|
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 audio_copy_in_progress;
|
||||||
volatile bool apu_lock;
|
volatile bool apu_lock;
|
||||||
|
|
||||||
@ -416,6 +420,7 @@ struct GB_gameboy_internal_s {
|
|||||||
GB_rumble_callback_t rumble_callback;
|
GB_rumble_callback_t rumble_callback;
|
||||||
GB_serial_transfer_start_callback_t serial_transfer_start_callback;
|
GB_serial_transfer_start_callback_t serial_transfer_start_callback;
|
||||||
GB_serial_transfer_end_callback_t serial_transfer_end_callback;
|
GB_serial_transfer_end_callback_t serial_transfer_end_callback;
|
||||||
|
|
||||||
/* IR */
|
/* IR */
|
||||||
long cycles_since_ir_change;
|
long cycles_since_ir_change;
|
||||||
long cycles_since_input_ir_change;
|
long cycles_since_input_ir_change;
|
||||||
|
@ -440,6 +440,10 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
|
|||||||
/* Todo: verify this. */
|
/* Todo: verify this. */
|
||||||
gb->display_cycles = gb->cgb_double_speed? -2 : -4;
|
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;
|
gb->io_registers[GB_IO_LCDC] = value;
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -1,4 +1,70 @@
|
|||||||
#include "gb.h"
|
#include "gb.h"
|
||||||
|
#ifdef _WIN32
|
||||||
|
#define _WIN32_WINNT 0x0500
|
||||||
|
#include <Windows.h>
|
||||||
|
#else
|
||||||
|
#include <sys/time.h>
|
||||||
|
#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)
|
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->apu.apu_cycles += cycles;
|
||||||
gb->cycles_since_ir_change += cycles;
|
gb->cycles_since_ir_change += cycles;
|
||||||
gb->cycles_since_input_ir_change += cycles;
|
gb->cycles_since_input_ir_change += cycles;
|
||||||
|
gb->cycles_since_last_sync += cycles;
|
||||||
GB_dma_run(gb);
|
GB_dma_run(gb);
|
||||||
GB_hdma_run(gb);
|
GB_hdma_run(gb);
|
||||||
GB_apu_run(gb);
|
GB_apu_run(gb);
|
||||||
|
@ -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_set_internal_div_counter(GB_gameboy_t *gb, uint32_t value);
|
||||||
void GB_rtc_run(GB_gameboy_t *gb);
|
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);
|
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 {
|
enum {
|
||||||
GB_TIMA_RUNNING = 0,
|
GB_TIMA_RUNNING = 0,
|
||||||
|
Loading…
Reference in New Issue
Block a user