Skip to content

Add adaptive 60 fps support for Pokémon Legends: Z-A 2.0.2 - #918

Open
thehobbit85 wants to merge 4 commits into
seedds:mainfrom
thehobbit85:codex/za-dynamic-60fps
Open

thehobbit85 wants to merge 4 commits into
seedds:mainfrom
thehobbit85:codex/za-dynamic-60fps

Conversation

@thehobbit85

@thehobbit85 thehobbit85 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

What and why

Z-A 2.0.2's native interval selector caps rendering at 30 fps. This adds a build-gated 60 fps option that feeds the existing host frame-step controller into the game's graphics interval and native clock dispatch, so ordinary frame drops advance game time by the corresponding whole-vsync duration. The launcher defaults to 60 fps and retains 30 fps; DM_FPS_TARGET=0 restores the original selector.

Independent verification requested before merging. At the 30 fps cap, an earlier headless station comparison measured p99 frame time rising from 37.6 to 52.3 ms (+39.1%). Both runs had competing shader compilation. The cause remains unresolved. The new series produced two qualified main runs and zero qualified PR runs before further benchmarking was waived for review.

The hook is restricted to main build b1f12fd919eae86ab8a978317677e64b, asserts the original instructions, and preserves the game's paused-clock behavior. It uses the original graphics setter, duration table, and all three clock adapters. The shared rewriter also reserves the state page before emitting appended patch-area branches: this build's key page ends at a 64 KB alignment boundary, where the previous layout overlapped state with code. Layouts that already have room, front patch areas, and modules without a frame step stay unchanged.

Adds ARM64 hook execution tests, state-layout tests, station benchmark/smoke routes, and the 60 fps launcher/gallery documentation. No host or shader-chain implementation changes.

Details: game note, state allocation finding.

Revision d56cb76ccd36f9b39fea1d69e960e3ea33d7a228 includes main 6136418bcd564f086cf57cb0a59e8f997d36445e. The upstream gallery now includes Pokémon Pokopia. The generated table retains both its entry and the Z-A caption. Upstream still lacks the build-specific Z-A frame-step and its shared state allocation.

Local validation on d56cb76ccd36. python3 -m unittest dm.tools.tests.test_docs dm.tools.tests.test_za_fps dm.tools.tests.test_patches — 29 passed and 5 optional Unicorn cases were skipped.

The gallery generator verified the published README. The historical station p99 regression still needs independent testing. These checks produced no new timing sample.

GitHub checks for this revision

Station comparison and pre-merge handoff

The 2.0.2 Mac assets were rebuilt from the NSPs on the M2 Max. All 28,603 Mac shader libraries built, and the applegpu_g14s archive contains 26,029 bases. Both revisions were set up to use the same archive with the matching chain, depth mode and shader pairs. This Mac used Metal 32023.921. The repository pins 32023.922.

The local saves/station fixture was copied from the original player save and checked byte-for-byte. Each trial got a fresh copy. A screenshot confirmed the starting scene and camera on the Lumiose Station platform. The headless route ran for 100 seconds at DM_FPS_TARGET=30 in docked mode with audio disabled. Measurements use seconds 40–90.

Qualified run FPS Frame p50 Frame p99
Main 99330eb8, run 1 25.4 39.62 ms 46.07 ms
Main 99330eb8, run 2 29.9 33.34 ms 33.85 ms

An earlier setup run of main measured 29.9 fps and 34.18 ms p99. The new PR attempts on a7be7395 were excluded for contention. None supplied a qualified candidate measurement. Both the original performance finding and the sustained-60-fps caveat remain open.

For an independent tester: run the full station route twice per revision on an idle Apple-silicon Mac with the same verified save, graphics settings and prewarmed archive. Check for competing work throughout each run and post the individual p99 values, FPS and screenshot comparisons here. If the increase repeats, investigate the per-frame CPU, GPU and present timings before merging.

