Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8e6de04
docs: bring the feature lists and the launcher picture up to date
Solganis Oct 11, 2026
8b9bddc
refactor: sort the tests into unit, contracts and support
Solganis Oct 11, 2026
fdb8807
refactor: move the status and disc notes into docs, the key presets i…
Solganis Oct 11, 2026
22bac48
refactor: sort the game's sources into modules under src/game
Solganis Oct 11, 2026
d11bef4
refactor: register the tests through two helper functions
Solganis Oct 11, 2026
3f862d3
docs: describe the layout in the build guide
Solganis Oct 11, 2026
8bd7de7
refactor: describe the settings once, in a table the launcher and the…
Solganis Oct 11, 2026
4317047
fix: remove the stray space in the mouse aim hint's middle-click
Solganis Oct 11, 2026
5f18d02
refactor: stage and save the menu's settings through the shared table
Solganis Oct 11, 2026
f12c2b8
refactor: draw the menu's settings from the shared table
Solganis Oct 12, 2026
bc1173b
fix: grey a setting by one rule of what it needs, in the launcher and…
Solganis Oct 12, 2026
fac502d
feat: show a setting's description under the pointer in the in-game menu
Solganis Oct 12, 2026
427d9c9
refactor: keep the framework changes as patches against the pinned re…
Solganis Oct 12, 2026
0c35c2a
refactor: let the host loop call the game through one interface
Solganis Oct 12, 2026
7e28c70
refactor: list the interpreter's instruction hooks in the game config…
Solganis Oct 12, 2026
d89a477
refactor: ask the game about a grown quad through the host interface
Solganis Oct 12, 2026
8c81a96
refactor: give the host loop a neutral switch for the in-between fram…
Solganis Oct 12, 2026
da76258
refactor: keep the game's settings section in the game's own files
Solganis Oct 12, 2026
f2321b9
refactor: let only the code generator name the game among the framewo…
Solganis Oct 12, 2026
2a9b6d3
fix: keep a key that types a name from pressing a pad button
Solganis Oct 12, 2026
a04a049
refactor: split the in-game menu into a folder, a file a tab
Solganis Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@
*.c text eol=lf
*.cpp text eol=lf
*.h text eol=lf

