mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
Checkpoint asynchronous overlay shard publication
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user