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
+2 -1
View File
@@ -30,7 +30,8 @@ int main(int argc, char *argv[])
video_cfg.render_height = SKELE_DEFAULT_RENDER_HEIGHT;
video_cfg.window_width = 0;
video_cfg.window_height = 0;
video_cfg.flags = 0;
video_cfg.max_fps = 0;
video_cfg.flags = SKELE_VIDEO_VSYNC;
if (skele_video_init(video_cfg) != SKELE_INIT_SUCCESS) {
skele_stk_teardown();
+2 -1
View File
@@ -29,7 +29,8 @@ int main(int argc, char *argv[])
video_cfg.render_height = SKELE_DEFAULT_RENDER_HEIGHT;
video_cfg.window_width = 0;
video_cfg.window_height = 0;
video_cfg.flags = 0;
video_cfg.max_fps = 0;
video_cfg.flags = SKELE_VIDEO_VSYNC;
if (skele_video_init(video_cfg) != SKELE_INIT_SUCCESS) {
skele_stk_teardown();
+8
View File
@@ -12,10 +12,14 @@ extern "C" {
#define SKELE_VIDEO_RESIZABLE 0x04
#define SKELE_VIDEO_HIGHDPI 0x08
#define SKELE_VIDEO_FULLSCREEN_EXCLUSIVE 0x10
#define SKELE_VIDEO_VSYNC 0x20
#define SKELE_DEFAULT_RENDER_WIDTH 320
#define SKELE_DEFAULT_RENDER_HEIGHT 200
#define SKELE_VIDEO_FPS_UNCAPPED UINT16_MAX
#define SKELE_VIDEO_FPS_FALLBACK 60
typedef enum {
SKELE_FULLSCREEN_BORDERLESS,
SKELE_FULLSCREEN_EXCLUSIVE
@@ -26,12 +30,16 @@ typedef struct {
uint16_t render_height;
uint16_t window_width;
uint16_t window_height;
uint16_t max_fps;
uint8_t flags;
} skele_video_config_t;
uint8_t skele_video_init(skele_video_config_t cfg);
void skele_video_shutdown(void);
void skele_video_present(void);
uint8_t skele_video_using_vsync(void);
void skele_video_set_vsync(uint8_t enabled);
void skele_video_set_max_fps(uint16_t max_fps);
void skele_video_set_title(const char *title);
void skele_video_toggle_fullscreen(void);
void skele_video_set_fullscreen_kind(skele_fullscreen_kind_t kind);
+73 -2
View File
@@ -1,4 +1,5 @@
#include "client/video.h"
#include "clock.h"
#include "skele.h"
#include <SDL3/SDL.h>
#include <stk/stk_log.h>
@@ -12,6 +13,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_GL_SwapWindow(window); }
static void present_capped(void)
{
uint64_t now, elapsed;
SDL_GL_SwapWindow(window);
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_GL_SetSwapInterval(1))
vsync_active = 1;
else {
SDL_GL_SetSwapInterval(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;
@@ -159,7 +214,10 @@ uint8_t skele_video_init(skele_video_config_t cfg)
return SKELE_INIT_FAILURE;
}
SDL_GL_SetSwapInterval(1);
want_vsync = (cfg.flags & SKELE_VIDEO_VSYNC) ? 1 : 0;
configured_max_fps = cfg.max_fps;
apply_present_mode();
SDL_ShowWindow(window);
return SKELE_INIT_SUCCESS;
}
@@ -179,7 +237,20 @@ void skele_video_shutdown(void)
is_fullscreen = 0;
}
void skele_video_present(void) { SDL_GL_SwapWindow(window); }
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)
+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)