A netplay match that ends early returns both players to the lobby. The
runtime knew why (netplay_soft_exit's origin) but only printed it to
stdout, so players saw the lobby again with no reason. A boot-digest
mismatch (peers booted different BIOS images or boot settings) first
held a black window for the full 20 s admit-stall watchdog.
netplay_soft_exit now records a sentence for its origin, and the soft
return to the lobby puts it on the launcher's status line through
last_error (the room and browser views already show it). The dig0 gate
timestamps a mismatch; once the same pair of digests has held for 3 s,
the watchdog ends the match with its own reason. A peer that gets the
other side's BYE while it sees the same mismatch reports the mismatch,
not "the other player left". A CLI match prints the sentence to stderr.
The origin-to-text map and the grace decision live in
netplay_exit_reason.c, covered by netplay_exit_reason_test. Builds on
the previous commit's psx_lobby_set_last_error.
The lobby's BIOS identity was a two-value token, openbios or scph1001,
and scph1001 meant "some retail dump". An SCPH-5552 peer and an
SCPH-1001 peer both offered it, the host settled it, each peer booted
its own image, and the rollback boot digest never matched: about 20 s
of black window, then both players back in the lobby with no reason
(reported in #243).
Each offer now carries the CRC-32 of the retail image that peer would
boot (online bios_offer.retail_crc; LAN JOIN line 6). The host settles
retail only when every seat offers the same CRC, else OpenBIOS when
every seat links it, else refuses to start and puts the reason on the
launcher's status line ("Players use different BIOS images (SCPH-5552
and SCPH-1001) ..."). The settled CRC ships in match_caps
(session_bios_crc) and on MOTK1 START line 7, and each peer boots only a
dump with that CRC. Rooms where every peer has the same image keep
working, including SCPH-5552-only builds with no OpenBIOS. Old peers
send no CRC, so a mixed old/new room falls back to OpenBIOS or refuses;
a session from an old host still accepts any retail dump. The lobby
server relays bios_offer and match_caps as opaque objects, so it needs
no change.
The rule lives in netplay_bios_settle.c, shared by the online and LAN
paths and covered by netplay_bios_settle_test. last_error grows to fit
one sentence, with psx_lobby_set_last_error to set it.
The lobby rematch re-enters the emulator through goto session_reboot,
below the first-boot session block. After its commit (offline) or
netplay clear it reset mod-owned presentation state, but it never ran
mod_runtime_activate_plugins() or the controller, load-acceleration and
disc-speed resets. So an offline rematch with mods enabled applied the
plan's main-EXE and disc patches and ran its VBlank callbacks, but no
activation callback ran, and the function-entry hook table, which the
commit empties and only activation rebuilds, stayed empty. It also
mounted the stock disc even when the plan had built a derived disc
image.
start_mod_session() now holds the first-boot sequence, unchanged:
clear the controller-mode overrides and presentation policies, load
acceleration and disc speed; run reset_mod_owned_presentation();
activate; then apply the netplay local viewport, the controller-mode
overrides and load acceleration. The first boot and the rematch both
call it after their commit or netplay clear, and both then take the
mounted disc from session_disc_path(), which returns the plan's derived
image when there is one and otherwise the stock disc. Activation still
precedes renderer and window creation on the rematch, because those
happen after session_reboot on every session. The rematch re-applies
its 4:3 Settings clamp before the session start, so a plugin's
activation or the netplay local viewport can still set the aspect.
A netplay rematch clears the plan, so activating it runs nothing and
the match stays vanilla. The first boot's behaviour is unchanged.
Tests: test_mod_owned_display_controls.py now requires activation and
the mod-owned reset to happen only inside start_mod_session, in the
order reset, activate, apply. It requires both paths to call it after
their commit and before the disc mount, and the rematch 4:3 clamp to
come before it. mod_runtime_test adds the lobby rematch sequence:
activating after a netplay clear runs no callback, VBlank plugin or
hook; the offline rematch's commit and activation re-run the
activation callback and VBlank plugin and arm exactly the plan's hook.
test_mod_load_acceleration.py follows the reset block into the lambda body.
Two comments that said a soft return never re-applies the controller-mode
override are corrected: test_launcher_pad_mode_wiring.py and
test_launcher_pad_mode_resolve.cpp. MOD_PACKAGES.md now describes the
rematch as a full session start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Manual fast-forward presents every second VBlank, so the plan still saw
presents and kept rendering passes, spending host time the fast-forward
wants, although the API said turbo refuses them. main.cpp now records when
presentation is sped up (manual fast-forward, turbo-through-loads, FMV
auto-skip, TCP turbo) and the plan and passes refuse meanwhile.
psx_mod_render_pass_status() tells a plugin why passes cannot run, the
host-time budget aside: NO_PRESENTER, BACKEND (the renderer declines passes
in its current mode), DISABLED (after repeated faults), SESSION, FAST_FORWARD
or BUSY. The GL backend reports its part through
gl_renderer_pass_unavailable(), where a renderer mode that cannot host
passes reports BACKEND. psx_mod_set_frame_interpolation_blend() now also
takes effect when called from a hook after activation, so a plugin can show a
crossfade while passes are unavailable and return to HOLD afterwards.
Debug: PSX_RENDER_PASS_REFUSE=1 or TCP render_pass_refuse on=1 makes the
backend decline passes, to test a plugin's fallback; render_pass_stats
reports the status.
Default off: nothing reads the new flag or status unless a plugin uses
render passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frame_fingerprint reset_on_load=1 restarts the rolling hashes and the ring
when the next savestate load completes, so two runs that load the same state
(for example with and without a presentation mod) compare frame by frame
from the same guest point. Debug tooling only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psx_mod_render_pass_plan() / psx_mod_render_pass() let a trusted plugin
redraw its game between two logic ticks with the game's own draw code. A
pass is a sandboxed nested guest call:
- time is frozen: cycles are counted (GTE and mult/div deadlines keep
working) but no device advances, no VBlank or device event fires and no
interrupt is delivered; GPU linked-list DMA and delayed completions finish
synchronously (psx_cycles freeze, interrupts, dma);
- stores go straight to RAM/scratchpad, bypassing code-page tracking,
overlay watch and write traces; MMIO is an allow-list (GP0, GP1 DMA mode /
info, GPU and OTC DMA, I_STAT/I_MASK) and SPU, CD, timer and other device
stores are dropped and counted (memory.c);
- afterwards CPU (with the GTE), RAM, scratchpad, I-cache tags, interrupt,
timer, DMA and GPU registers are restored exactly (render_pass.c, a GPU
register checkpoint without savestate side effects), with a guest-cycle
watchdog that rolls a runaway pass back;
- on OpenGL the declared display rect (hr colour + mask stencil, raw mirror,
native-wide band, CPU VRAM rows) is backed up, drawn, captured and rolled
back; writes outside it are journaled and restored, or refused.
The presenter keeps two generations of images (the game's own frame plus
one image per pass phase), promotes one when the flip-aware source sees the
display flip to its rect, and shows the newest image at each output
deadline's phase, crossfading across gaps when passes were shed. Presents
that fall due while passes run are made between them. The plan predicts the
output deadlines of the coming frame and sheds passes to fit a host-time
budget learnt from the presenter's idle time (PSX_RENDER_PASS_BUDGET).
PSX_MOD_FRAME_INTERPOLATION_HOLD repeats the newest frame where no pass
image applies, so a pass-driven title never shows anything later than stock.
Default off: nothing runs unless a plugin calls the API, and the plan
refuses in netplay, rollback, rewind, turbo, self-check resimulation, while
suspended (FMV) or without GL flip-aware interpolation. Pass writes are kept
out of frame_fingerprint. Debug: render_pass_stats, render_pass_dump,
PSX_RENDER_PASS_VERIFY=1 (state + VRAM hashes around every pass).
The freeze API lives in the runtime-only psx_cycle_freeze.h. psx_cycles.h is
part of the overlay codegen hash and stays byte-identical, so titles that
never use passes keep their overlay caches and savestates.
Tests: render_pass_plan_test (deadline-exact phases per rate, shedding,
selection), render_pass_freeze_test (10^6 frozen cycles, exact restore,
watchdog), render_pass_guards (sandbox choke points; the codegen-hashed headers stay
free of the pass API).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psx_mod_set_frame_interpolation_source(PSX_MOD_FRAME_SOURCE_FLIP) makes the
OpenGL presenter rotate its blend history only when the guest really flips
(the displayed VRAM origin moves or the displayed rect is redrawn) and spread
one crossfade over the measured flip period. A game that flips every second
VBlank (R4 races) used to blend for one VBlank and hold for the next; it now
blends across its whole frame.
VBLANK stays the default. reset_mod_owned_presentation() puts it back at
every session start (first boot and the lobby rematch), next to the blend
mode, so titles that never call the setter keep today's code path and a
netplay session never inherits it. begin() is begin_phase(0, 1) and stays
bit-identical (test). gl_interp reports the source, the flip period,
captures and duplicate VBlanks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A native-wide surface is g_wide_w*S wide and the driver limit bounds it
(1024 native columns at 16x on a 16384 limit, about 38:9 at a 320-px
display). An uncapped Fit to Window aspect could ask for more; wide_fbo_for
then refused the surface and the frame fell back to a 1x CPU present with
no native-wide margins.
update_adaptive_widescreen now narrows the Fit aspect to the widest the
surface holds (gl_renderer_fit_wide_aspect: psx_gl_max_wide_width at the
context's scale, psx_gl_fit_wide_aspect at the current display width,
gpu_ws_display_width). It changes nothing unless the surface would be
refused. The refusal stays for a fixed aspect on a wide display mode; its
comment now gives the right limit and what the frame falls back to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host built against a recomp-ui without the Internal resolution row
(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION undefined) still seeded
internal_resolution on first boot and applied it after the launcher.
From a fresh start the seed was "native", so a Supersampling pick of 3x
was reset to 1x and "native" was saved; on the netplay soft-return the
sticky preset was re-applied at session_reboot. The legacy row stopped
working.
Both launcher exits now go through two pure helpers in
internal_resolution.h:
- psx_ir_launcher_seed_supersampling: with the row, the factor as before;
without it, the factor itself when no preset is configured (the
historical round trip), else the preset's scale capped at the row's 4x.
- psx_ir_adopt_launcher: with the row, the row's preset as before;
without it, a pick in the Supersampling row drops the preset, and an
untouched row keeps a preset the player cannot see (game.toml or a
hand-edited settings.toml). It also returns what settings.toml saves:
no internal_resolution key unless a preset is kept.
internal_resolution_test drives seed -> launcher -> adopt for both
launcher kinds, first boot and soft-return, against a model of the
legacy row (clamp to 1..4, cycle); keeping the old sticky preset fails
it. gl_internal_scale_guards pins both exits to the helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The environment override was written into g_video_internal_res, the
configured preset. On a netplay soft-return the launcher was seeded from
it and saved it to settings.toml, so one local peer started with
PSX_INTERNAL_RESOLUTION=8k left 8k in the settings.toml the other peer
shares: the case the override exists for.
Keep the override in g_video_internal_res_env. It still wins for the run
(scale, Match display, HiDPI window, video_info), but the launcher only
shows, and settings.toml only saves, the configured preset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A preset id or a line count in PSX_INTERNAL_RESOLUTION overrides every
config layer, like the other validation env overrides (PSX_SCANLINES,
PSX_GEOMETRY_CORRECTION). Two local netplay peers share one settings.toml,
so this is how one runs at 4K beside another at Native. Also say
"measured when the game window opens" for Match display before the window
exists instead of logging a 1x request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a player-facing internal resolution on top of the raised GL ceiling.
A preset is a target height; the runtime renders at the integer scale
S = ceil(target / reference_lines), reference 240 by default: 720p 3x,
1080p 5x (area-resolved to 1080), 1440p 6x, 4K 9x, 5K 12x, 8K 18x. The
backend then clamps S to what it can allocate.
- runtime/include/internal_resolution.h: the preset table, stable ids,
the value encoding shared with recomp-ui (0 unset, 1 native, -1 match
display, N target lines) and the pure resolver.
- config: [video] internal_resolution (a preset id or a number of lines)
and resolution_reference_lines in game.toml; internal_resolution in
settings.toml, written as a stable id beside supersampling = min(S, 4)
for older runtimes. Precedence: game default < player's legacy factor <
player's preset. settings.toml window_width now accepts up to 7680.
- main.cpp: resolve the preset before gpu_init; Match display re-resolves
from the game window's monitor (SDL3 mode height x pixel density) before
the GL context allocates. The launcher gets the vocabulary (with a
synthesized "Nx (L lines)" entry for a legacy factor no preset matches)
behind RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION, on first boot and on the
netplay soft-return.
- The OpenGL game window gets SDL_WINDOW_HIGH_PIXEL_DENSITY (SDL2:
ALLOW_HIGHDPI) only when the player chose more than native, so the
drawable is in pixels on Retina; native keeps the window as it was.
- debug server: video_info (preset, requested and effective scale, GL
limit and clamp, hr surface, window points vs pixels), screenshot_hires
reads the GL hr FBO at full size, screenshot_wide_hires captures the
native-wide band at internal resolution.
- docs: config_schema (keys, precedence), NETPLAY (a per-peer presentation
setting), TCP_COMMANDS (index regenerated), ENHANCEMENTS IR1.
Default-off: with no internal_resolution anywhere the scale, the window
flags and the present path are unchanged.
Tests: internal_resolution_test (presets x reference 240/480 x backend
maxima, legacy mapping round trip), video_enhancement_settings_test (game
and settings keys, stable-id round trip, legacy key still written),
gl_internal_scale_guards (HiDPI only when opted in, launcher fields behind
the capability macro).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8K is 18x at a 240-line reference, and a full-VRAM surface at 18x is
18432 px wide: over the 16384 limit of Apple's GL, Intel and MoltenVK. A
title only needs its displayed frame at 18x, so past the full-VRAM limit
the GL backend now splits the work:
- The authoritative VRAM stays the ordinary hr surface at 1x, i.e. the
native renderer unchanged: pack, CPU readback, render-to-texture and VRAM
copies are exactly the native results (s_hr_scale; s_scale stays the
presented scale gr_scale reports).
- A presentation-only surface at S covers the displayed columns W =
[x0, x1) x all 512 rows (R4: 0..319, 5760x9216 at 18x, 405 MiB). Every
GPU write touching W is mirrored into it: textured and flat batches,
lines (quads), fills, uploads, the depth24 clear, and VRAM copies (hi to
hi when the source is in W, otherwise the 1x source upscaled; both staged
before drawing so overlapping copies read pre-copy pixels). It keeps its
own stencil. A separate blit program carries the window projection, so
the canonical shaders are untouched.
- W grows (64-column aligned) the first time a display rectangle outside it
is presented, seeded by upscaling the 1x content. Present, hold-last,
interpolation capture, screenshot_hires and the native-wide centre read
it; a display it cannot hold presents at 1x.
It engages only when the full-VRAM surface cannot hold the request (driver
limit or budget), sized so a 320-wide window fits both; up to 16x on the
M4 nothing changes. PSX_GL_HIRES_WINDOW=0/1 disables or forces it. The
launcher note and video_info (hr_scale, windowed, hires_window_*, hires_fbo)
report it.
Default-off: at 1x and whenever the full-VRAM surface fits, the paths are
the previous ones (same native-VRAM and present digests on a fixed scene at
1x and 2x).
Tests: gl_scale_invariance_test now also forces the window at 2/3/5/9x and
requires its frame at internal resolution to be byte-identical to the
full-VRAM surface at the same scale (copies inside, into and across the
window edge, fills/uploads across it, mask, all blend modes) with native
VRAM identical to 1x, and runs it at 18x; gl_scale_limits_test covers the
window clamp; guards pin that it only engages past the full-VRAM limit.
Verified in R4 on an M4: the 8K preset renders 5760x4320 in a race (rear-
view mirror present) and 5760x8640 on a 480i menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenGL backend capped supersampling at 4, the software mirror's limit,
although its hr surface lives on the GPU. Raise the compile-time ceiling to
32 (gpu_render.h) and decide the real scale at context init:
- Query GL_MAX_TEXTURE_SIZE, GL_MAX_RENDERBUFFER_SIZE and
GL_MAX_VIEWPORT_DIMS and clamp S so the 1024*S x 512*S surface fits them
and a 2 GiB memory budget (PSX_GL_VRAM_BUDGET_MB overrides it). Apple's
GL on Metal reports 16384, so 16x is the most a full-VRAM surface can be.
The clamp is logged with the limit that applied.
- If the surface still fails to allocate, retry one scale lower inside GL.
Only a 1x failure drops the backend to software, as before.
- Draw lines above 1x as quads one native pixel thick covering both
endpoints. glLineWidth(S) is capped at 1 on core profiles, so every line
was one hr pixel thick (1/9 of native at 9x).
- Present a supersampled source with an area resolve when it is more than
1.25x the output (1200 lines into a 1080 window, 480i menus), and inset the
UV rect by half a texel instead of half a native pixel, which cropped and
zoomed the image by (S-1)/2 texels per edge.
- Size the copy scratch to the largest copy (copies stage at its origin) and
rebuild the mask stencil in tiles, only over the primitive bbox union, so a
large S does not cost another full-size surface.
- Refuse a native-wide surface wider than the driver limit (uncapped Fit at
a high scale) with one log line; the frame presents 4:3.
- main.cpp: the offline and dual-raster clamps no longer cap GL at the
software 4x. The CPU present path presents GL frames at 1x (its readout is
native) and its staging buffer stays sized for at most 4x.
- [video] supersampling accepts 1..32 in game.toml and settings.toml; the
runtime clamps per backend.
Every change is gated on S > 1: at 1x the renderer is byte-identical (same
native VRAM and presented framebuffer digests as the previous source on a
fixed GP0 scene).
Tests: gl_scale_limits_test (clamp arithmetic), gl_internal_scale_guards
(source guards + runner parsing), and gl_scale_invariance_test, a hidden
real-GL run at 1,2,3,5,9x that requires identical guest-visible VRAM at
every scale, one-native-pixel lines at internal resolution, and a 32x
request that stays on GL at the driver's clamp (16x on Apple).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two generic explicit cull-site kinds for native-wide titles whose screen-X
rejects the existing keys cannot express exactly:
- bgez_sites: `bgez SX, keep` in a signed per-vertex left-edge chain
(`bgez x0,keep; ...; bltz xN,reject`). Emitted through psx_ws_cull_bgez
(keep while SX >= -margin). bltz_sites alone still rejects a primitive
whose first vertices sit in the revealed band; with both kinds the chain
rejects exactly when every vertex is left of -margin.
- clip_edge_x_load_sites (+ clip_edge_width, default screen_w_imms[0]):
lh/lhu/lw of a clip-rectangle X bound. psx_ws_clip_edge_x moves a loaded 0
to -margin and a loaded width to width+margin and leaves interior bounds
(mirrors, split-screen halves) unchanged. xclip_load_sites is lw-only and
upper-bound-only, so it cannot express this.
Both are identity at 4:3, emit nothing when empty, and fail main-EXE
generation on an opcode mismatch (overlay variants stay vanilla). They enter
the overlay-cache hash only when non-empty, so every existing title keeps
its hash (pinned in the test against the previous layout).
Interpreter parity: the dirty-RAM interpreter now also honours explicit
bltz_sites, bgez_sites, branch_keep_sites and clip_edge_x_load_sites. Its
explicit site lists are stored sorted (binary-search lookup), the cap rises
from 64 to 256 per key, and an over-long list is logged instead of being
silently truncated.
Shared math lives in runtime/include/ws_cull_edge.h. New ctest
ws_cull_edge_codegen_test covers parsing, hash identity, emitted code,
the opcode guards and the helper math.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lobby rematch is the only in-process second session: an offline
session ends the process, and only a netplay match launched from the
lobby soft-returns (netplay_soft_exit -> psx_request_return_to_lobby).
The rematch path re-enters via goto session_reboot, below the first-boot
session block, so it reset no mod-owned state at all. The reachable leak:
a match with [netplay] local_viewport_aspect = "adaptive" leaves Fit
(g_ws_adaptive_view) on, and an offline rematch then runs adaptive
widescreen. The launcher also round-trips the match's fixed aspect
(16:9/21:9 from the local viewport) through ls.aspect_index.
reset_mod_owned_presentation() runs immediately before activation in the
first-boot session block (absorbing the existing blend reset) and in the
rematch path after its commit or netplay clear. Every call clears Fit, the
world and retained scene predicates, the adaptive backdrop preload, bezel
artwork, the blend mode and native VBlank pacing. Later calls also restore
vsync, frame interpolation and its rate, and automatic FMV skipping to
their first-session values, and clear the 8 MiB RAM request (memory_init()
re-latches it at session_reboot) and the texture-bank resolver and
batching flag. Netplay clears the plan, so no plugin has activated before
a rematch; everything past Fit is defensive. The renderer and fixed aspect
are launcher controls and process-lifetime allocations are left alone
(documented in MOD_PACKAGES.md).
The first-call capture lives in mod_session_baseline.h: its first call
records the pre-activation values (settings.toml, PSX_VSYNC, launcher) and
writes nothing back, so the first session, and every run that never
soft-returns, is unchanged. A static_assert keeps the restore honest: it
is only valid while interpolation and Skip FMVs have no launcher control.
Function-entry hooks need nothing here: the commit or netplay clear
empties the hook table and only activation rebuilds it.
The rematch path also re-applies the Settings aspect clamp (4:3 while
widescreen is mod-owned) before apply_netplay_local_viewport_aspect, so a
netplay rematch still gets its local viewport. The first-boot path keeps
the launcher/lobby aspect as before.
Not changed here: the rematch path still skips activation and the
controller, load and disc-speed resets of the first-boot block, so an
offline rematch with mods enabled applies the plan's patches and runs its
VBlank callbacks without activation.
Tests: mod_session_baseline_test (new, CTest) drives the first-call
capture and later restore with vsync on and off; it fails if the first
call writes anything. test_mod_owned_display_controls.py guards the
reset list, both call sites, reset-before-activation on the rematch path
and the rematch 4:3 re-clamp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Default = largest multiple of 240 lines within 2/3 of the usable height;
an explicit window_width is still honoured.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The index derivation is guarded by !disc_override_path, so an explicit
--disc left selected_disc_index at its default of 1. Every savestate
then carried the _disc1 token whatever was mounted.
Measured on a two-disc set: booting disc 2 with --disc wrote
state_801255AC_disc1_slot08.pst. With this change the same run writes
state_801255AC_disc2_slot08.pst.
This is independent of the disc-change feature. It is in this branch
because the roster registration already computes the mounted index.
The runtime mounts one disc image at startup. No code path changes the
mounted image after that. Some multi-disc games prompt for a disc
change during play and then poll the drive. That poll never succeeds.
The CD-reinsert path reopens the same image path, so it cannot mount
the other disc.
cdrom_disc_select() closes the current image and opens another roster
entry, inside the existing lid window. cdrom_lid_media_ready() returns
0 for the whole window, so the guest cannot read the image until the
window ends. The existing timed close then ends the window.
cdrom_init() is not called. It also resets warm-route, trace and timing
state, which would discard a running session. cdrom_disc_select()
resets the sector buffers, the seek position, and the warm-route
arming. Registered warm routes are configuration and are not reset.
begin_tray_open_cycle() now holds the tray-open code, moved out of
debug_force_cd_reinsert(). Both paths call it.
Add a disc_select debug command. It returns the roster and mounts entry
n when n is given. It also returns the sector count and track count of
the mounted image, which confirms that the reader opened another image.
Tested on Ace Combat 3: Electrosphere (SLPS-02020 / SLPS-02021). Boot
disc 2 with no disc-2 save. The game prompts for disc 1. At that prompt
the CD read count is static at 2955 and frames run at 60.5 Hz, so the
drive is idle. disc_select n=1 mounts disc 1. The game resumes and
plays its first cutscene. Reads restart within two seconds and pass
19971. The read addresses change to disc-1 values. The frame rate does
not drop.
Co-Authored-By: Claude <noreply@anthropic.com>
No setup package ever staged one, so overlay_cache = true alone left every
streamed overlay on the interpreter. rebuild now stages the pinned Python,
tcc, this pin's recompiler, headers and the build's codegen hash beside the
normal and diagnostic products, and writes compiler.txt naming the compiler
the wizard already has; the runtime treats that as a gcc-tier compiler, so a
player with nothing on PATH gets -O2 shards. Verified on the NCII kit: gcc
tier reported and four shards built with no compiler on PATH.
(cherry picked from commit 03865e10f6ee198d58c47c74526df5db46bb35a9)
s_frame_count is already maintained under PSX_NO_DEBUG_TOOLS, but everything
that exposes it — the debug server, the freeze heartbeat, screenshots — is
compiled out of production. A shipped binary could not report its own speed, so
throughput could only be measured on the diagnostic product, which carries
instrumentation of its own and is not what players run.
That gap is why a 30% throughput regression reached a release candidate
unmeasured, and why the first four attempts to attribute it were all made
against the wrong binary.
Adds an opt-in periodic line on stdout, outside every debug guard:
PSX_FRAME_REPORT_MS=2000
psxrecomp: frames=1234 elapsed_ms=20000 fps=61.7 avg_fps=61.7
Unset, it is one branch on a cached int per vblank. Line-oriented on stdout so a
harness can read it without a debug connection.
(cherry picked from commit aac4dbf58331a413b4ac8be6890c7ff499c55ca0)
(cherry picked from commit c1fbb313d15fe00256bc68a805ba0df255feb249)
launcher_warning/launcher_info only ever raised a dialog under _WIN32, so on
macOS and Linux a first run with no BIOS printed to a terminal nobody was
looking at and quit. SDL3 is already linked; SDL_ShowSimpleMessageBox covers
all three platforms and keeps the headless guard.
(cherry picked from commit 22f4322eb33d46946f9b953f943bedf070eb75a6)
When `PSX_NETPLAY` is turned off, the build fails because `ae_np_account_sync` is referenced without being declared in that configuration. This issue caused automated continuous integration pipelines and local builds to fail unless netplay was explicitly forced on.
Wrapping this function call ensures that the runtime compiles cleanly for projects that do not require netplay support, restoring the ability to build with netplay disabled.
The toggle lives in the Create Lobby modal, before any room exists, and
its value goes out with `create`. The setter refused unless the client
was already seated in a server lobby, so the modal read the unchanged
preference back every frame and the checkbox snapped off. Refuse only in
a LAN / Direct-IP room, which has no gallery to allow -- the SNES host's
rule.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPXfGGhBBBxQPcEzaMsauF
Brings the PSX runtime level with what recomp-ui's netplay pages now ask a
host for, and with the SNES runner that already answers them
(snes_lobby_client.c / snes_host_lobby.c):
- Chat lines, seat rows and players-online rows carry the server's opaque
account key, so the launcher's ignore/block list survives a rename.
- psx_lobby_set_blocks pushes that list to the server as `set_blocks`, since
refusing to pair or seat the two together is the server's job; recomp-ui
re-sends it on every reconnect.
- list_scope_set: a LAN player no longer sees online rooms they have no
connection for, and an online player no longer sees their own LAN rows.
- Automatch: rulesets discovery, the relay latency probe (14-byte LE frame,
type 200 -> 201, nonce-matched), queue with the disc fingerprint, the
accept gate with a locally counted deadline, requeue/cancel/cooldown, and
claiming automatch's `error` codes only while a ticket is in flight.
Leaving an automatch room after the match: it is the server's room, has
no host to rematch with, and holding the seat blocks the next ticket.
- Account: init the auth client once per URL rather than every pump (the
login worker reads the host), anchor the device key to the executable
directory, and publish the signed-in handle as the lobby display name.
mods_enabled is asserted false on PSX: every netplay launch clears the mod
plan or refuses to start, and the caps a match runs are the ruleset's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPXfGGhBBBxQPcEzaMsauF
Focused port of the fork's fix/setup-host-bios-deadlock feature (10 commits) onto current upstream master, per the 2026-09-10 decision to port rather than rebase (the fork line and upstream share no merge base).
Content: the codegen host accepts any recompiled BIOS stem instead of only OpenBIOS/SCPH1001 (psx_bios_known_images.h); the runtime checks the setup host before validating a chosen BIOS and the first-run loop breaker reports a cause-specific reason; the CLI decodes subprocess output as UTF-8; --setup-selfcheck verifies a headless setup; the rbengine preflight names the recursive init. Two port conflicts resolved: the new CTest registrations in recompiler/CMakeLists.txt are kept (the names exist only in this change), and runtime.cmake takes the new recomp-ui guidance text. Cubic P3 finding fixed: the sibling PSX_NETPLAY FATAL_ERROR now also names --recursive.
Evidence: python syntax checks pass for psxrecomp_cli.py and the new tests; runtime/tests/test_codegen_host_bios_stems.py runs or skips only on a missing RECOMP_UI_ROOT. No full runtime build or retail route is claimed here. Developed with AI assistance (Cline, 2026-09-10).