overlay: load cached exports on POSIX

Port Douglas Vanderveer's manifest-driven macOS/Linux overlay discovery onto the current cache index and exact-dispatch loader. Validate both filename key fields, preserve ABI cleanup and rescan idempotence, and cover the POSIX primitives with a real shared-library fixture.

Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
This commit is contained in:
Matthew Stanley
2026-07-13 21:52:25 -07:00
parent dde268dc0f
commit c0fa9b1e89
8 changed files with 514 additions and 28 deletions
@@ -0,0 +1,37 @@
# POSIX overlay discovery provenance
This change ports one focused capability from Douglas Vanderveer's PSXRecomp
work: Linux/macOS discovery of cached overlay libraries and manifest-driven
resolution of their `func_XXXXXXXX` exports.
The original implementation was developed while validating the
[R4 recompilation](https://github.com/douglasjv/r4) and appeared in Douglas's
framework commits
[`df7d1faa5f27be0ba357463a763c77efc43e1f91`](https://github.com/douglasjv/psxrecomp-tweaks/commit/df7d1faa5f27be0ba357463a763c77efc43e1f91)
and
[`7abbfdf38b488d3764b96c155549bc930e0521b6`](https://github.com/douglasjv/psxrecomp-tweaks/commit/7abbfdf38b488d3764b96c155549bc930e0521b6).
It is rebuilt here against the newer cache index, ABI tag, codegen namespace,
lazy manifest index, exact native dispatch, and rescan behavior on current
master. The earlier upstream context is
[mstan/psxrecomp PR #15](https://github.com/mstan/psxrecomp/pull/15).
Included scope:
- strict `<addr8>_<crc8>.dll` validation for both cache-key fields;
- POSIX directory enumeration with GCC-before-TCC deduplication;
- POSIX codegen-tag mismatch diagnostics and ABI preflight cleanup;
- manifest-authoritative `dlsym` registration, with failed/empty libraries
closed and successful libraries retained while their function pointers live;
- additive, idempotent rescan behavior inherited from the current loader.
Explicitly excluded are frame splitting, SPU changes, forced-interpreter pages,
macOS GL context changes, CPU-copied overlay fallback, game configuration,
R4 addresses or timing policy, and all other R4-specific work.
The focused Linux test builds a real shared-library fixture, exercises strict
name parsing and directory filtering, detects a sibling codegen tag, resolves a
manifest-style `func_80012345` symbol, and verifies close/reopen behavior:
```sh
bash runtime/tests/run_overlay_posix_test.sh
```
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Parsed <addr8>_<crc8>.dll cache filename. The parser is shared by the
* Windows and POSIX directory walkers so both platforms reject partial hex
* fields and lookalike suffixes identically. */
typedef struct PsxOverlayCacheFile {
char name[22];
char path[768];
uint32_t region_start;
uint32_t content_crc;
uint64_t mtime;
} PsxOverlayCacheFile;
typedef int (*PsxOverlayCacheVisitor)(const PsxOverlayCacheFile *file,
void *opaque);
int psx_overlay_cache_name_parse(const char *name, uint32_t *region_start,
uint32_t *content_crc);
/* POSIX helpers used by overlay_loader.c and its focused regression test.
* The Windows build provides inert stubs; it keeps using Win32 enumeration and
* LoadLibrary while still sharing the strict filename parser above. */
int psx_overlay_posix_scan_cache_dir(const char *dir,
PsxOverlayCacheVisitor visitor,
void *opaque);
int psx_overlay_posix_find_other_cache_tag(const char *base_dir,
const char *expected_tag,
char *found_tag,
size_t found_tag_size);
void *psx_overlay_posix_library_open(const char *path, char *error,
size_t error_size);
void *psx_overlay_posix_library_symbol(void *handle, const char *name);
void *psx_overlay_posix_library_entry(void *handle, uint32_t entry);
void psx_overlay_posix_library_close(void *handle);
#ifdef __cplusplus
}
#endif
+4
View File
@@ -194,6 +194,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/card_data_writes.c
${PSXRECOMP_ROOT}/runtime/src/overlay_capture.c
${PSXRECOMP_ROOT}/runtime/src/overlay_loader.c
${PSXRECOMP_ROOT}/runtime/src/overlay_posix.c
${PSXRECOMP_ROOT}/runtime/src/overlay_compile_worker.c
${PSXRECOMP_ROOT}/runtime/src/overlay_sljit.c
${PSXRECOMP_ROOT}/runtime/src/overlay_backend.c
@@ -506,6 +507,9 @@ function(psxrecomp_add_runtime_target target)
# by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress.
target_link_libraries(${target} PRIVATE ws2_32 dbghelp comdlg32 opengl32)
else()
if(CMAKE_DL_LIBS)
target_link_libraries(${target} PRIVATE ${CMAKE_DL_LIBS})
endif()
find_package(OpenGL)
if(OpenGL_FOUND)
target_link_libraries(${target} PRIVATE OpenGL::GL)
+149 -28
View File
@@ -8,6 +8,7 @@
#include "interrupts.h"
#include "debug_server.h"
#include "psx_cycles.h"
#include "overlay_posix.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@@ -18,7 +19,7 @@
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#else
# include <dlfcn.h>
# include <unistd.h>
#endif
/* ============================================================================
@@ -1059,6 +1060,26 @@ static int cache_idx_has_basename(const char *fname) {
const char *overlay_loader_arch_abi(void) { return PSX_OVERLAY_ARCH_ABI; }
#ifndef _WIN32
static int add_posix_cache_file(const PsxOverlayCacheFile *file, void *opaque) {
(void)opaque;
if (s_cache_idx_count >= CACHE_IDX_CAP) {
loader_log("*** CACHE INDEX FULL (%d): further DLLs near %s are being "
"IGNORED — their regions will run interpreted. Raise "
"CACHE_IDX_CAP.", CACHE_IDX_CAP, file->path);
return 1;
}
if (cache_idx_has_basename(file->name)) return 0;
CacheEntry *e = &s_cache_idx[s_cache_idx_count++];
e->region_start = file->region_start;
e->mtime = file->mtime;
e->func_count = 0;
e->indexed_func_count = 0;
snprintf(e->path, sizeof(e->path), "%s", file->path);
return 0;
}
#endif
/* Scan one directory for <addr8>_<crc8>.dll cache entries into the index.
* `dir` is a full directory path. Idempotent (skips already-indexed paths). */
static void scan_one_cache_dir(const char *dir) {
@@ -1069,18 +1090,9 @@ static void scan_one_cache_dir(const char *dir) {
HANDLE h = FindFirstFileA(pattern, &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
if (strlen(fd.cFileName) != 21) continue; /* 8+1+8+4 = 21 */
/* Validate the <addr8>_<crc8>.dll shape explicitly: region_start 0 is
* LEGAL (the kernel-RAM window starts at phys 0), so a zero parse
* result can't be used as the invalid sentinel. */
int valid = (fd.cFileName[8] == '_');
for (int ci = 0; valid && ci < 8; ci++) {
char c = fd.cFileName[ci];
valid = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') ||
(c >= 'a' && c <= 'f');
}
if (!valid) continue;
uint32_t addr = (uint32_t)strtoul(fd.cFileName, NULL, 16);
uint32_t addr = 0, crc = 0;
if (!psx_overlay_cache_name_parse(fd.cFileName, &addr, &crc)) continue;
(void)crc;
if (s_cache_idx_count >= CACHE_IDX_CAP) {
/* Never-again (the silent-256 truncation): overflowing the index
* means real native coverage is being IGNORED. Shout once. */
@@ -1104,7 +1116,7 @@ static void scan_one_cache_dir(const char *dir) {
} while (FindNextFileA(h, &fd));
FindClose(h);
#else
(void)dir;
psx_overlay_posix_scan_cache_dir(dir, add_posix_cache_file, NULL);
#endif
}
@@ -1152,10 +1164,82 @@ static void warn_on_cgtag_mismatch(const char *tier) {
} while (FindNextFileA(h, &fd));
FindClose(h);
#else
(void)tier;
char base[768], expect[64], found[256];
snprintf(base, sizeof base, "%s/%s/%s/%s",
s_cache_dir, s_game_id, tier, PSX_OVERLAY_ARCH_ABI);
snprintf(expect, sizeof expect, "cg%d_%08x",
PSX_OVERLAY_CODEGEN_VER, (unsigned)PSX_OVERLAY_CODEGEN_HASH);
if (psx_overlay_posix_find_other_cache_tag(base, expect, found,
sizeof(found))) {
loader_log("*** OVERLAY CACHE HASH MISMATCH: this build reads %s/%s but "
"shards exist under %s/%s. The autocompile is writing to a "
"DIFFERENT codegen hash than this runtime reads -> ALL overlays "
"run INTERPRETED (slow). Fix overlay_autocompile_cmd's "
"--recompiler/--runtime-include to match THIS build's framework.",
tier, expect, tier, found);
}
#endif
}
static void cache_idx_remove_path(const char *path) {
for (int i = 0; i < s_cache_idx_count; i++) {
if (strcmp(s_cache_idx[i].path, path) == 0) {
s_cache_idx[i] = s_cache_idx[--s_cache_idx_count];
return;
}
}
}
static void remove_posix_dll_and_manifest(const char *dll_path) {
#ifndef _WIN32
remove(dll_path);
char ranges[800];
int n = snprintf(ranges, sizeof(ranges), "%s", dll_path);
if (n >= 4 && (size_t)n + 4 < sizeof(ranges) &&
strcmp(ranges + n - 4, ".dll") == 0) {
memcpy(ranges + n - 4, ".ranges", 8);
remove(ranges);
}
#else
(void)dll_path;
#endif
}
#ifndef _WIN32
typedef struct PosixAbiSweep {
int purged;
int kept;
} PosixAbiSweep;
static int posix_abi_sweep_file(const PsxOverlayCacheFile *file, void *opaque) {
PosixAbiSweep *sweep = (PosixAbiSweep *)opaque;
char error[256] = {0};
void *handle = psx_overlay_posix_library_open(file->path, error, sizeof(error));
int abi = 0;
if (handle) {
typedef int (*AbiFn)(void);
AbiFn abi_fn = (AbiFn)psx_overlay_posix_library_symbol(handle, "overlay_abi");
abi = abi_fn ? abi_fn() : 0;
psx_overlay_posix_library_close(handle);
}
if (handle && abi == PSX_OVERLAY_ABI_TAG) {
sweep->kept++;
return 0;
}
if (handle)
loader_log("ABI preflight rejecting %s: dll=0x%X runtime=0x%X",
file->path, abi, PSX_OVERLAY_ABI_TAG);
else
loader_log("ABI preflight rejecting unloadable %s: %s",
file->path, error);
remove_posix_dll_and_manifest(file->path);
cache_idx_remove_path(file->path);
sweep->purged++;
return 0;
}
#endif
/* ---- ABI pre-flight sweep (batch purge) ----------------------------------
* A contract-ABI bump (e.g. v9 -> v10) invalidates EVERY cached DLL at once.
* The lazy path handled that per-dispatch: try_load_region -> LoadLibrary ->
@@ -1225,7 +1309,18 @@ static void abi_preflight_sweep(const char *dir) {
FILE_ATTRIBUTE_NORMAL, NULL);
if (m != INVALID_HANDLE_VALUE) CloseHandle(m);
#else
(void)dir;
char marker[900];
snprintf(marker, sizeof marker, "%s/.abi_%08x.ok", dir,
(unsigned)PSX_OVERLAY_ABI_TAG);
if (access(marker, F_OK) == 0) return;
PosixAbiSweep sweep = {0};
psx_overlay_posix_scan_cache_dir(dir, posix_abi_sweep_file, &sweep);
if (sweep.purged)
loader_log("abi preflight: purged %d stale DLL(s), kept %d in %s",
sweep.purged, sweep.kept, dir);
FILE *m = fopen(marker, "wb");
if (m) fclose(m);
#endif
}
@@ -1833,34 +1928,60 @@ static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll
}
#else
static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll) {
(void)man; (void)man_n; (void)dll;
void *h = dlopen(dll_path, RTLD_NOW | RTLD_LOCAL);
if (!h) { loader_log("dlopen(%s) failed: %s", dll_path, dlerror()); return 0; }
char error[256] = {0};
void *h = psx_overlay_posix_library_open(dll_path, error, sizeof(error));
if (!h) { loader_log("dlopen(%s) failed: %s", dll_path, error); return 0; }
/* ABI gate (see the _WIN32 branch). */
typedef int (*AbiFn)(void);
AbiFn abi_fn = (AbiFn)dlsym(h, "overlay_abi");
AbiFn abi_fn = (AbiFn)psx_overlay_posix_library_symbol(h, "overlay_abi");
int abi = abi_fn ? abi_fn() : 0;
if (abi != PSX_OVERLAY_ABI_TAG) {
loader_log("ABI/flavor mismatch in %s: dll=0x%X runtime=0x%X — rejecting "
"and deleting stale cache entry", dll_path, abi,
PSX_OVERLAY_ABI_TAG);
dlclose(h);
remove(dll_path);
psx_overlay_posix_library_close(h);
remove_posix_dll_and_manifest(dll_path);
return 0;
}
typedef void (*InitFn)(const OverlayCallbacks *);
InitFn init_fn = (InitFn)dlsym(h, "overlay_init");
if (!init_fn) { loader_log("no overlay_init in %s", dll_path); dlclose(h); return 0; }
InitFn init_fn = (InitFn)psx_overlay_posix_library_symbol(h, "overlay_init");
if (!init_fn) {
loader_log("no overlay_init in %s", dll_path);
psx_overlay_posix_library_close(h);
return 0;
}
init_fn(&s_callbacks);
OverlayFlushFn flush_fn = (OverlayFlushFn)dlsym(h, "overlay_flush_cycles");
OverlayFlushFn flush_fn = (OverlayFlushFn)psx_overlay_posix_library_symbol(
h, "overlay_flush_cycles");
if (!flush_fn || dll < 0 || dll >= (int)(sizeof(s_dll_flush) / sizeof(s_dll_flush[0]))) {
loader_log("no ABI-v11 cycle flush export in %s", dll_path);
dlclose(h);
psx_overlay_posix_library_close(h);
return 0;
}
s_dll_flush[dll] = flush_fn;
loader_log("%s loaded (posix export scan TODO)", dll_path);
return 0;
/* ELF/Mach-O do not expose a portable export-table walker. The range
* manifest is already the loader's authority, so resolve only its exact
* callable entries. Missing symbols stay interpreted rather than acquiring
* a guessed extent. */
int registered = 0;
for (int i = 0; i < man_n; i++) {
ManFn *m = &man[i];
if (m->entry == 0 || m->n == 0) continue;
OverlayFn fn = (OverlayFn)psx_overlay_posix_library_entry(h, m->entry);
if (!fn) continue;
cand_register(m->entry & 0x1FFFFFFFu, fn, m, dll);
registered++;
}
loader_log("loaded %s -> %d manifest candidates", dll_path, registered);
if (registered == 0) {
s_dll_flush[dll] = NULL;
psx_overlay_posix_library_close(h);
}
/* A successful handle intentionally stays open for process lifetime: the
* registered function and flush pointers refer into it. Rescans are
* idempotent through s_loaded_paths and never acquire a second reference. */
return registered;
}
#endif
+161
View File
@@ -0,0 +1,161 @@
#include "overlay_posix.h"
#include <stdio.h>
#include <string.h>
static int hex_nibble(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
static int parse_hex8(const char *text, uint32_t *value) {
uint32_t parsed = 0;
for (int i = 0; i < 8; i++) {
int nibble = hex_nibble(text[i]);
if (nibble < 0) return 0;
parsed = (parsed << 4) | (uint32_t)nibble;
}
if (value) *value = parsed;
return 1;
}
int psx_overlay_cache_name_parse(const char *name, uint32_t *region_start,
uint32_t *content_crc) {
uint32_t addr = 0, crc = 0;
if (!name || strlen(name) != 21 || name[8] != '_' ||
strcmp(name + 17, ".dll") != 0 ||
!parse_hex8(name, &addr) || !parse_hex8(name + 9, &crc))
return 0;
if (region_start) *region_start = addr;
if (content_crc) *content_crc = crc;
return 1;
}
#ifndef _WIN32
#include <dirent.h>
#include <dlfcn.h>
#include <sys/stat.h>
int psx_overlay_posix_scan_cache_dir(const char *dir,
PsxOverlayCacheVisitor visitor,
void *opaque) {
DIR *dp = opendir(dir);
if (!dp) return 0;
int visited = 0;
struct dirent *de;
while ((de = readdir(dp)) != NULL) {
PsxOverlayCacheFile file;
memset(&file, 0, sizeof(file));
if (!psx_overlay_cache_name_parse(de->d_name, &file.region_start,
&file.content_crc))
continue;
int n = snprintf(file.path, sizeof(file.path), "%s/%s", dir, de->d_name);
if (n < 0 || (size_t)n >= sizeof(file.path)) continue;
memcpy(file.name, de->d_name, sizeof(file.name) - 1);
file.name[sizeof(file.name) - 1] = '\0';
struct stat st;
if (stat(file.path, &st) != 0 || !S_ISREG(st.st_mode)) continue;
file.mtime = (uint64_t)st.st_mtime;
visited++;
if (visitor && visitor(&file, opaque)) break;
}
closedir(dp);
return visited;
}
static int note_first_cache_file(const PsxOverlayCacheFile *file, void *opaque) {
(void)file;
*(int *)opaque = 1;
return 1;
}
int psx_overlay_posix_find_other_cache_tag(const char *base_dir,
const char *expected_tag,
char *found_tag,
size_t found_tag_size) {
DIR *dp = opendir(base_dir);
if (!dp) return 0;
int found = 0;
struct dirent *de;
while (!found && (de = readdir(dp)) != NULL) {
if (strncmp(de->d_name, "cg", 2) != 0 ||
strcmp(de->d_name, expected_tag) == 0)
continue;
char candidate[768];
int n = snprintf(candidate, sizeof(candidate), "%s/%s", base_dir,
de->d_name);
if (n < 0 || (size_t)n >= sizeof(candidate)) continue;
int has_cache_file = 0;
psx_overlay_posix_scan_cache_dir(candidate, note_first_cache_file,
&has_cache_file);
if (!has_cache_file) continue;
if (found_tag && found_tag_size)
snprintf(found_tag, found_tag_size, "%s", de->d_name);
found = 1;
}
closedir(dp);
return found;
}
void *psx_overlay_posix_library_open(const char *path, char *error,
size_t error_size) {
dlerror();
void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (!handle && error && error_size) {
const char *message = dlerror();
snprintf(error, error_size, "%s", message ? message : "unknown dlopen error");
}
return handle;
}
void *psx_overlay_posix_library_symbol(void *handle, const char *name) {
return handle ? dlsym(handle, name) : NULL;
}
void *psx_overlay_posix_library_entry(void *handle, uint32_t entry) {
char name[32];
snprintf(name, sizeof(name), "func_%08X", entry);
return psx_overlay_posix_library_symbol(handle, name);
}
void psx_overlay_posix_library_close(void *handle) {
if (handle) dlclose(handle);
}
#else
int psx_overlay_posix_scan_cache_dir(const char *dir,
PsxOverlayCacheVisitor visitor,
void *opaque) {
(void)dir; (void)visitor; (void)opaque;
return 0;
}
int psx_overlay_posix_find_other_cache_tag(const char *base_dir,
const char *expected_tag,
char *found_tag,
size_t found_tag_size) {
(void)base_dir; (void)expected_tag; (void)found_tag; (void)found_tag_size;
return 0;
}
void *psx_overlay_posix_library_open(const char *path, char *error,
size_t error_size) {
(void)path; (void)error; (void)error_size;
return NULL;
}
void *psx_overlay_posix_library_symbol(void *handle, const char *name) {
(void)handle; (void)name;
return NULL;
}
void *psx_overlay_posix_library_entry(void *handle, uint32_t entry) {
(void)handle; (void)entry;
return NULL;
}
void psx_overlay_posix_library_close(void *handle) { (void)handle; }
#endif
+12
View File
@@ -0,0 +1,12 @@
#include <stdint.h>
#if defined(__GNUC__)
#define EXPORT __attribute__((visibility("default")))
#else
#define EXPORT
#endif
EXPORT int overlay_abi(void) { return 0x12345678; }
EXPORT void overlay_init(const void *callbacks) { (void)callbacks; }
EXPORT void overlay_flush_cycles(void) {}
EXPORT uint32_t func_80012345(void) { return 0xC0DEF00Du; }
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "$0")/../.." && pwd)"
tmp="$(mktemp -d "${TMPDIR:-/tmp}/psxrecomp-posix-overlay.XXXXXX")"
trap 'rm -rf "$tmp"' EXIT
cache="$tmp/cache"
base="$tmp/tags"
expected="cg12_11111111"
other="cg12_22222222"
mkdir -p "$cache/00000000_11111111.dll" "$base/$expected" "$base/$other"
cc -std=c99 -Wall -Wextra -Werror -fPIC -shared \
"$root/runtime/tests/overlay_posix_fixture.c" \
-o "$cache/80010000_DEADBEEF.dll"
: > "$cache/00000000_00000000.dll"
: > "$cache/80010000_DEADBEG0.dll"
: > "$base/$other/80020000_12345678.dll"
cc -std=c99 -Wall -Wextra -Werror \
-I"$root/runtime/include" \
"$root/runtime/tests/test_overlay_posix.c" \
"$root/runtime/src/overlay_posix.c" \
-ldl -o "$tmp/test_overlay_posix"
"$tmp/test_overlay_posix" "$cache" "$base" "$expected" "$other"
+79
View File
@@ -0,0 +1,79 @@
#include "overlay_posix.h"
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
typedef struct ScanResult {
int count;
uint32_t addr;
uint32_t crc;
} ScanResult;
static int record_cache_file(const PsxOverlayCacheFile *file, void *opaque) {
ScanResult *result = (ScanResult *)opaque;
result->count++;
if (strcmp(file->name, "80010000_DEADBEEF.dll") == 0) {
result->addr = file->region_start;
result->crc = file->content_crc;
}
return 0;
}
static void check_name_parser(void) {
uint32_t addr = 1, crc = 1;
assert(psx_overlay_cache_name_parse("00000000_00000000.dll", &addr, &crc));
assert(addr == 0 && crc == 0);
assert(psx_overlay_cache_name_parse("89abcdef_ABCDEF01.dll", &addr, &crc));
assert(addr == 0x89ABCDEFu && crc == 0xABCDEF01u);
assert(!psx_overlay_cache_name_parse("G0010000_DEADBEEF.dll", &addr, &crc));
assert(!psx_overlay_cache_name_parse("80010000_DEADBEG0.dll", &addr, &crc));
assert(!psx_overlay_cache_name_parse("80010000_DEADBEEF.so", &addr, &crc));
assert(!psx_overlay_cache_name_parse("80010000-DEADBEEF.dll", &addr, &crc));
assert(!psx_overlay_cache_name_parse("80010000_DEADBEEF.dll.extra", &addr, &crc));
}
int main(int argc, char **argv) {
assert(argc == 5);
check_name_parser();
ScanResult result = {0};
assert(psx_overlay_posix_scan_cache_dir(argv[1], record_cache_file, &result) == 2);
assert(result.count == 2);
assert(result.addr == 0x80010000u);
assert(result.crc == 0xDEADBEEFu);
char found[64] = {0};
assert(psx_overlay_posix_find_other_cache_tag(argv[2], argv[3], found,
sizeof(found)) == 1);
assert(strcmp(found, argv[4]) == 0);
char error[256] = {0};
void *handle = psx_overlay_posix_library_open(argv[1], error, sizeof(error));
assert(handle == NULL); /* A directory is not a loadable overlay library. */
char fixture[1024];
snprintf(fixture, sizeof(fixture), "%s/80010000_DEADBEEF.dll", argv[1]);
handle = psx_overlay_posix_library_open(fixture, error, sizeof(error));
assert(handle != NULL);
typedef int (*AbiFn)(void);
typedef uint32_t (*EntryFn)(void);
AbiFn abi = (AbiFn)psx_overlay_posix_library_symbol(handle, "overlay_abi");
EntryFn entry = (EntryFn)psx_overlay_posix_library_entry(handle, 0x80012345u);
assert(abi && abi() == 0x12345678);
assert(entry && entry() == 0xC0DEF00Du);
assert(psx_overlay_posix_library_entry(handle, 0x80099999u) == NULL);
assert(psx_overlay_posix_library_symbol(handle, "func_80099999") == NULL);
psx_overlay_posix_library_close(handle);
/* A close followed by a fresh open must remain usable; the production
* loader closes rejected/empty libraries and retains successful handles. */
handle = psx_overlay_posix_library_open(fixture, error, sizeof(error));
assert(handle != NULL);
psx_overlay_posix_library_close(handle);
puts("overlay POSIX discovery tests passed");
return 0;
}