What is Vireo RHI?

C++23 / Modules / Vulkan 1.3 / DirectX 12 / Windows / Linux Henri Michelon
Table of Contents
  1. Goal & Positioning
  2. General Architecture
  3. API: Key Concepts
  4. Shaders & Compilation Pipeline
  5. Synchronization
  6. Build & Integration
  7. Documentation
  8. Examples — vireo_samples

Goal & Positioning

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.

Note

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.

Supported Backends

Two backends are currently implemented:

Development Language

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.

General Architecture

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.

Application / Engine (e.g. Lysa Engine)
vireo::Vireo — Unified abstract API
module vireo — Vireo.ixx
Vulkan 1.3 Backend
VKVireo, VKCommands…
DirectX 12 Backend
DXVireo, DXCommands…
GPU / Hardware Driver

Runtime Backend Selection

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.

Source Code Organization

Each backend is organized into a set of modules implementing the same functional domains:

Vireo.ixxAbstract interface
VKVireo / DXVireoImplementation
*DevicesInstance, GPU
*ResourcesBuffers, Images
*PipelinesGraphic/Compute
*CommandsCommandList
Each VK* / DX* prefix implements the same domain

Abstract Class Hierarchy

VireoEntry point, factory for all objects
InstanceVkInstance / IDXGIFactory4
PhysicalDeviceVkPhysicalDevice / IDXGIAdapter4
DeviceVkDevice / ID3D12Device
SwapChainDouble/triple buffering & presentation
BufferVertex, Index, Uniform, Storage…
Image2D/array texture, mipmaps, BCn
SamplerFilters, addressing modes, LOD
ShaderModuleSPIR-V / DXIL loading
GraphicPipelineFull GPU state for rendering
ComputePipelineGeneral-purpose compute pipeline
DescriptorLayoutDescribes bound resources
DescriptorSetResource instance
PipelineResourcesRoot Signature / Pipeline Layout
CommandAllocatorMemory pool for commands
CommandListGPU command recording
SubmitQueueGPU submission queue
FenceCPU/GPU sync
SemaphoreGPU/GPU sync between passes
RenderTargetRender attachment (color/depth)

All 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.

Key Concepts

GPU Resources (Buffers, Images, Samplers)

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.

Descriptors

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.

Command Lists & Render Pass

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();

Swap Chain

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.

Pipelines

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.

Critical Rule

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.

Shaders & Compilation Pipeline

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 source
slangcVulkan SDK
.spvSPIR-V (Vulkan)
+
.dxilDXIL (DirectX 12)
ShaderModuleAuto loading
Shader compilation pipeline — one source, two binaries

File 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.

Synchronization

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.

Frames in Flight

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();

Build & Integration

Prerequisites

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.

PlatformCompilerNotes
Windows MSVC 19+ / LLVM+MingW 21+ DirectX + Vulkan (LLVM: Vulkan only)
Linux LLVM 21+ Vulkan only, X11 & Wayland via SDL3

CMake Options

git clone https://github.com/HenriMichelon/vireo_rhi
cmake -B build -G Ninja -D CMAKE_BUILD_TYPE=Release
cmake --build build

Lua Bindings

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.


Documentation

The official Vireo RHI documentation is hosted on GitHub Pages.

Hello Triangle Tutorial (14 steps)

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.

Examples — vireo_samples

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.

Triangle

The classic "Hello Triangle" with a per-vertex RGB gradient. Absolute starting point: swap chain, command lists, vertex buffer, graphics pipeline, image barriers, fences.

Swap Chain Vertex Buffer Fence
Triangle Texture

Same triangle with a loaded and applied texture. Introduces images, samplers, descriptor layouts, and descriptor sets.

Images Samplers Descriptor Sets
Triangle Buffers

Multiple triangles with transparency and shader-based material. Uniform buffers, color blending, push constants, multiple pipelines.

Uniform Buffer Push Constants Blending
Indirect Drawing

Indexed indirect drawing (drawIndexedIndirect) — the GPU determines draw parameters from a buffer in VRAM, with no CPU round-trip.

Indirect Draw Index Buffer
MSAA

Multi-sample anti-aliasing. Introduces custom render targets and the MSAA pipeline with automatic resolve.

Render Target MSAA
Compute

Animated wave effect generated entirely on the GPU via a Slang compute shader. Read/Write images, compute pipeline, image copy.

Compute Pipeline RW Images
Cube

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).

Forward Rendering Depth Pre-pass Post-process Semaphores
Deferred

Same scene in deferred rendering: G-buffers, deferred lighting, weighted OIT (Order Independent Transparency), stencil for skybox optimization, push constants replacing dynamic uniforms.

Deferred Rendering G-Buffers OIT Stencil
Tip

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).