diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 90071c64..bc194e99 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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) diff --git a/runtime/include/overlay_loader.h b/runtime/include/overlay_loader.h index d82d32ff..cd7a4de4 100644 --- a/runtime/include/overlay_loader.h +++ b/runtime/include/overlay_loader.h @@ -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 _.{dll,so}. */ int overlay_loader_has_cached_crc(uint32_t region_start, uint32_t crc); diff --git a/runtime/src/autocompile.c b/runtime/src/autocompile.c index a64bdcd4..39055027 100644 --- a/runtime/src/autocompile.c +++ b/runtime/src/autocompile.c @@ -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 #include @@ -13,9 +14,6 @@ # include #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); } diff --git a/runtime/src/overlay_capture.c b/runtime/src/overlay_capture.c index af78df40..8e17ec22 100644 --- a/runtime/src/overlay_capture.c +++ b/runtime/src/overlay_capture.c @@ -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; diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 09bb4668..2ff6e89b 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -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) { diff --git a/runtime/tests/test_autocompile_publication.c b/runtime/tests/test_autocompile_publication.c new file mode 100644 index 00000000..af416172 --- /dev/null +++ b/runtime/tests/test_autocompile_publication.c @@ -0,0 +1,141 @@ +#include "autocompile.h" +#include "overlay_loader.h" + +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include +#include +#include + +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; +} diff --git a/runtime/tests/test_interpreter_perf_guards.py b/runtime/tests/test_interpreter_perf_guards.py index 34fd9020..f3ed271d 100644 --- a/runtime/tests/test_interpreter_perf_guards.py +++ b/runtime/tests/test_interpreter_perf_guards.py @@ -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")