Taiko Risshiden V DX: keep the frame limiter for N = 2, cut it only at N = 1, and record its cause. KTGL's frame end (main+0x443e8c) presents, acquires, then waits until N / 60 s since the frame's start tick in 2 ms SleepThread rounds; the tick is read after the scene's update (main+0x443d44), so every frame lasts N / 60 s plus the time to the next start, rounded up: never one vsync, on any hardware. That is the game's code, not the host (SleepThread and the tick are exact; a model of the loop gives 54 fps with the host's acquire and with the Switch's double-buffered one). N (GOT 0xa1c088, data 0x9ec764) is the game's vsyncs per frame: 2 in CPrivateBattleWindow / CPrivateBattleTutorialWindow (main+0xf02d0, main+0xec4cc), whose code counts 60 / N frames a second, and the game presents at interval 1 everywhere. The earlier cut (b.hs -> b at main+0x443fd4) skipped the wait for every N: with N forced to 2 it presented 60 frames a second against the game's 28.8. The new cut at main+0x443fac computes the threshold with udiv by 60 and sets it to 0 only when N = 1, where the host's present pacing holds one vsync. Checked on the Mac (host at d50ecc1, Mac libraries for chain 8393d85a4eae): title N = 1 54.6 -> 60.0 fps (frame p50 16.67 ms, wakeups 2,050 -> 300/s), N forced to 2 28.9 fps (no cut 28.8, old cut 60.0); town on the rewriter's own output 60.0 fps, p50 16.66 / p95 17.08 ms, no red line; patch_nso.py applies the cut with its byte checks. The CPrivateBattle scenes (small unit battles, PBBATTLE) were not reached: open in the game note. Docs: the game note and the frame-rate reference entry give the corrected cause; tool tests (17 files) and docs tests pass
This commit is contained in:
1 parent
fb20bce70e
commit
7753b7b685
3 files changed
+24
-14
No files matched your search
@@ -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 <pid>`: 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 <pid>`: 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).
|
||||
@@ -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'),
|
||||
],
|
||||
}
|
||||
@@ -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 <pid>`: 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 <pid>`: 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
|
||||
|
||||
Reference in new issue
Block a user