Vireo RHI is an open-source C++ library that implements a common abstraction layer on top of modern graphics APIs, with the goal of providing a low-level, high-performance interface that hides the boilerplate of Vulkan and DirectX 12 without sacrificing explicit control over the GPU.
Vireo is not a rendering engine. It is a library intended to be used as a submodule or dependency of an engine or application, as is the case with Lysa Engine. Features are added incrementally as needed by dependent projects.
Two backends are currently implemented:
Vireo relies on C++23 modules (.ixx) rather than traditional header files.
This speeds up compilation, reduces namespace pollution, and makes internal dependencies fully explicit.
The minimum required compiler is MSVC 19+ or LLVM/MingW 21+ on Windows, and LLVM 21+ on Linux.
Vireo's architecture is based on a layer stack: application code only interacts with the abstract RHI layer, which delegates to the Vulkan or DirectX 12 backend selected at runtime.
The backend is selected at instantiation of the Vireo object.
The main object is created via the static method Vireo::create(), which takes
a vireo::BackendConfiguration
object. The backend type is passed as an enumeration:
import vireo;
auto config = vireo::BackendConfiguration{};
config.backend = vireo::Backend::VULKAN; // or DIRECTX
auto vireoInstance = vireo::Vireo::create(config);
The static method Vireo::isBackendSupported()
allows testing at runtime whether a given backend is available.
Each backend is organized into a set of modules implementing the same functional domains:
Vireo.ixxAbstract interfaceVKVireo / DXVireoImplementation*DevicesInstance, GPU*ResourcesBuffers, Images*PipelinesGraphic/Compute*CommandsCommandListAll these classes are pure abstract interfaces whose concrete implementations
live in the backends' internal namespaces (VK* / DX*).
The application only manipulates std::shared_ptr
to the abstract interfaces.
Vireo precisely distinguishes memory location by buffer type.
VERTEX
and INDEX
buffers reside in device-only memory (pure VRAM, inaccessible to the CPU),
while UNIFORM buffers and transfer buffers reside in host-accessible memory.
To upload data to VRAM, the
CommandList::upload() method automatically handles
the creation of a temporary staging buffer:
// Creating a vertex buffer in device-only memory
auto vertexBuffer = vireo->createBuffer(
vireo::BufferType::VERTEX,
sizeof(Vertex),
vertices.size());
// Upload via automatic staging buffer
cmdList->begin();
cmdList->upload(vertexBuffer, vertices.data());
cmdList->end();
transferQueue->submit({cmdList});
transferQueue->waitIdle();
Images support a wide range of formats: R8 to R32G32B32A32 in integer/float,
depth/stencil formats D16, D24_UNORM_S8, D32_SFLOAT, and the 14 BCn compression formats (BC1 to BC7). Read-only, read/write, and render target images are created with dedicated methods on the Vireo object.
Vireo's descriptor system follows the Vulkan model: first declare a
DescriptorLayout that describes
which resources are accessible (uniform, image, sampler…), then create a
DescriptorSet that binds concrete resources
to that layout, and finally group the layouts into a
PipelineResources
(equivalent to DirectX's Root Signature or Vulkan's Pipeline Layout).
// Layout declaration
auto layout = vireo->createDescriptorLayout();
layout->add(BINDING_GLOBAL, vireo::DescriptorType::UNIFORM);
layout->add(BINDING_TEXTURES, vireo::DescriptorType::SAMPLED_IMAGE, texCount);
layout->build();
// Creating and populating the descriptor set
auto descSet = vireo->createDescriptorSet(layout);
descSet->update(BINDING_GLOBAL, globalUniform);
descSet->update(BINDING_TEXTURES, textures);
Vireo supports push constants for small, frequently updated data, dynamic uniforms for indexing into a uniform array without recreating a descriptor set, and read/write images (UAV) for compute shaders.
CommandList objects are created from
a CommandAllocator.
There is no render pass object in the classic Vulkan sense:
a render pass is simply a sequence of commands
delimited by beginRendering() / endRendering(),
with explicit pipeline barriers for resource state transitions.
// Typical render loop
frame.commandAllocator->reset();
auto cmd = frame.commandList;
cmd->begin();
// Transition: UNDEFINED → RENDER_TARGET_COLOR
cmd->barrier(swapChain,
vireo::ResourceState::UNDEFINED,
vireo::ResourceState::RENDER_TARGET_COLOR);
cmd->beginRendering(renderingConfig);
cmd->bindPipeline(pipeline);
cmd->bindDescriptor(pipeline, descSet, SET_GLOBAL);
cmd->setViewport(viewport);
cmd->setScissor(scissor);
cmd->bindVertexBuffer(vertexBuffer);
cmd->draw(3);
cmd->endRendering();
// Transition: RENDER_TARGET_COLOR → PRESENT
cmd->barrier(swapChain,
vireo::ResourceState::RENDER_TARGET_COLOR,
vireo::ResourceState::PRESENT);
cmd->end();
graphicQueue->submit(frame.inFlightFence, swapChain, {cmd});
swapChain->present();
swapChain->nextFrameIndex();
The SwapChain is the bridge between the GPU and system windows.
It manages double or triple buffering and exposes two presentation modes:
IMMEDIATE (may produce tearing) and
VSYNC.
GPU/GPU synchronization between rendering and presentation is handled internally by the SwapChain
and SubmitQueue to ensure portability across APIs.
Vireo exposes two types of pipelines.
The GraphicPipeline encapsulates the full GPU state required for rasterized rendering:
color attachment formats, per-attachment color blending, vertex format (VertexInputLayout),
primitive topology, polygon mode, culling, depth/stencil, MSAA, and shader modules.
The ComputePipeline binds a single compute shader to its resources.
Pipeline compilation (SPIR-V/DXIL → hardware ISA translation) is costly. You should never create a pipeline on the fly during rendering. All pipelines must be created during the application's initialization phase.
Vireo recommends using the Slang language, an HLSL-derived language designed for multi-API portability. A single .slang source file is compiled into the two required intermediate binary formats:
shader.slangSingle sourceslangcVulkan SDK.spvSPIR-V (Vulkan).dxilDXIL (DirectX 12)ShaderModuleAuto loadingFile naming conventions drive the compilation type automatically in the
CMake script provided with the examples:
.vert.slang for vertex shaders, .frag.slang for fragment shaders, .comp.slang for compute shaders, .hull.slang, .domain.slang and .geom.slang for optional tessellation and geometry stages. A .inc.slang file is ignored (it is a shared include).
At runtime, Vireo::createShaderModule() takes a filename
without extension. The library automatically appends the correct extension based on the active backend
(.spv or .dxil), making application code entirely backend-agnostic.
// Example: wave effect compute shader (Slang)
struct Params {
uint2 imageSize;
float time;
};
ConstantBuffer<Params> params : register(b0);
RWTexture2D output : register(u1);
[shader("compute")]
[numthreads(8, 8, 1)]
void main(uint3 id : SV_DispatchThreadID) {
float2 uv = float2(id.xy) / params.imageSize;
// No Vulkan or DirectX-specific directives
output[id.xy] = float4(computeColor(uv, params.time), 1.0);
}
SPIR-V compatibility for DirectX 12 is announced with Shader Model 7, which will eventually eliminate the need to maintain two separate binary files.
Vireo exposes three distinct synchronization primitives, covering the three levels of coordination required in a modern rendering engine:
| Primitive | Scope | Typical Use |
|---|---|---|
Fence |
CPU ↔ GPU | Wait for a frame's rendering to complete before reusing its resources |
Semaphore |
GPU ↔ GPU | Synchronize distinct render passes, or pipelined submissions |
Barrier (cmdList::barrier()) |
Internal GPU | Resource state transitions between sub-passes within the same command list |
The synchronization model is explicit: there is no implicit synchronization between passes.
The developer must insert barriers at the right places for layout transitions
(UNDEFINED → RENDER_TARGET_COLOR → PRESENT). This approach, faithful to Vulkan and DX12,
maximizes overlap opportunities on the GPU.
The recommended pattern for multi-frame rendering is to allocate one Fence
and one CommandAllocator per frame.
SwapChain::acquire(fence) blocks the CPU until the image is free,
then SubmitQueue::submit(fence, swapChain, {cmd}) signals the fence upon render completion:
// Per-frame data (allocated at init)
struct FrameData {
std::shared_ptr<vireo::CommandAllocator> cmdAlloc;
std::shared_ptr<vireo::CommandList> cmdList;
std::shared_ptr<vireo::Fence> inFlightFence;
};
// Render loop
auto& frame = frames[swapChain->getCurrentFrameIndex()];
if (!swapChain->acquire(frame.inFlightFence)) return;
frame.cmdAlloc->reset();
// ... record commands ...
graphicQueue->submit(frame.inFlightFence, swapChain, {frame.cmdList});
swapChain->present();
swapChain->nextFrameIndex();
Vireo RHI is designed to be integrated as a CMake submodule (add_subdirectory)
or as a Git submodule. Third-party dependencies (DX12 headers, SDL, etc.) are automatically fetched by
CMake via FetchContent, with the exception of the Vulkan SDK which must be installed manually.
| Platform | Compiler | Notes |
|---|---|---|
| Windows | MSVC 19+ / LLVM+MingW 21+ | DirectX + Vulkan (LLVM: Vulkan only) |
| Linux | LLVM 21+ | Vulkan only, X11 & Wayland via SDL3 |
DIRECTX_BACKEND — Enables the DirectX 12 backend (default: ON on Windows)LUA_BINDING — Compiles Lua 5.4+ bindings (default: OFF)USE_SDL3 — Uses SDL3 as the windowing abstraction layer (default: ON on Linux)git clone https://github.com/HenriMichelon/vireo_rhi
cmake -B build -G Ninja -D CMAKE_BUILD_TYPE=Release
cmake --build build
When LUA_BINDING=ON, an additional vireo.lua module
exposes the entire Vireo API via LuaBridge 3.
This allows writing the high-level logic of an application in Lua 5.4+ while preserving the C++23 runtime performance
for low-level rendering.
The official Vireo RHI documentation is hosted on GitHub Pages.
The Hello, Triangle! tutorial guides the developer from setting up the development environment to rendering the first colored triangle. It covers in order: CMake project configuration, instantiation of the Vireo object, creation of submission queues, the swap chain, command allocators and command lists, command recording, viewports, vertex data setup, pipeline creation, shader compilation and loading, final pipeline configuration, and draw commands.
The github.com/HenriMichelon/vireo_samples repository contains a series of progressive example programs, covering the essential features of Vireo. Each example is self-contained, includes its own Slang shaders compiled to SPIR-V and DXIL, and illustrates one or more additional concepts compared to the previous example.
The classic "Hello Triangle" with a per-vertex RGB gradient. Absolute starting point: swap chain, command lists, vertex buffer, graphics pipeline, image barriers, fences.
Same triangle with a loaded and applied texture. Introduces images, samplers, descriptor layouts, and descriptor sets.
Multiple triangles with transparency and shader-based material. Uniform buffers, color blending, push constants, multiple pipelines.
Indexed indirect drawing (drawIndexedIndirect) — the GPU determines draw parameters from a buffer in VRAM, with no CPU round-trip.
Multi-sample anti-aliasing. Introduces custom render targets and the MSAA pipeline with automatic resolve.
Animated wave effect generated entirely on the GPU via a Slang compute shader. Read/Write images, compute pipeline, image copy.
Two rotating textured cubes, camera, lighting, skybox. Full forward rendering with depth pre-pass, cubemap, semaphores, dynamic uniforms, and post-processing (SMAA, FXAA, gamma, voronoi).
Same scene in deferred rendering: G-buffers, deferred lighting, weighted OIT (Order Independent Transparency), stencil for skybox optimization, push constants replacing dynamic uniforms.
The Cube and Deferred samples are the most comprehensive references: they cover virtually all Vireo RHI features in a realistic rendering context, and their Slang shaders illustrate classic rendering techniques (Phong, Gbuffers, SMAA, FXAA, TAA, OIT).