Skip to content

core/nvrhi.ts per C++ ABI: MSVC x64 and Android's Itanium ABI (needs a new tslang) - #38

Merged
ASDAlexander77 merged 1 commit into
mainfrom
nvrhi-abi-split
Oct 11, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
nvrhi-abi-split

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Merge order: don't merge this PR until a tslang release with these is deployed:

Under pre-alpha94, import "./nvrhi_abi" finds no file, and every example that imports core/nvrhi stops compiling.

Summary

core/nvrhi.ts (NVRHI's interfaces called directly through @cppvtable classes) now works under two C++ ABIs: MSVC x64 on Windows, and Android's Itanium ABI (clang + libc++) on x86-64 and AArch64. I split it the way tslang's default library splits lib.ts and lib.<os>.ts:

Module Holds
core/nvrhi_types.ts structs and constants that every ABI lays out the same way
core/nvrhi_abi.win32.ts @cppvtable classes with MSVC's slots; the release CRT's std::string and the structs that hold one; helpers that depend on them
core/nvrhi_abi.linux.ts the same for Android's Itanium ABI and libc++
core/nvrhi.ts the functions; its import "./nvrhi_abi" picks the target's file (#552)

Examples still import { nvrhi } from "../core/nvrhi". CMake's NVRHI_TS_SOURCES lists the three modules for the target.

What differs between the ABIs

  • Destructor slots: MSVC has 1, Itanium has 2 (complete and deleting). Every slot after them is shifted by one. Overloads happen to keep MSVC's order.
  • std::string: 32 bytes in the release CRT, 24 in libc++. That makes BufferDesc 72 bytes against 80, and TextureDesc 104 against 112.
    • libc++'s short form is a first byte of size × 2, then up to 22 characters.
    • stdString(), stdStringSize() and sameShortStdString() are written once per ABI.
  • Structs returned by value: now declared as tuples (RefCountPtr, marked @cppnontrivial, and NativeObject), with no explicit hidden-pointer parameter.
    • tslang passes the hidden pointer where each ABI expects it (#551, #553): after this for MSVC, before this as sret for Itanium. On Itanium the 8-byte NativeObject comes back in a register.
    • Itanium has no workaround for the old explicit-parameter form, because its hidden pointer goes before this.
  • Structs passed by value: clearTextureFloat's 16-byte TextureSubresourceSet is the only one.
    • MSVC passes a pointer to a copy; Itanium passes two 64-bit registers.
    • The Linux declaration takes two u64s, and the clearTextureFloat helper moved into the ABI files.
    • I first got this wrong, and it crashed inside NVRHI on Android.

Other changes

  • Donut_CanCallNvrhiDirectly(): now also returns 1 on Android with libc++, x86-64 or AArch64. layoutMatches() still compares the struct sizes at run time.
  • tools/check_nvrhi_slots.py --abi itanium: checks nvrhi_abi.linux.ts against NDK clang's -fdump-vtable-layouts for both x86_64-linux-android29 and aarch64-linux-android29. Both ABIs match; swapping two slots makes it report both.
  • nvrhi_direct: builds on every platform (donut_compile_shaders_all_platforms) and runs through adb shell on Android. It now asks Vulkan for a compute queue only if the device has one.
  • NVRHI bug (not fixed here): Vulkan getNativeQueue dereferences m_Queues[queue] without a null check, and the emulator's device has no compute queue. nvrhi_direct now avoids asking for it.
  • tslang finding: << on a u64 keeps JavaScript's 32-bit semantics (<u64>3 << 33 is 6). The Linux packing multiplies instead.
  • README: "Calling NVRHI directly" updated.

Testing

All runs below used a local tslang built from TypeScriptCompiler #551, #552 and #553:

Platform Result
Windows, CTest with -debug headless.dx12/.vk, headless.donut.dx12/.vk and nvrhi_direct.dx12/.vk pass (6/6)
Windows, JIT (run_jit.bat-style) headless and nvrhi_direct pass on D3D12 and Vulkan
Android x86_64 emulator (API 34, -gpu host, Vulkan on the RTX 5080) headless -vk takes the direct path and passes, and so does headless -vk -donut. nvrhi_direct -vk passes every check: native objects, ref counts, buffers, debug names, compute, barriers, texture clear and readback
Android arm64-v8a builds and links; not run, since there's no arm64 device here. nvrhi.createBuffer's call puts the result pointer in X8 and goes through slot 20

Not covered:

  • -debug on the emulator, which has no Vulkan validation layer.
  • The software-Vulkan emulator (SwiftShader) can't create the device at all, with or without this change.

🤖 Generated with Claude Code

The `nvrhi` namespace is now three modules, as tslang's default library is
lib.ts and lib.<os>.ts:
- core/nvrhi_types.ts: the structs and constants every ABI lays out the same
  way;
- core/nvrhi_abi.win32.ts / core/nvrhi_abi.linux.ts: the @cppvtable classes,
  libc++'s or the release CRT's std::string and the structs holding one, and
  the helpers that depend on them. core/nvrhi.ts's import of "./nvrhi_abi"
  takes the target's file;
- core/nvrhi.ts: the functions.

The ABI files differ in these ways:
- Destructor slots: MSVC has one, the Itanium ABI two.
- std::string: 32 bytes in the release CRT, 24 in libc++ (short strings up
  to 22 characters).
- Structs passed by value: clearTextureFloat's TextureSubresourceSet goes as a
  pointer to a copy under MSVC, and as two u64 registers under Itanium.

Struct returns are now declared as tuples (RefCountPtr with @cppnontrivial,
NativeObject), with no explicit hidden-pointer parameter. tslang passes the
pointer each ABI wants.

- Donut_CanCallNvrhiDirectly: also true on Android (libc++, x86-64 or
  AArch64).
- tools/check_nvrhi_slots.py --abi itanium: checks core/nvrhi_abi.linux.ts
  against NDK clang's vtable dump for x86_64 and aarch64 Android. Both ABIs
  match.
- nvrhi_direct builds on every platform (shaders for all of them), and runs
  through adb shell on Android.
- nvrhi_direct no longer asks Vulkan for a compute queue the device lacks:
  NVRHI's Vulkan getNativeQueue dereferences it, and the Android emulator has
  none.

Needs a tslang newer than pre-alpha94, for <name>.<os>.ts imports and Itanium
struct returns (TypeScriptCompiler PRs 551, 552, 553).

Tested with a local tslang built from those three PRs:
- Windows: headless.dx12/.vk, headless.donut.dx12/.vk and
  nvrhi_direct.dx12/.vk pass (6/6, with -debug). headless and nvrhi_direct
  also pass under the JIT.
- Android x86_64 emulator (host GPU, Vulkan): headless takes the direct path
  and passes, and so does nvrhi_direct, every check included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit b54cc28 into main Oct 11, 2026
@ASDAlexander77
ASDAlexander77 deleted the nvrhi-abi-split branch October 11, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant