diff --git a/dm/games/0100346017304000/README.md b/dm/games/0100346017304000/README.md index 8e8de63..f19d33d 100644 --- a/dm/games/0100346017304000/README.md +++ b/dm/games/0100346017304000/README.md @@ -6,16 +6,18 @@ Update 1.1.1 (v131072) over the base game, SDK 12.3.3 (no other game's `sdk` bui | Setting | Where | Why | |---|---|---| -| Frame limiter wait skipped (`main+0x443fd4`, `b.hs end` → `b end`) | [`patches.py`](patches.py) `CUTS`, `main` build `f5ab74b9…` | its 2 ms sleep rounds ended frames at 18.2 ms (55 fps); see [Frame rate](#frame-rate) | +| Frame limiter threshold 0 when the game's vsyncs per frame N = 1 (`main+0x443fac`) | [`patches.py`](patches.py) `CUTS`, `main` build `f5ab74b9…` | its wait from the frame's start tick made every frame 18.2 ms (55 fps); the present pacing holds one vsync, and N = 2 scenes keep the wait; see [Frame rate](#frame-rate) | | `op_mode handheld` | [`game.tsv`](game.tsv) | handheld, the Switch build plays by touch; docked it reads only the pads | | `pad_layout touch` | `game.tsv` | the Tap layout sends every touch to the game (the pads' corners would cover the cards, the menu icons at the top right and the on-screen B button) | | 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000`, `sys_resource 0x0` | `game.tsv` | the NPDM declares no system resource (META +0x14 = 0): no virtual address memory; `nninitStartup` sets a heap of the pool − used memory (3.14 GB), which the game maps only as it touches it. The default 64 MB of used memory fits its small modules | ## Frame rate -- **Resolved: 55 fps with every frame 18.2 ms, the main thread asleep most of the time** (the menus and the town; the opening movie, paced by its own player, ran at 60). KTGL's frame end (`main+0x443e8c`, after the present) waits until N / 60 s have passed since the frame's start tick (taken at `main+0x443d44` after the previous present; N = 1, the int at GOT 0xa1c088), polling `nn::os::GetSystemTick` and sleeping 2 ms per round (`main+0x4c9f24`, `SleepThread`). With about 2 ms of work a frame the rounds end at 18.2 ms, never at 16.7: the Mac ran 55 fps (frame time p50 18.17 / p95 18.69 ms), every 11th frame on screen for two vsyncs. -- How it was found: sampling the host (`sample `: 90% of the main thread in `dm_guest_sleep_ns` under a guest return address of `main+0x443d74`) and a probe on the target's multiply (x20 = 1 frame, x0 = 19.2 MHz). -- **Current:** the game presents at interval 1, so the swap chain holds it to the display anyway; the cut makes the check jump straight to the end, and the host's present pacing (the acquire waits) sets the 60. In the town: 60.0 fps, frame time p50 16.66 / p95 16.85 ms, display interval p99 17.0 ms; the main thread waits in the acquire instead of nine sleeps a frame (wakeups 2,050 → 320 a second, CPU energy 1.7 → 0.7 mJ a frame). +- **Resolved: 55 fps with every frame 18.2 ms, the main thread asleep most of the time** (the menus and the town; the opening movie, paced by its own player, ran at 60). KTGL's frame end (`main+0x443e8c`) presents, acquires the next texture (the present wrapper `main+0x4fcf4c`), then waits until N / 60 s have passed since the frame's start tick, polling `nn::os::GetSystemTick` and sleeping 2 ms per round (`main+0x4c9f24`, `SleepThread`). The start tick is read at `main+0x443d44`, after the scene's update (the frame object's vtable +0x10, `main+0x443cec`). +- **Cause, in the game's code:** each frame therefore lasts N / 60 s plus the time from the wait's last poll to the next start tick, rounded up to the 2 ms rounds. That is never exactly one vsync, on any hardware. The host's `SleepThread` (`sleep_precise`) and tick are exact, and a model of the loop gives 54 fps both with the host's acquire, which returns at once when a frame is late, and with the Switch's double-buffered one, which blocks until the presented frame is on screen and so only moves where the rounds start. On the Mac, without a cut: 54.6 fps at the title, frame time p5 18.15 / p50 18.28 ms, every 11th frame on screen for two vsyncs. +- **N:** the int at GOT 0xa1c088 (data `0x9ec764`, initially 1) is the game's vsyncs per frame. `CPrivateBattleWindow` and `CPrivateBattleTutorialWindow` set it to 2 when they start (`main+0xf02d0`, `main+0xec4cc`), and `main+0xf04f4` puts it back to 1. The scenes' code counts 60 / N frames a second (`main+0x2f29c8`, `main+0x3106dc`). The game presents at interval 1 in every scene, so in those scenes only this wait holds a frame for two vsyncs. N stayed 1 in probe runs through the menus, the town, the field battle tutorial, a council, the castle training ground and the world map. The `CPrivateBattle` scenes, small unit battles by their assets (`PBBATTLE/PBUNIT_*`), have not been reached on the Mac yet (open). `main+0xea8b0` starts one and is called from about 50 event sites; the tutorial version comes from an event-script command (`main+0x163dd8`). +- How it was found: sampling the host (`sample `: 84% of the main thread in `dm_guest_sleep_ns` under `main+0x443d74`, 15% in `nvnWindowAcquireTexture` under the present wrapper), the per-frame times (`DM_NVN_TSV`), and probes on N's writers. +- **Current:** a cut at `main+0x443fac` computes the threshold N × tick frequency / 60 with `udiv` (the compiler's multiply by 1/60 took one more instruction), then sets it to 0 when N = 1. The wait then returns after its first tick, which it still stores, and the host's present pacing (the acquire waits) holds the frame to one vsync. With N = 2 the wait stays the game's own. The first cut (`b.hs` → `b` at `main+0x443fd4`) skipped the wait for every N: with N forced to 2 at the title it presented 60 frames a second against the game's 28.8, for scenes that count 30. At the title with the current cut: N = 1 gives 60.0 fps (frame time p50 16.67 ms, wakeups 2,050 → 300 a second); N forced to 2 gives 28.9 fps, the game's own pace. In the town: 60.0 fps, frame time p50 16.66 / p95 17.08 ms, about 300 wakeups a second, no red line. ## Input @@ -34,7 +36,7 @@ Update 1.1.1 (v131072) over the base game, SDK 12.3.3 (no other game's `sdk` bui ## Status on the Mac - The notice, the logos and the opening movie, the title, a new game (1560, Kinoshita Tokichiro) through the opening story, the town, the council with a battle proposal, the territory map, a rest at home, a save and a load, and the field battle tutorial, docked with buttons and in handheld mode by touch. -- 60 fps (frame time p50 16.66 ms, p95 16.85 ms in the town), sound; about 0.7 GB resident and 1.6 GB of Metal allocations, flat over 15 minutes. No red line on that route. The prewarm list covers it (18 lines). +- 60 fps (frame time p50 16.66 ms, p95 17.08 ms in the town), sound; about 0.7 GB resident and 1.6 GB of Metal allocations, flat over 15 minutes. No red line on that route. The prewarm list covers it (18 lines). - **Not a host bug:** the town's top-right panel runs off the screen on a council day (its "council today" text is longer than the usual "n days left"): the panel starts where the manual's screenshot has it and widens for the longer text; on other days it fits. ## Mac routes @@ -48,3 +50,4 @@ Update 1.1.1 (v131072) over the base game, SDK 12.3.3 (no other game's `sdk` bui General host changes first needed here; each applies to every game: - **`oe::SetCpuOverclockEnabled`** (`hle_oe.c`) and **gestures for `hid::GetGestureStates`** recognized from the touch points (`gesture_sample`: yuzu's rules plus an 8 px slop, `t_gesture`, `DM_TOUCH_LOG`, `DM_CTL` `drag`): [CPU overclock calls without an apm session](../../../docs/reference/services.md#cpu-overclock-calls-without-an-apm-session), [Touch gestures recognized from the touch points](../../../docs/reference/services.md#touch-gestures-recognized-from-the-touch-points). +- **KTGL's frame-end wait** (a game-code cause; this profile's cut, no host change): [A frame-end wait polled in 2 ms sleeps](../../../docs/reference/frame-rate.md#a-frame-end-wait-polled-in-2-ms-sleeps). diff --git a/dm/games/0100346017304000/patches.py b/dm/games/0100346017304000/patches.py index 366ec56..2531847 100644 --- a/dm/games/0100346017304000/patches.py +++ b/dm/games/0100346017304000/patches.py @@ -1,13 +1,19 @@ # Taiko Risshiden V DX (0100346017304000): the rewriter's patches of this game's own builds. dm/tools/patch_nso.py loads every profile's patches.py and applies those of a module's NSO build ID; # CUTS, OBS and FRAME_STEP as described there ("Registered patches"). Patches of Nintendo SDK module builds are in dm/tools/sdk_patches.py CUTS = { - # Taiko Risshiden V DX 1.1.1 (v131072) main: KTGL's frame end (main+0x443e8c, after the present) waits until N / 60 s have passed since the frame's start tick (taken at main+0x443d44 - # after the previous present; N = 1, the int at GOT 0xa1c088), polling nn::os::GetSystemTick and sleeping 2 ms per round (main+0x4c9f24, SleepThread). With about 2 ms of work - # a frame the rounds end at 18.2 ms, never at 16.7: the Mac ran 55 fps in the menus and the town (frame time p50 18.17 / p95 18.69 ms), every 11th frame on screen for two - # vsyncs. The game presents at interval 1, so the swap chain holds it to the display anyway; the check jumps straight to the end, and the host's present pacing (the acquire - # waits) sets the 60. In the town then: 60.0 fps, frame time p50 16.66 / p95 16.85 ms, display interval p99 17.0 ms; the main thread waits in the acquire instead of nine - # sleeps a frame (wakeups 2,050 → 320 a second, CPU energy 1.7 → 0.7 mJ a frame). The opening movie, paced by its own player, was at 60 before + # Taiko Risshiden V DX 1.1.1 (v131072) main: KTGL's frame end (main+0x443e8c: the present wrapper, which presents and acquires the next texture, then the wait) waits until + # N / 60 s have passed since the frame's start tick, polling nn::os::GetSystemTick and sleeping 2 ms per round (main+0x4c9f24, SleepThread). The start tick is read at + # main+0x443d44, after the scene's update (the frame object's vtable +0x10, main+0x443cec), so every frame takes N / 60 s plus the time from the last poll to the next start, + # rounded up to the 2 ms rounds: never one vsync, on any hardware (the Switch's double-buffered acquire only moves where the rounds start; a model of both gives 54 fps). That + # is the game's own code, not the host: SleepThread and the tick are exact there. The Mac ran 55 fps in the menus and the town (frame time p5 18.15 / p50 18.28 ms), every 11th + # frame on screen for two vsyncs. N, the int at GOT 0xa1c088 (data 0x9ec764), is the game's vsyncs per frame: 1, and 2 in CPrivateBattleWindow / CPrivateBattleTutorialWindow + # (main+0xf02d0, main+0xec4cc; back to 1 at main+0xf04f4), whose code counts 60 / N frames a second. The game presents at interval 1 in both, so with N = 1 the host's present + # pacing (the acquire waits) holds the frame to one vsync, and the wait's threshold becomes 0 (the loop returns after its first tick, which it still stores at +0x38). With N = 2 + # nothing else holds the frame two vsyncs, so the wait stays the game's (N forced to 2 at the title: 28.9 fps, as without a cut; the earlier cut, b.hs → b at main+0x443fd4, + # presented 60). In the town then: 60.0 fps, frame time p50 16.66 / p95 17.08 ms; the main thread waits in the acquire instead of nine sleeps a frame (wakeups 2,050 → 300 + # a second). The opening movie, paced by its own player, was at 60 before 'f5ab74b9d45f955de2dc91da8e039c57': [ - (0x443fd4, '82000054', '04000014', 'frame limiter: b.hs end → b end (no 2 ms sleep rounds until 1/60 s since the frame start; the present pacing holds 60)'), + (0x443fac, '087c149be9e301b2291191f2087dc99b14fd45d3', '087c149b890780520809c99a9f060071f403889a', + 'frame limiter threshold: N × tick frequency / 60 (umulh by 1/60 → udiv by 60), then 0 when N = 1 (cmp w20, #1; csel x20, xzr, x8, eq): one vsync a frame is the present pacing\'s; N = 2 keeps the 2 ms rounds'), ], } diff --git a/docs/reference/frame-rate.md b/docs/reference/frame-rate.md index 818600b..f338a36 100644 --- a/docs/reference/frame-rate.md +++ b/docs/reference/frame-rate.md @@ -50,8 +50,9 @@ Frame pacing: the host's frame step, engine frame limiters, present intervals an ### A frame-end wait polled in 2 ms sleeps - **Symptom:** 55 fps with every frame 18.2 ms, the main thread asleep most of the time; every 11th frame stays on screen two vsyncs. -- **Cause:** KTGL's frame end (`main+0x443e8c`) waits until 1/60 s has passed since the frame's start tick, polling `GetSystemTick` and sleeping 2 ms a round (`main+0x4c9f24`), so with about 2 ms of work the rounds end at 18.2 ms, never at 16.7. Found by sampling the host (`sample `: 90% of the main thread in `dm_guest_sleep_ns` under a guest return address of `main+0x443d74`) and a probe on the target's multiply (x20 = 1 frame, x0 = 19.2 MHz). -- **Fix:** the game presents at interval 1, so a build cut makes the check jump to the end, and the host's present pacing (the acquire waits) holds 60 (its `patches.py`): frame time p50 16.66 / p95 16.85 ms, wakeups 2,050 → 320 a second. +- **Cause:** the game's own code. KTGL's frame end (`main+0x443e8c`) presents, acquires the next texture, then waits until N / 60 s have passed since the frame's start tick, polling `GetSystemTick` and sleeping 2 ms a round (`main+0x4c9f24`). N is the game's vsyncs per frame, a global. The start tick is read after the scene's update, so each frame lasts N / 60 s plus the time from the last poll to the next start, rounded up to the rounds: never exactly one vsync, on any hardware. The host's `SleepThread` and tick are exact; a model of the loop gives 54 fps with the host's acquire and with the Switch's double-buffered one alike. Found by sampling the host (`sample `: the main thread in `dm_guest_sleep_ns` under `main+0x443d74`, and in `nvnWindowAcquireTexture` under the present wrapper). +- **Fix:** a build cut on the wait's threshold (its `patches.py`): 0 when N = 1, where the game presents at interval 1 and the host's present pacing (the acquire waits) holds the frame one vsync; frame time p50 16.67 ms, wakeups 2,050 → 300 a second. Scenes with N = 2 still present at interval 1, so the wait stays theirs. Skipping the wait for every N, the first cut, presented 60 frames a second in them (28.8 with the wait, N forced to 2), for code that counts 60 / N frames a second. +- **Check:** before cutting a game's frame limiter, find what sets its target and whether anything else holds those frames. A limiter that also enforces a slower mode (here N = 2) has to keep it. - **Found on:** [Taiko Risshiden V DX](../../dm/games/0100346017304000/README.md), in its menus and town; its movies, paced by their own player, ran at 60. ## Present interval and display rate