Second report on #244: main.cpp references C-defined globals that MSVC
cannot resolve. MSVC mangles namespace-scope variables; the Itanium ABI
does not, so a C++ reference to a C global links on GCC/Clang and fails
under MSVC with an unresolved external.
main.cpp already has the fix pattern -- the file-scope extern "C" block
added by the earlier MSVC pass, with a comment saying exactly this. Ten
globals added since then never made it into the block:
memory.c i_stat, i_mask, g_guest_store_count,
g_vblank_ack_count
interrupts.c g_vblank_raise_count, g_vblank_deliver_count
dirty_ram_interp.c g_dirty_ram_blocks_run, g_dirty_pump_count
overlay_loader.c g_call_unit_depth
psx_bios_backend.c g_psx_dispatch_depth
Each is referenced only through a block-scope `extern` inside a function.
With no namespace-scope declaration in view, that block-scope declaration
gives the entity C++ linkage, which is the failure. Declaring them in the
file-scope extern "C" block fixes it without touching the use sites: the
block-scope redeclarations now inherit C linkage, as the block's comment
already describes for the symbols that were there.
Which ten was measured, not eyeballed. Every block-scope extern in
main.cpp (31 distinct symbols) was probed by appending
`extern "C++" { extern T sym; }` to a copy of the file compiled with the
target's real command line: a conflicting-linkage diagnostic means the
symbol already has C linkage from an extern "C" header, silence means it
does not. 21 came back already-C (declared in psx_cycles.h, cpu_state.h,
debug_server.h and friends, or already in this block); these 10 came back
C++. After the change all 31 probe as C linkage.
Audited beyond main.cpp: it is the only C++ TU in the runtime with this
pattern. beetle_libretro.cpp's `extern PS_GPU GPU` / `extern PS_CDC *PSX_CDC`
are genuine C++ symbols from the Beetle core, correctly left with C++
linkage, and psx-beetle is not built under MSVC in any case.
No behaviour change on the existing toolchains: main.cpp.o is byte-identical
to its origin/master build. Full runtime suite on a clean build dir is 47/48,
the single failure being gte_register_access_test, which fails identically at
origin/master (43/44 there) -- it links gte.cpp + pgxp.cpp but references
gpu_ws_precise_nclip_enabled, defined in gpu.c, which is not in that target.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YD7rrVK63Kncki6prB65S
Two shipping gaps left Gran Turismo's gameplay and FMV code on the
interpreter (0.55-0.64x real time) even though the recompiled boot EXE was
fine:
1. The overlay region floor defaults to the boot EXE text end. GT's secondary
EXEs (GTOS/GTMENU/GTMAIN/GTEND) all load at 0x80010000, inside that range,
so they were never overlay-cache eligible. The only way to lower the floor
was the PSX_OVERLAY_REGION_FLOOR env var, which a shipped package cannot
carry. Add `[runtime] overlay_region_floor` (hex string or integer), applied
before the env override with the same kernel-window clamp.
2. The bundled overlay_toolchain/ was only used to build the tcc command.
A dev/production box with gcc on PATH but no game.toml autocompile command
got no compiler at all. Treat the bundle as a gcc toolchain provider too:
gcc counts as available when the bundle is present and a gcc/cc/clang is
on PATH, and the same bundle then drives compile_overlays.py with
--compiler gcc. One package yields gcc shards on a dev box and tcc shards
on a toolchain-less player box.
Measured on GT1 (SCUS-94194), headless boot -> intro FMV -> attract, with the
floor at 0x10000 and gcc autocompile: 1.27-1.38x real time vs 0.55-0.64x;
SPU production 27 kHz -> 59 kHz (the FMV audio distortion was starvation).
(cherry picked from commit 0d226e5069bdf1395c819110beec40367a44a699)
Supersedes the global HYBRID controller-mode shape from PR #233 with a trusted plugin callback that can choose analog or digital presentation per input sample.
Keeps analog mode hardware-faithful by no longer folding physical D-pad input onto the left stick, while leaving debug injection able to steer stick-only paths.
Includes the pad/cursor probe tooling from #233 for diagnosing input doubling.
The overlay-region model assumed the boot EXE sits at the bottom of RAM:
[KERNEL_END, text_end) is main-EXE text, [text_end, RAM) is runtime-loaded
overlay code. That holds for a load_address of 0x80010000, where the text
base equals the kernel-window end and the region below the text is empty.
It is false for a boot EXE that loads HIGH and streams its gameplay code
into the RAM beneath itself. Klonoa loads a 46 KB stub at 0x80180000 and
runs its overlays from 0x10000-0x130000, so the floor landed at 0x18B000
and the entire overlay region fell below it:
- dirty_ram_clear_image_baseline() wiped the dirty bits across
[0x10000, 0x18B000) believing it was the clean compiled text image.
- the dirty_ram_dispatch_inner admit heuristic requires phys >= floor,
so a JALR into an overlay page the CD had DMA'd there (never touched
by the CPU write hooks, so never marked dirty) was refused by the
interpreter, fell through to psx_unknown_dispatch and fail-fast
exit(1) — Klonoa, frame 687, target 0x80123D00.
Pin the text BASE (g_text_image_lo) alongside the floor and route the
gates through one predicate: the overlay region is both sides of the text
image, not just above it. Street Fighter Alpha 3 (0x80113B00) and
Bomberman Fantasy Race (0x8003004C) load high too.
No-op for bottom-loading games: g_text_image_lo stays at its 0x10000
default, the below-text clause is empty, and the baseline clear starts at
the same page as before.
Klonoa now runs 6874+ frames at a locked 60 fps with fail-fast on and
zero unknown dispatches; it previously died at frame 687.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit cf742f3190)
(cherry picked from commit 30e8a71f93539c4e10ff6767c44c24137c9dd714)
(cherry picked from commit 94f184f744)
psxrecomp_add_game_runtime() (added in #110) is unusable with
PSX_RECOMP_UI=OFF and real generated game C linked — a documented,
supported config ("PSX_RECOMP_UI OFF for headless/generated builds",
BUILDING.md) that nothing currently exercises. Found while bringing up
a new title with recomp-ui intentionally left out initially.
Two independent breaks in that combination:
1. main.cpp calls psx_game_codegen_forward_if_built() from main()
whenever PSX_HAS_GAME_CODEGEN is set (i.e. any time real game C is
linked), but only *declares* it under the narrower RECOMP_LAUNCHER
guard. Any no-launcher build with generated game C fails with
"use of undeclared identifier". Introduced a precise
PSX_HAS_CODEGEN_SETUP_HOST macro (set only when a codegen-setup
host is actually linked) and re-guarded both the declaration and
the call site on it instead of the mismatched pair of guards.
2. psxrecomp_codegen_host.c was unconditionally added to every game
target's extras, but it unconditionally #includes recomp_launcher.h
from the recomp-ui submodule. Any --no-recomp-ui build fails with
"recomp_launcher.h file not found". Gated its inclusion on
PSX_RECOMP_UI.
Verification:
- Built and ran a new title (headless: PSX_RECOMP_UI=OFF, real
generated game C, no launcher) end to end: OpenBIOS -> game entry ->
continuous gameplay, clean atexit shutdown, no crash.
- Regression-checked the one existing title that actually calls
psxrecomp_add_game_runtime() (TombaRecomp; every other shipped title
calls the lower-level psxrecomp_add_runtime_target directly and
never reaches this code): built TombaRecomp's real production config
(PSX_RECOMP_UI=ON, ENABLE_SETUP_WIZARD, recomp-ui launcher, mod
catalog, setup-host/no-disc mode) against this branch via
-DPSXRECOMP_ROOT override, no submodule surgery. Clean build, ran to
the launcher with no regressions. Both changes are conditionally
gated on flags Tomba always sets the opposite way, so this is
expected and was confirmed empirically, not just by inspection.
Not covered by this PR: CMAKE_CXX_STANDARD 17 is also never inherited
by a game repo that include()s runtime.cmake directly (as opposed to
add_subdirectory()-ing runtime/CMakeLists.txt), which silently
compiles text_xlate.cpp's std::filesystem usage against whatever the
host compiler's default standard is. Six of seven other title repos
work around this by setting it themselves; happy to send a follow-up
if that's wanted here instead of per-repo.
A still image drawn behind the game, filling whatever the letterbox or
pillarbox leaves over -- the trick vertical shmups use on horizontal displays,
where the dead space carries artwork so it reads as designed rather than as a
limitation. MAME calls the same idea bezel artwork.
Drawn across the whole drawable before the game quad, which is simpler and
safer than filling two margin strips: no margin arithmetic, it covers pillarbox
and letterbox alike, and the frame paints over the centre.
The reason this is the right shape and present_edge_fill was not: edge fill
stretches one edge COLUMN of the live frame across the margin. That suits a flat
menu backdrop and turns track geometry into horizontal smears, so it needs to
know whether the current frame is a menu -- a question it answers with
force_4_3, which is only ever set by the widescreen path. A still image has no
such question. It looks the same in a race and in a menu, which is the point.
Loaded after the GL context exists (the first attempt sat next to tex_pack_init,
which runs before the window, so g_gl_active was false and it silently never
loaded). Decoded with stb_image, already linked for the HD texture pack. A
relative path resolves against the disc directory, matching the pack convention,
so artwork can sit beside the disc.
Verified on a 7680-wide maximised window with 4:3 content: margins that were
pure black now carry the image on both sides, with the frame untouched in the
middle.
Known limits: the image is stretched to the drawable, so an aspect that does not
match the window will distort (cover-crop is a small follow-up), and this has
only been exercised in 4:3 pillarbox, not with the widescreen mod active.
For WipEout 3 the team wallpapers by MotorcycleEmptiness
(awesome-wipeout.github.io) suit this exactly; the shipped default is a
placeholder gradient used to verify the path, not final art.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
clamp_window_aspect fits the CLIENT area to the usable bounds, but a window is
client plus title bar and borders. On a display whose usable height it exactly
matched, the result was a window taller than the screen: client 5504x4128,
window 5550x4263, positioned at y = -112 and hanging off the top.
Deriving the decoration size first does not work either. SDL_GetWindowBordersSize
reports nothing useful before the window has been shown, so a post-creation
correction based on it silently did not apply -- the window stayed exactly as
wrong, which is worse than not trying.
The window manager already solves this exactly, so ask it: SDL_MaximizeWindow
fits the work area with decorations and taskbar included. Measured on a
7680x4320 panel, client area is now 7680x4039 against 5504x4128 before.
Only when the width was not explicitly chosen -- an explicit window_width is a
deliberate decision and is left alone.
Suggested by the user after two failed attempts at doing the arithmetic here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit d6d0946703)
Without it Windows virtualises everything the process sees. On a 7680x4320
panel at 400% scaling SDL_GetDisplayUsableBounds reports 1920x1032, so
clamp_window_aspect fits the window to roughly 1376 LOGICAL pixels and it opens
as a small box that the desktop compositor then upscales.
The internal render resolution is unaffected, which is what makes this
expensive rather than cosmetic: the game renders at supersampling 16 and the
result is thrown away scaling a 1376-wide window up to an 8K display. Choosing a
high internal resolution and getting a small blurry window is the exact opposite
of the intent.
permonitorv2 makes SDL report physical pixels, so the window sizes against the
real panel and the drawable matches it 1:1. Set before both SDL_Init call sites,
guarded on the hints existing so it builds against older SDL.
Also changes g_video_win_w's default from a hardcoded 1280 to 0, meaning "fit
the display", clamped to the usable bounds and the configured aspect. An
explicit width still wins. 1280 was a reasonable default when a window was a
window; it is not one on a 4K or 8K panel.
Diagnosed after reporting the display as 1920x1080 from a non-DPI-aware query
and telling the user their panel could not be larger. It is 7680x4320; the
measurement was virtualised, not the display.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 236778ed7b)
Closing the save-state menu (Square save / Circle back / toggle key) or
the rewind filmstrip resumes the guest while the closing press is still
held on the pad, so the game samples it as gameplay input - saving a
state in a fighter throws a punch, and since the queued save executes
after the guest resumes, that punch gets baked into the snapshot.
Loads already solve exactly this with savestate_input_guard_arm(),
which feeds neutral pads until the held input is released (min 90 ms,
capped at 700 ms). Arm the same guard whenever either host pause loop
hands control back to the guest, covering every close path: save,
cancel/back, Esc, the toggle hotkey, and rewind accept/cancel.
Some titles stream level code over pages inside their own static text
range (Driver 2 streams mission code over its boot text), leaving those
pages permanently diverged: below the overlay floor they are excluded
from overlay capture and fall into single-instruction dispatch thrash
(hundreds of thousands of one-instruction dispatches per window).
PSX_OVERLAY_REGION_FLOOR lowers the floor per title so diverged text
pages become overlay-cache candidates and chain through local flow.
Value is clamped above the kernel window; default behavior is unchanged
when the variable is unset. On Driver 2 this took text-blocked
interpreter work from 278M to 37M instructions per window.
Passing --disc suppressed the remembered settings/disc.cfg disc for the
launcher's initial state (the has_disc_path gate) without seeding the
launcher itself, so the window opened with "No disc selected" and the
player had to re-pick the disc on every launch even though a valid disc
was named on the command line.
Seed resolved_disc from the validated CLI path before the launcher
window opens. resolve_disc_for_runtime still applies the override
authoritatively for the runtime after the launcher returns, so
precedence is unchanged.
The vsync hotfix replaced the closing brace of the non-headless SDL
init else with log_present_cadence(), leaving main.cpp unbalanced.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep vsync/pacer XOR and skip the full-period wait while manual fast-forward already paces at the turbo multiplier.
Co-authored-by: Cursor <cursoragent@cursor.com>
Linux compositors wait a full refresh on SwapBuffers even after the pacer has already slept ~16.7 ms, which locked NTSC titles at 30 Hz / 0.50x with the CPU idle. Vsync now owns cadence only on ~60 Hz panels; otherwise the pacer holds 59.94 Hz and swap interval is forced to 0.
Co-authored-by: Cursor <cursoragent@cursor.com>
Interactive isolation on Ape Escape (in-game, live TCP toggles): OFF =
baseline jiggle; geometry ON unclamped = sparse hairline background-bleed
seams along corrected/uncorrected shared edges (both user-reported
artifacts vanished with the toggle off - the THPS2 line class, not wild
vertices); ON with tolerance 0.5 = seams gone, only sub-half-pixel
misalignment remains. Ship 0.5 as the default; negative still disables.
Also documents the screenshot_hires scale-2 windowed readback defect
(tiled/black PNG - row pitch) found during the session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Keep recomp-net at occupied_mask (d65918f); master's pin was an ancestor.
Brings in rewind/save-state controls from master.
Co-authored-by: Cursor <cursoragent@cursor.com>
Bumps recomp-net so empty lobby seats no longer block READY/admit; launch
builds the mask from seated members (online + LAN).
Co-authored-by: Cursor <cursoragent@cursor.com>