mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 10:38:46 -04:00
975f6646ef89566bcd89ccc8ff1c0ab8d89bf64c
1145 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
975f6646ef |
host_keymap: a present-but-empty / "None" [KeyMap] line is an explicit unbind
host_keymap_load zeroed an action's binds when its line was present and then apply_defaults() put the built-in key straight back, so "Turbo = None" (or an empty value) silently re-armed Tab. Track explicit_unbound per action: set when the line is present but yields no binds, and honoured by apply_defaults through want_default(). A missing line still receives the default, so existing config.ini files are unaffected. Pairs with the launcher's Backspace-to-clear (recomp-ui feat/additional-ui-functionality), which writes "None" for that reason. README documents the rule; test_rewind_toggle_combo.py guards it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
3b8b98fa29 |
runtime: fast-forward toggle shortcut + document the implicit Select chord
Turbo (Tab / [hotkeys] fast_forward_pad) is hold-to-run only. Add a
press-to-latch twin that feeds the same manual fast-forward block:
- host_keymap: HOST_KEYMAP_TURBO_TOGGLE, [KeyMap] TurboToggle, default F9.
- main.cpp: g_manual_turbo_latched flipped by the key edge or by a fourth
host shortcut, PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE ("Fast-forward
toggle", [hotkeys] fast_forward_toggle_pad, unbound by default), polled
next to Rewind / Save states through hotkey_pad_binding_down(). OSD shows
"Fast forward: Nx (locked)" / "Fast forward: off". Threaded through every
settings hop (settings.toml -> globals -> launcher seed -> LauncherSettings
-> back), mirroring fast_forward_pad.
- config_loader: [hotkeys] fast_forward_toggle_pad read/write.
- README: describe the toggle and state that a single-button / trigger host
shortcut is an implicit chord with Select (hotkey_pad_binding_down), which
the launcher now labels as "select + ..." (recomp-ui companion change).
- test_rewind_toggle_combo.py guards the new sites.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
471ee0d34b |
dma: expose GPU linked-list observability
Add passive DMA2 ordering-table counters to dma_state so long or aborted linked-list walks are visible without relying on screenshots. The counters track starts, duplicate starts dropped while a walk is active, completions, cancels, last/max walk size and cycle cost, CHCR poll counts, the initiator PC, and the last eight cancel records. This intentionally does not port PR #302's superseded walker changes or the PSX_GPU_LL_SYNC diagnostic lever; PR #304 remains the timing implementation. Co-authored-by: Kevin Kline <9866117+kerokline@users.noreply.github.com> |
||
|
|
240cff542e |
Merge remote-tracking branch 'origin/pr/312' into integrate/303-312
# Conflicts: # tools/compile_overlays.py |
||
|
|
254a14f3d7 |
Merge remote-tracking branch 'origin/pr/303' into integrate/303-312
# Conflicts: # tools/compile_overlays.py |
||
|
|
d1f776d497 |
Merge pull request #313 from kerokline/fix/gpu-polyline-terminator
gpu: polyline terminator never in the first two vertices |
||
|
|
1d414fc7b1 |
Merge pull request #314 from mstan/chore/repo-cleanup
chore: repo hygiene — purge dead scratch tooling, relocate root docs/fixtures/BIOS profiles |
||
|
|
71f3c8c360 |
Merge pull request #304 from Alexbeav/pr/gpu-dma2-correctness-ff7
dma: preserve live linked-list reads with word-cost timing |
||
|
|
772a3a956b | Merge remote-tracking branch 'origin/master' into pr293-vulkan-stage-idempotent | ||
|
|
bcd048f184 | CMake: make Vulkan header staging idempotent | ||
|
|
402cada6c4 |
gpu: polyline terminator only at vertex-unit boundaries, never in the first two vertices
The GP0 polyline parser tested every streamed word against the 0x55555555 terminator mask ((w & 0xF000F000) == 0x50005000). Hardware, per both oracles (Beetle mednafen/psx/gpu.c INCMD_PLINE, DuckStation HandleRenderPolyLineCommand / DrawingPolyLine), consumes the first two vertices unconditionally and afterwards tests only the first word of each vertex unit: the vertex word for mono polylines, the colour word for shaded ones. Shaded vertex words are never tested. Psy-Q leaves the top byte of LINE_G* colour words as junk, so a legitimate colour such as 0x52545454 matched the mask at the second vertex and ended the polyline early. The remaining words were re-parsed as new commands and a later colour word, 0x02010101, executed as a 341x341 FILL. In Breath of Fire III (SLPS-00990) the item-use effect's shaded polylines hit this every time: the fill covers the terrain texture page at VRAM (448,256) and the whole ground vanishes for the rest of the area (terrain renders as CLUT entry 0, i.e. transparent). The vertex count is folded into the existing polyline_has_prev int (mono: 0..2 vertices seen; shaded: 0=need V0, 1=need C1, 2=need Vn, 3=need Cn with n>=2) so the GPU savestate section keeps its size. Verified on the title: loading the reproducing savestate on the old build zeroed the page 35 frames in; on this build it stays intact over 6000+ frames and the terrain renders. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
e6ce5006b4 |
docs: move root and stale docs into docs/ and docs/internal/
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>
|
||
|
|
f55c731c5f |
chore: remove dead scratch tooling and stale root files
Removes 204 tools/ scripts (203 .py/.sh plus tools/_missing_funcs.json) that
no tracked file references. Verified mechanically, not by eye: for each of the
204 stems, a word-boundary grep across every tracked text file in the repo,
excluding files that are themselves being deleted, returns zero hits. So
nothing kept imports, execs, or documents any of them. Most are the
underscore-prefixed one-shot probes the .gitignore convention already treats
as scratch (tools/_*.py is ignored; these predate that rule and were force-
added), plus a long tail of session-specific dump/decode/poll scripts with
hardcoded local paths and addresses.
Also removed at the root:
* card2.mcd.bak.before_beetle_copy -- a stray backup of a memcard.
* .mcp.json -- local MCP server wiring, not repo configuration.
* SCPH1001.toml -- a root duplicate of bios/SCPH1001.toml, still on the
pre-refactor [[recompiler.bios_vectors]] schema while the bios/ copy has
moved to [recompiler.address_model]. Every reference in the tree already
names bios/SCPH1001.toml, so the root copy was a stale trap: editing it
would have had no effect on any build.
* docs/CYCLE_TIMING_ARCH.md -- superseded by
docs/internal/FAITHFUL_TIMING_PLAN.md, CYCLE_MODEL_BEETLE.md and
PRECISE_IRQ_SLICE.md; nothing linked to it.
Memcard fixtures move out of the repo root into
runtime/tests/fixtures/memcards/ with a README recording what each of the
four images actually contains (directory entries read out of the images, not
guessed). Nothing in the build read them from a fixed path -- the four
hardcoded consumers (tools/check_card.py, inspect_card.py, peek_cards.py,
verify_card_buffer.py) are all in the deletion above.
.gitignore is corrected to match: runtime memcards a session creates stay
ignored (*.mcr anywhere, /*.mcd at the root, which is where the runtime drops
card1.mcd/card2.mcd when no path is configured), while the tracked fixtures
are re-included. The old !dummy.*.mcr negation existed only to keep the two
root oracle cards tracked and is replaced by
!runtime/tests/fixtures/memcards/*. Verified with git check-ignore --no-index
on each moved file, on the pre-existing runtime/tests/test_card_synth.mcd and
runtime/tests/test_dir/card1.mcd fixtures (all not-ignored), and on
hypothetical root card1.mcd / card2.mcd / saves/card1.mcr (all ignored).
beetle_libretro.cpp's warn-only memcard sanity check keeps its behaviour; its
WARNING now names where populated fixtures can be copied from. The cards are
opened relative to the process's working directory because the libretro
GET_SAVE_DIRECTORY environment callback answers "." (same file, line 419), so
the message says to copy them into the CWD rather than implying a repo path.
BIOS profiles SCPH101.toml and SCPH5552.toml move from the root into bios/,
joining OpenBIOS.toml and SCPH1001.toml so that bios/*.toml is the whole set.
Behaviour-preserving: config paths inside a profile resolve against
find_project_root() (config_loader.cpp:821), which walks up from the profile's
directory for .gitignore/.git/CMakeLists.txt -- bios/ has none of those, so it
lands on the same repo root as before, and bios/SCPH1001.toml already relied
on exactly that. tools/build_cli.py's staging map is updated to the new
sources; it already copied them to framework/bios/, so the packaged layout is
unchanged. recompiler/src/main_cli.cpp's bios/-first lookup keeps its root
fallback (harmless, and it is what makes older layouts still resolve).
Fixes a real latent break while in bios/SCPH5552.toml: seeds pointed at
"seeds/phase2_ghidra_seeds_SCPH5552.json", but there is no seeds/ directory at
the repo root -- the file lives at
recompiler/seeds/phase2_ghidra_seeds_SCPH5552.json, which is what SCPH101.toml
already named. Porting these two profiles to the address_model schema is
deliberately NOT done here; it is a separate tracked follow-up.
Test files that no build system knew about:
* tools/test_coverage_vault.py PASSES (8/8). Moved to
recompiler/tests/test_coverage_vault.py and registered as the ctest
coverage_vault, which puts it inside runtime/check_test_registration.cmake's
glob so it can never silently rot again. Its ROOT is adjusted from
parents[1] to parents[2] for the new depth, matching every other python
test in that directory. Re-run from the new location and from an unrelated
CWD: 8/8. The registration guard was run standalone before and after, and
with the add_test() deliberately broken as a negative control -- it does
name recompiler/tests/test_coverage_vault.py as an orphan when
unregistered, so the registration is load-bearing rather than decorative.
* tools/test_compile_overlays_additive.py FAILS, 10 of 14 tests erroring
against an API that has moved on, so it is removed rather than wired up.
It calls four module attributes that no longer exist in
tools/compile_overlays.py -- load_additive_captures,
validated_cached_bundle_ids, cached_bundle_pairs, compile_and_publish_dll
(zero occurrences each) -- and two signatures have changed under it:
parse_overlay_func_ids() no longer takes require_stored_crc, and
load_region_coverage() now takes 2-3 positional arguments where the test
passes 5. Reviving it means rewriting it against the current additive
capture API, which is a change to overlay-cache test coverage and needs its
own commit and its own evidence, not a rename.
Verification: py_compile over all 113 remaining tracked tools/*.py plus
psxrecomp_cli.py passes; bash -n over every remaining tracked tools/*.sh
passes; importing sdk_progress, pad_delivery and cursor_writers from tools/
succeeds.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
d08d84a3df |
Merge pull request #296 from kerokline/perf/static-overlay-parallel
overlays: parallel static compile, linear resume-wrapper pass, split output |
||
|
|
533816ecfb |
Merge pull request #307 from kerokline/feat/fast-forward-pad
runtime: controller host shortcut for fast-forward ([hotkeys] fast_forward_pad) |
||
|
|
cd55e8a454 | feat(cdrom): deliver CDIRQ_DATA_END on Mode 2 Form 2 XA audio EOF and reset stream on SeekL | ||
|
|
a7459cddbe | Make packaged overlay toolchain self-contained (#311) | ||
|
|
6cb7a6b8c2 |
Stage overlay dispatch preamble with toolchain (#310)
Co-authored-by: Codex <codex@openai.com> |
||
|
|
a923d21a23 |
Merge pull request #298 from mstan/fix/lock-mode-launcher-reclamp
controller: re-clamp lock_mode across the launcher round-trip (defense-in-depth; root cause in recomp-ui) |
||
|
|
37b32bdd0c |
release: refuse to derive a release tag from an unbuilt runtime include tree
compile_overlays.codegen_hash() returns 0 when runtime/include/
overlay_codegen_hash.h is absent. That fallback is correct for its purpose and
should stay: overlay_api.h has the same __has_include fallback, so a runtime
built without the header and a compiler run without the header agree on
cg<N>_00000000_..., and the cache still loads.
It is never correct for a RELEASE. The header is generated by the runtime
BUILD, in runtime.cmake's "Hashing recompiler codegen" step, so a packager that
reads a tree which has not been built yet silently produces a tag with a zeroed
codegen hash: a plausible-looking string naming a namespace the shipped binary
does not read.
MEASURED 2026-09-02 while building the Tomba 2 Linux cache. Deriving the tag
before the runtime build produced
cg10_00000000_gc530dff78_f0
where the correct tag for that same tree, after the build, is
cg10_ecd487f7_gc530dff78_f0
Note what it looks like from the outside: a well-formed tag, the right number
of fields, the right config hash, the right flavor. A packager would happily
stage a whole cache under it and report a shard count. The runtime would ignore
every one of them. Nothing in a build, a configure, a link or a log would say
so -- which is the exact failure shape this bead exists to remove, one field
over from the field that started it.
So `release_stage.py cg-tag` now fails when the codegen hash reads as 0, and
says which header is missing and that the runtime target has to be built first
(the same build that publishes the flavor file the tag also needs). There is no
override: for a release the condition has no correct outcome.
test_release_stage_cache.py gains CodegenHashGuardTest -- refused on an unbuilt
tree, accepted on a built one. 18 cases, all passing.
Bead: beads-eio.3.102
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d655e47168 |
release: never stage a producer-internal cache file, even one ending in .so
Found by looking at a REAL cache tree instead of a synthetic one. A live Tomba 2
Linux cache build, sampled mid-run 2026-09-02, holds this beside its 184
finished shards:
184 *.so.pair-lock producer concurrency locks
185 *.c generated C the shard was compiled from
1 .overlay-candidate-capacity.lock
1 .overlay-candidate-capacity.lock.invocation
2 .00096000_407B1780.so.tmp.ryt4jqca.so
.00096000_407B1780.so.tmp.ryt4jqca.ranges
The last pair is the problem. compile_overlays writes each shard to a hidden
temp name and renames it into place, so a cache directory that is being written
-- or that was written by an INTERRUPTED run and still holds leftovers --
contains files that end in .so and .ranges and are not shards.
A suffix allowlist alone therefore ships them. So did the shell packagers'
`find ... -name '*.so' -o -name '*.ranges'`, which is a latent defect in all
three forks that nobody had hit yet. It is not loud when it happens: an extra
dlopen-able file with a hidden name lands in the release, the loader tries it,
and the outcome depends on how far the interrupted compile got.
FIX: the forbidden-artifact denylist is checked BEFORE the suffix allowlist, and
gains two rules -- a leading dot, and `.tmp.` anywhere in the name. Every
producer-internal file observed in a real cache is hidden and no shippable
artifact ever is, so the leading dot is the reliable discriminator; the `.tmp.`
rule is belt and braces for a future producer that stops hiding them.
Exclusions are now reported as a grouped tally rather than one line each. A real
cache holds one .pair-lock and one .c PER SHARD, so per-file notes would bury
the shard count under ~370 lines in every release log -- the same "the important
line scrolled past" failure this bead is about, one level down.
test_release_stage_cache.py gains test_real_producer_artifacts_never_ship, which
models that exact file set verbatim, including the temp pair, and asserts the
staged directory contains exactly the 11 shippable files and nothing hidden.
16 cases, all passing.
Also corrects the ApeEscapeRecomp line count in the header comment: 416 on
origin/master, not 403. 403 is what its parked working tree reports, and reading
a working tree instead of origin/master is the mistake that produced the wrong
first version of this bead.
Bead: beads-eio.3.102
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
94e3ef610d |
release: ONE staging implementation for both platforms (2 titles had no Linux release path)
tools/release_overlay_stage.ps1 exists because a packager that re-derived the
overlay cache tag in PowerShell went stale the moment the framework appended an
_f<flavor> field to it: the packager's filter matched nothing, a perfectly
regenerated cache staged ZERO shards, and the release shipped with no native
overlays (beads-eio.2.6). The fix was correct -- expose cache_tag() and route
the packager through it -- and it fixed Windows only.
Linux was never consolidated at all. Three title repositories each carry a
forked tools/package_appimage.sh, and each hand-builds the same tag string,
inside a python heredoc that has just imported the module that owns it:
spec = importlib.util.spec_from_file_location('co', mod_path)
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
print(<the tag format, reproduced by hand> % (m.codegen_ver(inc),
m.codegen_hash(inc), m.overlay_config_hash(exe, gt)))
MEASURED 2026-09-02, reading each repository's origin/master (not a working
tree -- the Ape checkout is parked on codex/ape-issue3-diagnostic-prerelease,
behind master, and reading it gives the wrong answer, including a wrong line
count):
repo lines calls cache_tag() composes _f stages toolchain
ApeEscapeRecomp 416 no yes no
Tomba2Recomp 436 no NO no
MegaManX6Recomp 408 no NO no
ApeEscapeRecomp 4a17272 (2026-08-28, "release: stage flavored Linux overlay
cache") hand-appended the field to ONE of the three forks. It did not adopt
cache_tag(); it wrote cache_tag="${cg_tag}_f${overlay_flavor}" and switched the
filters to it. So Ape is correct today and one character from the same failure
on the next tag-shape change, and the other two are broken right now:
find -path "*/$cg_tag/*" requires a separator immediately after the tag, the
real directory is named ..._f0, so the pattern cannot match. shards evaluates
to 0 and the packager exits 1.
That is not hypothetical. Tomba 2 v0.0.9, released 2026-09-02, has exactly two
assets: Tomba2Recomp-v0.0.9-windows-x64.zip and its .sha256. There is no
AppImage in that release at all, which is what a packager that exits 1 looks
like from the outside.
Two implementations, one per platform, have to be kept in step by review. This
bug is the proof that review does not hold: the identical defect had ALREADY
been fixed on the other platform, and the reason cache_tag() exists is that
fix. So there is now ONE implementation.
tools/release_stage.py the implementation (new)
tools/release_overlay_stage.ps1 thin wrapper, surface UNCHANGED
tools/release_overlay_stage.sh thin wrapper, new (Linux had none)
Both wrappers are argument marshalling. Neither contains a tag format string, a
shard filter, an extension list, an arch-abi segment or a catalog rule. Every
sh function forwards "$@" verbatim, so a new option needs no edit there at all.
No title's package_release.ps1 needs a change: every parameter, switch and
return value of Get-OverlayCgTag / Add-OverlayCache / Add-OverlayToolchain /
Add-ModCatalog / Get-PinnedArchive is preserved, verified by dot-sourcing the
module in PowerShell 5.1 and exercising all five.
WHAT THE SHARED IMPLEMENTATION DOES DIFFERENTLY
* The tag is cache_tag(). Not "a call that produces the same string" -- the
call. release_stage.py does not know the tag's shape and must never learn it.
It also takes the arch-abi and the shared-library suffix from
compile_overlays.cache_arch_abi()/overlay_ext() rather than naming them, so
".so under linux-x64" and ".dll under win-x64" are one decision, made in the
module the loader is kept in lockstep with, not two.
* Shards are selected by the tag DIRECTORY'S IDENTITY, not by a path substring.
-path "*/<tag>/*" is simultaneously too loose (it accepts a foreign arch-abi)
and too strict (it rejected the real _f0 directory and staged nothing). The
staged layout is then asserted -- exactly one tag directory, and it must be
this build's -- because the loader scans cache/<id>/<tier>/<arch-abi>/<tag>/
literally, and a shard anywhere else is worth exactly as much as no shard,
which no count can distinguish.
* Selection is an ALLOWLIST of {<ext>, .ranges, .resident}, so a new artifact
kind in a producer's cache is excluded by default rather than shipped by
default, and the staged tree is re-scanned afterwards to prove that
.abi_<tag>.ok, *.c and *.pair-lock did not ship. The sweep memo is the one
that matters: it tells a player's first launch that an ABI sweep it never ran
already passed, so a stale shard gets loaded instead of rejected.
* --allow-no-cache is gone from the Linux surface entirely. It is the
relaxation that let a packager staging nothing read as a successful build.
What remains is --ship-without-overlay-cache-because '<reason>', which is
unwieldy on purpose, prints the reason, and is not the documented path in any
recipe. PowerShell's -AllowNoCache is kept (unmigrated title packagers still
pass it) and maps onto that flag, so even the escape hatch is one
implementation.
* The mod catalog is verified against the manifest the BUILD published.
runtime.cmake's _psxrt_stage_mod_catalog already writes
psx_mod_catalog_<target>.txt -- the sorted, deduplicated id list that build
staged -- which is a strictly better authority than re-globbing source trees,
and it handles Tomba 2's Italian catalog overriding framework psx.* packages
at the same ids without a per-variant number.
LINUX GETS AN OVERLAY TOOLCHAIN, AND THE RUNTIME CAN NOW SEE IT
No AppImage packager has ever staged one (grep -c overlay_toolchain = 0 in all
three forks), so on Linux the shipped AOT cache was all a player ever got: the
capture-and-compile fail-safe could not fire, and an overlay outside the cache
was interpreted forever.
Staging one alone would not have worked. runtime/src/main.cpp gated autocompile
on overlay_toolchain/python/python.exe, and built the command from
psxrecomp-game.exe and tcc/tcc.exe -- hardcoded Windows spellings, so on Linux
the gate could never be true no matter what a packager staged. A bundled Linux
toolchain would have been dead weight the runtime never looked at. Those three
names are now per-platform; the layout is identical, only the spellings differ,
and they are kept in lockstep with release_stage.py's TOOLCHAIN_PY_REL /
TOOLCHAIN_RECOMPILER.
The Linux interpreter pin is a python-build-standalone CPython 3.13.1 --
deliberately the same version as the Windows embeddable-zip pin -- fetched and
SHA256-verified on every use by the same code path as the Windows one, so the
platforms cannot drift on retry, cache or verification behaviour either. The
_stripped asset (21 MB download, 78 MB unpacked) is used; compile_overlays.py
needs only the stdlib, verified by import. tinycc publishes no prebuilt Linux
binary, so there is no pinned tcc there and the bundle drives a gcc from PATH;
main.cpp already says so on stdout rather than silently doing nothing. The
SHA256 was measured by downloading the asset and matching the publisher's own
.sha256 sidecar, not copied from a listing.
THE FLAVOR IS NO LONGER GUESSED
PSX_OVERLAY_FLAVOR is the high half of overlay_abi() and the _f<n> field of the
tag. It is a property of the BINARY, decided in runtime.cmake (0 base, 2 for a
PGXP target), and it is NOT platform-dependent -- Windows and Linux builds of
the same target share it, so nobody should ever add a platform branch for it.
Every packager simply assumed 0, and that assumption is invisible when wrong: a
PGXP runtime reads ..._f2/ while the packager stages ..._f0/, the package ships
a cache the binary ignores completely, and nothing fails.
runtime.cmake now publishes psxrecomp_overlay_flavor-<target>.txt next to the
exe-name publication it already writes, for the same stated reason: so nothing
downstream re-derives it. release_stage.py cg-tag takes either --flavor or
--flavor-from-build, and there is no default -- a packager either states the
value or reads what the build published.
TESTS
runtime/tests/test_packagers_never_format_cache_tag.py asserts the rule the two
fixes above were supposed to establish: the tag format appears only in
tools/compile_overlays.py and runtime/src/overlay_loader.c (measured: 5
occurrences, all inside those two files -- note overlay_loader.c has four, at
1745, 1773, 1924 and 1930, the last two being the actual read path) and nowhere
else, and a release packager that touches the overlay cache must reference the
shared staging surface. It carries a positive control -- each owner must
actually contain the pattern -- because the investigation of this very bug once
reported "no cache present" from a probe that could not have found a cache at
all. It is also the SHARED check a title runs against its own tree
(--root <repo>), so the rule about not duplicating logic is not itself
duplicated per repository.
Run against Tomba2Recomp origin/master it reports, pre-fix:
tools\package_appimage.sh:292 formats the overlay cache tag itself
tools\package_appimage.sh is a release packager but does not reference the
framework shared staging surface
runtime/tests/test_release_stage_cache.py pins the behaviour, hermetically, on
synthetic cache trees -- no recompiler, no captures, no build, which is the
point: this is the assertion that could have run in CI on the day the tag grew
a field. 15 cases, including that a tag one field short selects nothing AND
raises rather than warns, that a foreign tag namespace / arch-abi / the removed
sljit tier are not shipped, that the three forbidden artifact kinds never ship,
and that developer state (mods/installed, mods/state.toml) never ships.
Both are registered in runtime/CMakeLists.txt, so the configure-time
test-registration guard covers them and ctest runs them.
NOT DONE, deliberately: tools/package_release.ps1,
tools/package_release_macos.sh and tools/package_setup_host.sh each carry their
own hand-written mod-catalog staging. That is the same duplication defect one
layer over and they should route through stage-mods too, but it is a separate
change needing separate proof (macOS cannot be built or verified here), so the
new test scopes its packager assertion to packagers that touch the overlay
cache rather than pretending to cover them.
Bead: beads-eio.3.102
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
2ae781095a |
runtime: controller host shortcut for fast-forward ([hotkeys] fast_forward_pad)
Turbo was keyboard-only ([KeyMap] Turbo, Tab). Add a third host shortcut the launcher can bind to a pad button or chord, next to Rewind and Save states: - main.cpp: PSX_ASSIST_BIND_FAST_FORWARD + "Fast-forward" label; default chord select+L1 (PSX_HOTKEY_PAD_SELECT_L1 = 1528); the manual fast-forward block now runs while either the keyboard key or the pad binding is held, through the same hotkey_pad_binding_down() matcher the other two use. Threaded through every settings hop (settings.toml -> globals -> launcher seed -> LauncherSettings -> back), mirroring save_state_menu_pad. - config_loader: [hotkeys] fast_forward_pad read/write, same pad_bind_value_ok bound as the siblings. - test_rewind_toggle_combo.py guards the new sites; README documents the chord. Launcher side: recomp-ui draws every advertised host shortcut instead of the first two (companion PR). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
57648745a3 |
mods: the framework stages the title's catalog too (5 titles could not package)
|
||
|
|
9dfb4174ad | runtime: version GPU DMA snapshot format | ||
|
|
4ce56cbc46 |
cache tag: hash the image view and the walker headers too
codegen_hash_sources.cmake is the canonical list of files folded into the overlay cache tag, and its own header comment records why the list keeps growing: something that changes emitted code but is not listed leaves the tag put, existing DLLs keep loading, and new shards are stamped with the current tag carrying different semantics. The comment names the 2026-07-03 instance (full_function_emitter.cpp changed, only the .h files were listed). The image VIEW was missing, one level below that. ps1_exe_parser.h now decides what the emitter is allowed to see at all: it carries the READ bound (end_address) and the ANALYSIS bound (analysis_end_address, which stops short of an overlay capture's trailing delay-slot guard words), and ps1_exe_parser.cpp decodes the producer's guard-byte declaration. A change confined to either file moves every overlay shard's CFG -- measured in the previous commit as a real change to Tomba 2's 0x8010A000 shard -- while leaving the tag untouched. That is the same stale-but-same-tag hazard, reachable from a one-line edit. Adds ps1_exe_parser.h, ps1_exe_parser.cpp, and the headers of the walker and analysis translation units already on the list (control_flow.h, function_analysis.h, basic_block.h, code_generator.h) for the same reason: their structs and bounds helpers are part of the emitter contract even when the .cpp files do not move. Verified the two independent computations still agree after the change -- runtime/include/overlay_codegen_hash.h says 0x73277fff and psxrecomp-game --codegen-hash reports 73277fff -- so the stale-recompiler guard in compile_overlays.py passes rather than being bypassed. This bumps the tag, which is the intended effect: every existing overlay cache is invalidated and resharded, because the analysis in the previous commit really did change. Bead beads-eio.3.100. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
80ff96780a |
recompiler: the overlay guard word is a delay-slot source, never a block leader
The overlay capture format DELIBERATELY appends one coherent guard instruction
past the end of a dirty-page run (overlay_capture.c write_json_window, `size
+= 4u`, pinned by runtime/tests/test_interpreter_perf_guards.py:47): a MIPS
branch sitting at the run's final word (...FFC) always executes its delay slot
in the next page (...000), and the shard must be able to emit and hash that
word. So a capture whose size is a page multiple PLUS 4 is a CORRECT capture --
N bytes of overlay plus one guard word. Measured: 36 of 36 Ape Escape
(SCUS-94423) vault records and 846 of 946 Tomba 2 (SCUS-94454) records are that
shape.
The recompiler was never taught the asymmetry: the guard word is a legal
DELAY-SLOT SOURCE but an ILLEGAL BLOCK LEADER, because the word after it does
not exist in the image. It had exactly one bound -- end_address() -- used both
to read words and to decide what is code.
MEASURED on Ape Escape, its own single-source vault at framework
v0.3.2-alpha-139-g1bf70960, 36 variants:
PSX_SHARD_RESULT ok=50 failed=17 skipped=32 capacity_fastpath=0
! [recompiler] overlay 0x80136000 crc 4BAB23C9: what(): cannot emit control
flow at 0x80138000: mandatory delay slot 0x80138004 is outside the input
image
+ 16 x [fragment] interior 0x801360.. @region 0x00136000, identical message
One root cause, seventeen shards. The whole region then falls back to the
dirty-RAM interpreter forever, and nothing at runtime says so.
Causal chain, every step read in source:
1. overlay_capture.c:349-361 appends the guard word; region 0x80136000 gets
size 0x2004, so the image is [0x80136000, 0x80138004).
2. compile_overlays.py make_psxexe wraps those bytes verbatim with text size
= len(data), so PS1Executable::end_address() = 0x80138004.
3. function_analysis.cpp in_exe / in_function admit 0x80138000 as ordinary
code, and the walk reaches it by fall-through from 0x80137FFC
(0x24022000, addiu -- not control flow), so func.end_addr becomes the
image end.
4. control_flow.cpp makes the final block end_addr = func.end_addr - 4 =
0x80138000: the block LEADER is the guard word.
5. control_flow.cpp's "Also check last instruction itself" promotion finds
0x80137FFC is not control flow and therefore promotes read_word(end_addr)
-- the guard word 0x1062005A, a beq -- to block.exit_instr, with
has_delay_slot = true and address == end_addr. THIS IS THE DEFECT.
6. code_generator.cpp computes delay_slot_addr = 0x80138004, read_word
returns nullopt, and it throws.
THE THROW AT STEP 6 IS CORRECT AND IS PRESERVED. Emitting a control transfer
without its architectural delay slot silently corrupts guest state, which is
what the code did before that throw was added. The gap is step 5.
Fix: give the image view two distinct bounds.
end_address() READ bound -- every byte the image supplies, so a
mandatory delay slot living at a guard word resolves.
analysis_end_address() ANALYSIS bound -- one past the last byte that may be
discovered, made a block leader, or made a block's
exit instruction.
read_word() keeps the READ bound. Discovery and block construction move to the
ANALYSIS bound: function_analysis.cpp in_exe / in_exe_p3 / add_forced_entry /
every hard_cap that used to be the image end / the jr-table producer_hi
defaults / the static jr-ra and JAL sweeps; control_flow.cpp gains
analysis_walk_hi(func) and uses it for cfg.function_end, alias entries,
add_boundary, the scan loop, the final block's end_addr and in_func;
code_generator.cpp's mid-function split pre-pass; main_psx.cpp seed admission
and the data-scan pointer promotion; analysis_db.cpp, main_toml.cpp,
main_cli.cpp target/seed admission. The clamp is applied only when it leaves a
non-empty walk, so a degenerate function starting at or past the bound keeps
failing closed rather than silently producing an empty CFG.
HOW THE BOUND IS DERIVED -- the producer STATES it; the recompiler is told.
The alternative was for the recompiler to infer the count from `size % 4096 ==
4`. That fits every capture the current writer has ever produced, but it is a
consumer guessing at a producer's intent, and this tree already has the
counter-example: 52 of the 946 SCUS-94454 records have residues from 112 to
3996 bytes (one not even 4-byte aligned) because they come from the STATIC
extractor, which records an exact file extent and appends no guard word. A rule
keyed to the page-run shape is a rule that shifts the moment a producer
changes.
So the count travels with the image, in a magic-tagged field in the otherwise
zero-filled tail of the 2048-byte PS-EXE header (exe_tag in ps1_exe_parser.h:
"PSXRGRD1" at 0x7E0, uint32 count at 0x7E8). Putting it in the header rather
than on the command line means every consumer gets it automatically and no
future invocation path can forget to pass it -- compile_overlays has three
make_psxexe sites and five recompiler spawn sites. An absent or malformed tag
means ZERO guard bytes, which is exactly what every genuine PS-X EXE is: the
BIOS and every retail main executable have no guard word, their analysis bound
equals their read bound, and their analysis is bit-for-bit unchanged.
Producer side:
overlay_capture.c writes "guard_bytes": 4|0 per region -- the writer
stating its own intent, additive to the v2 schema and
preserved through coverage_vault.py compaction.
compile_overlays.py capture_guard_bytes(): prefer that field; for captures
recorded before it existed, reconstruct the single
shape write_json_window can produce (page multiple + 4)
and return 0 for everything else, which is the
fail-closed answer -- an unrecognised guard word is
analysed as code and the step-6 throw still fires
loudly at generation time.
make_psxexe guard_bytes is keyword-only with NO default, so a new
call site is a TypeError, not a silently mis-analysed
shard. The count is threaded through
compile_fragment_batch / compile_interior_fragment /
generate_interior_fragment_static and into the fragment
fail-memo key (bumped to v3, so a memo recorded under
the old analysis cannot suppress a retry under the
corrected one).
SECOND-ORDER DEFECT FIXED IN THE SAME PASS. generate_ranges_manifest extends a
range by +4 for any block whose exit instruction IS its last word, so the
cloned delay slot joins the candidate CRC and the page-generation watch. That
extension was unclamped. Reachable TODAY, independent of the guard word: a
branch-likely opcode (0x14-0x17, RESERVED on the R3000A) as the image's very
last word is short-circuited by translate_basic_block into an inline RI raise
BEFORE any delay slot is read, so generation succeeds and the manifest claims 4
bytes past the image end. Measured on origin/master with a 28-byte synthetic
image: `R 80010000 20` for an image ending at 0x8001001C. The cache would then
validate the shard against bytes the shard never saw. Now clamped to the READ
bound -- not the analysis bound, because a guard word genuinely IS compiled in
when it serves as a delay slot and must participate in the CRC.
DEAD CODE, documented rather than trusted: basic_block.cpp's BasicBlockAnalyzer
bounds its walk correctly but nothing in the tree constructs it. It is still
compiled and still hashed into the codegen tag, so a change there invalidates
caches without moving an emitted byte. Its bounds move to the analysis bound
too -- wiring it up must not reintroduce this defect -- and a header comment now
says where the live walker actually is.
TESTS. Both registered, so ctest runs them and the configure-time
registration guard is satisfied.
recompiler/tests/test_overlay_guard_word_analysis_bound.py drives
psxrecomp-game over five synthetic images:
A tagged, 2 pages + guard word, last real word not control flow, guard word
IS a beq -- the measured Ape shape. Must generate; the guard word must not
be emitted as an instruction; the block must publish 0x80012000 for the
runtime to dispatch; no range may claim it.
B byte-for-byte the same payload with NO tag. Must still fail closed with
the mandatory-delay-slot diagnostic naming 0x80012000. A and B differ only
in the header tag, which proves the tag is what changed behaviour and that
the throw was not weakened.
B2 a misaligned declaration (6). Must be rejected and behave like B.
C tagged, with a real branch at the last analysable word. The guard word
must be emitted AS the delay slot and must be covered by the manifest.
D branch-likely as the final word. Generation must succeed and no range may
exceed the image end.
Against origin/master's psxrecomp-game the same file reports exactly the two
things this commit changes, and passes B, B2 and C:
FAIL: tagged guard word still fails generation: what(): cannot emit control
flow at 0x80012000: mandatory delay slot 0x80012004 is outside the
input image
FAIL: range [0x80010000,0x80010020) claims 4 byte(s) past the image end
0x8001001C
tools/tests/test_overlay_guard_bytes_declaration.py pins the producer half:
field precedence, the reconstruction table, rejection of nonsense
declarations, the tag's byte offsets, that make_psxexe with guard_bytes=0 is
byte-identical to the pre-fix wrapper, and that overlay_capture.c both appends
the guard word and now declares it.
MEASURED OUTCOME, Ape Escape, same vault, same flags, fresh cache:
before PSX_SHARD_RESULT ok=50 failed=17 skipped=32 capacity_fastpath=0
after PSX_SHARD_RESULT ok=31 failed=0 skipped=57 capacity_fastpath=0
ok falls because 19 isolated island fragment shards at 0x00136000 are no longer
needed once the region variant compiles as one shard. Coverage is what matters,
and it is measured from the DLL exports: 349 -> 361 unique func_XXXXXXXX
entries, ZERO lost, 12 gained -- 0x80136140, 0x8013614C, 0x80136154,
0x80136238, 0x801362D4, 0x801362DC, 0x801362F0, 0x80136324, 0x80136370,
0x80136400, 0x80136480, 0x80137950, all from the failure list. The other four
failing PCs were already served by a sibling variant's shard.
INERTNESS.
BIOS (SCPH1001, psxrecomp-bios --config bios/SCPH1001.toml): all four
generated artifacts BYTE-IDENTICAL, verified by sha256 rather than by exit
code, because tools/regen_bios.sh exits 0 on failure.
Tomba 2 main EXE (psxrecomp-game --config game.toml): all nine generated
artifacts BYTE-IDENTICAL, and the console log is identical too.
Tomba 2 overlays, 82 variants over 6 regions from its own vault: identical
result line (ok=98 failed=0 skipped=103) and identical shard identity set,
4901 unique func exports on both sides -- zero gained, zero lost. 97 of 98
range manifests are byte-identical. The one that differs is region
0x8010A000 variant A32DB188, size 0x2004, whose last function's range goes
from 0x218 to 0x214 bytes: its guard word (0x24840008, addiu) had been
emitted as an ordinary instruction and hashed, and now is not. That is
precisely the intended change and nothing else moved.
Bead beads-eio.3.100.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
bc88c06c5f | runtime: restore DMA2 word-cost timing | ||
|
|
99f775c839 | runtime: close GPU DMA review gaps | ||
|
|
f424a8144d | runtime: preserve live GPU DMA payload timing | ||
|
|
85126d8d1b | Revert "dma: charge DMA2 linked-list walks what the validated model charged" | ||
|
|
39bcdf0d56 |
dma: charge DMA2 linked-list walks what the validated model charged
Slicing the GPU linked-list DMA across guest cycles (
|
||
|
|
c5bac02214 |
controller: re-clamp lock_mode across the launcher round-trip
Ape Escape declares `default_mode = "analog"` + `lock_mode = true`
(ApeEscapeRecomp/game.toml:208-209) and shipped with the left stick acting as
camera and the right stick doing nothing -- i.e. the pad booted DIGITAL on a
dual-analog title, and because lock_mode hides the pad-mode selector there was
no UI with which to fix it.
The root cause is in recomp-ui and is fixed in the companion PR (see below).
This is the host side of the same defect: three separate places in main() where
a locked title's declared pad mode could be lost or where a keyboard seat's
configured mode was destroyed on its way past the launcher.
WHAT WENT WRONG, END TO END
---------------------------
1. recomp-ui launcher_model.c honours the lock at line 545-546
(`if (!pad_mode_selectable) pad_mode[p] = locked_pad_mode`) and then
clobbers it 15 lines later with an ungated keyboard coercion. Player 1's
device defaults to "keyboard" in a release build (main.cpp:11164), so
pad_mode[0] came back as 2 (D-Pad) from every fresh install.
2. `launcher_model_set_pad_mode` and the default-for-source helper both
early-return when the mode is not selectable -- correctly, since the mode
is locked -- so once the value was 2 nothing could move it back. Attaching
a real DualShock did not help.
3. Here, the launcher-exit readback took `player_mode[i] = ls.pad_mode[i]`
verbatim and then persisted it into settings.toml, so DIGITAL beat the
lock and kept beating it on every later launch.
4. `sio_set_pad_analog(slot, 0, ...)` then took the 4-byte pad_response_len
branch in runtime/src/sio.c: the pad reports no analog bytes, so the right
stick is dead and the left stick is folded onto the D-pad. Exactly the
reported symptom.
The clamp at main.cpp:11869 already existed and already covered two holes
(launcher-less builds, and a settings.toml written before the game declared
lock_mode). It runs BEFORE the launcher, which is why the launcher round-trip
was a third hole it could not see.
WHAT THIS CHANGES
-----------------
runtime/include/launcher_device.h gains
`resolve_player_mode_after_launcher(launcher_mode, lock_mode, locked_mode,
mod_override_mode)`: one pure function that fixes the precedence of the three
sources of truth for a seat's pad mode -- an active mod controller-mode
override, then game.toml lock_mode, then whatever the launcher returned. Both
launcher-exit readback loops call it, and both call it before their persistence
write, so an unsupported mode can no longer be latched into settings.toml.
The mod-override arm is not decoration. A bare `if (ctrl_lock_mode)
player_mode[i] = ctrl_locked_mode[i];` -- which is the obvious fix and was the
first version of this change -- breaks mod controller-mode overrides on locked
titles after a lobby soft return:
* `g_mod_controller_mode_override` is applied at main.cpp:13084-13085,
* the `session_reboot:` label is BELOW it at 13159,
* `goto session_reboot` (14914) therefore never re-runs the override block,
* but the soft-return readback loop does run.
Before this change an override survived a rematch only because it round-tripped
through `ls.pad_mode[i]`, seeded at the soft-return entry from the post-override
`player_mode[i]`. An unconditional clamp cuts that path and silently drops the
override. `psx_mod_set_controller_mode_override` is public plugin API
(runtime/include/mod_plugins.h:229) with no shipped caller today, so this is
latent rather than live -- fixed anyway, because the next caller would have had
no way to tell why their override stopped applying to a locked title after a
rematch.
Three destructive coercions are also removed, all of the same shape -- a
keyboard seat having its configured mode overwritten with DIGITAL:
* the two launcher-entry seeds (`ls.pad_mode[i] = (src == 1) ? DIGITAL :
player_mode[i]`) told the launcher a lie about what the seat is configured
for, and the launcher handed the lie straight back to be persisted;
* the launcher-exit readback did the same in the other direction.
None of them was load-bearing: `effective_player_mode()` (main.cpp:4359-4362)
already returns DIGITAL for any seat whose kind is keyboard, whatever the
seat's stored mode says, and every consumer of a seat mode goes through it or
through `effective_player_mode_for_sio()` -- `refresh_player_devices`,
`capture_pad_slot`, and the netplay sample path were all checked. So a keyboard
seat still presents as a plain SCPH-1080 while the keyboard is driving it; the
difference is that its stored mode is no longer corrupted for the pad that
arrives later.
Note the unlocked case was never as bad, and this is worth writing down because
it is what made the bug look title-specific: for a selectable title,
`launcher_model_set_source` -> `apply_default_pad_mode_for_source` re-defaults a
gamepad seat to Analog, so picking a device in the launcher repairs the value.
That is precisely the repair a locked title cannot perform, because both of
those functions refuse to touch a locked mode.
TESTS
-----
There was zero coverage for any of this. (ApeEscapeRecomp's own
`ape_stick_response_test` covers the stick RESPONSE CURVE, not mode or axis
routing, and lives in the title repo -- it cannot see this.)
* runtime/tests/test_launcher_pad_mode_resolve.cpp -- behaviour of the
resolver: a locked title yields its declared mode for every launcher value
including out-of-range ones (so persistence can never receive an
unsupported mode), an active override outranks the lock, and the "no
override" sentinel stays negative to match the direct apply site.
Registered as launcher_pad_mode_resolve_test.
* runtime/tests/test_launcher_pad_mode_wiring.py -- how it is WIRED: exactly
two readback sites call the resolver, the resolution precedes each
persistence write, the pre-launcher clamp still runs first, and
`player_mode[i] = PSXRecompV4::PAD_MODE_DIGITAL;` appears nowhere. Verified
to go red when the coercion is re-introduced at either site, not just when
the helper is deleted. Registered as launcher_pad_mode_wiring alongside the
other source-invariant guards in recompiler/CMakeLists.txt.
Both call sites were confirmed to be live code, not preprocessed-out, by
injecting a bad symbol name and reading the compiler errors back: main.cpp:12701
compiles under `-DRECOMP_LAUNCHER` and main.cpp:14665 additionally needs
`PSX_HAS_LOBBY_CLIENT` (i.e. `-DPSX_NETPLAY=ON`), so the second site is only
exercised in a netplay build and was verified in one.
Verified: launcher_pad_mode_resolve_test, launcher_device_roundtrip_test,
host_keymap_test, keyboard_pad_chord_test, sio_card_protocol_test all pass
(sio_dualshock_rumble_test is registered DISABLED); launcher_pad_mode_wiring and
every other python guard that reads main.cpp pass, except two that were already
red before this branch -- runtime_perf_diag_guards (registered DISABLED) and
launcher_vulkan_option, which dies in `Path.read_text()` with no encoding on a
cp1252 console. That last one is a real latent break in 13 guard scripts and
wants its own change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4cc04be3e6 | rebump | ||
|
|
7ab698caa5 |
overlays: parallel static compile, linear resume-wrapper pass, split output
Static mode (--static) was serial end to end and produced one translation unit, so a whole title compiled on one core twice: once in Python, once in gcc. Measured on BoF3 (339 captures, 79,688 identities) in a build-dbg loop: phase "compile all overlay bands" ~12 min, runtime build ~4 min at -O0. Three changes, each independently verified byte-identical: 1. static_capture_job(): the per-capture static work (recompiler subprocess, C audit, range audit, namespacing) is now a module-level, picklable job that returns its part + captured log. --jobs runs it in a ProcessPoolExecutor; results merge in capture order, so static_parts and entry sources are order-identical to the sequential loop. --jobs 1 and --jobs 3 on three bands: diff -r of the outputs is empty. 2. add_cps_resume_cases(): the universal CPS resume-wrapper pass called add_cps_resume_case() once per entry, and each call re-scanned and rebuilt the WHOLE part source. cProfile on three bands: 7,386 entries -> 22,159 re.search calls, 366 s of 424 s total. The batch form locates every signature once, edits each host's segment in isolation, rebuilds once. Same three bands: 418 s -> 5 s, outputs byte-identical (diff -r against the per-entry output). The per-entry function is kept; a unit test asserts the two agree on fresh-switch, existing-switch, missing label and missing host cases. 3. write_static_outputs(): overlays_static.c now holds the dispatcher plus an extern prototype per compiled entry, and every part becomes its own unit overlays_static_NNNN.c (each already carries its #includes and a private namespace). runtime.cmake globs the siblings with CONFIGURE_DEPENDS, so a changed part count needs no reconfigure. Unchanged units are not rewritten (mtime preserved -> not recompiled); stale units are removed. --static-single-file restores the monolithic file. Result, same BoF3 loop on a 12-core box: phase 5a 12 min -> 20 s, runtime build 4 min -> 69 s (358 units in parallel), whole loop ~16 min -> 90 s. Shard summary unchanged (332 ok / 7 benign audit fails), identity count unchanged. Headless boot of the rebuilt runtime: 485,650 static checks, 485,244 hits (99.92%), miss_total 0, 60 emu fps through the LOGO overlay. Note: tools/test_compile_overlays_additive.py already fails on master (it references removed functions: compile_and_publish_dll, load_additive_captures, validated_cached_bundle_ids); untouched here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
7c5b766fd5 |
overlay: ABI v22 game_text_native_ok shim (107 shards failed to link)
The emitter emits a direct psx_game_text_native_ok() stale-static guard at block
entry (code_generator.cpp:115) and before every direct jal (:2402). That function
is DEFINED IN THE GENERATED DISPATCH SHARD (main_psx.cpp:1620), which links into
the executable. An overlay DLL is a separate module and cannot resolve it, and
overlay_dispatch_preamble.c.inc had no shim for it.
Found by bumping Tomba 2 from a 254-commit-old pin to master and regenerating its
shard cache against the identical vault:
before bump : built OK 2799, FAILED 1
after bump : built OK 2233, FAILED 109
107 of the 109 were the same error:
ld.exe: undefined reference to `psx_game_text_native_ok'
collect2.exe: error: ld returned 1 exit status
This is the fifth instance of one class, and overlay_api.h's own version history
records the previous four:
v9 faithful-timing callbacks overlay code failed to link
v10 gte_read_ctrl GTE-heavy overlays failed to LINK without shims,
silently blocking new coverage
v12 rfe_mark_escape without the shim any rfe-containing overlay
fails to LINK
v14 gte_precision_store_word the rfe / gte_read_ctrl class again
Every time the shape is identical: an emitter gains a runtime call, the overlay
ABI does not gain the shim, and the only symptom is shards that quietly fail to
build. The affected code then runs interpreted forever and nothing at runtime
says so. Severity is coverage, not correctness -- but that is exactly the defect
that let Ape Escape ship 100% interpreted without anyone noticing.
Fix follows the v10/v12/v14 pattern exactly:
- overlay_api.h: append game_text_native_ok to OverlayCallbacks, bump
PSX_OVERLAY_ABI_VERSION 21 -> 22, add the version-history note
- overlay_dispatch_preamble.c.inc: shim forwarding to g_cbs
- overlay_loader.c init_callbacks: host populates it from the dispatch shard
The NULL guard deliberately returns 0, not 1. Returning 0 routes the guest
through the dispatcher -- slower, never wrong. Returning 1 would let an overlay
run native code the guard was explicitly trying to reject.
Follow-up worth doing, tracked as beads-eio.3.97: five instances of one class
means the process is the bug, not the symbol. The emitter knows every helper name
it can emit and the preamble enumerates every shim; a test comparing those two
sets would have caught v9, v10, v12, v14 and this one at authoring time instead
of at some future title's link step.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
67fed036b5 |
Vulkan: stage the headers instead of dropping the renderer
When the located Vulkan headers are not reachable from the compiler, the build already detects it and falls back to the software stub. That is correct but costs the renderer on a whole class of toolchain: the usual case is _vk_inc=/usr/include, which CMake refuses to emit into an include list, on a clang packing its own sysroot that never had the host include root on its search path. Such a build is left with no Vulkan even though the headers are sitting right there. Stage a private include dir holding only the Vulkan subtrees and retry before giving up. That directory is not /usr/include, so CMake emits it, and it brings none of the rest of the host include root ahead of the sysroot's own libc headers -- which is what makes it safe where adding _vk_inc itself is not. vk_video/ is staged alongside vulkan/ because vulkan_core.h includes the H.264/H.265 codec headers from it; staging vulkan/ alone still fails to compile. The stub fallback is unchanged and still runs when staging does not help. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016k5TMHeJ2xbzjKdP4H19vY |
||
|
|
1305cefc7a |
netplay: stop ENABLE_NETPLAY_IF_PRESENT from failing silently
The argument cannot turn netplay on by itself, and said nothing when it did not. By the time psxrecomp_add_game_runtime() reads it, runtime.cmake has already been included, and that include both resolved recomp-net and decided whether the netplay TUs compile for real or as stubs. The option(PSX_NETPLAY ... OFF) near the top has also already made the `if(NOT DEFINED PSX_NETPLAY)` below it false, so the assignment it guards never runs on a configured tree. The switch that works is the pre-include block the scaffold emits above the include(). Warn when the flag is passed but netplay is still off, and name that block -- separately reporting the case where recomp-net simply is not checked out, since the fix there is a submodule init rather than a CMake edit. No behaviour change beyond the diagnostic: the flag was already inert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016k5TMHeJ2xbzjKdP4H19vY |
||
|
|
1bf7096032 |
Merge pull request #289 from kerokline/fix/static-overlay-residency-signal
EMI to overlay extract, cached/mapped lookup table, "resident-occupant" memo |
||
|
|
0731be9218 | Merge remote-tracking branch 'origin/master' into HEAD | ||
|
|
1438876250 | runtime: clean scanline branch mechanics | ||
|
|
5b7bacb009 |
Merge pull request #292 from kerokline/perf/spu-sample-event-gate
runtime: cheap SPUCNT gate for the sample-event scheduler (~2x on MMIO-polling screens) |
||
|
|
8eb29d255d |
runtime: cheap SPUCNT gate for the sample-event scheduler
psx_spu_sample_event_service() and psx_spu_sample_event_cycles_to_next() built a full SpuGlobalState snapshot (memset + ~40 register reads + three 24-voice loops) on every device-service call and every deadline recompute, just to test one bit (SPUCNT & 0x40, SPU IRQ9 enable). Under an MMIO-polling guest loop — every PS1 wait loop, loudest on the BoF3 Capcom FMV whose LOGO overlay polls per tiny basic block — the snapshot was ~40% of emu-thread wall time (gdb stack-sampled, clean boot, live FMV). Add spu_ctrl_read() (one array load) and gate on it. Semantics identical: same register, same bit. Measured (BoF3 Capcom FMV, clean boot, headless vblank raises/s): Debug build: 28.5 -> ~52 RelWithDebInfo build: 97.6 uncapped (~1.6x realtime; paced holds 60) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
92e5f4b2e2 |
runtime: wire scanlines through the launcher + settings persistence
Threads Settings.scanlines / scanline_strength_pct from the recomp-ui launcher Display card through the same paths as screen_kind: seed the launcher from the current state, apply the player's choice back to the video globals (pushing it to the GL renderer immediately), and persist it to settings.toml. All launcher struct accesses are gated on RECOMP_LAUNCHER_HAS_SCANLINES so the runtime still builds against an older recomp-ui. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
97f58e19d7 |
runtime: present-time scanline post-process (host display enhancement)
Adds an optional scanline effect to the GL present path: a soft sinusoidal beam pitched to the PS1 display-line grid (one dark line per native scanline), applied in both the present and interpolation shaders and gated by output scale so it never shimmers below ~2x. Orthogonal to the crt_filter colour LUT (the two compose). Controls: [video] scanlines / scanline_strength (game.toml + settings.toml), PSX_SCANLINES / PSX_SCANLINE_STRENGTH env overrides, an F6 hotkey (HOST_KEYMAP_SCANLINES, rebindable via [KeyMap] Scanlines), and a `scanline` TCP command for live A/B. Launcher Display-card wiring follows separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ecc0de16a9 | Merge remote-tracking branch 'upstream/master' into fix/static-overlay-residency-signal | ||
|
|
fbf0427ba6 | Fix display cadence refresh on monitor moves | ||
|
|
9abb83de8f |
Merge PR 283 after PR 282 revert for regression validation
# Conflicts: # recompiler/CMakeLists.txt |
||
|
|
a4721966bc |
Revert "Merge pull request #282 from Alexbeav/pr/cdrom-seek-semantics"
This reverts commit |
||
|
|
be19a02902 |
Merge pull request #285 from Alexbeav/pr/zero-bios-registry
Make zero-BIOS registries portable to MSVC |