Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ concurrency:

jobs:
ci:
uses: THPS64Recomp/THPS64Host/.github/workflows/game-ci.yml@v0.1.0
uses: THPS64Recomp/THPS64Host/.github/workflows/game-ci.yml@v0.2.0
with:
game: thps2
host-tag: v0.1.0
host-tag: v0.2.0
secrets:
# Only while the repositories are private (RELEASING.md); empty, and unused, once they are public.
THPS64_READ_TOKEN: ${{ secrets.THPS64_READ_TOKEN }}
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ concurrency:

jobs:
release:
uses: THPS64Recomp/THPS64Host/.github/workflows/game-release.yml@v0.1.0
uses: THPS64Recomp/THPS64Host/.github/workflows/game-release.yml@v0.2.0
permissions:
contents: write # the draft release; the called workflow narrows it per job
with:
game: thps2
host-tag: v0.1.0
host-tag: v0.2.0
version: ${{ inputs.version }}
arch: ${{ inputs.arch }}
replace-assets: ${{ inputs.replace-assets }}
Expand Down
30 changes: 25 additions & 5 deletions BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ All systems need git, Python 3.11 or newer, CMake 3.20 or newer, and Ninja.
- **macOS:** Xcode or the Command Line Tools (with the Metal toolchain), plus `brew install cmake ninja sdl2 pkgconf`.
- **Windows:** Visual Studio 2022 with the "C++ Clang Compiler for Windows" and "C++ CMake tools" components. The build
uses clang-cl and finds Visual Studio itself. Clone to a short path (RT64 nests deeply) and turn on long paths first:
`git config --global core.longpaths true`.
`git config --global core.longpaths true`. Use `python` (or `py`) wherever this page says `python3`: on a stock
Windows PC, `python3` opens the Microsoft Store instead of running Python. To try the port on Windows, follow the
checklist in [WINDOWS-TEST.md](decomp/thps2/recomp-host/docs/WINDOWS-TEST.md).
- **Linux:** clang or gcc and the SDL2, Vulkan, X11 and GTK 3 development packages (on Debian or Ubuntu:
`ninja-build clang libsdl2-dev libvulkan-dev libx11-dev libgtk-3-dev`). Linux builds in CI but hasn't been run on a desktop.

Expand Down Expand Up @@ -60,8 +62,9 @@ On Windows, run the same command with `python` in a normal command prompt, or us
`powershell -ExecutionPolicy Bypass -File decomp/thps2/recomp-host\tools\build_windows.ps1 -Rom <your dump>`. The binary is then
`thps2_recomp.exe`; the release package names it `THPS2-64Recomp.exe`.

Building more than one game? Share one runtime: `--runtime ~/thps64-runtime`. The first build sets it up (about 850 MB),
and the other games reuse it. `tools/check.sh` uses the folder in `THPS_RUNTIME_DIR` the same way.
The runtime (about 850 MB) is set up in the build folder. To keep it elsewhere, or to share one between games, pass
`--runtime ~/thps64-runtime`. Set `THPS_RUNTIME_DIR` to that folder too, and `tools/check.sh` uses it as well instead of
setting up its own. (Without it, `check.sh` reuses the runtime of your last build if there is one.)

### Other builds

