diff --git a/runtime/include/autocompile.h b/runtime/include/autocompile.h index fe44077d..fb65a19f 100644 --- a/runtime/include/autocompile.h +++ b/runtime/include/autocompile.h @@ -16,6 +16,16 @@ extern "C" { void autocompile_configure(const char *cmd, const char *cwd); int autocompile_configured(void); +/* Pin the canonical overlay cache dir + captures file the LOADER actually uses + * (/cache and /overlay_captures.json — computed once in main.cpp). + * autocompile injects these into every spawned compile via the environment + * (PSX_OVERLAY_CACHE_DIR / PSX_OVERLAY_CAPTURES), so the WRITE cache and the + * READ captures can never diverge from where the loader reads — for every game, + * dev or prod, gcc or tcc. compile_overlays.py / coverage_vault.py honor these + * over any CLI --out-dir/--captures. This is the single source of truth for the + * cache location: no per-game config, no drift. */ +void autocompile_set_cache_paths(const char *cache_dir, const char *captures); + /* Probe whether a C compiler is actually reachable on PATH (gcc/cc/clang) — the * REAL "developer machine" signal, distinct from autocompile_configured() which * only reports that a command STRING is set (the shipped game.toml always sets diff --git a/runtime/src/autocompile.c b/runtime/src/autocompile.c index 7e6b3e97..58753fce 100644 --- a/runtime/src/autocompile.c +++ b/runtime/src/autocompile.c @@ -17,6 +17,11 @@ extern void overlay_loader_rescan(void); 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]; +/* Canonical loader cache dir + captures file (see autocompile_set_cache_paths). + * Injected into the compile child's environment so its WRITE location is exactly + * the loader's READ location, regardless of what the configured command says. */ +static char s_cache_dir[512]; +static char s_captures[512]; enum { AC_IDLE = 0, AC_RUNNING = 1, AC_DONE = 2 }; static volatile int s_state = AC_IDLE; @@ -84,6 +89,11 @@ void autocompile_configure(const char *cmd, const char *cwd) { #endif } +void autocompile_set_cache_paths(const char *cache_dir, const char *captures) { + snprintf(s_cache_dir, sizeof(s_cache_dir), "%s", cache_dir ? cache_dir : ""); + snprintf(s_captures, sizeof(s_captures), "%s", captures ? captures : ""); +} + int autocompile_configured(void) { return s_cmd[0] != '\0'; } int autocompile_busy(void) { return s_state == AC_RUNNING; } @@ -133,6 +143,14 @@ int autocompile_request(void) { if (!CreatePipe(&rd, &wr, &sa, 0)) return 0; SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0); + /* Pin the WRITE cache + READ captures to the loader's canonical locations. + * Set on the PARENT env so the child (CreateProcessA lpEnvironment=NULL => + * inherit) sees them; compile_overlays.py / coverage_vault.py prefer these + * over any CLI --out-dir/--captures, so the cache can never drift from where + * the loader reads. This is THE fix for the read/write-location divergence. */ + if (s_cache_dir[0]) SetEnvironmentVariableA("PSX_OVERLAY_CACHE_DIR", s_cache_dir); + if (s_captures[0]) SetEnvironmentVariableA("PSX_OVERLAY_CAPTURES", s_captures); + /* cmd.exe /C resolves the command via PATH and supports the relative * paths in the configured line (cwd = project root). */ char full[4200]; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 79c99e0f..ab8495bb 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -2039,11 +2039,21 @@ int main(int argc, char** argv) { ac_cmd = &gc.runtime.overlay_autocompile_cmd; } if (ac_cmd) { + /* Pin the compile's WRITE cache + READ captures to the SAME + * canonical locations the loader uses (cache_dir = /cache, + * /overlay_captures.json — set above). The framework owns + * the cache location; no game.toml --out-dir/--captures can make + * the write drift from the read. Single source of truth, all + * games, dev or prod. */ + std::string captures_path = + (exe_dir / "overlay_captures.json").string(); + autocompile_set_cache_paths(cache_dir.c_str(), + captures_path.c_str()); autocompile_configure(ac_cmd->c_str(), gc.project_root.string().c_str()); std::fprintf(stdout, - "psxrecomp: overlay autocompile enabled (%s)\n", - overlay_backend_name(eff)); + "psxrecomp: overlay autocompile enabled (%s); cache=%s\n", + overlay_backend_name(eff), cache_dir.c_str()); } code_provider_init(cfg_backend, gcc_avail); /* Now that the backend is resolved, apply the sljit live policy: diff --git a/tools/compile_overlays.py b/tools/compile_overlays.py index f57f7351..c8e98e27 100644 --- a/tools/compile_overlays.py +++ b/tools/compile_overlays.py @@ -853,6 +853,18 @@ int psx_ws_backdrop_x(int x) { int psx_ws_x_margin(void) { return g_cbs.ws_x_margin ? g_cbs.ws_x_margin() : 0; } +/* Shared render-funnel screen-X cull widening ([widescreen.cull] auto_screen_x). + * The recompiler emits `rt = psx_ws_cull_sltiu(sx, imm)` at every width-compare + * cull site, INCLUDING in overlay-resident code, so the overlay DLL must define + * it or it fails to link (undefined reference) -> no native shard -> that overlay + * variant runs interpreted forever (the dwarf-village-style lag). Self-contained: + * depends only on psx_ws_x_margin() above. Identity at 4:3 (margin 0). MUST stay + * byte-for-byte identical to gpu.c psx_ws_cull_sltiu. */ +int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm) { + int m = psx_ws_x_margin(); + return ((uint32_t)((int32_t)(int16_t)(uint16_t)sx + m) + < (uint32_t)((int32_t)imm + 2 * m)) ? 1 : 0; +} void psx_ws_sprite_tag(CPUState *cpu) { if (g_cbs.ws_sprite_tag) g_cbs.ws_sprite_tag(cpu); } @@ -1210,8 +1222,11 @@ def compile_dll(c_path: str, out_dll: str, include_dirs: list[str], def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - ap.add_argument('--captures', required=True, - help='overlay_captures.json from the runtime') + ap.add_argument('--captures', default=None, + help='overlay_captures.json from the runtime. Optional: the ' + 'runtime injects PSX_OVERLAY_CAPTURES (the canonical ' + '/overlay_captures.json), which wins. Required only ' + 'for manual/offline invocation.') ap.add_argument('--game-toml', required=True, help='game.toml (for game_id)') ap.add_argument('--recompiler', required=True, @@ -1242,6 +1257,27 @@ def main(): '+ carry an entry-switch. Must match the runtime build.') args = ap.parse_args() + # ---- Framework-injected cache location wins over CLI flags ---------------- + # The runtime (autocompile.c) exports PSX_OVERLAY_CACHE_DIR / PSX_OVERLAY_CAPTURES + # set to the loader's CANONICAL /cache and /overlay_captures.json. We + # honor those above --out-dir/--captures so the WRITE cache and READ captures + # are always exactly where the loader reads — no per-game config, no drift + # between dev and prod (the read/write-location divergence bug). The CLI flags + # remain the fallback for manual/offline invocation (no env set). + _env_out = os.environ.get('PSX_OVERLAY_CACHE_DIR') + if _env_out: + if _env_out != args.out_dir: + print(f'[cache] PSX_OVERLAY_CACHE_DIR overrides --out-dir: {_env_out}') + args.out_dir = _env_out + _env_cap = os.environ.get('PSX_OVERLAY_CAPTURES') + if _env_cap: + if _env_cap != args.captures: + print(f'[cache] PSX_OVERLAY_CAPTURES overrides --captures: {_env_cap}') + args.captures = _env_cap + if not args.captures: + ap.error('no captures file: set PSX_OVERLAY_CAPTURES (runtime injects it) ' + 'or pass --captures for manual/offline use') + # Resolve the recompiler exe and runtime-include to absolute paths up front. # game.toml's overlay_autocompile_cmd uses paths RELATIVE to the project root # (the runtime spawns this script via cmd.exe with cwd = project root) so the