Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dm/games/01007EF00011E000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ No `tex_flush_upload` (a minute's boot saw it flush 32 CPU-filled textures with
- The stubs: `setWaitVsyncInterval` (`main+0x11269b8`, called once a frame at `main+0x11267c0`; the framework keeps the interval at fw+0x244, getter vtable +0x138, read by both callers of `VFR::updateInterval` and directly in 43 places, and passes it to `nvnWindowSetPresentInterval`) takes the host's step unless the VFR override (+0x29 flag, +0x2c interval) is set. `ksys::VFR::updateInterval` (`main+0x151d3d0`, x0 = the VFR at `[main+0x1e93890]`, called from `GameScene::calc` and the per-frame delegate `main+0x1280f80`) takes the framework's interval in the next frame (ratio = step / 2, base interval 2, and the int interval 16 pieces of logic read); its stub publishes the override at state +0x24 so the host holds frames that long. The step is not written into the VFR call itself, because the game presents on its own thread and the step can change between the two VFR calls of a frame.
- Mac (M4 Pro), from the title into the save's scene: 1.000 of real time at 60; with 12 ms added to every frame (`DM_TEST_PRESENT_LAG=1,12`) it drops to 30 and stays at 1.000. `DM_FPS_TARGET=30` holds 30 (saves power, also from `Documents/env.txt`); `0` runs the game's own interval controller.
- **Frame-step probes (iPhone).** The GPU clock follows the hold: on an iPhone the game went from 14.4 to about 25 ms of GPU time a frame at 30 fps with the same 4,900 draws, its CPU still on the performance cores, and never tried 60 again; a GPU time at the longer hold of at least 1.3 times its time at 60 that alone does not fit 60 (`nvn_fstep_gpu_slowed`) now allows probes. Stepped up by pipeline compiles after a cutscene, it took 12.8–15.2 ms of GPU time a frame at 30 fps (p50) where 60 had held for minutes, and stayed at 30 for 36 and 46 s until the player switched out and back (the same 4,900 draws took 5.8 ms right after a trip to the background and 13.5 ms half a minute later); a step up after 8 s or more at 60 now keeps that run's cost, and a cost at the longer hold of at most 1.1 times it allows a probe (`nvn_fstep_same`). Log lines: `frame step: hold 2 → 1 … (probe, …)`, `the probe held / failed`, `hold 1 held at 0.86`. Details: [development.md, Power](../../../docs/development.md#power).
- **Frame-step probes on the Mac (resolved, 2026-11-01).** An M5 Pro, fullscreen, present snapshots on, encode thread off, the host's profiler (`dm/host/sysprof.m`, `@gpu_mhz` / `@gpu_on`): at 60 fps macOS lowered the GPU clock from about 1,200 to 400 MHz over 4 s whenever the frame fit, until the frame's GPU work was about 11.7 of 16.6 ms (GPU powered 98%, occupied 70%: the game's own work after its fence wait, the first cut and segments drained faster than encoded leave 2–5 ms a frame without GPU work). A heavier scene (4,200–4,700 draws, GPU 20–27 ms at 400 MHz, 8 ms at 1,200) stepped the hold to 2 or 3 within three frames, where the GPU is occupied 55–75% and the clock fell to its floor (338 MHz); each probe back to hold 1 saw the clock rise 338 → 432 MHz and the GPU time fall 24.8 → 18.5 ms over its 33 frames, then failed, and hold 2 stayed for minutes. The probe's grace now lasts while its frames keep getting cheaper: [A probe's grace lasts while its frames keep getting cheaper](../../../docs/reference/frame-rate.md#a-probes-grace-lasts-while-its-frames-keep-getting-cheaper). The game waits on the fence of the frame it just submitted (`@sync_ms` 9.7 ms ≈ the GPU's end after the present at 400 MHz, 0 at 1,200), as on the Switch; the host's fence is the last commit's and wakes on the GPU's signal. Holding back the first segment (`DM_NVN_CUT_FIRST=ab`) only moved the GPU's start 4 ms later and lost 7 fps. With the fix (a 470 s run, `DM_FSTEP_PROBE_RAMP=ab`, encode thread on) every probe after the first 80 s held, 11 in a row, each within about a second; the probes that failed (presents 3834, 3929, 4083) were in a phase with the rule off.
- **56 fps once the screen shifts under the display link (resolved, 2026-11-02).** At one moment of a fullscreen session (87, 251, 342 and 355 s in four of them) the system starts showing every frame 4.17 ms after the display link's target, and from then on calls the link back a tick late about every 11–12 frames (56 fps on the 60 Hz link, 58.6 on the 120 Hz link; our callbacks are idle and on time). Without the link (mode 0) the game held 60.03 fps for 15 minutes, so the host now leaves the link for mode 0 when it sees the shift. In run 11 (27 minutes, no settings) it did so 2 s after the shift at 210 s, and the game held 60 fps in every 30 s window after it: [The display link loses ticks when the screen shifts 1/240 s under it](../../../docs/reference/frame-rate.md#the-display-link-loses-ticks-when-the-screen-shifts-1240-s-under-it).
- **Screens whose rate is not a multiple of 60 (from PR #27).** A cheap 50 ms frame shows 54.5 ms at 165 Hz; counted as late it kept the game at 20 fps for good, so the frame step lets a frame on time show one refresh longer than its hold (`shown_slack`, `nvn_fstep.h`).
- **Resolved: menu repeat twice as fast at 60 fps (2026-10-26).** `sead::ControllerBase::updateDerivativeParams_` (`main+0xeb0ef0`) adds a constant 1.0 (`main+0xeb0f78`..`88`) to each held button's count and repeats when `fmod(count - delay, pulse)` falls below that step, so delays and pulses were counted in frames: holding Down in the System menu for 0.3 s went 5 entries at 60 and 2 at 30 (found by MacNativeGames, `866beb9`). A `FRAME_STEP` stub at `main+0xeb0f88` takes the VFR's ratio (`+0x3c`, interval / 2) instead, keeping 1.0 when there is no VFR or the ratio is not positive. Mac, 60 fps: 0.2 / 0.3 / 0.4 s now move 1 / 2 / 3 entries, as the game does at 30. The pause menu's fast-repeat countdown (`main+0xe67d00`) already reads the VFR's delta.
- `DM_NVN_CUT` growing segments (64, 128, 256 … draws) were tried for fewer commits: the game fell from 60 to 30 fps in the same scene (GPU idle while the long last segments were encoded).
Expand Down Expand Up @@ -68,6 +70,7 @@ Used at `DM_FPS_TARGET=0` and for the interval the framework keeps while the VFR
- Same-pass feedback: over a thousand draws a frame sample the G-buffer of their own pass (`4d3d5a4408c2e8d9`, `753d0169ae3ce4f0` and others on the 1600×900 G-buffer), and outdoors about 460 of them also write the target they sample (decals reading and blending the normals). A fresh snapshot for each (`DM_NVN_FB_SNAP=all`) takes the Mac's GPU time from about 4 to 28 ms a frame; none has shown magenta blocks on the iPhone so far. The host now snapshots only draws that write what they sample and reuses a snapshot until the game's next barrier: about ten snapshots a frame, each reused by about 50 draws, GPU time within the runs' noise of no snapshots (3.7–4.5 against 3.5–4.1 ms p50, four runs each on the Apple M6). See [Same-pass feedback read through a snapshot](../../../docs/reference/rendering.md#same-pass-feedback-read-through-a-snapshot).

- **Shrines at 20–27 fps (GitHub issues #534 and #598; current, the fix not yet seen in a shrine).** The reporters' shrines (Oman Au on a MacBook Air M2; every shrine on an M3) run at 20–27 fps where the open world holds 60. In the #598 log the slow stretches have about 1,000 draws a frame, as the fast ones, but a GPU time of about 36 ms against 9–13 ms. They have about 240 `passes ended to land them for a read` a frame, all `refused … other GPU writes`, and 280–330 `load-all store-all passes` against about 100. The probe on the Shrine of Resurrection save found the one write behind that refusal there: the GPU particle dispatch `9f8116baffddaf81` binds its three outputs with size 0, which counted as a write anywhere ([A storage buffer bound with size 0 counted as a write anywhere](../../../docs/reference/rendering.md#a-storage-buffer-bound-with-size-0-counted-as-a-write-anywhere)). With the fix, that save's world-load burst (1,200 refusals) is gone. Opening Oman Au's final gate ends the slowdown even out of view (#534): an emitter that stops with the gate would fit, but no shrine save was here to show it.
- **The MetalFX present inside the frame (resolved, 2026-11-01; M5 Pro, fullscreen, snapshots, encode thread).** With the probe fix every probe held (11 in a row), but 320–460 s of the 470 s run ran at 48–56 fps at hold 1: frames 17–19 ms, the frame's own GPU 12.5–13.5 ms (14.7–16 on the cube-map frames every tenth), clock 340–380 MHz, GPU powered 97–99%, `@sync_ms` 0 or 9–12 ms by turns (the game reaches its fence wait before or after the GPU's end), the GPU idle 2.7–5.6 ms between frames and 1–4 ms inside the frame's span. The idle inside the span is the present: MetalFX's 1920x1080 → 3456x1944 scaler costs 2.6 ms a present at the floor clock (1 ms at 850 MHz) on the game's queue, so GPU 13 + present 2.6 + the chain's own idle about 2 ms = 17.8 ms: [The MetalFX present runs on the game's queue, inside its frame](../../../docs/reference/frame-rate.md#the-metalfx-present-runs-on-the-games-queue-inside-its-frame). The game's dynamic resolution does not react: it scales down only above 175% of a 60 Hz frame (29 ms) and back up below 150%, thresholds of its 30 fps design; at the host's step 1 a 13–16 ms GPU time never reaches them (and on the Mac the GPU time did not change with the scale: the frame is pass-bound). Each 30 fps spike was a step up after three frames late on screen at cost 0.83, then a probe that held after 35 frames. The earlier 60–80 s drop (probes 3834/3929/4083 failed) was in a DM_AB phase with the probe rule off. Encode thread on: present-thread CPU 5.9 → 3.4–4.4 ms, the submit 6.6 → 5.0 ms; now the profile's default (`enc_thread on`). The scaler now runs at the snapshot, in the gap after the frame's last segment (`DM_PRESENT_FX_AT_SNAP`, the reference entry). A 560 s run with `DM_AB=1800 DM_PRESENT_FX_AT_SNAP=ab` (2026-11-01): the present command buffer 2.6 → 0.34–0.52 ms and the snapshot's 0.11 → 2.35 ms per frame (`@present_gpu_ms` / `@snap_gpu_ms`), the GPU idle inside the frame's span 0 in both; at 60 fps with the frame fitting (phases 1–9, GPU 5–11 ms) frame p50 16.67–16.71 / p90 17.1–17.5 ms in both placements, and the heavy phases (GPU 12 ms, 3,400–3,650 draws) came after 320 s, when that run lost one frame in 5.5 to the display link in both placements (the next bullet), so the placement's frame-time gain in a GPU-bound scene is still to be seen (open). The user judged the picture better too.
- **Present snapshots (Mac).** The game's two swapchain textures held its CPU work to display ticks and the GPU at 444 MHz on an M2 Max ([present snapshots](../../../docs/reference/frame-rate.md#present-snapshots-the-games-swapchain-texture-free-at-once)). Judged by cost, the frame step stepped to 30 at a GPU time of 0.75 vsyncs. Zora's Domain from a Cemu save (120 Hz), frames on screen longer than 1/60 s by a quarter: 64 of 8,684 with neither, 41 with the GPU filler alone, 4 of 10,800 with snapshots alone (no step to 30 in 3 minutes, at 444 MHz throughout).

## Rendering
Expand Down Expand Up @@ -199,6 +202,9 @@ Before publishing, the fixes were integrated with main's pipeline archive and GP
- **Open, a SIGSEGV in `nn::util::RelocationTable::Relocate` at one cutscene ([issue #810](https://github.com/seedds/nxnative/issues/810)):** not a host bug as far as the reporter's files show. Their `Event/Demo154_2.sbeventpack` has our length (1,196,295 bytes) but another SHA-256 (`e74e7826…`, ours `b822e8a2…`), from a base NSP that fails the hash check, extracted with `--allow-damaged`; the model's relocation table is inside that file, so heap placement is not the cause. Open until the scene loads after their extraction from a clean NSP. Led to [Extraction of a damaged NSP ends in "The LinkedList is empty"](../../../docs/reference/packing.md#extraction-of-a-damaged-nsp-ends-in-the-linkedlist-is-empty) and [A crash from a damaged NSP's file looks like a host bug](../../../docs/reference/packing.md#a-crash-from-a-damaged-nsps-file-looks-like-a-host-bug).
- **Resolved, parallel rewriting (2026-10-31):** [Parallel scans preserve rewrite output](../../../docs/reference/rewriter.md#parallel-scans-preserve-rewrite-output). All 21 binary and TSV outputs of the three modules of v1114112 are byte-identical to the sequential rewriter. The full regression against `635e8008` passed: zero red diagnostics, matching screenshots, and no frame-time or memory regression (M4 Pro, Xcode 27.2 with the per-run compiler override on both revisions).
- [Benchmark captures perturb the measured frame pacing](../../../docs/reference/frame-rate.md#benchmark-captures-perturb-the-measured-frame-pacing).
- **Resolved, 56 fps once the screen shifts 4.17 ms under the display link in fullscreen, whatever the scene:** [The display link loses ticks when the screen shifts 1/240 s under it](../../../docs/reference/frame-rate.md#the-display-link-loses-ticks-when-the-screen-shifts-1240-s-under-it).
- **Resolved, the Mac's GPU clock at its floor at 30 fps in heavy scenes:** [A probe's grace lasts while its frames keep getting cheaper](../../../docs/reference/frame-rate.md#a-probes-grace-lasts-while-its-frames-keep-getting-cheaper).
- **Resolved, MetalFX's present cost inside the frame:** [The MetalFX present runs on the game's queue, inside its frame](../../../docs/reference/frame-rate.md#the-metalfx-present-runs-on-the-games-queue-inside-its-frame).
- **Resolved, two stale chain folders held 4.4 GB:** [Old shader library folders grew a game's build folder with each chain change](../../../docs/reference/shaders.md#old-shader-library-folders-grew-a-games-build-folder-with-each-chain-change).
- **Resolved: 1.6.0 (v786432) crashed 10-60 s into the first scene (issue #251)** in `h_nvnBufferGetAddress` (`main+0x14ef6e0`, reader `main+0x14ef440`) and in other places (`main+0x12970dc`, `+0x13615f4`, `+0x1445d48`, `+0x1db5cb8`, `+0x1472470`): the game's job queue (`main+0x13a0730`) takes its lock with a bare `ldxr` (no acquire), so on Apple cores two workers popped the same job, and two threads built draw lists into one view and arena (`main+0x14e8884` begin, commit `main+0x14ee3c0`). [Lock acquire without acquire ordering runs a queue item twice](../../../docs/reference/threads.md#lock-acquire-without-acquire-ordering-runs-a-queue-item-twice).
- **Resolved build guard and cache isolation; rendering cause open:** [Tested tools and compiler-specific shader caches](../../../docs/reference/shaders.md#tested-tools-and-compiler-specific-shader-caches).
Expand Down
3 changes: 3 additions & 0 deletions dm/games/01007EF00011E000/game.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ fps_target 60
# pipeline archive, the prepare step and the hybrid pipelines on the iPhone too (5,100 prewarm lines prepared in 0.28 s at start instead of compiling on first use)
present_snap on
pso_mtl4 on
# Metal encoding on its own thread on the Mac too (nvn_rec.h): the game waits on the fence of the frame it just submitted, so the submit thread's translation is in its frame.
# M5 Pro (2026-11-01, 470 s outdoors): present-thread CPU 5.9 → 3.4–4.4 ms a frame, the submit 6.6 → 5.0 ms; M4 Pro 4.30 → 3.16 ms (PR #62); on by default on iOS already
enc_thread on

# ---- Versions ----
# only the merged 1.9.0 copy is extracted; the base game's own build IDs are added when needed
Expand Down
Loading