Skip to content

core/nvrhi.ts: NVRHI's interfaces called directly through @cppvtable classes - #36

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

ASDAlexander77 merged 1 commit into
mainfrom
nvrhi-cppvtable

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

This adds core/nvrhi.ts, which binds NVRHI's interfaces with tslang @cppvtable classes (TypeScriptCompiler#550, in pre-alpha94). Each method call goes through the C++ object's own vtable, so there is no wrapper per method in donut_interop.cpp:

import { nvrhi } from "../core/nvrhi";

const device = <nvrhi.IDevice>app.getNvrhiDevice();
const commandList = nvrhi.createCommandList(device, nvrhi.commandListParameters());
if (commandList) {
    commandList.open();
    commandList.clearBufferUInt(buffer, 0);
    commandList.close();
    nvrhi.executeCommandList(device, commandList);
    commandList.Release();
}

Classes and slot checking

  • nvrhi.IResource, IBuffer, ITexture, ICommandList and IDevice mirror the C++ hierarchy: the last four extend IResource.
  • Every slot is declared, because a missing one would make calls land on a different C++ method with no error.
  • Only the methods the test calls have signatures. Every other slot is a placeholder named _ + the C++ name. An overloaded method's slots are named _<name>_<slot>, because MSVC orders overloads in its own way.
  • tools/check_nvrhi_slots.py checks the slots. It asks MSVC for each interface's vtable (cl /d1reportSingleClassLayout<Name>) and compares it, slot by slot, with the TypeScript classes. All 163 slots match. Swapping open/close, or dropping a slot, makes it report the exact slots. It needs cl, so it isn't a CTest test; run it after changing the classes or updating NVRHI.

Helpers

  • Hidden return pointer: createBuffer, createCommandList, executeCommandList and getNativeObject hide MSVC's hidden return pointer, the parameter after this through which an object returned by value comes back.
  • Struct layout check: layoutMatches() compares the sizes of the BufferDesc (80 bytes) and CommandListParameters (48 bytes) tuples with the C++ structs. It uses a new Donut_GetNvrhiStructSize, because std::string's layout depends on the CRT.

Test

examples/nvrhi_direct.ts is a headless test of every bound method, not a sample:

  1. It uploads numbers into one buffer and clears another with clearBufferUInt.
  2. It copies both into a readback buffer and checks the mapped data.
  3. It checks getGraphicsAPI, the native device through getNativeObject (an nvrhi::Object returned by value), AddRef/Release/GetRefCount, getDesc, and the struct layouts.

CTest runs it as nvrhi_direct.dx12 and nvrhi_direct.vk with the debug layers. It is built for MSVC only: the slots and calling convention are MSVC x64's, and Android's Itanium ABI differs.

The sample count (112) and the repo description are unchanged, since this isn't a sample. The README gains a "Calling NVRHI directly" section, and the screenshot script skips nvrhi_direct (it has no window).

Also: example dependencies

add_tslang_example used to give an example's .ts and its module .ts files the same dependencies, the core sources only. So editing an imported module (core/nvrhi.ts, core/paged_images.ts, core/meshlet_model.ts, ...) didn't recompile the examples that import it. The example now depends on the modules listed with it. Checked: touching core/nvrhi.ts and core/paged_images.ts rebuilt nvrhi_direct and memory_management, and nothing else.

Testing

  • Full build: no warnings. CTest: 59/59, which is the 57 before plus nvrhi_direct.dx12/.vk.
  • JIT: run_jit.bat examples/nvrhi_direct.ts -- -dx12|-vk -debug -test passes.
  • Checkers: python tools/check_nvrhi_slots.py and python tools/check_interop_types.py --strict both pass.

🤖 Generated with Claude Code

…classes

core/nvrhi.ts lays tslang @cppvtable classes (pre-alpha94) over nvrhi::
IResource, IBuffer, ITexture, ICommandList and IDevice. A method call goes
through the C++ object's own vtable, with no wrapper in donut_interop.cpp.

Every slot is declared, because a missing one would call a different C++
method with no error. Only the methods the test calls have signatures; the
other slots are placeholders named `_` + the C++ name.

tools/check_nvrhi_slots.py compares the classes with the vtables MSVC builds
(cl /d1reportSingleClassLayout): all 163 slots match. Swapping two methods or
dropping one makes it fail.

Helpers hide the MSVC hidden return pointer: createBuffer, createCommandList,
executeCommandList, getNativeObject. layoutMatches() compares the tuples'
sizes with the C++ structs', via the new Donut_GetNvrhiStructSize, because
std::string's layout depends on the CRT.

examples/nvrhi_direct.ts is a headless test of every bound method, not a
sample. It uploads into one buffer and clears another, copies both into a
readback buffer and checks the mapped data. It also checks the graphics API,
the native device, AddRef/Release and getDesc. CTest runs it as
nvrhi_direct.dx12/.vk, with the debug layers. MSVC builds only.

add_tslang_example now makes an example's .ts depend on the .ts modules
listed with it, so changing core/nvrhi.ts or core/paged_images.ts recompiles
the examples that import them.

CTest: 59/59.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit d37c1be into main Oct 11, 2026
@ASDAlexander77
ASDAlexander77 deleted the nvrhi-cppvtable branch October 11, 2026 18:39
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