OHAO · Implementation Monograph

Monograph · 03 · GPU / Vulkan

03

Foundations · deep dive

GPU / Vulkan

Chapter contract

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.

VulkanRenderer::initialize · ordered steps
  1. createInstance — Vulkan 1.3 app info; optional validation if OHAO_VALIDATION=1; DLSS may add GET_PHYSICAL_DEVICE_PROPERTIES_2
  2. pickPhysicalDevice — enumerate GPUs; prefer discrete with a graphics queue
  3. createLogicalDevice — queue + RT/AS/descriptor-indexing features chain + device extensions
  4. createCommandPool — for graphics family
  5. createRenderPass + createOffscreenFramebuffer — legacy/offscreen color path
  6. Shadow render pass + resources (before descriptors)
  7. Descriptor set layout (forward path)
  8. BindlessTextureManager — up to 4096 textures; before pipeline so layouts include bindless set
  9. createPipeline + shadow pipeline
  10. UBOs: camera + lights; descriptor pool/sets; vertex buffer scaffolding; fences/semaphores
  11. initializeFrameResources — ring of N frames (cmd buf, staging, fence)
  12. DeferredRenderer::initialize — GBuffer/CSM/lighting/post (texture manager injected first)
  13. RTAccelerationStructure::init — TLAS host; PathTracers stay lazy
Why this order

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

WORKFLOW · VULKAN COLD START Instance Pick GPU DeviceRT features Cmd pool Offscreen FB Shadow RTs Bindless4096 slots Pipelines UBO / sets Frame ring DEFERREDGBuffer…post RT ASTLAS host PATH TRACERlazy later FIG. GPU-1 · INIT DEPENDENCY ORDER Solid boxes created in initialize(); dashed = ensureRTRenderer on first RT mode
Fig. GPU-1

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)

ExtensionWhy
KHR_acceleration_structureBLAS/TLAS
KHR_ray_tracing_pipelineraygen/hit/miss pipelines
KHR_deferred_host_operationsrequired by AS builds
KHR_buffer_device_addressAS device addresses
EXT_descriptor_indexingbindless arrays
KHR_spirv_1_4 + float controlsRT shader model
external memory/semaphoreCUDA/OIDN-style interop readiness
NVX binary import / image view handle / push descriptorDLSS-RR when GPU supports

Feature chain (pNext): RayTracingPipeline → AccelerationStructure → Vulkan1.3 → Vulkan1.2 (bufferDeviceAddress, descriptorIndexing, runtime arrays, update-after-bind, nonuniform indexing…).

Jargon · pNext feature chain

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.

Upload spine (conceptual)
  1. Collect mesh components → pack / upload vertex + index buffers; build m_meshBufferMap
  2. Upload textures into BindlessTextureManager; record indices on materials
  3. Pack matColors SSBO (3×vec4) + matID per triangle; keep order stable
  4. Upload lights → deferred UBO and/or PT GPULight SSBO
  5. Optional HDRI → image + env CDF buffers
  6. buildBLASTLAS: one BLAS per mesh actor; TLAS instances in same order as material rows
  7. If PathTracer already exists, re-bind buffers/descriptors (materials, textures, lights, env)
Invariant

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:

renderDeferred
  1. Select ring slot m_currentFrame; wait on its fence (GPU finished with that slot)
  2. Copy this slot’s staging buffer → m_pixelBuffer (RGBA8 readback of a prior submit)
  3. Reset command buffer; push camera matrices into DeferredRenderer
  4. Wire TLAS for hybrid if available; env map view; geometry buffers; update light UBO (sky/sun blend fallback)
  5. Begin cmd; DeferredRenderer::render(cmd, frameIndex) — full pass graph
  6. Copy deferred final color → staging; end cmd; queue submit with fence
  7. Advance frame index modulo ring size
Why wait-then-read staging

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.

Jargon · ring / frames 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 / mode
device_setup.cppinstance, device, pool, sync, frame resources
buffer_setup.cppVB/IB/UBO scaffolding
scene_upload.cppmesh draws, buffer maps
rt_build.cppRT buffers + buildBLASTLAS
light_upload.cpplights + env
render_dispatch.cpprenderDeferred / RT / readback
bindless_texture_manager.*indexed textures
frame/frame_resources.*ring slots

Design units in this module

Each card is a focused design page (what / how / why + sources). Full tree: Sitemap.