Commit Graph

287 Commits

Author SHA1 Message Date
Alex Vanderveen 84901d3131 Merge PR #156 (feat/ws-exact-nclip) into feat/rbengine
Widescreen precision: exact-sign NCLIP, GTE clamp rescue,
backend-invariant entry hooks — plus perf round 1, pacing fixes,
self-profiler, and IRQ-redirect recompiler fixes from the branch.

Conflict resolutions:
- runtime/src/gpu_gl_renderer.c (bezel_tiles_col): combine both bezel
  mark-sizing fixes — height-anchored size with PSX_BEZEL_SCALE
  (rbengine, fixes ultrawide growth) floored by the caller's
  window-proportioned mark_px (PR, fixes thin-margin shrink); an
  explicit env override wins in both directions.
- runtime/src/main.cpp: keep both additions — netplay_resolve_link_mode
  (rbengine) and the PSX_SELF_PROFILE sampler (PR).
- runtime/tests/test_fps_telemetry_opt_in.py: re-pin the structural
  guard's format needles to the PR's dual speed readout strings
  (VBlank/Render/Display); the opt-in guards are unchanged.

Verified: psxrecomp-game rebuilds; gpu_gl_renderer.c compiles clean;
ctest failure set identical to pre-merge HEAD (no regressions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdzpVVH912V9ktt4n4kntj
2026-08-20 14:48:29 -04:00
Alex Vanderveen d0536d7dcd duckstation tools 2026-08-20 13:47:12 -04:00
Alex Vanderveen 0e5d463356 duckstation linux build + frame capture diagnostics 2026-08-20 11:26:37 -04:00
Alex Vanderveen 1f65200e6f bump 2026-08-19 23:35:17 -04:00
Alex Vanderveen 3cb2a60dc3 bump 2026-08-19 22:50:13 -04:00
tetrisgm f3e3264822 perf: memoized static-identity checks, gated dispatch rings, inline i-cache hit
From the steady-state dispatch-cost audit of the WipEout 3 static bake
(80,847 baked variants; ~1M dispatches/s):

1. DOMINANT: psx_overlay_static_code_matches used a shared 4096-slot
   cache with no eviction — the first ~4096 tuples dispatched owned it
   forever, and every other variant paid a full 4096-entry linear probe
   PLUS a full CRC32 of its code bytes on EVERY dispatch, with the
   result uncached (entry NULL when the table is full). Boot-order
   dependence of which tuples win slots explains the session-dependent
   0.83-1.02x seesaw. New psx_overlay_static_code_matches_memo takes a
   per-variant state[2] emitted by the generator next to each ranges
   array: repeat calls are the page-gen sum plus one compare; the CRC
   runs only on first sight or after a real write to a covered page.
   Identical invalidation semantics; unbounded capacity; no probing.
   compile_overlays.py emits the state and calls the memo variant.

2. fntrace dispatch-tail + SP-domain rings gated out of play builds
   (PSX_NO_DEBUG_TOOLS): their per-dispatch writes (incl. a
   psx_get_cycle_count() call each) had no reader — the TCP server that
   dumps them is compiled out. Abnormal-exit forensics in play builds is
   carried by the pc0 escape journal. Debug builds unchanged.

3. Per-instruction psx_icache_fetch was an out-of-line call whose hit
   path is 3 branches + a tag compare; generated code now uses the
   inline tag-hit (psx_icache_fetch_interp) in-process, keeping the
   exported slow entry for DLL shards (identical cache evolution — the
   inline's miss path IS that entry). Both emitters.

Also converges the image-edge fallthrough publish with upstream PR #161
(cps_enabled_-gated; non-CPS keeps the legacy check-only fall-through).
Upstream PR #158 (xlate capture default off) is already superseded here
by the release-build gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:30:36 -07:00
tetrisgm 6ead85c9da recompiler/runtime: honor IRQ redirects at compiled leaders; revive async-RFE latch; pc0 escape journal
Fixes the deterministic top-level "execution completed, PC=0" ~10 s
into WipEout 3 boot with the static bake linked (the 120 Hz core bug).

Root cause (CPS-contract audit, H1): every emitted block-leader IRQ
check was `psx_check_interrupts_at(cpu, X); cpu->pc = X;` — whatever
resume PC the exception epilogue published (guest RFE to a non-EPC
target: longjmp-style card-ISR handlers, SetCustomExitFromException,
thread switch) was unconditionally overwritten by the static transfer.
The dirty interpreter honors these redirects ("Handler resumed
elsewhere — surface to dispatch"); the static bake moved the IRQ
take-points from interp pumps onto compiled leaders, where the redirect
was dropped — the guest's continuation vanished, downstream state was
never established, and a later jr through a null pointer surfaced as a
silent top-level PC=0 exit.

- code_generator/full_function_emitter: both check emitters (and the
  install-slot hook) now emit the compiled analogue of the interp
  contract: `if (cpu->pc != 0 && ((cpu->pc ^ X) & 0x1FFFFFFF)) return;`
  after the check, surfacing the redirect to the trampoline. Applies to
  branch/jr/jalr/jal/split sites in the AOT text, the recompiled BIOS,
  and every overlay shard/static bake on regeneration.
- interrupts.c: the async-RFE resume latch (g_async_rfe_resume_pc,
  "Tomba 2 frame-1997 fix") was documented but NEVER ASSIGNED — the
  whole rescue in the traps/dirty sentinel gates was inert (audit H3).
  Latch it at every exception entry that installs a real EPC.
- pc0 escape journal (always-on, play builds): every runtime site that
  publishes/rescues a null PC records {site, frame, dispatch depth, ra,
  epc, extras} in a 64-entry ring, dumped at the abnormal-exit print.
  The play build's exit trace was blind (fntrace is debug-only:
  fntrace_seq=0 in the death artifact); publishes are rare so the
  always-on cost is a few stores.
- compile_overlays.py: PSX_STATIC_NO_ISOLATED=1 A/B guard to skip the
  isolated-fragment pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:39:34 -07:00
tetrisgm 8b323ffe92 coverage_vault: 8 MB dev-RAM aware bounds
Titles running the 8MB enhancement stream code into the extended banks
(0x200000..0x7FFFFF). The vault's 2 MB mask corrupted those capture
addresses and its bound rejected legitimate regions ("capture exceeds
PSX RAM"), which blocked compacting WipEout 3's merged capture history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 03:41:22 -07:00
tetrisgm 45f9d0f866 autocompile: pin the overlay flavor through the spawn env (PSX_OVERLAY_FLAVOR)
The runtime pins the cache dir and captures path into the spawned
compile's environment so the cache can never drift from where the loader
reads - but not the FLAVOR. compile_overlays.py's --flavor defaults to 0
(play), so an instrumented (flavor 2) runtime spawned compiles whose
shards its own loader then rejected: the overlay_loader_status message
said it plainly - "this build reads ..._f2 but shards exist under ..._f0
-> ALL overlays run INTERPRETED". On mod-heavy titles (WipEout 3's
framerate packages patch most of the game text) that is the difference
between full speed and ~0.2x, misread as general slowness for weeks.

The runtime now exports PSX_OVERLAY_FLAVOR alongside the other pins, and
the tool honors it over --flavor, so each build population self-heals.

Measured after the fix (instrumented, WipEout 3 ntsc120full8): shard
loads 0 -> 8 (45 funcs), interpreted insns 6.6M/s -> 1.0M/s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 02:56:27 -07:00
Alex Vanderveen e40df3092b Merge pull request #150 from tetrisgm/pr/present-shot-and-hires-pitch
Fix screenshot_hires pitch; add present_shot for the composed present surface
2026-08-17 20:24:46 -04:00
tetrisgm 777117c1f2 Fix screenshot_hires pitch; add present_shot for the composed frame
screenshot_hires passed the pixel width as out_pitch, but the resolves
(sw_render_display / sw_render_display_hires) step rows in BYTES, as the
present path does (main.cpp: sw * sizeof(uint32_t)). Each row advanced a
quarter of a row, tiling the frame 4x across and leaving the bottom three
quarters black. Verified on WipEout 3: content rows 65/256 -> 256/256.

The same handler could also emit untouched allocation: gr_scale() reports
the GL backend's internal scale, but sw_render_display_hires falls back to
the native resolve when the CPU hi-res mirror does not exist, filling w*h
of an ow*oh buffer while still returning non-zero. The buffer is now
calloc'd and the resolve must report FULL cover or the capture redoes
itself honestly at native size.

Add present_shot / present_shot_seq: a capture of the COMPOSED present
surface. Every existing capture resolves the display buffer BEFORE the
backend fits it to the window, so on a 508x256 display in a 4:3 window
they answer 508x256 while the player sees 640x480. That is correct for
faithfulness work but wrong for anything aspect-shaped -- a widescreen
change moves the GTE squash and the present fit, which is exactly the
stage those captures skip.

Fulfilled by whichever backend owns the present: SDL_RenderReadPixels on
the software path, glReadPixels before SwapWindow on GL (with the
bottom-up flip). Notes on the contract:

- GL fulfilment can run on the frame-interpolation thread (interp_present
  -> gl_swap_with_osd), not just the emu thread, so the path buffer and
  pending flag are mutex-guarded and the counters are atomic.
  present_shot_take() claims the request under the lock, so two present
  paths cannot fulfil the same shot.
- A staged shot is OVERWRITTEN by a later request rather than refused,
  as savestate's request_save_inner does. A present that never arrives
  therefore cannot wedge the command.
- Vulkan presents through its own swapchain and has no readback hook, so
  the request is refused there instead of being accepted and never
  fulfilled (CLAUDE.md rule 15).
- present_shot_seq advances on success AND failure so a polling client
  always terminates; its `wrote` field reports whether a PNG landed.
- The SDL2 branch sizes its buffer from SDL_GetRendererOutputSize rather
  than a viewport*scale reconstruction, which could be smaller than the
  region SDL_RenderReadPixels fills.

Also: debug_client gains positional-path screenshot_file/present_shot and
turbo/turbo_state mappings (they previously fell through to the raw
key=value handler); TCP_COMMANDS.md marks screenshot_hires as native (it
is registered on the native server and the blank N column read as
Beetle-only), documents the two new commands, and the generated index is
regenerated via tools/gen_tcp_commands.py.

Tested on WipEout 3 SE (PAL, OpenGL, RelWithDebInfo + PSX_DEBUG_TOOLS=ON):
capture matches the on-screen window at 1280x960; seq/wrote reports 1 on
success and 0 for an unwritable path; back-to-back staging is accepted.
The SDL software path, the SDL2 branches and Vulkan refusal are
compile-verified only.
2026-08-17 02:09:54 -07:00
Alex Vanderveen aa208a9100 loader optimizations pushing for 8mb performance 2026-08-17 00:42:48 -04:00
Alex Vanderveen 72e18d7d19 patch new project flow 2026-08-16 22:23:21 -04:00
Alex Vanderveen 15699d4005 new project consistency patching project naming with retcomm-studio, toolchain patch 2026-08-16 22:13:37 -04:00
Alex Vanderveen 76945d3a65 8mb ram patches + toolchain windows hotfix 2026-08-16 21:19:05 -04:00
tetrisgm 8fee64f178 Fix screenshot_hires pitch; add present_shot for the composed frame
screenshot_hires passed the pixel width as out_pitch, but the resolves
(sw_render_display / sw_render_display_hires) step rows in BYTES, as the
present path does (main.cpp: sw * sizeof(uint32_t)). Each row advanced a
quarter of a row, tiling the frame 4x across and leaving the bottom three
quarters black. Verified on WipEout 3: content rows 65/256 -> 256/256.

The same handler could also emit untouched allocation: gr_scale() reports
the GL backend's internal scale, but sw_render_display_hires falls back to
the native resolve when the CPU hi-res mirror does not exist, filling w*h
of an ow*oh buffer while still returning non-zero. The buffer is now
calloc'd and the resolve must report FULL cover or the capture redoes
itself honestly at native size.

Add present_shot / present_shot_seq: a capture of the COMPOSED present
surface. Every existing capture resolves the display buffer BEFORE the
backend fits it to the window, so on a 508x256 display in a 4:3 window
they answer 508x256 while the player sees 640x480. That is correct for
faithfulness work but wrong for anything aspect-shaped -- a widescreen
change moves the GTE squash and the present fit, which is exactly the
stage those captures skip.

Fulfilled by whichever backend owns the present: SDL_RenderReadPixels on
the software path, glReadPixels before SwapWindow on GL (with the
bottom-up flip). Notes on the contract:

- GL fulfilment can run on the frame-interpolation thread (interp_present
  -> gl_swap_with_osd), not just the emu thread, so the path buffer and
  pending flag are mutex-guarded and the counters are atomic.
  present_shot_take() claims the request under the lock, so two present
  paths cannot fulfil the same shot.
- A staged shot is OVERWRITTEN by a later request rather than refused,
  as savestate's request_save_inner does. A present that never arrives
  therefore cannot wedge the command.
- Vulkan presents through its own swapchain and has no readback hook, so
  the request is refused there instead of being accepted and never
  fulfilled (CLAUDE.md rule 15).
- present_shot_seq advances on success AND failure so a polling client
  always terminates; its `wrote` field reports whether a PNG landed.
- The SDL2 branch sizes its buffer from SDL_GetRendererOutputSize rather
  than a viewport*scale reconstruction, which could be smaller than the
  region SDL_RenderReadPixels fills.

Also: debug_client gains positional-path screenshot_file/present_shot and
turbo/turbo_state mappings (they previously fell through to the raw
key=value handler); TCP_COMMANDS.md marks screenshot_hires as native (it
is registered on the native server and the blank N column read as
Beetle-only), documents the two new commands, and the generated index is
regenerated via tools/gen_tcp_commands.py.

Tested on WipEout 3 SE (PAL, OpenGL, RelWithDebInfo + PSX_DEBUG_TOOLS=ON):
capture matches the on-screen window at 1280x960; seq/wrote reports 1 on
success and 0 for an unwritable path; back-to-back staging is accepted.
The SDL software path, the SDL2 branches and Vulkan refusal are
compile-verified only.
2026-08-16 17:09:26 -07:00
Alex Vanderveen 1be8f88839 Merge origin/master into feat/rbengine.
Keep the 8 MB RAM builtin alongside PGXP Precision after the master merge.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 14:11:58 -04:00
Alex Vanderveen 8fb75061b4 8mb mod for wipeout and recomp-net bump, rb engine bump, mods in netplay enabled. 2026-08-16 13:35:48 -04:00
Matthew Stanley 38bdeeaade pgxp: emit hook macros + dual build targets + flavor-namespaced caches (phase 2)
Emitter side of the value-propagation engine (ENHANCEMENTS.md G1.10):

- recompiler/src/pgxp_hook_emitter.{h,cpp} (new, in the codegen-hash
  surface): ONE hook grammar shared by BOTH emitters, wrapping loads/
  stores/COP2 transfers and precision-carrying ALU ops (moves, add/sub,
  or-merge, shifts, LUI, HI/LO, muldiv) with PGXP_*() macros. Clobberable
  sources are captured into locals before the statement. Precision-
  destroying ops (AND/XOR/SLT/...) are deliberately unhooked - validate-
  on-read drops their stale shadows for free.
- code_generator.cpp: central append in translate_instruction; LWC2/SWC2
  emissions capture the RAW loaded/stored word (a masked GTE register
  write must validate against the word as loaded).
- strict_translator.cpp: translate() wraps translate_impl()'s c_code with
  the shared grammar; deferred load-delay variants hook the psx_ldd temp.
- BIOS regenerated, fingerprints verified CHANGED (SCPH1001 + OpenBIOS;
  42.6k/12.2k macro sites). Base runtime + 33/33 tests green against it.
- Gating: macros expand only under -DPSX_PGXP=1. runtime.cmake grows a
  PGXP option on psxrecomp_add_runtime_target (defines PSX_PGXP=1 +
  PSX_OVERLAY_FLAVOR=2, _pgxp exe suffix) and PSX_PGXP_VARIANT to build
  the sibling target from the same generated tree (framework runtime +
  game wrapper). No ABI version bump needed: the appended PGXPHooks
  table is NULL-tolerant and the flavor half of the ABI tag rejects any
  host/DLL flavor mix.
- Shard caches: flavor folded into the cache tag (cg<V>_<hash>_gc<hash>
  _f<flavor>) in overlay_loader.c + compile_overlays.py so base and pgxp
  caches never share a directory; compile_overlays.py passes
  -DPSX_PGXP=1 when flavor bit 2 is set; overlay preamble forwards
  psx_pgxp_* through the table. PSX_OVERLAY_CODEGEN_VER 9 -> 10.
- Env overrides PSX_GEOMETRY_CORRECTION / PSX_PERSPECTIVE_TEXTURING /
  PSX_PGXP_CPU_MODE (validation path: free-running headless boots arm
  from the first vertex); census grew produced/swc2_stores and the pgxp
  verb reports suppress/active gate state.

Validated: PSXRecomp_pgxp.exe boots the recompiled BIOS with hooks armed
(engine active, per-vertex lookups flowing; the BIOS shell has no RTPS
content so produced=0 there is correct - coverage measurement moves to a
3D title in phase 3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 19:03:36 -07:00
Matthew Stanley 0fb110939c Fix setup_project.ps1 codegen source wiring 2026-08-15 12:39:17 -07:00
LarsDuJour 17eddc0f1d Update setup_project.ps1 2026-08-15 21:17:33 +02:00
LarsDuJour 81431498aa Update setup_project.ps1
Changed various ANSI characters to UTF-8 equivalents.

This solves a blocker on systems with some  languages, preventing using this script.
2026-08-15 21:13:30 +02:00
Alex Vanderveen 904ef39073 zlib1.dll 2026-08-14 02:41:50 -04:00
Alex Vanderveen 48db3c6e6f Update stage_setup_sdk.sh 2026-08-13 18:37:52 -04:00
Alex Vanderveen 84cf668f00 Update bundle_mingw_dlls.sh 2026-08-13 18:30:06 -04:00
Alex Vanderveen b0a44ca414 mingw for local builds 2026-08-13 18:06:52 -04:00
Alex Vanderveen ae9e6a4e2b hotfix build error on ci 2026-08-12 22:00:46 -04:00
Alex Vanderveen d5b5a029fe hotfix ci template 2026-08-12 21:53:26 -04:00
Alex Vanderveen f3c6caf96c game_options.toml template 2026-08-12 20:07:44 -04:00
Alex Vanderveen c4f412be17 project studio, harden prefetch sdl3 2026-08-12 16:49:03 -04:00
Alex Vanderveen a73a5c1093 rewind feature 2026-08-12 02:42:30 -04:00
Alex Vanderveen b1c2317c44 Merge pull request #138 from mstan/master
upstream merge
2026-08-12 02:11:00 -04:00
Matthew Stanley 0bd6ddf730 Merge master into fix/issue72-overlay-project-root
Conflict: recompiler/CMakeLists.txt. Master gained #134's test registrations
in the same block where #133 added stall_report. Resolved as the union of both.

Also RE-ENABLES aot_overlay_discovery, which is not part of the conflict region
and so would otherwise have survived the merge untouched. #134 registered it
DISABLED with this reason:

  "fails in check_real_batched_fragment_publication: that check reaches
   MOD.compile_fragment_batch, which spawns psxrecomp-game with cwd set to its
   own TemporaryDirectory, so BIOS-profile resolution finds nothing ... it
   needs an owner, and DISABLED keeps it counted until then."

This branch is that owner -- resolving BIOS-profile lookup against
--project-root instead of the CWD is exactly that fix. Landing it while leaving
the test disabled would ship the fix with its own proof switched off, which is
the silent-rot class #134 exists to prevent.

Measured on this tree, msys64 toolchain driven from PowerShell:

  - pristine origin/master: test_aot_overlay_discovery.py exits 1 at
    check_real_batched_fragment_publication, "no BIOS profile found"
  - this merge: ALL PASS, and ctest -R aot_overlay_discovery passes in 7.64s
    with the DISABLED property removed
  - baked codegen hash 0xfbb44d49u on BOTH arms -- the cache tag does not move,
    so existing overlay caches keep loading
  - full ctest failure set identical to pristine master, no new failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:27:33 -07:00
Alex Vanderveen 3acb8f6d59 Integrate retcomm-rbengine as shared rollback host policy.
Wire lib/retcomm-rbengine beside recomp-net and replace MotK in-tree sched,
hash_confirm, input hist, snap ring, and rb_post with thin PSX shims so the
ecosystem can share invent/pacing policy outside each title facade.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 23:52:31 -04:00
Alex Vanderveen 37a5b7c7cd project studio 2026-08-11 23:17:47 -04:00
Alex Vanderveen 2207cd9a09 auto versioning CI releases 2026-08-11 22:05:20 -04:00
Alex Vanderveen 581d4e927c Merge origin/master into feat/legendofmanaetc
Resolve main.cpp by keeping ae_fill/rematch netplay bring-up and taking
master's mod-safe turbo_loads/skip_fmv apply (no pre-activation clobber).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 19:28:20 -04:00
Alexandros Mandravillis dd788d4652 tools: harden stall report evidence semantics
Keep dispatch counters as raw event counts, fail closed on malformed or reset evidence, label rolling and censored scopes, retain bounded-ring provenance, and write hash-bound diagnostic artifacts atomically. Add focused protocol tests and register them with CTest.
2026-08-11 09:37:32 +03:00
Alex Vanderveen 2c1942b569 project migration tools 2026-08-10 20:44:55 -04:00
Matthew Stanley 6a601ef8ac stall_report: read autocompile from the nested compile object; label the
phase window as rolling

Both reported by @Alexbeav on PR #131, both confirmed against the runtime
source and a live warm-cache instance.

1. autocompile_status nests every compile field under "compile" (see
   handle_autocompile_status in debug_server.c). The report read runs, fails,
   the shard counts and the degraded flag off the OUTER object, where none of
   them exist, so it always printed zeros and never printed a degradation
   reason.

   This was the worst possible placement for the bug: section [1] exists to
   catch a silently-failing autocompile, and it reported a healthy-looking
   row of zeros in exactly that case. It was visible in the validation output
   of the original commit -- "autocompile: runs=0 fails=0 shard_ok=0" -- and
   was not questioned, which is the same unexamined-quiet-number failure the
   tool is meant to prevent.

   Measured on a live instance during a 15s window: the fixed version reports
   runs=3 fails=3 consecutive_fails=3 last_exit=1 and the full DEGRADED
   reason string. The old version reported runs=0 fails=0 and no warning at
   all for the same window.

   The section now also reports `configured`, `state`, and
   `shard_result_seen`, because those are what make a zero interpretable:
   not-configured zeros are expected, whereas configured-but-never-saw-a-
   PSX_SHARD_RESULT zeros are meaningless rather than healthy, and the report
   now says which it is. A missing `compile` object is called out as a
   protocol mismatch instead of being silently treated as zeros.

2. phase_profile sums a ring of recent seconds and reports its own window_s;
   it is a ROLLING WINDOW, not a since-boot total, and it does not align with
   this report's window. It was labelled "cumulative since boot". It now
   prints the runtime's reported window and states plainly that the scope
   differs from the report's own.

Left deliberately for the follow-up @Alexbeav offered: fail-closed handling
of missing/non-numeric/decreasing counters, the dispatch-counter percentages
being dispatch-event shares rather than coverage or time-share, top-set
censoring in the phase_hot and per-PC deltas, ring metadata for
reproducibility, and artifact schema/tool-hash/atomicity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:53:37 -07:00
Matthew Stanley a3b7f47d17 tools: add stall_report.py, one-command interp/stall attribution
Diagnosing "why is this title spending its time in the interpreter" currently
means issuing eight TCP commands by hand and correlating their JSON. That is
enough friction that it does not get done, and when it is done by someone
without the runtime's internals in their head the numbers get misread -- #72
is a live example: an empty phase_hot was read as a possible bad query when it
in fact meant nothing native was executing at all.

stall_report.py collapses that into one command and one pasteable report,
usable against any title on any port:

  [1] Is the overlay cache actually BEING USED, or just present on disk?
      dispatch_native vs dispatch_interp_fallback, stale_blocked, the
      autocompile degraded channel, and shard ok/fail/skipped counts.
  [2] When code IS interpreted, is it chaining or thrashing?
      dirty_ram insns_run/blocks_run = instructions per dispatcher
      round-trip, with the kernel-window-is-per-block-by-design caveat
      stated inline so a low ratio there is not misread as a bug.
  [3] Where did the wall-clock actually go?
      The largest host_us gaps in the always-on PC-sample ring, each with
      the guest cycles that advanced across it. That ratio separates three
      failure shapes that look identical in a frame-time graph: CPU kept up
      (gap is GPU/present/vsync), CPU-bound but executing, and emu thread
      not running at all.
  [4]-[6] phase shares, hottest interpreted PCs tagged kernel vs
      above-floor, and phase_hot deltas for both the native and static sets.

PASSIVE RING CONSUMER by construction, per the project's observability rule:
it never arms a trace, never pauses, never single-steps. Everything is a
cumulative counter (two snapshots subtract to a window) or a read of a ring
that has been recording since process start, so attaching late still sees the
event. `snap` reports cumulative; `run --secs N` reports deltas.

Two caveats are printed in the output rather than left to be rediscovered:
current_func is a STATIC dispatch stamp and localizes without proving
interpretation, and an empty native phase_hot means nothing native ran rather
than a malformed query.

Validated against a live Tomba 2 debug build (Release + PSX_DEBUG_TOOLS=ON,
port 4515), both modes and the --out JSON artifact. Representative real
output: dispatch_native 3,253,132 (97.16%) vs interp_fallback 95,047 (2.84%),
16,329 instructions per dispatcher round-trip, and one 68 ms gap at 0.46x
realtime flagged in-exception. Development of the tool caught two defects in
itself that way -- snap mode subtracted a snapshot from itself and reported
zeros for everything, and non-ASCII output mojibaked on a cp1252 console --
both fixed before commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 12:58:23 -07:00
Matthew Stanley e3ecd67bd4 recompiler: resolve the BIOS profile against --project-root, not the CWD
compile_overlays.py spawns psxrecomp-game with cwd = dirname(game.toml),
because the seeds and capture paths in the config are relative to it. That
cwd is also where the recompiler probes for bios/SCPH1001.toml when the
config carries no explicit [recompiler] bios_config -- and --ws-config, the
only config channel overlay compiles use, deliberately does not adopt the
config's paths, so bios_config is never consulted for them.

For a PACKAGED game.toml (packaging/release/game.toml) that directory holds
neither bios/SCPH1001.toml nor a vendored framework one level down. Every
shard then died with "FATAL: no BIOS profile found", ok=0, and the overlay
cache silently failed to build. The run still exits, so the only symptom is
that the title ships with no cache and every player's first session runs
interpreted. Reported as issue #72, and it is what the recipe printed by the
game repos' package_release.ps1 told people to run.

Fix:

- psxrecomp-game takes --project-root <dir> and resolves the profile against
  it: the direct bios/SCPH1001.toml probe, the one-level vendored-framework
  probe, and a relative [recompiler] bios_config (documented as relative to
  the game project root, previously resolved against the CWD). Absent the
  flag, behaviour is unchanged.

- Every "no BIOS profile found" / bios_config failure now names the absolute
  directory it searched. Without it the message reads as a config error when
  it is a CWD error, which is what cost two vault parses on the Tomba 2
  v0.0.8 cut.

- compile_overlays.py forwards --project-root to all three recompiler spawn
  sites, defaulting it to the framework root derived from the required
  --runtime-include. The derivation is used ONLY when it actually locates a
  profile, so it can repair a broken invocation but never alter one that
  already worked -- and the recipe the packager prints now works as written,
  with no game-repo change. Every attribute lookup degrades to "pass no
  flag" so the in-process callers that build a partial args object keep
  working.

This also un-breaks the framework's own overlay test suite, which was RED on
master for the same reason: test_aot_overlay_discovery.py compiles against
tools/cycle_testrom/game.toml, so the recompiler probed
tools/cycle_testrom/ -- which has no BIOS profile -- and every shard failed.
Measured on this tree with one recompiler binary, changing only the tool and
tests: master exits 1 at check_real_batched_fragment_publication; this commit
reaches ALL PASS.

Two test repairs were needed to get there, both pre-existing and both masked
by the profile failure that stopped the suite before it could reach them:

- check_full_candidate_cli_fastpath stages a SYNTHETIC runtime_include in a
  temp dir, so the framework root is not derivable from it and the root must
  be named explicitly.
- the same test staged its cache prefill under cg<ver>_<hash>, missing the
  _gc<config-hash> component the cache path gained. A prefill one directory
  off is not found, so the run compiled for real instead of taking the fast
  path the test exists to assert.

The cache tag is unaffected: main_psx.cpp is not in codegen_hash_sources.cmake
(verified -- the baked hash is fbb44d49 before and after), and
--overlay-config-hash returns before any of this code runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 12:44:22 -07:00
Matthew Stanley 73a8b614b6 Bound overlay captures and compact legacy vaults 2026-08-10 00:06:29 -07:00
Matthew Stanley 20a8f4ad08 input: make Hybrid a mod-only mode, not a selectable one
Hybrid kept reappearing in titles that never meant to ship it, because it was
the ZERO value of PadMode and therefore the default everywhere: PlayerInput,
p1_mode/p2_mode, ctrl_locked_*, and default_p1/p2_mode all initialised to it.
A game then had to opt OUT with [controller] allow_hybrid = false, and every
shipping title did exactly that or pinned a mode - so no title actually ran it.

Hybrid now has exactly one route: a trusted game plugin calling
psx_mod_set_controller_mode_override(), which is how Tomba's hybrid controller
mod already requests it. Everything that made it reachable by accident is gone:

  - defaults are ANALOG (PlayerInput, p1/p2, ctrl_locked_*, default_p1/p2)
  - [controller] allow_hybrid is removed, along with the startup clamp that
    existed only to undo the bad default
  - game.toml default_mode/p1_mode/p2_mode = hybrid now THROWS, naming the
    mod API as the only route, rather than being silently coerced
  - a stale settings.toml hybrid migrates to analog via
    pad_mode_from_settings_string(); a user should not fail to launch over a
    value they never typed
  - pad_mode_to_string() never writes hybrid back

Deliberately KEPT, because the mod runs on it: PAD_MODE_HYBRID,
PSX_MOD_CONTROLLER_HYBRID, psx_mod_set_controller_mode_override(),
hybrid_stick_active/hybrid_dpad_active, PlayerInput::hybrid_analog and
psx_keybinds_dpad_active.

Paired with the recomp-ui change that drops the Hybrid segment and the
allow_hybrid field from RecompLauncherCGameInfo - the two must land together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 19:06:13 -07:00
Alex Vanderveen 63c7764241 bump 2026-08-09 13:22:37 -04:00
Alex Vanderveen 62424dc507 bump 2026-08-09 13:21:31 -04:00
Alex Vanderveen ceee034d59 Merge origin/CTR-Netplay into integrate/game-codegen-hooks-into-rollback
Keep both psx_bss.h (BSS rings) and nd_intro_ot.h from CTR-Netplay.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 22:53:34 -04:00
Alex Vanderveen 9689ad40dd shrink 0's and use explicit portable toolchain python 2026-08-08 22:50:17 -04:00
Alex Vanderveen e8fbceeca0 bump windows build errors 2026-08-08 21:11:34 -04:00
Alex Vanderveen 069adf871e bump windows build errors 2026-08-08 21:10:11 -04:00