Skip to content

Latest commit

 

History

History
340 lines (277 loc) · 16.4 KB

BUILD.md

File metadata and controls

340 lines (277 loc) · 16.4 KB

Building from source

Requirements

  • 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 build workflow'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.

Windows

Run from an x64 developer PowerShell at the repository root:

powershell -ExecutionPolicy Bypass -File .\build.ps1
.\run.ps1

The 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.

Releases

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.md needs the release's ## v0.3.1 section first. It becomes the package's notes, which the launcher shows.
  • The GitHub release's text is release/notes/v0.3.1.md when 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/packages holds DisruptorRecomp-win-Setup.exe and DisruptorRecomp-win-Portable.zip for players and the packages the launcher updates from. Setup installs the Visual C++ x64 Redistributable when it is missing, the ZIP does not. --publish creates 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 --sign alone, 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.json bundle and a line of SLSA provenance in DisruptorRecomp-<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:

  1. Pack without --publish. The build folder must hold a SOURCE_COMMIT file naming the checkout's commit. The packager records every package's digest in packed.json beside them and prints the next two commands with this build's paths.

  2. 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.

  3. 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.

  4. Push the commit and wait for the build workflow to pass.

  5. 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-failure

DisruptorLauncher.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.

Linux

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.

Without the game

A machine without the game can still check a change:

DISRUPTOR_WITHOUT_GAME=1 ./build.sh

This 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.

In-between frames

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.

Generated game code

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.

Incremental build and tests

After the initial setup, use the same compiler environment:

cmake --build build --config Release --parallel
ctest --test-dir build -C Release --output-on-failure

The 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 refresh

After pulling changes to framework/, bring the checkout up to date:

python tools/framework.py apply

apply 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.

Layout

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.

Replay capsules

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 out

This 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.