Also fixes frame_budget_ns being left unset when vsync paces presentation without an explicit sleep (present_fn == present), and exposes it as skele_frame_budget_ms for reporting alongside the sleep-based capped path.
294 lines
7.0 KiB
C
294 lines
7.0 KiB
C
#include "client/video.h"
|
|
#include "clock.h"
|
|
#include "fps.h"
|
|
#include "skele.h"
|
|
#include <SDL3/SDL.h>
|
|
#include <stk/stk_log.h>
|
|
|
|
static SDL_Window *window = NULL;
|
|
static SDL_GLContext gl_ctx = NULL;
|
|
static uint16_t vid_w = 0;
|
|
static uint16_t vid_h = 0;
|
|
static uint8_t cur_scale = 1;
|
|
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) {
|
|
frame_budget_ns = 0;
|
|
skele_frame_budget_ms = 0.0f;
|
|
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;
|
|
skele_frame_budget_ms = frame_budget_ns / 1e6f;
|
|
last_present_ns = skele_time_ns();
|
|
present_fn = vsync_active ? present : present_capped;
|
|
}
|
|
|
|
static uint8_t max_scale(uint16_t rw, uint16_t rh)
|
|
{
|
|
SDL_DisplayID display;
|
|
const SDL_DisplayMode *mode;
|
|
uint16_t aw, ah;
|
|
uint8_t scale;
|
|
|
|
display =
|
|
window ? SDL_GetDisplayForWindow(window) : SDL_GetPrimaryDisplay();
|
|
mode = SDL_GetCurrentDisplayMode(display);
|
|
if (!mode)
|
|
return 1;
|
|
|
|
aw = (uint16_t)((mode->w * 3) / 4);
|
|
ah = (uint16_t)((mode->h * 3) / 4);
|
|
scale = 1;
|
|
while ((rw * (scale + 1)) <= aw && (rh * (scale + 1)) <= ah)
|
|
scale++;
|
|
|
|
return scale;
|
|
}
|
|
|
|
static void set_scale(uint8_t scale)
|
|
{
|
|
SDL_DisplayID display;
|
|
cur_scale = scale;
|
|
display = SDL_GetDisplayForWindow(window);
|
|
SDL_SetWindowSize(window, (int)(vid_w * scale), (int)(vid_h * scale));
|
|
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED_DISPLAY(display),
|
|
SDL_WINDOWPOS_CENTERED_DISPLAY(display));
|
|
}
|
|
|
|
static void apply_fullscreen_kind(skele_fullscreen_kind_t kind)
|
|
{
|
|
if (kind == SKELE_FULLSCREEN_EXCLUSIVE) {
|
|
SDL_DisplayID display;
|
|
const SDL_DisplayMode *mode;
|
|
display = SDL_GetDisplayForWindow(window);
|
|
mode = SDL_GetCurrentDisplayMode(display);
|
|
SDL_SetWindowFullscreenMode(window, mode);
|
|
} else {
|
|
SDL_SetWindowFullscreenMode(window, NULL);
|
|
}
|
|
}
|
|
|
|
static void cycle_scale(void)
|
|
{
|
|
uint8_t next;
|
|
if (is_fullscreen)
|
|
return;
|
|
next = cur_scale + 1;
|
|
if (next > max_scale(vid_w, vid_h))
|
|
next = 1;
|
|
set_scale(next);
|
|
}
|
|
|
|
uint8_t skele_video_init(skele_video_config_t cfg)
|
|
{
|
|
SDL_DisplayID display;
|
|
SDL_WindowFlags flags = 0;
|
|
const SDL_DisplayMode *mode;
|
|
uint16_t window_w, window_h, aw, ah;
|
|
|
|
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) {
|
|
stk_log(STK_LOG_ERROR, "video: SDL_Init failed: %s",
|
|
SDL_GetError());
|
|
return SKELE_INIT_FAILURE;
|
|
}
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, GL_MAJOR);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, GL_MINOR);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
|
|
SDL_GL_CONTEXT_PROFILE_CORE);
|
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
|
|
|
flags |= SDL_WINDOW_OPENGL;
|
|
if (cfg.flags & SKELE_VIDEO_FULLSCREEN)
|
|
flags |= SDL_WINDOW_FULLSCREEN;
|
|
if (cfg.flags & SKELE_VIDEO_BORDERLESS)
|
|
flags |= SDL_WINDOW_BORDERLESS;
|
|
if (cfg.flags & SKELE_VIDEO_RESIZABLE)
|
|
flags |= SDL_WINDOW_RESIZABLE;
|
|
if (cfg.flags & SKELE_VIDEO_HIGHDPI)
|
|
flags |= SDL_WINDOW_HIGH_PIXEL_DENSITY;
|
|
|
|
if (cfg.flags & SKELE_VIDEO_FULLSCREEN_EXCLUSIVE)
|
|
last_fullscreen_kind = SKELE_FULLSCREEN_EXCLUSIVE;
|
|
else
|
|
last_fullscreen_kind = SKELE_FULLSCREEN_BORDERLESS;
|
|
|
|
display = SDL_GetPrimaryDisplay();
|
|
vid_w =
|
|
cfg.render_width ? cfg.render_width : SKELE_DEFAULT_RENDER_WIDTH;
|
|
vid_h =
|
|
cfg.render_height ? cfg.render_height : SKELE_DEFAULT_RENDER_HEIGHT;
|
|
|
|
if (cfg.window_width && cfg.window_height) {
|
|
window_w = cfg.window_width;
|
|
window_h = cfg.window_height;
|
|
cur_scale = 1;
|
|
} else {
|
|
mode = SDL_GetCurrentDisplayMode(display);
|
|
if (!mode) {
|
|
stk_log(STK_LOG_ERROR,
|
|
"video: SDL_GetCurrentDisplayMode failed: %s",
|
|
SDL_GetError());
|
|
SDL_Quit();
|
|
return SKELE_INIT_FAILURE;
|
|
}
|
|
|
|
aw = (uint16_t)((mode->w * 3) / 4);
|
|
ah = (uint16_t)((mode->h * 3) / 4);
|
|
cur_scale = 1;
|
|
while ((vid_w * (cur_scale + 1)) <= aw &&
|
|
(vid_h * (cur_scale + 1)) <= ah)
|
|
cur_scale++;
|
|
|
|
window_w = vid_w * cur_scale;
|
|
window_h = vid_h * cur_scale;
|
|
}
|
|
|
|
window = SDL_CreateWindow("skele", window_w, window_h,
|
|
flags | SDL_WINDOW_HIDDEN);
|
|
if (!window) {
|
|
stk_log(STK_LOG_ERROR, "video: SDL_CreateWindow failed: %s",
|
|
SDL_GetError());
|
|
SDL_Quit();
|
|
return SKELE_INIT_FAILURE;
|
|
}
|
|
|
|
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED_DISPLAY(display),
|
|
SDL_WINDOWPOS_CENTERED_DISPLAY(display));
|
|
|
|
if (cfg.flags & SKELE_VIDEO_FULLSCREEN) {
|
|
apply_fullscreen_kind(last_fullscreen_kind);
|
|
is_fullscreen = 1;
|
|
}
|
|
|
|
gl_ctx = SDL_GL_CreateContext(window);
|
|
if (!gl_ctx) {
|
|
stk_log(STK_LOG_ERROR, "video: SDL_GL_CreateContext failed: %s",
|
|
SDL_GetError());
|
|
SDL_DestroyWindow(window);
|
|
SDL_Quit();
|
|
return SKELE_INIT_FAILURE;
|
|
}
|
|
|
|
want_vsync = (cfg.flags & SKELE_VIDEO_VSYNC) ? 1 : 0;
|
|
configured_max_fps = cfg.max_fps;
|
|
apply_present_mode();
|
|
|
|
SDL_ShowWindow(window);
|
|
skele_fps_init();
|
|
return SKELE_INIT_SUCCESS;
|
|
}
|
|
|
|
void skele_video_shutdown(void)
|
|
{
|
|
if (gl_ctx)
|
|
SDL_GL_DestroyContext(gl_ctx);
|
|
if (window)
|
|
SDL_DestroyWindow(window);
|
|
SDL_Quit();
|
|
gl_ctx = NULL;
|
|
window = NULL;
|
|
vid_w = 0;
|
|
vid_h = 0;
|
|
cur_scale = 1;
|
|
is_fullscreen = 0;
|
|
}
|
|
|
|
void skele_video_present(void)
|
|
{
|
|
present_fn();
|
|
skele_fps_frame_mark();
|
|
}
|
|
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)
|
|
{
|
|
if (is_fullscreen) {
|
|
SDL_SetWindowFullscreen(window, false);
|
|
is_fullscreen = 0;
|
|
} else {
|
|
apply_fullscreen_kind(last_fullscreen_kind);
|
|
SDL_SetWindowFullscreen(window, true);
|
|
is_fullscreen = 1;
|
|
}
|
|
}
|
|
|
|
void skele_video_set_fullscreen_kind(skele_fullscreen_kind_t kind)
|
|
{
|
|
last_fullscreen_kind = kind;
|
|
if (is_fullscreen)
|
|
apply_fullscreen_kind(kind);
|
|
}
|
|
|
|
void skele_video_set_mouse_grab(uint8_t grab)
|
|
{
|
|
SDL_SetWindowMouseGrab(window, grab ? true : false);
|
|
SDL_SetWindowRelativeMouseMode(window, grab ? true : false);
|
|
}
|
|
|
|
void skele_video_set_title(const char *title)
|
|
{
|
|
SDL_SetWindowTitle(window, title);
|
|
}
|