overlay: add tcc tier (toolchain-free fallback) replacing sljit; extract overlay_backend

Compiler-neutral tier-selection policy extracted from the mis-named
overlay_sljit.{c,h} into NEW overlay_backend.{c,h} (OverlayBackend enum +
resolve/name/active). overlay_sljit.{c,h} is now just the deprecated emitter.

New tcc tier. gcc and tcc run the IDENTICAL pipeline (recompiler -> C ->
compiler -> DLL -> loader); only the compiler binary differs, so both map to the
autocompile-spawn provider (code_provider.c) and the COMMAND is chosen by the
resolved backend (main.cpp). compile_overlays.py gains --compiler gcc|tcc: tcc
writes a separate tcc/ cache namespace, BOM-strips the C + a copy of the include
headers (tcc 0.9.27 can't skip a UTF-8 BOM), and uses tcc's built-in linker.

Backends (overlay_backend): auto (gcc if a gcc TOOLCHAIN is present, else tcc --
was sljit), gcc, tcc, auto-no-gcc (dev/test: force tcc even with gcc present, to
exercise the toolchain-less user path), sljit (DEPRECATED, gated off, lowest).
Priority: static > gcc shard > tcc shard > sljit(off) > interp. The loader scans
gcc/ then tcc/ and dedups by region+crc (filename) so a gcc shard always wins a
tie.

Self-contained tcc: with no explicit overlay_autocompile_cmd_tcc, the runtime
CONSTRUCTS the tcc command from a toolchain bundled beside the exe
(<exe>/overlay_toolchain/: embedded python + tcc + recompiler + compile_overlays.py
+ runtime headers) -- no system python or gcc needed. config_loader gains
overlay_autocompile_cmd_tcc (a dev override). autocompile cmd buffers enlarged for
the constructed path-heavy command.

Compilation stays OFF the dispatch/render thread (off-process autocompile
subprocess, same model as gcc); the main thread only spawns it and dlopens the
finished DLL.

Validated: MMX6 with overlay_backend=auto-no-gcc fills the tcc/ namespace live
from the bundled toolchain and plays through save-load + gameplay -- the exact
path the sljit tier softlocked on. The gcc path is untouched.

Runtime-only, no regen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-25 13:10:57 -07:00
parent 71d6550eef
commit c2a3f6ad79
13 changed files with 310 additions and 83 deletions
+5
View File
@@ -140,6 +140,11 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
toml::find<std::string>(runtime, "overlay_autocompile_cmd");
rt.has_overlay_autocompile_cmd = !rt.overlay_autocompile_cmd.empty();
}
if (runtime.contains("overlay_autocompile_cmd_tcc")) {
rt.overlay_autocompile_cmd_tcc =
toml::find<std::string>(runtime, "overlay_autocompile_cmd_tcc");
rt.has_overlay_autocompile_cmd_tcc = !rt.overlay_autocompile_cmd_tcc.empty();
}
if (runtime.contains("overlay_backend")) {
rt.overlay_backend = toml::find<std::string>(runtime, "overlay_backend");
}
+16 -6
View File
@@ -98,12 +98,22 @@ struct RuntimeConfig {
bool has_overlay_autocompile_cmd = false;
std::string overlay_autocompile_cmd;
// overlay_backend: Tier-2 codegen backend selection (SLJIT.md §1).
// "auto" (default, empty == auto) | "gcc" | "sljit". auto resolves to gcc
// when overlay_autocompile_cmd is configured (a dev machine with a
// toolchain), else sljit (self-contained / toolchain-less). The env var
// PSX_OVERLAY_BACKEND overrides this at runtime. gcc stays the default
// until the sljit emitter passes the same-state differential gate.
// overlay_autocompile_cmd_tcc: the SAME pipeline as overlay_autocompile_cmd
// but invoking the bundled, toolchain-free TinyCC compiler instead of gcc.
// The runtime spawns THIS command instead of the gcc one when the resolved
// backend is tcc (a machine without a gcc toolchain, or overlay_backend =
// "tcc"/"auto-no-gcc"). Lets gcc stay the dev/production path while tcc is
// the user fallback. When unset, a tcc-resolved backend has no compiler and
// overlay gaps fall to the interpreter.
bool has_overlay_autocompile_cmd_tcc = false;
std::string overlay_autocompile_cmd_tcc;
// overlay_backend: overlay tier-selection (overlay_backend.h).
// "auto" (default, empty == auto) | "gcc" | "tcc" | "auto-no-gcc" | "sljit"
// (deprecated). auto resolves to gcc when a gcc TOOLCHAIN is present (a dev /
// production box), else tcc (bundled, toolchain-free). "auto-no-gcc" forces
// the tcc branch even with gcc present (simulate a toolchain-less user box;
// gcc shards still load). The env var PSX_OVERLAY_BACKEND overrides at runtime.
std::string overlay_backend;
// ---- [video] block — visual enhancement options ----
+57
View File
@@ -0,0 +1,57 @@
#ifndef OVERLAY_BACKEND_H
#define OVERLAY_BACKEND_H
/* overlay_backend — compiler-NEUTRAL overlay tier-selection policy.
*
* This is NOT specific to any one producer. It decides which native overlay
* tier fills a gap, in priority order:
*
* static > gcc shard > tcc shard > sljit shard (deprecated) > interp
*
* - gcc: spawn-gcc -> overlay DLL. The developer / production "holy grail"
* (best-optimized; the shards shipped in releases).
* - tcc: spawn-tcc -> overlay DLL. Bundled, toolchain-free user fallback that
* fills regions the shipped gcc cache misses on a machine without gcc.
* - sljit: in-process JIT. DEPRECATED (it mis-compiled; gated off) — kept only
* until it is removed, and always the lowest native priority.
*
* gcc and tcc go through the IDENTICAL pipeline (recompiler -> C -> compiler ->
* DLL -> loader); they differ only in the compiler binary the autocompile
* command runs. So both map to the same in-runtime "code provider" (the
* autocompile spawn) — see code_provider.c — and which compiler runs is chosen
* here by the resolved backend.
*/
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
OVERLAY_BACKEND_AUTO = 0, /* gcc if a gcc toolchain is present, else tcc */
OVERLAY_BACKEND_GCC = 1, /* force spawn-gcc->DLL (dev / production shards) */
OVERLAY_BACKEND_SLJIT = 2, /* DEPRECATED in-process sljit producer (gated off) */
OVERLAY_BACKEND_TCC = 3, /* spawn-tcc->DLL (bundled, toolchain-free fallback) */
OVERLAY_BACKEND_AUTO_NO_GCC = 4 /* dev/test: resolve like AUTO but pretend no gcc toolchain
* (gcc shards still LOAD; gaps fill via tcc) — simulate a
* toolchain-less user box on a dev machine. */
} OverlayBackend;
/* Resolve the effective backend once at startup. `gcc_toolchain_available` is 1
* when a gcc toolchain is actually reachable AND a compile command is wired (a
* real dev/production box). Precedence: env PSX_OVERLAY_BACKEND > game.toml
* [runtime] overlay_backend (`cfg`) > AUTO. AUTO -> gcc if the toolchain is
* present, else tcc. AUTO_NO_GCC -> tcc regardless (the simulate-user mode).
* The result is cached + logged. */
OverlayBackend overlay_backend_resolve(const char *cfg, int gcc_toolchain_available);
/* The cached resolution (OVERLAY_BACKEND_AUTO until resolve() runs). */
OverlayBackend overlay_backend_active(void);
/* "auto" | "gcc" | "sljit" | "tcc" | "auto-no-gcc". */
const char *overlay_backend_name(OverlayBackend b);
#ifdef __cplusplus
}
#endif
#endif /* OVERLAY_BACKEND_H */
+3 -16
View File
@@ -37,22 +37,9 @@ extern "C" {
/* The native shard ABI — identical to overlay_loader's OverlayFn. */
typedef void (*OverlaySljitFn)(CPUState *);
/* Backend selection policy (config + PSX_OVERLAY_BACKEND env override). */
typedef enum {
OVERLAY_BACKEND_AUTO = 0, /* gcc if a toolchain is configured, else sljit */
OVERLAY_BACKEND_GCC = 1, /* force the spawn-gcc->DLL producer (dev) */
OVERLAY_BACKEND_SLJIT = 2 /* force the in-process sljit producer */
} OverlayBackend;
/* Resolve the active backend once at startup. autocompile_configured indicates
* whether a gcc compile command is wired (game.toml overlay_autocompile_cmd).
* Reads PSX_OVERLAY_BACKEND (auto|gcc|sljit) env; falls back to `cfg` (from
* [runtime] overlay_backend) then AUTO. Result is logged and cached. */
OverlayBackend overlay_backend_resolve(const char *cfg, int autocompile_configured);
/* The cached resolution (OVERLAY_BACKEND_AUTO until resolve() runs). */
OverlayBackend overlay_backend_active(void);
const char *overlay_backend_name(OverlayBackend b);
/* Backend-selection policy (OverlayBackend enum + resolve/name/active) now lives
* in overlay_backend.h — it is compiler-neutral (gcc/tcc/sljit/auto) and not part
* of the sljit emitter. */
/* sljit availability — always 1 once the lib is linked (no external dep). */
int overlay_sljit_available(void);
+1
View File
@@ -155,6 +155,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/overlay_loader.c
${PSXRECOMP_ROOT}/runtime/src/overlay_compile_worker.c
${PSXRECOMP_ROOT}/runtime/src/overlay_sljit.c
${PSXRECOMP_ROOT}/runtime/src/overlay_backend.c
${PSXRECOMP_ROOT}/runtime/src/autocompile.c
${PSXRECOMP_ROOT}/runtime/src/code_provider.c
${PSXRECOMP_ROOT}/runtime/src/event_ring.c
+3 -2
View File
@@ -14,7 +14,8 @@
extern void overlay_loader_rescan(void);
static char s_cmd[1024];
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];
enum { AC_IDLE = 0, AC_RUNNING = 1, AC_DONE = 2 };
@@ -134,7 +135,7 @@ int autocompile_request(void) {
/* cmd.exe /C resolves the command via PATH and supports the relative
* paths in the configured line (cwd = project root). */
char full[1200];
char full[4200];
snprintf(full, sizeof(full), "cmd.exe /C %s", s_cmd);
STARTUPINFOA si;
+6
View File
@@ -10,6 +10,7 @@
#include "code_provider.h"
#include "autocompile.h"
#include "overlay_sljit.h"
#include "overlay_backend.h"
/* ---- gcc provider: pass-through to the validated spawn->DLL->rescan path --- */
/* The hook signatures match autocompile_* exactly, so we point straight at
@@ -68,6 +69,11 @@ void code_provider_init(const char *cfg_backend, int gcc_configured) {
* AUTO, so this maps cleanly to one of the two providers. It also logs the
* decision (surfaced via the sljit_status debug command). */
OverlayBackend b = overlay_backend_resolve(cfg_backend, gcc_configured);
/* The "gcc" provider is really the spawn-a-compiler->DLL->rescan path; it is
* compiler-agnostic (the actual compiler is whatever overlay_autocompile_cmd
* runs — gcc OR tcc, selected in main.cpp by the resolved backend). So BOTH
* gcc and tcc map to it. Only the deprecated in-process sljit producer is
* separate. */
s_active = (b == OVERLAY_BACKEND_SLJIT) ? &s_sljit : &s_gcc;
}
+1
View File
@@ -21,6 +21,7 @@
#include "overlay_capture.h"
#include "code_provider.h"
#include "overlay_sljit.h"
#include "overlay_backend.h"
#include "overlay_compile_worker.h"
#include "cpu_state.h"
#include "dma.h"
+65 -17
View File
@@ -15,6 +15,7 @@
#include "autocompile.h"
#include "code_provider.h"
#include "overlay_sljit.h"
#include "overlay_backend.h"
#include "gpu.h"
#include "gpu_sw_renderer.h"
#include "gpu_render.h"
@@ -1980,24 +1981,71 @@ int main(int argc, char** argv) {
* The background compile additionally needs a configured
* command; autocompile_request() no-ops without one. */
overlay_autocapture_set_enabled(1);
if (gc.runtime.has_overlay_autocompile_cmd) {
autocompile_configure(
gc.runtime.overlay_autocompile_cmd.c_str(),
gc.project_root.string().c_str());
std::fprintf(stdout,
"psxrecomp: overlay autocompile enabled\n");
/* Resolve the overlay tier first so we wire the RIGHT compiler's
* autocompile command. gcc is "available" only when a gcc cmd is
* configured AND a gcc toolchain is actually reachable (a real
* dev/production box). auto => gcc if so, else tcc; auto-no-gcc =>
* tcc even with gcc present (simulate a toolchain-less user box).
* env PSX_OVERLAY_BACKEND overrides. Tiers: static > gcc > tcc >
* (sljit, deprecated/off) > interp. */
const char *cfg_backend = gc.runtime.overlay_backend.empty()
? nullptr : gc.runtime.overlay_backend.c_str();
int gcc_avail = gc.runtime.has_overlay_autocompile_cmd
&& autocompile_toolchain_available();
OverlayBackend eff = overlay_backend_resolve(cfg_backend, gcc_avail);
/* gcc and tcc run the IDENTICAL recompiler->C->DLL->load pipeline;
* only the compiler binary differs. Wire the autocompile spawn with
* the command for the resolved tier (tcc cmd for the tcc tier, gcc
* cmd otherwise). gcc shards already on disk still LOAD either way
* (the loader is compiler-blind), so a tcc box uses shipped gcc
* shards first and fills the rest with tcc. */
std::string built_tcc_cmd; /* runtime-constructed bundled tcc cmd */
const std::string *ac_cmd = nullptr;
if (eff == OVERLAY_BACKEND_TCC) {
if (gc.runtime.has_overlay_autocompile_cmd_tcc) {
ac_cmd = &gc.runtime.overlay_autocompile_cmd_tcc; /* explicit override (dev) */
} else {
/* PRODUCTION: construct the tcc autocompile cmd from the
* self-contained toolchain bundled beside the exe
* (<exe>/overlay_toolchain/ = embedded python + tcc +
* recompiler + compile_overlays.py + runtime headers). No
* system python or gcc required. */
extern int g_psx_cps_mode;
std::filesystem::path xd = exe_dir_from_argv(argv[0]);
std::filesystem::path tk = xd / "overlay_toolchain";
std::filesystem::path py = tk / "python" / "python.exe";
if (std::filesystem::exists(py)) {
built_tcc_cmd =
py.string() + " " + (tk / "compile_overlays.py").string() +
" --captures " + (xd / "overlay_captures.json").string() +
" --game-toml " + std::string(game_config_path ? game_config_path : "game.toml") +
" --recompiler " + (tk / "psxrecomp-game.exe").string() +
" --runtime-include " + (tk / "include").string() +
" --out-dir " + (xd / "cache").string() +
(g_psx_cps_mode ? " --cps" : "") +
" --compiler tcc --tcc " + (tk / "tcc" / "tcc.exe").string();
ac_cmd = &built_tcc_cmd;
std::fprintf(stdout,
"psxrecomp: tcc tier using bundled toolchain (%s)\n",
tk.string().c_str());
} else {
std::fprintf(stdout,
"psxrecomp: tcc tier active but no bundled toolchain at %s "
"(overlay gaps -> interpreter)\n", tk.string().c_str());
}
}
} else {
if (gc.runtime.has_overlay_autocompile_cmd)
ac_cmd = &gc.runtime.overlay_autocompile_cmd;
}
/* Resolve the Tier-2 codegen backend now that we know whether a
* compile command is wired. auto => gcc only when a C compiler is
* ACTUALLY reachable (a real dev box — not merely a configured
* command string, which the shipped game.toml always carries),
* else sljit (toolchain-less production); env PSX_OVERLAY_BACKEND
* overrides. The active provider is what the capture/dispatch
* spine produces code through. */
code_provider_init(
gc.runtime.overlay_backend.empty()
? nullptr : gc.runtime.overlay_backend.c_str(),
autocompile_configured() && autocompile_toolchain_available());
if (ac_cmd) {
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));
}
code_provider_init(cfg_backend, gcc_avail);
/* Now that the backend is resolved, apply the sljit live policy:
* a toolchain-less (sljit) machine runs validated shards live so
* it self-improves on the normal play path; PSX_OVERLAY_SLJIT_LIVE
+66
View File
@@ -0,0 +1,66 @@
/* overlay_backend.c — compiler-neutral overlay tier-selection policy.
* See overlay_backend.h. Moved out of overlay_sljit.c (which is now just the
* deprecated sljit emitter): deciding gcc vs tcc vs sljit is not sljit's job. */
#include "overlay_backend.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
static OverlayBackend s_active = OVERLAY_BACKEND_AUTO;
static int s_resolved = 0;
static char s_last_msg[256];
static void backend_log(const char *fmt, ...) {
va_list ap; va_start(ap, fmt);
vsnprintf(s_last_msg, sizeof(s_last_msg), fmt, ap);
va_end(ap);
}
const char *overlay_backend_name(OverlayBackend b) {
switch (b) {
case OVERLAY_BACKEND_GCC: return "gcc";
case OVERLAY_BACKEND_SLJIT: return "sljit";
case OVERLAY_BACKEND_TCC: return "tcc";
case OVERLAY_BACKEND_AUTO_NO_GCC: return "auto-no-gcc";
default: return "auto";
}
}
static OverlayBackend parse_backend(const char *s, OverlayBackend dflt) {
if (!s || !*s) return dflt;
if (!strcmp(s, "gcc")) return OVERLAY_BACKEND_GCC;
if (!strcmp(s, "sljit")) return OVERLAY_BACKEND_SLJIT;
if (!strcmp(s, "tcc")) return OVERLAY_BACKEND_TCC;
if (!strcmp(s, "auto-no-gcc")) return OVERLAY_BACKEND_AUTO_NO_GCC;
if (!strcmp(s, "auto")) return OVERLAY_BACKEND_AUTO;
return dflt;
}
OverlayBackend overlay_backend_resolve(const char *cfg, int gcc_toolchain_available) {
/* Precedence: env PSX_OVERLAY_BACKEND > game.toml [runtime] overlay_backend
* (cfg) > AUTO. Tier order is static > gcc > tcc > (sljit, deprecated/off) >
* interp. AUTO prefers gcc when a gcc TOOLCHAIN is actually present (a dev /
* production box), else tcc (the bundled toolchain-free user fallback).
* AUTO_NO_GCC forces the tcc branch even when gcc IS present — it simulates a
* toolchain-less user box on a dev machine (gcc shards still LOAD via the
* loader; only the gap-fill compiler is forced to tcc). */
OverlayBackend want = parse_backend(getenv("PSX_OVERLAY_BACKEND"),
parse_backend(cfg, OVERLAY_BACKEND_AUTO));
OverlayBackend eff = want;
if (want == OVERLAY_BACKEND_AUTO)
eff = gcc_toolchain_available ? OVERLAY_BACKEND_GCC : OVERLAY_BACKEND_TCC;
else if (want == OVERLAY_BACKEND_AUTO_NO_GCC)
eff = OVERLAY_BACKEND_TCC;
s_active = eff;
s_resolved = 1;
backend_log("backend resolved: want=%s effective=%s (gcc_toolchain=%d)",
overlay_backend_name(want), overlay_backend_name(eff),
gcc_toolchain_available);
return eff;
}
OverlayBackend overlay_backend_active(void) { return s_active; }
+25 -1
View File
@@ -2,6 +2,7 @@
#include "overlay_api.h"
#include "code_provider.h"
#include "overlay_sljit.h"
#include "overlay_backend.h"
#include "crc32.h"
#include "dirty_ram_interp.h"
#include "interrupts.h"
@@ -475,6 +476,19 @@ static int cache_idx_has_path(const char *path) {
return 0;
}
/* Dedup by FILENAME (<addr8>_<crc8>.dll = region+crc). Used so a tcc/ shard for a
* region already covered by a gcc/ shard is skipped: scan_cache_dir scans gcc/
* FIRST, so gcc always wins the tie (tier order gcc > tcc). Also subsumes the
* per-path rescan-idempotence check (same dir => same filename). */
static int cache_idx_has_basename(const char *fname) {
for (int i = 0; i < s_cache_idx_count; i++) {
const char *b = strrchr(s_cache_idx[i].path, '/');
b = b ? b + 1 : s_cache_idx[i].path;
if (strcmp(b, fname) == 0) return 1;
}
return 0;
}
/* Canonical cache arch-abi tag (see SLJIT.md §4 — caches are namespaced per
* backend AND per target so a Windows-x64 gcc DLL and, later, a same-OS arm64
* or an sljit blob for the same fragment never comingle). compile_overlays.py
@@ -525,7 +539,9 @@ static void scan_one_cache_dir(const char *dir) {
if (s_cache_idx_count >= CACHE_IDX_CAP) break;
char full[768];
snprintf(full, sizeof(full), "%s/%s", dir, fd.cFileName);
if (cache_idx_has_path(full)) continue; /* rescan idempotence */
/* skip if this region+crc is already indexed (rescan idempotence, AND a
* higher-priority namespace already covers it — gcc/ scanned before tcc/). */
if (cache_idx_has_basename(fd.cFileName)) continue;
CacheEntry *e = &s_cache_idx[s_cache_idx_count++];
e->region_start = addr;
snprintf(e->path, sizeof(e->path), "%s", full);
@@ -544,10 +560,18 @@ static void scan_one_cache_dir(const char *dir) {
* older flat / unversioned caches are simply ignored and regenerated.) */
static void scan_cache_dir(void) {
char dir[768];
/* Tier order: scan gcc/ FIRST (highest native priority — the dev/production
* shards), THEN tcc/ (the toolchain-free fallback). scan_one_cache_dir dedups
* by filename (region+crc), so a tcc shard for a region a gcc shard already
* covers is skipped — gcc always wins the tie. */
snprintf(dir, sizeof(dir), "%s/%s/gcc/%s/cg%d_%08x",
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER,
(unsigned)PSX_OVERLAY_CODEGEN_HASH);
scan_one_cache_dir(dir);
snprintf(dir, sizeof(dir), "%s/%s/tcc/%s/cg%d_%08x",
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER,
(unsigned)PSX_OVERLAY_CODEGEN_HASH);
scan_one_cache_dir(dir);
}
/* ---- Persisted sljit shard cache (Stage 2, SLJIT_PERSIST_CACHE.md) -------- */
+2 -38
View File
@@ -26,7 +26,6 @@
#endif
/* ---- counters ---------------------------------------------------------- */
static OverlayBackend s_active = OVERLAY_BACKEND_AUTO;
/* RECURSION_BUG.md §25 — 1 when the build is continuation-passing (set by a
* constructor in the generated CPS dispatch). Under CPS this JIT must emit the
* tail-transfer contract: jr $ra publishes cpu->pc, and call-containing
@@ -34,7 +33,6 @@ static OverlayBackend s_active = OVERLAY_BACKEND_AUTO;
* nested psx_sljit_call. Defined in overlay_loader.c. */
extern int g_psx_cps_mode;
static int s_resolved = 0;
static int s_selftest_ok = -1; /* -1 = not run */
static uint64_t s_compiles = 0;
static uint64_t s_declines = 0;
@@ -47,42 +45,8 @@ static void sljit_log(const char *fmt, ...) {
va_end(ap);
}
/* ---- backend selection policy ----------------------------------------- */
const char *overlay_backend_name(OverlayBackend b) {
switch (b) {
case OVERLAY_BACKEND_GCC: return "gcc";
case OVERLAY_BACKEND_SLJIT: return "sljit";
default: return "auto";
}
}
static OverlayBackend parse_backend(const char *s, OverlayBackend dflt) {
if (!s || !*s) return dflt;
if (!strcmp(s, "gcc")) return OVERLAY_BACKEND_GCC;
if (!strcmp(s, "sljit")) return OVERLAY_BACKEND_SLJIT;
if (!strcmp(s, "auto")) return OVERLAY_BACKEND_AUTO;
return dflt;
}
OverlayBackend overlay_backend_resolve(const char *cfg, int autocompile_configured) {
/* Precedence: env PSX_OVERLAY_BACKEND > game.toml [runtime] overlay_backend
* (cfg) > AUTO. AUTO prefers gcc when a compile command is wired (a dev
* machine), else sljit (self-contained production / toolchain-less dev). */
OverlayBackend want = parse_backend(getenv("PSX_OVERLAY_BACKEND"),
parse_backend(cfg, OVERLAY_BACKEND_AUTO));
OverlayBackend eff = want;
if (want == OVERLAY_BACKEND_AUTO)
eff = autocompile_configured ? OVERLAY_BACKEND_GCC : OVERLAY_BACKEND_SLJIT;
s_active = eff;
s_resolved = 1;
sljit_log("backend resolved: want=%s effective=%s (autocompile=%d)",
overlay_backend_name(want), overlay_backend_name(eff),
autocompile_configured);
return eff;
}
OverlayBackend overlay_backend_active(void) { return s_active; }
/* Backend-selection policy moved to overlay_backend.{c,h} (it is compiler-
* neutral — gcc/tcc/sljit/auto — and does not belong in the sljit emitter). */
int overlay_sljit_available(void) {
/* Debug A/B knob: PSX_NO_SLJIT=1 makes sljit report unavailable, so the
+60 -3
View File
@@ -1116,14 +1116,64 @@ def _toolchain_env(gcc: str):
return env, None
# ---- TinyCC (tcc) overlay compile — toolchain-free user fallback -----------
# tcc has its own built-in linker (no ld/collect2) and bundles its own headers,
# so it is self-contained. The only friction is that tcc 0.9.27 does not skip a
# UTF-8 BOM, and the recompiler's runtime headers carry one — so we feed tcc a
# BOM-stripped copy of the include dirs (used ONLY for tcc's -I; the codegen hash
# still derives from the real headers, so the cache dir matches the runtime).
_TCC_BOMFREE_INC = {} # orig include dir -> bom-stripped temp dir (memoized)
def _bom_free_incdir(d: str) -> str:
d = os.path.abspath(d)
if d in _TCC_BOMFREE_INC:
return _TCC_BOMFREE_INC[d]
out = tempfile.mkdtemp(prefix='tcc_inc_')
for name in os.listdir(d):
if not name.endswith('.h'):
continue
with open(os.path.join(d, name), 'rb') as f:
data = f.read()
if data[:3] == b'\xef\xbb\xbf':
data = data[3:]
with open(os.path.join(out, name), 'wb') as f:
f.write(data)
_TCC_BOMFREE_INC[d] = out
return out
def _compile_dll_tcc(c_path: str, out_dll: str, include_dirs, flavor: int,
tcc: str) -> bool:
# strip a UTF-8 BOM off the overlay C itself (tcc 0.9.27 chokes on it)
with open(c_path, 'rb') as f:
data = f.read()
if data[:3] == b'\xef\xbb\xbf':
with open(c_path, 'wb') as f:
f.write(data[3:])
cmd = [tcc, '-shared',
'-DPSX_OVERLAY_DLL_BUILD',
f'-DPSX_OVERLAY_FLAVOR={int(flavor)}',
c_path, '-o', out_dll]
for d in include_dirs:
cmd.append('-I' + _bom_free_incdir(d))
print(f' compile (tcc): {" ".join(cmd)}')
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
print(f' TCC COMPILE ERROR (exit {r.returncode}):\n{r.stderr or r.stdout}')
return False
return True
def compile_dll(c_path: str, out_dll: str, include_dirs: list[str],
gcc: str = 'gcc', flavor: int = 0) -> bool:
gcc: str = 'gcc', flavor: int = 0,
compiler: str = 'gcc', tcc: str = 'tcc') -> bool:
import platform
# Absolute OS-native paths: gcc invoked from cmd.exe (the runtime's
# autocompile spawn) silently fails (exit 1, no stderr) on relative
# forward-slash paths; absolute backslash paths work in every context.
c_path = os.path.abspath(c_path)
out_dll = os.path.abspath(out_dll)
if compiler == 'tcc':
return _compile_dll_tcc(c_path, out_dll, include_dirs, flavor, tcc)
env, tc_dir = _toolchain_env(gcc)
includes = [f'-I{os.path.abspath(d)}' for d in include_dirs]
# On Windows, DLLs use PE relocations — -fPIC triggers GCC CRT init
@@ -1172,6 +1222,12 @@ def main():
help='cache root dir (default: build-dev/cache)')
ap.add_argument('--gcc', default='gcc',
help='GCC binary (default: gcc)')
ap.add_argument('--compiler', choices=['gcc', 'tcc'], default='gcc',
help='overlay shard compiler: gcc (default; dev/production, '
'best-optimized) or tcc (bundled, toolchain-free user '
'fallback). tcc shards land in the tcc/ cache namespace.')
ap.add_argument('--tcc', default='tcc',
help='TinyCC binary (used when --compiler tcc)')
ap.add_argument('--force', action='store_true',
help='recompile even if output already exists')
ap.add_argument('--static', action='store_true',
@@ -1221,7 +1277,7 @@ def main():
# overlay_loader.c scan_cache_dir(). Pre-1.0: no legacy fallback.
cg = codegen_ver(args.runtime_include)
ch = codegen_hash(args.runtime_include)
cache_dir = os.path.join(args.out_dir, game_id, 'gcc', cache_arch_abi(),
cache_dir = os.path.join(args.out_dir, game_id, args.compiler, cache_arch_abi(),
f'cg{cg}_{ch:08x}')
os.makedirs(cache_dir, exist_ok=True)
print(f'Cache dir: {cache_dir} (codegen ver {cg}, hash {ch:08x})')
@@ -1393,7 +1449,8 @@ def main():
include_dirs.append(p)
success = compile_dll(patched_c, dll_path, include_dirs,
gcc=args.gcc, flavor=args.flavor)
gcc=args.gcc, flavor=args.flavor,
compiler=args.compiler, tcc=args.tcc)
if success:
# Emit the per-entry code-range manifest beside the DLL.
# The loader keys it by the same filename stem with .ranges