Skip to content

Switch 2 SDK calls whose service the host does not serve are answered (refresh rates, the 120 Hz capability, GL / GR, native memory), and the acceleration-structure size query writes its sizes - #2246

Open
DeathKhan wants to merge 6 commits into
seedds:mainfrom
DeathKhan:claude/nx-switch2-sys

Conversation

@DeathKhan

@DeathKhan DeathKhan commented Nov 10, 2026 •

Copy link
Copy Markdown

What and why

Nintendo Switch 2 Welcome Tour (040057C02159A000, SDK 20.5.29) stops at start-up on SDK calls that talk to a system service whose session the host never opens, and on a red stub that leaves a size query unanswered. #1425 (its profile) takes over three of those calls: vi::InitializeWithVrrLfcEnabled, hid::SetSceneIdForPrivateApp and oe::QueryApplicationGpuTimeSpan. This PR has the others, with no overlap with #1425.

They come from DeathKhan/nx2android (cbeb5ccd6, eea4efef9, 87772ede9), an Android port of nxnative, as reviewed into DroidPlay Core: enjoyerofslop/droidplay-core#158 and #157. Its maintainer asked for this matching PR, since these files are shared.

The system calls (hle_vi.c, hle_hid.c, hle_oe.c):

  • vi::QueryDisplayCurrentRefreshRate: answers 60 Hz, the display's own, as one 32-bit Hz word and a bool. An answer of two 64-bit words overwrote the caller's saved frame pointer (main+0x5e3e20).
  • vi::QueryDisplayAvailableRefreshRates: lists 60 Hz alone.
  • vi::SetDisplay120HzRefreshRateCapability: is logged.
  • hid::GetGlGrButtonConfigState: answers no GL / GR assigned. Its SDK body aborts in sf (0xe15, sdk+0x65cfdc).
  • gs::sdk::CreateNativeMemory / DestroyNativeMemory: the handle is the range's address, and nothing is registered. Both are only seen as imports so far, never called, so the 8-byte handle isn't checked yet.

nvnDeviceCalculateAccelerationStructureMemoryRequirements writes its three sizes:

  • The bug: the game sums the structure, build-scratch and update-scratch sizes into the memory pool it makes for its acceleration structures (query at main+0x6130cc, the sum at main+0xfc2cc / +0x11bf1c), and doesn't zero them first. The red stub wrote nothing, so on the Android host the sum was stack garbage: a 4 GB pool, and the game ran out of memory in its loader 16 frames in. On the Mac the result depends on what the stack holds.
  • The fix: it now answers 64 KB for each, with a red line once.

Tests: t_oe checks the native memory handle.

History: an earlier version also took over #1425's three calls. They were dropped in 3cf027f, so the two PRs don't collide.

Checked

  • Tests, on an Apple-silicon Mac (macOS 27, Apple clang), built from this branch with GAME 0100606026F12000:
    • t_oe: 54 checks, 0 failed.
    • t_hid: 209 checks, 0 failed.
    • Built with the Makefile's own compile lines, from an export of dm/host and the game's profile. Not run: the whole make -C dm/host/tests run.
    • hle_vi.c, hle_hid.c and hle_oe.c pass clang -fsyntax-only -Wall -Wextra. hle_nvn.c's only warnings are existing ones on other lines.
  • Tool tests: dm/tools/tests test_docs, test_env_switches, test_lint_c and test_architecture pass. docs_symptoms.py was run.
  • Games run: none on the Mac host.
    • On DeathKhan/nx2android (Android, Ayn Odin 3, Adreno 830): Welcome Tour reached its title screen with these calls, game: Nintendo Switch 2 Welcome Tour plays on the Mac #1425's three and the acceleration-structure answer.
    • regress.py check: not run. These hooks are reached only by Switch 2 games that call them.
  • Devices and OS: the Mac above, for the unit tests.

Docs

  • The finding is in its docs/reference/services.md and docs/reference/nvn-objects.md entries, and python3 dm/tools/docs_symptoms.py was run
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

