mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 10:38:46 -04:00
449fbebe09656dae2cdc4e15358408b6d356ca6a
20 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
244474a147 | widescreen: scope curved backdrop projection sites | ||
|
|
ee09ab9bbb |
widescreen: generic Ape-Escape-class support + native-wide GL perf + cutscene flicker fix
Adds full 16:9/21:9 native-wide support for fully-3D titles (Ape Escape, SCUS-94423) as a generic, opt-in, 4:3-byte-identical framework capability, plus a native-wide GL compositor perf fix and a cutscene present-mode flicker fix. Widescreen (generic, per-game opt-in via [widescreen*]): - Shared screen-extent cull detector (runtime/include/ws_cull_detect.h) used by all three backends (recompiler, dirty-RAM interp, sljit). Per-game signature immediates ([widescreen.cull] screen_w_imms/screen_h_imms) — Ape's funnel tests 0x181/0xF1 on its 368px display, not Tomba's 0x140/0xE0. - Three funnel idioms widened: unsigned per-vertex sltiu (psx_ws_cull_sltiu), signed min/max slti right edge + bare-bltz left edge (psx_ws_cull_slti/bltz, new helpers + SLJIT_HLP appends), and billboard center+/-halfwidth. Explicit [widescreen.cull] slti_sites for X-only funnels the auto-detector can't qualify. - gte_game_mode: GTE-activity gameplay detector for 3D titles with no sprite-tag helper (gte.cpp notes every RTPS/RTPT). STICKY (>=3 verts, 45-frame hysteresis) so sparse cutscene frames never flip to a 4:3 pillarbox = fixes intro flicker. - nw_hud_corners: push outer-third screen-space HUD sprites to the true wide corners in native-wide (else inset by the reveal). - nw_backdrop: stretch a full-frame axis-aligned 2D backdrop quad to fill the wide frame (for titles with a flat sky plane). - Pattern-scanned interp/sljit widen hooks now gated per-game (auto_screen_x / auto_backdrop): fixes an ungated auto_backdrop false-positive that rewrote a live GPR = the ~24s native-wide crash on Ape. Native-wide GL perf (fixes 16:9 frame drops): - The wide surface centre equals the canonical 4:3 framebuffer, so wide_blit_center copies canonical->wide-centre at present (both present paths) and the per-prim mirror SKIPS prims fully inside the 4:3 frame (mirror_x/geo/batch_center_only). Only reveal-margin prims re-rasterize. Peak frame time 38.8ms -> 17.5ms, no vsync misses. Toggle gl_wide_fast; 4:3 untouched (all gated on g_wide_cur). Runtime-only except the recompiler cull emit (regen needed for cull widening). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4ddccccaa9 |
checkpoint: exact-store-PC tooling fix (verified) + freeze/scheduler diagnostics + faithful-timing WIP
recompiler/code_generator.cpp: stamp g_debug_last_store_pc before EVERY game store (SB/SH/SWL/SW/SWR, SWC2, + the ws-backdrop / game-option special paths) and declare the extern in the game-C preamble. Previously only the BIOS emitter (strict_translator.cpp) stamped it, so game stores inherited the last BIOS store pc -> wtrace/readtrace mis-attributed game writes to the BIOS. VERIFIED LIVE: MMX6's producer trace for the 0x800CD3F8 write now reports the real game store pc 0x8001A708 (matches Beetle), not the stale 0xBFC11BF0. runtime: g_pc0_reason slice-exit provenance, fail-closed exception-sentinel asserts, named thread-event kinds, freeze-heartbeat RVA emission (diagnostics, default-on, additive). Carries the in-progress faithful-timing / MDEC work (cpu_state.h, memory.c, dma.c, interrupts.c, dirty_ram_interp.c, full_function_emitter.cpp) as an uncommitted-WIP checkpoint. tools/: regen_bios.sh, bios_emitter_fingerprint.sh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
958a928d14 |
timing: faithful R3000A I-cache fetch model + interp validation (Stage 1)
New runtime/src/psx_icache.c: direct-mapped (4 KB / 256-line) instruction-
cache fetch cost, transcribed from Beetle PS_CPU::ReadInstruction:
HIT (TV == addr) -> +0, no give-back clear
MISS, KSEG1/uncached -> +4 (BIOS ROM)
MISS, cached (KSEG0/KUSEG) -> +3, then +1 per word refilled from the
missing word to the line end (earlier words
stay invalid); also clears the load give-back.
Mirrors only the per-word TV tag array (cost + tag state; the value is already
in hand). Wired into the dirty-RAM interpreter (exec_one) per instruction,
charged BEFORE the §1 base — Beetle order.
New ruler #2 loop `icache_miss`: loop top + a victim 0x1000 apart alias the
same direct-mapped line with different tags, forcing a refill miss every
iteration (other loops are all-hits after warm-up).
VALIDATED interp vs Beetle (PSX_FORCE_INTERP=1 PSX_ICACHE=1): icache_miss
native +14 == Beetle +14 (per-iter 17); the other 12 loops unchanged at +0
fetch. So the I-cache hit AND refill-miss costs are MEASURED equal to the
oracle on the interp path.
Opt-in via PSX_ICACHE=1 (default OFF) — charging fetch cost in only one
backend would fork mixed compiled/interp timing. Production activation +
the compiled-path emit (per cache-line leader) + ruler #1 cold-spike
validation are Stage 2. Default-off => no production change (the interp's
fetch call is a no-op without the env).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
d5977976e5 |
timing: GTE-read + MFC0 + muldiv-stall load-delay give-back (ruler#2 100%)
Closes the last steady-state cycle divergences; native now == Beetle on the complete 12-loop ruler #2 suite. - MFC2/CFC2 (GTE reg -> GPR read): new psx_gte_read stalls to gte_ts_done AND hands the stall amount to following instructions as a load-delay give-back (Beetle cpu.cpp:1332-1341: LDAbsorb=gte_ts_done-ts, LDWhich=rt). MTC2/CTC2 (writes) keep stall-only psx_gte_stall. - MFC0: arms ld_absorb=0 / ld_which_t=rt (Beetle delayed load) — sets ReadFudge=rt so a load in the next slot gets no fudge. - psx_muldiv_stall (MFLO/MFHI): now CONSUMES the pending load give-back while stalling to muldiv_ts_done, plus the muldiv_ts_done-1 off-by-one shortcut (Beetle cpu.cpp:1723-1736). No-load code is unaffected (read_absorb==0 -> plain advance). Wired in both static emitters (code_generator, strict_translator) and the dirty interp. METHODOLOGY (Rule 15): Beetle cyc_watch must be sampled at STEADY STATE — its warm-up window reports the no-give-back value (the source of the prior "gte_rtps +15 EXACT" mis-record; true steady Beetle = +11). Validated at steady state, native == Beetle on ALL 12 ruler #2 loops: the 4 divergences closed (gte_rtps 18->14, gte_nclip 11->7, gte_read_use 19->14, ld_div 45->49); the 8 CPU-load/alu/div/mult loops held. Ruler #1 delta 0. Tomba 2 FMV plays (no regression). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d8c4a8ec9f |
timing: faithful R3000A load-delay interlock (ReadFudge/LDAbsorb)
Implements the last unmodeled load-path cycle component — the R3000A load-delay pipeline interlock (Beetle ReadAbsorb/ReadFudge/LDAbsorb/ LDWhich) — as ONE shared per-instruction model across the dirty interp and BOTH static emitters, replacing the flat per-instruction +1 base and the flat +4 main-RAM read wait-state. Model (transcribed from beetle-psx cpu.cpp + libretro.cpp, verified Δ): - §1 base: a pending load's per-register give-back (read_absorb) is consumed INSTEAD of charging +1 (pipeline write-back overlap). - GPR_DEPRES: each opcode's src/dst regs end the give-back on use. - DO_LDS: commits the prior load's give-back + ReadFudge. - load ReadMemory: +2 fudge iff the predecessor committed no load; main-RAM region +3 + completion +2 (LWC2 +1) become the LDAbsorb give-back; scratchpad +0. New runtime/include/psx_cyc.h (inline §1/deps/DO_LDS + step) over new CPUState timing fields; psx_cyc_load_*/psx_cyc_lwc2_read in memory.c (region-aware, own the whole load interlock, value via uncharged psx_read_*); psx_cyc_dep_res_mask in psx_instr_cost.h (per-opcode GPR_DEP/RES, gen-time literal). cpu->read_* rewired to uncharged in main.cpp; charge_main_ram_read removed. Wired before each instruction body in code_generator.cpp (game) and full_function_emitter.cpp + strict_translator.cpp (BIOS); loads route through psx_cyc_load_*. Validated against the Beetle oracle: - ruler #2 load2: native +10 -> +11 == Beetle (all other components still exact: alu+1/load+5/div+38/mult+15/gte_rtps+15/gte_nclip+8). - ruler #1 [0x80001C5C->0x80001CA4]: 54 -> 56 == Beetle steady-state (residual 84/77 spikes = I-cache cold refill, the separate P2 axis). - Tomba 2 boots to the intro FMV (pixels on screen, no regression). Follow-up (separate commit): GTE-read (MFC2/CFC2) + MFC0 LDAbsorb give-back, and muldiv-stall give-back consumption — needed for mixed game/FMV faithfulness but do not affect the rulers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ec1fd76b56 |
timing: faithful GTE (COP2) per-command completion-stall
Models the GTE command latency + stall-on-COP2-access, the next Stage-2 cycle component. Mirrors the mult/div completion-stall mechanism: - cpu->gte_ts_done (new CPUState field, appended): a GTE command sets it to psx_cycle_count + (cost-1); back-to-back commands serialize (psx_gte_set stalls to the prior deadline first). Any COP2 register access (MFC2/CFC2/MTC2/CTC2/LWC2/SWC2) calls psx_gte_stall, advancing guest cycles to the deadline if the op is still in flight. - Cost table (psx_cycles.c, psx_gte_cmd_latency): per-op cost-1, transcribed + verified from beetle-psx gte.cpp op returns (RTPS15 NCLIP8 MVMVA8 NCDS19 NCDT44 RTPT23 NCCT39 ... AVSZ4=5 not 6). cost-1 because the COP2 instruction's own +1 base is charged separately (Beetle GTE_Instruction returns ret-1). - Set armed in the shared gte_execute() (one site for both backends); stall emitted before every COP2 reg access in both emitters (code_generator, strict_translator) and the dirty interp. The compiled "+1 at top" vs interp "base after exec_one" offset cancels (same as muldiv). Validated EXACT on ruler #2 (new gte_rtps + gte_nclip loops): native +15/+8 == Beetle +15/+8. All other components unchanged (no regression); Tomba2 boots to FMV. Requires regen (emitters changed); runtime+BIOS+game regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a3e8f2818b |
feat(timing): faithful mult/div completion-stall + ruler #2 native half (validated EXACT)
Stage-2's biggest cost gap closed. MULT/MULTU/DIV/DIVU now set a completion deadline; MFLO/MFHI stall guest cycles until it — exactly like Beetle's muldiv_ts_done. Latencies transcribed from cpu.cpp: DIV/DIVU=37; MULT via MULT_Tab24 (14/10/7) on the first-operand magnitude. Native previously charged ZERO for this. - Per-instruction cycle charging is now the DEFAULT on this audit branch (PSX_CODEGEN_CYCLE_PER_INSN), required so the stall absorbs correctly (the running cycle count must be accurate mid-block). Validated cycle-neutral for the identity base model; Tomba 2 still reaches the FMV. - runtime: CPUState.muldiv_ts_done + psx_muldiv_set/stall + psx_mult_latency_s/u in psx_cycles.c. Emitted at the mult/div/mflo/mfhi sites in the GAME emitter (gated PSX_ENABLE_BLOCK_CYCLES). Stall fires even for mflo/mfhi to $zero. - ruler #2 native half: tools/cycle_testrom/CMakeLists.txt builds psx-cyctest (links the recompiled test EXE), boots disc/cyctest.cue (serial CYCT-00101 so disc-identity matches, no warning dialog). measure.py --port 4600. VALIDATION (native vs Beetle oracle, per-iteration component cost): div +38 vs +38 EXACT | div_spaced +38 vs +38 EXACT (absorb works) | mult +15 vs +15 EXACT | baseline/alu match. Remaining: load native +7 vs Beetle +5 (memory.c flat +6 wait-state needs calibration to ~4) — a smaller refinement. BIOS emitter still block-mode (ruler #1 unchanged at 30) — applying per-insn + muldiv there is the next step to close ruler #1's div-stall gap. No FMV regression (i_stat 0x8D, screenshot verified). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f9d50d7503 |
fix(tomba2): delay-slot cycle accounting -> Tomba 2 reaches the intro FMV
The multi-week Whoopee-Camp logo->FMV stall was a TIMING-FAITHFULNESS bug, not
game logic: the static recompiler charged cycles block-granular and a branch
whose delay slot is a SEPARATE block leader (branch at end_addr, delay slot
outside the block) undercharged by one cycle on every execution -- the
always-executed delay-slot clone was emitted but counted by neither the branch
block nor the (unentered) delay-slot block. ~8 such sites on the boot init path
= the measured -8 cycle drift that made native run behind the interpreter, so
the frame-1824 elapsed-Timer1 logo-delay loop exited after ~42 iterations
(native) instead of ~1557 (interp/HW) and never reached the FMV.
Fix (code_generator.cpp translate_basic_block, the faithful class fix -- every
dynamically executed instruction charged exactly once):
block_exec_cycles = instruction_count
+ (exit branch at end_addr with delay slot outside ? 1 : 0)
applied to both the block cycle charge and the slice budget. R3000 is MIPS-I so
the delay slot is unconditional; the not-taken path already falls through to
branch_pc+8 (no double-count). Result, screenshot-verified: native passes the
logo-delay loop, screen animates, CDROM+DMA stream active, the jungle intro FMV
plays.
Also in this checkpoint (all on wt/tomba2, not merged/pinned):
- PARKED precise interrupt-slicing (PRECISE_IRQ_SLICE.md): root-caused its pc=0
boot exit to a mid-function clean-text resume PC the dispatcher can't re-enter;
ChatGPT-validated fix is "every block leader = re-enterable CPS continuation"
(psx_game_is_function_entry predicate added; precise_pc_dispatchable +
irq_taken-once restructure in psx_run_precise). Default OFF (PSX_PRECISE_SLICE=1
to resume); inert on the baseline. Slice-trace diagnostics added to freeze_check.
- FAITHFUL_TIMING_PLAN.md: authoritative game plan (P1-P6) + standing directive
(build the faithful core, never hacks; ecosystem is being built, not preserved);
referenced from CLAUDE.md Rule -1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
732e093ab2 |
heartbeat: IRQ raise/deliver/ack + bail-source telemetry (Tomba 2 spin diag)
Always-on, heartbeat-resident diagnostics for the Whoopee-Camp splash spin: - VBLANK raise/deliver/ack counters (interrupts.c + memory.c) + irq_deliver. - Deduped bail-source ledger keyed by (call-site $ra, wild target) (traps.c, hooked at psx_call_contract in cpu_state.h) + heartbeat top-key/first-bail. Result: REFUTES the exception-reentry-storm hypothesis. VBLANK is raised, delivered AND acked exactly 1/frame (textbook-healthy); irq_deliver is 1/frame, not a storm. So the freeze is NOT an interrupt/timer/CD problem -- the hardware sim is healthy. The ~68k bails/frame are the GAME splash loop busy-polling a kernel event (TestEvent/WaitEvent) that never fires -- the DeliverEvent/WaitEvent discriminator is the next probe. (The dominant dispatch-trampoline bails are emitted in generated BIOS code, so the precise bail call-site needs a recompiler-emit change + BIOS regen; the direct-call ledger here is partial. Deferred -- not needed to localize to the event wait.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4d50c2214a |
overlay: fail-closed native entry guard (port to master) — fixes the native-overlay blue-screen wedge
Brings the fail-closed native entry guard from wt/tomba2 (b5a6940 / bd60381) to master, where it had never been merged — which is why Tomba (and any master-built title) still wedged. This is the long-standing "blue screen" / shared-RAM- divergence-between-interp-and-native class. Root cause: an overlay function dispatched at a FOREIGN INTERIOR PC (a range- ownership mismatch routed a PC the function does not own into it) hit the CPS entry-switch `default: break`, which fell through and ran the function FROM ITS TOP — re-running its prologue and corrupting shared CPU/RAM state, so the guest's main loop then waits forever on a condition that never gets set (the 0xCF0 SIO spin / blue screen). Reproduced on Tomba's Village of All Beginnings: a native 0x000E7000-region shard function entered at a foreign PC wedged; the interpreter ran the same bytes correctly. Fix: the generated CPS entry-switch for OVERLAY functions (config_.overlay_mode) now FAILS CLOSED on an unknown non-zero continuation PC — it restores the PC and calls psx_native_bad_entry instead of running from the top. overlay_loader_dispatch consumes g_native_bad_entry and routes the PC to the sanctioned dirty-RAM interpreter (the live bytes run correctly — NOT a stub/HLE). True-prologue entry still runs from the top. Static (BIOS/boot-EXE) functions keep the legacy fall- through. ABI bumped 5->6 so stale ABI-5 caches are rejected and rebuilt with the guard. Minimal port: kept only the fail-closed mechanism + its ABI plumbing. The Tomba2 host_aliases interior-routing and the native-block / CPS-probe diagnostics (which depend on the overlay-cache-v2 multi-range Candidate fields not on master) were intentionally omitted — the fail-closed default alone is the safety net. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
94a1966444 |
recursion(fix): universal continuation-passing — FIXES the idle-freeze host-stack leak (BIOS+game)
The complete §25 fix: make BOTH recompiler emitters emit guest calls as TAIL-TRANSFERS driven by the unified flat trampoline + the guest's own $ra/$sp, instead of nesting one host C frame per guest call. The host stack can no longer mirror unbounded guest re-entrancy — the architectural root of the ~14-min idle freeze. Gated gen-time by PSX_CPS; legacy codegen byte-identical when unset. VALIDATED (user): full-CPS Tomba boots BIOS -> FMV -> New Game -> village idle and plays; ce_profile cur_max_kb FLAT at 5 KB from frame ~9.9k through 56,890 (turbo soak) — blew past the ~50k-frame freeze threshold with the host stack bounded (legacy climbed +1.17 KB/frame -> 48 MB -> freeze). KEY CORRECTION to the prior session's §25.1: there are TWO emitters. The BIOS (full_function_emitter.cpp) was done last session but the §8 leaking chain (func_8001A954 ...) is GAME code, emitted by code_generator.cpp (via main_psx.cpp) — never converted. Also, §25.1's "continuations not in the dispatch table" hypothesis was wrong (they were always added); the real boot regressions were found by dumping the always-on fntrace ring on the abnormal psx_dispatch return. Emitter changes (gated PSX_CPS): - full_function_emitter.cpp (BIOS): BIOS A0/B0/C0 vector runtime-fallback tail-transfers instead of nested psx_dispatch_call (was the frame-0 exit). - code_generator.cpp/.h (game): jal/jalr/jr-$ra/jr-table/branch-split/j-split -> tail-transfer (jalr captures rs before the link, alias-safe); each call's return point registered as a dispatchable continuation; entry-switch (keyed on cpu->pc) at the top of every function AND every overlapping-alias body. 0 psx_call_contract / call_by_address emitted under CPS. - main_psx.cpp: psx_dispatch_game_compiled gains continuation cases (case cont: cpu->pc=cont; func_owner(cpu)); entries clear cpu->pc. - strict_translator.cpp: syscall emits `if (psx_syscall(...)) return;` so a void syscall falls through to the inline post-syscall jr $ra (real-HW EPC+4). Runtime changes (the legacy "return via nested dispatch / cpu->pc=0" idioms that exit the flat CPS trampoline): - traps.c: psx_syscall returns int (0=void/fall-through, 1=transfer); Enter/ExitCriticalSection return 0; case-3 ChangeThread-success returns 0 — the fiber scheduler leaves cpu->pc=0 on resume, which was the frame-712 exit. - cpu_state.h / overlay_api.h: psx_syscall signature void->int; overlay callback ABI bumped 4->5 (stale overlay caches rejected). - dirty_ram_interp.c: gate interp_enter_compiled behind PSX_HAS_GAME_DISPATCH (un-breaks the BIOS-only build). - main.cpp: dump the always-on fntrace ring tail + stack to psx_cps_exit_trace.json on an abnormal psx_dispatch return (diagnostic; CLAUDE.md ring-buffer model). Docs: RECURSION_BUG.md §25.2-§25.4. Remaining: dwarf->overworld playtest; Stage 3 (sljit/overlay CPS parity); then consider making CPS default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5589966161 |
sljit persist-cache Stage 1: position-independent shards (cpu-relative helpers)
Prerequisite for persisting sljit shards to disk (SLJIT_PERSIST_CACHE.md). The JIT baked absolute host-helper pointers as SLJIT_IMM operands at three sites (emit_helper2: memx/cop2; emit_ws_cull; PEND_CALL: call), so a shard serialized in one process would call stale addresses when reloaded at a different ASLR base. Route all four helpers (SLJIT_HLP_MEMX/COP2/WS_CULL/CALL) through a cpu-relative table CPUState.sljit_helpers[], loaded via MEM1(R_CPU)[offset] instead of a baked immediate, so serialized LIR holds zero absolute host addresses. The table is appended at the END of CPUState (existing gcc overlay-DLL field offsets stay put) and populated once at startup (overlay_sljit_init_helpers). In-session JIT is unchanged behaviourally (one extra indirection per helper call). Smoke-tested: forced sljit with the gcc cache moved aside, 12 shards JIT'd and ran, title renders, no crash. Broader playtest folds into the Stage 2 reload test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9669d38d28 |
widescreen(8C): far-backdrop GTE un-squash, depth-gated
The far backdrop (ocean/cloud/distant mountain) is GTE-projected 3D drawn by the main-EXE driver FUN_8004db3c (sole caller of the GTE emitter FUN_80027600). The global widescreen GTE X-squash compresses it toward centre, so its geometry falls short of the revealed 16:9 edges -> blue void that pops as the camera moves. Fix: bracket the backdrop driver with gte_ws_set_suppress(1)/(0) so its draws skip the X-squash and fill the stretched frame (skybox treatment), while the near 3D world keeps the wider FOV. FUN_8004db3c draws a MIX (far backdrop + nearer props/scenery), so the suppress is DEPTH-GATED in gte_rtps_internal: only vertices with projected SZ >= s_ws_far_threshold (default 900, tunable live via the ws_far_threshold TCP cmd) un-squash; nearer props stay squashed and aligned. Validated: threshold 900 fills the void, keeps the foreground door + background scenery (path/tree) aligned. Residual: the mountain grass-top straddles the depth band (per-vertex limitation) -> flickers when the camera dips; this is the checkpoint before exploring native-wide rendering to remove the squash (and this whole artifact class) entirely. Mechanism: - recompiler: [widescreen.backdrop] unsquash_funcs emits gte_ws_set_suppress(1) at entry + (0) before each jr-$ra return (code_generator + config_loader + main_psx). Main-EXE -> regen-class. - runtime: gte.cpp depth-gated squash + s_ws_far_threshold; cpu_state.h decl; debug_server.c ws_aspect (live squash on/off) + ws_far_threshold (set split + read SZ stats) diagnostics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
be76e4d4b5 |
widescreen: [video] aspect_ratio — GTE X-squash + stretched present
Adds the DuckStation-style widescreen hack as a config option, implemented in the runtime (no regen needed): - gte.cpp: gte_set_display_aspect(num, den). RTPS/RTPT scale the IR1*h/sz screen-X term by (4*den)/(3*num) around the game's own OFX projection centre (identity at 4:3). Games cull against the SXY values they read back from us, so post-projection screen- bounds culls stay aligned with the visible frame. - present: GL letterbox_rect generalised to the configured aspect (gl_renderer_set_display_aspect); SDL_Renderer path gets an aspect-derived logical size + dst rect. Window opens at the configured aspect (clamp_window_4x3 -> clamp_window_aspect). - config: [video] aspect_ratio = W:H (4:3 default, 16:9, up to 32:9) in game.toml, settings.toml (load+save), launcher seed. World geometry keeps correct proportions on the stretched present; screen-space 2D (HUD, FMV, sprite quad widths) stretches — the known limitation of the squash tier. EXPERIMENTAL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2439d4dd44 |
dispatch call contract: continuations only run when the guest really returns
Bug D root cause (Tomba chest freeze, also QA issue #1): the game's own dispatcher func_80056E08 has a dead jumptable case (type 0x21 -> jal 0x80120B3C) whose target is mid-function in EVERY overlay variant on the disc (only the boot EXE references it; the real handler entry is 0x80120AC8). Entering mid-function skips the prologue, runs an epilogue for a frame that was never pushed, and returns through garbage. Our C model then resumed the suspended generated continuations on the moved guest stack -> double-executed tails -> infinite zombie loop (~3K wild calls/frame) -> MMIO fatal. Family root of Bugs A/C/D. The contract: a generated C continuation may only run if the guest actually arrived at it ($ra == call site return address, $sp == caller sp at the call). Violations begin a "bail" unwind: cpu->pc := the wild jr's true target, every generated frame returns immediately, and the unwind resolves at the first enclosing site whose contract matches (or flattens at the outermost dispatch loop, re-dispatching the wild target on a clean host stack -- matching real-HW one-stack semantics). - cpu_state.h: bail state + psx_call_contract() helper (+ overlay-DLL pointer indirection under PSX_OVERLAY_DLL_BUILD) - emitters: (ra, sp) checks at every BIOS and game jal/jalr site; psx_dispatch_impl validates pc==0 and stop-hits against (stop, sp), handles bail resolve/propagate/flatten - dirty_ram_interp: surfaced calls validate the contract on return; bail aborts the interp run; tail transfers into compiled code now run with pc cleared (closes a latent double-run on plain C returns) - overlay cache ABI v2: DLLs export overlay_abi(); loader rejects and deletes stale DLLs (autocompile regenerates with contract checks) - telemetry: bail_first/resolved/flattened/anomaly in heartbeat + dump headers; anomaly guard at exception entry; TCP_COMMANDS.md docs Verified: BIOS+Tomba regen, boot to intro at locked 60.0 fps, all bail counters 0 through boot/title/attract (no false positives). Chest-area gameplay verification pending (requires user play). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6f7fcdc89b |
Remove all psxrecomp-v4 naming — rename to psxrecomp throughout
The "v4" suffix was a holdover from iterative project restarts. There is now one psxrecomp; all identifiers, include guards, cmake variables, function names, log strings, schema tags, and tool paths drop the v4 suffix. Changes: - PSXRECOMP_V4_ROOT → PSXRECOMP_ROOT (cmake) - PSXRECOMP_V4_RUNTIME_SOURCES/_INCLUDE_DIRS/_BIOS_GENERATED → no V4 - psxrecomp_v4_add_runtime_target → psxrecomp_add_runtime_target - PSXRECOMP_V4_*_H include guards → PSXRECOMP_*_H - "psxrecomp-v4" log/window strings → "psxrecomp" - Schema version strings in recompiler JSON output - Tool script hardcoded absolute paths updated Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
cf05a0ab1f |
runtime + recompiler: Tomba boot through BIOS into Whoopee Camp FMV
Recompiler — `recompiler/src/function_analysis.cpp` now stops a function's end_addr at its mapped `jr $ra` + delay slot instead of stretching through trailing data. Fixes Tomba's 0x80076DB0, a real 136-byte function that was being emitted as a psx_unknown_dispatch stub because data after it poisoned the boundary heuristic. `code_generator.cpp` emits `func_<next>(cpu); return;` as a tail call when a mid-function seed split leaves the physical fall-through in a different C function, plus a `debug_server_log_call_entry` at every function entry. `full_function_emitter.cpp` splits dispatch into `psx_dispatch_impl(cpu, addr, stop_addr)` with public `psx_dispatch` and new `psx_dispatch_call(cpu, addr, return_addr)` so JAL/JALR into RAM stubs can return cleanly without growing the native stack. Runtime — `runtime/src/dma.c`/`include/dma.h`/`src/memory.c` now expose `dma_write_masked()`; the 16-bit and 8-bit mmio paths route DICR/DPCR/CHCR partial writes through it so a partial DICR write no longer accidentally clears unrelated channels' pending IRQ flags, and CHCR only triggers DMA when the written mask actually includes the start bit. Unified `complete_transfer(ch)` across all channels, removed `prev_master_flag` edge detection. MDEC (`runtime/src/mdec.c`, `include/mdec.h`) gains a full debug surface: `MDECDebugState` snapshot, an always-on 4096-entry `MDECDebugEvent` ring with 11 event kinds (reset, ctrl_write, cmd_begin/done, decode_done, dma_in/out start/end, output_drained, read_underflow), and `mdec_state` / `mdec_trace` / `mdec_trace_clear` debug-server commands. DMA ch0/1 emit start/end events around MDEC transfers. CDROM (`runtime/src/cdrom.c`, `include/cdrom.h`) — read stream separated from one-shot pending state (`read_cmd`, `read_delay`, `start_read_stream`, `stop_read_stream`, `deliver_read_sector`); `cdrom_state`/`cdrom_trace` report the new fields. Thread/fiber tracing — `traps.c`'s Win32-fiber TCB save/restore now emits 12 thread-event kinds; new `ThreadTraceEntry` ring (64 K) plus `thread_trace` / `thread_trace_clear` debug-server commands report both live CPU regs and the target TCB's full saved register block. wtrace / mmio rings — `WriteTraceEntry` grew (cpu_pc, sp, v0/v1/a0-a3/ t0/t1); capacity 1 M -> 4 M. `MmioTraceEntry` grew (pc, cpu_pc, sp, a0-a3, sr, epc, i_stat, i_mask). Both dump commands accept `count`/`newest=1` for reverse iteration. `restore_trace` and `dirty_block_log` emit extended register fields (a2/a3/sp). Misc — `interrupts.c` polls debug-server every 16K checks so it stays responsive without a separate thread. `psx_runtime.h`'s `call_by_address` routes through `psx_dispatch_call`. Dirty-RAM interp JAL/JALR consult `psx_dispatch_game_compiled` when the game is wired in. iso_reader gains the entry points needed by the game disc path. New `tools/_tomba_probe.py` for port-4470 health checks. Generated — regenerated `SCPH1001_dispatch.c` against the new `psx_dispatch_impl` shape. ISSUES.md — opened Issue #3 (BIOS PS-logo glyph missing on the disc-detected screen, cosmetic). .gitignore — added `recompiler/build-asan*/`, `build-checked/`, `__pycache__/`. Validated end-to-end: BIOS boots, disc detected, Whoopee Camp FMV plays with audio. Frame count advances through ~40K frames without freezes. `unknown_dispatch_log.total = 0`. Remaining FMV cluster issues (1/4-size render, FMV-2 audio missing, no skip) tracked under TombaRecomp/ISSUES.md. Co-Authored-By: Codex (OpenAI) <noreply@openai.com> |
||
|
|
adaea3d8bf |
Salvage clean hardware sim from v2/v3: GTE, SW renderer, ISO reader, CRC32
Audited psxrecomp-v3 and psxrecomp-v2 hardware simulation files. Carried forward only pure hardware simulation with zero HLE/BIOS contamination. Each file was read in full and checked for: - event_deliver / bios_ / interpreter_ references (none found) - printf/fprintf (removed from GTE, none in others) - stubs (return 0, TODO, FIXME) (none found) - v3-specific state dependencies (removed psx_runtime.h, automation.h) Added files: - gte.cpp/h: COP2 coprocessor (876 lines, all GTE commands) - gpu_sw_renderer.c/h: Software rasterizer with texture support (723 lines) - crc32.c/h: CRC32 utility (27 lines) - iso_reader.cpp/h + iso_reader_c.cpp: ISO 9660 reader (490 lines) Modified: - cpu_state.h: Added gte_data[32] and gte_ctrl[32] arrays - CMakeLists.txt: Added new source files Explicitly rejected (per CLAUDE.md rule 7): - bios.c, events.c, threads.c, interpreter.c (HLE/kernel state) - bios_trace.c, func_logger.c, main_runner.cpp (interpreter era) - cdrom.c (has event_deliver + hard-coded RAM writes, needs surgery) - spu.cpp from v2 (WinMM-only, needs SDL port) Build verified: clean compile and link. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
a1165d51b7 |
Phase 2 complete: BIOS boots to GPUSTAT read
Full recompilation pipeline working: - Recompiler emits 799 functions (46k instructions) from SCPH1001.BIN - Tail-call trampoline in dispatch loop prevents stack overflow - Timer registers (3 counters, mode, target) implemented - Interrupt check at labels + dispatch boundaries with vblank scheduling - SYSCALL 1/2/3 (Enter/ExitCriticalSection, ReturnFromException) handled - EPC correctly set from SYSCALL instruction address BIOS boot reaches first GPU hardware access: UNIMPLEMENTED MMIO READ32 (GPUSTAT) @ 0x1F801814 Phase 3 (Sony logo) starts here: GPU, DMA, display pipeline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |