From 11ba75f843bb573e43dfe26ba7befede223ddbf2 Mon Sep 17 00:00:00 2001 From: Chris Dixon Date: Tue, 27 Oct 2026 10:47:43 +0000 Subject: [PATCH] libretro: add a Vblank Pacing option (#2562) Without threaded rendering a run ends when a frame is presented, or after 50 ms of emulated time without one. Frontends that pace retro_run by the frame rate instead of blocking on audio, Kodi among them, then get several vblanks per run through loading screens and have to drop the extra audio. With the option on, every run ends at the next vblank. [ci skip] --- core/cfg/option.cpp | 1 + core/cfg/option.h | 1 + core/emulator.cpp | 2 +- core/hw/pvr/Renderer_if.cpp | 2 +- shell/libretro/libretro_core_options.h | 14 ++++++++++++++ shell/libretro/option.cpp | 1 + 6 files changed, 19 insertions(+), 2 deletions(-) diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index 82f3994f6..575622c99 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -110,6 +110,7 @@ Option PixelBufferSize("rend.PixelBufferSize", 512_MB); Option AnisotropicFiltering("rend.AnisotropicFiltering", 1); Option TextureFiltering("rend.TextureFiltering", 0); // Default Option ThreadedRendering("rend.ThreadedRendering", true); +Option VblankPacing(""); Option DupeFrames("rend.DupeFrames", false); Option PerPixelLayers("rend.PerPixelLayers", 32); #ifdef TARGET_UWP diff --git a/core/cfg/option.h b/core/cfg/option.h index 267c90b00..05f948935 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -471,6 +471,7 @@ extern Option PixelBufferSize; extern Option AnisotropicFiltering; extern Option TextureFiltering; // 0: default, 1: force nearest, 2: force linear extern Option ThreadedRendering; +extern Option VblankPacing; extern Option DupeFrames; extern Option NativeDepthInterpolation; extern Option EmulateFramebuffer; diff --git a/core/emulator.cpp b/core/emulator.cpp index 25e685bcc..9026f72af 100644 --- a/core/emulator.cpp +++ b/core/emulator.cpp @@ -1088,7 +1088,7 @@ void Emulator::vblank() EventManager::event(Event::VBlank); runner.execTasks(); // Time out if a frame hasn't been rendered for 50 ms - if (sh4_sched_now64() - startTime <= 50_sh4ms) + if (!config::VblankPacing && sh4_sched_now64() - startTime <= 50_sh4ms) return; if (ggpo::active()) ggpo::endOfFrame(); diff --git a/core/hw/pvr/Renderer_if.cpp b/core/hw/pvr/Renderer_if.cpp index 49c3f720f..3d4c0a47e 100644 --- a/core/hw/pvr/Renderer_if.cpp +++ b/core/hw/pvr/Renderer_if.cpp @@ -268,7 +268,7 @@ private: if (renderer->Present()) { presented = true; - if (!config::ThreadedRendering && !ggpo::active()) + if (!config::ThreadedRendering && !config::VblankPacing && !ggpo::active()) emu.getSh4Executor()->Stop(); #ifdef LIBRETRO retro_rend_present(); diff --git a/shell/libretro/libretro_core_options.h b/shell/libretro/libretro_core_options.h index 2cb2cb222..81c631fbb 100644 --- a/shell/libretro/libretro_core_options.h +++ b/shell/libretro/libretro_core_options.h @@ -644,6 +644,20 @@ struct retro_core_option_v2_definition option_defs_us[] = { }, "enabled", }, + { + CORE_OPTION_NAME "_vblank_pacing", + "Vblank Pacing", + NULL, + "Each frame the frontend runs emulates exactly one vblank, whether or not the game drew. For frontends that pace the core by its frame rate rather than by blocking on audio; without it, loading screens that draw nothing run several vblanks per frame and their sound arrives in bursts. Needs Threaded Rendering disabled.", + NULL, + "performance", + { + { "disabled", NULL }, + { "enabled", NULL }, + { NULL, NULL }, + }, + "disabled", + }, { CORE_OPTION_NAME "_auto_skip_frame", "Auto Skip Frame", diff --git a/shell/libretro/option.cpp b/shell/libretro/option.cpp index 6d317df65..ee041d7fb 100644 --- a/shell/libretro/option.cpp +++ b/shell/libretro/option.cpp @@ -89,6 +89,7 @@ Option IntegerScale(""); Option LinearInterpolation("", true); Option VSync("", true); Option ThreadedRendering(CORE_OPTION_NAME "_threaded_rendering", true); +Option VblankPacing(CORE_OPTION_NAME "_vblank_pacing"); Option AnisotropicFiltering(CORE_OPTION_NAME "_anisotropic_filtering"); Option TextureFiltering(CORE_OPTION_NAME "_texture_filtering"); Option PowerVR2Filter(CORE_OPTION_NAME "_pvr2_filtering");