Historical author validation

  • Rebased only this feature onto upstream 372a8d8a0404; unrelated controller work is excluded. The hook and state-layout logic are unchanged from the initial gameplay tests.
  • python3 -m unittest discover -s dm/tools/tests: 861 tests, 21 optional skips, passed on the rebased tree. The native Mac packaging tests required running outside the filesystem sandbox.
  • python -m unittest dm.tools.tests.test_za_fps dm.tools.tests.test_patches: 23 passed, including the five new hook tests under Unicorn/x86 Python. Covers both caps, varying steps, startup, disabled mode, registers/flags, original-byte/build gates, and state allocation.
  • Local execution of the original game selector, all three native adapters, the real duration table, pause dispatch, clock stores, and graphics setter: 24 cases passed again after rebasing. This check uses the owner's original executable and is not committed.
  • make -C dm/gfx/tests GAME=0100F43008C44000 -j6 run: complete Metal graphics suite passed, including the shared frame-step controller tests.
  • dm/tools/lint_python.py: 391 baseline findings, zero new findings. Generated docs and diff whitespace checks passed.
  • Pokémon Legends: Z-A 0100F43008C44000, 2.0.2, on Apple M2 Max / macOS 26.6 / Xcode 27.0. Earlier windowed gameplay covered the station, camera movement, and timed two-second walks. Native clock counters measured 59.70 game seconds / 59.73 real seconds (1.000) and 45.57 / 45.52 (1.001) through natural frame drops; a probe saw all 11 clocks receive 33,333,334 ns once per presented frame at hold 2.
  • The earlier self-contained Mac app's headless smoke reached station gameplay for 120 seconds, produced a correct screenshot, and exited gracefully. Its resource signature verified. That local app predates the PR rebase and is not distributed with this PR.
  • Historical full headless regression: upstream 372a8d8a0404 versus feature commit 68d75dfab2f9, 100 seconds each, target 30. Both reached gameplay with matching screenshot dHashes, unchanged 28,603 shaders, and no new red-line kinds. The overall result is REGRESSION due to p99 frame time, not a clean performance pass; exact report below. --record correctly did not write a green validated.tsv.

Caveats and remaining validation

  • This enables a 60 fps cap; it does not make the GPU render every scene at 60. The title held 60 fps, but the station took roughly 27 ms of GPU work per frame and held mostly 30 fps in the windowed test. Sustained 60 fps gameplay has not been demonstrated on this M2 Max.
  • Normal-speed compensation is bounded. The shared controller uses one, two, or three vsyncs per update (60/30/20 fps) and bounded catch-up for late frames. Sustained rendering below 20 fps or long stalls can still slow simulation. This PR does not change those controller limits.
  • Natural frame drops, variable-step execution, startup, disabled mode, and paused clocks were checked. There is no controlled runtime stall-injection test establishing behavior for every hitch pattern.
  • Runtime coverage is the station arrival save, standing, camera movement, and short walks. Battles, longer world transitions, other cutscenes, full-game progression, and audio synchronization have not been systematically checked. These automated runs had audio disabled.
  • No iPhone/iOS runtime validation, other Mac GPU validation, or other Z-A version validation. Other main builds receive no fixed-address hook; there is no automatic function-matched port for frame-step stubs.
  • Only Z-A was available for game regression testing. regress.py impact broadly flags the rewriter for every game (the available manifests list Hades, Breath of the Wild, Pokémon Scarlet, and Tears of the Kingdom; 122 profiles have no manifest); this PR limits the shared change to registered frame-step mains and tests every key-page alignment, SDK/unregistered modules, and front layouts. Other games remain unchecked.
  • Window focus and Spaces changes interrupted some windowed attempts. The regression and final automated comparisons therefore ran headlessly at the user's request, using a temporary harness wrapper that removes DM_WINDOW/DM_WINDOW_TOP/DM_METAL_HUD and records DM_TEST_HEADLESS=1. Headless runs establish execution/rendering, not display-link or fullscreen pacing.
  • The historical PR regression also ran while an unrelated shader compiler was active (--wait-idle 0); both runs are marked busy. The run uses the preserved pipeline archive stamped for chain b7cd656abd7c, rather than the shared build directory whose stamp changed during PR preparation. Its timings are non-isolated. The p99 increase could reflect load, patch behavior, or both; an idle-machine rerun is required before merge.
  • The four single-run headless walking samples below experienced changing GPU load (individual GPU frames reached 191.64 ms). They are not evidence of a performance improvement, a slowdown caused by the patch, or a precise cap-to-cap comparison. None reached the second 1,800-frame counter sample needed for the harness's game_speed result; the measured normal-speed evidence comes from the separate windowed clock run.
  • Existing SetRenderEnableConditional (the host draws anyway) and retry-success memory-pool red lines occurred in the earlier tests. No new red-line kinds were introduced in the recorded comparison. This PR does not resolve those existing limitations.
  • Native arm64 Unicorn 2.1.4 faulted during its own initialization on this Mac, before executing a test. The five hook tests were executed successfully using Unicorn under x86 Python/Rosetta; the game and Metal runs themselves were native arm64.
  • The tracked station route depends on the owner's local <NSP folder>/saves/station fixture. The rebuilt Mac assets and restored fixture are local test inputs. The package, game files, save and screenshots are excluded from the PR.

