Monograph · 03 · GPU / Vulkan
Foundations · deep dive
GPU / Vulkan
This is the GPU host spine: how Vulkan comes up, what resources exist before any frame, how a scene becomes buffers + TLAS, and how one frame is recorded and read back. After this chapter you should be able to walk VulkanRenderer::initialize and renderDeferred without guessing.
Role
VulkanRenderer is an offscreen-capable host. It owns the device, queues, frame ring, deferred child, RT acceleration structure, and optional PathTracers. Examples and embeds call initialize → setScene → render → getPixels (or present via GLFW in interactive).
It is deliberately split across TUs so “init plumbing” does not sit in the same file as path-trace dispatch.
Cold-start init pipeline
Exact order in VulkanRenderer::initialize() (renderer.cpp). Failures return false early; deferred/RT failures can be non-fatal so forward still works.
- createInstance — Vulkan 1.3 app info; optional validation if
OHAO_VALIDATION=1; DLSS may addGET_PHYSICAL_DEVICE_PROPERTIES_2 - pickPhysicalDevice — enumerate GPUs; prefer discrete with a graphics queue
- createLogicalDevice — queue + RT/AS/descriptor-indexing features chain + device extensions
- createCommandPool — for graphics family
- createRenderPass + createOffscreenFramebuffer — legacy/offscreen color path
- Shadow render pass + resources (before descriptors)
- Descriptor set layout (forward path)
- BindlessTextureManager — up to 4096 textures; before pipeline so layouts include bindless set
- createPipeline + shadow pipeline
- UBOs: camera + lights; descriptor pool/sets; vertex buffer scaffolding; fences/semaphores
- initializeFrameResources — ring of N frames (cmd buf, staging, fence)
- DeferredRenderer::initialize — GBuffer/CSM/lighting/post (texture manager injected first)
- RTAccelerationStructure::init — TLAS host; PathTracers stay lazy
Device before anything GPU. Bindless before pipelines that reference its layout. Shadows before lighting descriptors that sample the map. Deferred after textures. RT AS after queue/pool exist. PathTracer images are huge — creating both profiles at 4K OOMs, so they wait for ensureRTRenderer(mode).
Init DAG. PathTracer images are intentionally not allocated until a render mode needs them.
Instance · device · features
Instance
VK_API_VERSION_1_3. Validation only if env set — keeps release paths clean. DLSS path may request instance extension for device properties2.
Physical device
Loop all devices; keep a graphics-capable queue family; always prefer discrete over integrated when both appear (comment in code: NVIDIA over Intel iGPU).
Logical device extensions (RT path)
| Extension | Why |
|---|---|
KHR_acceleration_structure | BLAS/TLAS |
KHR_ray_tracing_pipeline | raygen/hit/miss pipelines |
KHR_deferred_host_operations | required by AS builds |
KHR_buffer_device_address | AS device addresses |
EXT_descriptor_indexing | bindless arrays |
KHR_spirv_1_4 + float controls | RT shader model |
| external memory/semaphore | CUDA/OIDN-style interop readiness |
| NVX binary import / image view handle / push descriptor | DLSS-RR when GPU supports |
Feature chain (pNext): RayTracingPipeline → AccelerationStructure → Vulkan1.3 → Vulkan1.2 (bufferDeviceAddress, descriptorIndexing, runtime arrays, update-after-bind, nonuniform indexing…).
Vulkan enables optional features by linking structs. Order matters for the driver; OHAO builds one chain so RT + bindless + 1.2/1.3 bits light up together.
What exists after a successful init
- Device / queue
- Single graphics queue family (present is host-side for GLFW examples)
- Offscreen targets
- Color (+ depth as needed) for legacy path; deferred owns its GBuffer later
- Bindless manager
- Descriptor set with variable count; materials store indices
- Frame ring
- Typically 3: command buffer, fence, staging mapped for CPU readback
- DeferredRenderer
- Full pass graph object, resized to width×height
- RTAccelerationStructure
- Ready to receive BLAS/TLAS; empty until scene upload
- PathTracer
- Not constructed yet (lazy)
Scene → GPU upload workflow
When setScene runs (or scene dirties), the renderer walks actors and materializes GPU truth. RT path is concentrated in rt_build.cpp.
- Collect mesh components → pack / upload vertex + index buffers; build
m_meshBufferMap - Upload textures into BindlessTextureManager; record indices on materials
- Pack matColors SSBO (3×vec4) + matID per triangle; keep order stable
- Upload lights → deferred UBO and/or PT
GPULightSSBO - Optional HDRI → image + env CDF buffers
- buildBLASTLAS: one BLAS per mesh actor; TLAS instances in same order as material rows
- If PathTracer already exists, re-bind buffers/descriptors (materials, textures, lights, env)
TLAS instance order = material row order
Closesthit / custom indices assume this. Two independent loops that sort differently will silently swap materials across the whole scene.
One deferred frame (host side)
renderDeferred() in render_dispatch.cpp:
- Select ring slot
m_currentFrame; wait on its fence (GPU finished with that slot) - Copy this slot’s staging buffer →
m_pixelBuffer(RGBA8 readback of a prior submit) - Reset command buffer; push camera matrices into DeferredRenderer
- Wire TLAS for hybrid if available; env map view; geometry buffers; update light UBO (sky/sun blend fallback)
- Begin cmd;
DeferredRenderer::render(cmd, frameIndex)— full pass graph - Copy deferred final color → staging; end cmd; queue submit with fence
- Advance frame index modulo ring size
CPU readback of a buffer the GPU still writes is a race. The ring keeps N frames of staging; you always read the slot you just waited on — data from a completed submit, not the one in flight.
Multiple command buffers so CPU can record frame N while GPU executes N−1. OHAO’s offscreen path also uses the ring for safe pixel download.
RT attach & lazy PathTracer
ensureRTRenderer(mode) creates either realtime or offline PathTracer profile, allocates AOV images, builds RT pipeline/SBT, then re-uploads scene bindings if a scene is already set. setRenderMode switches dispatch between deferred / RT paths without recreating the device.
PT render path: bind descriptors → trace rays → optional denoise → copy beauty to staging (see Ch. 06).
Source map by concern
renderer.cppinitialize / shutdown / setScene / modedevice_setup.cppinstance, device, pool, sync, frame resourcesbuffer_setup.cppVB/IB/UBO scaffoldingscene_upload.cppmesh draws, buffer mapsrt_build.cppRT buffers + buildBLASTLASlight_upload.cpplights + envrender_dispatch.cpprenderDeferred / RT / readbackbindless_texture_manager.*indexed texturesframe/frame_resources.*ring slotsDesign units in this module
Each card is a focused design page (what / how / why + sources). Full tree: Sitemap.
VulkanRenderer facade
Public API: modes, setScene, ensureRT, denoise, pixels, present.
Device init
createInstance → pickPhysicalDevice → createLogicalDevice feature chain.
Bindless textures
BindlessTextureManager: variable count array, update-after-bind.
Buffers & allocator
VMA-style allocation, staging, UBOs, GPU allocator helpers.
Scene upload
Meshes → VB/IB map; materials; lights; env.
RT build (BLAS/TLAS)
buildBLASTLAS order, instance transforms, material lockstep.
Render dispatch
renderDeferred vs RT path, staging readback ring.
Layout contracts
OHAO_ASSERT_GPU_LAYOUT, MaterialGpuPack, push-constant sizes.
Legacy pipeline & framebuffer
Forward pipeline creation, offscreen FB, shadow resources.