# A patch must reach every machine byte for byte: one framework file has CRLF lines.
framework/patches/** -text
650 changes: 161 additions & 489 deletions CMakeLists.txt

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ Keep contributions source-only. Do not commit disc images, extracted game code
or assets, generated translations, captured overlays, proprietary BIOS files,
binaries, saves, captures, logs, credentials, or machine-specific paths.

Edit project source, configuration, seeds, tests, or `psxrecomp-overlay/`, then
regenerate locally. Every framework-overlay change must be listed in
`PSXRECOMP_OVERLAY_FILES.txt`; do not hand-edit `generated/*.c` or include
Edit project source, configuration, seeds, tests, or the framework checkout,
then regenerate locally. Save a framework change with
`python tools/framework.py refresh`, and list a newly changed file in
`framework/FILES.txt` first. Do not hand-edit `generated/*.c` or include
unrelated framework changes.

Before submitting a change:

- Run the relevant tests and review both the working diff and staged paths.
- Keep experimental visual features optional.
- Update [STATUS.md](STATUS.md) when behavior, verified coverage, or a known
- Update [STATUS.md](docs/STATUS.md) when behavior, verified coverage, or a known
limitation changes. Keep it focused on the current build.

For bug reports, include the level, steps to reproduce, render scale/aspect,
Expand Down
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ disc revision; the redistributable OpenBIOS backend is used for booting.
the screen too, movies stay at 4:3, and the HUD retains its proportions. A
HUD size setting shrinks it toward the screen edges.
- Optional geometry and perspective-texture correction.
- Optional gap filling, a depth-sorted world, and experimental ambient
occlusion on top of geometry correction.
- Optional 60 FPS gameplay and in-between frames.
- Keyboard/controller input and optional modern WASD/mouse controls.
- Optional smooth mouse turning between the game's 256 turn steps.
- Experimental vertical mouse look and weapon aim.
- In-game settings for controls, display, fullscreen, VSync, volume, and mute.
- Diagnostic views and a screen recorder for reports of a fault in the picture.
- Memory-card saves and twelve save-state slots.
- Windows launcher with disc-image browsing, automatic copy/CUE setup,
full-disc SHA-256 verification, the game's settings, and game launch.
full-disc SHA-256 verification, the game's settings with a hint for each,
and game launch.
- Optional French, German, and Japanese game content from an additional disc
image you own; the supported USA disc remains required.
- Optional intro skipping and a key to skip a logo or movie.
Expand All @@ -48,15 +53,15 @@ and share feedback.

## Build and run

Install Git, Python 3, CMake 3.20+, and a Windows x64 C++ toolchain. Visual Studio
Install Git, Python 3.10+, CMake 3.20+, and a Windows x64 C++ toolchain. Visual Studio
with **Desktop development with C++** is supported; Ninja is recommended.

Place these files from your own supported disc in `input/`:

- `SLUS_002.24`
- `Disruptor (USA).cue` and the `.bin` track it references

Check the required hashes and disc format in [DISC.md](DISC.md). Then run from
Check the required hashes and disc format in [DISC.md](docs/DISC.md). Then run from
an x64 developer PowerShell:

```powershell
Expand Down Expand Up @@ -100,8 +105,8 @@ retest and save-state restart check passed after fixing an ultrawide rendering
buffer overflow. Full campaign and release validation remain incomplete; minor
geometry and texture artifacts can remain.

- [Current status and known limits](STATUS.md)
- [Current status and known limits](docs/STATUS.md)
- [Build details](docs/BUILD.md)
- [Supported disc revision](DISC.md)
- [Supported disc revision](docs/DISC.md)
- [Contributing](CONTRIBUTING.md)
- [Third-party notices](THIRD_PARTY_NOTICES.md)
2 changes: 1 addition & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Third-party notices

`psxrecomp-overlay/` contains modified files based on PSXRecomp revision
`framework/` contains patches to, and files written for, PSXRecomp revision
`854a658372b304c222a0d0cbbae16c66264b785e`. PSXRecomp is copyright Matthew
Stan and is provided under the PolyForm Noncommercial License 1.0.0. The
upstream license is reproduced in
Expand Down
11 changes: 6 additions & 5 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ $ErrorActionPreference = 'Stop'

$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
$Framework = Join-Path $Root 'psxrecomp'
$Pin = (Get-Content (Join-Path $Root 'PSXRECOMP_PIN') -Raw).Trim()
$Pin = (Get-Content (Join-Path $Root 'framework/PIN') -Raw).Trim()
$Exe = Join-Path $Root 'input/SLUS_002.24'

# Ninja inherits its target architecture from the active compiler environment.
Expand Down Expand Up @@ -101,6 +101,10 @@ foreach ($Command in @('git', 'cmake', 'ctest', 'python')) {
throw "Missing prerequisite: $Command"
}
}
python -c "import sys; sys.exit(sys.version_info < (3, 10))"
if ($LASTEXITCODE -ne 0) {
throw 'Python 3.10 or newer is required.'
}
if (!(Test-Path $Exe)) {
throw "Copy the verified SLUS_002.24 into input/ before building."
}
Expand All @@ -114,10 +118,7 @@ if ($LASTEXITCODE -ne 0) {
}
Invoke-Checked { git -C $Framework checkout --detach $Pin }
Invoke-Checked {
python (Join-Path $Root 'tools/apply_framework_overlay.py') --framework $Framework
}
Invoke-Checked {
python (Join-Path $Root 'tools/patch_openbios_seeds.py') --framework $Framework
python (Join-Path $Root 'tools/framework.py') apply --framework $Framework
}

$Generator = @()
Expand Down
11 changes: 7 additions & 4 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ set -eu

ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
FRAMEWORK="$ROOT/psxrecomp"
PIN=$(tr -d '[:space:]' < "$ROOT/PSXRECOMP_PIN")
PIN=$(tr -d '[:space:]' < "$ROOT/framework/PIN")

for command in git cmake ctest python3; do
command -v "$command" >/dev/null 2>&1 || {
echo "Missing prerequisite: $command" >&2
exit 1
}
done
python3 -c 'import sys; sys.exit(sys.version_info < (3, 10))' || {
echo "Python 3.10 or newer is required." >&2
exit 1
}
# DISRUPTOR_WITHOUT_GAME=1 is for a machine without the game: every source is
# compiled, the runtime is not linked, and the tests that read no retail data run.
WITHOUT_GAME="${DISRUPTOR_WITHOUT_GAME:-}"
Expand All @@ -24,8 +28,7 @@ if [ ! -d "$FRAMEWORK/.git" ]; then
fi
git -C "$FRAMEWORK" fetch --quiet origin "$PIN"
git -C "$FRAMEWORK" checkout --detach "$PIN"
python3 "$ROOT/tools/apply_framework_overlay.py" --framework "$FRAMEWORK"
python3 "$ROOT/tools/patch_openbios_seeds.py" --framework "$FRAMEWORK"
python3 "$ROOT/tools/framework.py" apply --framework "$FRAMEWORK"

GENERATOR=""
if command -v ninja >/dev/null 2>&1; then GENERATOR="-G Ninja"; fi
Expand Down Expand Up @@ -86,7 +89,7 @@ if [ -n "$WITHOUT_GAME" ]; then
printf '%s\n' $OBJECTS $TESTS | xargs -n 100 ninja -C "$BUILD"
ctest --test-dir "$BUILD" --output-on-failure --no-tests=error -LE retail
# The retail contracts still check the sources when the image is not asked for.
for contract in tests/test_*_contract.py; do
for contract in tests/contracts/test_*_contract.py; do
python3 -B "$contract"
done
echo "Built and tested without the game: the runtime was compiled and not linked."
Expand Down
56 changes: 56 additions & 0 deletions cmake/DisruptorTests.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# disruptor_contract(<name> [RETAIL])
#
# Registers tests/contracts/test_<name>_contract.py as the test disruptor_<name>_contract.
# RETAIL: the contract needs the retail image or the translated code. It is passed
# --require-artifacts and labelled so that `ctest -LE retail` leaves it out.
function(disruptor_contract name)
cmake_parse_arguments(PARSE_ARGV 1 CONTRACT "RETAIL" "" "")
if(CONTRACT_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "disruptor_contract(${name}): unknown arguments ${CONTRACT_UNPARSED_ARGUMENTS}")
endif()
set(script "${PROJECT_SOURCE_DIR}/tests/contracts/test_${name}_contract.py")
if(NOT EXISTS "${script}")
message(FATAL_ERROR "disruptor_contract(${name}): ${script} does not exist")
endif()
set(arguments "")
if(CONTRACT_RETAIL)
set(arguments --require-artifacts)
endif()
add_test(NAME disruptor_${name}_contract
COMMAND "${Python3_EXECUTABLE}" -B "${script}" ${arguments})
if(CONTRACT_RETAIL)
set_tests_properties(disruptor_${name}_contract PROPERTIES LABELS retail)
endif()
endfunction()

# disruptor_unit_test(<test> <source> [TARGET <target>] [RUNTIME_HEADERS]
# [SOURCES <file>...] [INCLUDES <folder>...] [DEFINITIONS <definition>...]
# [ARGUMENTS <argument>...])
#
# Builds one C++20 executable from <source>, a path from the project's root, and registers
# it as the test <test>. The executable is named after the test, disruptor_far_rendering
# giving disruptor-far-rendering-test, unless TARGET names it.
# RUNTIME_HEADERS puts the framework's runtime headers on its include path.
function(disruptor_unit_test test source)
cmake_parse_arguments(PARSE_ARGV 2 UNIT "RUNTIME_HEADERS" "TARGET"
"SOURCES;INCLUDES;DEFINITIONS;ARGUMENTS")
if(UNIT_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "disruptor_unit_test(${test}): unknown arguments ${UNIT_UNPARSED_ARGUMENTS}")
endif()
set(target "${UNIT_TARGET}")
if(NOT target)
string(REPLACE "_" "-" target "${test}-test")
endif()
add_executable(${target} "${PROJECT_SOURCE_DIR}/${source}" ${UNIT_SOURCES})
if(UNIT_RUNTIME_HEADERS)
target_include_directories(${target} PRIVATE "${PSXRECOMP_ROOT}/runtime/include")
endif()
if(UNIT_INCLUDES)
target_include_directories(${target} PRIVATE ${UNIT_INCLUDES})
endif()
if(UNIT_DEFINITIONS)
target_compile_definitions(${target} PRIVATE ${UNIT_DEFINITIONS})
endif()
target_compile_features(${target} PRIVATE cxx_std_20)
add_test(NAME ${test} COMMAND ${target} ${UNIT_ARGUMENTS})
endfunction()
2 changes: 1 addition & 1 deletion tools/stage_launcher.cmake → cmake/stage_launcher.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
foreach(pair IN ITEMS "game.toml|game.toml" "mouse-aim.ini|mouse-aim.ini"
"docs/PLAYING.md|GETTING_STARTED.md"
"release/windows/Play Disruptor.cmd|Play Disruptor.cmd"
"keybinds-modern.ini|keybinds.ini"
"config/keybinds-modern.ini|keybinds.ini"
"release/windows/settings.toml|settings.toml")
string(REPLACE "|" ";" paths "${pair}")
list(GET paths 0 source)
Expand Down
File renamed without changes.
File renamed without changes.
56 changes: 50 additions & 6 deletions docs/BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
## Requirements

- The verified Disruptor USA executable and BIN/CUE files in `input/`; see
[DISC.md](../DISC.md).
- Git, Python 3, CMake 3.20+, and a C++20 toolchain.
[DISC.md](DISC.md).
- Git, Python 3.10+, CMake 3.20+, and a C++20 toolchain.
- Windows x64: Visual Studio with **Desktop development with C++**, or
MSYS2 MinGW-w64. Ninja is recommended.
- Linux: GCC and the platform development libraries needed to build SDL.
Expand Down Expand Up @@ -118,17 +118,61 @@ cmake --build build --config Release --parallel
ctest --test-dir build -C Release --output-on-failure
```

Changes to `psxrecomp-overlay/` must be applied before rebuilding:
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:

```sh
python tools/apply_framework_overlay.py --framework psxrecomp
python tools/framework.py refresh
```

The framework revision is pinned in `PSXRECOMP_PIN`, and the reviewed overlay
files are listed in `PSXRECOMP_OVERLAY_FILES.txt`. Runtime preferences live in
After pulling changes to `framework/`, bring the checkout up to date:

```sh
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` beside the executable; 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
Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions docs/PLAYING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ are enabled initially. Vertical mouse look is experimental and starts off;
enable it in **Settings → Controls** if desired.
**Smooth mouse turning** in the same tab turns the picture between the game's
256 heading steps. It requires captured mouse aim and exact geometry.
A setting shows its description under the pointer, the same one as in the
launcher. A setting that needs another one stays greyed until that one is on.
In-between frames that are on can still be switched off.

**Settings → Enhancements** offers 1x–8x resolution, widescreen options,
HUD size for widescreen, and 60 FPS gameplay and in-between frames. Both
Expand Down
2 changes: 1 addition & 1 deletion STATUS.md → docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ validation is incomplete.
individually exercised live.
- Live volume adjustment and its relaunch persistence need user validation.

See [build details](docs/BUILD.md) and [disc requirements](DISC.md) before
See [build details](BUILD.md) and [disc requirements](DISC.md) before
building or reporting a problem.
Binary file modified docs/images/launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions PSXRECOMP_OVERLAY_FILES.txt → framework/FILES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Reviewed files applied from psxrecomp-overlay/ to the pinned framework.
# Prefix an entry with '+' only when the overlay intentionally creates it.
# Files of the pinned framework that this project changes, each with a patch under patches/.
# '+' marks a file the project adds, kept whole under files/.
recompiler/include/code_generator.h
recompiler/src/code_generator.cpp
recompiler/src/config_loader.cpp
Expand All @@ -9,6 +9,7 @@ recompiler/tests/recompiler_patch_test.cpp
recompiler/tests/test_reachable_discovery_codegen.py
runtime/CMakeLists.txt
runtime/include/dirty_ram_interp.h
+runtime/include/game_host.h
+runtime/include/gte_precision.h
runtime/include/gpu.h
+runtime/include/gpu_ws_hud_scale.h
Expand Down Expand Up @@ -49,3 +50,5 @@ runtime/src/overlay_loader.c
runtime/src/psx_cycles.c
runtime/src/ws_aspect_cone_math.c
runtime/tests/test_ws_aspect_cone_math.c
recompiler/seeds/openbios_elf_seeds.json
recompiler/src/full_function_emitter.cpp
File renamed without changes.
45 changes: 45 additions & 0 deletions framework/files/runtime/include/game_host.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#ifndef PSX_GAME_HOST_H
#define PSX_GAME_HOST_H

/* What the host asks of the game project it is built into. A project that defines PSX_HAS_GAME_HOST supplies every one. */

