Files
goldeneye-pc-port/port/fast3d/gfx_sdl2.cpp
T
James Dansereau 70bc9d65b5 D250: getenv() caching fix -- root cause of the Dam fps drop (plus sysSleep unit fix, GE_D250 probe, perf.ps1) (#75)
* D250: fix sysSleep unit-conversion bug in frame-pacing; add GE_D250 lvlRender probe

port/fast3d/gfx_sdl2.cpp sync_framerate_with_timer() computed the
remaining frame budget in 100ns units (qpc_to_100ns) and passed it
straight into sysSleep(), which takes microseconds -- a real 10x
oversleep whenever that branch fires. Genuine bug, but only active
when target_fps is nonzero (tied to Input/Video FpsCap); confirmed via
live testing that it is NOT the active cause of the user's reported
~45-50fps with FpsCap=0/VSync=0, where this pacing path never runs.

Added GE_D250 (src/boss.c, #ifdef PORT) to isolate lvlRender()'s
DL-building cost (culling/animation/scene traversal) from gfx_run's
already-measured software-RSP interpretation cost. Live test on Dam
shows lvlRender averaging ~0.5-1.5ms -- also not the bottleneck.

Documented in docs/dev/GE-ENV-PROBES.md (GE_D250 row). findings.md
D250 update to follow after rebase.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog

* docs(D250): live A/B session -- MSAA/aniso/FOV/resolution/vsync all ruled out

Live session with the user driving the game directly, confirmed the
sim-healthy/render-sagging pattern from earlier idle testing reproduces
under real play on Dam. One-at-a-time A/B via the F10 overlay ruled out
MSAA, anisotropy, FOV, resolution (1280x960 fullscreen vs 640x480
windowed), and VSync (render never exceeds ~60 even uncapped) as the
cause -- the cost is resolution/GPU-setting-independent.

gfx_run (~6-10ms) + lvlRender (~0.5-1.5ms, via the new GE_D250 probe)
together don't account for the real ~20-22ms/frame. Leading remaining
suspect: the emulated N64 thread-handoff/scheduler round-trip
(gesched.c/sched.c) -- real OS context-switch/wake latency standing in
for what was free hardware handoff on N64. Needs a real profiler
(Visual Studio / WPR) attached during a live bad-fps moment to confirm;
ad-hoc per-call timers have now ruled out the two most obvious CPU call
sites without finding the cost.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog

* D250: fix uncached getenv() in fast3d hot paths -- root cause of the fps drop

A live WPR/xperf CPU trace (tools_pc/perf.ps1, new sister script to
debug.ps1) with Microsoft public symbols resolved found the dominant
render thread spending only 5.2% of its CPU time in the game's own
code -- 43% was in msvcrt.dll, and getenv()/_getenv_helper_nolock
alone accounted for ~50% of the thread's total CPU time.

Root cause: port/fast3d/gfx_pc.cpp's import_texture() (runs on every
texture bind) and several per-triangle/per-G_SETCOMBINE debug-probe
checks (GE_DTEX, GE_TEXDUMP, GE_TEXRAW, GE_D229 x2, GE_D172 x2, plus
GE_TEXDUMP/GE_PCDUMP in gfx_opengl.cpp) called getenv() uncached on
every call, unlike the safe static-cached pattern already used
elsewhere in this codebase. On a texture-heavy scene that's tens of
thousands of locked, locale-aware Windows CRT environment lookups per
second -- invisible to both of the timers added earlier this session
(gfx_run's own cost measurement and the new GE_D250 lvlRender probe),
since neither isolates CRT/library time from the game's own code.

Fix: cache every getenv() site in gfx_pc.cpp/gfx_opengl.cpp with the
established static-int pattern. Narrow, mechanical, no logic change.

Verified: the exact idle-Dam test that was pinned at 45-50fps before
the fix now holds a rock-steady 60-61fps, zero dips. Also explains why
the earlier MSAA/aniso/FOV/resolution/vsync A/B pass all showed "no
change" -- the getenv() cost was a large per-call constant that
swamped any real, smaller resolution-dependent GPU cost at every
setting tried.

Residual, separate issue found once this was fixed: still runs badly
at 1440p+ (640x480 now holds a clean 60fps) -- likely the real GPU
fill-rate cost the getenv() bug was masking, now exposed on its own.
Not yet investigated.

docs/dev/findings.md D250 updated with the full root-cause writeup.
docs/porting-notes.md gets a new lesson under section E (cache
getenv() in any hot-path probe). tools_pc/perf.ps1 automates the
trace-and-analyze workflow for next time.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog

* docs(D250): correct the 1440p+ follow-up -- it was a stale-build false alarm

User re-tested 1440p/4K on the actual getenv-fix build and confirmed
it holds up fine too. The "still runs badly at 1440p+" report was
from before the fix was live, same stale-build class as the DLL-load
false alarm earlier in the session. D250 is fully resolved across all
resolutions tested, not just 640x480.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog

* docs: add missing GE_D219RAW row to GE-ENV-PROBES.md (CI validate fix)

Pre-existing gap unrelated to this branch's own changes -- gen_env_probes.py
--check caught it now since it runs on every PR. GE_D219RAW has had live
getenv() sites in src/game/image.c since the D219 investigation but was
never added to the probe doc.
2026-09-13 09:33:36 -04:00

562 lines
20 KiB
C++

#include <stdio.h>
#include <SDL.h>
#include <time.h>
#include "platform.h"
#include "system.h"
#include "gfx_window_manager_api.h"
#include "gfx_screen_config.h"
#include "input.h"
static SDL_Window* wnd;
static SDL_GLContext ctx;
static SDL_Renderer* renderer;
static int sdl_to_lus_table[512];
static bool vsync_enabled = true;
// OTRTODO: These are redundant. Info can be queried from SDL.
static int window_width = DESIRED_SCREEN_WIDTH;
static int window_height = DESIRED_SCREEN_HEIGHT;
static uint32_t fullscreen_flag = SDL_WINDOW_FULLSCREEN_DESKTOP;
static bool fullscreen_state;
static bool maximized_state;
static bool is_running = true;
static void (*on_fullscreen_changed_callback)(bool is_now_fullscreen);
// GE port: frame pacing is owned by the cooperative thread kernel's vsync
// tick (libultra.c posts one VI retrace per frame at the console rate), so
// fast3d's internal busy-wait sync is disabled to avoid double-pacing. The
// GL swap still honors display vsync (vsync_enabled below).
static int target_fps = 0;
static uint64_t previous_time;
static uint64_t qpc_freq;
#define FRAME_INTERVAL_US_NUMERATOR 1000000
#define FRAME_INTERVAL_US_DENOMINATOR (target_fps)
static int32_t gfx_sdl_get_maximized_state(void) {
return (int32_t)maximized_state;
}
static int32_t gfx_sdl_get_fullscreen_state(void) {
return (int32_t)fullscreen_state;
}
static int32_t gfx_sdl_get_fullscreen_flag_mode(void) {
return fullscreen_flag == SDL_WINDOW_FULLSCREEN_DESKTOP ? 0 : 1;
}
static void gfx_sdl_set_fullscreen_flag(int32_t mode) {
switch (mode) {
case 0: {
fullscreen_flag = SDL_WINDOW_FULLSCREEN_DESKTOP;
} break;
case 1: {
fullscreen_flag = SDL_WINDOW_FULLSCREEN;
} break;
}
}
static void set_fullscreen(bool on, bool call_callback) {
fullscreen_state = on;
SDL_SetWindowFullscreen(wnd, on ? fullscreen_flag : 0);
if (call_callback && on_fullscreen_changed_callback) {
on_fullscreen_changed_callback(on);
}
}
static void set_maximize_window(bool on) {
maximized_state = on;
if (on) {
SDL_MaximizeWindow(wnd);
} else {
SDL_RestoreWindow (wnd);
}
}
static void gfx_sdl_get_active_window_refresh_rate(uint32_t* refresh_rate) {
int display_in_use = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode mode;
SDL_GetCurrentDisplayMode(display_in_use, &mode);
*refresh_rate = mode.refresh_rate;
}
static void gfx_sdl_init(const struct GfxWindowInitSettings *set) {
window_width = set->width;
window_height = set->height;
#ifdef SDL_HINT_VIDEO_HIGHDPI_DISABLED
if (!set->allow_hidpi) {
// HiDPI control, if available
SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, "1");
#if defined(PLATFORM_WIN32) && defined(SDL_HINT_WINDOWS_DPI_AWARENESS)
// if HiDPI is disabled, declare ourselves DPI aware to get 1:1 window size on Windows
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitor");
#endif
}
#endif
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
sysFatalError("Could not init SDL:\n%s", SDL_GetError());
}
// Auto window size: a width/height <= 0 means "pick a sensible default from
// the desktop" rather than the 640x480 native res. Fit a 4:3 window into
// ~85% of the primary desktop -- big enough to actually judge mouse/menu
// behaviour, and 4:3 so the pointer maps onto the render with no letterbox.
if (window_width <= 0 || window_height <= 0) {
SDL_DisplayMode dm;
if (SDL_GetDesktopDisplayMode(0, &dm) == 0 && dm.w > 0 && dm.h > 0) {
int maxw = (int)(dm.w * 0.85);
int maxh = (int)(dm.h * 0.85);
if (maxw * 3 > maxh * 4) { window_height = maxh; window_width = maxh * 4 / 3; }
else { window_width = maxw; window_height = maxw * 3 / 4; }
}
if (window_width < 640 || window_height < 480) { window_width = 640; window_height = 480; }
sysLogPrintf(LOG_INFO, "SDL: auto window size %dx%d", window_width, window_height);
}
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
if (sysArgCheck("--debug-gl")) {
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
}
int posX = set->x;
int posY = set->y;
int display_in_use = SDL_GetWindowDisplayIndex(wnd);
if (display_in_use < 0) { // Fallback to default if out of bounds
posX = SDL_WINDOWPOS_UNDEFINED;
posY = SDL_WINDOWPOS_UNDEFINED;
}
if (set->centered) {
SDL_DisplayMode mode = {};
SDL_GetCurrentDisplayMode(0, &mode);
posX = mode.w / 2 - window_width / 2;
posY = mode.h / 2 - window_height / 2;
}
if (set->fullscreen_is_exclusive) {
fullscreen_flag = SDL_WINDOW_FULLSCREEN;
}
// we will unhide the window once the GL context is successfully created
Uint32 flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL;
// if fullscreen was requested, start the window in fullscreen right away
if (set->fullscreen) {
flags |= fullscreen_flag;
fullscreen_state = true;
}
if (set->maximized) {
flags |= SDL_WINDOW_MAXIMIZED;
maximized_state = true;
}
#ifdef SDL_WINDOW_ALLOW_HIGHDPI
if (set->allow_hidpi) {
flags |= SDL_WINDOW_ALLOW_HIGHDPI;
}
#endif
// ideally we need 3.0 compat
// if that doesn't work, try 3.2 core in case we're on mac, 2.1 compat as a last resort
static u32 glver[][3] = {
{ 0, 0, 0 }, // for command line override
{ 3, 0, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY }, // 3.0: default, has all the features required
{ 4, 1, SDL_GL_CONTEXT_PROFILE_CORE }, // 4.1core: macs only have core profile and this is the latest
{ 3, 2, SDL_GL_CONTEXT_PROFILE_CORE }, // 3.2core: older macs will only have this at best
{ 3, 0, SDL_GL_CONTEXT_PROFILE_ES }, // es3: don't really support ES properly, but we can try
{ 2, 1, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY }, // 2.1: absolute last resort, will still require GLSL130 as an extension
};
u32 verstart = 1;
const u32 verend = sizeof(glver) / sizeof(*glver);
const char *verstr = sysArgGetString("--gl-version");
if (verstr && *verstr) {
// user override
glver[0][2] = strstr(verstr, "core") ? SDL_GL_CONTEXT_PROFILE_CORE :
(strstr(verstr, "es") ? SDL_GL_CONTEXT_PROFILE_ES :
SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
sscanf(verstr, "%d.%d", &glver[0][0], &glver[0][1]);
if (glver[0][0] >= 1 && glver[0][0] <= 4 && glver[0][1] < 9) {
verstart = 0;
}
}
ctx = NULL;
u32 vmin = 0, vmaj = 0, vprof = SDL_GL_CONTEXT_PROFILE_COMPATIBILITY;
const char *vprofstr = "";
for (u32 i = verstart; i < verend && !ctx; ++i) {
vmaj = glver[i][0];
vmin = glver[i][1];
vprof = glver[i][2];
vprofstr = (vprof == SDL_GL_CONTEXT_PROFILE_CORE ? "core" :
(vprof == SDL_GL_CONTEXT_PROFILE_ES ? "es" : ""));
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, vmaj);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, vmin);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, vprof);
wnd = SDL_CreateWindow(set->title, posX, posY, window_width, window_height, flags);
if (!wnd) {
sysLogPrintf(LOG_WARNING, "SDL: could not open SDL window for GL%d.%d%s:\n%s", vmaj, vmin, vprofstr, SDL_GetError());
continue;
}
ctx = SDL_GL_CreateContext(wnd);
if (!ctx) {
sysLogPrintf(LOG_WARNING, "SDL: could not create GL%d.%d%s context: %s", vmaj, vmin, vprofstr, SDL_GetError());
SDL_DestroyWindow(wnd);
wnd = nullptr;
}
}
if (!wnd || !ctx) {
sysFatalError("Could not open SDL window with an OpenGL context of any supported version:\n%s", SDL_GetError());
} else {
sysLogPrintf(LOG_NOTE, "SDL: created GL%d.%d%s context", vmaj, vmin, vprofstr);
}
SDL_GL_MakeCurrent(wnd, ctx);
SDL_GL_SetSwapInterval(1);
SDL_ShowWindow(wnd);
qpc_freq = SDL_GetPerformanceFrequency();
}
static void gfx_sdl_close(void) {
is_running = false;
}
static void gfx_sdl_set_fullscreen_changed_callback(void (*on_fullscreen_changed)(bool is_now_fullscreen)) {
on_fullscreen_changed_callback = on_fullscreen_changed;
}
static void gfx_sdl_set_fullscreen(bool enable) {
set_fullscreen(enable, true);
}
static void gfx_sdl_set_fullscreen_exclusive(bool enable) {
const uint32_t newflag = enable ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_FULLSCREEN_DESKTOP;
if (fullscreen_flag != newflag) {
fullscreen_flag = newflag;
// reset fullscreen to take new value into account if it already is in fullscreen
if (fullscreen_state) {
fullscreen_state = false;
set_fullscreen(enable, true);
}
}
}
static void gfx_sdl_set_maximize_window(bool enable) {
set_maximize_window(enable);
}
static void gfx_sdl_set_cursor_visibility(bool visible) {
if (visible) {
SDL_ShowCursor(SDL_ENABLE);
} else {
SDL_ShowCursor(SDL_DISABLE);
}
}
static void get_centered_positions_native(int32_t width, int32_t height, int32_t *posX, int32_t *posY) {
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode mode = {};
SDL_GetDesktopDisplayMode(disp_idx, &mode);
*posX = mode.w / 2 - width / 2;
*posY = mode.h / 2 - height / 2;
}
static void gfx_sdl_get_centered_positions(int32_t width, int32_t height, int32_t *posX, int32_t *posY) {
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode mode = {};
SDL_GetCurrentDisplayMode(disp_idx, &mode);
*posX = mode.w / 2 - width / 2;
*posY = mode.h / 2 - height / 2;
}
static void gfx_sdl_set_closest_resolution(int32_t width, int32_t height, bool should_center) {
const SDL_DisplayMode mode = {.w = width, .h = height};
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode closest = {};
if (SDL_GetClosestDisplayMode(disp_idx, &mode, &closest)) {
SDL_SetWindowDisplayMode(wnd, &closest);
SDL_SetWindowSize(wnd, closest.w, closest.h);
if (should_center) {
int32_t posX = 0;
int32_t posY = 0;
get_centered_positions_native(closest.w, closest.h, &posX, &posY);
SDL_SetWindowPosition(wnd, posX, posY);
}
}
}
static void gfx_sdl_set_dimensions(uint32_t width, uint32_t height, int32_t posX, int32_t posY) {
SDL_SetWindowSize(wnd, width, height);
SDL_SetWindowPosition(wnd, posX, posY);
}
static void gfx_sdl_get_dimensions(uint32_t* width, uint32_t* height, int32_t* posX, int32_t* posY) {
SDL_GL_GetDrawableSize(wnd, static_cast<int*>((void*)width), static_cast<int*>((void*)height));
SDL_GetWindowPosition(wnd, static_cast<int*>(posX), static_cast<int*>(posY));
}
static void gfx_sdl_handle_events(void) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_KEYDOWN:
if (event.key.keysym.sym == SDLK_RETURN && (event.key.keysym.mod & KMOD_ALT)) {
// alt-enter received, switch fullscreen state
set_fullscreen(!fullscreen_state, true);
} else if (event.key.keysym.sym == SDLK_F4 && (event.key.keysym.mod & KMOD_ALT)) {
// D145: Alt+F4 quits; bare ESC no longer does (it is the
// menu "back" key -- see port/src/video.c / input.c).
exit(0);
} else if (event.key.keysym.sym == SDLK_ESCAPE && !event.key.repeat) {
// WI-1: this render-thread pump and port/src/video.c's
// host-thread pump both drain the same SDL queue, so either
// one can be the loop that dequeues a given ESC keydown.
// Handle it here too or click-to-lock's ESC-to-free races
// (and usually loses to this loop, which runs every frame).
inputReleaseCapture();
}
break;
case SDL_MOUSEBUTTONDOWN:
inputNotifyClick(); // WI-1: (re)lock in click-to-lock mode
break;
case SDL_MOUSEWHEEL:
inputPostWheel(event.wheel.y); // weapon cycle
break;
case SDL_WINDOWEVENT:
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
SDL_GL_GetDrawableSize(wnd, &window_width, &window_height);
if (!fullscreen_state) {
maximized_state = SDL_GetWindowFlags(wnd) & SDL_WINDOW_MAXIMIZED ? true : false;
}
} else if (event.window.event == SDL_WINDOWEVENT_CLOSE &&
event.window.windowID == SDL_GetWindowID(wnd)) {
// We listen specifically for main window close because closing main window
// on macOS does not trigger SDL_Quit.
exit(0);
} else if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
inputSetMouseGrab(0); // free + show cursor on alt-tab (also handled by video.c's pump; whichever dequeues it)
} else if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
inputSetMouseGrab(1);
}
break;
case SDL_QUIT:
exit(0);
break;
}
}
}
/* The window/context are created on the host main thread, but the game's
* scheduler thread (shedThread) does all rendering. Make the context current
* there before each frame. extern "C": called from C (port/src/video.c). */
extern "C" void gfx_sdl_make_context_current(void) {
if (wnd && ctx) {
/* Note: SDL2's MakeCurrent return value is unreliable on this MinGW
* toolchain (ABI artifact), so we don't branch on it. A genuine bind
* failure will surface later as a GL call returning nothing / shader
* compile failure, which is far louder. */
SDL_GL_MakeCurrent(wnd, ctx);
}
}
/* Release the GL context from the calling (host main) thread. WGL only allows
* a context to be current on one thread at a time, and while another thread
* still holds it, wglMakeContextCurrent fails with "The requested resource is
* in use" (SDL2 reports this via SDL_GetError). The host thread must release
* after init so the game's scheduler thread can bind it per frame. */
extern "C" void gfx_sdl_release_context(void) {
if (ctx) {
SDL_GL_MakeCurrent(NULL, NULL);
}
}
/* The host thread owns SDL event pumping (see videoPumpEvents); when it sees
* a window resize it calls this to refresh the cached drawable size that
* fast3d uses for scaling. */
extern "C" void gfx_sdl_update_cached_size(void) {
if (wnd) {
SDL_GL_GetDrawableSize(wnd, &window_width, &window_height);
}
}
static bool gfx_sdl_start_frame(void) {
return true;
}
static uint64_t qpc_to_100ns(uint64_t qpc) {
return qpc / qpc_freq * 10000000 + qpc % qpc_freq * 10000000 / qpc_freq;
}
static inline void sync_framerate_with_timer(void) {
uint64_t t;
t = qpc_to_100ns(SDL_GetPerformanceCounter());
const int64_t next = previous_time + 10 * FRAME_INTERVAL_US_NUMERATOR / FRAME_INTERVAL_US_DENOMINATOR;
int64_t left = next - t;
// We want to exit a bit early, so we can busy-wait the rest to never miss the deadline
left -= 15000UL;
if (left > 0) {
/* D250: `left` is in 100ns units (qpc_to_100ns); sysSleep() takes
* microseconds. Passing it unconverted slept ~10x too long every
* time this branch fired (e.g. an intended 2ms sleep became a real
* ~20ms one), silently eating the rest of the frame budget whenever
* per-frame work left little slack -- a resolution/GPU-independent
* frame-pacing bug, not a real render cost. */
sysSleep((uint32_t)(left / 10));
}
do {
sysCpuRelax();
t = qpc_to_100ns(SDL_GetPerformanceCounter());
} while ((int64_t)t < next);
t = qpc_to_100ns(SDL_GetPerformanceCounter());
if (left > 0 && t - next < 10000) {
// In case it takes some time for the application to wake up after sleep,
// or inaccurate timer,
// don't let that slow down the framerate.
t = next;
}
previous_time = t;
}
/* Optional hook run with the freshly-composited frame still in the back
* buffer, immediately before the swap. video.c uses it for GE_PCDUMP /
* screenshots: reading the back buffer AFTER SDL_GL_SwapWindow is undefined
* on buffer-exchange drivers (Mesa, WSLg) and yields a black capture. */
extern "C" void (*gfx_pre_swap_hook)(void) = NULL;
static void gfx_sdl_swap_buffers_begin(void) {
if (target_fps) {
sync_framerate_with_timer();
}
if (gfx_pre_swap_hook) {
gfx_pre_swap_hook();
}
SDL_GL_SwapWindow(wnd);
}
static void gfx_sdl_swap_buffers_end(void) {
}
static double gfx_sdl_get_time(void) {
return SDL_GetPerformanceCounter() / (double)qpc_freq;
}
static int32_t gfx_sdl_get_target_fps(void) {
return target_fps;
}
static void gfx_sdl_set_target_fps(int fps) {
/* D186: the pacing wait below runs inline on the game's scheduler thread
* (libultra.c osSpTaskStartGo -> gfx_run -> swap_buffers_begin), so a low
* cap stalls VI-retrace delivery and throttles the whole sim, not just
* presentation. Until pacing is moved off that thread, refuse caps low
* enough to be user-visible as a slowdown: treat 1..29 as uncapped. */
if (fps > 0 && fps < 30) {
sysLogPrintf(LOG_WARNING, "gfx_sdl: ignoring FpsCap=%d (< 30 throttles the sim); running uncapped", fps);
fps = 0;
}
target_fps = fps;
}
static bool gfx_sdl_can_disable_vsync(void) {
return true;
}
static void *gfx_sdl_get_window_handle(void) {
return (void *)wnd;
}
static void gfx_sdl_set_window_title(const char *title) {
SDL_SetWindowTitle(wnd, title);
}
static int gfx_sdl_get_swap_interval(void) {
return SDL_GL_GetSwapInterval();
}
static bool gfx_sdl_set_swap_interval(int interval) {
const bool success = SDL_GL_SetSwapInterval(interval) >= 0;
vsync_enabled = success && (interval != 0);
if (!success) {
sysLogPrintf(LOG_WARNING, "SDL: failed to set vsync %d: %s", interval, SDL_GetError());
}
return success;
}
int gfx_sdl_get_display_mode(int modenum, int *out_w, int *out_h) {
const int display_in_use = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode sdlmode;
if (SDL_GetDisplayMode(display_in_use, modenum, &sdlmode) == 0) {
*out_w = sdlmode.w;
*out_h = sdlmode.h;
return 1;
}
return 0;
}
int gfx_sdl_get_current_display_mode(int *out_w, int *out_h) {
const int display_in_use = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode sdlmode;
if (SDL_GetCurrentDisplayMode(display_in_use, &sdlmode) == 0) {
*out_w = sdlmode.w;
*out_h = sdlmode.h;
return 1;
}
return 0;
}
int gfx_sdl_get_num_display_modes(void) {
const int display_in_use = SDL_GetWindowDisplayIndex(wnd);
return SDL_GetNumDisplayModes(display_in_use);
}
struct GfxWindowManagerAPI gfx_sdl = {
gfx_sdl_init,
gfx_sdl_close,
gfx_sdl_get_display_mode,
gfx_sdl_get_current_display_mode,
gfx_sdl_get_num_display_modes,
gfx_sdl_get_fullscreen_state,
gfx_sdl_set_fullscreen_changed_callback,
gfx_sdl_set_fullscreen,
gfx_sdl_set_fullscreen_exclusive,
gfx_sdl_set_fullscreen_flag,
gfx_sdl_get_fullscreen_flag_mode,
gfx_sdl_get_maximized_state,
gfx_sdl_set_maximize_window,
gfx_sdl_get_active_window_refresh_rate,
gfx_sdl_set_cursor_visibility,
gfx_sdl_set_closest_resolution,
gfx_sdl_set_dimensions,
gfx_sdl_get_dimensions,
gfx_sdl_get_centered_positions,
gfx_sdl_handle_events,
gfx_sdl_start_frame,
gfx_sdl_swap_buffers_begin,
gfx_sdl_swap_buffers_end,
gfx_sdl_get_time,
gfx_sdl_get_target_fps,
gfx_sdl_set_target_fps,
gfx_sdl_can_disable_vsync,
gfx_sdl_get_window_handle,
gfx_sdl_set_window_title,
gfx_sdl_get_swap_interval,
gfx_sdl_set_swap_interval,
};