/* test_host_overlay.c -- unit tests for host_overlay.c hooks. */ #include "host_overlay.h" #include #if defined(_WIN32) #include #include #endif #include #include 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 #include 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; }