CRT Filter, Image Filtering, Enhanced Audio and Rewind become default-off mods (netplay = host_only,
outside the plan fingerprint); Internal resolution gains Auto in place of dynamic resolution.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mods declare quick = true for the Quick Mods card; [launcher] quick_controls opts a title in. Regional identity now hashes portably. Based on michaelm-afk's handoff.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep selector zero stock and seal all choices until the next session restart. Refuse nonzero online activation, scope non-stock save states by frozen choices, and preserve original instruction guards when combined with effective-address adapters. Use overlay ABI34 on current master without the old menu dependency stack.
Extract the audited adapters and card transport onto master without the held selector/menu stack. Keep adapters and codecs tied to mod activation, use overlay ABI 33, retain rejected-load protection on codec registration, and refuse extended-card codecs in netplay before frame zero. Preserve default atomic save publication and guest-visible write failures.
The manifest check on `default` is text-only; a symlink the package ships
could still point outside it. The default now resolves only when it exists
with the resource's kind (folder / file) and its canonical path stays
under the package's canonical root; otherwise the resource is unset and a
required one blocks launch. mod_packages_test covers a cleared or removed
selection restoring the default, a missing default folder, and a default
that is a symlink out of the package.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A [[resource]] may name default = "<path>" inside its package. While the
player has selected nothing, that path resolves (a required resource with a
default no longer blocks launch); a selection always wins. Lets a title ship
an HD texture pack (psx.hd-textures "pack" folder) in its own package.
Unsafe paths and verified/input/shared resources are rejected at scan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve negotiated content and dual-raster pass gates. Retain both fixture assertion sets and HD-off seams. Regenerate both BIOS with actual native emitter. Fix type-only rollback corrections and exclude extra-pad changes from digital-only promote gates.
A [[plugin]] may declare netplay = "local_view": it only changes what one
player sees. A netplay session now keeps the player's own features whose every
contribution is such a plugin (no writes, overlays or derived discs) instead of
clearing them, without saving state.toml. They activate as usual, but their
hooks run only inside psx_mod_render_local_view (psx_mod_local_view_scope) and
their vblank/savestate callbacks not at all. With such a plan the widescreen
cull margin is the stock 0 in the shared game and this peer's own margin only
inside the sandboxed own-view draw; squash is not used. The canonical frame,
snapshots and digests stay identical on every peer whatever each player chose,
so nothing is negotiated, and each keeps their own aspect.
Tests: netplay_view_mods_test (flag, plan filter, parked hooks, no vblank,
state.toml untouched), ws_local_view_margin_test (margin scope), render-pass
sandbox asserts the scope around a local view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Master moved the textured-dot test stubs into gpu_exec_stubs.inc; the PGXP stubs move there too. pgxp_note_rect_bypass stub takes (int). Stamps recomputed.
Many PS1 renderers drop a primitive whose depth index falls past the end
of their ordering table. Raising that limit can write past the table, so
a title can now list such guards in game.toml with the register that
carries the index and the last safe value. While a trusted mod has called
psx_mod_set_draw_distance_clamp(1), generated main-EXE code clamps that
register before the guard runs, so far geometry is kept in the farthest
slot instead of popping in later.
- Off by default and reset at every session start, so netplay and every
title that lists no sites run unchanged; with no sites the generated
code and declarations are byte-identical.
- Main executable only: the full instruction word is the guard,
generation fails on a mismatch or a load-delay hazard, captured
overlay code keeps its own code (the sites are not part of the overlay
config identity), and the dirty-RAM interpreter applies the clamp
where the game's text image holds the listed word.
- Compatibility: code_generator.cpp is a codegen-hash input, so the
codegen hash and the overlay-cache tag change for every title, sites
or not. Savestates from earlier builds are refused, and overlay shards
rebuild once on first visit. Generated code and behaviour are
unchanged for a title without sites or with the switch off.
- Tests: draw_distance_codegen_test (config rules, emission, guards,
declarations, overlay identity) and draw_distance_interp_test (store,
switch, interpreted guards); the session-reset guard covers the switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
G1.11: the hook-flavor runtime, the mod armed at session start, dataflow
only, exact projection, and the rectangle shortcut. It covers what tri_mixed
can and cannot see, and the R4 census after every change:
- 1P: 99.91% dataflow, 0 of 389k drawn triangles mixed;
- menus, attract and 2P VS: every truncation reject is a vertex at the
saturation limit.
The wobble table drops the preserve-projection row, which is 0 by
construction.
G1.12: precise culling (the culled-row crack, the rule, why it is mod-only,
packet headroom, identity, cost under R4's default stack) and
[video] pgxp_mod_only.
TCP_COMMANDS: the new geom_correction fields and the pgxp verb's culling
knob. MOD_PACKAGES: the session arming takes the request; how a title
ships PGXP on. GAME_PROJECT_SETUP: pointer to pgxp_mod_only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psx.enhancement.pgxp did nothing at boot. Its activation armed geometry and
texture correction, then main.cpp's renderer setup, which runs after
activation, applied the [video] baseline and switched both back off. Only the
[video] keys could turn PGXP on.
The activation now records a request (pgxp_mod_request), cleared at every
session start in reset_mod_owned_presentation(). The renderer setup arms PGXP
from the [video] baseline OR the request (pgxp_session.h); the validation env
overrides (PSX_GEOMETRY_CORRECTION, PSX_PERSPECTIVE_TEXTURING,
PSX_PGXP_CPU_MODE) still win. The g_video_* values stay the player's
settings, so the mod never leaks into a launcher seed, and a netplay session,
whose plan is cleared, keeps the [video] keys alone, as before.
No change for a title whose mod is off. pgxp_session_test drives the real
builtin activation and the resolver in main.cpp's order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/SEGMENT_AWARE_CODE.md:
- §5.7 gains "Implemented in PR E": capture's per-segment bitmaps and schema
v3; compile_overlays.py's segment views (including dispatch-less records and
declared demands); the static fragment images; overlay_codegen_config() for
config code sites; the cache layout and the S record; the loader's
per-segment lookups and counters (unmapped segments included) and the
overlay_candidates fields; the full-VA compares; tests.
- §3.4, §5.3 and §5.4 point at it.
- §7.1 describes the overlay acceptance cases and the overlay-segment-shards
guard; the ledger stays at zero gaps.
- §7.2 records the probe's new overlay phase against Beetle (both BIOSes,
cold, warm and interpreter), the LLE boot after E, and R4's KUSEG and KSEG1
kernel shards with segment_alias_interp at 0 on a warm cache.
- §8 E is recorded.
AOT_OVERLAY_PLAN (the manifest contract and capture schema), AOT_SHARDING,
OVERLAY_CACHE_V2, COMPILING_OVERLAYS (the cache layout) and OVERLAY_XREF
(schema v3) follow the design, and MOD_PACKAGES notes that function-entry
hooks reach overlay shards of every segment. FAITHFUL_TIMING_PLAN and
ACCURACY_BURNDOWN get dated entries: overlay segments are closed, the last
masked return compares (the overlay shadow run's) are now exact, mod hooks
reach KUSEG/KSEG1 shards, and axis 5 records the probe's BIOS file read taking
about half Beetle's cycles (found here, not caused here).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The session reset clears g_bezel_path, which only stops the next load.
gl_renderer_shutdown() deleted the context but kept s_bezel_tex, and
present_bezel() draws whenever that name is nonzero, so a lobby rematch
would bind the previous session's texture name in its new context.
Shutdown now drops it through gl_renderer_set_bezel(NULL, 0, 0) while
the context is still current. Only a soft return to the lobby and
process exit shut the renderer down.
The source guard checks the shutdown call and set_bezel's NULL path.
MOD_PACKAGES.md says what each half of the bezel reset does and gives
the exact reset values for the adaptive cap and the native pacing rate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MOD_PACKAGES.md: which mod-owned host state the runtime resets at each
session start and when (every session, or later sessions only), what is
exempt and why (the renderer and fixed aspect are launcher controls;
guest/GPU-DMA memory, texture-packet arenas and banks live for the
process), and what the lobby rematch, the one in-process second session,
does not re-run: activation and the controller, load and disc-speed
resets. Function-entry hooks are described through the existing
plan-gated contract (commit and netplay clear empty the table; only
activation rebuilds it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4cc04be3 ("rebump") renamed the staged mod catalog from <exe>/mods/packages to
<exe>/mods/bundled so that a rebuild could wipe build output without deleting
the player's own installed mods. runtime.cmake followed the rename. The shared
release module followed it (release_overlay_stage.ps1's Add-ModCatalog reads
mods/bundled). docs/MOD_PACKAGES.md already described the destination as "the
framework's mods/builtin/packages PLUS the title's mods/preloaded/packages".
No title CMakeLists followed, because the second half of that sentence was
implemented five times, once per repository, as a hand-written
add_custom_command(TARGET psx-runtime POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${<TITLE>_PRELOADED_MODS}" "$<TARGET_FILE_DIR:psx-runtime>/mods")
in ApeEscapeRecomp, Tomba2Recomp and MegaManX4/5/6Recomp. That names the
destination as a STRING. There is no symbol, no header, no link dependency --
so renaming a staged output directory breaks nothing a configure, compile or
link step can see, and the diffstat of the commit that does it looks cosmetic.
The five titles went on staging into a directory neither the launcher nor any
packager reads, and discovery was deferred to whichever title next ran a
release packager, in a different repository, on a later day.
MEASURED, on real build output. Framework origin/master 22fbbfca plus
Tomba2Recomp origin/master cdf00af, both unmodified, both built:
build-dir mods/
bundled/psx.enhancement.cd-speed <- framework, correct
bundled/psx.enhancement.fast-loading
bundled/psx.enhancement.pgxp
bundled/psx.presentation.bezel
packages/tomba2.debug.debug-menu <- title, stranded
packages/tomba2.enhancement.skip-fmvs
packages/tomba2.enhancement.widescreen
packages/tomba2.experimental.interpolated-frame-rate
and Add-ModCatalog run against that tree, from this commit's parent:
Mod catalog is missing package(s) the sources define:
tomba2.debug.debug-menu, tomba2.enhancement.skip-fmvs,
tomba2.enhancement.widescreen, tomba2.experimental.interpolated-frame-rate.
They exist in the source tree but did not reach ...\mods\bundled, so the
release would ship a Mods page the dev build does not have.
Shipped releases are NOT affected. Tomba 2 v0.0.9 pins 04d9184b, which is an
ancestor of 4cc04be3 and contains zero occurrences of migrate_legacy_root and
bundled_root; its artifact is a self-consistent mods/packages layout. This is a
forward break only.
THE FIX PUTS THE LAYOUT IN ONE PLACE.
psxrecomp_add_runtime_target() gains a one-value keyword PRELOADED_MODS_DIR.
A title declares WHERE its packages live and never HOW they are laid out:
set(APE_PRELOADED_MODS "${CMAKE_CURRENT_SOURCE_DIR}/mods/preloaded")
psxrecomp_add_runtime_target(psx-runtime
...
PRELOADED_MODS_DIR "${APE_PRELOADED_MODS}")
A keyword argument rather than a positional one or a variable read out of the
caller's scope, for three reasons. It is what the other fourteen one-value
arguments already are, so it needs no new convention and reads the same as the
LAUNCHER_BOXART line next to it. A pre-set variable would be invisible at the
call site and would silently do nothing if set after the call -- the same
"fails quietly in the caller's repository" shape as the bug being fixed. And it
is PER TARGET, which a global variable cannot be: Tomba 2 has two runtime
targets needing two different catalogs (mods/preloaded, mods/preloaded_ita).
psxrecomp_v4_add_runtime_target and psxrecomp_add_game_runtime both forward
their remaining arguments, so it reaches through both wrappers unchanged.
_psxrt_stage_mod_catalog() now emits, in one POST_BUILD command sequence per
runtime target: wipe mods/bundled; remove exactly the ids it is about to stage
from any pre-existing mods/packages; copy the framework builtins; copy the
title's packages; copy <dir>/README.md to mods/README.md; verify. Consequences
worth stating:
* mods/README.md previously reached mods/ only as a side effect of copying
the whole mods/preloaded tree. Now that only packages/ is staged it is
copied on purpose, or it would silently disappear. Checked per title: all
five carry mods/preloaded/README.md and none carries a root mods/README.md,
so mods/preloaded/README.md is the only source there ever was.
* a title may still OVERRIDE a framework builtin at the same id and version,
because the framework copy runs first and the title copy overwrites it.
Tomba 2's Italian catalog relies on exactly this, shipping localized psx.*
manifests. The id LIST is deduplicated for the assertion and the reported
count while the copies keep both, or the build claims to stage 11 packages
for a catalog that holds 7.
* COSIM targets stage nothing. They have no launcher and no Mods page, and
they land in the same output directory as the real runtime, so letting one
stage had it wipe and re-stage the runtime's catalog with only the
framework half whenever -DPSX_BUILD_COSIM=ON built it last.
TWO GUARDS, because the failure mode being closed is silence.
Configure time: a project with packages under mods/preloaded/packages that does
not declare PRELOADED_MODS_DIR is a FATAL_ERROR naming the packages and the
argument to add. That is exactly the missed-title state and it is detectable
before any build runs. An empty packages/ is fine (the scaffold creates one),
and PRELOADED_MODS_DIR NONE is the explicit opt-out.
Build time: runtime/psx_check_mod_catalog.cmake fails the build if a declared
package did not reach mods/bundled, or if any package this build stages turned
up under mods/packages -- which after this change can only mean a CMakeLists
still stages its own catalog by hand. It runs as the LAST POST_BUILD step, via
cmake_language(DEFER), because POST_BUILD commands execute in registration
order and the framework block registers BEFORE anything a title adds after its
psxrecomp_add_runtime_target() call. Verified by inspecting build.ninja: the
deferred command lands after a title-registered POST_BUILD, not before it.
The deferred pass takes NO arguments and reads its work list out of global
properties. cmake_language(DEFER CALL <fn> "${var}") does not carry a
function-local variable through -- measured with cmake 4.2.2, the callee
receives an empty argument and an add_custom_command built from it fails at
directory end with "CMakeLists.txt:DEFERRED". Recorded so the next reader does
not re-derive it.
Ids the build does NOT stage are deliberately left alone in mods/packages. On a
self-compiling setup release that directory can hold packages the player
installed under the pre-split layout, and mod_packages.cpp's
migrate_legacy_root() relocates those into mods/installed on the next launch.
Removing only the ids this build stages is also what lets a build directory
made before the split be migrated silently, so the guard's "an id turned up in
mods/packages" report has no false positives.
The same script is registered as the ctest psx_staged_mod_catalog_test in every
project that stages a catalog, with REQUIRE_STAGED=0 so a tree where the
runtime was never built reports a skip rather than a spurious failure. Where
two runtime targets share an output directory it accepts either target's
complete catalog, since after a full build mods/bundled holds whichever linked
last -- which is correct for the packager, as it builds exactly one target per
variant, and each target's own POST_BUILD guard already verified its catalog at
stage time.
TESTS. runtime/tests/test_mod_catalog_layout.py exercises the guard against
on-disk fixtures: a reproduction of the broken state as it actually appeared
(bundled/ holding the four psx.* ids, packages/ holding the four game ids), a
declared package simply absent, a package directory with no manifest.toml, a
FOREIGN legacy package that must NOT fail the build, the require-staged split
between the two callers, and the shared-output-directory sibling case. It also
pins two source invariants in runtime.cmake so the layout cannot drift back
into per-title copies. Registered in runtime/CMakeLists.txt; the
test-registration guard reports 123 test file(s), all registered.
Framework ctest: 59/59 pass, 2 pre-existing DISABLED.
Also updated so the next title does not repeat this: the project scaffold
template now passes PRELOADED_MODS_DIR (it staged no catalog at all before, so
a scaffolded project with mods/preloaded/packages shipped none), and
docs/MOD_PACKAGES.md gains a "How bundled/ gets staged" section written at
titles.
Bead beads-eio.3.101. The five title-side patches must land AFTER this commit:
they reference PRELOADED_MODS_DIR, which does not exist before it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mods under active development had nowhere to live: a package was either in
the catalog and therefore in every release, or not built at all. Widescreen is
the immediate case -- it engages, but it is unvalidated as gameplay and FF7's
pre-rendered field backdrops still cull at the 4:3 bound, so it is not
something to put in front of players yet.
A package now declares its own maturity with channel = "developer". It still
ships with ordinary local builds and local exports, so it can be tested
exactly as a player would see it, and release packaging drops it.
The exclusion defaults from $CI -- on under any CI provider, off locally.
That is deliberate: a per-workflow opt-in is precisely the shape that left 21
of 23 ports shipping no mods at all, so this must not need 23 release
workflows edited to take effect. --exclude-dev-mods / --include-dev-mods and
EXCLUDE_DEV_MODS=0|1 override in either direction, and Studio's
`build export --exclude-dev-mods` reproduces what a release contains.
Applied in all three packagers (setup-host, Windows, macOS). Each prunes the
whole PACKAGE directory and then asserts no developer manifest survived --
including the SOURCE catalog under mods/preloaded, since the setup-host zip
rebuilds on the player's machine and would otherwise re-stage the package the
release just removed.
A manifest is never rewritten during packaging: editing a package while
packaging it would change content meant to be verifiable, which is why the
marker is package-level rather than per-feature.
Verified by packaging: local keeps 5 catalogs, CI=true drops to 4 with no
ff7.enhancements entry anywhere in the zip, and EXCLUDE_DEV_MODS=0 under
CI=true restores it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