RHI
Lumina’s RHI is not a class hierarchy. It is a flat set of free functions in
the Lumina::RHI namespace over opaque handles, declared in
Engine/Source/Runtime/Renderer/RHI.h and implemented per backend. There is one
backend today, Vulkan (Renderer/API/Vulkan/VulkanRHI.cpp). The enum reserves
Metal and DX12 slots, both unimplemented.
There is no OpenGL backend, and there is no render graph. Passes are recorded by hand into command lists, in an explicit order, with explicit barriers.
Shape of the API
Section titled “Shape of the API”RHI::FCmdListH CL = RHI::OpenCommandList();RHI::CmdSetTextureHeap(CL, RHI::Core::GetGlobalHeap());RHI::CmdBeginRenderPass(CL, PassDesc);RHI::CmdSetPipeline(CL, Pipeline);RHI::CmdDrawIndexed(CL, IndexBuffer, 0, Args, IndexCount, 1, 0, 0, 0);RHI::CmdEndRenderPass(CL);RHI::Core::Submit(CL);Four design decisions explain most of what you will read:
- Handles, not objects.
THandle<T>wraps an integer.RHI::IsValid(H)tests it. There is no vtable, no reference counting, and noIRHITextureinterface to implement. - Buffers are raw GPU addresses. There is no buffer object at all.
RHI::Mallocreturns aGPUPtr(auint64device address), and shaders dereference it directly through buffer device address.RHI::ToHost(GPUPtr)gives you the mapped CPU pointer for host-visible memory. - Everything a shader reads is bindless. There are no descriptor sets in the API. Textures, storage images, and samplers live in a texture heap and are addressed by an integer slot. One heap is bound per command list.
- Draw arguments are a pointer. Every
Cmd*Draw/CmdDispatchtakes aGPUPtr DrawArgsthat becomes the shader’s push-constant payload. Passing per-draw data means writing a struct into transient memory and handing over its address.
Handles
Section titled “Handles”using FPipelineH = THandle<FPipeline>;using FTextureH = THandle<FTexture>;using FTextureHeapH = THandle<FTextureHeap>;using FSemaphoreH = THandle<FSemaphore>;using FDepthStencilH = THandle<FDepthStencilState>;using FCmdListH = THandle<FCommandList>;using FSwapchainH = THandle<FSwapchain>;using FSurfaceH = THandle<FSurface>;Every handle type has a FreeH overload. RAII wrappers exist and should be
preferred for anything with a lifetime longer than a function:
RHI::TUniqueH<T>(aliased asFPipelineUH,FTextureUH,FTextureHeapUH,FSemaphoreUH,FDepthStencilUH) frees the handle on destruction.RHI::FUniqueGPUPtrdoes the same for aGPUPtr.
Memory
Section titled “Memory”GPUPtr Malloc(uint64 Size, uint64 Alignment = 16, EMemoryType Type = EMemoryType::Default);void* ToHost(GPUPtr GPU);void Free(GPUPtr GPU);EMemoryType is CPUWrite (the default), CPURead, or GPUOnly. Allocations
above kDedicatedMemoryThreshold (32 MB) get a dedicated allocation.
RHI::New<T>(Count) allocates and value-constructs an array, returning both the
host pointer and the GPU address:
auto [Host, Gpu] = RHI::New<FMyStruct>(Count);Host[0].Value = 1.0f;// pass Gpu to a shaderFreeing memory the GPU may still be reading is the classic bug here. Use
RHI::Core::DeferredFree(Ptr, ExtraFrames) instead of RHI::Free for anything
referenced by submitted work. It retires the allocation once every in-flight
frame has completed. ExtraFrames extends that window for memory whose address
can still be handed out after the free was queued, for instance a game-thread
cache that will return the old address for one more tick.
Transient memory
Section titled “Transient memory”RHI::Core::AllocTransient(Size, Alignment) is a per-frame bump allocator over
CPU-write, device-addressable memory. It is thread safe (atomic bump) and valid
until its frame slot is reused.
GPUPtr Args = RHI::Core::CopyTransient(MyPushConstants);GPUPtr Data = RHI::Core::CopyTransientArray(Items.data(), Items.size());This is the intended way to pass per-draw and per-pass data. It is not for geometry. Vertex and index data belong in a persistent allocation; the transient ring is sized for small per-frame payloads and cycling megabytes of mesh data through it will exhaust it.
Textures
Section titled “Textures”FTextureDesc Desc{ .Type = ETextureType::Tex2D, .Dimension = { Width, Height, 1 }, .MipCount = Mips, .LayerCount = 1, .Format = EFormat::RGBA8_UNORM, .Usage = EImageUsageFlags::Sampled | EImageUsageFlags::TransferDst,};RHI::FTextureH Texture = RHI::CreateTexture(Desc);Types cover 1D, 2D, 3D, cube, 2D array, and cube array. Usage flags are
Sampled, Storage, ColorAttachment, DepthAttachment, TransferSrc,
TransferDst.
CreateTexture takes an optional GPUPtr Location to place the image in memory
you already own. GetTextureDesc(Texture) reads a description back.
Copies and blits operate on FTextureSlice (mip, layer, layer count, offset,
extent; an extent of 0 means the full mip):
CmdCopyTexture, CmdCopyMemoryToTexture, CmdCopyTextureToMemory,CmdBlitTexture, CmdResolveTexture, CmdClearTexture, CmdClearTextureUIntThe texture heap
Section titled “The texture heap”FTextureHeapH CreateTextureHeap(uint32 TextureCount, uint32 RWTextureCount, uint32 SamplerCount);
uint32 HeapWriteTexture(FTextureHeapH Heap, FTextureH Texture);uint32 HeapWriteRWTexture(FTextureHeapH Heap, FTextureH Texture, uint32 Mip = 0);uint32 HeapWriteSampler(FTextureHeapH Heap, const FSamplerDesc& Desc);void HeapFreeTexture / HeapFreeRWTexture / HeapFreeSampler(Heap, uint32 Slot);Each HeapWrite* returns a slot index. That integer is what you put in a shader
struct; the shader indexes the heap with it. kInvalidHeapSlot (~0u) marks an
unset slot.
Binding slots inside the heap are fixed:
| Constant | Value | Contents |
|---|---|---|
kSamplerBindingSlot | 0 | Samplers |
kImageBindingSlot | 1 | Sampled images |
kRWImageBindingSlot | 2 | Storage images |
Limits: kMaxTextureHeapSize (INT16_MAX) entries, kMaxNumSamplers (4000),
kMaxNumTextureHeaps (1024).
RHI::Core::GetGlobalHeap() is the process-wide heap that virtually everything
uses. CmdSetTextureHeap(CL, Heap) binds it, and every render path does this as
its first command.
Slot lifetime is the sharp edge. A slot freed while a submitted command list still references it produces a GPU-side read of a destroyed image. Free heap slots on the same deferred schedule as the resources behind them.
Stock samplers are registered by Core::Initialize in a fixed order, exposed as
EStockSampler: LinearWrap, LinearClamp, LinearMirror, PointWrap,
PointClamp, AnisoWrap, AnisoClamp, Shadow, MinReduction,
MaxReduction. These must stay in lockstep with the SAMPLER_* constants in
GlobalRHI.slang.
GetTextureHeapTextures(Heap, Out) enumerates every occupied sampled slot, which
is what the editor’s GPU resource views display.
Pipelines
Section titled “Pipelines”FPipelineH CreateGraphicsPipeline(const FShaderSource& Vertex, const FShaderSource& Fragment, const FRasterDesc& Desc, TSpan<const FSpecializationConstant> = {});FPipelineH CreateComputePipeline(const FShaderSource& Compute, TSpan<const FSpecializationConstant> = {});FPipelineH CreateMeshShaderPipeline(const FShaderSource& Task, const FShaderSource& Mesh, const FShaderSource& Fragment, const FRasterDesc& Desc, TSpan<const FSpecializationConstant> = {});FShaderSource is a SPIR-V byte span plus an entry point name.
RHI::Core::CreateGraphicsPipeline(VertexName, PixelName, Desc) is the
convenience form that pulls bytecode from the engine shader library by
FName.
FRasterDesc carries topology, sample count, wireframe, alpha to coverage, the
depth and stencil formats, and the color target list (each with its own
FBlendDesc). There is no vertex input layout: vertices are pulled from
buffers through device addresses in the shader.
Mesh shader pipelines require RHI::SupportsMeshShaders(); the call returns an
invalid handle otherwise. The task stage is optional (an empty source means a
mesh-only pipeline).
Depth and stencil state is a separate object (CreateDepthStencil) bound with
CmdSetDepthStencilState, because the engine reuses one pipeline across
different depth modes.
Dynamic state
Section titled “Dynamic state”Front face, cull mode, line width, viewport, scissor, depth and stencil state, and the index buffer are all dynamic:
CmdSetFrontFace, CmdSetCullMode, CmdSetLineWidth,CmdSetViewport, CmdSetScissor,CmdSetDepthStencilState, CmdSetIndexBufferThis keeps the pipeline permutation count down: the same pipeline serves both cull modes and every viewport size.
Command lists and submission
Section titled “Command lists and submission”FCmdListH OpenCommandList(EQueueType Type = EQueueType::Graphics);void ResetCommandList(FCmdListH CL);void Submit(EQueueType Queue, TSpan<const FCmdListH> CLs, TSpan<const FSemaphoreInfo> Waits = {}, TSpan<const FSemaphoreInfo> Signals = {});void Submit(FCmdListH CL, EQueueType Type = EQueueType::Graphics);void SubmitAndWait(FCmdListH CL);EQueueType is Graphics, Transfer, or Compute.
SubmitAndWait submits on the graphics queue and blocks until only that
submission completes, by waiting its own frame-timeline value. Use it for
one-off captures. Do not use Submit followed by WaitDeviceIdle from
inside a render command: WaitDeviceIdle blocks on unrelated in-flight frame
work while holding the cooperative drain, which hangs the next
FlushRenderingCommands.
Command list recording is single threaded per list. Multiple scenes may record concurrently because each opens its own list; shared resource creation inside the RHI is internally locked.
Synchronization
Section titled “Synchronization”Semaphores are timeline semaphores:
FSemaphoreH CreateSemaphore(uint64 InitialValue);void WaitSemaphore(FSemaphoreH Semaphore, uint64 Value);FSemaphoreInfo { Semaphore, Value, Stage } is what you pass to Submit as a
wait or a signal.
Barriers are expressed as stage-to-stage transitions, not as per-resource transitions:
void CmdBarrier(FCmdListH CL, EStageFlags Before, EStageFlags After);EStageFlags covers IndirectArguments, Transfer, Compute,
RasterColorOut, PixelShader, FragmentTests, VertexShader, Host,
MeshShader, TaskShader, and AllCommands.
The RHI::Barriers namespace holds the canonical combinations, and passes should
use these rather than hand-rolling stage masks:
| Helper | Orders |
|---|---|
ComputeToAll | Compute writes before any later read, including indirect args. |
RasterToRead | Color and depth writes before shader reads. |
RasterToRaster | Attachment writes before the next attachment writes. |
TransferToAll | Copies before everything. |
TransferToTransfer | Copy before copy (resolves write-after-write hazards between two copies to the same image). |
AllToTransfer | Everything before a copy. |
Image layout transitions are handled by the backend. See Vulkan Backend for how unified image layouts remove most of that bookkeeping.
Render passes
Section titled “Render passes”CmdBeginRenderPass / CmdEndRenderPass take an FRenderPassDesc of color
attachments, a depth attachment, a stencil attachment, and a render area. Each
FRenderAttachment has a texture, an optional MSAA resolve target, a load op, a
store op, and a clear color. This maps onto Vulkan dynamic rendering; there are
no VkRenderPass or framebuffer objects.
Draws and dispatches
Section titled “Draws and dispatches”CmdDraw(CL, DrawArgs, VertexCount, InstanceCount, FirstVertex, FirstInstance);CmdDrawIndexed(CL, IndexBuffer, IndexOffset, DrawArgs, IndexCount, InstanceCount, FirstIndex, VertexOffset, FirstInstance, IndexType);CmdDrawIndirect(CL, DrawArgs, IndirectBuffer, Offset, DrawCount, Stride);CmdDrawIndexedIndirect(CL, DrawArgs, Offset, DrawCount, Stride);CmdDispatch(CL, DrawArgs, GroupX, GroupY, GroupZ);CmdDispatchIndirect(CL, DrawArgs, IndirectBuffer, Offset);CmdDrawMeshTasks(CL, DrawArgs, GroupCountX, GroupCountY, GroupCountZ);CmdDrawMeshTasksIndirect(CL, DrawArgs, IndirectBuffer, Offset, DrawCount, Stride);CmdDrawMeshTasksIndirectCount(CL, DrawArgs, IndirectBuffer, Offset, CountBuffer, CountOffset, MaxDrawCount, Stride);Indirect argument structs mirror the Vulkan ones: FDrawIndirectArguments,
FDrawIndexedIndirectArguments, FDispatchIndirectArguments, and
FDrawMeshTasksIndirectArguments.
Device, swapchain, and presentation
Section titled “Device, swapchain, and presentation”void CreateDevice(const FDeviceDesc& Desc = {}); // bValidation, bDebugUtilsvoid FreeDevice();void WaitDeviceIdle();void TickFrame();
FSurfaceH CreateSurface(void* WindowHandle); // MAIN THREAD ONLYFSwapchainH CreateSwapchain(FSurfaceH Surface, const FUIntVector2& Extent);void RecreateSwapchain(FSwapchainH Swapchain, const FUIntVector2& Extent);FTextureH AcquireNextImage(FSwapchainH Swapchain); // invalid handle if out of datebool PresentSwapchain(FSwapchainH, FCmdListH Final, FSemaphoreH FrameSignal, uint64 Value);void SetVSync(bool) / bool GetVSync();CreateSurface must be called on the thread that owns the window, because GLFW’s
window calls are main-thread only. The handle is then passed to the render side,
where CreateSwapchain consumes it and takes ownership. FreeH on a surface is
only for the case where the window died before a swapchain was built.
AcquireNextImage returning an invalid handle means out of date; recreate the
swapchain and skip the frame.
kFramesInFlight is 3.
Introspection
Section titled “Introspection”FGPUDeviceInfo GetDeviceInfo(); // name, API version string, vendor ID, discretevoid GetGPUMemoryStats(FGPUMemoryStats& Out);bool SupportsMeshShaders();ICrashTracker& GetCrashTracker();void HandleDeviceLost();FGPUMemoryStats breaks down per heap: budget and usage as reported by the OS,
plus allocated and block bytes from the allocator. The gap between allocated and
block bytes is fragmentation and reserve. bReBAR marks a heap that is both
device local and host visible and larger than the legacy 256 MB BAR window.
Common failure modes
Section titled “Common failure modes”| Symptom | Cause |
|---|---|
| GPU reads garbage from a buffer freed last frame | RHI::Free instead of Core::DeferredFree. |
| Validation error about a destroyed image still in a descriptor | A heap slot was freed before the frames referencing it retired. |
Hang on the next FlushRenderingCommands | WaitDeviceIdle called from inside a render command. Use SubmitAndWait. |
| Transient allocation failure mid-frame | Geometry or large buffers pushed through the transient ring. |
| Crash creating a surface | CreateSurface called off the main thread. |
| Mesh shader pipeline handle is invalid | SupportsMeshShaders() is false on this device. |
| Write-after-write hazard between two copies | Missing Barriers::TransferToTransfer between them. |