claude and others added 3 commits November 10, 2026 15:00
…: Nintendo Switch 2 Welcome Tour stops at start-up on nn::vi::InitializeWithVrrLfcEnabled and nn::gs::sdk::QueryApplicationGpuTimeSpan (a null read in nn::os::SdkMutexType::Lock, sdk+0x151fb0), on hid calls that abort in sf (0xe15, sdk+0x65cfdc), and on a refresh-rate answer of two 64-bit words that overwrote its caller's saved frame pointer (main+0x5e3e20). hle_vi.c: InitializeWithVrrLfcEnabled does nothing and returns a Result of 0 (void or Result, 0 is safe either way); QueryDisplayCurrentRefreshRate answers 60 Hz, the display's own, as one 32-bit Hz word and a bool; QueryDisplayAvailableRefreshRates lists 60 Hz alone; SetDisplay120HzRefreshRateCapability is logged. hle_hid.c: GetGlGrButtonConfigState answers no GL / GR assigned; SetSceneIdForPrivateApp returns a Result of 0. hle_oe.c: QueryApplicationGpuTimeSpan answers a fixed 8 ms, which keeps the game's dynamic resolution at full size, and the host's measured GPU time of its newest completed frame (mtl_gpu_ms, up to 8 frames back) only with DM_DYNAMIC_RESOLUTION=1, since the host's GPU time held Ocarina of Time's controller at its floor (frame-rate.md); gs::sdk::CreateNativeMemory gives the range's address as its handle and DestroyNativeMemory nothing, both only seen as imports and never called, so the 8-byte handle is not checked yet. Weak stand-ins of mtl_gpu_ms and dm_nvn_frame_count keep host unit tests without a graphics backend linking. From DeathKhan/nx2android cbeb5ccd6 and eea4efef9, as reviewed into DroidPlay Core (enjoyerofslop/droidplay-core#158); services.md entry, the DM_DYNAMIC_RESOLUTION row and a line in frame-rate.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…three sizes: the call writes the size of an acceleration structure, of its build scratch and of its update scratch through three pointers, which Nintendo Switch 2 Welcome Tour sums into the memory pool it makes for its acceleration structures (query at main+0x6130cc, pool from the sum at main+0xfc2cc / +0x11bf1c) without zeroing them first. The red stub wrote nothing, so the sum was stack garbage: on an Android host 0xffffffff + 1 + 1, a 4 GB pool committed before the first frame, and the game ran out of memory in its loader 16 frames in. The host builds no acceleration structure and traces no ray, so it answers 64 KB for each, with a red line once: small sizes in the pool's 4 KB granule keep every later offset the game takes valid. From DeathKhan/nx2android 87772ede9 (its hle_nvn.c hunk), as taken into DroidPlay Core (enjoyerofslop/droidplay-core#157); nvn-objects.md entry

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s a fixed 8 ms without DM_DYNAMIC_RESOLUTION=1, and CreateNativeMemorys handle is the ranges address (55 checks, 0 failed on an Apple-silicon Mac)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation labels Nov 10, 2026
…tionGpuTimeSpan are left to seedds#1425, which takes them over with the same C names: with both, the second to land would not build (a redefinition, or a duplicate in the forced-takeover list). Their code, the GPU time's weak stand-ins, the frame-rate.md line and the DM_DYNAMIC_RESOLUTION row's addition go, and t_oe keeps only its native memory check; the services.md entry keeps the refresh rates, the 120 Hz capability, GL / GR and native memory (only seen as imports, never called) and names seedds#1425 for the rest. As DroidPlay Core's review asked of enjoyerofslop/droidplay-core#158

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DeathKhan DeathKhan changed the title Switch 2 SDK calls whose service the host does not serve are answered (VRR init, refresh rates, GL / GR, a frame's GPU time, native memory), and the acceleration-structure size query writes its sizes Switch 2 SDK calls whose service the host does not serve are answered (refresh rates, the 120 Hz capability, GL / GR, native memory), and the acceleration-structure size query writes its sizes Nov 10, 2026
@DeathKhan

Copy link
Copy Markdown
Author

Updated in 3cf027f, a new commit on the same branch:

  • Dropped: InitializeWithVrrLfcEnabled, SetSceneIdForPrivateApp and QueryApplicationGpuTimeSpan, with the GPU time's weak stand-ins, its frame-rate.md line and its addition to the DM_DYNAMIC_RESOLUTION row. game: Nintendo Switch 2 Welcome Tour plays on the Mac #1425 takes over the same three calls with the same C names, so whichever landed second would not build. This PR now has no overlap with game: Nintendo Switch 2 Welcome Tour plays on the Mac #1425.
  • Kept: the refresh rates, the 120 Hz capability, GL / GR, native memory (with its never-called note) and the acceleration-structure sizes commit.
  • t_oe: keeps only its native memory check.
  • Title and body: updated.

Checked, re-run on an Apple-silicon Mac from this branch:

  • t_oe: 54 checks, 0 failed.
  • t_hid: 209 checks, 0 failed.
  • clang -fsyntax-only -Wall -Wextra is clean on the three changed host files.
  • test_docs, test_env_switches, test_lint_c and test_architecture pass.

Not run: the full make run, any game.

DeathKhan and others added 2 commits November 11, 2026 13:28
One conflict, in dm/host/tests/t_oe.c: both sides added checks at the end of main(); both are kept.

Co-Authored-By: Claude <noreply@anthropic.com>
hle_hid.c: the GL / GR answer stays beside the npad IMU calls main added at the same place

Co-Authored-By: Claude <noreply@anthropic.com>
@DeathKhan

Copy link
Copy Markdown
Author

The one failing check (t_save_sync, 1 of 106) is not from this PR and is not flaky: it fails the same way on main. The check at dm/host/tests/t_save_sync.c (main, line 346) expects cdevice=Mac, but on the GitHub macOS runners the device model is VirtualMac2,1, so strstr(line, "cdevice=Mac") never matches:

✗ both changed: the report says conflict, with the cloud's device (... cdevice=VirtualMac2,1 sametitle=1 conflict=1 pull=0)
== save sync: 106 checks, 1 failed ==

Same line in both jobs (Host unit tests, Sanitizers) of this PR's run 43218207071 and of main's run https://github.com/seedds/nxnative/actions/runs/43326823244 (8d44aaf, 2026-11-12); everything else in the conflict report is as expected (conflict=1). This PR does not touch save sync (its files are hle_vi.c, hle_nvn.c, hle_hid.c, hle_oe.c, t_oe.c and docs), so merging main would not change the result. Matching the model on a real Mac and on the runner (for example checking that the cdevice= value contains Mac) would fix it on main.

@DeathKhan

Copy link
Copy Markdown
Author

The host and sanitizer failures here are main's own t_save_sync check (cdevice=Mac vs the runner's VirtualMac2,1); #2608 fixes the test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation host The Horizon host: SVCs, services, threads, memory (dm/host) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants