overlay: unify cache read/write location + fix missing ws_cull symbol in DLLs

Two framework bugs that made overlay variants run interpreted forever (the
"agonizing lag" in overlay-heavy areas like Tomba's dwarf village):

1. Cache read/write divergence. The loader reads <exe>/cache and the runtime
   writes captures to <exe>/overlay_captures.json, but the gcc autocompile path
   used the game.toml command verbatim, whose hard-coded --out-dir/--captures
   pointed elsewhere (build-stable/build-modern). So compiled shards landed where
   the loader never looked and captures were read stale -> the variant never went
   native. Fix: the runtime now injects PSX_OVERLAY_CACHE_DIR / PSX_OVERLAY_CAPTURES
   (the canonical loader paths) into every autocompile spawn; compile_overlays.py
   honors them over any CLI flag (--captures now optional). One source of truth for
   the cache location, uniform across all games, dev or prod. gcc/tcc stay separated
   by the existing <compiler> namespace.

2. Overlay DLL preamble missing psx_ws_cull_sltiu. auto_screen_x emits this helper
   at every width-cull site into EVERY build's C (identity at 4:3), and the main EXE
   links it from gpu.c, but the overlay DLL preamble never defined it -> any overlay
   variant with cull sites failed to link -> no shard -> interpreter. Fix: provide
   the self-contained helper (depends only on the already-forwarded psx_ws_x_margin)
   in the DLL preamble.

Proven on Tomba dwarf village: dispatch_native 29/s -> 11454/s (was ~97% interp,
now ~97% native).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-25 18:57:57 -07:00
parent 1915b5c1bf
commit e7f7fb6cf0
4 changed files with 78 additions and 4 deletions
+10
View File
@@ -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
* (<exe>/cache and <exe>/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
+18
View File
@@ -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];
+12 -2
View File
@@ -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 = <exe>/cache,
* <exe>/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:
+38 -2
View File
@@ -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 '
'<exe>/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 <exe>/cache and <exe>/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