Earlier headless benchmark samples

station-walk, 40–140 seconds, one run per variant; these pre-rebase measurements are retained with the load caveat above. The hook and frame-state layout logic did not change during the rebase.

Variant Operation mode Measured fps Frame p50 / p99 (ms) GPU p50 (ms)
Original, target 30 Handheld 28.5 33.37 / 71.16 26.43
Patched, target 30 Handheld 22.4 37.05 / 111.35 32.77
Patched, target 60 Handheld 20.6 49.72 / 77.70 36.31
Patched, target 60 Docked 19.7 49.34 / 109.89 36.35

Regression report: full check — REGRESSION

Baseline origin/main 372a8d8, candidate HEAD 68d75df; Apple M2 Max, macOS 26.6, Xcode 27.0; 237.4 s

game status shaders (base → cand) newly failing newly building changed MSL interface (new / known) boot fps p50 frame p99 ms peak MB prepare s new RED
LegendsZA 0100F43008C44000 REGRESSION 28603/28603 → 28603/28603 0 0 0 0 / 0 ok 29.8 → 29.6 37.6 → 52.3 2352 → 2317 None → None 0

LegendsZA 0100F43008C44000: REGRESSION

  • REGRESSION: frame p99 37.6 → 52.3 ms (+39.1 %)
    • screenshots (s: dHash bits / RGB diff / baseline noise): 50: 0/0.45/0, 80: 0/0.45/0
    • baseline: exit None, first frame 1.4 s, max frame 2927, reached True, prepare None s, prebuild 3.2 s (0 built, 0 failed), fps 29.8, p50/p99 33.33/37.6 ms, peak 2352 MB, busy Mac
    • candidate: exit None, first frame 1.4 s, max frame 2918, reached True, prepare None s, prebuild 0.9 s (0 built, 0 failed), fps 29.6, p50/p99 33.31/52.3 ms, peak 2317 MB, busy Mac
    • time: chain 31.1 s, smoke 205.9 s

Docs

@thehobbit85
thehobbit85 force-pushed the codex/za-dynamic-60fps branch from 176ca3e to a7be739 Compare November 2, 2026 06:00
@thehobbit85
thehobbit85 marked this pull request as ready for review November 2, 2026 08:31
@thehobbit85
thehobbit85 force-pushed the codex/za-dynamic-60fps branch 5 times, most recently from e4d94a0 to f0895cc Compare November 2, 2026 17:44
@thehobbit85
thehobbit85 force-pushed the codex/za-dynamic-60fps branch from f0895cc to d56cb76 Compare November 3, 2026 02:57
@github-actions github-actions Bot added rewriter The NSO rewriter and per-build patches tools Tools and scripts (dm/tools, scripts) docs Documentation game: 0100F43008C44000 Pokémon Legends: Z-A labels Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0100F43008C44000 Pokémon Legends: Z-A rewriter The NSO rewriter and per-build patches tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants