From 55055a042d635a8cfe6fa4016e3cac4fe2693787 Mon Sep 17 00:00:00 2001 From: anth64 Date: Sun, 23 Aug 2026 17:06:33 +0200 Subject: [PATCH] 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. --- example/client/main.c | 3 +- example/gl_client/main.c | 3 +- include/client/video.h | 8 ++++ src/platform/client/video/gl.c | 75 +++++++++++++++++++++++++++++++- src/platform/client/video/sdl.c | 76 ++++++++++++++++++++++++++++++++- 5 files changed, 159 insertions(+), 6 deletions(-) diff --git a/example/client/main.c b/example/client/main.c index bd06235..ac7be64 100644 --- a/example/client/main.c +++ b/example/client/main.c @@ -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(); diff --git a/example/gl_client/main.c b/example/gl_client/main.c index 01df189..a2e2bca 100644 --- a/example/gl_client/main.c +++ b/example/gl_client/main.c @@ -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(); diff --git a/include/client/video.h b/include/client/video.h index 6c02c6e..ddcc5c0 100644 --- a/include/client/video.h +++ b/include/client/video.h @@ -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); diff --git a/src/platform/client/video/gl.c b/src/platform/client/video/gl.c index edb9404..f24a52d 100644 --- a/src/platform/client/video/gl.c +++ b/src/platform/client/video/gl.c @@ -1,4 +1,5 @@ #include "client/video.h" +#include "clock.h" #include "skele.h" #include #include @@ -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) diff --git a/src/platform/client/video/sdl.c b/src/platform/client/video/sdl.c index 282fcf8..1aae316 100644 --- a/src/platform/client/video/sdl.c +++ b/src/platform/client/video/sdl.c @@ -1,6 +1,7 @@ #include "client/blit.h" #include "client/palette.h" #include "client/video.h" +#include "clock.h" #include "skele.h" #include #include @@ -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)