Expand Down Expand Up @@ -124,8 +127,25 @@ decomp/thps2/recomp-host/tools/test.sh # the whole suite; exit code
decomp/thps2/recomp-host/tools/test.sh --first-run # the first-start flow of the split build
```

The suite finds your dump through `$THPS2_ROM`, or `--rom`. Only one copy of the game may run at a time, so close
it before running tests. On Windows, run `test.sh` from Git Bash.
`--first-run` tests the split engine, so build that first with `build_host.py --split` (see "Other builds").

The suite finds your dump through `$THPS2_ROM`, or `--rom`. On Windows, run `test.sh` from Git Bash, and write the path
the Windows way: `export THPS2_ROM="C:/path/to/dump.z64"`. Only one copy of the game may run at a time, so close it
before running tests.

Each run keeps its pictures and logs in `decomp/thps2/recomp-host/build/tests/out/` (a few GB for a whole suite). You can
delete that folder when you're done.

**Other releases.** The suite runs on USA by default. To test Europe, set `THPS2_ROM_EU` to your Europe dump and record
its pictures once:

```sh
decomp/thps2/recomp-host/tools/test.sh --version thps2_eu --record # records Europe's own pictures, then runs the suite
decomp/thps2/recomp-host/tools/test.sh --version thps2_eu # later runs
```

Most screens look the same as in USA and use its pictures. The few that differ get their own (Europe: the main menu
wheel and the Create Skater roster).

Some checks need no dump: `test.sh --controls-cfg`, `test.sh --gfx-cfg`, and
`python3 -I decomp/thps2/recomp-host/tools/gen_versions.py check` (the version tables).
Expand Down
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,61 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based

## [Unreleased]

### Added
- Graphics > Picture Border (`border = fill`, `--border fill`): the 3D picture reaches the edges of the window, without the
thin black frame the game leaves for a TV's overscan. It shows a little more of the scene at the same size (no zoom). Off by
default; menus and loading screens keep their own frame. In a two-player split screen, both views reach the edges
too and meet at the game's thin divider line in the middle.
- Rebinding in the settings menu: the Controls tab has a row per N64 input with two controller slots and two keyboard
slots. Choose a slot and press the new button or key; a button another action uses trades places with it, and
Default puts a row back. A slot that differs from the layout shows in yellow, and Reset all to default (it asks first)
puts every binding back. Changes work at once and are saved to `controls.cfg` ([MANUAL](docs/MANUAL.md#controls)).
- A Controllers list in the Controls tab: which controller is player 1 or 2, what it is pressing right now, and buttons
to swap players or give a controller either player. One controller on player 2 with the keyboard alone on player 1
makes a two-player game with one controller. The choice lasts until you quit.
- Test sessions `menu-controls`, `menu-controls-reload` and `menu-ports` (virtual pads).
- Draw distance (Graphics tab, `draw_distance` in `graphics.cfg`, `--draw-distance`): Original (the default, the game as it
was), 2x, 4x and Full (the whole course; scenery the game hides by area as you skate, secrets and items a goal
reveals stay hidden until the game shows them). It applies without a restart. A shorter distance applies at
once; a longer one reaches the far scenery after a RETRY, the next course or a camera change. Every course start sets up
bigger matrix buffers and display lists (144 KiB instead of 72), of which the game uses only its own part at Original.
It steps back on its own if a frame comes close to filling the game's buffers. `dd_buffers = original` in `graphics.cfg`
(`--dd-buffers original`; not in the menu) keeps the game's own buffers and memory layout exactly; it needs a restart.
- Tests: Europe's own reference pictures, for the few screens that differ from USA (`OWN_REFS` in
`tests/scenarios.py`), are recorded by `test.sh --version thps2_eu --record`.
- Fog (`fog` in `graphics.cfg`, `--fog`; not in the menu or a preset): Original (the default), Soft or Off. The game's
fog is thin by design, a haze over the far end of the view (in most courses the last eighth). Soft starts it at 70 %
of the draw distance, so far scenery fades in instead of popping in; Off shows a long draw distance most clearly. At
Full draw distance the game's fog is far beyond the courses, so Soft looks like Original there. Test sessions
`fog-check` (in the suite) and `fog-shot` (opt-in pictures).

### Changed
- Built on THPS64Host v0.2.0.
- Frame Rate (Graphics tab, `refresh` in `graphics.cfg`, `--refresh`) is now Original (30) or 60, default 60. "Match display"
and 120 are gone: at 120 Hz (what "Match display" gave on a ProMotion Mac) the picture stuttered. A `graphics.cfg` with
`refresh=display` or another rate moves to 60 once, with a log line.
- Graphics > Preset (`--preset original|enhanced`): Original sets 4:3, the HUD in the 4:3 area, the 4:3 split screen,
the black frame, 30 fps and the game's draw distance, as on the N64. Enhanced sets 16:9, the HUD at the screen edges,
the wide split screen, Picture Border Fill, 60 fps and Full draw distance. Choosing one replaces your choices in those
six rows; Custom shows when they match neither. A new install starts with Enhanced. An existing `graphics.cfg` keeps
its values, and a setting it lacks takes the new default (so an older file gets Picture Border Fill and Full draw
distance). Fog is in neither preset.
- Removed the known issue "at 21:9, a far wall can drop out at the right edge". It was reported in THPS1, where it
doesn't happen in v0.1.0, and the widescreen code involved is shared by all three games.
- Two-player split screen fills the picture in widescreen (16:9, 21:9, following the window): each player's view is widened
to its half, and each player's HUD sits at its own view's edge (also with HUD Placement `original`). 4:3 is unchanged.
It's on by default; Graphics > Two-Player Split Screen (`split`, `--split original`) keeps both views in the 4:3 area,
each with its HUD inside it, and black bands at the sides.
- Level starts freeze the picture for about 0.16 s instead of 0.2 s: the shared host unpacks the game's data twice as
fast and runs the game's `memset` and `memcpy` natively.
- BUILDING.md: on Windows use `python` instead of `python3` and a `C:/...` path in `THPS2_ROM`, with a link to the Windows
test checklist; how `tools/check.sh` shares the build's runtime; `--first-run` needs `build_host.py --split` first; where
the test output goes; how to test Europe.

### Fixed
- With a `controls.cfg` that matches no preset (edited by hand), Xbox B flipped and grabbed at once while skating. The
east button's split between flip and back now follows the bindings, not the preset name.

## [0.1.0-preview] - 2026-10-18

An early preview, released next to THPS1 64: Recompiled v0.1.0 and built on THPS64Host v0.1.0. It runs
Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,36 @@ the Cheats tab gets you to every level quickly.
## Features

- **Widescreen:** 16:9, 21:9 or fit to window. The HUD moves to the screen edges.
- **High frame rate:** the game's 30 fps interpolated to your display's refresh rate (60 Hz, 120 Hz, or higher).
- **High frame rate:** the game's 30 fps interpolated to 60 fps.
- **Draw distance:** see much more of each course (Original, 2x, 4x or Full).
- **Picture Border:** Fill removes the thin black frame the N64 left around the picture.
- **Two-player split screen** fills the picture in widescreen.
- **Modern rendering:** RT64 on Metal (macOS) or Direct3D 12 (Windows), any resolution, MSAA.
- **Recomp settings:** press **Esc** (or **Back** on a controller) for graphics, controls, sound and cheats. The game's
own options are still in its menus.
- **Modern controls:** any SDL controller, hot-plugging, two players. Xbox-style layout (ollie, flip, grab and grind on the
face buttons instead of the N64's C buttons) or the original N64 layout.
face buttons instead of the N64's C buttons) or the original N64 layout. Rebind any button or key in the settings menu,
and pick which controller is player 1 or 2 there.
- **Saves:** Controller Pak saves in a standard `.mpk` file: careers, created skaters and parks.
- **Cheats:** Unlock All Levels, Perfect Balance, Slow Motion, Skip to Restart, Always Special. Off by default.

![The same moment at 16:9](docs/screenshots/hangar-16x9.jpg)
## Original or Enhanced

The Preset row at the top of the Graphics tab sets six settings at once. New installs start with Enhanced. If you
change a row yourself, the preset shows Custom.

![Philadelphia at the same moment: Original on the left, Enhanced on the right](docs/screenshots/original-vs-enhanced.jpg)

| Setting | Original | Enhanced (default) |
|---|---|---|
| Aspect Ratio | 4:3 | 16:9 |
| HUD Placement | 4:3 middle | Screen edges |
| Frame Rate | 30 fps | 60 fps |
| Draw Distance | As on the N64 | Full (the whole course) |
| Picture Border | Thin black frame | Fill |
| Two-Player Split Screen | 4:3 middle | Fills the picture |

## System requirements

- **macOS:** 13 or later, Apple Silicon or Intel. The first start needs Apple's Command Line Tools (the app offers to
Expand Down Expand Up @@ -88,8 +108,6 @@ All bindings and the N64 layout: [docs/MANUAL.md](docs/MANUAL.md).

- An early preview: this list is incomplete.
- A short freeze when a level loads.
- At 21:9, a far wall can drop out at the right edge.
- Two players (split screen) stays a 4:3 picture in widescreen.
Full list: [docs/KNOWN-ISSUES.md](docs/KNOWN-ISSUES.md).

## Bugs and feedback
Expand Down
2 changes: 1 addition & 1 deletion decomp/common
Submodule common updated 45 files
+111 −1 CHANGELOG.md
+1 −1 docs/THIRD_PARTY.md
+11 −0 host/cmake/thps_host.cmake
+1 −1 host/docs/interpolation-ids.md
+2 −1 host/patches/README.md
+232 −0 host/patches/rt64-splitscreen-widescreen.patch
+223 −0 host/src/border_crop.cpp
+24 −2 host/src/control.cpp
+1 −1 host/src/dl_inspect.cpp
+469 −0 host/src/draw_distance.cpp
+142 −0 host/src/draw_distance.h
+11 −0 host/src/draw_distance_none.cpp
+40 −12 host/src/erz.cpp
+3 −1 host/src/erz.h
+234 −0 host/src/fog.cpp
+45 −4 host/src/frametime.cpp
+7 −1 host/src/game_abi.h
+2 −0 host/src/game_imports.def
+114 −18 host/src/gfx_config.cpp
+4 −2 host/src/hfr.h
+61 −4 host/src/hooks.cpp
+65 −1 host/src/host.h
+170 −35 host/src/hud_anchor.cpp
+495 −35 host/src/input.cpp
+12 −2 host/src/interpolation.cpp
+13 −2 host/src/main.cpp
+15 −4 host/src/rt64_context.cpp
+44 −0 host/src/thps_hooks.h
+417 −10 host/src/ui/ui_host.cpp
+19 −1 host/src/widescreen.cpp
+11 −1 host/tests/controls_cfg_test.py
+187 −0 host/tests/crop_sessions.py
+32 −3 host/tests/firstrun_test.py
+179 −7 host/tests/gfx_cfg_test.py
+37 −7 host/tests/harness.py
+1 −1 host/tests/hfr_steps.py
+22 −0 host/tests/hud_sessions.py
+51 −0 host/tests/load_freeze.py
+422 −1 host/tests/menu_session.py
+320 −11 host/tests/run.py
+24 −3 host/tools/build_host.py
+2 −1 host/tools/check.sh
+2 −1 host/tools/setup_runtime.py
+20 −2 host/tools/test.sh
+132 −18 host/ui/menu/settings.rml
12 changes: 9 additions & 3 deletions decomp/thps2/recomp-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,19 @@ tools/test.sh --record # first time on a machine: capture reference fra
tools/test.sh # controls.cfg test + the whole suite
tools/test.sh --only pak_save # one scenario (+ the ones before it in its session)
tools/test.sh --check-refs # how distinct the recorded references are
tests/run.py --version thps2_eu # the suite on Europe (its references live in build/tests/refs-thps2_eu/; the rest fall through to USA's)
tools/test.sh --version thps2_eu --record # Europe: records the screens that differ (OWN_REFS) into build/tests/refs-thps2_eu/
tools/test.sh --version thps2_eu # the suite on Europe (the other screens use USA's references)
```

