feat: add vsync-priority frame pacing to video backends

Present now dispatches through a function pointer chosen at init and
re-chosen by two new runtime setters: real vsync if requested and the
driver grants it, else a manual sleep-based cap (auto-detects display
refresh rate, falls back to 60fps if that can't be queried), else
fully uncapped. skele_video_using_vsync() reports what was actually
granted; skele_video_set_vsync()/skele_video_set_max_fps() let a game
change either live without tearing down the renderer/GL context.
This commit is contained in:
2026-08-23 17:06:33 +02:00
parent ddaeafc14f
commit 55055a042d
5 changed files with 159 additions and 6 deletions
+74 -2
View File
@@ -1,6 +1,7 @@
#include "client/blit.h"
#include "client/palette.h"
#include "client/video.h"
#include "clock.h"
#include "skele.h"
#include <SDL3/SDL.h>
#include <stdint.h>
@@ -21,6 +22,60 @@ static uint8_t is_fullscreen = 0;
static skele_fullscreen_kind_t last_fullscreen_kind =
SKELE_FULLSCREEN_EXCLUSIVE;
static void (*present_fn)(void);
static uint64_t frame_budget_ns = 0;
static uint64_t last_present_ns = 0;
static uint8_t vsync_active = 0;
static uint8_t want_vsync = 0;
static uint16_t configured_max_fps = 0;
static void present(void) { SDL_RenderPresent(renderer); }
static void present_capped(void)
{
uint64_t now, elapsed;
SDL_RenderPresent(renderer);
now = skele_time_ns();
elapsed = now - last_present_ns;
if (elapsed < frame_budget_ns)
skele_sleep_ns(frame_budget_ns - elapsed);
last_present_ns = skele_time_ns();
}
static void apply_present_mode(void)
{
const SDL_DisplayMode *refresh_mode;
uint16_t cap;
if (want_vsync && SDL_SetRenderVSync(renderer, 1))
vsync_active = 1;
else {
SDL_SetRenderVSync(renderer, 0);
vsync_active = 0;
}
cap = configured_max_fps;
if (cap == SKELE_VIDEO_FPS_UNCAPPED || (vsync_active && !cap)) {
present_fn = present;
return;
}
if (!cap) {
refresh_mode = SDL_GetCurrentDisplayMode(
SDL_GetDisplayForWindow(window));
cap = (refresh_mode && refresh_mode->refresh_rate > 0)
? (uint16_t)refresh_mode->refresh_rate
: SKELE_VIDEO_FPS_FALLBACK;
}
frame_budget_ns = 1000000000ULL / cap;
last_present_ns = skele_time_ns();
present_fn = present_capped;
}
static uint8_t max_scale(uint16_t rw, uint16_t rh)
{
SDL_DisplayID display;
@@ -181,7 +236,11 @@ uint8_t skele_video_init(skele_video_config_t cfg)
}
SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST);
SDL_SetRenderVSync(renderer, 1);
want_vsync = (cfg.flags & SKELE_VIDEO_VSYNC) ? 1 : 0;
configured_max_fps = cfg.max_fps;
apply_present_mode();
memset(palette, 0, sizeof(palette));
SDL_ShowWindow(window);
return SKELE_INIT_SUCCESS;
@@ -223,7 +282,20 @@ void skele_video_blit(uint8_t *pixels)
SDL_RenderTexture(renderer, texture, NULL, NULL);
}
void skele_video_present(void) { SDL_RenderPresent(renderer); }
void skele_video_present(void) { present_fn(); }
uint8_t skele_video_using_vsync(void) { return vsync_active; }
void skele_video_set_vsync(uint8_t enabled)
{
want_vsync = enabled;
apply_present_mode();
}
void skele_video_set_max_fps(uint16_t max_fps)
{
configured_max_fps = max_fps;
apply_present_mode();
}
void skele_video_cycle_scale(void) { cycle_scale(); }
void skele_video_toggle_fullscreen(void)