Checkpoint asynchronous overlay shard publication

This commit is contained in:
Matthew Stanley
2026-07-19 15:30:25 -07:00
parent a73a4055d1
commit 45a2dda040
7 changed files with 613 additions and 58 deletions
+11
View File
@@ -66,6 +66,17 @@ if(BUILD_TESTING)
SDL_MAIN_HANDLED PSX_OVERLAY_CAPTURE_TEST=1)
target_link_libraries(overlay_capture_retry_test PRIVATE ${SDL2_LIBRARIES})
add_test(NAME overlay_capture_retry_test COMMAND overlay_capture_retry_test)
if(WIN32)
add_executable(autocompile_publication_test
tests/test_autocompile_publication.c
src/autocompile.c)
target_include_directories(autocompile_publication_test PRIVATE include)
target_compile_definitions(autocompile_publication_test PRIVATE
PSX_AUTOCOMPILE_TEST=1)
add_test(NAME autocompile_publication_test
COMMAND autocompile_publication_test)
endif()
endif()
if(NOT MSVC)
+8 -3
View File
@@ -47,9 +47,14 @@ int overlay_loader_dispatch(CPUState *cpu, uint32_t addr);
* (loaded DLLs stay loaded); emu thread only. */
void overlay_loader_rescan(void);
/* Load one atomically-published autocompile artifact directly. The path must be
* inside this loader's canonical cache root. Emulation thread only. */
int overlay_loader_load_published(const char *dll_path);
/* Two-phase live publication: the compile-output watcher maps a freshly
* published image off the emulation thread, then the emulation thread performs
* callback wiring, manifest validation, and candidate registration. The opaque
* object owns exactly one speculative library reference until commit/discard. */
typedef struct OverlayPreparedImage OverlayPreparedImage;
OverlayPreparedImage *overlay_loader_prepare_published(const char *dll_path);
int overlay_loader_commit_published(OverlayPreparedImage *image);
void overlay_loader_discard_prepared(OverlayPreparedImage *image);
/* True if the cache holds <region_start8>_<crc8>.{dll,so}. */
int overlay_loader_has_cached_crc(uint32_t region_start, uint32_t crc);
+325 -33
View File
@@ -2,6 +2,7 @@
* platform); on other hosts the spawn is a graceful no-op and the manual
* compile_overlays.py flow still works. */
#include "autocompile.h"
#include "overlay_loader.h"
#include <stdint.h>
#include <stdio.h>
@@ -13,9 +14,6 @@
# include <windows.h>
#endif
extern void overlay_loader_rescan(void);
extern int overlay_loader_load_published(const char *dll_path);
static char s_cmd[4096]; /* large: the runtime-constructed bundled tcc cmd has
* many absolute paths (python+script+recompiler+tcc+...) */
static char s_cwd[512];
@@ -62,17 +60,36 @@ static int s_shard_result_seen = 0; /* did we parse a result line? */
#define AC_OUT_CAP 8192
static char s_out[AC_OUT_CAP];
static int s_out_len = 0;
static unsigned s_publish_drops_run = 0;
static unsigned s_publish_load_fail_run = 0, s_publish_parse_fail_run = 0;
#ifdef _WIN32
static CRITICAL_SECTION s_out_lock;
static CONDITION_VARIABLE s_publish_cv;
static int s_out_lock_init = 0;
static HANDLE s_proc = NULL;
static HANDLE s_watch_thread = NULL;
static HANDLE s_prepare_thread = NULL;
#define AC_PUBLISH_CAP 128
static char s_publish_paths[AC_PUBLISH_CAP][768];
static unsigned s_publish_head = 0, s_publish_count = 0;
static unsigned s_publish_drops_run = 0;
static unsigned s_publish_load_fail_run = 0, s_publish_parse_fail_run = 0;
typedef struct PublishItem {
struct PublishItem *next;
OverlayPreparedImage *image;
char path[768];
} PublishItem;
static PublishItem *s_publish_raw_head, *s_publish_raw_tail;
static PublishItem *s_publish_ready_head, *s_publish_ready_tail;
enum { AC_PUBLISH_READY_LIMIT = 1 };
static unsigned s_publish_ready_count;
static unsigned s_publish_ready_highwater;
static unsigned s_publish_preparing;
static unsigned s_publish_commit_active;
static int s_publish_input_done;
static int s_publish_stop;
static uint64_t s_publish_prepare_total_us;
static uint64_t s_publish_prepare_max_us;
static uint64_t s_publish_prepare_last_us;
static unsigned s_publish_prepare_count;
static unsigned s_publish_prepare_fail;
static char s_child_line[1024];
static int s_child_line_len = 0;
static int s_child_line_overflow = 0;
@@ -82,13 +99,20 @@ static void publish_line_locked(void) {
if (strncmp(s_child_line, marker, sizeof(marker) - 1) != 0) return;
const char *path = s_child_line + sizeof(marker) - 1;
if (!path[0]) return;
if (s_publish_count >= AC_PUBLISH_CAP) {
if (strlen(path) >= sizeof(((PublishItem *)0)->path)) {
s_publish_parse_fail_run++;
return;
}
PublishItem *item = (PublishItem *)calloc(1, sizeof(*item));
if (!item) {
s_publish_drops_run++;
return;
}
unsigned slot = (s_publish_head + s_publish_count) % AC_PUBLISH_CAP;
snprintf(s_publish_paths[slot], sizeof(s_publish_paths[slot]), "%s", path);
s_publish_count++;
snprintf(item->path, sizeof(item->path), "%s", path);
if (s_publish_raw_tail) s_publish_raw_tail->next = item;
else s_publish_raw_head = item;
s_publish_raw_tail = item;
WakeConditionVariable(&s_publish_cv);
}
static void publish_parse_locked(const char *buf, int n) {
@@ -111,27 +135,160 @@ static void publish_parse_locked(const char *buf, int n) {
}
}
static int publish_pop(char *out, int cap) {
int found = 0;
/* The pipe reader only queues paths. A separate worker first-maps them, so a
* slow antivirus/disk/loader event cannot stop stdout draining and deadlock the
* compiler. There is exactly one mapped handoff at a time. The worker also
* waits for the preceding main-thread commit to finish before entering the
* process-wide Windows loader lock again. */
static DWORD WINAPI publish_prepare_thread_main(LPVOID unused) {
(void)unused;
for (;;) {
PublishItem *item = NULL;
EnterCriticalSection(&s_out_lock);
while (!s_publish_stop && !s_publish_raw_head &&
!s_publish_input_done)
SleepConditionVariableCS(&s_publish_cv, &s_out_lock, INFINITE);
while (!s_publish_stop && s_publish_raw_head &&
(s_publish_ready_count != 0 || s_publish_commit_active != 0))
SleepConditionVariableCS(&s_publish_cv, &s_out_lock, INFINITE);
if (s_publish_stop || (!s_publish_raw_head && s_publish_input_done)) {
LeaveCriticalSection(&s_out_lock);
break;
}
item = s_publish_raw_head;
s_publish_raw_head = item->next;
if (!s_publish_raw_head) s_publish_raw_tail = NULL;
item->next = NULL;
s_publish_preparing++;
LeaveCriticalSection(&s_out_lock);
LARGE_INTEGER q0, q1, qf;
QueryPerformanceCounter(&q0);
item->image = overlay_loader_prepare_published(item->path);
QueryPerformanceCounter(&q1);
QueryPerformanceFrequency(&qf);
uint64_t elapsed_us = qf.QuadPart > 0
? (uint64_t)((q1.QuadPart - q0.QuadPart) * 1000000LL /
qf.QuadPart) : 0;
if (!item->image) {
EnterCriticalSection(&s_out_lock);
s_publish_preparing--;
s_publish_prepare_count++;
s_publish_prepare_fail++;
s_publish_prepare_last_us = elapsed_us;
s_publish_prepare_total_us += elapsed_us;
if (elapsed_us > s_publish_prepare_max_us)
s_publish_prepare_max_us = elapsed_us;
s_publish_load_fail_run++;
LeaveCriticalSection(&s_out_lock);
free(item);
continue;
}
EnterCriticalSection(&s_out_lock);
s_publish_preparing--;
s_publish_prepare_count++;
s_publish_prepare_last_us = elapsed_us;
s_publish_prepare_total_us += elapsed_us;
if (elapsed_us > s_publish_prepare_max_us)
s_publish_prepare_max_us = elapsed_us;
if (s_publish_stop) {
LeaveCriticalSection(&s_out_lock);
overlay_loader_discard_prepared(item->image);
free(item);
break;
}
/* The wait-before-prepare invariant makes this queue depth exactly one.
* Keep a defensive condition in case future code adds another worker. */
while (!s_publish_stop &&
s_publish_ready_count >= AC_PUBLISH_READY_LIMIT)
SleepConditionVariableCS(&s_publish_cv, &s_out_lock, INFINITE);
if (s_publish_stop) {
LeaveCriticalSection(&s_out_lock);
overlay_loader_discard_prepared(item->image);
free(item);
break;
}
if (s_publish_ready_tail) s_publish_ready_tail->next = item;
else s_publish_ready_head = item;
s_publish_ready_tail = item;
s_publish_ready_count++;
if (s_publish_ready_count > s_publish_ready_highwater)
s_publish_ready_highwater = s_publish_ready_count;
LeaveCriticalSection(&s_out_lock);
}
if (!s_publish_stop)
ac_state_store(AC_DONE);
return 0;
}
static PublishItem *publish_ready_pop(void) {
PublishItem *item;
EnterCriticalSection(&s_out_lock);
if (s_publish_count) {
snprintf(out, (size_t)cap, "%s", s_publish_paths[s_publish_head]);
s_publish_head = (s_publish_head + 1u) % AC_PUBLISH_CAP;
s_publish_count--;
found = 1;
item = s_publish_ready_head;
if (item) {
s_publish_ready_head = item->next;
if (!s_publish_ready_head) s_publish_ready_tail = NULL;
item->next = NULL;
s_publish_ready_count--;
s_publish_commit_active++;
}
LeaveCriticalSection(&s_out_lock);
return found;
return item;
}
static void publish_commit_finished(void) {
EnterCriticalSection(&s_out_lock);
if (s_publish_commit_active) s_publish_commit_active--;
WakeAllConditionVariable(&s_publish_cv);
LeaveCriticalSection(&s_out_lock);
}
static int publish_pending(void) {
int pending;
EnterCriticalSection(&s_out_lock);
pending = s_publish_count != 0;
pending = s_publish_raw_head != NULL || s_publish_ready_head != NULL ||
s_publish_preparing != 0 || s_publish_commit_active != 0;
LeaveCriticalSection(&s_out_lock);
return pending;
}
static void publish_note_load_failure(void) {
EnterCriticalSection(&s_out_lock);
s_publish_load_fail_run++;
LeaveCriticalSection(&s_out_lock);
}
static int publish_discard_all(void) {
PublishItem *raw, *ready;
EnterCriticalSection(&s_out_lock);
/* Only an idle provider may reset publication ownership. Never pretend an
* image currently inside LoadLibrary is no longer preparing: doing so
* would let a new run race the old watcher and corrupt the FIFO invariant. */
if (s_publish_preparing != 0 || s_publish_commit_active != 0) {
LeaveCriticalSection(&s_out_lock);
return 0;
}
raw = s_publish_raw_head;
ready = s_publish_ready_head;
s_publish_raw_head = s_publish_raw_tail = NULL;
s_publish_ready_head = s_publish_ready_tail = NULL;
s_publish_ready_count = 0;
WakeAllConditionVariable(&s_publish_cv);
LeaveCriticalSection(&s_out_lock);
while (raw) {
PublishItem *next = raw->next;
free(raw);
raw = next;
}
while (ready) {
PublishItem *next = ready->next;
overlay_loader_discard_prepared(ready->image);
free(ready);
ready = next;
}
return 1;
}
static void out_append(const char *buf, int n) {
EnterCriticalSection(&s_out_lock);
publish_parse_locked(buf, n);
@@ -150,6 +307,67 @@ static void out_append(const char *buf, int n) {
LeaveCriticalSection(&s_out_lock);
}
#ifdef PSX_AUTOCOMPILE_TEST
void autocompile_test_feed_output(const char *buf, int n) {
out_append(buf, n);
}
int autocompile_test_start_preparer(void) {
EnterCriticalSection(&s_out_lock);
s_publish_input_done = 0;
s_publish_stop = 0;
LeaveCriticalSection(&s_out_lock);
s_prepare_thread = CreateThread(NULL, 0, publish_prepare_thread_main,
NULL, 0, NULL);
return s_prepare_thread != NULL;
}
void autocompile_test_finish_input(void) {
EnterCriticalSection(&s_out_lock);
s_publish_input_done = 1;
WakeAllConditionVariable(&s_publish_cv);
LeaveCriticalSection(&s_out_lock);
}
int autocompile_test_join_preparer(DWORD timeout_ms) {
if (!s_prepare_thread) return 1;
if (WaitForSingleObject(s_prepare_thread, timeout_ms) != WAIT_OBJECT_0)
return 0;
CloseHandle(s_prepare_thread);
s_prepare_thread = NULL;
return 1;
}
int autocompile_test_ready_count(void) {
int count = 0;
EnterCriticalSection(&s_out_lock);
for (PublishItem *item = s_publish_ready_head; item; item = item->next)
count++;
LeaveCriticalSection(&s_out_lock);
return count;
}
int autocompile_test_ready_highwater(void) {
int highwater;
EnterCriticalSection(&s_out_lock);
highwater = (int)s_publish_ready_highwater;
LeaveCriticalSection(&s_out_lock);
return highwater;
}
int autocompile_test_preparing_count(void) {
int preparing;
EnterCriticalSection(&s_out_lock);
preparing = (int)s_publish_preparing;
LeaveCriticalSection(&s_out_lock);
return preparing;
}
void autocompile_test_discard_all(void) {
(void)publish_discard_all();
}
#endif
typedef struct { HANDLE read_pipe; HANDLE proc; } WatchCtx;
static DWORD WINAPI watch_thread(LPVOID arg) {
@@ -175,7 +393,10 @@ static DWORD WINAPI watch_thread(LPVOID arg) {
CloseHandle(ctx->proc);
HeapFree(GetProcessHeap(), 0, ctx);
InterlockedExchange(&s_exit_code, (LONG)code);
ac_state_store(AC_DONE); /* emu thread applies via autocompile_poll_main */
EnterCriticalSection(&s_out_lock);
s_publish_input_done = 1;
WakeAllConditionVariable(&s_publish_cv);
LeaveCriticalSection(&s_out_lock);
return 0;
}
#endif /* _WIN32 */
@@ -186,6 +407,7 @@ void autocompile_configure(const char *cmd, const char *cwd) {
#ifdef _WIN32
if (!s_out_lock_init) {
InitializeCriticalSection(&s_out_lock);
InitializeConditionVariable(&s_publish_cv);
s_out_lock_init = 1;
}
#endif
@@ -242,10 +464,21 @@ int autocompile_request(void) {
* poll may return it to IDLE; never overwrite it with another child. */
if (!autocompile_configured() || ac_state_load() != AC_IDLE) return 0;
#ifdef _WIN32
/* IDLE should imply empty queues; discard defensively so a prior aborted
* run can never leak a speculative module reference into the next one. */
if (!publish_discard_all()) return 0;
EnterCriticalSection(&s_out_lock);
s_publish_head = s_publish_count = 0;
s_publish_drops_run = 0;
s_publish_load_fail_run = s_publish_parse_fail_run = 0;
s_publish_ready_highwater = 0;
s_publish_prepare_total_us = 0;
s_publish_prepare_max_us = 0;
s_publish_prepare_last_us = 0;
s_publish_prepare_count = 0;
s_publish_prepare_fail = 0;
s_publish_commit_active = 0;
s_publish_input_done = 0;
s_publish_stop = 0;
s_child_line_len = 0;
s_child_line_overflow = 0;
s_out_len = 0;
@@ -310,10 +543,21 @@ int autocompile_request(void) {
s_proc = pi.hProcess;
ac_state_store(AC_RUNNING);
s_runs++;
HANDLE th = CreateThread(NULL, 0, watch_thread, ctx, 0, NULL);
if (th) {
CloseHandle(th);
} else {
s_prepare_thread = CreateThread(NULL, 0, publish_prepare_thread_main,
NULL, 0, NULL);
if (!s_prepare_thread) {
TerminateProcess(pi.hProcess, ERROR_NOT_ENOUGH_MEMORY);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(rd);
CloseHandle(pi.hProcess);
HeapFree(GetProcessHeap(), 0, ctx);
s_proc = NULL;
ac_state_store(AC_IDLE);
s_fails++;
return 0;
}
s_watch_thread = CreateThread(NULL, 0, watch_thread, ctx, 0, NULL);
if (!s_watch_thread) {
/* Without the watcher nobody drains stdout, observes completion, or
* closes the process handle. Cancel the just-created child fail-closed. */
TerminateProcess(pi.hProcess, ERROR_NOT_ENOUGH_MEMORY);
@@ -322,6 +566,13 @@ int autocompile_request(void) {
CloseHandle(pi.hProcess);
HeapFree(GetProcessHeap(), 0, ctx);
s_proc = NULL;
EnterCriticalSection(&s_out_lock);
s_publish_stop = 1;
WakeAllConditionVariable(&s_publish_cv);
LeaveCriticalSection(&s_out_lock);
WaitForSingleObject(s_prepare_thread, INFINITE);
CloseHandle(s_prepare_thread);
s_prepare_thread = NULL;
ac_state_store(AC_IDLE);
s_fails++;
return 0;
@@ -382,12 +633,16 @@ static int parse_shard_result(void) {
void autocompile_poll_main(void) {
/* Drain at most one atomic publication per frame. The producer only sends
* paths; candidate registration remains single-threaded here. */
* mapped-but-uninitialized images; callback wiring, validation, and
* candidate registration remain single-threaded here. */
#ifdef _WIN32
char published[768];
if (publish_pop(published, (int)sizeof(published))) {
if (overlay_loader_load_published(published) <= 0)
s_publish_load_fail_run++;
PublishItem *published = publish_ready_pop();
if (published) {
if (overlay_loader_commit_published(published->image) <= 0)
publish_note_load_failure();
published->image = NULL; /* commit consumed it on every path */
free(published);
publish_commit_finished();
s_rescans++;
}
if (ac_state_load() == AC_RUNNING) return;
@@ -395,6 +650,19 @@ void autocompile_poll_main(void) {
if (ac_state_load() != AC_DONE) return;
#ifdef _WIN32
if (publish_pending()) return;
/* DONE is published immediately before the preparer returns. Join both
* per-run workers before exposing IDLE, so a new run cannot inherit a live
* consumer of the shared FIFO. Neither worker needs the emulation thread. */
if (s_prepare_thread) {
WaitForSingleObject(s_prepare_thread, INFINITE);
CloseHandle(s_prepare_thread);
s_prepare_thread = NULL;
}
if (s_watch_thread) {
WaitForSingleObject(s_watch_thread, INFINITE);
CloseHandle(s_watch_thread);
s_watch_thread = NULL;
}
#endif
ac_state_store(AC_IDLE);
s_shard_result_seen = parse_shard_result();
@@ -437,6 +705,11 @@ int autocompile_status_json(char *out, int cap) {
static const char *names[] = { "idle", "running", "done" };
char tail[2048];
int tn = 0;
unsigned publish_ready = 0, publish_ready_highwater = 0;
unsigned publish_preparing = 0, publish_prepare_count = 0;
unsigned publish_prepare_fail = 0;
uint64_t publish_prepare_total_us = 0, publish_prepare_max_us = 0;
uint64_t publish_prepare_last_us = 0;
tail[0] = '\0';
#ifdef _WIN32
if (s_out_lock_init) {
@@ -444,6 +717,14 @@ int autocompile_status_json(char *out, int cap) {
int take = s_out_len < 900 ? s_out_len : 900; /* newest tail */
tn = json_escape_into(tail, sizeof(tail),
s_out + (s_out_len - take), take);
publish_ready = s_publish_ready_count;
publish_ready_highwater = s_publish_ready_highwater;
publish_preparing = s_publish_preparing;
publish_prepare_count = s_publish_prepare_count;
publish_prepare_fail = s_publish_prepare_fail;
publish_prepare_total_us = s_publish_prepare_total_us;
publish_prepare_max_us = s_publish_prepare_max_us;
publish_prepare_last_us = s_publish_prepare_last_us;
LeaveCriticalSection(&s_out_lock);
}
#endif
@@ -453,9 +734,20 @@ int autocompile_status_json(char *out, int cap) {
"\"rescans\":%u,\"last_exit\":%d,"
"\"shard_ok\":%u,\"shard_fail\":%u,\"shard_skipped\":%u,"
"\"shard_fail_total\":%u,\"shard_result_seen\":%d,"
"\"publish_ready\":%u,\"publish_ready_highwater\":%u,"
"\"publish_preparing\":%u,\"publish_prepare_count\":%u,"
"\"publish_prepare_fail\":%u,"
"\"publish_prepare_total_us\":%llu,"
"\"publish_prepare_max_us\":%llu,"
"\"publish_prepare_last_us\":%llu,"
"\"output_tail\":\"%s\"}",
autocompile_configured(), names[ac_state_load() & 3], s_runs, s_fails,
s_rescans, s_exit_code,
s_shard_ok, s_shard_fail, s_shard_skipped,
s_shard_fail_total, s_shard_result_seen, tail);
s_shard_fail_total, s_shard_result_seen,
publish_ready, publish_ready_highwater, publish_preparing,
publish_prepare_count, publish_prepare_fail,
(unsigned long long)publish_prepare_total_us,
(unsigned long long)publish_prepare_max_us,
(unsigned long long)publish_prepare_last_us, tail);
}
+17 -11
View File
@@ -723,11 +723,26 @@ static int autocap_provider_request_try(const CodeProvider *cp, uint64_t frame)
{
if (!s_autocap_provider_sig_pending) return 0;
if (frame < s_autocap_provider_retry_frame) return 1;
/* busy remains true through AC_DONE until the emulation thread has
* committed every prepared DLL and completed the additive final rescan. */
if (cp->busy && cp->busy()) return 1;
int reg = overlay_loader_registered_count();
if (s_autocap_provider_sig_pending == s_autocap_sig_at_req &&
reg == s_autocap_reg_at_req) {
s_autocap_futile++;
if (s_autocap_backoff < AUTOCAP_BACKOFF_MAX)
s_autocap_backoff <<= 1;
s_autocap_next_ok = frame +
(uint64_t)AUTOCAP_COOLDOWN_FRAMES * s_autocap_backoff;
s_autocap_provider_sig_pending = 0;
s_autocap_provider_retry_frame = 0;
s_autocap_provider_attempts = 0;
return 0;
}
if (cp->request && cp->request()) {
s_autocap_backoff = 1;
s_autocap_sig_at_req = s_autocap_provider_sig_pending;
s_autocap_reg_at_req = overlay_loader_registered_count();
s_autocap_reg_at_req = reg;
s_autocap_triggers++;
s_autocap_provider_sig_pending = 0;
s_autocap_provider_retry_frame = 0;
@@ -965,16 +980,7 @@ void overlay_autocapture_tick(void)
return;
}
s_autocap_write_job = NULL;
int reg = overlay_loader_registered_count();
if (sig && sig == s_autocap_sig_at_req &&
reg == s_autocap_reg_at_req) {
s_autocap_futile++;
s_autocap_reg_at_req = reg;
if (s_autocap_backoff < AUTOCAP_BACKOFF_MAX)
s_autocap_backoff <<= 1;
s_autocap_next_ok = s_frame_count +
(uint64_t)AUTOCAP_COOLDOWN_FRAMES * s_autocap_backoff;
} else if (sig) {
if (sig) {
s_autocap_provider_sig_pending = sig;
s_autocap_provider_retry_frame = s_frame_count;
s_autocap_provider_attempts = 0;
+85 -9
View File
@@ -1920,9 +1920,21 @@ static void init_callbacks(void) {
/* ---- DLL loading and export enumeration -------------------------------- */
#ifdef _WIN32
static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll) {
typedef HMODULE OverlayLibraryHandle;
#else
typedef void *OverlayLibraryHandle;
#endif
struct OverlayPreparedImage {
OverlayLibraryHandle handle;
char path[768];
};
#ifdef _WIN32
static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll,
OverlayLibraryHandle prepared) {
int tier = cache_tier_from_path(dll_path);
HMODULE h = LoadLibraryA(dll_path);
HMODULE h = prepared ? prepared : LoadLibraryA(dll_path);
if (!h) {
loader_log("LoadLibrary(%s) failed: %lu", dll_path, GetLastError());
return 0;
@@ -1998,13 +2010,20 @@ static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll
}
loader_log("loaded %s -> %d candidates (%u no-manifest)",
dll_path, registered, s_no_manifest);
if (registered == 0) {
s_dll_flush[dll] = NULL;
FreeLibrary(h);
}
return registered;
}
#else
static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll) {
static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll,
OverlayLibraryHandle prepared) {
int tier = cache_tier_from_path(dll_path);
char error[256] = {0};
void *h = psx_overlay_posix_library_open(dll_path, error, sizeof(error));
void *h = prepared ? prepared
: 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);
@@ -2265,7 +2284,19 @@ static int dll_already_loaded(const char *path) {
return 0;
}
static int load_one_dll(const char *dll_path) {
static void overlay_library_close(OverlayLibraryHandle handle) {
if (!handle) return;
#ifdef _WIN32
FreeLibrary(handle);
#else
psx_overlay_posix_library_close(handle);
#endif
}
/* Consumes prepared on every path. A successful registration intentionally
* retains that reference for process lifetime, matching the ordinary loader. */
static int load_one_dll_prepared(const char *dll_path,
OverlayLibraryHandle prepared) {
#ifdef _WIN32
LARGE_INTEGER q0, q1, qf;
QueryPerformanceCounter(&q0);
@@ -2284,9 +2315,10 @@ static int load_one_dll(const char *dll_path) {
if (!man || man_n == 0) {
loader_log("no/empty manifest %s — DLL left to interpreter", ranges_path);
free(man);
overlay_library_close(prepared);
return 0;
}
int registered = load_overlay_dll(dll_path, man, man_n, s_ndlls);
int registered = load_overlay_dll(dll_path, man, man_n, s_ndlls, prepared);
#ifdef _WIN32
QueryPerformanceCounter(&q1);
QueryPerformanceFrequency(&qf);
@@ -2311,7 +2343,11 @@ static int load_one_dll(const char *dll_path) {
return registered;
}
int overlay_loader_load_published(const char *dll_path) {
static int load_one_dll(const char *dll_path) {
return load_one_dll_prepared(dll_path, NULL);
}
static int published_path_valid(const char *dll_path) {
if (!s_active || !dll_path || !dll_path[0]) return 0;
/* The compiler command is trusted local code, but keep its native-library
* output confined to the cache root the framework injected. */
@@ -2325,8 +2361,48 @@ int overlay_loader_load_published(const char *dll_path) {
if (!cache_name_is_immutable(base)) return 0;
(void)addr;
(void)crc;
if (dll_already_loaded(dll_path)) return 0;
return load_one_dll(dll_path);
return 1;
}
OverlayPreparedImage *overlay_loader_prepare_published(const char *dll_path) {
if (!published_path_valid(dll_path)) return NULL;
OverlayPreparedImage *image =
(OverlayPreparedImage *)calloc(1, sizeof(*image));
if (!image) return NULL;
snprintf(image->path, sizeof(image->path), "%s", dll_path);
#ifdef _WIN32
image->handle = LoadLibraryA(dll_path);
#else
char error[256] = {0};
image->handle = psx_overlay_posix_library_open(dll_path, error,
sizeof(error));
#endif
if (!image->handle) {
free(image);
return NULL;
}
return image;
}
int overlay_loader_commit_published(OverlayPreparedImage *image) {
if (!image) return 0;
OverlayLibraryHandle handle = image->handle;
image->handle = NULL;
int loaded = 0;
if (published_path_valid(image->path) &&
!dll_already_loaded(image->path))
loaded = load_one_dll_prepared(image->path, handle);
else
overlay_library_close(handle);
free(image);
return loaded;
}
void overlay_loader_discard_prepared(OverlayPreparedImage *image) {
if (!image) return;
overlay_library_close(image->handle);
image->handle = NULL;
free(image);
}
static int lazy_man_contains(const ManFn *m, uint32_t phys) {
@@ -0,0 +1,141 @@
#include "autocompile.h"
#include "overlay_loader.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void autocompile_test_feed_output(const char *buf, int n);
int autocompile_test_start_preparer(void);
void autocompile_test_finish_input(void);
int autocompile_test_join_preparer(DWORD timeout_ms);
int autocompile_test_ready_count(void);
int autocompile_test_ready_highwater(void);
int autocompile_test_preparing_count(void);
void autocompile_test_discard_all(void);
struct OverlayPreparedImage {
unsigned id;
};
static DWORD s_main_thread;
static LONG s_prepared;
static LONG s_committed;
static LONG s_discarded;
static LONG s_wrong_prepare_thread;
static LONG s_wrong_commit_thread;
OverlayPreparedImage *overlay_loader_prepare_published(const char *path) {
OverlayPreparedImage *image =
(OverlayPreparedImage *)calloc(1, sizeof(*image));
if (!image) return NULL;
image->id = (unsigned)strtoul(strrchr(path, '_') + 1, NULL, 16);
if (GetCurrentThreadId() == s_main_thread)
InterlockedIncrement(&s_wrong_prepare_thread);
InterlockedIncrement(&s_prepared);
return image;
}
int overlay_loader_commit_published(OverlayPreparedImage *image) {
if (GetCurrentThreadId() != s_main_thread)
InterlockedIncrement(&s_wrong_commit_thread);
free(image);
InterlockedIncrement(&s_committed);
return 1;
}
void overlay_loader_discard_prepared(OverlayPreparedImage *image) {
free(image);
InterlockedIncrement(&s_discarded);
}
void overlay_loader_rescan(void) {}
typedef struct {
const char *text;
int length;
} FeedArgs;
static DWORD WINAPI feed_thread(void *opaque) {
FeedArgs *args = (FeedArgs *)opaque;
autocompile_test_feed_output(args->text, args->length);
autocompile_test_finish_input();
return 0;
}
int main(void) {
enum { ITEM_COUNT = 300 };
s_main_thread = GetCurrentThreadId();
autocompile_configure("unused", ".");
size_t capacity = (size_t)ITEM_COUNT * 96u;
char *text = (char *)malloc(capacity);
if (!text) return 1;
int length = 0;
for (unsigned i = 0; i < ITEM_COUNT; ++i) {
length += snprintf(text + length, capacity - (size_t)length,
"PSX_SHARD_PUBLISHED C:\\cache\\00010000_DEADBEEF_%08X.dll\n", i);
}
FeedArgs args = { text, length };
if (!autocompile_test_start_preparer()) { free(text); return 1; }
HANDLE worker = CreateThread(NULL, 0, feed_thread, &args, 0, NULL);
if (!worker) { free(text); return 1; }
DWORD deadline = GetTickCount() + 10000u;
while (s_committed < ITEM_COUNT && GetTickCount() < deadline) {
autocompile_poll_main();
Sleep(1);
}
if (WaitForSingleObject(worker, 1000) != WAIT_OBJECT_0) {
fprintf(stderr, "FAIL: bounded producer did not finish\n");
return 1;
}
CloseHandle(worker);
if (!autocompile_test_join_preparer(1000)) {
fprintf(stderr, "FAIL: preparer did not exit\n");
return 1;
}
free(text);
if (s_prepared != ITEM_COUNT || s_committed != ITEM_COUNT ||
autocompile_test_ready_count() != 0 ||
autocompile_test_preparing_count() != 0 ||
autocompile_test_ready_highwater() > 1 ||
s_wrong_prepare_thread != 0 || s_wrong_commit_thread != 0) {
fprintf(stderr,
"FAIL: bounded handoff prep=%ld commit=%ld ready=%d "
"preparing=%d highwater=%d wrong=%ld/%ld\n",
s_prepared, s_committed, autocompile_test_ready_count(),
autocompile_test_preparing_count(),
autocompile_test_ready_highwater(),
s_wrong_prepare_thread, s_wrong_commit_thread);
autocompile_test_discard_all();
return 1;
}
/* A prepared-but-uncommitted item owns one reference and must be consumed
* by reset/shutdown discard exactly once. */
const char extra[] =
"PSX_SHARD_PUBLISHED C:\\cache\\00010000_DEADBEEF_FFFFFFFF.dll\n";
args.text = extra;
args.length = (int)strlen(extra);
if (!autocompile_test_start_preparer()) return 1;
worker = CreateThread(NULL, 0, feed_thread, &args, 0, NULL);
if (!worker) return 1;
WaitForSingleObject(worker, INFINITE);
CloseHandle(worker);
if (!autocompile_test_join_preparer(1000)) return 1;
autocompile_test_discard_all();
if (s_discarded != 1 || autocompile_test_ready_count() != 0) {
fprintf(stderr, "FAIL: prepared reference discard count=%ld\n", s_discarded);
return 1;
}
puts("PASS: publications prepare off-thread, backpressure at one mapped "
"images, and commit on the emulation thread");
return 0;
}
+26 -2
View File
@@ -10,7 +10,7 @@ ROOT = Path(__file__).resolve().parents[2]
def body(source, name):
match = re.search(rf"\b(?:static\s+)?(?:inline\s+)?(?:int|void|uint8_t|uint16_t|uint32_t|uint64_t)\s+{name}\s*\([^;]*?\)\s*\{{",
match = re.search(rf"\b(?:static\s+)?(?:inline\s+)?(?:int|void|uint8_t|uint16_t|uint32_t|uint64_t|DWORD|OverlayPreparedImage|PublishItem)\s*\*?\s*(?:WINAPI\s+)?{name}\s*\([^;]*?\)\s*\{{",
source, re.S)
if not match:
raise AssertionError(f"missing function {name}")
@@ -106,7 +106,7 @@ def main():
raise AssertionError("negative miss cache is not guarding lookup/recorded at final miss")
if "lazy_miss_invalidate_loader();" not in body(loader, "overlay_loader_rescan"):
raise AssertionError("cache rescan does not invalidate negative misses")
if "lazy_miss_invalidate_loader();" not in body(loader, "load_one_dll"):
if "lazy_miss_invalidate_loader();" not in body(loader, "load_one_dll_prepared"):
raise AssertionError("DLL publication does not invalidate negative misses")
lazy_match = body(loader, "lazy_man_matches")
if "man_delay_slots_hashed(&lm->fn)" not in lazy_match:
@@ -147,6 +147,30 @@ def main():
ac_poll = body(autocompile, "autocompile_poll_main")
if "one idempotent batch-end" not in ac_poll or "overlay_loader_rescan();" not in ac_poll:
raise AssertionError("direct shard handoff is not reconciled into the additive index")
if ("overlay_loader_commit_published(published->image)" not in ac_poll or
"overlay_loader_load_published(" in ac_poll):
raise AssertionError("emulation thread regressed to first-mapping published DLLs")
watcher = body(autocompile, "watch_thread")
preparer = body(autocompile, "publish_prepare_thread_main")
if "out_append(buf, (int)got);" not in watcher:
raise AssertionError("compile watcher no longer drains child output")
if ("overlay_loader_prepare_published(item->path)" in watcher or
"overlay_loader_prepare_published(item->path)" not in preparer):
raise AssertionError("pipe draining and DLL preparation are not separated")
if ("s_publish_commit_active" not in preparer or
"s_publish_commit_active++" not in body(autocompile,
"publish_ready_pop") or
"publish_commit_finished();" not in ac_poll):
raise AssertionError("worker mapping can overlap main-thread loader commit")
if "AC_PUBLISH_CAP" in autocompile:
raise AssertionError("live publication queue can silently overflow a fixed path ring")
prepare_image = body(loader, "overlay_loader_prepare_published")
commit_image = body(loader, "overlay_loader_commit_published")
if "LoadLibraryA(dll_path)" not in prepare_image:
raise AssertionError("Windows published image is not first-mapped on the worker")
if ("load_one_dll_prepared(image->path, handle)" not in commit_image or
"overlay_library_close(handle)" not in commit_image):
raise AssertionError("prepared image commit does not consume every speculative reference")
watched_write = body(memory, "overlay_watch_note_write")
if "g_dirty_ram_exec_page_bitmap" not in watched_write:
raise AssertionError("RAM writes do not clear stale per-page capture evidence")