`tests/run.py` starts the host once per **session** and walks the game through **scenarios**, waiting for **checkpoints**
instead of sleeping. Only one `thps2_recomp` runs at a time (`--no-pads --mute`; `uptime` is logged with every number). The scenarios cover the
front end, a career level, audio, controls, widescreen, Controller Pak, the settings, the camera and aspect checks, high frame rate and the
in-game settings menu (`menu` and `menu-reload` sessions, six scenarios, skipped when `--features` says `menu=0`).
in-game settings menu (`menu` and `menu-reload` sessions, six scenarios; `menu-controls`, `menu-controls-reload` and `menu-ports` for
rebinding and the Controllers list, ten scenarios; all skipped when `--features` says `menu=0`).

`lf-check` (`tests/load_freeze.py`) times the first three course starts of the course cycle with `ftime`, with no screenshot inside
the window: the median over the starts of the longest gap between game frames while the course loads stays under 140 ms (104-120 ms seen).
`lf-probe` (opt-in, `--session lf-probe`) runs all four starts in a visible window and keeps the CSVs in `build/tests/lf/`.

Full runs on the static build, 2026-10-12: Europe 32 pass, 0 fail, 4 skipped; USA 32 pass, 0 fail, 4 skipped, after re-running `level_grab`, a
known flake (the in-level HUD checkpoint was missed under load; `pause_resume` failed about one run in three in earlier runs). The 4 skips are
Expand All @@ -191,4 +197,4 @@ CI (`.github/workflows/ci.yml`) builds the stub host on Linux, Windows and macOS
- No in-game pak swapping; the in-game pak manager is not exercised by the tests; Rumble Pak output is untested on a real pad.
- Widescreen: the in-level HUD is anchored to the screen edges by default (`--hud expand`; `--hud original` keeps it centred 4:3); menus and loading screens are pillarboxed.
- High frame rate is checked in The Hangar only.
- Texture packs: not attempted. The settings menu has no rebinding of single keys or buttons yet (edit `controls.cfg`).
- Texture packs: not attempted. The settings menu rebinds single keys and buttons in its Controls tab (saved to `controls.cfg`).
48 changes: 48 additions & 0 deletions decomp/thps2/recomp-host/config/thps2.hooks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,57 @@ text = "ctx->r2 = 1; return;"
func = "Cheat_IsOn"
text = "if (thps_cheat_is_on(rdram, ctx)) { ctx->r2 = 1; return; }"

