Files
psxrecomp/runtime/tests/test_host_overlay.c
Matthew Stanley 7a786f8e84 Move PSX enhancement settings to built-in host-only mods; remove Graphics presets
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>
2026-10-24 19:23:40 -07:00

309 lines
12 KiB
C

/* test_host_overlay.c -- unit tests for host_overlay.c hooks. */
#include "host_overlay.h"
#include <stdio.h>
#if defined(_WIN32)
#include <windows.h>
#include <shellapi.h>
#endif
#include <stdlib.h>
#include <string.h>
static int g_fail = 0;
#define CHECK(c) do { if (!(c)) { fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #c); g_fail++; } } while (0)
static int refuse_on = 0;
static int refuse(void) { return refuse_on; }
static void test_pause(void) {
CHECK(psx_host_pause_depth() == 0 && psx_host_pause_reason() == NULL);
CHECK(psx_host_pause_push("overlay"));
CHECK(psx_host_pause_push("other"));
CHECK(psx_host_pause_depth() == 2 && strcmp(psx_host_pause_reason(), "other") == 0);
psx_host_pause_pop();
CHECK(strcmp(psx_host_pause_reason(), "overlay") == 0);
psx_host_pause_pop();
psx_host_pause_pop(); /* extra pop is harmless */
CHECK(psx_host_pause_depth() == 0);
psx_host_pause_set_refuse_probe(refuse);
refuse_on = 1;
CHECK(!psx_host_pause_push("netplay") && psx_host_pause_depth() == 0);
refuse_on = 0;
CHECK(psx_host_pause_push(NULL) && strcmp(psx_host_pause_reason(), "host") == 0);
psx_host_pause_pop();
for (int i = 0; i < 8; i++) CHECK(psx_host_pause_push("n"));
CHECK(!psx_host_pause_push("overflow"));
for (int i = 0; i < 8; i++) psx_host_pause_pop();
}
static int draws = 0, last_w = 0, last_h = 0;
static void *last_ctx = NULL;
static void on_draw(int w, int h, void *ctx) { draws++; last_w = w; last_h = h; last_ctx = ctx; }
static void test_draw(void) {
int tag = 7;
CHECK(!psx_host_overlay_has_draw());
psx_host_overlay_draw(640, 480); /* no callback: nothing happens */
psx_host_overlay_set_draw_cb(on_draw, &tag);
CHECK(psx_host_overlay_has_draw());
psx_host_overlay_draw(1280, 800);
CHECK(draws == 1 && last_w == 1280 && last_h == 800 && last_ctx == &tag);
psx_host_overlay_draw(0, 800); /* minimised window: skipped */
CHECK(draws == 1);
psx_host_overlay_set_draw_cb(NULL, NULL);
psx_host_overlay_draw(1280, 800);
CHECK(draws == 1 && !psx_host_overlay_has_draw());
}
/* Lifetime: once set_draw_cb(NULL) returns, the old callback never runs
* again, so its context can be freed. A reader thread draws continuously;
* the main thread unregisters and immediately marks the context dead. */
#if !defined(_WIN32) || defined(__MINGW32__)
#include <pthread.h>
#include <stdatomic.h>
typedef struct { atomic_int alive; atomic_int calls; atomic_int used_dead; } LifeCtx;
static atomic_int g_stop;
static void life_draw(int w, int h, void* p) {
LifeCtx* c = (LifeCtx*)p;
(void)w; (void)h;
for (volatile int i = 0; i < 2000; ++i) {} /* a frame's worth of work */
if (!atomic_load(&c->alive)) atomic_fetch_add(&c->used_dead, 1);
atomic_fetch_add(&c->calls, 1);
}
static void* life_reader(void* arg) {
(void)arg;
while (!atomic_load(&g_stop)) psx_host_overlay_draw(64, 64);
return NULL;
}
static void* hud_life_reader(void* arg) {
(void)arg;
while (!atomic_load(&g_stop)) psx_host_hud_draw(64, 64);
return NULL;
}
static void test_hud_lifetime(void) {
pthread_t t;
atomic_store(&g_stop, 0);
pthread_create(&t, NULL, hud_life_reader, NULL);
for (int round = 0; round < 100; ++round) {
LifeCtx* c = (LifeCtx*)calloc(1, sizeof(*c));
atomic_store(&c->alive, 1);
psx_host_set_hud_draw_cb(life_draw, c);
while (atomic_load(&c->calls) < 2) {}
/* Also models the activation-reset / renderer-shutdown clear. */
psx_host_set_hud_draw_cb(NULL, NULL);
atomic_store(&c->alive, 0);
CHECK(atomic_load(&c->used_dead) == 0);
free(c);
}
atomic_store(&g_stop, 1);
pthread_join(t, NULL);
}
static void test_draw_lifetime(void) {
pthread_t t;
atomic_store(&g_stop, 0);
pthread_create(&t, NULL, life_reader, NULL);
int dead_uses = 0, total = 0;
for (int round = 0; round < 300; ++round) {
LifeCtx* c = (LifeCtx*)calloc(1, sizeof(*c));
atomic_store(&c->alive, 1);
psx_host_overlay_set_draw_cb(life_draw, c);
while (atomic_load(&c->calls) < 2) {}
psx_host_overlay_set_draw_cb(NULL, NULL); /* must wait for the reader */
atomic_store(&c->alive, 0); /* the owner tears down */
for (volatile int i = 0; i < 20000; ++i) {}
dead_uses += atomic_load(&c->used_dead);
total += atomic_load(&c->calls);
free(c);
}
atomic_store(&g_stop, 1);
pthread_join(t, NULL);
CHECK(total > 0);
CHECK(dead_uses == 0);
}
#else
static void test_draw_lifetime(void) {}
static void test_hud_lifetime(void) {}
#endif
/* Changing the callback from inside it must not deadlock. */
static void self_unregister(int w, int h, void* p) {
(void)w; (void)h;
++*(int*)p;
psx_host_overlay_set_draw_cb(NULL, NULL);
}
static void test_draw_reentrant(void) {
int n = 0;
psx_host_overlay_set_draw_cb(self_unregister, &n);
psx_host_overlay_draw(64, 64);
psx_host_overlay_draw(64, 64);
CHECK(n == 1 && !psx_host_overlay_has_draw());
}
static void hud_self_unregister(int w, int h, void* p) {
(void)w; (void)h;
++*(int*)p;
psx_host_set_hud_draw_cb(NULL, NULL);
}
static void test_hud_reentrant(void) {
int calls = 0;
psx_host_set_hud_draw_cb(hud_self_unregister, &calls);
psx_host_hud_draw(64, 64);
psx_host_hud_draw(64, 64);
CHECK(calls == 1);
}
static int sunk = 0;
static int sink(const void *ev, void *ctx) { (void)ctx; sunk++; return *(const int *)ev == 1; }
static void test_sink(void) {
int a = 1, b = 2;
CHECK(!psx_host_input_sink_dispatch(&a)); /* none registered */
psx_host_set_input_sink(sink, NULL);
CHECK(psx_host_input_sink_dispatch(&a) && !psx_host_input_sink_dispatch(&b) && sunk == 2);
CHECK(!psx_host_input_sink_dispatch(NULL) && sunk == 2);
psx_host_set_input_sink(NULL, NULL);
CHECK(!psx_host_input_sink_dispatch(&a));
CHECK(!psx_host_ui_capture_active());
psx_host_set_ui_capture(5);
CHECK(psx_host_ui_capture_active() == 1);
psx_host_set_ui_capture(0);
CHECK(!psx_host_ui_capture_active());
}
static void test_video(void) {
PsxHostVideoSettings a, b;
memset(&a, 0, sizeof a);
a.renderer = 1; a.render_thread = 1;
b = a;
CHECK(psx_host_video_live_changes(&a, &b) == 0 && psx_host_video_restart_bits(&a, &b) == 0);
b.frame_generation = 1; b.fullscreen = 1;
CHECK(psx_host_video_live_changes(&a, &b) ==
(PSX_VIDEO_LIVE_FRAME_GEN | PSX_VIDEO_LIVE_FULLSCREEN));
CHECK(psx_host_video_restart_bits(&a, &b) == 0);
{
/* Auto (Match display with dynamic resolution) is an internal
* resolution like any other: a live change, never a restart. */
PsxHostVideoSettings c = a;
c.internal_resolution = -2; /* PSX_IR_AUTO */
CHECK(psx_host_video_live_changes(&a, &c) == PSX_VIDEO_LIVE_RESOLUTION);
CHECK(psx_host_video_restart_bits(&a, &c) == 0);
}
b.internal_resolution = 4; b.supersample_milli = 1500; b.present_thread = 1;
CHECK(psx_host_video_live_changes(&a, &b) & PSX_VIDEO_LIVE_RESOLUTION);
CHECK(psx_host_video_restart_bits(&a, &b) == PSX_VIDEO_RESTART_THREADS);
/* Live-capable GL: no relaunch for resolution. Refused / non-GL (hiw,
* CPU-authoritative dual, no context, software): the change waits. */
CHECK(psx_host_video_restart_bits_backend(&a, &b, 1) == PSX_VIDEO_RESTART_THREADS);
CHECK(psx_host_video_restart_bits_backend(&a, &b, 0) ==
(PSX_VIDEO_RESTART_RESOLUTION | PSX_VIDEO_RESTART_THREADS));
{
PsxHostVideoSettings c2 = a;
c2.supersample_milli = 1250;
CHECK(psx_host_video_restart_bits_backend(&a, &c2, 0) == PSX_VIDEO_RESTART_RESOLUTION);
CHECK(psx_host_video_restart_bits_backend(&a, &a, 0) == 0);
/* A -> B succeeded, then B -> A was refused. Boot A cannot stand in
* for the applied baseline B when deciding whether restart is needed. */
PsxHostVideoSettings applied = a;
applied.internal_resolution = c2.internal_resolution;
applied.supersample_milli = c2.supersample_milli;
CHECK(psx_host_video_restart_bits_backend(&applied, &a, 0) ==
PSX_VIDEO_RESTART_RESOLUTION);
CHECK(psx_host_video_restart_bits_backend(&applied, &c2, 0) == 0);
}
b.supersampling = 2;
CHECK(psx_host_video_restart_bits(&a, &b) == (PSX_VIDEO_RESTART_RESOLUTION | PSX_VIDEO_RESTART_THREADS));
b.renderer = 0;
CHECK(psx_host_video_restart_bits(&a, &b) & PSX_VIDEO_RESTART_RENDERER);
CHECK(psx_host_video_live_changes(NULL, &b) == 0 && psx_host_video_restart_bits(&a, NULL) == 0);
}
/* Windows relaunch quoting: a parser for the CommandLineToArgvW rules (the
* real one too, on Windows) must give every argument back unchanged. */
static int parse_cmdline(const char* line, char out[][256], int max) {
int n = 0;
const char* p = line;
while (*p) {
while (*p == ' ' || *p == '\t') ++p;
if (!*p || n >= max) break;
char* o = out[n];
size_t len = 0;
int inq = 0;
for (;;) {
size_t bs = 0;
while (*p == '\\') { ++bs; ++p; }
if (*p == '"') {
for (size_t i = 0; i < bs / 2; ++i) o[len++] = '\\';
if (bs & 1) { o[len++] = '"'; ++p; continue; }
++p;
if (inq && *p == '"') { o[len++] = '"'; ++p; continue; }
inq = !inq;
continue;
}
for (size_t i = 0; i < bs; ++i) o[len++] = '\\';
if (!*p || (!inq && (*p == ' ' || *p == '\t'))) break;
o[len++] = *p++;
}
o[len] = '\0';
++n;
}
return n;
}
static void test_cmdline_quote(void) {
const char* args[] = {
"C:\\Games\\R4 Recomp\\r4-runtime.exe",
"--disc",
"D:\\Discs\\R4 - Ridge Racer Type 4 (USA).cue",
"--memcard-dir",
"C:\\Users\\Me\\Saved Games\\R4\\", /* trailing separator */
"C:\\path\\with \"quote\".bin", /* embedded quote */
"C:\\a\\b\\\\\"c", /* backslashes before a quote */
"", /* empty argument */
"plain",
"tab\there",
"trailing\\\\", /* backslashes, no spaces */
};
const int n = (int)(sizeof(args) / sizeof(args[0]));
char* line = psx_win_cmdline_join(n, args);
char got[16][256];
CHECK(line != NULL);
CHECK(parse_cmdline(line, got, 16) == n);
for (int i = 0; i < n; ++i) CHECK(strcmp(got[i], args[i]) == 0);
/* No quoting when not needed; measuring matches writing. */
char small[8];
CHECK(psx_win_cmdline_quote("abc", small, sizeof small) == 3 && strcmp(small, "abc") == 0);
CHECK(psx_win_cmdline_quote("a b", NULL, 0) == 5);
CHECK(psx_win_cmdline_quote("", NULL, 0) == 2);
#if defined(_WIN32)
{
int wn = 0;
int len = MultiByteToWideChar(CP_UTF8, 0, line, -1, NULL, 0);
wchar_t* wl = (wchar_t*)malloc(sizeof(wchar_t) * (size_t)len);
MultiByteToWideChar(CP_UTF8, 0, line, -1, wl, len);
wchar_t** wv = CommandLineToArgvW(wl, &wn);
CHECK(wv && wn == n);
for (int i = 1; wv && i < n && i < wn; ++i) { /* argv[0] has its own rules; no quotes in exe paths */
char u[512];
WideCharToMultiByte(CP_UTF8, 0, wv[i], -1, u, sizeof u, NULL, NULL);
CHECK(strcmp(u, args[i]) == 0);
}
LocalFree(wv);
free(wl);
}
#endif
free(line);
}
int main(void) {
test_hud_lifetime();
test_hud_reentrant();
test_draw_lifetime();
test_draw_reentrant();
test_cmdline_quote();
test_video();
test_sink();
test_draw();
test_pause();
printf("host_overlay_test: %s\n", g_fail ? "FAIL" : "ok");
return g_fail ? 1 : 0;
}