#include <stddef.h>
#include <stdint.h>

typedef struct CPUState CPUState;

#ifdef __cplusplus
namespace PSXRecompV4 { struct UserSettings; }
/* Called when the user's settings are read, before the drive first reads the disc. True asks for a boot without the BIOS animation. */
bool psx_game_settings_loaded(const PSXRecompV4::UserSettings &settings);
extern "C" {
#endif

/* Called when the disc's serial is known, `length` characters of it. 0 stops the start. */
int psx_game_configure(const char *game_id, size_t length);
/* Nonzero asks for a boot without the BIOS animation whatever the settings say. */
int psx_game_fast_boot_forced(void);
/* Called when save states have their folder, before any can be asked for. */
void psx_game_states_configured(void);
/* Called after a save state is restored. */
void psx_game_state_loaded(void);
/* A key went down or up. `plain` is set for a press without Ctrl, Alt or the system key and for every release. Nonzero when the game took a press. */
int psx_game_key(int scancode, uint32_t keycode, int down, int plain);
/* The keyboard's state as a pad may read it: `count` keys, nonzero for down, copied from `keys` to `kept` with the keys
* the game is typing with shown as up. Called at every reading, which can be several a frame. */
void psx_game_keys_for_pad(const uint8_t *keys, uint8_t *kept, int count);
/* Called around the sampling of the pad, outside netplay. */
void psx_game_before_input(void);
void psx_game_after_input(void);
/* Nonzero when the four-cornered polygon whose command sits at `command_address` is the game's second copy of the one
* before it, grown outwards. Then each corner's source address and source word are those of the first copy's corner. */
int psx_game_grown_quad_sources(uint32_t command_address, const uint32_t *packet, uint32_t source_address[4],
uint32_t source_word[4]);
/* Supplied by the generated code, from [[recompiler.instruction_hook]]: the game's calls for an instruction that ran from RAM. */
void psx_game_instruction_ran(CPUState *cpu, uint32_t address, uint32_t instruction);

#ifdef __cplusplus
}
#endif

#endif
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* Exact GTE projection provenance shared by the GTE and GPU paths.
*
* SWC2 records the projection associated with a guest RAM word. Some games,
* including Disruptor, instead read SXY through MFC2 and later use an ordinary
* SWC2 records the projection associated with a guest RAM word. Some games
* instead read SXY through MFC2 and later use an ordinary
* SW. Those routes are admitted only through opcode/dataflow-reviewed MFC2
* and store PCs registered by the game runtime; a separately reviewed route
* may seed a finite set of scratchpad words for a later exact packet copy.
Expand Down
Loading
Loading