- The verified Disruptor USA executable and BIN/CUE files in
input/; see DISC.md. - Git, Python 3.11+, CMake 3.24+, and a C++20 toolchain.
- Windows x64: Visual Studio with Desktop development with C++, or MSYS2 MinGW-w64. Ninja is recommended.
- Linux x64: GCC 13 or newer and the development libraries SDL builds
against (the
buildworkflow's list). The launcher builds there too. Windows is the platform play is tested on.
The first build needs network access to fetch the pinned PSXRecomp revision and dependencies. OpenBIOS is built locally; no proprietary PlayStation BIOS is required.
Run from an x64 developer PowerShell at the repository root:
powershell -ExecutionPolicy Bypass -File .\build.ps1
.\run.ps1The build verifies the executable, applies the framework overlay, generates and
audits the translated code, builds the runtime, and runs the tests. The runtime
is produced in build/, or build/Release/ for a multi-configuration generator.
Set DISRUPTOR_BUILD_DIRECTORY to build into and run from another folder of the
repository instead. build.sh and run.sh read it too. Set
DISRUPTOR_COMPILER_LAUNCHER to a compiler cache such as ccache and build.sh
compiles through it.
After a first build, tools/regen.ps1 regenerates only the translated code, and
tools/regen.sh does the same on Linux.
Windows builds also produce DisruptorLauncher.exe beside the runtime and
stage its configuration, help, and initial settings there. The launcher draws
with Dear ImGui through SDL3's renderer, Direct3D 11 or SDL's software renderer.
Under MSVC it is compiled with a static
runtime, with an SDL3 build of its own that holds only the window, its events
and dialogs, so setup can open before the game runtime's VC++ redistributable
is installed. There it also links Velopack's C library, which configuring
downloads, to update a copy the updater installed. A MinGW launcher has no
updater. It imports existing disc dumps; physical drive reading is outside the
current scope. Release packaging includes it automatically through
tools/package_release.py and still excludes game data.
A release is a Velopack Setup.exe and a portable ZIP, packed from a Release
configuration whose PSX_GAME_VERSION is the release's version:
python tools/package_release.py --version v0.3.1 --build build --zstd-license <zstd LICENSE>CHANGELOG.mdneeds the release's## v0.3.1section first. It becomes the package's notes, which the launcher shows.- The GitHub release's text is
release/notes/v0.3.1.mdwhen it is written, else that section. The folder is kept out of git. The packager adds the Install section below it. A release page gives its headings no ids, so a link to one of them gets an explicit anchor from the packager. - The .NET SDK runs the pinned Velopack packer, the version of the C library the launcher links.
- The previous release's package is fetched from GitHub when there is one,
and the update then also gets a delta package. Without it, or with
--no-delta, an update downloads the full package. build/releases/<version>/windows/packagesholdsDisruptorRecomp-win-Setup.exeandDisruptorRecomp-win-Portable.zipfor players and the packages the launcher updates from. Setup installs the Visual C++ x64 Redistributable when it is missing, the ZIP does not.--publishcreates the GitHub release with them as a draft, which players do not see yet. It stops when the version already has a release or a draft.--publish, or--signalone, signs the players' files with Sigstore first. A browser opens: sign in with the organisation's Google account,recomp.team@gmail.com. Each gets a.sigstore.jsonbundle and a line of SLSA provenance inDisruptorRecomp-<channel>.intoto.jsonl, and the script stops when a bundle names another account. It signs only a checkout without uncommitted changes.
A release goes in two runs for each system:
-
Pack without
--publish. The build folder must hold aSOURCE_COMMITfile naming the checkout's commit. The packager records every package's digest inpacked.jsonbeside them and prints the next two commands with this build's paths. -
Run the smoke test on the player's file:
python tools/release_smoke.py --version v0.3.1 --platform windows ` --package build/releases/v0.3.1/windows/packages/DisruptorRecomp-win-Portable.zip ` --disc <cue> --capsule <capsule> --stamp build/releases/v0.3.1/windows.smoke.json
It tests a copy of the package, keeps the player's files in another folder as the launcher does and checks every file against
MANIFEST.json. The package's own entry, the portable stub or the AppImage, must start the launcher and report this version. It starts the game from power-on, hidden and muted with detailed logging: the game must draw its first frame, show new pictures from the game in every pulse of the log after the first, and still run after 30 seconds. Without its own package the game stops at the start with a message. On Windows it also replays the capsule, which must be recorded on this build and stay in step. On Linux it runs inside the build's distribution on the AppImage, without a capsule. -
Rehearse the update: close the game, start the installed launcher with
--updates-from build/releases/v0.3.1/windows/packages, update, then check EXIT in the main menu, a mission start and a memory card load. -
Push the commit and wait for the build workflow to pass.
-
Run again with
--publish. It packs nothing: it signs and uploads that packing. It stops unless the packing came from a checkout of this commit without uncommitted changes, its files are unchanged, the smoke test passed on its player's file, and the newest build run on the commit is green.
A Linux release is an AppImage packed from a Linux Release build of the same
commit. It is built on Ubuntu 22.04, so that it runs on glibc 2.35 and newer:
GCC 13 from the ubuntu-toolchain-r/test PPA, Kitware's CMake and Python 3.11,
newer than the ones 22.04 ships. The C++ runtime is linked statically and OpenGL through
libGL.so.1, so glibc and a GL driver are all it asks of a player's system.
From Windows, with that build reachable through WSL:
python tools/package_release.py --platform linux --version v0.3.1 `
--build \\wsl.localhost\<distro>\<checkout>\build --zstd-license <zstd LICENSE>The Linux build folder holds a SOURCE_COMMIT file too, and the script stops
unless it names the checkout's commit. It makes
DisruptorRecomp.AppImage and the packages the launcher updates from in
build/releases/<version>/linux/packages. With --publish they go to the draft the Windows run made, which must
come first and from the same commit. The release text gets the Linux
install lines, and the draft is published as a regular release and the
latest one. A release without Linux files is published by hand with
gh release edit <version> --draft=false --latest.
The launcher and the game find the player's files by one rule,
src/game/settings/user_folder.cpp. A copy the updater installed keeps them in
%LOCALAPPDATA%\DisruptorRecompData, a portable copy of the updater in data
beside its current folder, the AppImage in $XDG_DATA_HOME or
~/.local/share under DisruptorRecompData, and any other copy, a build
folder included, beside the executable. A whole path in PSX_USER_DIR names another folder.
The framework's main.cpp asks psx_game_user_folder for it before anything
else and writes settings, bindings, memory cards, caches, mods and the notes of
a run there. Program files are still found beside the executable. The shipped
keybinds.ini is copied into a new folder of the player's files. No
settings.toml is shipped: a setting the file does not name runs with the
initial value of the shared table in src/game/settings/options.cpp, the one
the launcher shows.
For launcher-only iteration after configuration:
cmake --build build --config Release --target disruptor-launcher disruptor-disc-import-test
ctest --test-dir build -C Release -R disruptor_disc_import --output-on-failureDisruptorLauncher.exe --verify IMAGE checks an image without copying it.
--import IMAGE --root FOLDER performs the normal import without starting the
game; --check --root FOLDER verifies installed data and required build files.
--root names one folder for the program and the player's files alike.
These diagnostic modes return zero only on success and report to redirected
stdout/stderr. The GUI always resolves the build folder from its executable,
so shortcuts work even when opened from a different working directory.
Supported revisions are defined in src/launcher/disc_import.cpp. Future PAL
and Japanese entries must include the full raw-disc size/hash and a matching
game configuration and compiled runtime support. Do not enable a region by
serial alone; the current game hooks and translated code are USA-specific.
Optional launch switches can be combined:
.\run.ps1 -ModernControls -Widescreen -GeometryCorrection -PerspectiveTextures
.\run.ps1 -ModernControls -VerticalLook-MouseAim enables horizontal mouse aim without the modern action bindings.
Use the in-game settings menu for render scale, other aspect ratios, fullscreen,
audio, and control preferences.
chmod +x build.sh run.sh tools/regen.sh
./build.sh
./run.sh --modern-controls --widescreen --geometry-correction --perspective-textures--mouse-aim and --vertical-look are also available.
A machine without the game can still check a change:
DISRUPTOR_WITHOUT_GAME=1 ./build.shThis compiles every source except the translated game code, leaves the runtime
unlinked, and runs the tests that read no retail data. It needs Ninja. The
build workflow runs it on Linux and under MSYS2 MinGW-w64 for every pull
request.
The presentation-only frame interpolator is built by default, but starts off
until enabled in Settings → Enhancements. Configure with
-DDISRUPTOR_FRAME_INTERPOLATION=OFF to leave it out.
The build scripts generate the translated resident game code locally from your verified executable. That retail-derived code is excluded from this source repository. A release reads game assets and runtime-loaded code from the player's disc; it does not require loose game executables or captured files.
If a private regeneration produces generated/overlays_static.c, CMake can
also link that optional static overlay. Releases do not use it.
Keep generated game translations, disc files, captures, and binaries outside
Git; publish runtime binaries as release assets rather than source commits.
After the initial setup, use the same compiler environment:
cmake --build build --config Release --parallel
ctest --test-dir build -C Release --output-on-failureThe project's changes to the framework are patches against the revision in
framework/PIN. framework/FILES.txt lists every changed file, a + marking
one the project adds. To change the framework, edit the file in the psxrecomp
checkout, build and test, then save the edit as a patch:
python tools/framework.py refreshAfter pulling changes to framework/, bring the checkout up to date:
python tools/framework.py applyapply refuses to overwrite an edit that refresh has not saved, and
python tools/framework.py check tells whether the checkout holds exactly what
framework/ describes. Runtime preferences live in
settings.toml in the folder of the player's files, beside the executable in a
build folder; game files, generated code, and build directories are ignored by
Git.
The framework reaches the game through one header, runtime/include/game_host.h,
which src/game/host.cpp answers. The instructions the game hooks are listed in
game.toml under [[recompiler.instruction_hook]] for code that runs from RAM.
The code generator holds its own copy for native code, and a contract keeps the
two equal. The game's section of settings.toml belongs to the game as well:
its fields are in src/game/settings/game_user_settings.h, and
src/game/settings/saved.cpp reads and writes them for the framework's reader.
A save state carries a key made of the code generator's sources, listed in
psxrecomp/runtime/codegen_hash_sources.cmake. Any edit to one of them, a
comment included, makes every earlier save state and capsule unloadable. Those
files change only together with the pin.
| Folder | Holds |
|---|---|
src/game/ |
The game's own module by area: input, render, frame, language, session, settings, ui |
src/launcher/ |
The Windows launcher |
framework/ |
The pinned framework revision, the project's patches to it and the files it adds |
tests/unit/ |
Unit tests, one executable each |
tests/contracts/ |
Source contracts, run by Python |
tests/support/ |
Stubs and fakes the unit tests build against |
cmake/ |
Build helpers: test registration, launcher staging |
config/ |
Key presets |
tools/ |
Regeneration, the overlay, packaging, capsule replay |
game.toml, game-widescreen.toml and mouse-aim.ini stay at the root: the
game is started from there and reads them by those names.
A unit test is registered in CMakeLists.txt with
disruptor_unit_test(<test> tests/unit/<file>) and a contract with
disruptor_contract(<name>) for tests/contracts/test_<name>_contract.py.
Ctrl+F12 in the game starts a recording and Ctrl+F12 again ends it. The game
writes a folder under capsules/ beside settings.toml: a save state, the
input of every frame after it, the settings and the build it was made with.
A recording ends by itself after ten minutes, or when a save state is loaded.
The word REC shows in a corner for a second when a recording starts, and REC
saved when it ends.
Ctrl+F11 while recording marks the moment: the capsule keeps the frame and a
full-size picture of the screen as mark_000.ppm and on, listed in
marks.csv with the number of the shown picture it was read from. MARK shows
for a second once the picture is kept.
The capsule also keeps every picture that reached the screen in
presented.csv: the game frame it showed, named by the VBlank the game
presented it in, its place between that frame and the one before when it is
an in-between picture, and the window's size. Each frame of input.bin holds
the part of a game step smooth aim turned the picture by.
python tools/replay_capsule.py capsules/1791553413 --pictures outThis plays the capsule back without a window or sound, writes each frame as a PNG and exits with 0 when the replay stayed in step with the recording. A replay is the same run every time. It is not the recorded run bit for bit: a loaded state takes its interrupts a few instructions apart, so some frames differ from the recording in a few bytes of the game's memory, from one in ten to seven in ten in the runs measured. A replay counts as in step when it has the recording's memory at some frame of its last 60. It also prints how many frames matched and the longest run that did not.
--pose out.csv writes the camera's place and heading at every frame. With
a window, PSX_DISRUPTOR_CAPSULE_AS_SHOWN=1 makes the replay present exactly
the pictures of presented.csv instead of its own: each in-between picture
between the same two game frames at the same place, which the game waits
for. PSX_GL_SHOWN_DUMP then writes them with their number in the list.
A capsule holds the pad buttons, the heading the mouse wrote and the part of a step it turned the picture by, direct weapon selections and the pitch of the vertical look. It does not hold analog sticks, cheats or anything changed in the settings menu while it was recorded. It plays in the build that made it and with the settings it was made with, and it is not recorded in a network game. The pictures are of the software frame, which a wide aspect squashes: the stretched and in-between frames need a window.
capsule.toml states the aspect the game was shown at when the recording
began, which with adaptive view is the window's. A replay shows that aspect
from its first VBlank, whatever its own settings and window say. pages.bin
holds the game's memory of the first four frames by 4 KiB page: a replay whose
memory is apart at one of them names the pages that differ.