Skip to content
Merged
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
2 changes: 1 addition & 1 deletion dm/games/01007EF00011E000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ 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.
- **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 (an A/B switch of the experiment, not kept) 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.
- **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
26 changes: 22 additions & 4 deletions dm/games/0400E88025B1A000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A Switch 2 game: update v131072, SDK 21.4.4 built for the Switch 2 (`Ounce`), NV
| `zip_store .zip:.metallib` | `game.tsv` | the archive's members are LZ4-compressed already |
| `shader_groups /data.zip=shader` | `game.tsv` | every shader is a member of the archive |

There is no game-specific patch, and only v131072's build row: the base game, v0, has not been run.
The resolution override is gated to v131072's build ID. The base game, v0, has not been run.

## Files

Expand All @@ -27,14 +27,29 @@ There is no game-specific patch, and only v131072's build row: the base game, v0

## The picture

Every 3D target is 768x432 from the first frame of the title to the tutorial: MetalFX stands in for DLSS at 768x432 to 768x432, the game's NDSR compute networks take that to 1536x864, and the result reaches the 3840x2160 swap chain, whose top-left 1920x1080 holds the picture. The game's own defaults give the reason for the figure, not the cause: `render.dynres_min_scale_x` is 0.5 and `max` 1.0, so 768x432 is the minimum of a 1536x864 base, at 6 to 11 ms of GPU time a frame on the M4 Pro. `gpu_timestamps segment` changes nothing. Whether a console stays there too is not known.
The scene's size, measured with probes (`DM_PROBE_FP`, `DM_PROBE_POLL=as+…`): the renderer (`main+0xb447300`) takes a target and multiplies it by a scale it reads from its settings at `main+0xb4498a8`, `render.resolution_scale_console` 0.5 docked or `render.resolution_scale_handheld` 0.83. It runs DLSS at that size, as anti-aliasing (input = output: `nvnDlssNetworkResize` from `main+0xb75a440`, an output texture of that size), and its NDSR compute networks take the picture to the target. Docked, the target is the window's size (`[obj+0x400]`), or an override at `[obj+0x428]` that the window manager writes right after it reads the graphics scale: 1536x864, 0.8 of 1920x1080, at graphics scale 1. Hence 768x432 (1536x864 x 0.5). Two host answers made it small, each in a pull request of its own: the graphics scale, 1 docked where the full GPU share is 0 (#1304: 960x540), and the display's largest layer, 1920x1080 where a docked Switch 2 on a 4K screen outputs 4K (#1305: 1920x1080 on a 4K screen). Handheld it renders at 1600x900.

**Resolution** (`DM_RESOLUTION_SCALE`, the launcher's Game's own / 75 % / 100 %) replaces that scale in both modes: `patches.py`, a stub at `main+0xb4498ac` in the back patch area (main's text is 189 MB, beyond the front area's reach: `ctx.area`), state +0xa0, offered through the `RESOLUTION_SCALE` capability of build `a7730d0f`. At 0.75 and 1.0 the game evaluates no DLSS. Measured on an M1 Pro, docked, graphics scale 0, 1080p target, 300 s from boot into the tutorial's flight:

| Scale | Scene | Flight: frame p50 | Flight: GPU p50 / p95 | Cinematic (30 fps): GPU p50 |
|---|---|---|---|---|
| Game's own (0.5) | 960x540 | 16.7 ms | 10–11 / 15–16 ms | 22–24 ms |
| 0.75 | 1440x810 | 16.7 ms | 9–11 / 17–19 ms | 17–18 ms |
| 1.0 | 1920x1080 | 16.7 ms | 9–11 / 16–17 ms | 19 ms |

At 1.0 Peppy's close-up (frame 3,000) shows the fur and whiskers sharply that 0.5 blurs.

## Driving it on the Mac

This fork combines upstream pull requests #1304, #1305, #1308, #1310 and #1323 for a Mac app. Its `pso_manifest.tsv` records the title, opening cinematic and flight tutorial (772 unique entries: 393 shader, 367 render-pipeline and 12 compute-pipeline rows). It does not cover the campaign. The host prepares this list before the first frame.

On an M5 Pro with 24 GB, macOS 27.0, handheld mode and Resolution 100% give 1920x1080 scene targets. Physical DualSense steering, firing and the full-window picture were confirmed by the player. One 1,800-frame flight sample took 30.01 s (59.98 fps, no dropped presents); a longer 7,636-frame sample with new assets and diagnostic captures took 130.54 s (58.49 fps, 37 dropped presents). First-use and streaming hitches remain; this does not establish flawless campaign performance or 4K/120 fps. The raw screenshot is the whole 3840x2160 texture; the window crop selects its top-left 1920x1080 picture for display.

`python3 scripts/build_ipa_from_nsp.py <folder> --platform mac` builds the Mac shaders and the host and prints the run command. Headless, through `DM_CTL`: the title is up about 40 s after boot and `A` starts the game. The prologue cinematic follows, about 4 minutes on every run (a save holds no progress yet, and skipping it was not tried), then the tutorial's first question, about 300 s after boot: `LDown`, `A` answers no. Shots of the swap chain hold the picture in their top-left quarter. A `shot` takes about 165 ms: a hitch every few seconds in a run that takes them is the shot's.

## Found on this game

- The avatar settings present the virtual camera's picture into an NVN window of their own; the host makes it an off-screen window: [GameChat queries from the title screen (SDK 21)](../../../docs/reference/services.md#gamechat-queries-from-the-title-screen-sdk-21).
- [SDK 21 socket initialization and overloads](../../../docs/reference/services.md#sdk-21-socket-initialization-and-overloads) and [Socket `Poll` and `Select` with nothing to wait on](../../../docs/reference/services.md#socket-poll-and-select-with-nothing-to-wait-on): the game's HTTP thread aborted on a failed `poll`.
- [SDK 21's OutputStream on a virtual output device](../../../docs/reference/audio.md#sdk-21s-outputstream-on-a-virtual-output-device) and [SDK 21's hardware Opus decoder manager](../../../docs/reference/audio.md#sdk-21s-hardware-opus-decoder-manager): its voice and music.
- [GameChat queries from the title screen (SDK 21)](../../../docs/reference/services.md#gamechat-queries-from-the-title-screen-sdk-21): a null read in `nn::chat::GetChattingUid`.
Expand All @@ -47,8 +62,11 @@ Every 3D target is 768x432 from the first frame of the title to the tutorial: Me

## Open

- [The picture](#the-picture): why the 3D stays at 768x432.
- Native 120 Hz: the host has no takeover for `nn::vi::QueryDisplayCurrentRefreshRate` or `SetDisplay120HzRefreshRateCapability`. SDK 21 rejects the host's display handle, and the game's refresh queries keep their prefilled 60 Hz answer. Exposing 120 Hz still needs correct display semantics and simulation, audio, movie and input checks. No frame-rate unlock is enabled in this fork.

- [The picture](#the-picture): 768x432 docked until #1304 and #1305 are in; the Resolution setting raises the scene above the game's own scale.
- `[RED] DLSS: the game's bias mask (a reactive mask for MetalFX) is not passed on`.
- The game's wake-up eventfd fails (the host refuses `nn::socket::EventFd`), so its HTTP thread polls without a wake-up; harmless offline.
- A `pso_manifest.tsv` has not been recorded; `render_cond` not tried; the later levels and combat, and iOS, not tested.
- The campaign was played through its last level, Venom 2, on a Mac (32 GB) with #1304, #1305, #1308 and #1310 in a test build. Without #1310, the GPU ran out of memory at the load of Venom 2's second part (`map_andross`), after 69 minutes of play: the residency set held 972 textures, 23.8 GB, against a recommended working set of 25.5 GB, and every upload batch after that was dropped, so the screen stayed black. Each scene change had uploaded 60 to 86 of the streamed BC7 arrays again, 4 to 5 GB in one frame. On the same route through Venom 2's first part, #1310 removed those uploads: 0 instead of 20 to 26, at about 1.4 GB each. With that build, the player got through the second part. Scripted runs did not reach it, so the memory there was not measured.
- A `pso_manifest.tsv` has not been recorded; `render_cond` not tried; iOS not tested.
- Mario Kart World 1.8.0 has not been run with the camera answers changed for this game.
35 changes: 35 additions & 0 deletions dm/games/0400E88025B1A000/patches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# StarFox (0400E88025B1A000): 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;
# FRAME_STEP and CAPABILITIES as described there ("Registered patches"). Patches of Nintendo SDK module builds are in dm/tools/sdk_patches.py
from nso_asm import ADRP, B

# Resolution scale (state +0xa0, a float the loader writes from DM_RESOLUTION_SCALE; 0 = the game's own). The renderer (main+0xb447300) sizes its scene each frame as its target
# (1920x1080 at graphics scale 0) times a scale it reads from its settings at main+0xb4498a8: render.resolution_scale_console docked (0.5) or render.resolution_scale_handheld
# (0.83), and runs DLSS at that size, as anti-aliasing; its own upscaler (NDSR) then takes the picture to the target. The stub replaces the value read, the load at main+0xb4498ac,
# when the state holds one: the rest (a near-zero value falls back to render.resolution_scale, the minimum with the benchmark scale, GameShare's scales) runs as before. x9, s1
# and the flags are dead there: main+0xb4498b4, +0xb4498b8 and +0xb4498bc set them. With 0 every instruction the game runs is its own
def frame_step(ctx):
here, emit, site, put, state = ctx.here, ctx.emit, ctx.site, ctx.put, ctx.state
site(0xb4498ac, 'e0a343bd') # ldr s0,[sp,#0x3a0]: the scale read from the settings
ctx.area(0xb4498ac) # main's text is 189 MB: the back patch area reaches the site
start = here()
emit(0xbd43a3e0) # ldr s0,[sp,#0x3a0] (original)
emit(ADRP(9, here(), state))
for w in (0xbd40a121, # ldr s1,[x9,#0xa0] the host's scale
0x1e202028, # fcmp s1,#0.0
0x5400004d, # b.le done (+8) 0: the game's own
0x1e204020): # fmov s0,s1
emit(w)
emit(B(here(), 0xb4498b0)) # done
put(0xb4498ac, [B(0xb4498ac, start)], 'scene resolution scale from the host (DM_RESOLUTION_SCALE; 0 = the game\'s own)')

FRAME_STEP = {'a7730d0f74d168d90422354c8317f76f': frame_step}
# The launcher offers DM_RESOLUTION_SCALE (dm/games/settings.tsv) only for this build
CAPABILITIES = {'a7730d0f74d168d90422354c8317f76f': ('RESOLUTION_SCALE',)}

# How general each patch is and could be (dm/tools/docs_patches.py "Binding", listed in docs/agents/game-patches.md): what, its binding now, the loosest it could have (goal),
# purpose, why; blocked or kept: why the gap stays open (cannot close yet, or left open on purpose); checked: what was checked on game files (else a judgement)
BINDING = [
{'what': 'scene resolution scale', 'now': 'build', 'goal': 'game', 'purpose': 'rendering',
'why': "the game's own settings read in its renderer",
'blocked': 'frame-step stubs have no function-matched form yet'},
]
Loading