# Draw distance "full" (game/draw_distance.cpp, off by default): World_DrawItems(list, view, player) draws the course's items and skips the
# ones the trigger scripts have hidden by zone (item flag bit 1, player 2: 0x40). With "full" the hook clears those bits for the length of
# the call and puts them back, so everything is drawn while the game's own state (collision, triggers) is unchanged.
[[patches.hook]]
func = "World_DrawItems"
text = "if (thps_world_draw_items(rdram, ctx)) return;"

# Draw distance (game/draw_distance.cpp): Mesh_QueueEmit(renderer) draws one pass's mesh queue (512 entries) and depth-sorted list of
# translucent meshes (96); past either size the game drops meshes. The hook notes how full both are, so the governor can keep a longer
# far plane from filling them. Read only.
[[patches.hook]]
func = "Mesh_QueueEmit"
text = "thps_mesh_queue_emit(rdram, ctx);"

# Draw distance (game/draw_distance.cpp): when the setting is not `original` at the time, the gfx set-up's Mem_Alloc for the two matrix
# pools (0xD200 bytes, cap 420 stored just before) asks for a bigger block and raises the cap word; every other call runs unchanged.
[[patches.hook]]
func = "Mem_Alloc"
text = "if (thps_mem_alloc(rdram, ctx)) return;"

