From d73b030ba1fb79721d1ea6314744637c559be33b Mon Sep 17 00:00:00 2001 From: trentw <532672+trentw@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:07:18 -0700 Subject: [PATCH 1/9] Move to THPS64Host v0.2.0 - The THPS64Host submodule, and the CI and release workflows (`@v0.2.0`, `host-tag: v0.2.0`), move to v0.2.0. - CHANGELOG: built on THPS64Host v0.2.0. Made with AI assistance. --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- CHANGELOG.md | 3 +++ decomp/common | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5efd0ac..5c21bf6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 115af5b..9f80c62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 268ee1a..7c204bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based ## [Unreleased] +### Changed +- Built on THPS64Host v0.2.0. + ## [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 diff --git a/decomp/common b/decomp/common index 080d380..0792b98 160000 --- a/decomp/common +++ b/decomp/common @@ -1 +1 @@ -Subproject commit 080d380d5250160f527e11a11b84e6a7da7145bc +Subproject commit 0792b989655155808a9730d0dae1b80588daaaab From 934fdd3bb03b1dc2e1f64c31d338b2bea667de51 Mon Sep 17 00:00:00 2001 From: trentw <532672+trentw@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:10:16 -0700 Subject: [PATCH 2/9] Remove the 21:9 far-wall known issue - Removes "at 21:9, a far wall can drop out at the right edge" from KNOWN-ISSUES. 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. Made with AI assistance. --- CHANGELOG.md | 2 ++ docs/KNOWN-ISSUES.md | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c204bc..41ae7c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based ### Changed - Built on THPS64Host v0.2.0. +- 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. ## [0.1.0-preview] - 2026-10-18 diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md index 87f9c27..1882c97 100644 --- a/docs/KNOWN-ISSUES.md +++ b/docs/KNOWN-ISSUES.md @@ -43,7 +43,6 @@ been played through by a person on this build: the park editor, split screen in - **Widescreen:** the 3D view is widened and the in-level HUD moves to the screen edges (`hud = expand`; `original` keeps it in the centred 4:3 area). The HUD keeps its 4:3 size, and a few elements (the timer, trick lines, the pause menu, result pages) stay centred on purpose. Menus, loading screens and other 2D screens are pillarboxed 4:3. -- At **21:9**, a far wall can drop out at the right edge of the picture. - **Frame interpolation** at 60 Hz and up pairs 97 % or more of the moving parts between game frames in the Hangar (the first career level) and the menus. Other levels have cars and other moving objects whose pairing hasn't been checked. Replays and cutscenes are untested. From e787d7bfeed2d741a98f464040d51452c67beab4 Mon Sep 17 00:00:00 2001 From: trentw <532672+trentw@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:10:16 -0700 Subject: [PATCH 3/9] Make two-player split screen fill the picture in widescreen - Uses the matching change in THPS64Host v0.2.0. - New sessions `two-players-169`, `two-players-219` and `two-players-169-hudorig`: a TRICK ATTACK game with two pads; the sides of the picture are drawn, the HUDs are at the outer edges, each pad controls its own player. - Docs: MANUAL, KNOWN-ISSUES, CHANGELOG, THIRD_PARTY; the README's known issue "split screen stays 4:3" is gone. - New session `two-players-169-splitorig`: at split Original each player's HUD stays with its view and the side bands are black. Made with AI assistance. --- CHANGELOG.md | 4 ++ README.md | 1 - decomp/thps2/recomp-host/tests/scenarios.py | 67 ++++++++++++++++----- docs/KNOWN-ISSUES.md | 3 +- docs/MANUAL.md | 5 +- docs/THIRD_PARTY.md | 2 +- 6 files changed, 61 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ae7c4..e03b8e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based - Built on THPS64Host v0.2.0. - 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. ## [0.1.0-preview] - 2026-10-18 diff --git a/README.md b/README.md index ad33a86..1c7ccf9 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,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 diff --git a/decomp/thps2/recomp-host/tests/scenarios.py b/decomp/thps2/recomp-host/tests/scenarios.py index abe45f9..de7cf3f 100644 --- a/decomp/thps2/recomp-host/tests/scenarios.py +++ b/decomp/thps2/recomp-host/tests/scenarios.py @@ -174,6 +174,28 @@ ("ctl", "vpad axis leftx " + ("-32000" if k % 2 else "32000")), ("ctl", "vpad button x 1"), ("wait", 6), ("ctl", "vpad button x 0"), ("wait", 14), ("ctl", "vpad axis leftx 0")]] +# 2 PLAYER > TRICK ATTACK with two virtual pads (pad 1 = port 1, pad 2 = port 2) up to the split-screen run; then each pad reaches its own port. +_TWO_PADS_TO_LEVEL = [ + *BOOT_TO_MENU, + ("vpad", "attach"), ("vpad2", "attach"), + ("vpress", "leftshoulder"), ("vpress2", "leftshoulder"), # pad 1 = port 1, pad 2 = port 2 + ("log_n", r"Virtual Controller takes port 2", 1), + ("wait", 90), *[("vpress", "dpright"), ("wait", 40)] * 3, + *[x for _ in range(4) for x in (("vpress", "a"), ("wait", 150))], # the wheel's 2 PLAYER, TRICK ATTACK, PLAYER 1 select, its CONTINUE + *[x for _ in range(2) for x in (("vpress2", "a"), ("wait", 150))], # PLAYER 2 select, its CONTINUE: port 2 (pad 1's A does nothing there) + ("vpress", "a"), ("wait", 60), ("vpress2", "a"), ("wait", 150), # HANDICAP: each player accepts his own ("WAITING FOR OTHER PLAYER") + *[x for _ in range(3) for x in (("vpress", "a"), ("wait", 150))], # LEVEL SELECT, rules, SELECT TIME + ("wait_peek", "@gRunActive", 1, 60), +] +_TWO_PADS_PORTS = [ + ("ctl", "vpad2 button leftshoulder 1"), ("wait", 8), + ("ctl_match", "inputstate", r"last=0x0000 last2=0x0020$"), # pad 2 reaches port 2 only + ("ctl", "vpad2 button leftshoulder 0"), + ("ctl", "vpad button rightshoulder 1"), ("wait", 8), + ("ctl_match", "inputstate", r"last=0x0010 last2=0x0000$"), # pad 1 port 1 only + ("ctl", "vpad button rightshoulder 0"), +] + SESSIONS = [ dict(name="frontend", config="suite-config", fresh_pak=True, wav=True, scenarios=[ ("boot_title", "boot: Expansion Pak notice, legal text, then the logo/title screen (overlay t2_intro)", [ @@ -395,22 +417,8 @@ ]), dict(name="two-players-pads", config="two-players-pads-config", fresh_pak=True, wav=False, args=["--skip-intro", "--hidden"], scenarios=[ ("two_p_two_pads", "TWO pads: each picks its own skater, the split-screen level loads, each pad reaches its own port", [ - *BOOT_TO_MENU, - ("vpad", "attach"), ("vpad2", "attach"), - ("vpress", "leftshoulder"), ("vpress2", "leftshoulder"), # pad 1 = port 1, pad 2 = port 2 - ("log_n", r"Virtual Controller takes port 2", 1), - ("wait", 90), *[("vpress", "dpright"), ("wait", 40)] * 3, - *[x for _ in range(4) for x in (("vpress", "a"), ("wait", 150))], # the wheel's 2 PLAYER, TRICK ATTACK, PLAYER 1 select, its CONTINUE - *[x for _ in range(2) for x in (("vpress2", "a"), ("wait", 150))], # PLAYER 2 select, its CONTINUE: port 2 (pad 1's A does nothing there) - ("vpress", "a"), ("wait", 60), ("vpress2", "a"), ("wait", 150), # HANDICAP: each player accepts his own ("WAITING FOR OTHER PLAYER") - *[x for _ in range(3) for x in (("vpress", "a"), ("wait", 150))], # LEVEL SELECT, rules, SELECT TIME - ("wait_peek", "@gRunActive", 1, 60), - ("ctl", "vpad2 button leftshoulder 1"), ("wait", 8), - ("ctl_match", "inputstate", r"last=0x0000 last2=0x0020$"), # pad 2 reaches port 2 only - ("ctl", "vpad2 button leftshoulder 0"), - ("ctl", "vpad button rightshoulder 1"), ("wait", 8), - ("ctl_match", "inputstate", r"last=0x0010 last2=0x0000$"), # pad 1 port 1 only - ("ctl", "vpad button rightshoulder 0"), + *_TWO_PADS_TO_LEVEL, + *_TWO_PADS_PORTS, ]), ]), # QOL features: Skip Intro, Show FPS, remappable bindings with a virtual controller, @@ -700,3 +708,30 @@ [("press", "START"), ("wait", 40), ("menu", "pause", 3), ("press", "A"), ("wait", 120), ("press", "A"), ("wait", 20), ("press", "DOWN"), ("wait", 20), ("press", "DOWN"), ("wait", 20), ("press", "A"), ("wait", 150)], )] + +# Two players in widescreen (v0.2, rt64-splitscreen-widescreen.patch + hud_anchor.cpp): the side-by-side split screen fills the wide picture, +# each view widened like the one-player camera (`split` views > 0), each player's HUD block at its own view's edge (the game mirrors player 2's +# HUD to the right of its view: groups at q0 and q4), both pads on their own ports. present_split: in each side band outside the central 4:3 +# area >= 90 % of the columns are drawn. Then `split off` (live) for a before picture. `169-hudorig`: HUD Placement `original` places the +# split screen's HUD the same way (the views fill the picture; the 4:3 spot would leave each HUD inside its view), so the same groups are expected. +SESSIONS += [ + dict(name=f"two-players-{var}", config=f"two-players-{var}-config", fresh_pak=True, wav=False, present_every=10, + args=["--skip-intro", "--hidden", "--aspect", asp, "--window", win, "--res-scale", "2", *extra], scenarios=[ + (f"two_players_{var}", f"two players at {asp}{' (hud original)' if extra else ''}: the split screen fills the picture, each HUD at its view's edge, each pad on its port", [ + *_TWO_PADS_TO_LEVEL, + ("wait", 60), + ("present_split", win, 0.90), + ("keep_present", f"two_players_{var}_after"), + ("ctl_match", "split", r"wide=1 views=[1-9]\d* .* hud_tasks=[1-9]\d* groups q0=[1-9]\d* .* q4=[1-9]\d* "), + ("ctl_match", "hud", r"mode=%s " % ("original" if extra else "expand")), + *_TWO_PADS_PORTS, + ("ctl", "split off"), ("wait", 20), ("keep_present", f"two_players_{var}_before"), + ]), + ]) + for (var, asp, win, extra) in (("169", "16:9", "1280x720", []), ("219", "21:9", "1680x720", []), + ("169-hudorig", "16:9", "1280x720", ["--hud", "original"])) +] + +# Two players with split original (hud expand): each player's HUD stays with its view in the 4:3 middle (hud_sessions.two_players_split_original). +from hud_sessions import two_players_split_original # noqa: E402 +SESSIONS.append(two_players_split_original(_TWO_PADS_TO_LEVEL)) \ No newline at end of file diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md index 1882c97..be90bab 100644 --- a/docs/KNOWN-ISSUES.md +++ b/docs/KNOWN-ISSUES.md @@ -42,7 +42,8 @@ been played through by a person on this build: the park editor, split screen in - **Widescreen:** the 3D view is widened and the in-level HUD moves to the screen edges (`hud = expand`; `original` keeps it in the centred 4:3 area). The HUD keeps its 4:3 size, and a few elements (the timer, trick lines, the pause menu, - result pages) stay centred on purpose. Menus, loading screens and other 2D screens are pillarboxed 4:3. + result pages) stay centred on purpose. Menus, loading screens and other 2D screens are pillarboxed 4:3. In **two-player** + split screen each player's view fills its half of the wide picture, and each player's HUD sits at its own view's edge. - **Frame interpolation** at 60 Hz and up pairs 97 % or more of the moving parts between game frames in the Hangar (the first career level) and the menus. Other levels have cars and other moving objects whose pairing hasn't been checked. Replays and cutscenes are untested. diff --git a/docs/MANUAL.md b/docs/MANUAL.md index da3c5d8..2e50089 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -111,7 +111,7 @@ manual: down then up). **Esc** (controller: **Back**) opens the settings menu over the game. Its tabs: - **General:** skip intro, show FPS; - **Controls:** the controller layout; -- **Graphics:** aspect ratio, HUD placement, resolution, MSAA, frame rate, window mode; +- **Graphics:** aspect ratio, HUD placement, two-player split screen, resolution, MSAA, frame rate, window mode; - **Sound:** volume; - **Cheats:** see [Cheats](#cheats). @@ -142,7 +142,8 @@ A command-line flag overrides the value and **is saved back**. | `res_scale` | `0` | internal resolution multiplier; `0` is an integer scale of the window (3x at 720p) | `--res-scale N` | | `msaa` | `4` | `0`, `2`, `4` (Apple GPUs have no 8x; asking for it turns MSAA off) | `--msaa N` | | `refresh` | `display` | `original` (the game's 30 fps), `display` (the display's rate: 60, or 120 on ProMotion), or a number from 30 to 1000 | `--refresh X` | -| `hud` | `expand` | HUD placement in widescreen: `original` (the centred 4:3 area, as on a TV), `16:9` (the edges of a 16:9 picture), `expand` (the edges of the window's picture). No effect at `aspect original`. Menus, the pause menu and result pages always stay a centred 4:3 page | `--hud X` | +| `hud` | `expand` | HUD placement in widescreen: `original` (the centred 4:3 area, as on a TV), `16:9` (the edges of a 16:9 picture), `expand` (the edges of the window's picture). No effect at `aspect original`. Menus, the pause menu and result pages always stay a centred 4:3 page. In a widened two-player split screen each player's HUD sits at the edge of their own view, with `original` too | `--hud X` | +| `split` | `wide` | two-player split screen in widescreen: `wide` each player's view fills its half of the picture; `original` both views stay in the centred 4:3 area with black bands at the sides, as on a TV, and each player's HUD stays inside its view. No effect at `aspect original` | `--split X` | The file also holds a `cfg_rev` line that the app writes itself; leave it. Files from earlier builds that say `hud=original` (the old default) are moved to `expand` once; choose `original` again to keep it. diff --git a/docs/THIRD_PARTY.md b/docs/THIRD_PARTY.md index f7f813d..ab52038 100644 --- a/docs/THIRD_PARTY.md +++ b/docs/THIRD_PARTY.md @@ -18,7 +18,7 @@ frameworks), the symbols of the statically linked components, and the file list | Component | Pin | Licence | Notes | |---|---|---|---| | N64ModernRuntime (librecomp, ultramodern) | `cdf5abb` | **GPL-3.0** | linked; one patch of ours applied (`ultramodern-sp-complete-after-dl.patch`: the game is told "RSP done" after the display list is parsed, not before; a few lines in `events.cpp`; it fixes a crash in RT64's display-list parser). A second patch, `ultramodern-vi-rate.patch` (real 50 Hz PAL timing), is in the repository but **not applied** to any release build. Its vector-unit code (`rsp_vu_impl.hpp`) is adapted from the Ares emulator (ISC licence, notice in the file). Its `thirdparty/`: concurrentqueue (BSD-2-Clause, with a separate Boost-licensed semaphore header), nlohmann json (MIT), miniz (MIT), o1heap (MIT), sse2neon (MIT), xxHash (BSD-2-Clause) | -| RT64 | `4337374` (+ plume `d890ac8`) | MIT | linked; four patches of ours (`rt64-hlslpp-labs.patch`, `rt64-present-capture-widescreen.patch`, `rt64-hfr.patch`, `rt64-metal-clear-state.patch`; the first is a one-line include fix, the last a Metal-only state-cache fix) | +| RT64 | `4337374` (+ plume `d890ac8`) | MIT | linked; five patches of ours (`rt64-hlslpp-labs.patch`, `rt64-present-capture-widescreen.patch`, `rt64-hfr.patch`, `rt64-metal-clear-state.patch`, `rt64-splitscreen-widescreen.patch`; the first is a one-line include fix, the fourth a Metal-only state-cache fix) | | RT64 `src/contrib/` | as RT64 | per directory: hlslpp, ddspp, plume, re-spirv, Dear ImGui, implot, im3d (MIT); nativefiledialog-extended (zlib); SPIRV-Cross (Apache-2.0; a build tool on macOS: its output, the Metal shaders, is compiled into the engine); stb (MIT or public domain); xxHash (BSD-2-Clause); zstd (BSD-3-Clause, dual with GPL-2.0; used under BSD); **mupen64plus-core API headers (GPL-2.0-or-later; compatible with GPL-3.0)**; Microsoft DirectX Shader Compiler (a build tool: RT64's copy is used at build time and is not in the app; the Windows zip ships `dxcompiler.dll` (the LLVM Release License, University of Illinois/NCSA) and `dxil.dll` (Microsoft Software License Terms for the DirectX Shader Compiler, whose "Distributable Code" section sets the terms for distributing it), both **from Microsoft's DXC release v1.8.2407** (`dxc_2024_07_31.zip`, SHA-256 `e2627f00...67b5`; downloaded at build time, not RT64's v1.7.2212 copies), with both texts from that release, unchanged, in `resources/licenses/DXC-*.txt`) | the exact list is generated at release time from the pin | | SDL2 2.32.10 | built from the upstream release tarball (SHA-256 pinned in `decomp/common/host/tools/build_sdl2.py`); bundled as `libSDL2-2.0.0.dylib` in the macOS app and as `SDL2.dll` in the Windows zip | zlib | the only non-system library the macOS engine depends on | | SDL's built-in controller mappings (SDL_GameControllerDB) | compiled into SDL2 | zlib | no separate `gamecontrollerdb.txt` is shipped | From d9f516c2e812265d793ccb39690555576c355d10 Mon Sep 17 00:00:00 2001 From: trentw <532672+trentw@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:10:16 -0700 Subject: [PATCH 4/9] Add a Picture Border setting that removes the black frame - Uses the matching change in THPS64Host v0.2.0. - The crop sessions: `crop` (16:9, also switches the menu row back and forth), `crop-219`, `crop-43`, and `border-default` (the default keeps the frame and changes nothing). - New session `two-players-169-crop`: two players at 16:9, paused. Both views reach the window's edges, and switching the border back to original brings the black frame back and changes nothing else. - MANUAL and CHANGELOG. - The setting's values are `original` and `fill` (Graphics > Picture Border: Fill). Made with AI assistance. --- CHANGELOG.md | 6 ++++++ decomp/thps2/recomp-host/tests/scenarios.py | 8 +++++++- docs/MANUAL.md | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e03b8e6..122518a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ 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. + ### Changed - Built on THPS64Host v0.2.0. - Removed the known issue "at 21:9, a far wall can drop out at the right edge". It was reported in THPS1, where it diff --git a/decomp/thps2/recomp-host/tests/scenarios.py b/decomp/thps2/recomp-host/tests/scenarios.py index de7cf3f..af2bc18 100644 --- a/decomp/thps2/recomp-host/tests/scenarios.py +++ b/decomp/thps2/recomp-host/tests/scenarios.py @@ -734,4 +734,10 @@ # Two players with split original (hud expand): each player's HUD stays with its view in the 4:3 middle (hud_sessions.two_players_split_original). from hud_sessions import two_players_split_original # noqa: E402 -SESSIONS.append(two_players_split_original(_TWO_PADS_TO_LEVEL)) \ No newline at end of file +SESSIONS.append(two_players_split_original(_TWO_PADS_TO_LEVEL)) +# Border crop (graphics setting `border`, src/border_crop.cpp): the shared tests/crop_sessions.py. +from crop_sessions import crop_sessions # noqa: E402 +SESSIONS += crop_sessions([*CAREER_TO_GOALS, ("go", "A", "goals", "in_level", 90)], boot=BOOT_TO_MENU, pause=[("go", "START", "in_level", "pause")]) +# Border crop in a two-player split screen at 16:9: each view's own inset scissor goes to the frame's edges too (crop_sessions.crop_two_players). +from crop_sessions import crop_two_players # noqa: E402 +SESSIONS.append(crop_two_players(_TWO_PADS_TO_LEVEL, [("ctl", "vpad button start 1"), ("wait", 10), ("ctl", "vpad button start 0"), ("wait", 45)])) diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 2e50089..51e8bab 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -111,7 +111,7 @@ manual: down then up). **Esc** (controller: **Back**) opens the settings menu over the game. Its tabs: - **General:** skip intro, show FPS; - **Controls:** the controller layout; -- **Graphics:** aspect ratio, HUD placement, two-player split screen, resolution, MSAA, frame rate, window mode; +- **Graphics:** aspect ratio, HUD placement, two-player split screen, picture border, resolution, MSAA, frame rate, window mode; - **Sound:** volume; - **Cheats:** see [Cheats](#cheats). @@ -144,6 +144,7 @@ A command-line flag overrides the value and **is saved back**. | `refresh` | `display` | `original` (the game's 30 fps), `display` (the display's rate: 60, or 120 on ProMotion), or a number from 30 to 1000 | `--refresh X` | | `hud` | `expand` | HUD placement in widescreen: `original` (the centred 4:3 area, as on a TV), `16:9` (the edges of a 16:9 picture), `expand` (the edges of the window's picture). No effect at `aspect original`. Menus, the pause menu and result pages always stay a centred 4:3 page. In a widened two-player split screen each player's HUD sits at the edge of their own view, with `original` too | `--hud X` | | `split` | `wide` | two-player split screen in widescreen: `wide` each player's view fills its half of the picture; `original` both views stay in the centred 4:3 area with black bands at the sides, as on a TV, and each player's HUD stays inside its view. No effect at `aspect original` | `--split X` | +| `border` | `original` | the thin black frame around the 3D picture. The game draws its 3D 8 pixels in from each edge of its 320x240 frame, and a TV hid that edge. `original` keeps the frame; `fill` draws the picture all the way to the edges of the window, at every aspect ratio. Fill shows a little more of the scene at the same size: nothing is cut off and nothing is zoomed. Menus and loading screens keep their own frame | `--border X` | The file also holds a `cfg_rev` line that the app writes itself; leave it. Files from earlier builds that say `hud=original` (the old default) are moved to `expand` once; choose `original` again to keep it. From 2307669f9fe692dfe30a4841d892fae6cfd7b8f0 Mon Sep 17 00:00:00 2001 From: trentw <532672+trentw@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:10:16 -0700 Subject: [PATCH 5/9] Document rebinding and the Controllers list - MANUAL: how to rebind, how trades and Default work, the presets, editing `controls.cfg`, and the Controllers list. - KNOWN-ISSUES and the README: rebinding and player assignment are no longer listed as missing. - CHANGELOG (the feature and the fix), the test README and the Windows checklist. - MANUAL: changed bindings are marked in yellow, and Reset all to default puts every binding back to the layout; `menu-controls` checks both. - Docs and tests: Default on one row after a swap (`menu-controls-row`), and a customised N64 layout keeps its base after a restart (`menu-controls-base`). Made with AI assistance. --- CHANGELOG.md | 12 +++++ README.md | 3 +- decomp/thps2/recomp-host/README.md | 5 +- decomp/thps2/recomp-host/docs/WINDOWS-TEST.md | 2 +- docs/KNOWN-ISSUES.md | 9 ++-- docs/MANUAL.md | 50 +++++++++++++------ 6 files changed, 57 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 122518a..dc4473d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based 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). ### Changed - Built on THPS64Host v0.2.0. @@ -22,6 +30,10 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based 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. +### 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 diff --git a/README.md b/README.md index 1c7ccf9..35e1901 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ the Cheats tab gets you to every level quickly. - **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. diff --git a/decomp/thps2/recomp-host/README.md b/decomp/thps2/recomp-host/README.md index cc66b7d..de73bf7 100644 --- a/decomp/thps2/recomp-host/README.md +++ b/decomp/thps2/recomp-host/README.md @@ -170,7 +170,8 @@ tests/run.py --version thps2_eu # the suite on Europe (its references live in bu `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`). 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 @@ -191,4 +192,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`). diff --git a/decomp/thps2/recomp-host/docs/WINDOWS-TEST.md b/decomp/thps2/recomp-host/docs/WINDOWS-TEST.md index 345414f..256d7d8 100644 --- a/decomp/thps2/recomp-host/docs/WINDOWS-TEST.md +++ b/decomp/thps2/recomp-host/docs/WINDOWS-TEST.md @@ -84,7 +84,7 @@ Run from a terminal (PowerShell) in `build\cmake`: - [ ] Save a Controller Pak file (career), quit, relaunch, load it. ### 4.3 Controls (10 min) -- [ ] Keyboard: defaults work, all tricks; rebind a key in `controls.cfg` and restart. +- [ ] Keyboard: defaults work, all tricks; rebind a key in the settings menu's Controls tab (and one in `controls.cfg`, then restart). - [ ] Xbox pad: sticks, triggers, d-pad; unplug and replug mid-game; rumble with `--rumble-pak`. - [ ] Two pads: port assignment by first press; a Steam Input / DS4Windows setup if you have one. - [ ] Input latency feels the same as on the Mac. diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md index be90bab..e7d983d 100644 --- a/docs/KNOWN-ISSUES.md +++ b/docs/KNOWN-ISSUES.md @@ -60,11 +60,10 @@ been played through by a person on this build: the park editor, split screen in (`course-cycle`), the path of the crash fixed in this release. ## Input and saves -- **Player 2:** there's no screen for assigning controllers. The first controller to press a button is player 1 and the - next is player 2; the keyboard is always player 1, so two players need two controllers ([MANUAL](MANUAL.md#controls)). +- **Player 2:** by default the first controller to press a button is player 1 and the next is player 2. The Controls + tab's Controllers list can swap them or give a controller either player; that choice lasts until you quit + ([MANUAL](MANUAL.md#controls)). The keyboard is always player 1. - OPTIONS > CONTROLLER 2 opens only for the second controller's A, as in the original game. -- **Single keys and buttons** can't be rebound in the settings menu yet; it picks a layout. Edit `controls.cfg` for the - rest. - **Cheats are test aids:** off by default. They aren't stored in the Controller Pak save, but progress you make with a cheat on (goals, scores, levels unlocked by playing) is saved as usual. While one is on, the log and `crash.log` name it ([MANUAL](MANUAL.md#cheats)). @@ -82,4 +81,4 @@ been played through by a person on this build: the park editor, split screen in ## Not in this release -Texture packs, mods, rebinding single keys in the menu, and a launcher beyond the first-start window. +Texture packs, mods, and a launcher beyond the first-start window. diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 51e8bab..6ce9347 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -83,25 +83,46 @@ direction for a grab, and Y near a rail for a grind. Manuals need no button: tap manual: down then up). - **B is grab while you skate, and back in menus.** Since the game reads N64 B as flip while you skate, sending "grab and B" from one button would flip and grab at once. So the app drops that B while you skate in a level, and drops the grab on - every other screen (menus, Create-a-Skater, the pause menu and the screens over a level). + every other screen (menus, Create-a-Skater, the pause menu and the screens over a level). This applies whenever one + button or key is bound to both N64 B and C-right, as the default layout's B is, also after you rebind other actions. - **The game's own controller options** (OPTIONS > CONTROLLER 1) only swap L and R and set autokick and rumble. They can't remap the trick buttons. -- **Presets.** The settings menu's Controls tab picks one. `--controls-preset n64` switches to the plain N64 layout (A - south, B west, the C buttons on the right stick, Y, B, RB and RS), and `--controls-preset thps-xbox` switches back. A - preset replaces every binding and is saved; the previous file is kept as `controls.cfg.bak`. -- **Remapping.** Edit `controls.cfg`. Each line is `INPUT = FIELD, FIELD ...`, with up to four fields per input. +- **Rebinding.** Open the settings menu (Esc, or Back on a controller) and its Controls tab. Under **Bindings**, each + N64 input has a row with two controller slots and two keyboard slots, and the row names what the game does with it. + Choose a slot (click it, or move to it and press Enter or A), then press the button, trigger or stick direction for a + controller slot, or the key for a keyboard slot. Esc or Back cancels, Backspace or Delete empties the slot, and after + 5 seconds the slot gives up and stays as it was. + - The change works at once and is saved to `controls.cfg` with `preset = custom`. The first change in a session keeps + the file it replaces as `controls.cfg.bak`. + - If another action already uses that button, the two trade: the other action gets the slot's old button. Menu + accept and menu back (N64 A and B) may share a button with a trick, as the default layout does, so they keep theirs. + - **Default** on a row puts that row back as the layout has it. + - A slot shown in yellow differs from the layout you chose (Face buttons or N64), so you can see what you changed. + - **Reset all to default**, below the rows, puts every binding back to that layout. It asks first: press it again + (it now says **Confirm: reset all**) to do it; anything else cancels. The old file is kept as `controls.cfg.bak`. +- **Presets.** Controller Layout, at the top of the Controls tab, resets every binding to a layout: **Face buttons** + (`thps-xbox`, the default) or **N64 layout** (`n64`: A south, B west, the C buttons on the right stick, Y, B, RB and + RS). `--controls-preset n64` and `--controls-preset thps-xbox` do the same from the command line. The previous file is + kept as `controls.cfg.bak`. +- **Editing `controls.cfg`.** The app reads it at start. Each line is `INPUT = FIELD, FIELD ...`, with up to four fields + per input. - Inputs: `a b z l r start c_up c_down c_left c_right dpad_up dpad_down dpad_left dpad_right stick_left stick_right stick_up stick_down`. - Fields: `key:NAME` (an SDL key name such as `Space`, `Return`, `Left`, `A`; the keyboard counts on port 1 only), `button:NAME` (`a b x y back start leftstick rightstick leftshoulder rightshoulder dpup dpdown dpleft dpright`), `axis:NAME+` or `axis:NAME-` (`leftx lefty rightx righty lefttrigger righttrigger`), or `none`. - - The `preset =` line says `thps-xbox`, `n64` or `custom`. A file you edited by hand is never overwritten. -- **Players 1 and 2.** Ports are given out at each start by the first button press. Player 1 has the keyboard and the - first controller that presses a button; player 2 is the next controller that presses one; a third is ignored. Moving a - stick doesn't claim a port. Plug in or pair a controller at any time, then press a button on it. If player 1's - controller is unplugged, player 2's becomes player 1. The keyboard always belongs to player 1, so **two players need two - controllers**. There's no screen yet for choosing who is who: press a button first on the controller that should be - player 1. + - The `preset =` line says `thps-xbox`, `n64` or `custom`. The app never rewrites a file you edited by hand on its + own; a change in the menu does, after keeping the old file as `controls.cfg.bak`. +- **Players 1 and 2.** By default, ports are given out by the first button press. Player 1 has the keyboard and the first + controller that presses a button; player 2 is the next controller that presses one; a third is ignored. Moving a stick + doesn't claim a port. Plug in or pair a controller at any time, then press a button on it. If player 1's controller is + unplugged, player 2's becomes player 1. +- **The Controllers list** (Controls tab) shows each connected controller, which player it is and the buttons it's + pressing right now, so you can tell two controllers apart. **Player 1** or **Player 2** on a controller's row gives it + that player (a controller already there trades places with it), and **Swap players 1 and 2** trades them. The + keyboard always belongs to player 1. With one controller set to Player 2, the keyboard alone is player 1, so the + keyboard and one controller can play a two-player game. The choice lasts until you quit; at the next start, the first + press decides again. - **With one controller**, the 2 PLAYER screens that wait for player 2 (choose skater, continue, handicap) take player 1's input, so they can't lock up. In a level, player 2 stays empty. OPTIONS > CONTROLLER 2 opens only for the second controller's A, as in the original game. @@ -110,14 +131,13 @@ manual: down then up). **Esc** (controller: **Back**) opens the settings menu over the game. Its tabs: - **General:** skip intro, show FPS; -- **Controls:** the controller layout; +- **Controls:** the layout presets, the Controllers list and rebinding (see [Controls](#controls)); - **Graphics:** aspect ratio, HUD placement, two-player split screen, picture border, resolution, MSAA, frame rate, window mode; - **Sound:** volume; - **Cheats:** see [Cheats](#cheats). Move with the arrow keys or the D-pad, pick with Enter or A, change tab with LB / RB, and close with Esc, B or Back. The -menu writes the same files as the command-line flags (below). It doesn't rebind single keys or buttons yet: edit -`controls.cfg` for that. Full screen: Alt+Enter on Windows; on macOS, the window's full-screen button or Ctrl+Cmd+F. +menu writes the same files as the command-line flags (below). Full screen: Alt+Enter on Windows; on macOS, the window's full-screen button or Ctrl+Cmd+F. ## Settings files From fdae57ac4d4a8d42202dab2012e913a47a0a2c34 Mon Sep 17 00:00:00 2001 From: trentw <532672+trentw@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:10:16 -0700 Subject: [PATCH 6/9] Shorten the freeze when a level starts - The hooks file sends the game's `memset` and `memcpy` to THPS64Host's native versions. They are named `memset_recomp` and `memcpy_recomp` there, because N64Recomp renames functions that share a name with the C library. - A new `lf-check` session in the suite times three course starts. `lf-probe` (opt-in) times all four in a visible window. - KNOWN-ISSUES and the CHANGELOG give the new times; the test README describes the sessions. Made with AI assistance. --- CHANGELOG.md | 2 ++ decomp/thps2/recomp-host/README.md | 4 ++++ decomp/thps2/recomp-host/config/thps2.hooks.toml | 10 ++++++++++ decomp/thps2/recomp-host/tests/scenarios.py | 16 ++++++++++++++++ docs/KNOWN-ISSUES.md | 4 ++-- 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc4473d..43e2fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based 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. ### Fixed - With a `controls.cfg` that matches no preset (edited by hand), Xbox B flipped and grabbed at once while skating. The diff --git a/decomp/thps2/recomp-host/README.md b/decomp/thps2/recomp-host/README.md index de73bf7..2add29b 100644 --- a/decomp/thps2/recomp-host/README.md +++ b/decomp/thps2/recomp-host/README.md @@ -173,6 +173,10 @@ front end, a career level, audio, controls, widescreen, Controller Pak, the sett 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 launcher-only scenarios. The split-built Europe library passed the full suite (26/0/4) before the menu scenarios were added, and diff --git a/decomp/thps2/recomp-host/config/thps2.hooks.toml b/decomp/thps2/recomp-host/config/thps2.hooks.toml index 6bb5f58..f1ccad1 100644 --- a/decomp/thps2/recomp-host/config/thps2.hooks.toml +++ b/decomp/thps2/recomp-host/config/thps2.hooks.toml @@ -108,3 +108,13 @@ text = "if (thps_cheat_is_on(rdram, ctx)) { ctx->r2 = 1; return; }" [[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;" diff --git a/decomp/thps2/recomp-host/tests/scenarios.py b/decomp/thps2/recomp-host/tests/scenarios.py index af2bc18..527798b 100644 --- a/decomp/thps2/recomp-host/tests/scenarios.py +++ b/decomp/thps2/recomp-host/tests/scenarios.py @@ -741,3 +741,19 @@ # Border crop in a two-player split screen at 16:9: each view's own inset scissor goes to the frame's edges too (crop_sessions.crop_two_players). from crop_sessions import crop_two_players # noqa: E402 SESSIONS.append(crop_two_players(_TWO_PADS_TO_LEVEL, [("ctl", "vpad button start 1"), ("wait", 10), ("ctl", "vpad button start 0"), ("wait", 45)])) +# Level-start freeze (v0.2 queue item 3, tests/load_freeze.py): the course cycle's four starts with frame timestamps over each load +# phase (A on the level select, A on the goals). `lf-probe` (optin) runs in a visible window; THPS_LF_SAMPLE=8 adds a `sample`. +from load_freeze import load_freeze_session # noqa: E402 +_LF = dict( + to_select=[*BOOT_TO_MENU, *CAREER_TO_GOALS[:-1]], + courses=[[], [("press", "RIGHT"), ("wait", 30)], [("press", "RIGHT"), ("wait", 30), ("press", "RIGHT"), ("wait", 30)], [("press", "RIGHT"), ("wait", 30)]], + phases=[("A", []), ("A", [("wait_peek", "@gRunActive", 1, 60)])], + end_run=[("press", "START"), ("wait", 40), ("menu", "pause", 3), ("press", "A"), ("wait", 120), ("press", "A"), ("wait", 20), + ("press", "DOWN"), ("wait", 20), ("press", "DOWN"), ("wait", 20), ("press", "A"), ("wait", 150)], +) +SESSIONS += [load_freeze_session("lf-probe", **_LF, hidden=False, optin=True, limits={"composited": 1000.0, "game": 1000.0})] +# The suite's check (hidden window, the first three courses): the median over the three starts of the longest gap between game +# frames while the course loads stays under LF_GAME_MS (one preempted window on a busy machine does not fail it; a warning only +# when the machine is overloaded). Before the v0.2 fixes (ERZ decoder, native memset / memcpy) every start was over it. +LF_GAME_MS = 140.0 # 2026-10-18: 104-120 ms after the fixes, 134-174 ms before (load 4-6) +SESSIONS += [load_freeze_session("lf-check", **{**_LF, "courses": _LF["courses"][:3]}, limits={"game": 1000.0}, median=("game", LF_GAME_MS))] diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md index e7d983d..830af9d 100644 --- a/docs/KNOWN-ISSUES.md +++ b/docs/KNOWN-ISSUES.md @@ -47,8 +47,8 @@ been played through by a person on this build: the park editor, split screen in - **Frame interpolation** at 60 Hz and up pairs 97 % or more of the moving parts between game frames in the Hangar (the first career level) and the menus. Other levels have cars and other moving objects whose pairing hasn't been checked. Replays and cutscenes are untested. -- **Each level start freezes the picture briefly** while the game loads the level, and the **attract demo** has short - stalls every ten seconds or so. (Measured at 100 to 150 ms in THPS1, which loads levels the same way.) +- **Each level start freezes the picture briefly** while the game loads the level (about 0.16 s, 0.2 s in the first + preview), and the **attract demo** has short stalls every ten seconds or so. - RT64 **compiles shaders the first time an effect appears**, which causes short hitches. - With the window hidden or covered, the app stops presenting frames; the game keeps running. - Apple GPUs have no 8x MSAA; asking for it turns MSAA off. From 0874a2ed7474dc9879d04410b30824ed20b92e85 Mon Sep 17 00:00:00 2001 From: trentw <532672+trentw@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:07:18 -0700 Subject: [PATCH 7/9] Add a draw distance setting - The game's side of the host's new Draw Distance setting, for USA and Europe. - Full leaves the game's hidden items to the game: scenery it hides by area as you skate, secrets and things a goal reveals appear when the game shows them. Unhiding the areas hidden by box was tried and is off: in the Hangar those boxes also swap the floor's light overlays and keep the crashed ceiling hidden until its event. `dd showall all` still unhides everything, for exploring. - Each course start gives the game room for 2048 matrices per frame instead of 420 (205 KB more), which Full needs in the busiest courses. - Five new hooks (`World_DrawItems`, `Mem_Alloc`, `World_SetVisibilityInBox`, `World_FindItem`, `Mesh_QueueEmit`) and their names in the symbol files and recipes. - Opt-in test sessions `dd-probe` and `dd-long` (not part of the suite). - Docs: the MANUAL, CHANGELOG, KNOWN-ISSUES. - The course items are drawn in passes. The renderer keeps the frame's meshes in two fixed lists and drops what doesn't fit, so at Full near items could vanish and the governor fell back to 1x in most courses. - The governor also watches those lists. - `dd-probe` gets vantages, traces and restart points, and a new opt-in session `dd-2p` checks two players. - The course display lists are 144 KiB instead of 72, and the extra passes are drawn only while there is room for the rest of the frame. At Full, School II could overrun the game's 72 KiB list and damage the heap (a crash in one run in three). - The bigger matrix pools and display lists are always set up, and only when the heap has room. A shorter distance applies at once; a longer one at the next RETRY, course or camera change. - A new test session `dd-stress` and the display-list probes. - Tests for the review fixes: `dd-heap-plan` (a constrained heap keeps the game's sizes and nothing fails), RETRY keeps the zone record honest, `dd_stale_feedback`, `dd-cut` (a forced budget drops far items, never near ones), and `dd-stress` runs with guard bytes on. Made with AI assistance. --- CHANGELOG.md | 7 + .../thps2/recomp-host/config/thps2.hooks.toml | 38 + .../thps2/recomp-host/game/draw_distance.cpp | 838 ++++++++++++++++++ decomp/thps2/recomp-host/game/game_hooks.def | 6 + .../recipes/eu/thps2.host.syms.toml | 16 +- .../recipes/us/thps2.host.syms.toml | 16 +- decomp/thps2/recomp-host/tests/scenarios.py | 326 +++++++ .../recomp-host/versions/eu/data_syms.toml | 12 + .../recomp-host/versions/eu/host_force.toml | 8 + .../thps2/recomp-host/versions/host_syms.toml | 17 +- .../recomp-host/versions/us/data_syms.toml | 12 + .../recomp-host/versions/us/host_force.toml | 8 + docs/KNOWN-ISSUES.md | 6 + docs/MANUAL.md | 5 +- 14 files changed, 1297 insertions(+), 18 deletions(-) create mode 100644 decomp/thps2/recomp-host/game/draw_distance.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 43e2fb2..b62559f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,13 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based 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. ### Changed - Built on THPS64Host v0.2.0. diff --git a/decomp/thps2/recomp-host/config/thps2.hooks.toml b/decomp/thps2/recomp-host/config/thps2.hooks.toml index f1ccad1..2753b0c 100644 --- a/decomp/thps2/recomp-host/config/thps2.hooks.toml +++ b/decomp/thps2/recomp-host/config/thps2.hooks.toml @@ -102,6 +102,44 @@ 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). diff --git a/decomp/thps2/recomp-host/game/draw_distance.cpp b/decomp/thps2/recomp-host/game/draw_distance.cpp new file mode 100644 index 0000000..e18e9b7 --- /dev/null +++ b/decomp/thps2/recomp-host/game/draw_distance.cpp @@ -0,0 +1,838 @@ +// THPS2's side of the extended draw distance (the setting and governor: common/host/src/draw_distance.cpp; THPS1's side, with the +// notes on the shared engine, is game/draw_distance.cpp in THPS1-64Recomp). +// +// The same engine as THPS1. The gameplay camera's far plane is one float, gFarPlane (US 0x800E4A80: the Hangar's is 407.75 = +// 6524 / 16), set with the level's fog (func_80023CFC) and handed to View_SetFrustum every frame (twice: near 10 and 10.5); +// the fog is the same constant gSPFogPosition(996, 1000). +// +// The buffers. Two top-level display lists of gDlBufSize bytes (0x12000; pointers gDlBufs) and two pools per frame, each with +// its size in a word next to its pointers: matrices (gMtxPools, gMtxCap = 420 x 64 bytes) and vertices (gVtxPools, gVtxCap = +// 500 x 16 bytes). Unlike THPS1's, these allocators clamp: past the cap they hand out the pool's last slots again (shared +// transforms / vertices for one frame, not memory damage). The display list has no cap. The game writes the list's length into +// the gfx task's data_size; pool use is read from the list's G_MTX / G_VTX commands that point into the task's pools. +// +// The renderer's lists (as in THPS1, which has the same mesh queue). Between a pass's begin (0x800C50B0) and end (0x800C50D0) the meshes of +// what is drawn are batched (16 instances of one model, flushed by 0x800C5A64) into two fixed lists of the renderer (gRenderer): the mesh +// queue, 512 entries of 16 bytes (+0x1CC, count +0x21CC: mesh, matrix, next, linked per material) and a depth-sorted list of translucent +// meshes, 96 entries of 24 bytes (+0x21D8, count +0x2AD8). The flush drops what does not fit, without a word (`li v0,512` / `li v0,96`). +// The pass end draws both (Mesh_QueueEmit) and empties them. The course items (World_DrawItems) are one pass, the scenery (the 20000 +// frustum) and the moving objects are passes of their own. Mesh_QueueEmit's hook notes both counts, read only, so they are buffers for the +// governor too. +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "draw_distance.h" +#include "recomp.h" +#include "thps_hooks.h" +#include "host.h" +#include "versions.h" + +namespace { + +uint32_t word(const uint8_t* rdram, uint32_t vaddr) { return *reinterpret_cast(rdram + (vaddr & 0x3FFFFFC)); } +float f32(const uint8_t* rdram, uint32_t vaddr) { + uint32_t w = word(rdram, vaddr); + float f; + std::memcpy(&f, &w, 4); + return f; +} + +constexpr uint32_t kQueueCount = 0x21CC, kQueueEntries = 512, kSortCount = 0x2AD8, kSortEntries = 96; +std::atomic g_runs_hw{0}, g_queue_hw{0}, g_sort_hw{0}; // most entries one pass used since the last gfx task (game thread writes, gfx thread takes) +void max_into(std::atomic& a, uint32_t v) { + uint32_t m = a.load(); + while (v > m && !a.compare_exchange_weak(m, v)) {} +} + +} // namespace + +// Mesh_QueueEmit(a0 = renderer) entry, game thread: the pass's lists are complete here (its end has just flushed the last batch). +extern "C" void thps_mesh_queue_emit(uint8_t* rdram, recomp_context* ctx) { + thps::dd::HookTimer timer; + const uint32_t r = uint32_t(ctx->r4); + if (r != word(rdram, thps::sym().gRenderer)) return; // not the renderer this was written for + max_into(g_queue_hw, std::min(word(rdram, r + kQueueCount), kQueueEntries)); + max_into(g_sort_hw, std::min(word(rdram, r + kSortCount), kSortEntries)); +} + +// The world view. Besides the gameplay camera's own frustum calls (every frame), the camera code keeps a second view (0x800DB290 in US) +// for the world: func_80023800 sets its perspective only when gFarPlane differs from the copy it cached (gWorldFarCache), i.e. once per +// level. A factor chosen after the level loaded therefore never reached it (the first measurements of this game, made that way, +// showed "no change"; with the setting from start-up the Hangar start draws more). So when the factor in use changes, the cached copy +// is cleared, and the camera code sets the world view's perspective again in the next frame, through View_SetFrustum and this hook. +bool thps::dd::supported() { return true; } +namespace thps::dd { void dd_game_sync_pools(uint8_t* rdram); } // the matrix pools' cap in force (below) + +// As THPS1: a call is the gameplay camera's (or the world view's) when its far is gFarPlane, or the far returned last; the answer is +// gFarPlane x k (k = 1 gives the game's value back). Game thread only. +float thps::dd::dd_game_far(uint8_t* rdram, float f, float k_in_use) { + // The far plane follows the setting (projection_factor: the setting, lowered only by a back-off), not the factor in use: the world view takes a + // new far only when the camera code sets it (a course start, camera changes), and the factor in use is applied to the extra items + // in thps_world_draw_items instead. + (void)k_in_use; + const float k = projection_factor(); + static float last_out = 0.0f, applied_k = 1.0f; + const auto& s = thps::sym(); + if (k != applied_k) { + applied_k = k; + *reinterpret_cast(rdram + (s.gWorldFarCache & 0x3FFFFFC)) = 0; // 0.0f: never a far plane, so the next frame refreshes the world view + } + dd_game_sync_pools(rdram); // the matrix pools' cap in force follows the setting (below) + const float game_far = f32(rdram, s.gFarPlane); + if (!(game_far > 0.0f) || (f != game_far && f != last_out)) return f; + note_game_far(game_far); + const float out = std::min(game_far * k, std::max(game_far, kFullFarPlane)); // full mode: as far as kFullFarPlane + last_out = out == game_far ? 0.0f : out; + note_effective(out / game_far); + return out; +} + +// Display-list measurements, debug only (off in normal runs; read once at start-up). THPS_DD_DLPROBE=1: the bytes each part of a frame +// writes into the display list (gDlHead - the frame's buffer): before World_DrawItems, each extra pass (with its pass end and begin), the +// game's own pass, and the rest of the frame after the extra passes (the game's pass, the scenery, the moving objects, the HUD, the other +// player's view), per frame; `dd dlprobe [reset]` reports their medians, 99th percentiles and maxima. THPS_DD_CANARY=1: each display +// list the course set-up allocates gets kGuard bytes of a pattern after its end, checked at every gfx task (a changed word = an overrun +// past gDlBufSize; counted and logged once per list; the pattern is written only at the allocation, so a guard can never damage memory +// that has a new owner since). +namespace { +bool probe_on() { static const bool on = std::getenv("THPS_DD_DLPROBE") != nullptr; return on; } +bool canary_on() { static const bool on = std::getenv("THPS_DD_CANARY") != nullptr; return on; } +constexpr uint32_t kGuard = 64, kGuardWord = 0xDDC0FFEEu; +void fill_guard(uint8_t* rdram, uint32_t buf, uint32_t size) { + for (uint32_t k = 0; k < kGuard; k += 4) *reinterpret_cast(rdram + ((buf + size + k) & 0x3FFFFFC)) = kGuardWord; +} +// Bytes the frame being built has written into its display list so far (three word reads). +uint32_t dl_used(const uint8_t* rdram) { + const auto& s = thps::sym(); + const uint32_t buf = word(rdram, s.gDlBufs + 4 * (word(rdram, s.gDlFrame) & 1)), head = word(rdram, s.gDlHead); + return head >= buf && head - buf < (64u << 20) ? head - buf : 0; +} +void probe_view(uint8_t* rdram, uint32_t player, uint32_t entry, uint32_t after_extras); +struct Samples { + std::vector v; + void add(uint32_t x) { if (v.size() < 500000) v.push_back(x); } + std::string str(const char* name) const { + if (v.empty()) return std::string(" ") + name + "=-"; + std::vector c = v; + std::sort(c.begin(), c.end()); + char b[96]; + std::snprintf(b, sizeof b, " %s=%u/%u/%u(n%zu)", name, c[c.size() / 2], c[std::min(c.size() - 1, c.size() * 99 / 100)], c.back(), c.size()); + return b; + } +}; +struct ProbeFrame { uint32_t after_extras[2] = {}, extra_bytes[2] = {}; uint8_t views = 0; }; +struct Probe { + Samples entry, pass, per_item, own, rest_1p, rest_2p_v0, rest_2p_v1, total_1p, total_2p, extras_1p; + uint32_t frames = 0, canary_bad = 0; + ProbeFrame frame[2]; // per buffer: the game thread fills it while it builds that frame, the gfx thread takes it at the task +}; +std::mutex g_probe_mutex; +Probe g_probe; +std::atomic g_direct_calls{0}; // World_DrawItems calls for the course items outside a pass (drawn at once, not in passes) +std::atomic g_dl_guarded[2] = {0, 0}; // THPS_DD_CANARY: the display lists that have a guard after them (their addresses) +std::atomic g_dl_guard_at[2] = {0, 0}; // and the size the guard follows (the front end's set-up reuses the addresses with 72 KiB lists) +} // namespace + +namespace { +// THPS_DD_DLPROBE, game thread: one World_DrawItems call (a view; player 2 = the second view of a two-player frame) wrote `entry` bytes +// before it, and its extra passes ended at `after_extras`. +void probe_view(uint8_t* rdram, uint32_t player, uint32_t entry, uint32_t after_extras) { + std::lock_guard lock{g_probe_mutex}; + ProbeFrame& f = g_probe.frame[word(rdram, thps::sym().gDlFrame) & 1]; + const uint32_t v = player ? 1 : 0; + f.after_extras[v] = after_extras, f.extra_bytes[v] = after_extras - std::min(after_extras, entry), f.views |= uint8_t(1u << v); + g_probe.entry.add(entry); +} +} // namespace + +std::string thps::dd::dd_game_dlprobe(uint8_t*, bool reset) { + std::lock_guard lock{g_probe_mutex}; + char b[160]; + std::snprintf(b, sizeof b, "probe %d canary %d frames %u canary_bad %u direct %u", int(probe_on()), int(canary_on()), g_probe.frames, g_probe.canary_bad, g_direct_calls.load()); + const Probe& p = g_probe; + std::string r = b + p.entry.str("entry") + p.pass.str("pass") + p.per_item.str("per_item") + p.own.str("own") + p.extras_1p.str("extras_1p") + + p.rest_1p.str("rest_1p") + p.total_1p.str("total_1p") + p.rest_2p_v0.str("rest_2p_v0") + p.rest_2p_v1.str("rest_2p_v1") + p.total_2p.str("total_2p"); + if (reset) { + const uint32_t bad = g_probe.canary_bad; + g_probe = Probe{}; + g_probe.canary_bad = bad; + } + return r; +} + +bool thps::dd::dd_game_measure(uint8_t* rdram, uint32_t data_ptr, uint32_t data_size, BufferUse& u) { + const auto& s = thps::sym(); + const uint32_t phys = data_ptr & 0x3FFFFFF; + int idx = -1; + for (int i = 0; i < 2; i++) + if ((word(rdram, s.gDlBufs + 4 * i) & 0x3FFFFFF) == phys) idx = i; + if (idx < 0) return false; + u.dl_cap = word(rdram, s.gDlBufSize); + u.dl_bytes = data_size; + u.mtx_cap = word(rdram, s.gMtxCap); + u.vtx_cap = word(rdram, s.gVtxCap); + if (u.mtx_cap > 4096 || u.vtx_cap > 65536) return false; // not set up yet + u.queue_used = g_queue_hw.exchange(0), u.queue_cap = kQueueEntries; + u.blend_used = g_sort_hw.exchange(0), u.blend_cap = kSortEntries; + u.runs = g_runs_hw.exchange(0); + const uint32_t mpool = word(rdram, s.gMtxPools + 4 * idx) & 0x3FFFFFF, vpool = word(rdram, s.gVtxPools + 4 * idx) & 0x3FFFFFF; + const uint32_t end = phys + std::min(data_size, u.dl_cap); + for (uint32_t pc = phys; pc + 8 <= end; pc += 8) { + const uint32_t w0 = word(rdram, pc), op = w0 >> 24; + if (op == 0xDA) { // G_MTX + const uint32_t m = word(rdram, pc + 4) & 0x3FFFFFF; + if (m >= mpool && m < mpool + u.mtx_cap * 64) u.mtx_used = std::max(u.mtx_used, (m - mpool) / 64 + 1); + } else if (op == 0x01) { // G_VTX: n vertices from w1 + const uint32_t v = word(rdram, pc + 4) & 0x3FFFFFF, n = (w0 >> 12) & 0xFF; + if (v >= vpool && v < vpool + u.vtx_cap * 16) u.vtx_used = std::max(u.vtx_used, (v - vpool) / 16 + n); + } + } + // The allocators clamp at the cap (they then hand out the last slots again): a pool counts as run out when its top slot is used. + u.mtx_capped = (u.mtx_cap && u.mtx_used >= u.mtx_cap) || (u.vtx_cap && u.vtx_used >= u.vtx_cap); + if (canary_on()) { // the guard after this buffer (only lists the course set-up allocated have one) + const uint32_t buf = word(rdram, s.gDlBufs + 4 * idx); + bool bad = false; + if (g_dl_guarded[idx].load() == buf && g_dl_guard_at[idx].load() == u.dl_cap) + for (uint32_t k = 0; k < kGuard; k += 4) bad |= word(rdram, buf + u.dl_cap + k) != kGuardWord; + if (bad) { + std::lock_guard lock{g_probe_mutex}; + if (g_probe.canary_bad++ < 10) { + std::printf("[thps] draw distance: WARNING display list %d (0x%08X, %u bytes) written past its end: task %u bytes\n", idx, buf, u.dl_cap, data_size); + std::fflush(stdout); + } + g_dl_guarded[idx] = 0; // counted once per list; never written again (the memory may be another owner's by now) + } + } + if (probe_on()) { // the rest of the frame after the extra passes of each view (the later views' extra passes not counted) + std::lock_guard lock{g_probe_mutex}; + ProbeFrame f = g_probe.frame[idx]; + g_probe.frame[idx] = ProbeFrame{}; + g_probe.frames++; + if (f.views == 1) { + g_probe.rest_1p.add(data_size - std::min(data_size, f.after_extras[0])); + g_probe.total_1p.add(data_size); + g_probe.extras_1p.add(f.extra_bytes[0]); + } else if (f.views == 3) { + g_probe.rest_2p_v0.add(data_size - std::min(data_size, f.after_extras[0] + f.extra_bytes[1])); + g_probe.rest_2p_v1.add(data_size - std::min(data_size, f.after_extras[1])); + g_probe.total_2p.add(data_size); + } + } + return true; +} + +// Which hidden bits the zone culling set. The trigger scripts hide items in two ways: World_SetVisibilityInBox (by box, as the skater moves: +// draw-cost culling) and per-item commands (by name, on events: secret areas, things revealed by a gap or a goal). Only the first kind may +// be undone for "full". The box hook below runs the game's function and records, per item, the hidden bits that call changed (set -> +// noted, cleared -> forgotten); any other writer leaves the record alone, so a bit set by an event command is never in it. The record is +// per course instance: a new item list (pointer or count) or a new course set-up (thps::dd::course_generation: Gfx_SetupCourse, also at a +// RETRY, which can rebuild the list at the same address; review 2026-10-26, finding 7) starts it again: empty, except for what the zone culling did and the items an event +// named since the course items were last drawn (the new course's set-up scripts, whether they ran before the set-up or after it). Game +// thread only. +namespace { +struct ZoneRecord { + uint32_t arr = 0, n = 0; + uint64_t gen = 0; // the course set-up it belongs to + std::vector bits; // per item: hidden bits (of 0x41) the zone culling set and nothing has cleared since + std::vector after; // per item: the item's hidden bits right after the zone culling last changed them + std::vector event; // per item: named by World_FindItem in this course (a script / goal / gap command): never unhidden +} g_zone; +std::vector g_before; +std::vector g_named_since_draw; // items World_FindItem named since the last World_DrawItems of the course items +std::vector g_zoned_since_draw; // and the indices of the items whose hidden bits the zone culling changed since then +uint16_t* flag_ptr(uint8_t* rdram, uint32_t arr, uint32_t i) { + return reinterpret_cast(rdram + ((word(rdram, word(rdram, arr + 4 * i) + 4) & 0x3FFFFFE) ^ 2)); +} +void zone_sync(uint8_t* rdram, uint32_t list) { + const uint32_t n = word(rdram, list + 16), arr = word(rdram, list + 12); + const uint64_t gen = thps::dd::course_generation(); + if (n != g_zone.n || arr != g_zone.arr || gen != g_zone.gen) { + const size_t m = n <= 8192 ? n : 0; + ZoneRecord old = std::move(g_zone); + g_zone = ZoneRecord{arr, n, gen, std::vector(m, 0), std::vector(m, 0), std::vector(m, 0)}; + // The same list, a new course instance: the zone culling done since the last draw (the new run's, while it loaded) is kept. Event + // ownership comes only from the names below, never from the old record. + if (arr == old.arr && n == old.n) + for (uint32_t i : g_zoned_since_draw) + if (i < m && i < old.bits.size()) g_zone.bits[i] = old.bits[i], g_zone.after[i] = old.after[i]; + for (uint32_t i = 0; i < m && !g_named_since_draw.empty(); i++) + if (std::find(g_named_since_draw.begin(), g_named_since_draw.end(), word(rdram, arr + 4 * i)) != g_named_since_draw.end()) g_zone.event[i] = 1; + } +} +} // namespace + +extern "C" int thps_world_set_visibility_in_box(uint8_t* rdram, recomp_context* ctx) { + thread_local bool inside = false; + if (inside) return 0; + const uint32_t list = thps::sym().gWorldItems; + zone_sync(rdram, list); + const uint32_t n = uint32_t(g_zone.bits.size()), arr = g_zone.arr; + g_before.resize(n); + for (uint32_t i = 0; i < n; i++) g_before[i] = *flag_ptr(rdram, arr, i); + inside = true; + thps::call_guest(thps::sym().World_SetVisibilityInBox, rdram, ctx); + inside = false; + for (uint32_t i = 0; i < n; i++) { + const uint16_t after = *flag_ptr(rdram, arr, i), changed = uint16_t((g_before[i] ^ after) & 0x41); + if (changed) { + g_zone.bits[i] = uint8_t((g_zone.bits[i] & ~changed) | (after & changed)); + if (g_zoned_since_draw.size() < 65536) g_zoned_since_draw.push_back(i); + g_zone.after[i] = uint8_t(after & 0x41); + } + } + return 1; +} + +// Event ownership. Whatever looks an item up by name (the scripts' per-item show / hide / kill commands, goal and gap code) owns it from +// then on, for this course: "full" never unhides it, even if the zone culling had also hidden it. Conservative on purpose: a secret +// revealed or hidden by an event stays under the game's own control. +extern "C" int thps_world_find_item(uint8_t* rdram, recomp_context* ctx) { + thread_local bool inside = false; + if (inside) return 0; + const uint32_t list = uint32_t(ctx->r4); + inside = true; + thps::call_guest(thps::sym().World_FindItem, rdram, ctx); + inside = false; + const uint32_t item = uint32_t(ctx->r2); + if (item && list == thps::sym().gWorldItems) { + if (g_named_since_draw.size() < 8192) g_named_since_draw.push_back(item); + zone_sync(rdram, list); + for (uint32_t i = 0; i < g_zone.event.size(); i++) + if (word(rdram, g_zone.arr + 4 * i) == item) { g_zone.event[i] = 1; break; } + } + return 1; +} + +// Zone visibility (draw call). The course's items (World_DrawItems' list: count at +16, pointers at +12; each item's flag halfword through the +// pointer at item +4) carry a "hidden" bit per player (1, player 2: 0x40) that the trigger scripts set and clear by box as the skater +// moves (the command at 0x800A53F0 calls 0x80033FAC in THPS2 US), so most of a course is not drawn from most places: at the Hangar +// start 144 of 470 items are hidden. With "full" the hook clears those bits, runs the function, and puts every flag back exactly, so +// collision and the trigger state never see the change. +// +// Off since v0.2 (playtest 2026-10-26): "full" no longer clears them. The box zones do more than culling here: the Hangar swaps its +// floor's light overlays by box and keeps the crashed ceiling hidden by box until its event, so clearing every box-hidden bit stacked +// the overlays (flickering squares on the floor) and showed the ceiling early. A sweep of all eight courses found almost nothing the +// unhiding added to the picture (New York: under 0.25 % of the pixels). Telling culling boxes from state boxes needs the trigger that +// issued each call (v0.3). The record below is kept, and `dd showall all` still clears every hidden bit for exploration. +// +// Runs of at most kChunk items. World_DrawItems is one pass of the renderer: its mesh queue (512) and sorted list (96) must hold every +// mesh of every item drawn, and what does not fit is dropped (in course-item order, so near items can be the ones lost). The original +// game stays far below that (School II, Marseille: up to ~210 entries and ~55 sorted); with a longer far plane and the zones unhidden the +// first frames of School II and Marseille filled both lists and dropped near items, and then the governor held the course at x1. So +// when the call draws more than the game would (a longer far plane, or zone-hidden items unhidden), the items only the setting adds are +// drawn first, farthest first, in runs of at most kChunk visible items, each run its own pass (the pass end draws and empties the lists, +// 0x800C50D0, then the next pass begins, 0x800C50B0). The last run is exactly what the game itself draws, as one pass, so it always fits. +// Far before near keeps translucent meshes over what is behind them. An item is left out of a run by its +0x44 float, which the loop +// tests right after the sphere test (negative = not drawn): set to -1 for that run only and put back. Only when the call runs inside a +// pass (the renderer's batch flag +0x2AE0; World_DrawItems' other caller, 0x8006F864, draws without one). Visibility here is the game's +// own test (0x800C364C: the view's five planes at +0x1D0 and the far distance at +0x174, camera at +0x90; item centre +0x34, radius +0x40). +// +// The display-list budget (a backstop; with the 144 KiB lists it is not expected to act). Nothing in the game stops a display list at +// its end, and the extra passes come before the parts of the frame that must be drawn: the game's own pass, the scenery, the moving +// objects, the HUD and, in two-player games, the other view. So before each extra pass the hook reads how much of the frame's list is +// written (gDlHead - gDlBufs[gDlFrame]: three words) and draws the next passes only while used + kPassBound + the rest's bound + kMargin +// fits the buffer. When it does not fit them all, it keeps the nearest extra items it can afford and leaves the farthest out (the +// remaining ones are still drawn far to near); the cut is counted (`dd`: cuts, cut_items) and logged with --verbose. The bounds are +// measured maxima with room to spare (2026-10-23, THPS_DD_DLPROBE at Full with 256 KiB lists): see kPassBound / kRestBound. +// The nearest pass keeps its bound reserved from the first extra pass on (review 2026-10-26, finding 9): the passes are drawn far to near, +// and until then the passes after the next were all counted at their running average, so an expensive far pass could leave the room short +// for the nearest items. Now the next pass and the nearest one count at their bound and only those between at the average; a shortfall +// drops more of the far ones. The nearest pass is cut (late_cuts) only if a pass took more than kPassBound. (Every pass at its bound, the +// first version of this fix, cut School II's far items at the default budget with the list less than half full.) +namespace { +constexpr uint32_t kBatchOn = 0x2AE0; +constexpr uint32_t kChunk = 48; +constexpr uint32_t kPassBound = 0x2000; // one extra pass (48 items, its pass end and begin): measured at most 6,264 bytes (THPS3 Rio) +constexpr uint32_t kItemBound = 0x200; // one extra item drawn outside a pass: at most 336 bytes per item in the passes +constexpr uint32_t kRestBound = 0xC000; // the rest of the frame after the extra passes: at most 32,144 (THPS3 Rio; p99 13-20 KB); + // two players, after the first view's (the second view's own part included): 18,088 +constexpr uint32_t kViewBound = 0x2000; // and per view still to come (two players: the second view's own part, about 5.5 KB) +constexpr uint32_t kMargin = 0x1000; +float g_pass_avg = 4096.0f; // the bytes an extra pass has been taking (a running average; game thread) +// How many of `wanted` more extra items (in passes of kChunk, or one by one outside a pass) the frame's display list can still take, with +// `used` bytes written. The next pass and the nearest (last) one must fit with their bound (kPassBound); the ones between are counted at +// what passes have been taking (g_pass_avg, at most the bound), since each of them is checked against its bound again before it is drawn. +size_t affordable(const uint8_t* rdram, uint32_t used, size_t wanted, bool in_passes, bool view_follows) { + const thps::dd::DlBudget o = thps::dd::dl_budget(); + const uint32_t buf = word(rdram, thps::sym().gDlBufSize), cap = o.cap ? std::min(o.cap, buf) : buf; + const uint64_t need = uint64_t(used) + (o.rest ? o.rest : kRestBound) + (view_follows ? kViewBound : 0) + (o.margin ? o.margin : kMargin); + if (need >= cap) return 0; + const uint64_t room = cap - need; + if (!in_passes) return size_t(std::min(room / kItemBound, wanted)); + const uint64_t bound = o.pass ? o.pass : kPassBound; + if (room < bound) return 0; + if (wanted <= kChunk || room < 2 * bound) return std::min(wanted, kChunk); // one pass: the nearest + const uint64_t est = uint64_t(std::clamp(g_pass_avg, 1024.0f, float(bound))); + return size_t(std::min((2 + (room - 2 * bound) / est) * kChunk, wanted)); +} +float fword(const uint8_t* rdram, uint32_t a) { return f32(rdram, a); } +} // namespace + +extern "C" int thps_world_draw_items(uint8_t* rdram, recomp_context* ctx) { + thread_local bool inside = false; + if (inside) return 0; + thps::dd::HookTimer timer; // ftime: this hook's own time (the game's drawing excluded) + const auto& sy = thps::sym(); + const uint32_t list = uint32_t(ctx->r4), view = uint32_t(ctx->r5), n = word(rdram, list + 16), arr = word(rdram, list + 12); + if (n == 0 || n > 8192 || list != sy.gWorldItems) return 0; + zone_sync(rdram, list); // a new course instance takes the items named since the last draw (above), then that list starts again + g_named_since_draw.clear(), g_zoned_since_draw.clear(); + const bool unhide = thps::dd::unhide_everything(); // zone unhiding is off (above); `dd showall all` only + // The view's far (+0x174) is the world view's, which the camera code sets at a course start and on camera changes, with the far plane + // of the setting (dd_game_far: projection_factor), not every frame. The governor's factor in use (lowered by its + // back-offs) is applied to the extra items instead (hold_far below). + const float game_far = f32(rdram, sy.gFarPlane), view_far = fword(rdram, view + 0x174); + const uint32_t probe_entry = probe_on() ? dl_used(rdram) : 0; + if (!unhide && !(game_far > 0.0f && view_far > game_far * 1.001f)) { // the game's own picture: one run, as the game does + if (probe_on()) probe_view(rdram, uint32_t(ctx->r6), probe_entry, probe_entry); + return 0; + } + const uint16_t mask = uint16_t((ctx->r6 ? 0x40 : 0x01) | 0x1000); // the loop's own skip test (player 2: 0x40; scenery 0x1000) + zone_sync(rdram, list); + static std::vector saved; // flag halfword address, value (game thread only) + static std::vector before; // each item's flags before any unhiding: what the game itself would draw + saved.clear(); + before.assign(n, 0); + uint32_t hidden = 0; + for (uint32_t i = 0; i < n; i++) { + const uint32_t fl = word(rdram, word(rdram, arr + 4 * i) + 4) & 0x3FFFFFE; + uint16_t* h = reinterpret_cast(rdram + (fl ^ 2)); + before[i] = *h; + if (!unhide || i >= g_zone.bits.size()) continue; + hidden += (*h & 0x41) != 0; + // A record goes stale as soon as anything else changed the bits since the zone culling did (an event hid or showed the item: + // the game's own state governs from then on), and an item named by an event is never unhidden. + if (g_zone.bits[i] && ((*h & 0x41) != g_zone.after[i] || g_zone.event[i])) g_zone.bits[i] = 0; + const uint16_t zone = uint16_t(*h & (thps::dd::unhide_everything() ? 0x41 : g_zone.bits[i])); // only what the zone culling hid + if (zone) { + saved.push_back(fl); + saved.push_back(*h); + *h = uint16_t(*h & ~zone); + } + } + static uint32_t frames = 0; + if (unhide && thps::verbose() && (frames++ % 300) == 0) { // evidence: hidden items, and how many of them only the zone culling hid + uint32_t named = 0; + for (uint8_t e : g_zone.event) named += e; + std::printf("[thps] draw distance: items %u, hidden %u, of them by the zone culling (unhidden for this draw) %u; items named by events %u\n", n, hidden, + uint32_t(saved.size() / 2), named); + // the ones kept hidden (not the zone culling's): index, flags, position (item +36/+40/+44, floats) + for (uint32_t i = 0, shown = 0; i < n && i < g_zone.bits.size() && shown < 12; i++) { + const uint32_t it = word(rdram, arr + 4 * i); + const uint16_t f = *flag_ptr(rdram, arr, i); + if (!(f & 0x41) || (f & g_zone.bits[i]) == (f & 0x41)) continue; + float pos[3]; + for (int k = 0; k < 3; k++) { uint32_t w = word(rdram, it + 36 + 4 * k); std::memcpy(&pos[k], &w, 4); } + std::printf("[thps] draw distance: kept hidden: item %u flags 0x%04X zone 0x%02X at (%.0f, %.0f, %.0f)\n", i, f, g_zone.bits[i], pos[0], pos[1], pos[2]); + shown++; + } + std::fflush(stdout); + } + const uint32_t renderer = word(rdram, sy.gRenderer); + const uint64_t a0 = ctx->r4, a1 = ctx->r5, a2 = ctx->r6; + const bool batched = renderer && word(rdram, renderer + kBatchOn) == 1; + // Two players: the first view's call (a2 = 0) is followed by the second's in the same frame (seen within the last few calls). + static uint32_t calls = 0, last_second = 0x80000000u; + calls++; + if (a2) last_second = calls; + const bool view_follows = !a2 && calls - last_second < 4; + // Which items this call draws, and which of them only the setting adds (beyond the game's far, or unhidden here). + const float cam[3] = {fword(rdram, view + 0x90), fword(rdram, view + 0x94), fword(rdram, view + 0x98)}; + // Extra items beyond the factor in use are held back (a back-off, a lower setting, which the world view's far does not follow until the + // camera code sets it again): those beyond the game's far past game far x factor, the unhidden ones, while the factor is below its end, + // past (factor - 1) x the game's far. + const float reach = unhide ? thps::dd::unhide_reach() * game_far : 0.0f, hold_far = game_far * thps::dd::factor(); + static std::vector> extra; // (distance to the bounding sphere, item index) + static std::vector held; // extra items beyond the factor in use (left out of every run) + static std::vector skip_addr; // each item's +0x44 (RDRAM word address) + static std::vector skip_was; + extra.clear(), held.clear(); + skip_addr.resize(n), skip_was.resize(n); + for (uint32_t i = 0; i < n; i++) { + const uint32_t it = word(rdram, arr + 4 * i); + skip_addr[i] = (it + 0x44) & 0x3FFFFFC; + skip_was[i] = word(rdram, it + 0x44); + const uint32_t fl = word(rdram, it + 4) & 0x3FFFFFE; + if (*reinterpret_cast(rdram + (fl ^ 2)) & mask) continue; // not drawn by this call at all + const float d[3] = {fword(rdram, it + 0x34) - cam[0], fword(rdram, it + 0x38) - cam[1], fword(rdram, it + 0x3C) - cam[2]}; + const float r = fword(rdram, it + 0x40), d2 = d[0] * d[0] + d[1] * d[1] + d[2] * d[2]; + if ((r + view_far) * (r + view_far) < d2) continue; // the game's far test with the far in use + bool in = true; + for (int k = 0; k < 5 && in; k++) { + const uint32_t pl = view + 0x1D0 + 12 * k; + in = fword(rdram, pl) * d[0] + fword(rdram, pl + 4) * d[1] + fword(rdram, pl + 8) * d[2] + r >= 0.0f; + } + if (!in) continue; + const bool game_draws = !(before[i] & mask) && (r + game_far) * (r + game_far) >= d2; + if (game_draws) continue; + const float gap = std::sqrt(d2) - r; + if ((before[i] & mask) ? gap > reach : gap > hold_far) held.push_back(i); + else extra.emplace_back(gap, i); + } + constexpr uint32_t kNeg = 0xBF800000u; // -1.0f + inside = true; + if (!batched) { + // Not inside a pass (THPS2's other caller, 0x8006F864, draws its own list this way; for the course items it has not been seen, + // `dd dlprobe` direct): the batcher draws at once, no lists to fill. One call, the held items left out. + g_direct_calls.fetch_add(1, std::memory_order_relaxed); + if (probe_on()) probe_view(rdram, uint32_t(a2), probe_entry, probe_entry); + // The budget per item (kItemBound): the farthest extra items that do not fit are left out with the held ones. + const size_t keep = affordable(rdram, dl_used(rdram), extra.size(), false, view_follows); + if (keep < extra.size()) { + std::sort(extra.begin(), extra.end(), [](const auto& x, const auto& y) { return x.first > y.first; }); + const size_t cut = extra.size() - keep; + for (size_t k = 0; k < cut; k++) held.push_back(extra[k].second); + thps::dd::note_cut(uint32_t(cut), dl_used(rdram), word(rdram, sy.gDlBufSize)); + } + for (uint32_t i : held) *reinterpret_cast(rdram + skip_addr[i]) = kNeg; + timer.guest([&] { thps::call_guest(sy.World_DrawItems, rdram, ctx); }); + for (uint32_t i : held) *reinterpret_cast(rdram + skip_addr[i]) = skip_was[i]; + } else { + std::sort(extra.begin(), extra.end(), [](const auto& x, const auto& y) { return x.first > y.first; }); + static std::vector in_run; + in_run.assign(n, 0); + auto run = [&](size_t from, size_t to, bool last) { + // last: everything except the extra and held items (= what the game draws); otherwise extra[from, to) + std::fill(in_run.begin(), in_run.end(), uint8_t(last ? 1 : 0)); + for (size_t k = last ? 0 : from; k < (last ? extra.size() : to); k++) in_run[extra[k].second] = uint8_t(last ? 0 : 1); + if (last) + for (uint32_t i : held) in_run[i] = 0; + for (uint32_t i = 0; i < n; i++) + if (!in_run[i]) *reinterpret_cast(rdram + skip_addr[i]) = kNeg; + ctx->r4 = a0, ctx->r5 = a1, ctx->r6 = a2; + timer.guest([&] { thps::call_guest(sy.World_DrawItems, rdram, ctx); }); + for (uint32_t i = 0; i < n; i++) *reinterpret_cast(rdram + skip_addr[i]) = skip_was[i]; + if (!last) { // this run's pass ends (its lists drawn and emptied) and the next one begins + ctx->r4 = int32_t(renderer); // sign-extended, as the recompiled code keeps addresses + timer.guest([&] { thps::call_guest(sy.Mesh_PassEnd, rdram, ctx); }); + ctx->r4 = int32_t(renderer); // sign-extended, as the recompiled code keeps addresses + timer.guest([&] { thps::call_guest(sy.Mesh_PassBegin, rdram, ctx); }); + } + }; + // The extra passes, far to near, within the display-list budget: before each pass, the items the list can no longer take are + // dropped from the far end of what is left; the nearest pass keeps its bound reserved throughout (affordable), so it is the one + // kept. Items cut from the nearest pass once passes were drawn (late) mean a pass took more than its bound. + size_t cut = 0, late = 0, passes = 0; + uint32_t cut_used = 0, late_used = 0; + for (size_t k = 0; k < extra.size();) { + const uint32_t used = dl_used(rdram); + const size_t left = extra.size() - k, keep = affordable(rdram, used, left, true, view_follows); + if (keep < left) { + const size_t near_cut = passes && std::min(left, size_t(kChunk)) > keep ? std::min(left, size_t(kChunk)) - keep : 0; + if (near_cut && !late) late_used = used; + if (left - keep > near_cut && !cut) cut_used = used; + late += near_cut, cut += left - keep - near_cut, k += left - keep; + if (k >= extra.size()) break; + } + const uint32_t u0 = probe_on() ? dl_used(rdram) : 0; + const size_t to = std::min(extra.size(), k + kChunk); + run(k, to, false); + passes++; + const uint32_t items = uint32_t(to - k); + if (items == kChunk) g_pass_avg += (float(dl_used(rdram) - used) - g_pass_avg) * 0.125f; // full passes only + k = to; + if (probe_on()) { + const uint32_t d = dl_used(rdram) - u0; + std::lock_guard lock{g_probe_mutex}; + g_probe.pass.add(d), g_probe.per_item.add(d / items); + } + } + if (cut) thps::dd::note_cut(uint32_t(cut), cut_used, word(rdram, sy.gDlBufSize)); + if (late) thps::dd::note_cut(uint32_t(late), late_used, word(rdram, sy.gDlBufSize), true); + const uint32_t probe_after = probe_on() ? dl_used(rdram) : 0; + if (probe_on()) probe_view(rdram, uint32_t(a2), probe_entry, probe_after); + run(0, 0, true); + if (probe_on()) { + const uint32_t d = dl_used(rdram) - probe_after; + std::lock_guard lock{g_probe_mutex}; + g_probe.own.add(d); + } + max_into(g_runs_hw, uint32_t((extra.size() + kChunk - 1) / kChunk + 1)); + } + ctx->r4 = a0, ctx->r5 = a1, ctx->r6 = a2; + inside = false; + for (size_t i = 0; i + 1 < saved.size(); i += 2) *reinterpret_cast(rdram + (saved[i] ^ 2)) = uint16_t(saved[i + 1]); + return 1; +} + +// The matrix pools. The gfx set-up gets both pools (420 x 64 bytes each) with one Mem_Alloc(heap, 0xD200, 8) and stores the cap (420) in +// that call's delay slot; right after the call it reads the cap back to place the second pool (pool 2 = pool 1 + cap x 64), and the +// per-frame allocator caps at it (the only other reader of the cap word). 420 is what limits `full` in the busiest courses. +// With dd_buffers = bigger (the default; src/draw_distance.cpp) this asks for kBigCap matrices per pool at every course start, whatever the +// setting, and raises the cap word before the call so the set-up places pool 2 at pool 1 + kBigCap x 64. The cap word is then set to +// what is in force: kBigCap while the setting is not `original`, else the game's 420 (sync_cap: first at the set-up's next request, the +// vertex pools, which comes after the cap was read back; then every frame from dd_game_far). Both caps index the same two pools, so the +// setting takes effect at once, in either direction; past the cap the allocator hands out the pool's last slots again, as the game does. +// With dd_buffers = original the game's own request and cap (the governor then keeps the far plane within 420). If the heap has no room +// for the bigger block (checked before the request, see thps_mem_alloc), the game's own request is made. +// The heap has room (2026-10-22, every course, one and two players): at least 2.9 MB free and a largest free block of 894,880 bytes. +// +// The display lists. The same set-up first allocates the two top-level display lists: gDlBufSize = gDlUnits x 8 (0x2400 x 8 = 72 KiB; the +// setter 0x800B8710 picks 0x3000, 96 KiB, in one mode), then two Mem_Alloc(heap, gDlBufSize, 8), before the matrix pools. Nothing caps the +// writes to a list (the frame end notices an overrun only afterwards and puts the end commands back inside the buffer), and School II at +// Full went past 72 KiB (78,376 bytes, 2026-10-22). With dd_buffers = bigger each list gets at least kBigDl bytes, whatever the setting +// (as the matrix pools: so the setting takes effect at once); a larger size of the game's own is kept. thps_gfx_setup_course marks the +// set-up, so only its two requests are changed (the front end's set-up, 0x800B9438, keeps its own 72 KiB lists). gDlBufSize is set to the +// size really allocated before each request, so both buffers always hold what it says: if the heap has no room for a bigger list (checked +// before the request: a failed request hangs the game, see thps_mem_alloc), gDlBufSize stays the game's size for that request and the next. +namespace { +constexpr uint32_t kBigDl = 0x24000; // 144 KiB per display list (2 x 72 KiB more heap in all; the heap has room: 2026-10-23 dd-heap runs) +// Tests / measurements: THPS_DD_DL_BYTES=N asks for N bytes per list instead (rounded up to 8; a larger buffer than the game's still). +uint32_t dl_min_bytes() { + static const uint32_t n = std::getenv("THPS_DD_DL_BYTES") ? (uint32_t(std::strtoul(std::getenv("THPS_DD_DL_BYTES"), nullptr, 0)) + 7u) & ~7u : kBigDl; + return n; +} +thread_local bool t_setup = false, t_dl_fallback = false; +thread_local uint32_t t_dl_calls = 0, t_dl_native = 0; +std::atomic g_dl_native{0}, g_dl_fallbacks{0}; // the game's own size of the current lists; requests that fell back to it +} // namespace + +extern "C" int thps_gfx_setup_course(uint8_t* rdram, recomp_context* ctx) { + thread_local bool inside = false; + if (inside) return 0; + inside = true, t_setup = true, t_dl_calls = 0, t_dl_native = 0, t_dl_fallback = false; + thps::dd::heap_view_begin(); // tests (THPS_DD_HEAP_FREE): the planning's view of the free list starts again + thps::dd::note_course_start(); // the governor: the setting again for this course (ceiling reset) + thps::call_guest(thps::sym().Gfx_SetupCourse, rdram, ctx); + t_setup = false, inside = false; + if (t_dl_calls) g_dl_native = t_dl_native; + // Every set-up, like the matrix pools' line (until 2026-10-23 only the first three: a player's log then looked as if later courses had + // the game's own lists) + std::printf("[thps] draw distance: display lists: 2 x %u bytes (the game's %u)%s\n", word(rdram, thps::sym().gDlBufSize), t_dl_native, + t_dl_fallback ? ", no room for the bigger ones" : ""); + std::fflush(stdout); + return 1; +} + +namespace { +constexpr uint32_t kGameCap = 420, kBigCap = 2048; +std::atomic g_heap{0}; // the heap the gfx set-up allocates from (Mem_Alloc's a0 for the matrix pools) +std::atomic g_big_block{0}; // the bigger pools of the current course (0: the game's own) +struct HeapStats { uint32_t failed, free_bytes, largest, blocks; }; +HeapStats heap_stats(uint8_t* rdram, uint32_t heap) { // free list: head at +0, next at +4, free bytes at +20; +16 = the last failed size (-1 = none) + HeapStats h{word(rdram, heap + 16), 0, 0, 0}; + for (uint32_t b = word(rdram, heap), k = 0; b && k < 100000; b = word(rdram, b + 4), k++) { + const uint32_t f = word(rdram, b + 20); + h.free_bytes += f, h.largest = std::max(h.largest, f), h.blocks++; + } + return h; +} +void put_word(uint8_t* rdram, uint32_t vaddr, uint32_t v) { *reinterpret_cast(rdram + (vaddr & 0x3FFFFFC)) = v; } +// The bigger pools are the ones in use (the set-up placed both in the block this hook got), so the cap word may be set. +bool big_in_use(uint8_t* rdram) { + const uint32_t b = g_big_block.load(); + const auto& s = thps::sym(); + return b && word(rdram, s.gMtxPools) == b && word(rdram, s.gMtxPools + 4) == b + kBigCap * 64; +} +void sync_cap(uint8_t* rdram) { // game thread + if (!big_in_use(rdram)) return; + const uint32_t want = thps::dd::enlarge_pools() ? kBigCap : kGameCap; + if (word(rdram, thps::sym().gMtxCap) != want) put_word(rdram, thps::sym().gMtxCap, want); +} +// Low-water marks of the gfx heap since the start or `dd heap reset` (under g_low_mutex). +struct HeapLow { + uint32_t calls = 0, failed = 0, biggest = 0; + uint32_t free_min = UINT32_MAX, largest_min = UINT32_MAX; // after an allocation + int64_t margin_min = INT64_MAX; // largest free block before a request - its size + uint32_t margin_size = 0; // the request that had the least margin +}; +std::mutex g_low_mutex; +HeapLow g_low; +void note_heap(const HeapStats& before, const HeapStats& after, uint32_t size, bool ok) { + std::lock_guard lock{g_low_mutex}; + g_low.calls++; + if (!ok) g_low.failed++; + g_low.biggest = std::max(g_low.biggest, size); + g_low.free_min = std::min(g_low.free_min, after.free_bytes); + g_low.largest_min = std::min(g_low.largest_min, after.largest); + const int64_t m = int64_t(before.largest) - int64_t(size); + if (m < g_low.margin_min) g_low.margin_min = m, g_low.margin_size = size; +} +} // namespace + +void thps::dd::dd_game_sync_pools(uint8_t* rdram) { sync_cap(rdram); } + +namespace { +// The set-up's requests to the gfx heap, in order (Gfx_SetupCourse, US 0x800B90BC: two display lists of gDlBufSize, the matrix pools +// 0xD200, the vertex pools 0x3E80, all align 8; nothing else from that heap in between), each as the allocator counts it. +constexpr uint32_t kGameVtxBytes = 500 * 16 * 2; +uint32_t heap_need(uint32_t n) { return ((n + 0x23) & ~3u) + 8; } +struct PlanReq { uint32_t native, big; bool can; }; +// Which of the set-up's remaining requests to make bigger (review 2026-10-26, finding 1; rest[0] is the request at hand). A request that +// fails hangs the game, and so does a later one the bigger ones left no room for: so the plan places every remaining request, first fit, on +// a copy of the free list. The native plan first: if it does not fit, nothing is enlarged (the game's own set-up fails there anyway). Then +// each enlargement in order (the first `pair` requests, the two display lists, together) is kept when the plan with it still places every +// request and leaves a largest free block of thps::dd::plan_floor (the native plan's, at most 768 KiB), so the requests after the set-up +// find a block as big as the biggest one they make. +std::vector plan_enlarge(uint8_t* rdram, uint32_t heap, const std::vector& rest, size_t pair) { + const std::vector blocks = thps::dd::heap_free_blocks(rdram, heap); + const uint32_t cache = thps::dd::heap_failed_cache(rdram, heap); + std::vector big(rest.size(), false); + auto needs = [&] { + std::vector v; + for (size_t i = 0; i < rest.size(); i++) v.push_back(big[i] ? rest[i].big : rest[i].native); + return v; + }; + const int64_t native = thps::dd::first_fit_after(blocks, needs(), cache); + if (native < 0) return big; + const int64_t floor = thps::dd::plan_floor(native); + for (size_t i = 0; i < rest.size();) { + const size_t n = i < pair ? pair - i : 1; + bool can = true; + for (size_t k = i; k < i + n && k < rest.size(); k++) can = can && rest[k].can && rest[k].big != rest[k].native; + if (can) { + for (size_t k = i; k < i + n && k < rest.size(); k++) big[k] = true; + if (thps::dd::first_fit_after(blocks, needs(), cache) < floor) + for (size_t k = i; k < i + n && k < rest.size(); k++) big[k] = false; + } + i += n; + } + return big; +} +} // namespace + +extern "C" int thps_mem_alloc(uint8_t* rdram, recomp_context* ctx) { + const auto& s = thps::sym(); + static thread_local bool inside = false, pending = false; + if (inside) return 0; + const uint32_t heap = uint32_t(ctx->r4), size = uint32_t(ctx->r5), align = uint32_t(ctx->r6); + // The set-up's next request after the pools (the vertex pools): pool 2 is placed by now, so the cap in force can be set. + if (pending) pending = false, sync_cap(rdram); + // The recompiled code does not keep a return address in ra, so the call is told apart by its arguments and by the caller's state at + // entry: size 0xD200, align 8, v0 = 420 (the cap it has just stored, in the delay slot) and the cap word = 420. No other call matches. + const bool pools = size == kGameCap * 64 * 2 && align == 8 && uint32_t(ctx->r2) == kGameCap && word(rdram, s.gMtxCap) == kGameCap; + if (pools) g_heap = heap, g_big_block = 0; + // One of the set-up's two display-list requests (see thps_gfx_setup_course): size = gDlBufSize, align 8. + const bool dl = t_setup && t_dl_calls < 2 && align == 8 && size != 0 && size == word(rdram, s.gDlBufSize); + uint32_t dl_want = size; + // A bigger request is made only when it and every request after it will succeed: the game's allocator does not survive a failure + // (Heap_AllocUnlocked's failure path calls 0x800B4818, which takes the heap's lock again while Mem_Alloc holds it: the game hangs; found + // 2026-10-23 by forcing one). So the set-up's remaining requests are planned on the free list first (plan_enlarge: first fit; size + + // 0x23 rounded to 4, plus the alignment; a size at or above the failed-size cache fails at once), and the game's own size is asked for + // when the bigger one does not fit the plan. (Until the review of 2026-10-26 only the request at hand was checked: an enlarged first + // list could leave no room for the second one's native size.) + const bool pools_can = thps::dd::big_buffers() && !std::getenv("THPS_DD_NOPOOL"); + const PlanReq mtx_req{heap_need(kGameCap * 64 * 2), heap_need(kBigCap * 64 * 2), pools_can}, vtx_req{heap_need(kGameVtxBytes), heap_need(kGameVtxBytes), false}; + if (dl) { + if (t_dl_calls++ == 0) t_dl_native = size; + if (thps::dd::big_buffers() && !t_dl_fallback) dl_want = std::max(t_dl_native, dl_min_bytes()); + // Tests: THPS_DD_DL_FAIL=1|2: that request of the set-up finds no room for the bigger list (the fallback runs). + static const uint32_t fail_at = std::getenv("THPS_DD_DL_FAIL") ? uint32_t(std::atoi(std::getenv("THPS_DD_DL_FAIL"))) : 0; + const uint32_t g = canary_on() ? kGuard : 0; + const PlanReq dl_req{heap_need(t_dl_native + g), heap_need(dl_want + g), true}; + const bool planned = dl_want != t_dl_native && fail_at != t_dl_calls && + (t_dl_calls == 1 ? plan_enlarge(rdram, heap, {dl_req, dl_req, mtx_req, vtx_req}, 2) : plan_enlarge(rdram, heap, {dl_req, mtx_req, vtx_req}, 0))[0]; + if (dl_want != t_dl_native && !planned) { + // no room for the bigger list: the game's own size for this one and the next + t_dl_fallback = true; + g_dl_fallbacks.fetch_add(1); + std::printf("[thps] draw distance: WARNING no room for a %u-byte display list (request %u of the set-up): the game's %u\n", dl_want, t_dl_calls, + t_dl_native); + std::fflush(stdout); + dl_want = t_dl_native; + } + } + const bool enlarge = pools && pools_can && plan_enlarge(rdram, heap, {mtx_req, vtx_req}, 0)[0]; + const bool measure = heap == g_heap.load(); + const HeapStats before = measure ? heap_stats(rdram, heap) : HeapStats{}; + if (enlarge) { + ctx->r5 = int32_t(kBigCap * 64 * 2); + put_word(rdram, s.gMtxCap, kBigCap); + } + const uint32_t guard = dl && canary_on() ? kGuard : 0; // THPS_DD_CANARY (debug): room for the guard after the list + if (dl) { // gDlBufSize = what this request gets (the game reads it again for the second request and every frame) + put_word(rdram, s.gDlBufSize, dl_want); + ctx->r5 = int32_t(dl_want + guard); + } + // Tests (THPS_DD_HEAP_FREE): the set-up's request, as made, from the planning's view of the free list (a WARNING if it could not fit). + const bool vpools = t_setup && size == kGameVtxBytes && align == 8; + if (t_setup && (dl || pools || vpools)) thps::dd::heap_view_take(heap_need(uint32_t(ctx->r5)), dl ? "a display list" : pools ? "the matrix pools" : "the vertex pools"); + inside = true; + thps::call_guest(s.Mem_Alloc, rdram, ctx); + if (dl) { + const uint32_t b = uint32_t(ctx->r2); + if (guard && b) fill_guard(rdram, b, word(rdram, s.gDlBufSize)); + g_dl_guarded[(t_dl_calls - 1) & 1] = guard ? b : 0; + g_dl_guard_at[(t_dl_calls - 1) & 1] = word(rdram, s.gDlBufSize); + } + const bool big = enlarge && uint32_t(ctx->r2) != 0; + inside = false; + const uint32_t got = uint32_t(ctx->r2); + if (measure) note_heap(before, heap_stats(rdram, heap), big ? kBigCap * 64 * 2 : dl ? word(rdram, s.gDlBufSize) : size, got != 0); + if (big) g_big_block = got, pending = true; + if (enlarge) { + std::printf("[thps] draw distance: matrix pools: %s (%u bytes) at 0x%08X, heap largest free block before %u\n", + big ? "2048 per frame" : "the game's 420", big ? kBigCap * 64 * 2 : size, got, before.largest); + std::fflush(stdout); + } else if (pools && thps::dd::big_buffers()) { + std::printf("[thps] draw distance: WARNING no room for the 2048-matrix pools: the game's 420 (%u bytes) at 0x%08X\n", size, got); + std::fflush(stdout); + } + if (got == 0 && measure) { // a failed allocation in the gfx heap: say so (the game may carry on without it) + static std::atomic warned{0}; + if (warned++ < 20) { + const HeapStats h = heap_stats(rdram, heap); + std::printf("[thps] draw distance: WARNING Mem_Alloc(0x%08X, %u, %u) failed: free %u bytes in %u blocks, largest %u (pools %s)\n", heap, size, + align, h.free_bytes, h.blocks, h.largest, big_in_use(rdram) ? "2048" : "the game's"); + std::fflush(stdout); + } + } + return 1; +} + +// `dd heap [reset]` (tests): the gfx heap's failed-size cache, free bytes, free blocks and the largest one, the pools' size and the cap in +// force, then the low-water marks since the start (or the last reset): least free bytes and smallest largest block after an allocation, +// the least margin between a request and the largest free block (and that request's size), failed requests, the biggest request. +std::string thps::dd::dd_game_heap(uint8_t* rdram, bool reset) { + const uint32_t heap = g_heap.load(); + if (!heap) return "none"; + const HeapStats h = heap_stats(rdram, heap); + HeapLow l; + { + std::lock_guard lock{g_low_mutex}; + l = g_low; + if (reset) g_low = HeapLow{}; + } + char buf[480]; + std::snprintf(buf, sizeof buf, "at 0x%08X failed %d free %u blocks %u largest %u mtx_pool %u mtx_cap %u dl %u dl_game %u dl_fallbacks %u low: calls %u free_min %u largest_min %u margin_min %lld " + "(size %u) failed_calls %u biggest %u view_fail %u", heap, int32_t(h.failed), h.free_bytes, h.blocks, h.largest, big_in_use(rdram) ? kBigCap : word(rdram, sym().gMtxCap), + word(rdram, sym().gMtxCap), word(rdram, sym().gDlBufSize), g_dl_native.load(), g_dl_fallbacks.load(), l.calls, l.calls ? l.free_min : 0, l.calls ? l.largest_min : 0, + l.calls ? (long long)l.margin_min : 0LL, l.margin_size, l.failed, l.biggest, thps::dd::heap_view_fails()); + return buf; +} + +// `dd item N` (tests): the item's hidden bits now, the zone record (after the draw call's stale-record check), event ownership, and +// whether "full" draws it this frame (the same rule as thps_world_draw_items). +std::string thps::dd::dd_game_item(uint8_t* rdram, uint32_t index) { + const uint32_t list = thps::sym().gWorldItems; + const uint32_t n = word(rdram, list + 16), arr = word(rdram, list + 12); + if (n == 0 || n > 8192 || index >= n) return "none"; + zone_sync(rdram, list); + const uint16_t f = *flag_ptr(rdram, arr, index); + const uint8_t zone = index < g_zone.bits.size() ? g_zone.bits[index] : 0, event = index < g_zone.event.size() ? g_zone.event[index] : 0; + const uint16_t unhidden = thps::dd::unhide_everything() ? uint16_t(f & 0x41) : 0; + uint32_t named = 0; + for (uint8_t e : g_zone.event) named += e; + char buf[160]; + std::snprintf(buf, sizeof buf, "flags 0x%04X zone 0x%02X event %u drawn %d named %u gen %llu", f, zone, event, ((f & 0x41) & ~unhidden) == 0, named, + (unsigned long long)g_zone.gen); + return buf; +} + +// `dd own N` (tests): item N is event-owned from now on, as World_FindItem would make it (thps_world_find_item). +std::string thps::dd::dd_game_own(uint8_t* rdram, uint32_t index) { + const uint32_t list = thps::sym().gWorldItems; + const uint32_t n = word(rdram, list + 16); + if (n == 0 || n > 8192 || index >= n) return "none"; + zone_sync(rdram, list); + if (index >= g_zone.event.size()) return "none"; + g_zone.event[index] = 1; + return dd_game_item(rdram, index); +} diff --git a/decomp/thps2/recomp-host/game/game_hooks.def b/decomp/thps2/recomp-host/game/game_hooks.def index f1e8bca..7e62d92 100644 --- a/decomp/thps2/recomp-host/game/game_hooks.def +++ b/decomp/thps2/recomp-host/game/game_hooks.def @@ -3,3 +3,9 @@ THPS_IMPORT(CTX, thps_skip_intro_update) THPS_IMPORT(CTX_RET, thps_unlock_level) THPS_IMPORT(CTX_RET, thps_cheat_is_on) +THPS_IMPORT(CTX_RET, thps_world_draw_items) +THPS_IMPORT(CTX_RET, thps_mem_alloc) +THPS_IMPORT(CTX_RET, thps_gfx_setup_course) +THPS_IMPORT(CTX_RET, thps_world_set_visibility_in_box) +THPS_IMPORT(CTX_RET, thps_world_find_item) +THPS_IMPORT(CTX, thps_mesh_queue_emit) diff --git a/decomp/thps2/recomp-host/recipes/eu/thps2.host.syms.toml b/decomp/thps2/recomp-host/recipes/eu/thps2.host.syms.toml index 4490332..5d9b24d 100644 --- a/decomp/thps2/recomp-host/recipes/eu/thps2.host.syms.toml +++ b/decomp/thps2/recomp-host/recipes/eu/thps2.host.syms.toml @@ -642,7 +642,7 @@ functions = [ { name = "func_80033974", vram = 0x80033974, size = 0xD4 }, { name = "func_80033A48", vram = 0x80033A48, size = 0x3BC }, { name = "func_80033E04", vram = 0x80033E04, size = 0x1D8 }, - { name = "func_80033FDC", vram = 0x80033FDC, size = 0x1A0 }, + { name = "World_SetVisibilityInBox", vram = 0x80033FDC, size = 0x1A0 }, { name = "func_8003417C", vram = 0x8003417C, size = 0x1E8 }, { name = "func_80034364", vram = 0x80034364, size = 0x178 }, { name = "func_800344DC", vram = 0x800344DC, size = 0xC4 }, @@ -813,7 +813,7 @@ functions = [ { name = "func_80041198", vram = 0x80041198, size = 0x12C }, { name = "func_800412C4", vram = 0x800412C4, size = 0x58C }, { name = "func_80041850", vram = 0x80041850, size = 0x144 }, - { name = "func_80041994", vram = 0x80041994, size = 0x3C0 }, + { name = "World_DrawItems", vram = 0x80041994, size = 0x3C0 }, { name = "func_80041D54", vram = 0x80041D54, size = 0x300 }, { name = "func_80042054", vram = 0x80042054, size = 0x5C4 }, { name = "func_80042618", vram = 0x80042618, size = 0x198 }, @@ -828,7 +828,7 @@ functions = [ { name = "func_80042E10", vram = 0x80042E10, size = 0x18 }, { name = "func_80042E28", vram = 0x80042E28, size = 0x40 }, { name = "func_80042E68", vram = 0x80042E68, size = 0x3C }, - { name = "func_80042EA4", vram = 0x80042EA4, size = 0x40 }, + { name = "World_FindItem", vram = 0x80042EA4, size = 0x40 }, { name = "func_80042EE4", vram = 0x80042EE4, size = 0xB0 }, { name = "func_80042F94", vram = 0x80042F94, size = 0xF4 }, { name = "func_80043088", vram = 0x80043088, size = 0x94 }, @@ -2627,7 +2627,7 @@ functions = [ { name = "func_800B3DA0", vram = 0x800B3DA0, size = 0x5C }, { name = "Heap_Realloc", vram = 0x800B3DFC, size = 0x84 }, { name = "func_800B3E80", vram = 0x800B3E80, size = 0x10C }, - { name = "Heap_Alloc_maybe", vram = 0x800B3F8C, size = 0x74 }, + { name = "Mem_Alloc", vram = 0x800B3F8C, size = 0x74 }, { name = "Heap_AllocUnlocked_maybe", vram = 0x800B4000, size = 0x1EC }, { name = "Heap_AllocTop_maybe", vram = 0x800B41EC, size = 0x214 }, { name = "func_800B4400", vram = 0x800B4400, size = 0x84 }, @@ -2785,7 +2785,7 @@ functions = [ { name = "func_800B8E78", vram = 0x800B8E78, size = 0xDC }, { name = "func_800B8F54", vram = 0x800B8F54, size = 0x28 }, { name = "func_800B8F7C", vram = 0x800B8F7C, size = 0x164 }, - { name = "func_800B90E0", vram = 0x800B90E0, size = 0x37C }, + { name = "Gfx_SetupCourse", vram = 0x800B90E0, size = 0x37C }, { name = "func_800B945C", vram = 0x800B945C, size = 0x374 }, { name = "func_800B97D0", vram = 0x800B97D0, size = 0x20C }, { name = "func_800B99DC", vram = 0x800B99DC, size = 0x25C }, @@ -2979,15 +2979,15 @@ functions = [ { name = "func_800C4FDC", vram = 0x800C4FDC, size = 0x34 }, { name = "func_800C5010", vram = 0x800C5010, size = 0x74 }, { name = "func_800C5084", vram = 0x800C5084, size = 0x4C }, - { name = "func_800C50D0", vram = 0x800C50D0, size = 0x20 }, - { name = "func_800C50F0", vram = 0x800C50F0, size = 0x3C }, + { name = "Mesh_PassBegin", vram = 0x800C50D0, size = 0x20 }, + { name = "Mesh_PassEnd", vram = 0x800C50F0, size = 0x3C }, { name = "func_800C512C", vram = 0x800C512C, size = 0xF0 }, { name = "func_800C521C", vram = 0x800C521C, size = 0x128 }, { name = "func_800C5344", vram = 0x800C5344, size = 0x740 }, { name = "func_800C5A84", vram = 0x800C5A84, size = 0x6BC }, { name = "func_800C6140", vram = 0x800C6140, size = 0x24 }, { name = "func_800C6164", vram = 0x800C6164, size = 0x9C }, - { name = "func_800C6200", vram = 0x800C6200, size = 0x8B8 }, + { name = "Mesh_QueueEmit", vram = 0x800C6200, size = 0x8B8 }, { name = "func_800C6AB8", vram = 0x800C6AB8, size = 0x14 }, { name = "func_800C6ACC", vram = 0x800C6ACC, size = 0xB8 }, { name = "func_800C6B84", vram = 0x800C6B84, size = 0x84 }, diff --git a/decomp/thps2/recomp-host/recipes/us/thps2.host.syms.toml b/decomp/thps2/recomp-host/recipes/us/thps2.host.syms.toml index fab576c..bc607ff 100644 --- a/decomp/thps2/recomp-host/recipes/us/thps2.host.syms.toml +++ b/decomp/thps2/recomp-host/recipes/us/thps2.host.syms.toml @@ -642,7 +642,7 @@ functions = [ { name = "func_80033944", vram = 0x80033944, size = 0xD4 }, { name = "func_80033A18", vram = 0x80033A18, size = 0x3BC }, { name = "func_80033DD4", vram = 0x80033DD4, size = 0x1D8 }, - { name = "func_80033FAC", vram = 0x80033FAC, size = 0x1A0 }, + { name = "World_SetVisibilityInBox", vram = 0x80033FAC, size = 0x1A0 }, { name = "func_8003414C", vram = 0x8003414C, size = 0x1E8 }, { name = "func_80034334", vram = 0x80034334, size = 0x178 }, { name = "func_800344AC", vram = 0x800344AC, size = 0xC4 }, @@ -813,7 +813,7 @@ functions = [ { name = "func_80041168", vram = 0x80041168, size = 0x12C }, { name = "func_80041294", vram = 0x80041294, size = 0x58C }, { name = "func_80041820", vram = 0x80041820, size = 0x144 }, - { name = "func_80041964", vram = 0x80041964, size = 0x3C0 }, + { name = "World_DrawItems", vram = 0x80041964, size = 0x3C0 }, { name = "func_80041D24", vram = 0x80041D24, size = 0x300 }, { name = "func_80042024", vram = 0x80042024, size = 0x5C4 }, { name = "func_800425E8", vram = 0x800425E8, size = 0x198 }, @@ -828,7 +828,7 @@ functions = [ { name = "func_80042DE0", vram = 0x80042DE0, size = 0x18 }, { name = "func_80042DF8", vram = 0x80042DF8, size = 0x40 }, { name = "func_80042E38", vram = 0x80042E38, size = 0x3C }, - { name = "func_80042E74", vram = 0x80042E74, size = 0x40 }, + { name = "World_FindItem", vram = 0x80042E74, size = 0x40 }, { name = "func_80042EB4", vram = 0x80042EB4, size = 0xB0 }, { name = "func_80042F64", vram = 0x80042F64, size = 0xF4 }, { name = "func_80043058", vram = 0x80043058, size = 0x94 }, @@ -2627,7 +2627,7 @@ functions = [ { name = "func_800B3D70", vram = 0x800B3D70, size = 0x5C }, { name = "Heap_Realloc", vram = 0x800B3DCC, size = 0x84 }, { name = "func_800B3E50", vram = 0x800B3E50, size = 0x10C }, - { name = "Heap_Alloc_maybe", vram = 0x800B3F5C, size = 0x74 }, + { name = "Mem_Alloc", vram = 0x800B3F5C, size = 0x74 }, { name = "Heap_AllocUnlocked_maybe", vram = 0x800B3FD0, size = 0x1EC }, { name = "Heap_AllocTop_maybe", vram = 0x800B41BC, size = 0x214 }, { name = "func_800B43D0", vram = 0x800B43D0, size = 0x84 }, @@ -2785,7 +2785,7 @@ functions = [ { name = "func_800B8E48", vram = 0x800B8E48, size = 0xE8 }, { name = "func_800B8F30", vram = 0x800B8F30, size = 0x28 }, { name = "func_800B8F58", vram = 0x800B8F58, size = 0x164 }, - { name = "func_800B90BC", vram = 0x800B90BC, size = 0x37C }, + { name = "Gfx_SetupCourse", vram = 0x800B90BC, size = 0x37C }, { name = "func_800B9438", vram = 0x800B9438, size = 0x374 }, { name = "func_800B97AC", vram = 0x800B97AC, size = 0x20C }, { name = "func_800B99B8", vram = 0x800B99B8, size = 0x25C }, @@ -2979,15 +2979,15 @@ functions = [ { name = "func_800C4FBC", vram = 0x800C4FBC, size = 0x34 }, { name = "func_800C4FF0", vram = 0x800C4FF0, size = 0x74 }, { name = "func_800C5064", vram = 0x800C5064, size = 0x4C }, - { name = "func_800C50B0", vram = 0x800C50B0, size = 0x20 }, - { name = "func_800C50D0", vram = 0x800C50D0, size = 0x3C }, + { name = "Mesh_PassBegin", vram = 0x800C50B0, size = 0x20 }, + { name = "Mesh_PassEnd", vram = 0x800C50D0, size = 0x3C }, { name = "func_800C510C", vram = 0x800C510C, size = 0xF0 }, { name = "func_800C51FC", vram = 0x800C51FC, size = 0x128 }, { name = "func_800C5324", vram = 0x800C5324, size = 0x740 }, { name = "func_800C5A64", vram = 0x800C5A64, size = 0x6BC }, { name = "func_800C6120", vram = 0x800C6120, size = 0x24 }, { name = "func_800C6144", vram = 0x800C6144, size = 0x9C }, - { name = "func_800C61E0", vram = 0x800C61E0, size = 0x8B8 }, + { name = "Mesh_QueueEmit", vram = 0x800C61E0, size = 0x8B8 }, { name = "func_800C6A98", vram = 0x800C6A98, size = 0x14 }, { name = "func_800C6AAC", vram = 0x800C6AAC, size = 0xB8 }, { name = "func_800C6B64", vram = 0x800C6B64, size = 0x84 }, diff --git a/decomp/thps2/recomp-host/tests/scenarios.py b/decomp/thps2/recomp-host/tests/scenarios.py index 527798b..698b78f 100644 --- a/decomp/thps2/recomp-host/tests/scenarios.py +++ b/decomp/thps2/recomp-host/tests/scenarios.py @@ -697,6 +697,163 @@ from menu_session import MENU_SESSIONS # noqa: E402 SESSIONS += MENU_SESSIONS +# Draw distance (2026-10-17): career (unlocked levels), one level with the setting under test. +# Same steps as THPS1's dd-probe: a paused spot compared per factor (DD_AB), then a ride with frame times. Exploration only (optin). +# DD_RIGHTS: RIGHT presses at the level select; DD_TAG / DD_OUT: output; DD_ARGS: extra host args; DD_RIDE "lefty:frames ..." (x=... for leftx). +_DD_DIR = _os.path.join(_os.environ.get("DD_OUT", "/tmp/dd"), _os.environ.get("DD_TAG", "run")) +_DD_SHOT = lambda name: [("ctl", f"pcap 1 {_DD_DIR}/{name}"), ("wait", 3), ("ctl", "pcap 0"), ("wait", 10)] +# DD_AB "K[:S] ...": factor K, with `dd showall S` first when given (1:0 = the game's own picture inside `full`); DD_TRACE=1: `dltrace 1` after +# each shot (the frame's commands in the host log: which course items are drawn, tools in the private notes); DD_VTRACE=1 / DD_VRAMSAVE=1: +# the same, and a RAM dump, at the "vantage" frame (DD_WATCH runs). +_DD_TRACE = [("ctl", "dltrace 1"), ("wait", 5)] if _os.environ.get("DD_TRACE") else [] +_DD_AB = [s for t in _os.environ.get("DD_AB", "1 2 4 8").split() for k, sa in [(t.split(":") + [""])[:2]] for s in ( + *([("ctl", f"dd showall {sa}")] if sa else []), + ("ctl", f"dd factor {k}"), ("ctl", "dd reset"), ("wait", 20), *_DD_SHOT(f"start_x{k}{'_s' + sa if sa else ''}"), *_DD_TRACE, ("dl", 30, 0), ("ctl", "dd"))] +SESSIONS += [ + dict(name="dd-probe", config="dd-config", fresh_pak=True, wav=False, optin=True, + args=["--skip-intro", "--hidden", "--unlock-levels", "--present-size", "960x540", "--aspect", "16:9", "--window", "1280x720", + "--refresh", "60"] + _os.environ.get("DD_ARGS", "").split(), + scenarios=[ + ("dd_probe", "draw distance: paused spot per factor (frame, triangles, buffers), camera swing, ride with frame times", [ + ("wait_for", "title"), ("go", "START", "title", "main_menu"), + *CAREER_TO_GOALS[:-1], + *[s for _ in range(int(_os.environ.get("DD_RIGHTS", "0"))) for s in (("press", "RIGHT"), ("wait", 45))], + ("shot", "level_select"), + ("press", "A"), ("wait", 150), ("shot", "goals"), + # DD_WATCH=1: start counting at the run flag (gRunActive 0 -> 1), so DD_RIDE and the "vantage" frame land on the same game frame in + # every run (THPS2/3 runs are repeatable from there; the load time before it is not). + *([("ctl", "dd showall " + _os.environ["DD_SHOWALL"])] if _os.environ.get("DD_SHOWALL") else []), # A/B of the zone visibility alone + *([("press", "A"), ("ctl", "watch @gRunActive 1")] if _os.environ.get("DD_WATCH") else [("press", "A"), ("wait", 300)]), + ("shot", "in_level"), + ("ctl", "dd"), + *([("ctl", f"ramsave {_DD_DIR}/ram_start.bin")] if _os.environ.get("DD_RAMSAVE") else []), + # DD_PRE "KEY:frames ...": presses before the ride, with a shot after each (e.g. with --skip-to-restart in DD_ARGS: the pause menu's + # SKIP TO RESTART to one of the course's restart points: repeatable spots all over the course) + *[s for st in _os.environ.get("DD_PRE", "").split() for s in (("press", st.split(":")[0]), ("wait", int(st.split(":")[1])), ("shot", "pre"))], + ("vpad", "attach"), + *[s for st in _os.environ.get("DD_RIDE", "").split() for s in ( + (("ctl", f"vpad axis leftx {st.split(':')[0][2:]}"),) if st.startswith("x=") else (("ctl", f"vpad axis lefty {st.split(':')[0]}"),)) + + (("wait", int(st.split(':')[1])),)], + *(_DD_SHOT("vantage") if _os.environ.get("DD_WATCH") else []), + *([("ctl", "dltrace 1"), ("wait", 2)] if _os.environ.get("DD_VTRACE") else []), + *([("ctl", f"ramsave {_DD_DIR}/ram_vantage.bin")] if _os.environ.get("DD_VRAMSAVE") else []), + ("ctl", "vpad axis lefty 0"), ("ctl", "vpad axis leftx 0"), + *([("press", "START"), ("wait", 45)] if _os.environ.get("DD_PAUSE", "1") == "1" else []), + *_DD_AB, + # DD_CTL "cmd1;cmd2;..." at the paused spot after DD_AB (exploration: "dd factor 2;dltrace 1;dd factor 4;dltrace 1") + *[s for c in _os.environ.get("DD_CTL", "").split(";") if c.strip() for s in (("ctl", c.strip()), ("wait", 20))], + # DD_BUDGET "CAP [REST PASS MARGIN]": `dd dlbudget` at the paused spot (a shot of the same frame after it) and for the ride + # (the byte budget forced to cut: near scenery and the game's own pass must stay) + *([("ctl", "dd dlbudget " + _os.environ["DD_BUDGET"]), ("wait", 20), *_DD_SHOT("budget"), ("ctl", "dd")] if _os.environ.get("DD_BUDGET") else []), + ("ctl", "dd mode " + _os.environ.get("DD_MODE", "original")), + *([("ctl", "dd factor " + _os.environ["DD_FACTOR"])] if _os.environ.get("DD_FACTOR") else []), + *([("ctl", "dd testcap " + _os.environ["DD_TESTCAP"])] if _os.environ.get("DD_TESTCAP") else []), + ("ctl", "dd reset"), + *([("press", "START"), ("wait", 45)] if _os.environ.get("DD_PAUSE", "1") == "1" else []), + ("ctl", "vpad axis lefty 32767"), ("wait", 120), ("ctl", "vpad axis lefty 0"), ("wait", 40), + *_DD_SHOT("back"), + ("dl", 30, 0), + ("ctl", "ftime start"), + *([("ctl", f"pcap {_os.environ['DD_TOUR']} {_DD_DIR}/tour")] if _os.environ.get("DD_TOUR") else []), # every Nth composited frame of the ride + ("ctl", "vpad axis lefty -32767"), + # DD_BURSTS=N: N short bursts of consecutive frames along the ride (b0, b1, ...: pairs of runs line up within a burst) + *[s for k in range(int(_os.environ.get("DD_BURSTS") or "0")) for s in (("wait", 40), *_DD_SHOT(f"b{k}"), + ("ctl", f"vpad axis leftx {(-32767, 0, 32767, 0)[k % 4]}"))], + ("wait", 150), + *_DD_SHOT("ride1"), + *[s for k in range(6) for s in (("ctl", f"vpad axis leftx {32767 if k % 2 else -32767}"), ("wait", 20), ("ctl", "vpad axis leftx 0"), ("wait", 20))], + *_DD_SHOT("ride2"), + ("dl", 30, 0), + *([("ctl", "pcap 0")] if _os.environ.get("DD_TOUR") else []), + ("ctl", f"ftime stop {_DD_DIR}/ftime.csv"), + ("ctl", "vpad axis lefty 0"), ("ctl", "vpad axis leftx 0"), ("wait", 20), + ("ctl", "dd"), ("ctl", "stats"), ("ctl", "dd dlprobe"), ("ctl", "dd heap"), + ]), + ]), +] + +# Draw distance in a two-player split screen (exploration, optin): the one-pad 2 PLAYER > TRICK ATTACK start of `two-players` (first course), +# then the same paused (THPS3: standing, DD_PAUSE=0) A/B as dd-probe (DD_AB, DD_TRACE, DD_ARGS, DD_OUT / DD_TAG) and a RAM dump. +SESSIONS += [ + dict(name="dd-2p", config="dd-2p-config", fresh_pak=True, wav=False, optin=True, + args=["--skip-intro", "--hidden", "--present-size", "960x540", "--aspect", "16:9", "--window", "1280x720", + "--refresh", "60"] + _os.environ.get("DD_ARGS", "").split(), + scenarios=[ + ("dd_2p", "draw distance in a two-player split screen: paused spot per factor (frames, traces, buffers)", [ + *BOOT_TO_MENU, + ("vpad", "attach"), ("vpress", "leftshoulder"), + ("wait", 90), + *[("vpress", "dpright"), ("wait", 40)] * 3, + *[x for _ in range(10) for x in (("vpress", "a"), ("wait", 150))], + ("wait_peek", "@gRunActive", 1, 60), + ("wait", int(_os.environ.get("DD_SETTLE") or "60")), + ("shot", "dd_2p_level"), + *([("vpress", "start"), ("wait", 45)] if _os.environ.get("DD_PAUSE", "1") == "1" else []), + ("ctl", f"ramsave {_DD_DIR}/ram_vantage.bin"), + *_DD_AB, + ("ctl", "dd showall auto"), ("ctl", "dd"), ("ctl", "dd heap"), ("ctl", "dd dlprobe"), + ]), + ]), +] + +# Heap headroom with the always-allocated 2048-matrix pools (2026-10-22, the private development notes; optin, measurement): one career +# course (DH_COURSE: list position), loaded, skated ~12 s and ended from the pause menu (END RUN), `dd heap` (low-water marks since the +# start) after the load, after the ride and after the end. DD_ARGS: extra host args (e.g. "--draw-distance full", "--dd-buffers original"). +_DH_PLAY = [("ctl", "vpad axis lefty -32767"), + *[x for k in range(12) for x in (("ctl", f"vpad axis leftx {(-32767, 0, 32767, 0)[k % 4]}"), + ("ctl", "vpad button a 1"), ("wait", 8), ("ctl", "vpad button a 0"), ("wait", 22))], + ("ctl", "vpad axis lefty 0"), ("ctl", "vpad axis leftx 0")] +SESSIONS += [ + dict(name="dd-heap", config="dd-heap-config", fresh_pak=True, wav=False, optin=True, + args=["--skip-intro", "--hidden", "--unlock-levels"] + _os.environ.get("DD_ARGS", "").split(), scenarios=[ + ("dd_heap", "heap headroom: one course, `dd heap` low-water marks", [ + ("wait_for", "title"), ("go", "START", "title", "main_menu"), + *CAREER_TO_GOALS[:-1], + *[s for _ in range(int(_os.environ.get("DH_COURSE", "0"))) for s in (("press", "RIGHT"), ("wait", 30))], + ("shot", "select"), + ("press", "A"), ("wait", 150), ("press", "A"), ("ctl", "watch @gRunActive 1"), ("wait", 30), + ("shot", "in_level"), ("ctl", "dd heap"), + ("vpad", "attach"), *_DH_PLAY, + ("ctl", "dd heap"), ("ctl", "dd"), + ("press", "START"), ("wait", 40), ("menu", "pause", 3), ("press", "A"), ("wait", 150), ("shot", "ended"), + ("ctl", "dd heap"), + ]), + ]), +] + +# Draw distance long run (2026-10-17): several courses back to back in one host, each played DD_LONG_S seconds with stick +# input, then pause -> END RUN and on through the end-of-run screens (DD_LONG_NAV: presses after END RUN) to the next course. Exploration +# only (optin): shots at every step, `dd` stats after each course (buffer peaks, governor), `stats` at the end. +_DDL_COURSES = [int(x) for x in _os.environ.get("DD_LONG_COURSES", "0 1 2").split()] # increasing list positions +_DDL_PLAY = lambda s: [("ctl", "vpad axis lefty -32767"), *[x for k in range(max(1, s // 2)) for x in ( + ("ctl", f"vpad axis leftx {(-32767, 0, 32767, 0)[k % 4]}"), ("ctl", "vpad button a 1"), ("wait", 8), ("ctl", "vpad button a 0"), ("wait", 52))], + ("ctl", "vpad axis lefty 0"), ("ctl", "vpad axis leftx 0")] +_DDL_NAV = [s for p in _os.environ.get("DD_LONG_NAV", "A:120 DOWN:20 DOWN:20 A:150").split() for s in (("press", p.split(":")[0]), ("wait", int(p.split(":")[1])), ("shot", "nav"))] +SESSIONS += [ + dict(name="dd-long", config="dd-long-config", fresh_pak=True, wav=False, optin=True, + args=["--skip-intro", "--hidden", "--unlock-levels", "--present-size", "480x270", "--refresh", "60"] + _os.environ.get("DD_ARGS", "").split(), + scenarios=[ + ("dd_long", "draw distance: several courses back to back (heap, buffers, governor)", [ + *([*BOOT_TO_MENU] if "BOOT_TO_MENU" in globals() and _os.environ.get("DD_LONG_BOOT3") else [("wait_for", "title"), ("go", "START", "title", "main_menu")]), + *CAREER_TO_GOALS[:-1], + *[s for i, c in enumerate(_DDL_COURSES) for s in ( + # the level select keeps its cursor on the last course: move by the difference + *[x for _ in range(c - (_DDL_COURSES[i - 1] if i else 0)) for x in (("press", _os.environ.get("DD_LONG_KEY", "RIGHT")), ("wait", 30))], + ("shot", f"select_{i}"), + ("press", "A"), ("wait", 150), ("press", "A"), ("ctl", "watch @gRunActive 1"), ("vpad", "attach"), + *([("wait", 30), ("ctl", f"ramsave {_DD_DIR}/long_a.bin"), ("wait", 300), ("ctl", f"ramsave {_DD_DIR}/long_b.bin")] if _os.environ.get("DD_LONG_RAMSAVE") and i == 0 else []), + *([] if _os.environ.get("DD_LONG_NODD") else [("ctl", "dd reset")]), ("ctl", "ftime start"), # DD_LONG_NODD: a host without `dd` (v0.1) + *_DDL_PLAY(int(_os.environ.get("DD_LONG_S", "120"))), + ("ctl", "ftime stop"), *([] if _os.environ.get("DD_LONG_NODD") else [("ctl", "dd"), ("ctl", "dd heap")]), ("shot", f"played_{i}"), + # DD_LONG_TIMEOUT=1: the run ends by itself (career 2:00; DD_LONG_S >= 125), not from the pause menu (the course-cycle crash, 2026-10-17) + *([("wait", 40), ("shot", f"timeup_{i}")] if _os.environ.get("DD_LONG_TIMEOUT") else + [("press", "START"), ("wait", 40), ("menu", "pause", 3), ("press", "A"), ("wait", 120), ("shot", f"endrun_{i}")]), + *_DDL_NAV, + )], + ("ctl", "stats"), *([] if _os.environ.get("DD_LONG_NODD") else [("ctl", "dd heap")]), + ]), + ]), +] # Course cycle (src/music.cpp; the crash after a few courses, 2026-10-17): The Hangar, School II, NY City, Venice Beach back to back # (Marseille, a competition, ends with a different menu), each ended from the pause menu. The level select keeps its cursor on the last # course; END RUN -> statistics -> A -> the run-over menu -> LEVEL SELECT (third entry). @@ -757,3 +914,172 @@ # when the machine is overloaded). Before the v0.2 fixes (ERZ decoder, native memset / memcpy) every start was over it. LF_GAME_MS = 140.0 # 2026-10-18: 104-120 ms after the fixes, 134-174 ms before (load 4-6) SESSIONS += [load_freeze_session("lf-check", **{**_LF, "courses": _LF["courses"][:3]}, limits={"game": 1000.0}, median=("game", LF_GAME_MS))] + +# Draw distance's matrix pools (2026-10-22, the private development notes): with dd_buffers = bigger (the default) the game gets pools of +# 2048 matrices at every course start and keeps its own cap (420) at `original`, so the setting takes effect at once, both ways; with +# --dd-buffers original the game's own pools (420) whatever the setting. +_DDB_TO_LEVEL = [("wait_for", "title"), ("go", "START", "title", "main_menu"), *CAREER_TO_GOALS[:-1], ("press", "A"), ("wait", 150), ("press", "A"), ("ctl", "watch @gRunActive 1"), ("wait", 30)] +SESSIONS += [ + dict(name="dd-buffers", config="dd-buffers-config", fresh_pak=True, wav=False, + args=["--skip-intro", "--hidden", "--draw-distance", "original", "--dd-buffers", "bigger"], scenarios=[ + ("dd_buffers_now", "default buffers: pools of 2048 with the game's cap 420 at Original; Full raises the cap at once, Original puts it back", [ + *_DDB_TO_LEVEL, + ("ctl_match", "dd heap", r"mtx_pool 2048 mtx_cap 420 "), + ("ctl_match", "dd", r"buffers=bigger .* mtx_cap=420 "), + ("ctl", "dd mode full"), ("wait", 30), + ("ctl_match", "dd heap", r"mtx_pool 2048 mtx_cap 2048 "), + ("ctl_match", "dd", r"mtx_cap=2048 "), + ("ctl", "dd mode original"), ("wait", 30), + ("ctl_match", "dd heap", r"mtx_pool 2048 mtx_cap 420 .* failed_calls 0 "), + ]), + ]), + dict(name="dd-buffers-own", config="dd-buffers-own-config", fresh_pak=True, wav=False, + args=["--skip-intro", "--hidden", "--draw-distance", "original", "--dd-buffers", "original"], scenarios=[ + ("dd_buffers_own", "--dd-buffers original: the game's own pools (420), at Original and at Full", [ + *_DDB_TO_LEVEL, + ("ctl_match", "dd heap", r"mtx_pool 420 mtx_cap 420 "), + ("ctl_match", "dd", r"buffers=original .* mtx_cap=420 "), + ("ctl", "dd mode full"), ("wait", 30), + ("ctl_match", "dd heap", r"mtx_pool 420 mtx_cap 420 "), + ("ctl", "dd mode original"), + ]), + ]), + # Opt-in demo: NY City at Original, then Graphics > Draw Distance > Full in the menu in the middle of the run, a ride: the pool use goes past + # the game's 420 with no restart (`dd`: mtx_hw > 420, mtx_capped=0). DD_ARGS: extra host args. + dict(name="dd-switch", config="dd-switch-config", fresh_pak=True, wav=False, optin=True, requires="menu", + fresh_files=["graphics.cfg"], + args=["--skip-intro", "--hidden", "--unlock-levels", "--window", "1280x720", "--draw-distance", "original"] + _os.environ.get("DD_ARGS", "").split(), scenarios=[ + ("dd_switch", "switch Original -> Full in the menu mid-run: the matrix pool use passes 420 at once", [ + ("wait_for", "title"), ("go", "START", "title", "main_menu"), *CAREER_TO_GOALS[:-1], + *[s for _ in range(3) for s in (("press", "RIGHT"), ("wait", 30))], + ("shot", "select"), + ("press", "A"), ("wait", 150), ("press", "A"), ("ctl", "watch @gRunActive 1"), ("wait", 30), + ("shot", "in_level_original"), + ("ctl", "dd reset"), ("wait", 60), + ("ctl_match", "dd", r"mode=original .* mtx_cap=420 "), + ("ui", "esc"), ("ui_state", "open=1"), ("ui", "tab 2"), ("ui", "click #lbl_dd_full"), ("log", r"draw distance: full"), ("ui", "esc"), + ("ctl", "dd reset"), + ("vpad", "attach"), + ("ctl", "vpad axis lefty -32767"), ("wait", 150), + *[s for k in range(6) for s in (("ctl", f"vpad axis leftx {32767 if k % 2 else -32767}"), ("wait", 20), ("ctl", "vpad axis leftx 0"), ("wait", 20))], + ("ctl", "vpad axis lefty 0"), ("wait", 30), + # pause -> RETRY: the run starts again (the game is not restarted); the course's start view is the busy one + ("press", "START"), ("wait", 40), ("press", "DOWN"), ("wait", 10), ("press", "A"), ("wait", 200), + ("shot", "in_level_full"), + ("ctl_match", "dd", r"mode=full .* mtx_hw=(42[1-9]|4[3-9]\d|[5-9]\d\d|1\d\d\d|20[0-4]\d) mtx_cap=2048 "), # past the game's 420, no restart + ("ctl_match", "dd", r"mtx_capped=0 "), + ("ctl", "dd heap"), + ]), + ]), +] + +# Draw distance stress (2026-10-23, the bigger display lists, the governor's ramp and the byte budget; optin): Full from the start. School +# II (the course that overran its 72 KiB display list) started DD_STRESS_N times (pause > RETRY runs the course gfx set-up again): each +# time the first ride with camera turns and quick setting changes (Original, Full, 2x, Full, a few frames apart); then END RUN and the +# same rides in NY City and Philadelphia. The session sets THPS_DD_CANARY=1 (guard bytes after the display lists) and checks that the +# guards are on (`dd dlprobe` canary 1; until 2026-10-26 it only asked for the variable in this comment, and `canary_bad 0` passed with no +# guard at all). Expected: no guard word changed, no budget cut at the default budget, no panic, no failed heap request. The forced cuts +# are dd-cut's. +_DS_RIDE = [("ctl", "vpad axis lefty -32767"), + *[x for k in range(6) for x in (("ctl", f"vpad axis leftx {(-32767, 32767)[k % 2]}"), ("wait", 15), ("ctl", "vpad axis leftx 0"), ("wait", 10))], + ("ctl", "dd mode original"), ("wait", 4), ("ctl", "dd mode full"), ("wait", 4), ("ctl", "dd mode 2x"), ("wait", 4), ("ctl", "dd mode full"), + *[x for k in range(4) for x in (("ctl", f"vpad axis leftx {(32767, -32767)[k % 2]}"), ("wait", 20))], + ("ctl", "vpad axis lefty 0"), ("ctl", "vpad axis leftx 0"), ("wait", 45)] +_DS_END = [("press", "START"), ("wait", 40), ("menu", "pause", 3), ("press", "A"), ("wait", 120), ("press", "A"), ("wait", 20), + ("press", "DOWN"), ("wait", 20), ("press", "DOWN"), ("wait", 20), ("press", "A"), ("wait", 150)] +_DS_START = [("press", "A"), ("wait", 150), ("press", "A"), ("wait_peek", "@gRunActive", 1, 60), ("vpad", "attach")] +_DS_CHECK = [("ctl_match", "dd", r"mode=full .* backoffs=\d+ panics=0 cuts=0 "), ("ctl_match", "dd dlprobe", r"canary 1 frames \d+ canary_bad 0 "), + ("ctl_match", "dd heap", r" dl 147456 .* dl_fallbacks 0 .* failed_calls 0 ")] +SESSIONS += [ + dict(name="dd-stress", config="dd-stress-config", fresh_pak=True, wav=False, optin=True, env={"THPS_DD_CANARY": "1"}, + args=["--skip-intro", "--hidden", "--unlock-levels", "--draw-distance", "full", "--dd-buffers", "bigger"] + _os.environ.get("DD_ARGS", "").split(), scenarios=[ + ("dd_stress", "School II starts and first rides, setting changes, camera turns; NY City and Philadelphia: guard bytes intact, no cut", [ + ("wait_for", "title"), ("go", "START", "title", "main_menu"), *CAREER_TO_GOALS[:-1], + ("press", "RIGHT"), ("wait", 30), ("shot", "select_school2"), + *_DS_START, ("ctl", "dd reset"), + *[x for _ in range(int(_os.environ.get("DD_STRESS_N", "4"))) for x in ( + *_DS_RIDE, ("shot", "school2_ride"), + ("press", "START"), ("wait", 40), ("menu", "pause", 1), ("press", "A"), ("wait", 150), ("vpad", "attach"))], + *_DS_RIDE, ("ctl", "dd"), *_DS_CHECK, *_DS_END, + ("press", "RIGHT"), ("wait", 30), ("press", "RIGHT"), ("wait", 30), ("shot", "select_ny"), + *_DS_START, *_DS_RIDE, ("shot", "ny_ride"), ("ctl", "dd"), *_DS_CHECK, *_DS_END, + *[x for _ in range(3) for x in (("press", "RIGHT"), ("wait", 30))], ("shot", "select_philly"), + *_DS_START, *_DS_RIDE, ("shot", "philly_ride"), ("ctl", "dd"), *_DS_CHECK, + ]), + ]), +] +# A RETRY and the zone record (review 2026-10-26, finding 7): School II at Full. RETRY runs the course set-up again and rebuilds the item +# list at the same address and count, so the record (which items the zone culling hid, which an event owns) must start again: an item made +# event-owned in the first run (`dd own`, as an event naming it would) is not after the RETRY, and the items the set-up named are the same +# number as before. Then the governor across a transition (finding 6, deterministic): `dd stale 99` gives a 99 % sample measured across a +# transition and one built before it, in one critical section: both dropped, Full keeps its ceiling; `dd feed 99` (the control case) gives +# a current one once the settle is over: it panics. +_DR_ITEM = 7 +SESSIONS += [ + dict(name="dd-retry", config="dd-retry-config", fresh_pak=True, wav=False, + args=["--skip-intro", "--hidden", "--unlock-levels", "--draw-distance", "full", "--window", "960x540"], scenarios=[ + ("dd_retry_zone", "an item an event owned in the last run is the game's again after RETRY; the set-up's own names are kept", [ + *_fs_to_run(1, 30), + ("ctl_match", f"dd item {_DR_ITEM}", r" event 0 "), + ("ctl_same", f"dd item {_DR_ITEM}", r" named (\d+) ", "named"), + ("ctl_match", f"dd own {_DR_ITEM}", r" event 1 "), + ("press", "START"), ("wait", 40), ("menu", "pause", 1), ("press", "A"), ("wait", 150), ("wait_peek", "@gRunActive", 1, 60), ("wait", 30), + ("ctl_match", f"dd item {_DR_ITEM}", r" event 0 "), + ("ctl_same", f"dd item {_DR_ITEM}", r" named (\d+) ", "named"), + ("ctl_match", "dd", r"course_starts=[2-9]"), + ]), + ("dd_stale_feedback", "a stale buffer sample after a setting change is dropped; a current one still acts", [ + ("ctl", "dd mode full"), ("wait", 10), ("ctl", "dd reset"), + ("ctl_match", "dd stale 99", r"ceiling=64\.00 .* panics=0 .* stale_dropped=1 settle_dropped=[1-9]"), + ("wait", 10), + ("ctl_match", "dd feed 99", r"ceiling=(?!64\.00)\d.* panics=1 "), + ("ctl_match", "dd mode full", r"ceiling=64\.00 "), + ]), + ]), +] +# The display-list budget with a forced cap (review 2026-10-26, findings 9 and 10): School II at Full with `dd dlbudget CAP` (DD_CUT_CAP, +# bytes) so tight that only some of the extra passes fit. The nearest extra items are reserved before the first pass with every pass at +# its bound, so the cuts leave out the farthest and none is made after the passes began (late_cuts=0: before the fix the passes after the +# first were counted at their average and an expensive far pass left the nearest ones out). The guard bytes are on (THPS_DD_CANARY) and +# intact. dd-stress checks the default budget (no cut at all). +_DC_RIDE = [("ctl", "vpad axis lefty -32767"), + *[x for k in range(8) for x in (("ctl", f"vpad axis leftx {(-32767, 32767)[k % 2]}"), ("wait", 15), ("ctl", "vpad axis leftx 0"), ("wait", 10))], + ("ctl", "vpad axis lefty 0"), ("wait", 20)] +SESSIONS += [ + dict(name="dd-cut", config="dd-cut-config", fresh_pak=True, wav=False, env={"THPS_DD_CANARY": "1"}, + args=["--skip-intro", "--hidden", "--unlock-levels", "--draw-distance", "full", "--window", "960x540"], scenarios=[ + ("dd_cut", "a forced display-list budget: the farthest extra items are left out before the passes, never the nearest after them", [ + *_fs_to_run(1, 30), ("vpad", "attach"), + ("ctl_match", "dd dlprobe", r"canary 1 "), + ("ctl", f"dd dlbudget {_os.environ.get('DD_CUT_CAP', '81920')} 0 0 0"), ("ctl", "dd reset"), + *_DC_RIDE, + ("ctl_match", "dd", r"cuts=[1-9]\d* cut_items=[1-9]\d* late_cuts=0 "), + ("ctl_match", "dd dlprobe", r"canary 1 frames \d+ canary_bad 0 "), + ("ctl", "dd dlbudget 0 0 0 0"), + ]), + ]), +] +# The bigger buffers on a constrained heap (review 2026-10-26, finding 1): THPS_DD_HEAP_FREE makes the planning see a free list of the given +# blocks from the course set-up's start, and every set-up request is taken from that view (`dd heap` view_fail counts the ones it could +# not have satisfied: the real allocator hangs there). One block of 266,240 bytes holds the game's own set-up (2 x 72 KiB lists, the +# matrix and vertex pools: 217,392) but not one 144 KiB list more: everything stays the game's size and nothing fails (before the fix the +# first list was enlarged and the second one's native size no longer fitted). With 600,000 + 900,000 bytes the lists and the matrix pools +# fit the first block and are enlarged (the second block, the one later requests rely on, is not touched). +_DHP_TO_COURSE = [*_fs_to_run(0, 30)] +SESSIONS += [ + dict(name="dd-heap-plan", config="dd-heap-plan-config", fresh_pak=True, wav=False, env={"THPS_DD_HEAP_FREE": "266240"}, + args=["--skip-intro", "--hidden", "--draw-distance", "full", "--window", "960x540"], scenarios=[ + ("dd_heap_plan_tight", "a free list with room for the game's own set-up only: every buffer the game's size, no request that would fail", [ + *_DHP_TO_COURSE, + ("log", r"draw distance: WARNING no room for a 147456-byte display list \(request 1"), + ("ctl_match", "dd heap", r"mtx_pool 420 .* dl 73728 dl_game 73728 .* failed_calls 0 .* view_fail 0"), + ]), + ]), + dict(name="dd-heap-plan-room", config="dd-heap-plan-config", fresh_pak=True, wav=False, env={"THPS_DD_HEAP_FREE": "600000,900000"}, + args=["--skip-intro", "--hidden", "--draw-distance", "full", "--window", "960x540"], scenarios=[ + ("dd_heap_plan_room", "room for the bigger lists and matrix pools in the first block: both enlarged, nothing fails", [ + *_DHP_TO_COURSE, + ("ctl_match", "dd heap", r"mtx_pool 2048 .* dl 147456 dl_game 73728 .* failed_calls 0 .* view_fail 0"), + ]), + ]), +] diff --git a/decomp/thps2/recomp-host/versions/eu/data_syms.toml b/decomp/thps2/recomp-host/versions/eu/data_syms.toml index 8724fed..365e0a5 100644 --- a/decomp/thps2/recomp-host/versions/eu/data_syms.toml +++ b/decomp/thps2/recomp-host/versions/eu/data_syms.toml @@ -8,6 +8,18 @@ gSControl = 0x800EA9A4 gSkater = 0x800EA59C gTvType = 0x800F5924 # GameEntry's init copies osTvType here (0x800B6A7C) gRunActive = 0x800DC030 # same role as in us (+0xC0 like gSControl: translate_data.py, 1 vote; behaviour checked on the EU ROM, tests: pause_back) +gFarPlane = 0x800E4B40 # these seven = us + 0xC0 (translate_data.py, 3-10 votes each) +gWorldFarCache = 0x800DBE08 # us + 0xC0 (translate_data.py, 7 votes) +gDlBufSize = 0x80101EB4 +gDlBufs = 0x80101ED0 +gDlHead = 0x800F8E24 # us + 0xC0 (the frame start's store, checked in the generated code) +gDlFrame = 0x800F8A38 # us + 0xC0 (same) +gMtxPools = 0x80101ED8 +gMtxCap = 0x80101EE4 +gVtxPools = 0x80101EF0 +gVtxCap = 0x80101F00 +gRenderer = 0x800FB078 # us + 0xC0: the same `lw a0` in World_DrawItems (eu 0x80041D08 = 0x8EE4B078) +gWorldItems = 0x800DD7B4 # us + 0xC0 (translate_data.py, 26 votes) gTimeScale = 0x800E886C # gPhysicsFrameTime * 7680 each frame (the frame update; Slow-Nic: / 4 while the skater does a trick) gMusNumChannels = 0x801020A0 # MUS library globals: channel count; USA + 0xC0 (MusHandleStop 0x800D403C) gMusChannels = 0x801020A8 # MUS library globals: the channel records (304 bytes each) diff --git a/decomp/thps2/recomp-host/versions/eu/host_force.toml b/decomp/thps2/recomp-host/versions/eu/host_force.toml index 9a3a151..5478497 100644 --- a/decomp/thps2/recomp-host/versions/eu/host_force.toml +++ b/decomp/thps2/recomp-host/versions/eu/host_force.toml @@ -23,6 +23,14 @@ splits = [0x800B6390] # USA 0x800B6360 + 0x30: the same hidden start after the "0x800BFFD4" = "MsgBox_SetText" "0x800570F4" = "Level_IsSelectable" "0x80058040" = "Cheat_IsOn" # = USA 0x80058010 (sigmatch C; same 7 instructions, its own base) +"0x80041994" = "World_DrawItems" # = USA (sigmatch A) +"0x800B3F8C" = "Mem_Alloc" # (heap, size, align): first fit (draw distance: the matrix-pool request at start-up) +"0x80033FDC" = "World_SetVisibilityInBox" # (min, max, ..., set mask, clear mask): the trigger scripts' zone culling; draw distance tracks which items it hid +"0x800B90E0" = "Gfx_SetupCourse" # = USA 0x800B90BC + 0x24 (the same size, 0x37C) +"0x800C50D0" = "Mesh_PassBegin" # = USA + 0x20 (the same body) +"0x800C50F0" = "Mesh_PassEnd" # = USA + 0x20 (the same body) +"0x800C6200" = "Mesh_QueueEmit" # = USA + 0x20 (the same body; the renderer offsets +0x21CC / +0x2AD8 are the same) +"0x80042EA4" = "World_FindItem" # (list, name checksum) -> item: how the scripts' per-item commands (show / hide / kill by name) and the gameplay code find an item "0x800B6848" = "Tv_IsHomeRegion" # returns gTvType == 0 (PAL console; USA's has the opposite sense): the region check "0x800D1EF4" = "Music_LoadSong" # = USA 0x800D1E34 + 0xC0 (same code) diff --git a/decomp/thps2/recomp-host/versions/host_syms.toml b/decomp/thps2/recomp-host/versions/host_syms.toml index d4b1329..aebaece 100644 --- a/decomp/thps2/recomp-host/versions/host_syms.toml +++ b/decomp/thps2/recomp-host/versions/host_syms.toml @@ -11,7 +11,10 @@ funcs = [ "thps_osSetEventMesg", "thps_osPiRawStartDma", "thps_osAiGetStatus", "thps_osContGetQuery", "Cop2_GteLoad", "Pi_DmaThread", "GameEntry", "View_SetFrustum", "View_ComputeCullPlanes", "MsgBox_SetText", "Level_IsSelectable", "Cheat_IsOn", - "Tv_IsHomeRegion", + "Tv_IsHomeRegion", "World_DrawItems", "Mem_Alloc", "World_SetVisibilityInBox", "World_FindItem", + "Mesh_PassBegin", "Mesh_PassEnd", # (renderer): a renderer pass begins (batching on) / ends (flush, Mesh_QueueEmit, lists emptied) + "Gfx_SetupCourse", # (): the course gfx set-up (each course start, RETRY): display lists, matrix and vertex pools (draw distance: bigger display lists) + "Mesh_QueueEmit", # (renderer): draws the renderer's mesh queue and sorted list of one pass (draw distance: game/draw_distance.cpp) "Music_LoadSong", # (player, song): the music player copies the next song over the playing one (src/music.cpp) ] @@ -30,6 +33,18 @@ addr = [ "gTvType", # game copy of osTvType (read by the TV-type accessor) "gTimeScale", # physics step word (256 = 1/30 s; every frame; Slow-Nic divides it by 4 during tricks) "gRunActive", # word: 1 only while a level run is being played (unpaused); 0 in the front end, pause and every screen over the level (src/run_state.cpp) + "gFarPlane", # draw distance: the gameplay camera's far plane (float; set with the level's fog) + "gWorldFarCache", # draw distance: the camera code's copy of gFarPlane for the world view (cleared to make it set that view again) + "gDlBufSize", # draw distance: bytes per top-level display list buffer (word, 0x12000) + "gDlBufs", # draw distance: the two display list buffers (pointers) + "gDlHead", # draw distance: the display list's write pointer (the next free command of the frame being built) + "gDlFrame", # draw distance: which of the two display lists the frame being built uses (word, 0 or 1) + "gMtxPools", # draw distance: the two matrix pools (pointers); gMtxCap = matrices per pool (word, 420) + "gMtxCap", + "gVtxPools", # draw distance: the two vertex pools (pointers, 16-byte vertices); gVtxCap = vertices per pool (word, 500) + "gVtxCap", + "gWorldItems", # draw distance: the course's item list (World_DrawItems' a0): count at +16, pointers at +12 + "gRenderer", # draw distance: pointer to the renderer (mesh queue: 512 entries at +0x1CC, count +0x21CC; sorted list: 96 at +0x21D8, count +0x2AD8) "gMusNumChannels", # MUS library: number of channels (word) "gMusChannels", # MUS library: pointer to the channel records (304 bytes each; src/music.cpp) ] diff --git a/decomp/thps2/recomp-host/versions/us/data_syms.toml b/decomp/thps2/recomp-host/versions/us/data_syms.toml index 2e22f92..d2a65af 100644 --- a/decomp/thps2/recomp-host/versions/us/data_syms.toml +++ b/decomp/thps2/recomp-host/versions/us/data_syms.toml @@ -7,6 +7,18 @@ gSControl = 0x800EA8E4 gSkater = 0x800EA4DC gTvType = 0x800F5864 # GameEntry's init copies osTvType here (0x800B6A4C) gRunActive = 0x800DBF70 # 1 while a level run is played, 0 paused / front end / screens over the level (RAM diffs; set by the pause handler at 0x8006C94C) +gFarPlane = 0x800E4A80 # draw distance: far plane (float, fog end / 16; written by func_80023CFC); View_SetFrustum's far for the gameplay camera +gWorldFarCache = 0x800DBD48 # func_80023800 sets the world view 0x800DB290 again only when gFarPlane != this copy +gDlBufSize = 0x80101DF4 # 0x12000 +gDlBufs = 0x80101E10 # [2] display list buffers (OSTask data_ptr) +gDlHead = 0x800F8D64 # the frame start (0x800B98C8) sets it to gDlBufs[gDlFrame] + 8; the frame end (0x800B9A24) checks head - buffer against gDlBufSize +gDlFrame = 0x800F8978 # 0 or 1: the buffer the frame being built uses +gMtxPools = 0x80101E18 # [2] matrix pools; allocator 0x800B8AD4 (count 0x80101E20, cap below, overflow counter 0x80101E28) +gMtxCap = 0x80101E24 # 420 +gVtxPools = 0x80101E30 # [2] vertex pools (count 0x80101E38) +gVtxCap = 0x80101E40 # 500 +gWorldItems = 0x800DD6F4 # the course item list (World_DrawItems, World_SetVisibilityInBox) +gRenderer = 0x800FAFB8 # pointer to the renderer: World_DrawItems hands it to the mesh batcher 0x800C51FC (`lw a0` at 0x80041CD8) gTimeScale = 0x800E87AC # gPhysicsFrameTime * 7680 each frame (the frame update; Slow-Nic: / 4 while the skater does a trick) gMusNumChannels = 0x80101FE0 # MUS library globals: channel count; MusHandleStop 0x800D3F7C / __MusIntMain 0x800D4A20 read both gMusChannels = 0x80101FE8 # MUS library globals: the channel records (304 bytes each) diff --git a/decomp/thps2/recomp-host/versions/us/host_force.toml b/decomp/thps2/recomp-host/versions/us/host_force.toml index eeed46b..983d5d7 100644 --- a/decomp/thps2/recomp-host/versions/us/host_force.toml +++ b/decomp/thps2/recomp-host/versions/us/host_force.toml @@ -24,6 +24,14 @@ splits = [0x800B6360] # run-time lookup miss (LOG H1): hidden start inside fun "0x800BFFB4" = "MsgBox_SetText" # (box, text, ...): stores a message box's text pointer "0x800570C4" = "Level_IsSelectable" # (level, mode): the level select's "available?" test "0x80058010" = "Cheat_IsOn" # (index): the cheat-on byte of cheat `index` (game state +0x15A6 + 4 * index); every cheat test of the game goes through it +"0x80041964" = "World_DrawItems" # the world item loop: skips items whose trigger-visibility bit (1, player 2: 0x40) is set; draw distance "full" (game/draw_distance.cpp) +"0x800B3F5C" = "Mem_Alloc" # (heap, size, align): first fit (draw distance: the matrix-pool request at start-up) +"0x80033FAC" = "World_SetVisibilityInBox" # (min, max, ..., set mask, clear mask): the trigger scripts' zone culling; draw distance tracks which items it hid +"0x800B90BC" = "Gfx_SetupCourse" # (): the course gfx set-up (callers 0x80037F78 .. 0x80038440): gDlBufSize = gDlUnits x 8 (0x800F8944, set to 0x2400 / 0x3000 by 0x800B8710), the two display lists (0x800B92B8), then the matrix and vertex pools; the front end has its own (0x800B9438) +"0x800C50B0" = "Mesh_PassBegin" # (renderer): batching on (+0x2AE0 = 1); World_DrawItems' caller 0x800336E0 wraps it in a begin / end pair +"0x800C50D0" = "Mesh_PassEnd" # (renderer): flushes the batch (0x800C5A64), Mesh_QueueEmit, then empties the lists and turns batching off +"0x800C61E0" = "Mesh_QueueEmit" # (renderer): draws the mesh queue (512 entries, the flush 0x800C5A64 drops the rest) and the depth-sorted list (96); called from the pass end 0x800C50D0 +"0x80042E74" = "World_FindItem" # (list, name checksum) -> item: how the scripts' per-item commands (show / hide / kill by name) and the gameplay code find an item "0x800B6818" = "Tv_IsHomeRegion" # returns gTvType != 0 (NTSC/MPAL console): the region check of func_80037350 "0x800D1E34" = "Music_LoadSong" # (player, song): MusHandleStop(old, 0), then the song is copied into the player's one buffer (src/music.cpp) diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md index 830af9d..66625b8 100644 --- a/docs/KNOWN-ISSUES.md +++ b/docs/KNOWN-ISSUES.md @@ -52,6 +52,12 @@ been played through by a person on this build: the park editor, split screen in - RT64 **compiles shaders the first time an effect appears**, which causes short hitches. - With the window hidden or covered, the app stops presenting frames; the game keeps running. - Apple GPUs have no 8x MSAA; asking for it turns MSAA off. +- **Draw distance:** 2x, 4x and Full were checked on one 15-second ride per course (all at 30 fps), not in long play, two-player + split screen or replays. At Full the fog is effectively off. A longer distance chosen during a run reaches the far scenery only + after a RETRY, the next course or a camera change (the game sets that part of the camera only then); a shorter one + applies at once. No restart is needed. + Parts of a course the game hides by area appear only as you get near them, as in the original game, and a few far + pieces the game never drew from that distance can look odd (the Hangar: a dark patch on the far wall). ## Stability diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 6ce9347..0e17aad 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -132,7 +132,7 @@ manual: down then up). **Esc** (controller: **Back**) opens the settings menu over the game. Its tabs: - **General:** skip intro, show FPS; - **Controls:** the layout presets, the Controllers list and rebinding (see [Controls](#controls)); -- **Graphics:** aspect ratio, HUD placement, two-player split screen, picture border, resolution, MSAA, frame rate, window mode; +- **Graphics:** aspect ratio, HUD placement, draw distance, two-player split screen, picture border, resolution, MSAA, frame rate, window mode; - **Sound:** volume; - **Cheats:** see [Cheats](#cheats). @@ -155,6 +155,7 @@ Written with defaults on the first start. ### `graphics.cfg` A command-line flag overrides the value and **is saved back**. + | Key | Default | Values | Flag | |---|---|---|---| | `aspect` | `16:9` | `original` (4:3), `16:9`, `21:9`, `expand` (follow the window) | `--aspect` | @@ -165,6 +166,8 @@ A command-line flag overrides the value and **is saved back**. | `hud` | `expand` | HUD placement in widescreen: `original` (the centred 4:3 area, as on a TV), `16:9` (the edges of a 16:9 picture), `expand` (the edges of the window's picture). No effect at `aspect original`. Menus, the pause menu and result pages always stay a centred 4:3 page. In a widened two-player split screen each player's HUD sits at the edge of their own view, with `original` too | `--hud X` | | `split` | `wide` | two-player split screen in widescreen: `wide` each player's view fills its half of the picture; `original` both views stay in the centred 4:3 area with black bands at the sides, as on a TV, and each player's HUD stays inside its view. No effect at `aspect original` | `--split X` | | `border` | `original` | the thin black frame around the 3D picture. The game draws its 3D 8 pixels in from each edge of its 320x240 frame, and a TV hid that edge. `original` keeps the frame; `fill` draws the picture all the way to the edges of the window, at every aspect ratio. Fill shows a little more of the scene at the same size: nothing is cut off and nothing is zoomed. Menus and loading screens keep their own frame | `--border X` | +| `draw_distance` | `original` | how far the course is drawn: `original` (the N64 game's own distance), `2x`, `4x`, `full` (the whole course). Scenery the game hides by area as you skate, secrets and things a goal reveals stay hidden until the game shows them. The fog moves out with it. While a frame comes close to filling the game's buffers, the distance steps back on that course. 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 | `--draw-distance X` | +| `dd_buffers` | `bigger` | the matrix buffers and display lists behind draw distance. `bigger` sets up larger ones at every course start, whatever `draw_distance` says, so that switching draw distance works at once; at `original` distance the game still uses only its own part of them. `original` keeps the game's own memory layout exactly; draw distance still works, but steps back sooner in the busiest courses. Not in the menu. A change needs a restart of the game | `--dd-buffers X` | The file also holds a `cfg_rev` line that the app writes itself; leave it. Files from earlier builds that say `hud=original` (the old default) are moved to `expand` once; choose `original` again to keep it. From 93ed4318f700148ea20d17535110030a01773a33 Mon Sep 17 00:00:00 2001 From: trentw <532672+trentw@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:07:18 -0700 Subject: [PATCH 8/9] Make building and testing easier to follow for a new contributor - BUILDING.md: the same Windows, runtime, `--first-run` and test-output notes as THPS1, and how to test Europe. - `tests/scenarios.py` lists the screens that look different in Europe (the main menu wheel with CAREER MODE and with CREATE SKATER on top, and the Create Skater roster) in `OWN_REFS`, so `test.sh --version thps2_eu --record` records Europe's own pictures. - WINDOWS-TEST.md's suite step uses `tools/test.sh`. VERIFICATION.md and the test README say where Europe's pictures go. Made with AI assistance. --- BUILDING.md | 30 +++++++++++++++---- CHANGELOG.md | 5 ++++ decomp/thps2/recomp-host/README.md | 3 +- decomp/thps2/recomp-host/docs/WINDOWS-TEST.md | 4 +-- decomp/thps2/recomp-host/tests/scenarios.py | 6 ++++ docs/VERIFICATION.md | 5 ++-- 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 8e7369f..b463e13 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -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. @@ -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 `. 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 @@ -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). diff --git a/CHANGELOG.md b/CHANGELOG.md index b62559f..59bccc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based 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`. ### Changed - Built on THPS64Host v0.2.0. @@ -38,6 +40,9 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based 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 diff --git a/decomp/thps2/recomp-host/README.md b/decomp/thps2/recomp-host/README.md index 2add29b..b119be1 100644 --- a/decomp/thps2/recomp-host/README.md +++ b/decomp/thps2/recomp-host/README.md @@ -164,7 +164,8 @@ 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** diff --git a/decomp/thps2/recomp-host/docs/WINDOWS-TEST.md b/decomp/thps2/recomp-host/docs/WINDOWS-TEST.md index 256d7d8..1184c10 100644 --- a/decomp/thps2/recomp-host/docs/WINDOWS-TEST.md +++ b/decomp/thps2/recomp-host/docs/WINDOWS-TEST.md @@ -104,8 +104,8 @@ Run from a terminal (PowerShell) in `build\cmake`: `[game 0x........]` frames and an `innermost game function` line. - [ ] Run two instances at once: the second one should fail cleanly. - [ ] The regression suite (needs the references to be recorded on this machine, Metal frames are not valid references): - `python -I tests\run.py --record`, then `python -I tests\run.py`. Attach the summary table. Expected differences: - anything pixel-compared may need `--rerecord`; the `crash_log` step accepts Windows exit codes. + `tools/test.sh --record`, then `tools/test.sh`, in Git Bash (BUILDING.md, "Tests"). Attach the summary table. + Expected differences: anything pixel-compared may need `--rerecord`; the `crash_log` step accepts Windows exit codes. ### 4.6 Hardware matrix, if available Intel integrated, an NVIDIA and an AMD GPU; D3D12 versus `--api vulkan`. diff --git a/decomp/thps2/recomp-host/tests/scenarios.py b/decomp/thps2/recomp-host/tests/scenarios.py index 698b78f..eeff27d 100644 --- a/decomp/thps2/recomp-host/tests/scenarios.py +++ b/decomp/thps2/recomp-host/tests/scenarios.py @@ -27,6 +27,12 @@ rev0_header=(b"THPS2", b"NTQP"), ) +# Checkpoints whose screen differs on another release, by version id: that release needs its own reference for them, recorded with +# `tools/test.sh --version ID --record` (every other checkpoint uses the first release's picture; games.toml `refs_from`). +OWN_REFS = { + "thps2_eu": ["main_menu", "wheel_create", "cas_roster"], +} + # name: size, regions [(region (x0, y0, x1, y1), gw, gh, tol)], record # A region (region, "mask", luma_min, min_frac) is a template match instead: at least min_frac # of the reference's pixels brighter than luma_min must have the same colour (for HUD text over diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md index b176836..af089f6 100644 --- a/docs/VERIFICATION.md +++ b/docs/VERIFICATION.md @@ -52,8 +52,9 @@ cheats, and the `hole` and `vp_split` checks of 60 Hz interpolation (both fail w | High frame rate | input latency; motion smoothness at 60 Hz | | Course cycle | four courses back to back (pause, END RUN, back to the select screen): each course's song loads only after the old song's channels have stopped reading the shared song buffer (`src/music.cpp`; without it this game crashed after a few courses); the last course has music | -Reference pictures are ROM-derived, so each machine records its own (`test.sh --record`); they are never committed. The suite -records `uptime` with its timings. Only one host runs at a time. +Reference pictures are ROM-derived, so each machine records its own (`test.sh --record`); they are never committed. Europe +uses USA's except for the few screens that differ (`OWN_REFS` in `tests/scenarios.py`), which +`test.sh --version thps2_eu --record` records. The suite records `uptime` with its timings. Only one host runs at a time. **Windows** (2026-10-14, Windows 11 x64, Intel Core i7-8809G, AMD Radeon RX Vega M, D3D12, 3840x2160 at 60 Hz, 150 %, on the head before the last fixes): the static build passes **41 passed, 0 failed, 4 skipped** for USA; Europe 39 passed with one failure in From 5b659147b90a6c0709c1bf152fc6970f62f4eefa Mon Sep 17 00:00:00 2001 From: trentw <532672+trentw@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:07:18 -0700 Subject: [PATCH 9/9] Settings: presets, fog, and frame rate Original or 60 - Docs: Frame Rate is Original (30) or 60, default 60 (THPS64Host v0.2.0 removes "Match display" and 120). - Tests: a new opt-in session `pace` measures the frame pacing of the attract demo or a ride. - Docs: the Graphics presets (Original and Enhanced) and the fog option (Original, Soft, Off; `graphics.cfg` and `--fog`, default Original) in the MANUAL and CHANGELOG. - Tests: `fog-check` in the suite (Soft rewrites the game's fog command; Original after Soft gives the same picture) and the opt-in `fog-shot`. - Test `fresh-defaults`: a first start with no graphics.cfg runs with Enhanced (Fill, Full) from the first frame. - README: the v0.2 features, an Original or Enhanced section (a table of the six settings and a comparison screenshot), and the fixed 21:9 known issue removed. Made with AI assistance. --- CHANGELOG.md | 14 +++ README.md | 22 ++++- decomp/thps2/recomp-host/tests/scenarios.py | 89 ++++++++++++++++++++ docs/MANUAL.md | 10 ++- docs/TESTING.md | 2 +- docs/screenshots/original-vs-enhanced.jpg | Bin 0 -> 140509 bytes 6 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 docs/screenshots/original-vs-enhanced.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index 59bccc2..da20ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,9 +29,23 @@ All notable changes to THPS2 64: Recompiled are listed here. The format is based (`--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 diff --git a/README.md b/README.md index 35e1901..f2c35c6 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,10 @@ 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. @@ -42,6 +45,22 @@ the Cheats tab gets you to every level quickly. - **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 @@ -89,7 +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. Full list: [docs/KNOWN-ISSUES.md](docs/KNOWN-ISSUES.md). ## Bugs and feedback diff --git a/decomp/thps2/recomp-host/tests/scenarios.py b/decomp/thps2/recomp-host/tests/scenarios.py index eeff27d..0077f99 100644 --- a/decomp/thps2/recomp-host/tests/scenarios.py +++ b/decomp/thps2/recomp-host/tests/scenarios.py @@ -1014,6 +1014,93 @@ ]), ]), ] + +# Frame pacing in a visible window (2026-10-23, the user's "stuttery" report; optin): the title left alone until the attract demo runs +# (frame times over PACE_DEMO frames), or with PACE_MODE=ride the first course (frame times over a 12 s ride). PACE_ARGS: host args (e.g. +# "--refresh 60"; another rate, test-only: THPS_TEST_REFRESH=display|N in the environment). Run it with THPS2_BINARY of the build to compare. +_PACE_RIDE = [("ctl", "vpad axis lefty -32767"), + *[x for k in range(12) for x in (("ctl", f"vpad axis leftx {(-32767, 0, 32767, 0)[k % 4]}"), ("wait", 30))], + ("ctl", "vpad axis lefty 0"), ("ctl", "vpad axis leftx 0")] +SESSIONS += [ + dict(name="pace", config="pace-config", fresh_pak=True, wav=False, optin=True, fresh_files=["graphics.cfg"], + args=["--skip-intro", "--window", "1280x720"] + _os.environ.get("PACE_ARGS", "").split(), scenarios=[ + ("pace", "frame pacing: attract demo and a ride, ftime in a visible window", [ + *([("wait_for", "title"), ("wait", int(_os.environ.get("PACE_IDLE", "600"))), ("shot", "demo"), + ("ctl", "ftime start"), ("wait", int(_os.environ.get("PACE_DEMO", "600"))), ("ctl", "ftime stop"), ("shot", "demo_end")] + if _os.environ.get("PACE_MODE", "demo") == "demo" else + [("wait_for", "title"), ("go", "START", "title", "main_menu"), *CAREER_TO_GOALS, ("go", "A", "goals", "in_level", 90), ("wait", 60), ("vpad", "attach"), + ("ctl", "ftime start"), *_PACE_RIDE, ("ctl", "ftime stop"), ("shot", "ride")]), + ("ctl", "dd"), + ]), + ]), +] + +# Fog (src/fog.cpp, graphics setting `fog`; optin, evidence): one course's run start, paused (FS_PAUSE=0: not paused), captured with the +# game's fog, Soft and Off, then the game's fog again (a second capture of the same state shows what moves on its own). FS_RIGHTS picks +# the course, FS_ARGS adds flags (e.g. `--draw-distance full`), FS_RIDE "lefty:frames ..." (x=leftx:frames turns) rides to another spot +# first, FS_STEPS "ctl command;shot NAME;..." adds steps (`fog soft 0.5;shot s50`: Soft starting at half the far plane). Captures: +# /fs_/ (composited frames at --present-size), the `fog` counters in the notes. +_FS_SHOT = lambda name: [("pcap", 3, f"fs_{name}")] +_FS_STEPS = [s for c in _os.environ.get("FS_STEPS", "").split(";") if c.strip() + for s in (_FS_SHOT(c.strip()[5:].strip()) if c.strip().startswith("shot ") else [("ctl", c.strip()), ("wait", 20)])] +_FS_AB = [ + ("ctl_match", "fog", r"fog=original "), *_FS_SHOT("original"), + ("ctl_match", "fog soft", r"fog=soft "), ("wait", 20), *_FS_SHOT("soft"), ("ctl_match", "fog", r"fog=soft "), + ("ctl_match", "fog off", r"fog=off "), ("wait", 20), *_FS_SHOT("off"), ("ctl_match", "fog", r"fog=off "), + ("ctl_match", "fog original", r"fog=original "), ("wait", 20), *_FS_SHOT("original2"), + *_FS_STEPS, +] +_FS_ARGS = ["--skip-intro", "--hidden", "--unlock-levels", "--present-size", "960x540", "--aspect", "16:9", "--window", "1280x720", + "--refresh", "60"] + _os.environ.get("FS_ARGS", "").split() +_FS_PAUSE = [ + *([("vpad", "attach"), + *[s for st in _os.environ["FS_RIDE"].split() for s in ( + ("ctl", f"vpad axis leftx {st[2:].split(':')[0]}") if st.startswith("x=") else ("ctl", f"vpad axis lefty {st.split(':')[0]}"), + ("wait", int(st.split(":")[1])))], + ("ctl", "vpad axis lefty 0"), ("ctl", "vpad axis leftx 0")] if _os.environ.get("FS_RIDE") else []), + *([("press", "START"), ("wait", 45)] if _os.environ.get("FS_PAUSE", "1") == "1" else []), +] +def _fs_to_run(rights, settle=2): + """Career mode to the run start of course RIGHTS (1: School II).""" + return [*BOOT_TO_MENU, *CAREER_TO_GOALS[:-1], *[s for _ in range(rights) for s in (("press", "RIGHT"), ("wait", 45))], + ("shot", "level_select"), ("press", "A"), ("wait", 150), ("press", "A"), ("ctl", "watch @gRunActive 1"), ("wait", settle)] +SESSIONS += [ + dict(name="fog-shot", config="fog-config", fresh_pak=True, wav=False, optin=True, fresh_files=["graphics.cfg"], args=_FS_ARGS, scenarios=[ + ("fog_shot", "fog: one course's run start with the game's fog, Soft and Off", [ + *_fs_to_run(int(_os.environ.get("FS_RIGHTS", "1")), int(_os.environ.get("FS_SETTLE", "2"))), + *_FS_PAUSE, *_FS_AB, + ]), + ]), +] + +# fog-check: School II's run start, paused, at the harness's fog = original. The `fog` counters say the game's fog command is found in +# each frame and rewritten with Soft; Original after Soft gives back the same picture (the edit is undone); Soft changes a little of it +# (the far haze) and not more than a quarter. +SESSIONS += [ + dict(name="fog-check", config="fog-check-config", fresh_pak=True, wav=False, present_every=10, fresh_files=["graphics.cfg"], + args=["--skip-intro", "--hidden", "--unlock-levels", "--aspect", "16:9", "--window", "1280x720"], scenarios=[ + ("fog_check", "fog: Soft rewrites the game's fog command, Original puts it back (same picture)", [ + *_fs_to_run(1), + ("press", "START"), ("wait", 60), + ("ctl_match", "fog", r"fog=original .*edits=0 "), + ("keep_present", "fog_original", "now"), ("wait", 30), ("keep_present", "fog_original_b", "now"), + ("ctl_match", "fog soft", r"fog=soft "), ("wait", 30), + ("keep_present", "fog_soft", "now"), + ("ctl_match", "fog", r"fog=soft start=\S+ tasks=[1-9]\d* found=[1-9]\d* edits=[1-9]\d* dirty=1 outstanding=[1-9]"), + ("ctl_match", "fog original", r"fog=original "), ("wait", 30), + ("keep_present", "fog_original2", "now"), + ("ctl_match", "fog", r"fog=original .* dirty=0 outstanding=0 "), # every edit undone or overwritten: Original skips the walk again + ("present_cmp", "fog_original", "fog_original2", 0, 0.002, "mask:fog_original_b"), + ("present_cmp", "fog_original", "fog_soft", 0.003, 0.25, "mask:fog_original_b"), # Soft adds haze far away, little else + ]), + ]), +] + +# The fresh-install defaults in force (review 2026-10-26, finding 2; the host's tests/menu_session.py): no graphics.cfg, not pinned to +# the test defaults; School II's run start, where the game writes its fog command. +from menu_session import fresh_defaults_session # noqa: E402 +SESSIONS.append(fresh_defaults_session(_fs_to_run(1))) + # A RETRY and the zone record (review 2026-10-26, finding 7): School II at Full. RETRY runs the course set-up again and rebuilds the item # list at the same address and count, so the record (which items the zone culling hid, which an event owns) must start again: an item made # event-owned in the first run (`dd own`, as an event naming it would) is not after the RETRY, and the items the set-up named are the same @@ -1043,6 +1130,7 @@ ]), ]), ] + # The display-list budget with a forced cap (review 2026-10-26, findings 9 and 10): School II at Full with `dd dlbudget CAP` (DD_CUT_CAP, # bytes) so tight that only some of the extra passes fit. The nearest extra items are reserved before the first pass with every pass at # its bound, so the cuts leave out the farthest and none is made after the passes began (late_cuts=0: before the fix the passes after the @@ -1065,6 +1153,7 @@ ]), ]), ] + # The bigger buffers on a constrained heap (review 2026-10-26, finding 1): THPS_DD_HEAP_FREE makes the planning see a free list of the given # blocks from the course set-up's start, and every set-up request is taken from that view (`dd heap` view_fail counts the ones it could # not have satisfied: the real allocator hangs there). One block of 266,240 bytes holds the game's own set-up (2 x 72 KiB lists, the diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 0e17aad..6ea1e27 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -132,7 +132,7 @@ manual: down then up). **Esc** (controller: **Back**) opens the settings menu over the game. Its tabs: - **General:** skip intro, show FPS; - **Controls:** the layout presets, the Controllers list and rebinding (see [Controls](#controls)); -- **Graphics:** aspect ratio, HUD placement, draw distance, two-player split screen, picture border, resolution, MSAA, frame rate, window mode; +- **Graphics:** a preset (Original or Enhanced), aspect ratio, HUD placement, draw distance, two-player split screen, picture border, resolution, MSAA, frame rate, window mode; - **Sound:** volume; - **Cheats:** see [Cheats](#cheats). @@ -158,15 +158,17 @@ A command-line flag overrides the value and **is saved back**. | Key | Default | Values | Flag | |---|---|---|---| +| (preset) | Enhanced | not a key: the menu's Preset row and `--preset` set six keys at once. `original`: `aspect original`, `hud original`, `split original`, `border original`, `refresh original`, `draw_distance original` (the game as on the N64). `enhanced`: `16:9`, `expand`, `wide`, `fill`, `60`, `full` (a new install starts with these). Choosing one replaces your choices for those six keys; resolution, MSAA, window mode and fog stay. The menu shows Custom when the keys match neither. An older file keeps every value it has, and a key it lacks takes the new default | `--preset original\|enhanced` | | `aspect` | `16:9` | `original` (4:3), `16:9`, `21:9`, `expand` (follow the window) | `--aspect` | | `window` | `auto` | the first window size: `auto` is 1280x720 (on Windows, enlarged by the display scaling); `WxH` is exact pixels | `--window WxH`, `--fullscreen`, `--windowed` | | `res_scale` | `0` | internal resolution multiplier; `0` is an integer scale of the window (3x at 720p) | `--res-scale N` | | `msaa` | `4` | `0`, `2`, `4` (Apple GPUs have no 8x; asking for it turns MSAA off) | `--msaa N` | -| `refresh` | `display` | `original` (the game's 30 fps), `display` (the display's rate: 60, or 120 on ProMotion), or a number from 30 to 1000 | `--refresh X` | +| `refresh` | `60` | `original` (the game's 30 fps) or `60` (in-between frames interpolated). An older `graphics.cfg` with `display` or another rate moves to 60 once | `--refresh X` | | `hud` | `expand` | HUD placement in widescreen: `original` (the centred 4:3 area, as on a TV), `16:9` (the edges of a 16:9 picture), `expand` (the edges of the window's picture). No effect at `aspect original`. Menus, the pause menu and result pages always stay a centred 4:3 page. In a widened two-player split screen each player's HUD sits at the edge of their own view, with `original` too | `--hud X` | | `split` | `wide` | two-player split screen in widescreen: `wide` each player's view fills its half of the picture; `original` both views stay in the centred 4:3 area with black bands at the sides, as on a TV, and each player's HUD stays inside its view. No effect at `aspect original` | `--split X` | -| `border` | `original` | the thin black frame around the 3D picture. The game draws its 3D 8 pixels in from each edge of its 320x240 frame, and a TV hid that edge. `original` keeps the frame; `fill` draws the picture all the way to the edges of the window, at every aspect ratio. Fill shows a little more of the scene at the same size: nothing is cut off and nothing is zoomed. Menus and loading screens keep their own frame | `--border X` | -| `draw_distance` | `original` | how far the course is drawn: `original` (the N64 game's own distance), `2x`, `4x`, `full` (the whole course). Scenery the game hides by area as you skate, secrets and things a goal reveals stay hidden until the game shows them. The fog moves out with it. While a frame comes close to filling the game's buffers, the distance steps back on that course. 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 | `--draw-distance X` | +| `border` | `fill` | the thin black frame around the 3D picture. The game draws its 3D 8 pixels in from each edge of its 320x240 frame, and a TV hid that edge. `original` keeps the frame; `fill` draws the picture all the way to the edges of the window, at every aspect ratio. Fill shows a little more of the scene at the same size: nothing is cut off and nothing is zoomed. Menus and loading screens keep their own frame | `--border X` | +| `draw_distance` | `full` | how far the course is drawn: `original` (the N64 game's own distance), `2x`, `4x`, `full` (the whole course). Scenery the game hides by area as you skate, secrets and things a goal reveals stay hidden until the game shows them. The fog moves out with it. While a frame comes close to filling the game's buffers, the distance steps back on that course. 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 | `--draw-distance X` | +| `fog` | `original` | the game's distance fog, in `graphics.cfg` and on the command line only (not in the menu or a preset). The game fades the far end of the course toward the sky colour, but in most courses only over about the last eighth of the view, so it is thin by design and far scenery can pop in. `original` keeps the game's fog; `soft` starts it earlier (at 70 % of the draw distance), so far scenery fades in instead of popping in, and never has less fog than the game's own; `off` turns it off, which shows a long draw distance most clearly. At draw distance `full` the game's fog is already far beyond the courses, so `soft` looks like `original` there. Menus, the HUD and the sky are never fogged | `--fog X` | | `dd_buffers` | `bigger` | the matrix buffers and display lists behind draw distance. `bigger` sets up larger ones at every course start, whatever `draw_distance` says, so that switching draw distance works at once; at `original` distance the game still uses only its own part of them. `original` keeps the game's own memory layout exactly; draw distance still works, but steps back sooner in the busiest courses. Not in the menu. A change needs a restart of the game | `--dd-buffers X` | The file also holds a `cfg_rev` line that the app writes itself; leave it. Files from earlier builds that say diff --git a/docs/TESTING.md b/docs/TESTING.md index 8a02894..d0cb5a8 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -30,7 +30,7 @@ You don't have to do all of this. Even one item, with a good report, helps. Abou game build. Note how long the build took and anything unclear on the way. 2. **Front end.** The intro and logos, the main menu, OPTIONS. Turn on **Skip Intro** in the settings menu, quit, and start again. 3. **Settings menu.** Open it with Esc (Back on a controller) and try each tab: aspect ratio (4:3, 16:9, 21:9), HUD placement, - frame rate (`display` and `original`), resolution, MSAA, window mode and full screen, volume. Quit and restart: the settings + frame rate (Original and 60), resolution, MSAA, window mode and full screen, volume. Quit and restart: the settings should still be there. 4. **A career.** Start a career and play a level for a few minutes: ollie, flip, grab, grind, manuals, the special meter and a special trick. Pause, **Retry** a few times, then **End Run**. diff --git a/docs/screenshots/original-vs-enhanced.jpg b/docs/screenshots/original-vs-enhanced.jpg new file mode 100644 index 0000000000000000000000000000000000000000..63b3c7529c8fb784d496ffba8c8fe42ae9d77657 GIT binary patch literal 140509 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 literal 0 HcmV?d00001