Files
Valiantsin DzerakhandClaude Opus 5.5 4e90c61c52 Choose the amiibo folder with the host's folder dialog
The Amiibo tab's folder field took no typing: the menu never passes typed characters to Dear ImGui, so "Open" only
re-read "./amiibo". A missing folder said so at the bottom of the right pane, below its scroll.

"Open" now shows the shell's folder dialog on Windows (IFileOpenDialog on a thread of its own, owned by the game
window) and SDL_ShowOpenFolderDialog on the SDL window thread elsewhere; the tab shows the folder as text and names a
missing one inside the empty list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:36:09 +03:00

341 lines
12 KiB
C

// The SDL3 back end of the window (gfx_wsys.h), for hosts without Win32: the window on a thread of its own that also
// runs SDL's event loop, F11 fullscreen (borderless, the desktop's mode), the settings TOTK_WINDOW_SIZE and
// TOTK_FULLSCREEN, the display's refresh rate, focus, the Vulkan surface. The swapchain and the present are
// gfx_present.c's. Keyboard and mouse events go to input_sdl.c and to the menu (ui/menu_sdl.cpp).
//
// SDL wants its video calls on the thread that initialised video, so this thread does them all: the others hand it
// work through run_on_window (a user event and a wait) or post a user event. Video is initialised before the window
// exists: the Vulkan instance asks for SDL's surface extensions first (gfx_window_vk_extensions), the window comes with
// gfx_window_start (wsys_open).
//
// TOTK_WINDOW_SIZE is in pixels. On a display that scales windows (Wayland, HiDPI) the window asks for its pixels in
// the display's units (SDL_GetWindowDisplayScale), and SDL reports mouse positions in those units: input_sdl.c and the
// menu scale them back (gfx_window_sdl_density).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include "cfg.h"
#include "gfx.h"
#include "gfx_wsys.h"
#include "input.h"
#include "input_sdl.h"
#include "ui/menu.h"
#include "vk.h"
#include "platform/platform.h"
static SDL_Window *g_win;
static uint32_t g_ev_base; // SDL_RegisterEvents: the user events below
enum { EV_RUN, EV_SETTINGS, EV_CLOSE, EV_COUNT };
static PlatEvent g_video_ready; // manual-reset: SDL video is up (or failed)
static int g_video_ok;
static PlatOnce g_thread_once = PLAT_ONCE_INIT;
static uint32_t g_window_tid; // the window thread, which runs the video calls
static volatile int32_t g_density_milli = 1000; // pixels per window unit, x1000
// ---- work on the window thread
typedef struct {
void (*fn)(void *arg);
void *arg;
PlatEvent done;
} RunReq;
static void run_on_window(void (*fn)(void *arg), void *arg) {
if (plat_thread_id() == g_window_tid) {
fn(arg);
return;
}
RunReq r = {fn, arg};
plat_event_init(&r.done, 1, 0);
SDL_Event e = {.user = {.type = g_ev_base + EV_RUN, .data1 = &r}};
if (!SDL_PushEvent(&e)) rt_fatal("gfx: cannot reach the window thread (%s)", SDL_GetError());
plat_event_wait(&r.done);
}
static void post(int ev) {
SDL_Event e = {.user = {.type = g_ev_base + (uint32_t)ev}};
SDL_PushEvent(&e);
}
double gfx_window_sdl_density(void) { return __atomic_load_n(&g_density_milli, __ATOMIC_RELAXED) / 1000.0; }
SDL_Window *gfx_window_sdl(void) { return g_win; }
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg) { run_on_window(fn, arg); }
// ---- settings: size and fullscreen
static char g_applied_size[32];
static int g_applied_fs;
static void note_applied(void) {
snprintf(g_applied_size, sizeof g_applied_size, "%s", cfg_value("TOTK_WINDOW_SIZE"));
g_applied_fs = gfx_cfg_fullscreen();
}
static int is_fullscreen(void) { return (SDL_GetWindowFlags(g_win) & SDL_WINDOW_FULLSCREEN) != 0; }
// The display scale as a DPI, 96 at 100% (the menu's text size floor); 96 before the window exists.
unsigned gfx_window_dpi(void) {
float scale = g_win ? SDL_GetWindowDisplayScale(g_win) : 0;
return scale > 0 ? (unsigned)(scale * 96 + 0.5f) : 96;
}
// The window's size in its units for w x h pixels.
static void set_pixel_size(int w, int h) {
float scale = SDL_GetWindowDisplayScale(g_win);
if (!(scale > 0)) scale = 1;
SDL_SetWindowSize(g_win, (int)(w / scale + 0.5f), (int)(h / scale + 0.5f));
}
static void set_fullscreen(int on) {
input_cursor_suspend(1);
SDL_SetWindowFullscreen(g_win, on); // the desktop's mode: borderless over the display
SDL_SyncWindow(g_win);
input_cursor_suspend(0);
}
// Window thread: the window takes the size and fullscreen state of the settings when they changed. A window resized by
// hand keeps its size when another setting changes.
static void apply_settings(void) {
int size_changed = strcmp(cfg_value("TOTK_WINDOW_SIZE"), g_applied_size) != 0, fs = gfx_cfg_fullscreen();
int fs_changed = fs != g_applied_fs;
note_applied();
if (size_changed) {
int w, h;
gfx_cfg_window_size(&w, &h);
if (is_fullscreen()) SDL_SetWindowFullscreen(g_win, false);
set_pixel_size(w, h);
if (is_fullscreen() || (fs && !fs_changed)) SDL_SetWindowFullscreen(g_win, true);
}
if (fs_changed && fs != is_fullscreen()) set_fullscreen(fs);
}
void gfx_window_apply_settings(void) {
if (g_win) post(EV_SETTINGS);
}
void gfx_window_request_close(void) {
if (g_win) post(EV_CLOSE);
}
// ---- the display's refresh rate and size
static void refresh_rate_update(void) {
const SDL_DisplayMode *m = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(g_win));
double hz = 0;
if (m && m->refresh_rate_denominator) hz = (double)m->refresh_rate_numerator / m->refresh_rate_denominator;
else if (m) hz = m->refresh_rate;
gfx_window_set_refresh(hz);
}
int gfx_window_monitor_size(int *w, int *h) {
if (!g_win) return 0;
const SDL_DisplayMode *m = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(g_win));
if (!m) return 0;
*w = (int)(m->w * m->pixel_density + 0.5f);
*h = (int)(m->h * m->pixel_density + 0.5f);
return 1;
}
static void density_update(void) {
float d = SDL_GetWindowPixelDensity(g_win);
__atomic_store_n(&g_density_milli, (int32_t)((d > 0 ? d : 1) * 1000 + 0.5f), __ATOMIC_RELAXED);
}
// ---- the window thread
static void handle(const SDL_Event *e) {
if (e->type == g_ev_base + EV_RUN) {
RunReq *r = e->user.data1;
r->fn(r->arg);
plat_event_set(&r->done);
return;
}
if (e->type == g_ev_base + EV_SETTINGS) {
if (g_win) apply_settings();
return;
}
if (e->type == g_ev_base + EV_CLOSE) gfx_window_closed();
if (!g_win) return;
input_sdl_event(e);
menu_sdl_event(e);
switch (e->type) {
case SDL_EVENT_KEY_DOWN:
if (e->key.scancode == SDL_SCANCODE_F11 && !e->key.repeat) {
set_fullscreen(!is_fullscreen());
cfg_set("TOTK_FULLSCREEN", is_fullscreen() ? "1" : "0"); // the menu shows it; the next start keeps it
cfg_save();
note_applied();
}
break;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_RESTORED:
density_update();
gfx_window_mark_dirty();
break;
case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
case SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED:
density_update();
refresh_rate_update();
gfx_window_mark_dirty();
break;
case SDL_EVENT_WINDOW_FOCUS_GAINED:
case SDL_EVENT_WINDOW_FOCUS_LOST:
gfx_window_focus_changed(e->type == SDL_EVENT_WINDOW_FOCUS_GAINED);
break;
case SDL_EVENT_QUIT:
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
gfx_window_closed();
break;
}
}
static void window_main(void *arg) {
(void)arg;
g_window_tid = plat_thread_id();
SDL_SetHint(SDL_HINT_APP_ID, "totk-recomp"); // Wayland's app id: the desktop file and the taskbar match it
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "0");
g_video_ok = SDL_Init(SDL_INIT_VIDEO);
if (!g_video_ok) rt_log("gfx: SDL video init failed (%s)", SDL_GetError());
// SDL's Vulkan queries need its Vulkan loader, which a window would load at creation: the instance and the device
// are made before the window.
else if (!SDL_Vulkan_LoadLibrary(NULL)) {
rt_log("gfx: SDL cannot load Vulkan (%s)", SDL_GetError());
g_video_ok = 0;
} else {
g_ev_base = SDL_RegisterEvents(EV_COUNT);
rt_log("gfx: SDL %d.%d.%d, video driver %s", SDL_VERSIONNUM_MAJOR(SDL_GetVersion()),
SDL_VERSIONNUM_MINOR(SDL_GetVersion()), SDL_VERSIONNUM_MICRO(SDL_GetVersion()), SDL_GetCurrentVideoDriver());
}
plat_event_set(&g_video_ready);
if (!g_video_ok) return;
for (;;) {
SDL_Event e;
if (SDL_WaitEventTimeout(&e, 100)) handle(&e);
else input_sdl_idle(); // a cursor change requested from another thread meanwhile
}
}
static void video_start(void *arg) {
(void)arg;
plat_event_init(&g_video_ready, 1, 0);
if (!plat_thread_spawn(window_main, NULL, "window", 0)) rt_fatal("gfx: cannot start the window thread");
plat_event_wait(&g_video_ready);
}
static int video_up(void) {
plat_once(&g_thread_once, video_start, NULL);
return g_video_ok;
}
int gfx_window_vk_extensions(const char **out, int max) {
if (!video_up()) return 0;
Uint32 n = 0;
const char *const *ext = SDL_Vulkan_GetInstanceExtensions(&n);
if (!ext) {
rt_log("gfx: SDL_Vulkan_GetInstanceExtensions failed (%s)", SDL_GetError());
return 0;
}
int k = 0;
for (Uint32 i = 0; i < n && k < max; i++) out[k++] = ext[i];
return k;
}
int gfx_window_vk_presentable(VkInstance inst, VkPhysicalDevice pd, uint32_t family) {
return video_up() && SDL_Vulkan_GetPresentationSupport(inst, pd, family);
}
// ---- gfx_wsys.h
static int open_ok;
static void open_window(void *arg) {
(void)arg;
int w, h;
gfx_cfg_window_size(&w, &h);
g_win = SDL_CreateWindow("TotK Recomp", w, h, SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
if (!g_win) {
rt_log("gfx: SDL_CreateWindow failed (%s)", SDL_GetError());
return;
}
set_pixel_size(w, h); // w x h were window units; the setting is in pixels
density_update();
note_applied();
refresh_rate_update();
if (cfg_get("TOTK_BACKGROUND_WINDOW")) rt_log("gfx: TOTK_BACKGROUND_WINDOW has no effect with SDL windows");
if (gfx_cfg_fullscreen()) SDL_SetWindowFullscreen(g_win, true);
SDL_SyncWindow(g_win);
input_sdl_window(g_win);
open_ok = 1;
}
int wsys_open(void) {
if (!video_up()) return 0;
if (!g_win) run_on_window(open_window, NULL);
return open_ok;
}
typedef struct { VkInstance inst; VkSurfaceKHR surface; } SurfaceReq;
static void make_surface(void *arg) {
SurfaceReq *r = arg;
if (!SDL_Vulkan_CreateSurface(g_win, r->inst, NULL, &r->surface)) {
rt_log("gfx: SDL_Vulkan_CreateSurface failed (%s)", SDL_GetError());
r->surface = VK_NULL_HANDLE;
}
}
VkSurfaceKHR wsys_surface(VkInstance inst) {
SurfaceReq r = {inst, VK_NULL_HANDLE};
run_on_window(make_surface, &r);
return r.surface;
}
void wsys_client_size(int *w, int *h) {
*w = *h = 0;
if (g_win) SDL_GetWindowSizeInPixels(g_win, w, h);
}
int wsys_minimized(void) { return g_win && (SDL_GetWindowFlags(g_win) & (SDL_WINDOW_MINIMIZED | SDL_WINDOW_HIDDEN)); }
static void resize_window(void *arg) {
const int *wh = arg;
set_pixel_size(wh[0], wh[1]);
SDL_SyncWindow(g_win);
}
void wsys_resize(int w, int h) {
int wh[2] = {w, h};
run_on_window(resize_window, wh);
}
// ---- --probe-window: synthetic events through the event handler, on the window thread
static void probe_event(void *arg) { handle(arg); }
void wsys_probe_focus(int in) {
SDL_Event e = {.window = {.type = in ? SDL_EVENT_WINDOW_FOCUS_GAINED : SDL_EVENT_WINDOW_FOCUS_LOST,
.windowID = SDL_GetWindowID(g_win)}};
run_on_window(probe_event, &e);
}
void wsys_probe_escape(void) {
SDL_Event e = {.key = {.type = SDL_EVENT_KEY_DOWN, .scancode = SDL_SCANCODE_ESCAPE, .key = SDLK_ESCAPE, .down = true,
.windowID = SDL_GetWindowID(g_win)}};
run_on_window(probe_event, &e);
e.key.type = SDL_EVENT_KEY_UP, e.key.down = false;
run_on_window(probe_event, &e);
}
// SDL reports no activation by a title bar click: only the client click exists.
int wsys_probe_click(int where) {
if (where == 2) return 0;
SDL_Event e = {.button = {.type = SDL_EVENT_MOUSE_BUTTON_DOWN, .button = SDL_BUTTON_LEFT, .down = true,
.windowID = SDL_GetWindowID(g_win)}};
run_on_window(probe_event, &e);
e.button.type = SDL_EVENT_MOUSE_BUTTON_UP, e.button.down = false;
run_on_window(probe_event, &e);
return 1;
}