# Draw distance (game/draw_distance.cpp): the course gfx set-up. The hook runs it and tells the Mem_Alloc hook that the two display-list
# requests come from it, so with dd_buffers = bigger they get at least 144 KiB each (the front end's own set-up is not hooked).
[[patches.hook]]
func = "Gfx_SetupCourse"
text = "if (thps_gfx_setup_course(rdram, ctx)) return;"

# Draw distance "full" (game/draw_distance.cpp): World_SetVisibilityInBox is the trigger scripts' zone culling (hide / show every item in a
# box as the skater moves). The hook runs it as is and notes which hidden bits it set on which items, so "full" unhides only those and
# leaves items hidden by other commands (secrets, things revealed by events) alone. Read-only for the game.
[[patches.hook]]
func = "World_SetVisibilityInBox"
text = "if (thps_world_set_visibility_in_box(rdram, ctx)) return;"

# Draw distance "full" (game/draw_distance.cpp): World_FindItem(list, name) is how anything addresses one course item by name (the scripts'
# show / hide / kill commands, goals, gaps). The hook runs it as is and marks the item it returns as event-owned: "full" never unhides it.
[[patches.hook]]
func = "World_FindItem"
text = "if (thps_world_find_item(rdram, ctx)) return;"
# Music (src/music.cpp). Music_LoadSong(player, song) copies the next song over the one still playing; the old song's
# channels, only marked to stop, would run one more frame of events from the new bytes (THPS2: SIGBUS in n_alLoadParam
# at the end of a run). The hook stops them without further events first. THPS_MUSIC_GUARD=0 turns it off (A/B).
[[patches.hook]]
func = "Music_LoadSong"
text = "thps_music_load_song(rdram, ctx);"

# libc memset / memcpy, done natively (src/hooks.cpp; the level-load freeze, v0.2): the recompiled byte loops were ~50 ms of
# each THPS1 level start (Heap_Free clears every freed block). Same bytes, same return value; a range outside RDRAM runs the original.
[[patches.hook]]
func = "memset_recomp" # N64Recomp adds _recomp to names that clash with C library functions
text = "if (thps_memset(rdram, ctx)) return;"

[[patches.hook]]
func = "memcpy_recomp"
text = "if (thps_memcpy(rdram, ctx)) return;"
Loading
Loading