The repository root carried seven markdown files that were not onboarding
material, plus two that had been superseded by the code they described. This
sorts them: reader-facing docs to docs/, working notes to docs/internal/, and
the two superseded ones merged into the docs that replaced them.
Moved to docs/: WIDESCREEN.md, ENHANCEMENTS.md, TCP_COMMANDS.md
Moved to docs/internal/: PRINCIPLES.md, ISSUES.md, DEBUG.md, GTE_LM_FIX.md,
and from docs/: bios-hardening.md, overlay-status.md,
overlay-plan.md, overlay-recompilation-design.md,
overlay-discovery.md, TOMBA2_EXC_CONSULT_RESPONSE.md,
CASE_A_AOT_GAP.md, audit_inventory.md
Moved to docs/internal/upstream/: UPSTREAM_2026-08-20.md
The root keeps only what belongs there: README.md, CONTRIBUTING.md, LICENSE,
CLAUDE.md, THIRD_PARTY_ATTRIBUTION.md, RELEASE_NOTES.md, the dotfiles and
psxrecomp_cli.py.
MERGES
SPLITGEN_MIGRATION.md folded into docs/BUILDING.md as "Split generated C". Its
migration instructions were stale in a way that mattered: it told every game
repo to hand-edit its CMakeLists and add a file(GLOB ...) over
<serial>_full*.c, but runtime/runtime.cmake now does that itself. GEN_FULL_GLOB
is a multi-value argument of psxrecomp_add_game_runtime(); the loop at
runtime.cmake:1985 makes each pattern absolute against CMAKE_CURRENT_SOURCE_DIR,
expands it with file(GLOB), and concatenates the hits into
GAME_GENERATED_FULL_C. So a title names the pattern and gets however many shards
a regeneration produced. The rewritten section describes that, the two-step
fallback when the globs match nothing (GEN_FULL_FALLBACK, else <serial>_full.c
derived from the dispatch marker by substituting _dispatch.c), and the one
caveat the code actually has: that file(GLOB) is plain, not CONFIGURE_DEPENDS,
so a regeneration which changes the shard COUNT needs a re-configure to be seen.
psxrecomp-toml-readme.md folded into docs/GAME_PROJECT_SETUP.md as "EXE-only
autofill (psxrecomp-toml)", placed after "Disc autofill (probe_disc.py)" and
before "Project Studio" so the two autofill paths sit together. Its documented
flags were checked against the usage text in recompiler/src/main_toml.cpp:28-42
and against the argument parser at :150-165 -- all seven options match with no
drift, so the flag list is reproduced as-is. Added a pointer that probe_disc.py
is preferable when a cue exists, since that tool does the same EXE-header work
plus disc identity and netplay gates.
FUNCTIONAL FIXES
tools/gen_tcp_commands.py wrote to REPO/"TCP_COMMANDS.md". After the move that
path does not exist, so the generator would have created a new stray root file
and --check would have failed against it forever. Retargeted to
REPO/"docs"/"TCP_COMMANDS.md", along with the seven docstring and message
strings that name the path.
Running it surfaced two further problems, both fixed here:
* The doc was genuinely stale -- pre-existing drift, not introduced by this
commit (the moved file is byte-identical to origin/master, and no
debug-server command table is touched here). Four commands registered since
the last regeneration were missing from the index -- cdrom_timing_dump,
savestate_status, ws_ui_groups, xprobe_watch -- and scanline had gained a
backend. Regenerated: 307 -> 311 commands, and the change is confined to the
AUTOGEN block (the hand-written head and tail are byte-identical to master).
tools/gen_tcp_commands.py --check now exits 0.
* DOC.write_text() has no newline argument, so Python translated every newline
to os.linesep. On Windows that rewrote all 585 lines of the file to CRLF and
turned a four-line index update into a whole-file diff -- measured: 589 CRLF
/ 0 LF after a run, against 0 CRLF / 585 LF in the repo. Now written through
an explicit newline="" handle, verified to leave the file at 0 CRLF.
runtime/include/debug_server.h:9 and runtime/src/debug_server.c:8 cited "TCP.md
and DEBUG.md". TCP.md has never existed in this repo under that name; both now
name docs/TCP_COMMANDS.md and docs/internal/DEBUG.md.
LINK AND REFERENCE FIXES
Every inbound reference to a moved file was located by grepping each basename
across the whole tracked tree, not just the markdown. Markdown hrefs in
README.md, CONTRIBUTING.md, docs/ARCHITECTURE.md, docs/COMPILING_OVERLAYS.md,
docs/AOT_OVERLAY_PLAN.md, docs/RELOCATION_MANIFEST_FORMAT.md,
docs/config_schema.md, docs/OVERLAY_XREF.md, docs/TCP_COMMANDS.md and several
docs/internal/ files now point at the new locations; where two files became
siblings under docs/internal/ the path was shortened rather than lengthened
(bios-hardening.md's reference to overlay-discovery.md, for instance).
docs/COMPILING_OVERLAYS.md's "in this folder" phrasing was wrong once two of the
three files it listed moved, so it now names internal/ explicitly.
24 source-comment mentions of ENHANCEMENTS.md sections G1.x across 19 files
(recompiler, runtime, cmake) were retargeted to docs/ENHANCEMENTS.md with the
section numbers untouched. docs/SHADOW_ENHANCEMENTS.md is a DIFFERENT file and
was deliberately excluded.
20 rationale strings citing docs/CASE_A_AOT_GAP.md across the four
recompiler/seeds/*.json files were updated. Done as a byte-level substring
replacement rather than a JSON round-trip: two of those files are stored with
CRLF endings and two with LF, and re-serializing them reformatted 5,700 lines.
The byte-level version changes exactly 20 lines and each file still parses.
References that look similar but point elsewhere were deliberately left alone,
and are worth naming so a future pass does not "fix" them:
* "recomp-template PRINCIPLES.md" / "NES recomp template PRINCIPLES.md" in
CLAUDE.md:43, docs/SHADOW_ENHANCEMENTS.md:28,
docs/internal/HLE_SCHEDULER_CARVEOUT_PLAN.md:5,
runtime/src/debug_server.c:4934 and tools/parity_diff.py:8 are a different
repository's file. Only the five that mean this repo's PRINCIPLES.md were
updated.
* Every remaining bare ISSUES.md mention is a GAME repo's tracker
(MegaManX6Recomp 2/7/8/9, TombaRecomp 6), not this repo's.
* docs/disc-speed.md:4,54,56 and docs/internal/bios-hardening.md:5 name
overlay-discovery as a BRANCH, not a file, so no path applies.
INDEXES
README.md's "New here?" table gains rows for docs/WIDESCREEN.md,
docs/ENHANCEMENTS.md and docs/TCP_COMMANDS.md. docs/internal/README.md gains
sections indexing all fourteen documents that moved in, plus an "Upstream
contributor reviews" section describing upstream/ and the new sweep file, plus a
"Removed" bullet for CYCLE_TIMING_ARCH.md (deleted in the previous commit)
pointing at FAITHFUL_TIMING_PLAN.md, CYCLE_MODEL_BEETLE.md and
PRECISE_IRQ_SLICE.md -- following the SLJIT_PERSIST_CACHE.md precedent already
in that file.
docs/ENHANCEMENTS.md:7 claimed framework bugs live in "game repos' ISSUES.md
until a framework tracker exists". A framework tracker does exist; it now says
so and names the Beads epic.
VERIFICATION
A link checker was written to scan every tracked *.md for relative links,
resolving each against the tree being scanned. Baseline at origin/master: 129
files, 137 relative links, 3 broken. After both commits: 127 files, 159 relative
links, 3 broken -- the SAME three, all pre-existing and untouched here:
docs/DATA_SHARDS.md -> INSTANT_LOADS_PARKED.md (never existed)
docs/DATA_SHARDS.md -> INSTANT_LOADS_SPIKE2.md (never existed)
docs/NETPLAY.md -> ../lib/recomp-net/README.md (inside a submodule)
Zero new broken links. The checker's own coverage was checked against a raw grep
for markdown link openers: the 3-link difference is C++ lambdas inside code
blocks, correctly not treated as links.
Also verified: py_compile over all 113 tracked tools/*.py plus psxrecomp_cli.py;
all four seed JSONs still parse; the test-registration guard still reports 123
test files all registered; recompiler/tests/test_coverage_vault.py still passes;
and no file in this commit had its line endings flipped (checked mechanically
across all 50 modified files, after two files were caught and redone).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
14 KiB
Native-Wide Rendering — High-Level Plan (handoff)
Branch: feat/native-wide (both repos), forked from feat/widescreen
(psxrecomp b1b7d6b / TombaRecomp 36dcc42). Tomba's psxrecomp-v4.pin
on this branch points at the psxrecomp feat/native-wide tip.
For the next session: this is a direction + phased plan, not a spec. Read the "Current state" and "Core idea" sections, then execute the phases in order, verifying each against the oracle before moving on. The completeness rule applies: native-wide replaces the squash hack — do not ship both side by side. The last phase deletes the hack.
Implementation status (2026-06-15)
Foundation implemented + verified (runtime-only, committed on this branch).
The centering mechanic adopted is the proven draw_offset-shift approach
(not the OFX-bias framing the phases below were originally written around — see
"Core idea" for why it's strictly simpler). A wide-aspect mode model now
lives in gpu.c: 0 = off (4:3 identity), 1 = squash (legacy), 2 = native-wide. Native-wide, on a game frame (not FMV/menu):
- GTE is fed the 4:3 ratio → identity, no squash (
main.cppengage). draw_offset_x += OFFSETat the GP0(E5) chokepoint (gpu.c) → 3D and 2D shift together byOFFSET = round(W*(3num-4den)/(8den))(=53 @ 320/16:9).draw_area_right += EXTRAat GP0(E4), clamped to 1023 so a buffer at the VRAM edge clips rather than wraps into textures (EXTRA = 2*OFFSET = 106).- Present widens the display read by EXTRA (
main.cpppresent_w); the existing 2× present then maps it 1:1 (isotropic) — the old squash-stretch becomes an honest 2× with no magnification. No renderer-backend change needed (both already render full-VRAM-width; only the read width + offsets changed). - Diagnostics:
gpu_statereportsws.mode/ws.nw_extra;ws_nw on=<0|1>TCP command toggles native-wide vs squash live for A/B.
Verified in gameplay (16:9): content renders at correct native
proportions (A/B vs squash: squash visibly stretches the dialogue text; native-
wide keeps it compact) and present is 1:1. 4:3 is identity by construction
(mode 0 → every native-wide term is 0; present_w == w).
Decision point #1 RESOLVED with ground truth (the gating finding). Tomba's
VRAM double-buffer is two adjacent 320-wide buffers: front x∈[384,703],
back x∈[704,1023] — the back is flush against the 1024 VRAM right edge, and
the two buffers touch (704 = 384+320). So there is no horizontal headroom for
the +EXTRA widening on either buffer: widening the back wraps past x=1023 into the
texture region; widening the front overlaps the back. The revealed margin strips
therefore show adjacent-buffer / VRAM-wrap bleed (a duplicated scene fragment
on the right), NOT the clean "stale VRAM" the earlier PoC notes assumed. The
center is correct; the edges are not.
The flicker (why the shared-canvas approach fails). The first cut shifted +OFFSET
into draw_offset and widened the display read by +EXTRA inside canonical VRAM.
This flickers badly (and even segfaults) and cannot be made clean, because:
- The display alternates between the two framebuffers every frame. Buffer A's
widened read
[384,810]is in-bounds (→ bleed strip), but buffer B's[704,1130]falls off the 1024 VRAM edge → black strip + the draw wraps[1024,1129]→[0,105]over textures/CLUTs (the idle segfault). Two totally different frames → flicker. - Even widening VRAM to 2048 does not fix it: the buffers are adjacent
(Δ=320), so widened-A
[384,809]and widened-B[704,1129]share storage at[704,809]. A's content[437,756]and B's[757,1076]are adjacent → each buffer's margin reads into the other's content, and clearing one margin erases the other's content. Unfixable in a shared canvas. - VRAM is ~95% full (occupancy-mapped) → no room to relocate buffers apart.
- Changing the canonical wrap mask to
&2047is also unsafe: it alters PS1 coordinate semantics (negative/clipped coords, VRAM copies, clears, mask ops) for the opted-in game, not just our framebuffer draws.
Adopted architecture (separate wide compositor surface). Keep canonical PSX
VRAM 1024-wide and completely faithful — every GPU op unchanged, so 4:3 / non-
opted games are byte-identical (the extensibility guarantee). For an opted-in wide
game, maintain independent wide present-surfaces keyed by framebuffer base
(learned from the display_x set). Mirror each framebuffer-targeting primitive
into its buffer's surface at local coords (SX+OFFSET, SY); clear margins
in-surface; present from the surface matching display_x. Textures/CLUTs always
sample canonical VRAM; the wide surfaces are render targets, not texture memory.
Independent surfaces ⇒ no A↔B sharing ⇒ no bleed, clean margins, no flicker.
Milestones: (1) wide surface + mirror + present → flicker/bleed gone (margins
blank until culls widen); (2) regen-class cull-widening submits the off-screen FOV
→ it rasterizes into the margins → true native-wide. Key complexity driver:
whether the game does framebuffer feedback (reads its framebuffer back / VRAM→VRAM
copies between buffers); gp0_copy is nonzero for Tomba so worth confirming.
Reusable from the first cut: the opt-in mode descriptor, GTE-identity feed,
ws_nw_offset/extra math, ws_nw live toggle, debug fields. Reverted: the
canonical draw_offset/draw_area/clamp injections (they move into the wide
surface's local coords). Files: runtime/src/gpu.c, runtime/include/gpu.h,
runtime/src/main.cpp, runtime/src/debug_server.c, runtime/src/gpu_sw_renderer.c,
runtime/src/gpu_gl_renderer.c.
Goal
Render Tomba (and the framework generally) at a genuinely wide internal frame (16:9 / 21:9) with correct proportions everywhere and no magnification, replacing the current squash-then-stretch hack. The extra horizontal field of view should be real rendered pixels, not a blow-up of a 4:3 frame.
Why (what's wrong with what ships today)
feat/widescreen implements the squash hack:
- Squash —
gte_set_display_aspect()inruntime/src/gte.cppmultiplies projected screen-X by(4·den)/(3·num)(s_ws_xnum/s_ws_xden), cramming more world into the native ~640-wide internal frame. - Stretch — the present path in
runtime/src/main.cpp(g_logical_w,SDL_RenderSetLogicalSize, thedstrect around line 1339) stretches that 640-wide frame out to the wide window.
Squash-then-stretch cancels for true 3D geometry → wider FOV, but it is structurally lossy and spawns a pile of compensations that all exist only to fight the stretch / squash:
- 2D screen-space (HUD, sprites, FMV, parallax backdrops) is computed without the
GTE, so the present-stretch distorts it →
hud_sprt_squash, per-prim proportion correction, FMV pillarbox,psx_ws_backdrop_xscreenX squash. - Backdrops authored for a 4:3 extent fall short of the revealed wide edges
→ the 8C blue void, patched by the depth-gated un-squash
(
s_ws_suppress,s_ws_far_threshold = 900, the per-vertex SZ split ingte.cpp). - It's a magnification → softness.
Native-wide removes the cause, so most of the above compensations can be deleted rather than tuned.
Core idea
Make the runtime render into an actually wider framebuffer and stop squashing / stretching:
- Internal frame width grows from the 4:3
WtoW · (num/den)/(4/3)(e.g. ×1.333 for 16:9), height unchanged. - The GTE projection is identity-scaled (no X-squash); instead the
projection center X (
OFX-equivalent, thes_ws_*bias) is shifted so the 3D scene is centered in the wider frame. The extra width reveals world that was previously off-screen left/right — true FOV. - The GPU draw area / clip / scissor and the display region widen to match.
- Present is 1:1 (no horizontal stretch); the window's logical width equals the wide internal width.
The hard truth (scope driver)
3D geometry adapts for free via the projection center shift. 2D
screen-space elements do not — anything the game positions in raw screen
coordinates (HUD, dialogue boxes, the parallax 2D backdrop layers, menus) will
cluster at the left and leave the new right-side blank. So native-wide still
needs per-game 2D re-anchoring — but the goal flips from "de-stretch" to
"re-anchor / widen-layout with correct proportions," which is cleaner and reuses
the existing Ghidra-evidenced site lists in Tomba's game.toml
([widescreen], [widescreen.cull], [widescreen.backdrop]).
Phased plan
Each phase is independently verifiable. 4:3 must remain pixel-identical
throughout (the wide path is gated; identity when num/den == 4/3).
Phase 0 — One wide-viewport abstraction.
Introduce a single descriptor (wide internal width, projection-center bias,
present mapping) consumed uniformly by GTE, GPU, and present. Replace the
scattered s_ws_xnum/xden + g_logical_w stretch with this. Keep 4:3 = identity.
Files: gte.cpp, gpu.c (gpu_ws_configure, gpu_ws_present_native_43),
main.cpp.
Phase 1 — Widen the render target (both renderers), present 1:1.
- Software: widen the VRAM display/draw region and the
g_hrSSAA mirror (gpu_sw_renderer.c); present without horizontal stretch (main.cppdst rect). - OpenGL: widen the VRAM FBO + viewport (
gpu_gl_renderer.c, note ~line 1115). - Checkpoint: with no GTE change yet, the game renders into the left portion with a blank wide margin — proves the pipeline widened and present is 1:1.
Phase 2 — GTE: projection-center shift, identity scale.
Remove the X-squash; add a horizontal projection-center bias so 3D centers in
the wide frame. Now 3D fills the wider frame with correct proportions and real
extra FOV. Delete s_ws_xnum/s_ws_xden and the depth-gated un-squash
(s_ws_suppress, s_ws_far_threshold) — native-wide makes them unnecessary.
Checkpoint: 3D world correct & wide; 2D still left-clustered.
Phase 3 — GPU draw offset / clip / scissor for the wide frame.
Make drawing-area, clip rects, and scissor respect the wide width so 3D prims
and clipping aren't cut at the old 320/640 boundary (gpu.c,
gpu_sw_renderer.c, gpu_gl_renderer.c).
Phase 4 — 2D screen-space re-anchoring (the per-game bulk). Repurpose Tomba's existing site lists from "squash" to "re-anchor":
- HUD/sprite tags (
[widescreen] sprite_tag_funcs,hud_sprt_squash) → edge-anchor instead of squash. - Parallax backdrops (
[widescreen.backdrop] x_sites,psx_ws_backdrop_x) → widen the layout span instead of squashing screenX. - Dialogue / menus → center or edge-anchor. This is the long tail; drive it by the Ghidra-evidenced PCs already documented.
Phase 5 — Culling widening relative to the new half-width.
The [widescreen.cull] bias/range sites (psx_ws_x_margin) already widen the
world-space cull; recompute the margin against the new wide half-width so far
geometry isn't culled before it reaches the wide edges. Confirm the 8C void,
edge-drift, and grass-top straddle are gone (they should be, structurally).
Phase 6 — 4:3 content in a wide frame. FMV (4:3 source) and the BIOS boot pillarbox/center in the wide frame; menus that must stay 4:3 likewise. Reuse the existing FMV-4:3 / BIOS-4:3 logic.
Phase 7 — Delete the squash hack (completeness).
Once native-wide reaches parity, remove the squash codepath entirely:
s_ws_xnum/xden, the present-stretch (g_logical_w stretch semantics),
gte_set_display_aspect squash math, psx_ws_backdrop_x squash, the
depth-gated un-squash, and the now-dead compensation patches. Update
../WIDESCREEN.md. No dual-mode left behind.
Decision points for the handoff session
- Internal width vs VRAM. PSX VRAM is 1024×512; a 16:9@240p wide frame
(~853 at 2× internal) fits, but confirm the display-region/
DISPENV-width override approach and how it interacts with the game's own GPU display reg writes. Decide: runtime overrides display width, game keeps drawing. - Projection-center bias source. Per-frame from camera/projection state, or a fixed center shift? Start fixed-center (symmetric reveal); revisit if the game re-centers projection mid-scene.
- 2D re-anchoring coverage. How much of the long tail (every UI/backdrop site) is in scope for first landing vs. follow-up. Recommend: core HUD + backdrops first, log uncovered sites (no silent gaps).
- Single wide mode vs. keep squash as fallback. Recommendation: native-wide is the wide mode; 4:3 stays identity; squash is deleted (Phase 7). Do not ship both.
Verification
- Per phase: oracle screenshot compare + the
tools/capture_game_window.ps1client-area grab (note: this build's window title is "Tomba! Recompiled", so match*Tomba*, notTombaRecomp*). - 4:3 regression gate: with
aspect_ratio = "4:3"the output must stay pixel-identical to master at every phase. - Known-issue sites to confirm fixed by Phase 5: 8C far-backdrop void, edge object-pop/drift, mountain grass-top straddle, and likely 8F dialogue-split.
ws_census/ws_margin/ws_far_thresholddebug-server commands (port 4470) remain useful for inspecting per-draw extents while bringing this up.
Build / run notes for the next session
- Both repos are on
feat/native-wide. Tomba'spsxrecomp-v4.pinpoints at the psxrecompfeat/native-widetip; build withtools/build-linux.sh --nopin(or check the psxrecomp tip out so HEAD matches the sha). - Regen Tomba C after recompiler-side changes:
recompiler/build/psxrecomp-game.exe --config game.toml(run from the Tomba repo root).regen.ps1is broken — invoke the exe directly. - Canonical build dir:
TombaRecomp/build-stable(Ninja, RelWithDebInfo, TCP debug on 4470). Build targetpsx-runtime. - Reference: the squash-hack equivalents of every symbol above live on
feat/widescreen; diff against it to see what each compensation was doing.