Detailed notes on how VRI implements each feature across backends — the fallbacks, caveats, and the native extension each path uses. For the at-a-glance support matrix, see the feature support table in the README. For the thread-safety contract (which calls may run concurrently and where the caller must synchronize), see docs/threading.md.
✅ Native · 🟡 Emulated / Simulated · ⬜ Planned (the API supports it; not yet in VRI) · ❌ Unsupported by the API
- Planned (⬜). A few cells are things the backend's API can do but VRI hasn't wired yet —
distinct from ❌ (the API genuinely can't). Currently: Metal subgroup / wave operations
(SIMD-group functions) and fragment-shader barycentrics (
[[barycentric_coord]]) — these await Apple hardware to implement and verify. - Push constants / bindless are exposed uniformly: where the backend lacks the hardware feature, VRI provides a transparent fallback (an emulated uniform path, or a texture-array path) that produces the same result.
- Ray tracing is hardware-accelerated on Vulkan & Direct3D 12 (ray query and a full raygen/miss/hit pipeline, plus acceleration-structure compaction to shrink a built BLAS/TLAS to its exact size) and on Metal (inline ray query). On WebGPU and OpenGL — which have compute but no ray-tracing hardware — VRI traces rays in a compute shader instead (🟡). Where there's no compute at all (WebGL 2 / OpenGL ES), ray tracing is unavailable.
- Bindless on Metal is implemented but currently unverified (no Apple hardware in CI).
- External memory / interop exports OS handles (Win32 / fd) for a buffer/texture's backing
memory and for timeline fences (
ext/vri_ext_external.h), so a CUDA kernel — or OptiX, NVENC, another process, or another graphics API — can import the same memory and timeline. VRI itself does not depend on CUDA; it only produces the handles the consumer imports. Implemented on Vulkan (VK_KHR_external_memory/semaphore) and Direct3D 12 (shared committed resources + shared fences); the opt-inexample-cuda-interopruns on both (VRI_API=vulkan|d3d12). - Native-handle interop (
ext/vri_ext_interop.h,VRI_INTERFACE_INTEROP) is the OpenXR seam: it hands out VRI's native objects (GetDeviceNativeHandles→VkInstance/VkPhysicalDevice/VkDevice/queue) to build anXrGraphicsBindingVulkanKHR, and wraps externally-created textures (WrapTexture, e.g. OpenXR swapchainVkImages) back intoVriTexture. ForXR_KHR_vulkan_enable2,VriDeviceCreationDesc::nativeCreateInfo(aVriVulkanCreateHooks) lets OpenXR interposevkCreate{Instance,Device}while VRI still builds the create-infos. VRI does not depend on OpenXR — the opt-inexample-openxr(--vri_build_openxr=y) links the loader and renders both eyes in one multiview pass; verified rendering on the Meta XR Simulator. Needs an OpenXR runtime to run, so it is never built in CI. - GPU timestamp queries (
ext/vri_ext_query.h) record the GPU clock at points in a command buffer and resolve the ticks into a buffer; scale byVriDeviceDesc::timestampPeriodNanosecondsfor nanoseconds. The basis of a GPU profiler — seeexample-profiler. On Vulkan, Direct3D 12, Metal (anMTLCounterSampleBuffer; Apple GPUs sample only at encoder stage boundaries, so each timestamp opens a one-shot blit encoder, and the counter buffer is resolved CPU-side in the submit's completion handler), WebGPU (desktop wgpu-native, via thetimestamp-query+TimestampQueryInsideEncodersfeatures; browser WebGPU timestamps are pass-scoped and not yet wired), and desktop OpenGL 4.4+ (glQueryCounter+ a query buffer object; GLES/WebGL have no core timer query). Occlusion queries (samples-passed) are also supported on Vulkan + D3D12 + Metal (a native visibility-result buffer +MTLVisibilityResultModeCounting) + desktop OpenGL (WebGPU's occlusion binds the query set at pass-begin, which this API doesn't express), pipeline-statistics queries (per-stage invocation counts, a portableVriPipelineStatisticsstruct) on Vulkan + D3D12 + desktop OpenGL 4.6 (viaARB_pipeline_statistics_query, one GL query object per stat; Apple GPUs expose no statistic counter set), and calibrated timestamps (a correlated GPU+CPU clock pair, for aligning GPU spans with a CPU trace) on Vulkan + D3D12 + Metal ([MTLDevice sampleTimestamps:gpuTimestamp:]). - Multiview (single-pass layered rendering) renders to several layers of an array target in one
pass, selected by a
viewMaskon both the graphics pipeline (VriOutputMergerDesc::viewMask) and the render pass (VriAttachmentsDesc::viewMask); the shader reads the per-view index viaSV_ViewID(→ SPIR-Vgl_ViewIndex/gl_ViewID_OVR). This is the basis of single-pass VR stereo. Implemented on Vulkan (core 1.1 multiview), Direct3D 12 (ViewInstancing— one draw fans out to N view instances, each routed to its own render-target array slice via the pipeline's per-viewRenderTargetArrayIndex; the shader readsSV_ViewID), Metal (SPIRV-Cross emits the instanced form — the draw's instance count is multiplied by the view count and each view writes its array slice via[[render_target_array_index]]), and OpenGL / OpenGL ES (GL_OVR_multiview2, the standalone-headset path; SPIRV-Cross emits thegl_ViewID_OVRform). Exposed viaVriDeviceDesc::hasMultiview+maxViewCount. - Built-in Dear ImGui renderer (
ext/vri_ext_imgui.h,VRI_INTERFACE_IMGUI) draws ImGui through VRI's own core interface, so a single renderer covers every backend with no per-backendimgui_impl_*and it flows through the validation layer for free. VRI does not depend on or link Dear ImGui: the application owns the ImGui environment (context, input,NewFrame) and each frame hands VRI a backend-neutral, flattenedVriImguiDrawData(so no ImGui types cross into VRI). Each draw command carries the texture it samples (VriImguiDrawCommand::textureView), so user images and ImGui 1.92's dynamic, host-owned textures work, not just the font atlas — the host creates textures with the core interface, routes them through ImGui's texture id, and tells the renderer to drop a cached binding (FreeImguiTexture) when a texture is destroyed. Docking + multi-viewport are supported: each detached OS window ImGui spawns gets its ownVriImguiViewport(independent geometry buffers, sharing the pipeline/font/texture cache) driven by the*Tocalls, so several windows render per frame without clobbering each other. The examples enable docking and multi-viewport (detached OS windows) by default — drag a panel out of the main window to pop it into its own OS window; opt out withVRI_IMGUI_VIEWPORTS=0(auto-off in headless capture). The examples' control panel is drawn with it. - Pipeline cache (
ext/vri_ext_pipeline_cache.h,VRI_INTERFACE_PIPELINE_CACHE) lets the driver reuse work (shader compilation, PSO assembly) across pipeline creations and across runs: pass a cache inVriGraphicsPipelineDesc::pipelineCache, serialize it on exit (GetPipelineCacheData), and seed the next launch (CreatePipelineCache) for fast warm startup. A stale or foreign blob is detected and ignored, so seeding is always safe. On Vulkan (VkPipelineCache), Direct3D 12 (ID3D12PipelineLibrary, keyed by a stable hash of each pipeline's definition), Metal (anMTLBinaryArchivepopulated at pipeline creation; the blob is bridged through a temp file since Metal only serializes to a file URL), and desktop OpenGL (emulated with program binaries — a cache hit restores the linked program viaglProgramBinary, skipping the GLSL compile + link). WebGPU and WebGL 2 expose no cache blob (their runtime caches shaders internally), so they reportUnsupported. - GPU-driven draw count.
CmdDrawIndirectCount/CmdDrawIndexedIndirectCounttake the number of draws from a GPU buffer (auint32at an offset, clamped tomaxDrawNum) instead of the CPU — so a compute pass can decide how many draws to issue. Gated byVriDeviceDesc::hasDrawIndirectCount. On Vulkan (vkCmdDraw*IndirectCount), Direct3D 12 (ExecuteIndirectwith a count buffer, command signature cached by stride), and desktop OpenGL 4.6 (glMultiDraw*IndirectCount). (PlainCmdDrawIndirect/CmdDrawIndexedIndirect, with a CPU-known count, are separate and more widely supported.) - Live VRAM budget.
GetVideoMemoryInfo(device, location, &info)reports the OS-current memory budget and current usage (bytes) for a memory location, for streaming / eviction decisions. On Vulkan (VK_EXT_memory_budget), Direct3D 12 (IDXGIAdapter3::QueryVideoMemoryInfo), and Metal (working-set size); OpenGL and WebGPU have no portable query, so they returnUnsupported. - Clear storage buffer.
CmdClearStorageBuffer(cmd, buffer, offset, size, value)fills a storage buffer region with a repeateduint32outside a render pass — e.g. resetting an atomic counter or accumulation buffer before a compute pass. Gated byVriDeviceDesc::hasClearStorageBuffer. On Vulkan (vkCmdFillBuffer), Direct3D 12 (ClearUnorderedAccessViewvia a transient UAV), and desktop OpenGL 4.3 (glClearBufferSubData); WebGPU/Metal (no arbitrary-value buffer fill) reportUnsupported.CmdClearStorageTextureclears a storage texture to a color the same way (vkCmdClearColorImage/ClearUnorderedAccessView{Float,Uint}/glClearTexImage, GL 4.4), gated byhasClearStorageTexture.
VriGraphicsAPI_Software renders without a GPU by running VRI's Vulkan backend on top of a
software Vulkan implementation — a CPU ICD that JITs SPIR-V shaders. This is the same approach
Chrome and the Android emulator use for their GPU-less fallback. It exposes the full Vulkan
feature set (the whole Vulkan column above) at CPU speed, and the device reports
VriAdapterType_Software.
Selecting it. Pass VriGraphicsAPI_Software to vriCreateDevice, or run the examples with
VRI_API=software. It is also the last Auto fallback on every non-web platform, so a machine
with no usable GPU still renders instead of failing outright. If no software Vulkan device is
present, a Software request returns VriResult_Unsupported — it never silently falls back to a
GPU, because "software" means the CPU path specifically.
Providing the ICD (bring-your-own). VRI does not bundle a software renderer; it uses whichever
software Vulkan ICD is on the system, resolved in this order: (1) a caller-set VK_ICD_FILENAMES /
VK_DRIVER_FILES (the standard Vulkan loader env vars) is respected as-is; (2) otherwise, a
SwiftShader manifest (vk_swiftshader_icd.json)
sitting next to the executable is selected automatically. If neither is found, the request fails
with a message naming both options.
Per platform:
-
Linux —
sudo apt install mesa-vulkan-driversinstalls lavapipe; the loader discovers it automatically, soVRI_API=softwarejust works with no env var. This is the standard headless-CI setup (equivalents:dnf install mesa-vulkan-drivers,pacman -S vulkan-swrast, …).sudo apt install -y mesa-vulkan-drivers VRI_API=software xmake run example-triangle
-
Windows — download a Mesa build that includes lavapipe (e.g. pal1000/mesa-dist-win, the
mesa3d-<ver>-release-msvc.7z), extract it, and point the loader at the ICD manifest:$env:VK_ICD_FILENAMES = "C:\path\to\mesa\x64\lvp_icd.x86_64.json" $env:VRI_API = "software" xmake run example-triangle
Or build SwiftShader and drop
vk_swiftshader.dll+vk_swiftshader_icd.jsonnext to the app for zero-config auto-discovery (no env var needed). -
macOS — build SwiftShader (
vk_swiftshader.dylib+ manifest) and either setVK_ICD_FILENAMESor place it beside the app. Note MoltenVK is not a software renderer — it maps Vulkan onto the Metal GPU; for a genuine no-GPU path use SwiftShader.cmake -S swiftshader -B swiftshader/build -DSWIFTSHADER_BUILD_TESTS=OFF cmake --build swiftshader/build --config Release export VK_ICD_FILENAMES=.../swiftshader/build/Darwin/vk_swiftshader_icd.json VRI_API=software xmake run example-triangle
The selection logic lives in source/backend_vk/software_icd_vk.cpp.