mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
b1ce3be00a
Keep the requested internal resolution / supersample (shown and saved) apart from what the backend last applied. When the live change is unavailable (no GL, windowed high-resolution mode, CPU-authoritative dual rendering, no context) or refused, the session scale is left as applied and PSX_VIDEO_RESTART_RESOLUTION stays set, in both psx_video_apply_live and the menu's restart bits (psx_host_video_restart_bits_backend, unit-tested for the capable and refused cases). video_set rejects invalid presets and factors without applying or saving, and reports applied values, capability and pending restart. TCP command index regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
265 lines
10 KiB
C
265 lines
10 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 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) {}
|
|
#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 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.fmv_filter = 2; a.renderer = 1; a.render_thread = 1; a.scanline_strength_pct = 50;
|
|
b = a;
|
|
CHECK(psx_host_video_live_changes(&a, &b) == 0 && psx_host_video_restart_bits(&a, &b) == 0);
|
|
b.scanline_strength_pct = 60; b.present_linear = 1; b.fullscreen = 1;
|
|
CHECK(psx_host_video_live_changes(&a, &b) ==
|
|
(PSX_VIDEO_LIVE_SCANLINES | PSX_VIDEO_LIVE_PRESENT_LINEAR | PSX_VIDEO_LIVE_FULLSCREEN));
|
|
CHECK(psx_host_video_restart_bits(&a, &b) == 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);
|
|
}
|
|
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);
|
|
}
|
|
|
|
/* One texture-filter enum for settings, handoff and menu (xBR included). */
|
|
static void test_texture_filter(void) {
|
|
CHECK(psx_texture_filter_from_ui(0) == PSX_TEXTURE_FILTER_NEAREST);
|
|
CHECK(psx_texture_filter_from_ui(1) == PSX_TEXTURE_FILTER_BILINEAR);
|
|
CHECK(psx_texture_filter_from_ui(3) == PSX_TEXTURE_FILTER_XBR);
|
|
CHECK(psx_texture_filter_from_ui(9) == PSX_TEXTURE_FILTER_NEAREST);
|
|
CHECK(psx_texture_filter_from_ui(-1) == PSX_TEXTURE_FILTER_NEAREST);
|
|
}
|
|
|
|
/* 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_draw_lifetime();
|
|
test_draw_reentrant();
|
|
test_cmdline_quote();
|
|
test_video();
|
|
test_texture_filter();
|
|
test_sink();
|
|
test_draw();
|
|
test_pause();
|
|
printf("host_overlay_test: %s\n", g_fail ? "FAIL" : "ok");
|
|
return g_fail ? 1 : 0;
|
|
}
|