NVRHI (C++)
NVIDIA's production rendering hardware interface — one API over D3D11, D3D12, and Vulkan — and the catalog's clearest production example of auto-sync-by-default with per-resource opt-out: every command list runs a D3D12-style resource-state tracker that places barriers automatically, and every piece of that tracking can be selectively switched off (setEnableAutomaticBarriers, keepInitialState, setPermanentTextureState, per-resource UAV-barrier toggles) when a hot path needs manual control.
| Field | Value |
|---|---|
| Language | C++17 (CMakeLists.txt sets CMAKE_CXX_STANDARD 17) |
| License | MIT |
| Repository | NVIDIA-RTX/NVRHI (formerly NVIDIAGameWorks/nvrhi; the old URL redirects) |
| Documentation | ProgrammingGuide.md · Tutorial.md (in-repo) |
| Category | Render-graph / auto-sync layer (cross-API RHI with command-list-scoped state tracking — no graph, no task DSL) |
| First release | Repository created July 13, 2021 (open-sourced together with the Donut framework) |
| Latest release | No tagged releases — rolling main (CMake project(nvrhi VERSION 1.0.0)); last pushed June 10, 2026 |
NOTE
NVRHI is not Vulkan-first: it abstracts Direct3D 11, Direct3D 12, and Vulkan 1.3 behind one interface (Windows x64, Linux x64/ARM64), and its synchronization vocabulary is D3D12's resource states, lowered onto Vulkan barriers. It is the rendering layer under NVIDIA's Donut sample framework and the RTX SDK sample suite (RTXDI, RTXGI, RTX Path Tracing), so its auto-sync design has shipped in many production-grade ray-tracing codebases.
Overview
What it solves
NVRHI targets the same two Vulkan/D3D12 pain points as Daxa and vuk — synchronization and lifetime — but from the opposite direction: instead of a redesigned modern-GPU-only API, it offers a DX11-flavoured programming model portable across three APIs of very different explicitness. The programming guide states the contract up front:
"Unlike the modern GAPIs, the library tracks the resources created by the application, where they are used, when the GPU work using the resources finishes, and when it's safe to release the resources. The library also implements resource state tracking and automatic barrier placement, although it needs some hints to do so and maintain support for multiple command lists that might use the same resource." (
doc/ProgrammingGuide.md)
The "hints" caveat is the design's honest core: because multiple independently-recorded command lists may touch the same resource, no single command list can know a resource's current state. NVRHI resolves this with three explicit per-resource policies (see Synchronization safety) rather than a global mutex-guarded state database — and that decision is what makes both parallel recording and per-resource opt-out cheap.
Design philosophy
The README sells exactly the trade-off this survey cares about, with the opt-out in the same breath as the feature:
"Automatic tracking of resource states and barrier placement (optional)" … "Automatic tracking of resource usage and lifetime, deferred and safe resource destruction" … "Convenient and efficient resource binding model with little runtime overhead" (README)
Philosophically NVRHI is a lowest-common-denominator with escape hatches: the API is "mostly a blend between DX11 and DX12, with a flavor of Vulkan" (programming guide), each command-list method documents its per-API lowering (- Vulkan: Maps to vkCmdPushConstants, include/nvrhi/nvrhi.h), and getNativeObject/createHandleForNativeTexture provide two-way interop with the underlying API when the abstraction runs out.
How it works
The public interface is one hand-written header, include/nvrhi/nvrhi.h (~3,900 lines): COM-style refcounted interfaces (IDevice, ICommandList, ITexture, IBuffer, …) held through RefCountPtr<T> handles (TextureHandle, BufferHandle). The application creates the native device itself and wraps it — on Vulkan via nvrhi::vulkan::createDevice(DeviceDesc), whose DeviceDesc takes the application's VkInstance/VkPhysicalDevice/VkDevice, up to three VkQueues (graphics required; compute and transfer optional), and the enabled extension lists. Command lists are recorded between open()/close() and run with IDevice::executeCommandList, which returns a 64-bit instance used for cross-queue waits (queueWaitForCommandList).
An NVRHI command list is fatter than a VkCommandBuffer:
"On DX12 and Vulkan, NVRHI command lists do not map to GAPI command lists 1:1, they aggregate more resources in order to make the programming model easier to use. One command list will typically keep multiple GAPI command lists and use them in a round-robin fashion if the previously recorded instance of the command list is still being executed when the command list is re-opened." (
doc/ProgrammingGuide.md)
Each command list owns its upload manager (versioned constant/upload buffers), its acceleration- structure scratch allocator, and — centrally for this survey — its own CommandListResourceStateTracker (src/common/state-tracking.h), shared by the D3D12 and Vulkan backends.
Binding generation & API coverage
Nothing is generated from vk.xml; the abstraction is hand-written — necessarily, since the interface must be the intersection-plus-union of three APIs that no Vulkan registry describes. Registry metadata (externsync, success codes, structure-chain validity) does not survive into the interface; thread-safety and per-API behaviour are hand-maintained doc comments on each ICommandList method. Generation appears only indirectly: the Vulkan backend is written against Vulkan-Hpp (vk::ImageMemoryBarrier2, vk::DependencyInfo in src/vulkan/vulkan-state-tracking.cpp), so the type-safe enums and builders it uses internally are vk.xml products one layer down.
Coverage is broad for a portability layer: graphics, compute, ray tracing (TLAS/BLAS, opacity micromaps, the optional RTXMU compaction integration), meshlet pipelines, variable-rate shading, sparse/tiled resources (Queue::bindSparse in src/vulkan/vulkan-queue.cpp), cooperative vectors, and push constants (exactly one block per pipeline). Non-portable features are kept and labelled rather than dropped — sampler-feedback methods are documented "DX11, Vulkan: Unsupported" (nvrhi.h) — and the ResourceStates enum carries DX12-shaped bits (PixelShaderResource vs NonPixelShaderResource) that Vulkan must approximate (see Overhead & escape hatches).
Handle lifetime & ownership model
Lifetime is COM-style reference counting plus per-queue deferred destruction. Every NVRHI object descends from IResource with AddRef/Release; RefCountPtr<T> is documented as "same as ComPtr provided by WRL" (programming guide). The safety mechanism is internal references: binding sets strongly reference their resources, and each in-flight Vulkan command buffer accumulates a referencedResources vector while recording (src/vulkan/vulkan-commandlist.cpp). Each Queue owns a timeline semaphore (trackingSemaphore, src/vulkan/vulkan-queue.cpp) signalled with the submission ID; IDevice::runGarbageCollection() — "supposed to be called at least once per frame" (programming guide) — reads vkGetSemaphoreCounterValue per queue and, for every command buffer with submissionID <= lastFinishedID, clears its referencedResources and returns it to the pool (vulkan-queue.cpp, CommandListLifetimeTracker::runGarbageCollection). The result is the guide's "fire and forget" model:
"it is valid to create resources and even pipelines in local scope, record the draw commands into a command list, maybe execute that command list, and just exit the scope" (
doc/ProgrammingGuide.md)
The tracking has a deliberate hole at the bindless boundary:
"descriptor tables do not keep strong references to their resources, and therefore provide no resource lifetime tracking or automatic barrier placement. Applications must take care to synchronize descriptor table writes with GPU work and to ensure the correct state of each referenced resource - most likely, by using only permanent resources in descriptor tables." (
doc/ProgrammingGuide.md)
— i.e. exactly where per-resource tracking would be O(thousands), NVRHI turns it off and points users at the permanent-state escape hatch instead.
Synchronization safety
NVRHI's model is named usage states + command-list-scoped automatic tracking. A resource is in one ResourceStates bitmask at a time (ShaderResource, UnorderedAccess, RenderTarget, CopyDest, AccelStructWrite, … — nvrhi.h); state-setting commands (setGraphicsState, writeTexture, copies, clears) call requireTextureState/ requireBufferState, which diff against the tracker and append TextureBarrier/BufferBarrier records to a pending list (src/common/state-tracking.cpp). Barriers flush lazily: commitBarriers() — called implicitly before draws/dispatches — ends any dynamic render pass and emits one batched vkCmdPipelineBarrier2 for images and one for buffers (src/vulkan/vulkan-state-tracking.cpp, commitBarriersInternal).
Because trackers are per-command-list, the cross-command-list state problem is solved by three user-chosen policies (programming guide):
- Explicit bracketing —
beginTrackingTextureState/beginTrackingBufferStatedeclares the entry state afteropen();setTextureState/setBufferStatetransitions to a known exit state beforeclose(). keepInitialState— set onTextureDesc/BufferDescwith aninitialState: "command lists that use the texture will automatically begin tracking the texture from the initial state and transition it to the initial state on command list close" (nvrhi.h). The invariant "always ininitialStateat command-list boundaries" replaces global tracking.- Permanent state —
setPermanentTextureState/setPermanentBufferStatefreezes a static resource (material textures, vertex buffers): "Permanent resources do not require any state tracking and are therefore cheaper on the CPU side" (programming guide). Subsequent incompatible uses are diagnosed byverifyPermanentResourceState(state-tracking.cpp).
Hazard-class coverage. Transitions cover RAW/WAW across state changes; the same-state WAW case (UAV→UAV) is handled by automatic UAV barriers "between successive uses of the same resource in UnorderedAccess state" (programming guide) — on Vulkan a GENERAL-layout vkCmdPipelineBarrier2 with shader read+write access both sides. The tracker emits a barrier when transitionNecessary || uavNecessary, where uavNecessary requires tracking->enableUavBarriers || !tracking->firstUavBarrierPlaced (state-tracking.cpp) — so setEnableUavBarriersForTexture(tex, false) suppresses inter-dispatch UAV barriers but the first transition into UnorderedAccess is still placed, a carefully-shaped opt-out for accumulation passes where dispatches are known independent.
The global opt-out is one method on the command list:
"Enables or disables the automatic barrier placement on set[...]State, copy, write, and clear operations. By default, automatic barriers are enabled, but can be optionally disabled to improve CPU performance and/or specific barrier placement. When automatic barriers are disabled, it is application's responsibility to set correct states for all used resources." (
setEnableAutomaticBarriers,include/nvrhi/nvrhi.h)
In manual mode the same tracker machinery is driven by hand — setTextureState/setBufferState plus the convenience sweeps setResourceStatesForBindingSet and setResourceStatesForFramebuffer accumulate pending barriers, commitBarriers() flushes them — so manual and automatic code share one barrier path and one diagnostic system. Mis-declared entry states are caught at runtime, not compile time: "Unknown prior state of texture …. Call CommandList::beginTrackingTextureState(...) before using the texture or use the keepInitialState and initialState members of TextureDesc." (state-tracking.cpp).
Host-side external synchronization is prose, not types: command lists are single-threaded objects, parallel recording means one command list per thread, and cross-queue ordering uses IDevice::queueWaitForCommandList(waitQueue, executionQueue, instance) against the per-queue timeline semaphores. Queue-family ownership transfer is not modeled at all: every barrier sets VK_QUEUE_FAMILY_IGNORED on both sides (vulkan-state-tracking.cpp) and images are created with vk::SharingMode::eExclusive (src/vulkan/vulkan-texture.cpp) — the D3D12-shaped model simply has no vocabulary for QFOT, and NVRHI relies on same-family queues / ignored-family semantics in practice.
Type-system techniques
Modest, deliberately so — NVRHI predates and out-ships most type-level safety experiments:
- Distinct interface types per resource kind (
ITexturevsIBuffervsrt::IAccelStruct) and typed handles (TextureHandle = RefCountPtr<ITexture>), so kind confusion is a compile error; but states, subresources, and slots are all plain integers/bitmasks. ResourceStatesasenum classbitflags — the whole sync vocabulary is one 32-bit enum (nvrhi.h); nothing distinguishes read-only from read-write states in types.- Builder setters (
CommandListParameters& setQueueType(...),TextureDescfluent setters) for ergonomics only — no typestate; command-listopen()/close()ordering is checked at runtime by the validation layer, not by types. - Tag-dispatched native interop —
getNativeObject(ObjectType)returns a type-erasedObjectkeyed by anObjectTypeenum (VK_Image,D3D12_Resource, …), the classic cross-API escape-hatch pattern.
Absent, and worth recording: no phantom/branded types, no typestate, no ownership beyond refcounting, no compile-time distinction between implicitly and explicitly synchronized operations. NVRHI's safety budget goes entirely into runtime tracking plus a wrappable validation layer — the same position as Daxa, with even less type-level ambition, and the diametric opposite of vulkano.
Overhead & escape hatches
The cost model is per-use hash-map tracking, amortized by opt-outs:
- Tracking cost. Each
requireTextureStatedoes anstd::unordered_maplookup keyed on the resource pointer, a per-subresource state diff, and possibly a vector push (state-tracking.cpp). This runs on every state-setting command for every tracked resource — the costsetEnableAutomaticBarriers(false)and permanent states exist to remove. Binding-set application is incremental:insertResourceBarriersForBindingSetsdiffs new against current bindings (arrayDifferenceMask) and only re-walks changed sets — except sets with UAV bindings, which are re-walked every draw to place UAV barriers (vulkan-state-tracking.cpp). - The opt-out ladder (cheapest correctness work to most): default automatic tracking →
keepInitialState(no per-list bracketing calls) →setEnableUavBarriersFor*(.., false)(suppress inter-dispatch UAV sync) →setPermanentTexture/BufferState(resource leaves tracking entirely) →setEnableAutomaticBarriers(false)+ manualsetTextureState/commitBarriers(application-placed barriers through the same accumulator). - Lowering is conservative — the Vulkan expressiveness tax.
convertResourceState(src/vulkan/vulkan-constants.cpp) maps state bits through a fixed table to(stage, access, layout)triples, so precision is capped by the table:NonPixelShaderResource,ConstantBuffer, andUnorderedAccessall lower tovk::PipelineStageFlagBits2::eAllCommands— a full pipeline sync where hand-written Vulkan would scope to, say,COMPUTE_SHADER. Buffer barriers are whole-buffer (setOffset(0).setSize(buffer->desc.byteSize),vulkan-state-tracking.cpp). There are no split barriers/events, no fine-grainedVkAccessFlags2beyond the table, no QFOT, and image layouts are dictated per state bit (with one curated exception:ShaderResource | DepthReadresolves toeDepthStencilReadOnlyOptimal,vulkan-constants.cpp). This is the cross-API price: the sync interface can express only what D3D11 can survive and D3D12 can name. - Escape hatches are first-class and bidirectional:
getNativeObject/getNativeViewhand outVkImage/VkImageView/VkBuffer/VkDevice/queues;createHandleForNativeTexture/createHandleForNativeBufferimport externally-created native resources into the tracking system (nvrhi.h) — the pattern swapchains use.
Error handling & validation integration
No exceptions and no result codes on the hot path: fallible operations report through the application-supplied IMessageCallback and return null handles. The header is explicit that severity policy belongs to the application:
"NVRHI will call message(...) whenever it needs to signal something. The application is free to ignore the messages, show message boxes, or terminate." (
include/nvrhi/nvrhi.h)
Validation is an interposing wrapper device — nvrhi::validation::createDevice wraps any IDevice and every command list it creates (src/validation/validation-commandlist.cpp), checking the runtime state machine the types don't ("Cannot open a command list that is already open", "A command list should be executed before it is reopened", at most one immediate command list open). The state tracker itself diagnoses sync misuse at MessageSeverity::Error (unknown prior state, permanent-state violations via verifyPermanentResourceState, state-tracking.cpp). Since NVRHI places the barriers, Khronos sync validation findings in tracked code indicate NVRHI bugs or lied-about entry states; the layers regain their usual role wherever the application opted out. Nsight Aftermath crash-dump integration is built in (aftermathEnabled in DeviceDesc), and beginMarker/endMarker lower to vkCmdBeginDebugUtilsLabelEXT.
Strengths
- The clearest production statement of auto-sync-by-default, opt-out-by-resource: one default that is always correct, plus a graded ladder (
keepInitialState→ UAV-barrier toggles → permanent states → fully manual) that removes tracking cost exactly where profiling says to — and manual mode reuses the same accumulator/diagnostics instead of a separate API. - Per-resource entry-state policies solve the multi-command-list problem without a global lock, preserving parallel recording — a problem render graphs solve by owning the whole frame and vulkano-style trackers solve with runtime locking.
- Lifetime is genuinely fire-and-forget: refcounts + per-queue timeline semaphores + once-a- frame
runGarbageCollection(), with command-list-attached references making even transient resources safe. - Battle-tested breadth: ray tracing (incl. opacity micromaps, RTXMU), meshlets, VRS, sparse resources, cooperative vectors — under one interface shipped across NVIDIA's sample/SDK fleet.
- Two-way native interop (
getNativeObjectout,createHandleForNativeTexturein) keeps the abstraction non-totalizing. - The validation story is layered like Vulkan's own: zero-cost release builds, a wrapper device in debug — a structure a D library could mirror with a compile-time hook policy instead of a runtime wrapper.
Weaknesses
- D3D12-shaped sync caps Vulkan precision: fixed state→
(stage, access, layout)table witheAllCommandsfor common states, whole-buffer barriers, no split barriers/events, no QFOT, no per-aspect layouts — over-synchronization a hand-tuned Vulkan renderer (or a graph like vuk's) would avoid. - Tracking is per-use CPU work (hash lookups + subresource diffs on every state-setting call); the opt-outs exist precisely because the default shows up in profiles of draw-heavy frames.
- Cross-command-list correctness rests on honest declarations — a wrong
beginTrackingTextureStateor a violatedkeepInitialStateinvariant is a runtime error message at best and a GPU hazard at worst; nothing is compile-time. - Bindless breaks the safety net by design: descriptor tables carry no lifetime or state tracking, pushing users to permanent states — safe but layout-frozen.
- No
vk.xml-derived metadata:externsync, success-code, and validity knowledge is hand-written doc-comment prose that can drift; thread-safety contracts are conventions. - No versioning signal: no tags or releases, CMake version pinned at
1.0.0— consumers vendor a commit (the intended model, but it complicates downstream packaging).
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| One interface over D3D11/D3D12/Vulkan | Renderer code ports unmodified; NVIDIA SDK samples ship on all three | Sync vocabulary is the D3D12 intersection; Vulkan-only expressiveness (events, QFOT) is dropped |
D3D12-style ResourceStates instead of stage/access/layout | One enum a human can reason about; identical model on all backends | Fixed conservative lowering table; eAllCommands stages; whole-buffer barriers |
| Command-list-scoped tracker + 3 entry-state policies | No global state database → parallel recording stays lock-free | User must pick a policy per resource; wrong hints surface at runtime |
setEnableAutomaticBarriers / permanent states / UAV-barrier toggles | "improve CPU performance and/or specific barrier placement" where tracking shows up in profiles | Manual islands forfeit the guarantee; correctness burden returns to the application |
Auto UAV barriers between successive UnorderedAccess uses | WAW hazards covered by default; first barrier always placed even when toggled off | Re-walks UAV binding sets every draw; independent dispatches pay until opted out |
| COM refcounts + per-queue timeline-semaphore GC | Fire-and-forget lifetimes; one runGarbageCollection() call per frame | Frees are batched/deferred; upload/scratch pools never shrink short of dropping the list |
IMessageCallback + wrapper validation device, no exceptions | Zero-cost release path; app owns severity policy; validation is opt-in layering | Errors are observational — execution continues unless the app terminates |
Hand-written header, no vk.xml generation | The cross-API surface exists in no registry; per-API behaviour documented per method | No machine-checked externsync/validity metadata; doc comments can drift |
Sources
- NVIDIA-RTX/NVRHI — GitHub repository · README
doc/ProgrammingGuide.md— state-tracking policies, lifetime model, binding model ·doc/Tutorial.mdinclude/nvrhi/nvrhi.h—ResourceStates,keepInitialState,setEnableAutomaticBarrierset al. doc commentsinclude/nvrhi/vulkan.h—DeviceDesc, queue/extension wiringsrc/common/state-tracking.h/.cpp—CommandListResourceStateTracker, UAV-barrier logic, diagnostics (implementation)src/vulkan/vulkan-state-tracking.cpp— barrier lowering tovkCmdPipelineBarrier2,VK_QUEUE_FAMILY_IGNOREDsrc/vulkan/vulkan-constants.cpp—g_ResourceStateMapstate→(stage, access, layout) tablesrc/vulkan/vulkan-queue.cpp— timelinetrackingSemaphore,CommandListLifetimeTracker::runGarbageCollectionsrc/vulkan/vulkan-commandlist.cpp—referencedResources, submission/recording IDssrc/vulkan/vulkan-texture.cpp—eExclusivesharing modesrc/validation/validation-commandlist.cpp— wrapper validation layer- NVIDIA-RTX/Donut — the sample framework built on NVRHI
- Related: Daxa (C++) · vuk (C++) · Tephra (C++) · Granite (C++) · vulkano (Rust) · wgpu (Rust) · Vulkan-Hpp (C++) · Sync validation · Concepts · Comparison · Survey index