mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
7a786f8e84
CRT Filter, Image Filtering, Enhanced Audio and Rewind become default-off mods (netplay = host_only, outside the plan fingerprint); Internal resolution gains Auto in place of dynamic resolution. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
367 lines
12 KiB
C
367 lines
12 KiB
C
/* host_overlay.c -- see host_overlay.h. */
|
|
#include "host_overlay.h"
|
|
|
|
#include <stdatomic.h>
|
|
#include <stddef.h>
|
|
#include <stdlib.h>
|
|
#if defined(_WIN32)
|
|
#include <windows.h>
|
|
#else
|
|
#include <sched.h>
|
|
#endif
|
|
|
|
/* ---- P1: host pause ------------------------------------------------------ */
|
|
#define PSX_HOST_PAUSE_MAX 8
|
|
static int s_pause_depth = 0;
|
|
static const char *s_pause_reason[PSX_HOST_PAUSE_MAX];
|
|
static PsxHostPauseRefuseFn s_pause_refuse = NULL;
|
|
|
|
int psx_host_pause_push(const char *reason) {
|
|
if (s_pause_refuse && s_pause_refuse()) return 0;
|
|
if (s_pause_depth >= PSX_HOST_PAUSE_MAX) return 0;
|
|
s_pause_reason[s_pause_depth++] = reason ? reason : "host";
|
|
return 1;
|
|
}
|
|
|
|
void psx_host_pause_pop(void) {
|
|
if (s_pause_depth > 0) s_pause_reason[--s_pause_depth] = NULL;
|
|
}
|
|
|
|
int psx_host_pause_depth(void) { return s_pause_depth; }
|
|
|
|
const char *psx_host_pause_reason(void) {
|
|
return s_pause_depth > 0 ? s_pause_reason[s_pause_depth - 1] : NULL;
|
|
}
|
|
|
|
void psx_host_pause_set_refuse_probe(PsxHostPauseRefuseFn fn) { s_pause_refuse = fn; }
|
|
|
|
/* ---- P2: overlay draw callback -------------------------------------------- */
|
|
/* Registered on the main thread, read on the render / present thread. The
|
|
* function and its context are published together as one immutable record
|
|
* behind an atomic pointer, so a reader never pairs a live function with a
|
|
* stale context.
|
|
*
|
|
* Lifetime: every call runs inside an in-flight count. Replacing or removing
|
|
* the callback swaps the record and then waits until no call that could have
|
|
* read the old record is still running, so when psx_host_overlay_set_draw_cb
|
|
* returns, the previous fn will not run again and its ctx (and the GL objects
|
|
* it draws with) may be freed by the caller. The old record is freed there
|
|
* too. A change made from inside the callback itself does not wait (it would
|
|
* wait for itself); that caller's own frame is the only one still using the
|
|
* old ctx and it returns before the next frame reads the new record. */
|
|
typedef struct {
|
|
PsxHostOverlayDrawFn fn;
|
|
void *ctx;
|
|
} PsxHostOverlayDraw;
|
|
|
|
static _Atomic(PsxHostOverlayDraw *) s_draw = NULL;
|
|
static atomic_int s_draw_inflight = 0;
|
|
#if defined(_MSC_VER)
|
|
static __declspec(thread) int s_in_draw = 0;
|
|
#else
|
|
static _Thread_local int s_in_draw = 0;
|
|
#endif
|
|
|
|
static void draw_yield(void) {
|
|
#if defined(_WIN32)
|
|
Sleep(0);
|
|
#else
|
|
sched_yield();
|
|
#endif
|
|
}
|
|
|
|
void psx_host_overlay_set_draw_cb(PsxHostOverlayDrawFn fn, void *ctx) {
|
|
PsxHostOverlayDraw *rec = NULL, *old;
|
|
if (fn) {
|
|
rec = (PsxHostOverlayDraw *)malloc(sizeof(*rec));
|
|
if (!rec) return;
|
|
rec->fn = fn;
|
|
rec->ctx = ctx;
|
|
}
|
|
old = atomic_exchange(&s_draw, rec); /* seq_cst: ordered with the readers' count */
|
|
if (s_in_draw) return; /* from inside the callback: see above; old stays (rare) */
|
|
while (atomic_load(&s_draw_inflight) > 0) draw_yield();
|
|
free(old);
|
|
}
|
|
|
|
int psx_host_overlay_has_draw(void) {
|
|
return atomic_load_explicit(&s_draw, memory_order_acquire) != NULL;
|
|
}
|
|
|
|
void psx_host_overlay_draw(int width, int height) {
|
|
PsxHostOverlayDraw *rec;
|
|
if (width <= 0 || height <= 0) return;
|
|
atomic_fetch_add(&s_draw_inflight, 1); /* seq_cst, before reading the record */
|
|
rec = atomic_load(&s_draw);
|
|
if (rec && rec->fn) {
|
|
s_in_draw = 1;
|
|
rec->fn(width, height, rec->ctx);
|
|
s_in_draw = 0;
|
|
}
|
|
atomic_fetch_sub(&s_draw_inflight, 1);
|
|
}
|
|
|
|
/* ---- Game HUD layer ------------------------------------------------------- */
|
|
static PsxHostOverlayDrawFn s_hud_fn = NULL;
|
|
static void *s_hud_ctx = NULL;
|
|
static atomic_flag s_hud_lock = ATOMIC_FLAG_INIT;
|
|
static atomic_int s_hud_inflight = 0;
|
|
#if defined(_MSC_VER)
|
|
static __declspec(thread) int s_in_hud = 0;
|
|
#else
|
|
static _Thread_local int s_in_hud = 0;
|
|
#endif
|
|
|
|
static void hud_lock(void) {
|
|
while (atomic_flag_test_and_set_explicit(&s_hud_lock, memory_order_acquire))
|
|
draw_yield();
|
|
}
|
|
static void hud_unlock(void) {
|
|
atomic_flag_clear_explicit(&s_hud_lock, memory_order_release);
|
|
}
|
|
|
|
void psx_host_set_hud_draw_cb(PsxHostOverlayDrawFn fn, void *ctx) {
|
|
hud_lock();
|
|
s_hud_ctx = ctx;
|
|
s_hud_fn = fn;
|
|
hud_unlock();
|
|
/* Match the overlay lifetime contract without allocating callback records.
|
|
* The single GL owner may unregister itself; its current call then ends
|
|
* before another frame can consume the replacement. */
|
|
if (!s_in_hud)
|
|
while (atomic_load(&s_hud_inflight) > 0) draw_yield();
|
|
}
|
|
|
|
void psx_host_hud_draw(int width, int height) {
|
|
if (width <= 0 || height <= 0) return;
|
|
hud_lock();
|
|
PsxHostOverlayDrawFn fn = s_hud_fn;
|
|
void *ctx = s_hud_ctx;
|
|
atomic_fetch_add(&s_hud_inflight, 1);
|
|
hud_unlock();
|
|
if (fn) {
|
|
s_in_hud = 1;
|
|
fn(width, height, ctx);
|
|
s_in_hud = 0;
|
|
}
|
|
atomic_fetch_sub(&s_hud_inflight, 1);
|
|
}
|
|
|
|
/* ---- P3: input sink -------------------------------------------------------- */
|
|
static PsxHostInputSinkFn s_sink_fn = NULL;
|
|
static void *s_sink_ctx = NULL;
|
|
static volatile int s_ui_capture = 0;
|
|
|
|
void psx_host_set_input_sink(PsxHostInputSinkFn fn, void *ctx) {
|
|
s_sink_fn = fn;
|
|
s_sink_ctx = ctx;
|
|
}
|
|
|
|
int psx_host_input_sink_dispatch(const void *event) {
|
|
return (s_sink_fn && event) ? (s_sink_fn(event, s_sink_ctx) != 0) : 0;
|
|
}
|
|
|
|
void psx_host_set_ui_capture(int on) { s_ui_capture = on ? 1 : 0; }
|
|
int psx_host_ui_capture_active(void) { return s_ui_capture; }
|
|
|
|
/* ---- P4: live video settings ------------------------------------------------ */
|
|
unsigned psx_host_video_live_changes(const PsxHostVideoSettings *a,
|
|
const PsxHostVideoSettings *b) {
|
|
unsigned m = 0;
|
|
if (!a || !b) return 0;
|
|
if (a->fullscreen != b->fullscreen) m |= PSX_VIDEO_LIVE_FULLSCREEN;
|
|
if (a->frame_generation != b->frame_generation) m |= PSX_VIDEO_LIVE_FRAME_GEN;
|
|
if (a->internal_resolution != b->internal_resolution ||
|
|
a->supersample_milli != b->supersample_milli)
|
|
m |= PSX_VIDEO_LIVE_RESOLUTION;
|
|
return m;
|
|
}
|
|
|
|
unsigned psx_host_video_restart_bits(const PsxHostVideoSettings *boot,
|
|
const PsxHostVideoSettings *next) {
|
|
unsigned m = 0;
|
|
if (!boot || !next) return 0;
|
|
if (boot->renderer != next->renderer) m |= PSX_VIDEO_RESTART_RENDERER;
|
|
/* Internal resolution and supersample apply live; only the legacy
|
|
* integer factor still needs a relaunch. */
|
|
if (boot->supersampling != next->supersampling)
|
|
m |= PSX_VIDEO_RESTART_RESOLUTION;
|
|
if (boot->render_thread != next->render_thread ||
|
|
boot->present_thread != next->present_thread)
|
|
m |= PSX_VIDEO_RESTART_THREADS;
|
|
return m;
|
|
}
|
|
|
|
unsigned psx_host_video_restart_bits_backend(const PsxHostVideoSettings *boot,
|
|
const PsxHostVideoSettings *next,
|
|
int res_live_ok) {
|
|
unsigned m = psx_host_video_restart_bits(boot, next);
|
|
if (boot && next && !res_live_ok &&
|
|
(boot->internal_resolution != next->internal_resolution ||
|
|
boot->supersample_milli != next->supersample_milli))
|
|
m |= PSX_VIDEO_RESTART_RESOLUTION;
|
|
return m;
|
|
}
|
|
|
|
/* ---- seamless relaunch ----------------------------------------------------- */
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* Windows command-line quoting (the rules CommandLineToArgvW and the MSVC CRT
|
|
* parse with): an argument is quoted when empty or holding a space, tab or
|
|
* quote; inside quotes, backslashes are literal except before a quote, so a
|
|
* run of n backslashes before a quote becomes 2n+1 plus the quote, and a run
|
|
* before the closing quote becomes 2n. */
|
|
size_t psx_win_cmdline_quote(const char* arg, char* out, size_t cap) {
|
|
size_t n = 0, i, bs;
|
|
const char* p;
|
|
#define PUT(c) do { if (out && n + 1 < cap) out[n] = (c); ++n; } while (0)
|
|
if (!arg) arg = "";
|
|
if (*arg && !strpbrk(arg, " \t\n\v\"")) {
|
|
for (p = arg; *p; ++p) PUT(*p);
|
|
} else {
|
|
PUT('"');
|
|
for (p = arg;; ++p) {
|
|
bs = 0;
|
|
while (*p == '\\') { ++bs; ++p; }
|
|
if (!*p) { for (i = 0; i < bs * 2; ++i) PUT('\\'); break; }
|
|
if (*p == '"') { for (i = 0; i < bs * 2 + 1; ++i) PUT('\\'); PUT('"'); continue; }
|
|
for (i = 0; i < bs; ++i) PUT('\\');
|
|
PUT(*p);
|
|
}
|
|
PUT('"');
|
|
}
|
|
#undef PUT
|
|
if (out && cap) out[n < cap ? n : cap - 1] = '\0';
|
|
return n;
|
|
}
|
|
|
|
char* psx_win_cmdline_join(int argc, const char* const* argv) {
|
|
size_t total = 1;
|
|
int i;
|
|
char* out;
|
|
char* w;
|
|
for (i = 0; i < argc; ++i) total += psx_win_cmdline_quote(argv[i], NULL, 0) + 1;
|
|
out = (char*)malloc(total);
|
|
if (!out) return NULL;
|
|
w = out;
|
|
for (i = 0; i < argc; ++i) {
|
|
if (i) *w++ = ' ';
|
|
w += psx_win_cmdline_quote(argv[i], w, total - (size_t)(w - out));
|
|
}
|
|
*w = '\0';
|
|
return out;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
#include <stdio.h>
|
|
#include <windows.h>
|
|
#include <shellapi.h>
|
|
/* Re-run this exe with the original arguments. The arguments come from
|
|
* GetCommandLineW (full Unicode, no ANSI code-page loss, any count or length),
|
|
* are re-quoted by the rules above, and the new process is started before
|
|
* this one exits. argc/argv are only a fallback. */
|
|
static wchar_t* utf8_to_wide(const char* s) {
|
|
int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);
|
|
wchar_t* w = n > 0 ? (wchar_t*)malloc(sizeof(wchar_t) * (size_t)n) : NULL;
|
|
if (w && MultiByteToWideChar(CP_UTF8, 0, s, -1, w, n) <= 0) { free(w); w = NULL; }
|
|
return w;
|
|
}
|
|
static char* wide_to_utf8(const wchar_t* s) {
|
|
int n = WideCharToMultiByte(CP_UTF8, 0, s, -1, NULL, 0, NULL, NULL);
|
|
char* u = n > 0 ? (char*)malloc((size_t)n) : NULL;
|
|
if (u && WideCharToMultiByte(CP_UTF8, 0, s, -1, u, n, NULL, NULL) <= 0) { free(u); u = NULL; }
|
|
return u;
|
|
}
|
|
void psx_relaunch_self(int argc, char** argv) {
|
|
int wargc = 0, i, ok = 1;
|
|
wchar_t** wargv = CommandLineToArgvW(GetCommandLineW(), &wargc);
|
|
const char** args = NULL;
|
|
char* line = NULL;
|
|
wchar_t* wline = NULL;
|
|
wchar_t* exe = NULL;
|
|
DWORD cap = MAX_PATH, got = 0;
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
if (wargv && wargc > 0) {
|
|
args = (const char**)calloc((size_t)wargc, sizeof(*args));
|
|
for (i = 0; args && i < wargc; ++i)
|
|
if (!(args[i] = wide_to_utf8(wargv[i]))) ok = 0;
|
|
if (args && ok) line = psx_win_cmdline_join(wargc, args);
|
|
for (i = 0; args && i < wargc; ++i) free((void*)args[i]);
|
|
free((void*)args);
|
|
LocalFree(wargv);
|
|
} else if (argv) {
|
|
line = psx_win_cmdline_join(argc, (const char* const*)argv);
|
|
}
|
|
if (!line) return;
|
|
wline = utf8_to_wide(line);
|
|
free(line);
|
|
for (;;) { /* long paths: grow until the module path fits */
|
|
free(exe);
|
|
exe = (wchar_t*)malloc(sizeof(wchar_t) * cap);
|
|
if (!exe) break;
|
|
got = GetModuleFileNameW(NULL, exe, cap);
|
|
if (got && got < cap) break;
|
|
if (!got || cap >= 32768) { free(exe); exe = NULL; break; }
|
|
cap *= 2;
|
|
}
|
|
if (wline && exe) {
|
|
memset(&si, 0, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
memset(&pi, 0, sizeof(pi));
|
|
fflush(NULL);
|
|
if (CreateProcessW(exe, wline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
free(wline);
|
|
free(exe);
|
|
_exit(0);
|
|
}
|
|
}
|
|
free(wline);
|
|
free(exe);
|
|
}
|
|
#else
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/time.h>
|
|
#include <unistd.h>
|
|
#if defined(__APPLE__)
|
|
#include <mach-o/dyld.h>
|
|
#endif
|
|
void psx_relaunch_self(int argc, char** argv) {
|
|
char path[4096];
|
|
(void)argc;
|
|
if (!argv || !argv[0]) return;
|
|
path[0] = '\0';
|
|
#if defined(__APPLE__)
|
|
{
|
|
uint32_t cap = sizeof(path);
|
|
if (_NSGetExecutablePath(path, &cap) != 0) path[0] = '\0';
|
|
}
|
|
#elif defined(__linux__)
|
|
{
|
|
ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
|
path[n > 0 ? n : 0] = '\0';
|
|
}
|
|
#endif
|
|
/* Interval timers and the signal mask survive exec: a still-armed
|
|
* profiling/watchdog timer would kill the new image (default SIGPROF)
|
|
* before it reaches main. */
|
|
{
|
|
struct itimerval off;
|
|
sigset_t none;
|
|
memset(&off, 0, sizeof(off));
|
|
setitimer(ITIMER_REAL, &off, NULL);
|
|
setitimer(ITIMER_VIRTUAL, &off, NULL);
|
|
setitimer(ITIMER_PROF, &off, NULL);
|
|
sigemptyset(&none);
|
|
sigprocmask(SIG_SETMASK, &none, NULL);
|
|
}
|
|
fflush(NULL);
|
|
execv(path[0] ? path : argv[0], argv);
|
|
}
|
|
#endif
|