Tutorial 85: The Vulkan Renderer: Setup and Quirks
What you’ll learn
- Enabling the
VULKANrenderer in CMake and what the Vulkan SDK is needed for. - Turning on validation layers while developing.
- Where Vulkan differs from the EasyGL GL profiles: SPIR-V shaders and explicit synchronisation.
- The modern capabilities it reports in this snapshot (float render targets, shadow and image-based-lighting sampling, base-instance drawing, compute and indirect-draw answers), how to query them, and which of them game code can actually use.
- Its real limits, and its state on macOS and Android.
Before you start — Tutorial 72: Choosing a Renderer — read the group comparison before committing to one renderer. Tutorial 52: Writing Custom Shaders (ShaderEffect) explains the shader source this renderer consumes differently from the GL family.
Why Vulkan?
VULKAN is one of CNA's 14 renderer identities and one of the 4 modern-GPU-API renderers (with SDL_GPU, WEBGPU and METAL). It gives you explicit GPU control, lower driver overhead and more multi-threading headroom than the GL family, it is one of the renderers on which a custom ShaderEffect genuinely executes, and in this snapshot it reports a broad modern surface: base-instance drawing, float and half-float render targets, shadow sampling, image-based lighting, and compute and indirect-draw support (each conditional on the device; see the limits below for what game code can use).
CNA does record a maturity per renderer in code (the GraphicsBackendMaturity classification, also exposed through the C API): VULKAN is declared Production, alongside SDL_RENDERER, the native EasyGL profiles and DIRECTX9/11. That is CNA's own declaration, not a measurement, so choose Vulkan because you want its execution model, not because of a label. The one place a renderer choice is objectively settled is XNA-authentic output, and there the answer is DIRECTX9, not Vulkan — it is the only renderer recorded as matching the 39-scene oracle corpus at tolerance 0.
Required: Vulkan SDK
# Ubuntu/Debian
sudo apt install libvulkan-dev vulkan-validationlayers spirv-tools glslc
# Arch Linux
sudo pacman -S vulkan-headers vulkan-validation-layers shaderc
# Windows: Download LunarG Vulkan SDK from https://vulkan.lunarg.com
# and set VULKAN_SDK environment variable
Configure uses find_package(Vulkan REQUIRED), so the loader and headers must be discoverable. A Linux CNA build also needs the ../sharp-runtime sibling checkout — on its apple/m4-stabilization branch for this snapshot — whichever renderer you pick. FFmpeg is now optional (CNA_ENABLE_VIDEO=AUTO enables video only when the FFmpeg development packages are found), so it is no longer a hard requirement.
The renderer needs a window system that can create a Vulkan surface: the SDL3 platform provides one through SDL’s Windows, X11, Wayland and Android video drivers (subject to the runtime Vulkan library being present); the windowless TERMINAL and HEADLESS platforms do not.
Enabling the Vulkan renderer in CMake
cmake -S . -B build-vulkan \
-DCNA_GRAPHICS_RENDERER=VULKAN \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-vulkan
# The per-renderer option form is equivalent — use one or the other, never both:
# -DCNA_RENDERER_VULKAN=ON
Do not pass --target CNA: CNA is an interface library with no sources and is not a buildable target. Build CnaTests, a demo target, or just the whole build directory.
Validation layers for debugging
Always enable validation layers during development. They catch API misuse, resource leaks, and synchronization errors. In a build without NDEBUG (a Debug configuration) CNA's Vulkan renderer requests VK_LAYER_KHRONOS_validation itself and, if the layer is not installed, prints a note and carries on without it; in a Release build validation is off. You can also ask the Vulkan loader for the layer from outside the process:
# Enable validation layers at runtime via environment variable:
export VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation
./build-vulkan/MyGame
Differences from the EasyGL profiles (SPIR-V shaders, explicit sync)
The three GL profile identities (OPENGLES3, OPENGL33, WEBGL2) share the EasyGL implementation and consume GLSL source directly. Vulkan consumes SPIR-V. Pipeline state — blend, rasterizer, depth — is baked into pipeline objects rather than set dynamically, which makes state changes comparatively more expensive and draw submission comparatively cheaper. CNA's Effect API hides the difference for the stock effects: the same Apply() call works on every renderer that implements them.
Where it does not hide the difference is ShaderEffect, CNA's hand-written-shader extension. The source you hand it must be in the language the selected renderer expects, so a Vulkan build wants SPIR-V where an OPENGLES3 build wants GLSL. The renderer has exactly one custom-effect intake and reports its dialect as SPIR-V; a payload that does not start with the SPIR-V magic word is refused with a compile error rather than handed to the driver. See Tutorial 52.
The modern-feature surface in this snapshot
Alpha.1's Vulkan renderer stopped at the classic XNA surface. In this snapshot a “modern graphics” campaign landed for VULKAN (and for SDL_GPU and WEBGPU; DIRECTX11 reports compute and indirect draw too). The VULKAN renderer reports the new GraphicsCapability members — FloatRenderTargets, HalfFloatRenderTargets, HalfFloatTextureLinearFiltering, ComputeShaders and IndirectDraw — from the device's real features and limits rather than a fixed answer, and backs shadow sampling, image-based lighting and base-instance drawing for the stock effects and GraphicsDevice. Multisampling, multiple render targets, anisotropic filtering, wireframe (fillModeNonSolid), stencil, compute (a compute queue) and indirect draw (drawIndirectFirstInstance) are all device-conditional, so a query is the only honest way to know.
Ask the device: GraphicsDevice::SupportsCapability(GraphicsCapability::ComputeShaders) for the coarse answer, or the detailed profile — GetRendererCapabilityProfileEXT(), GetRendererFeatureSupportEXT(), GetRendererLimitEXT() and the readable GetRendererCapabilityReportEXT() — for 32 named features and 22 limits. Tutorial 101 and Tutorial 133 walk through both.
Limits worth knowing
- Compute shaders are not part of the XNA API surface, and CNA has no public compute API. XNA 4.0 has no compute concept.
GraphicsCapability::ComputeShadersand the capability profile report whether the device could run compute work (VULKANdoes when the device offers a compute queue), and the renderer implements compute, storage buffers and GPU timers internally, but no public class lets game code dispatch a compute shader, create a storage buffer or read a GPU timer, so treat those answers as information only. Indirect draw is in the same position:GraphicsDevicedeclaresDrawPrimitivesIndirectEXT, but no public class can create the argument buffer it needs. - Compiled effects are opt-in —
-DCNA_VULKAN_COMPILED_EFFECTS=ONenables XNA/FNA D3D9 Effect Framework bytecode throughEffectandEffectReader; the option defaults toOFF, so a default configure reportsCompiledEffectsfalse. HLSL.fxsource, DXBC and MGFX are not accepted. Renderer-nativeShaderEffectremains available separately. - Cube faces inside a multi-target set are supported by this renderer's code (a single render-target set can name faces of a cube), unlike
DIRECTX9,SDL_GPUandWEBGPU; check the renderer you ship on before you depend on it. - Apple targets are served by
METAL, not by Vulkan.METALis CNA’s native renderer for macOS and iOS, measured on a physical Mac mini M4 and built by a hostedmetal-macos-cijob. Vulkan through MoltenVK is out of scope on Apple and not tested. - Automatic CI touches Vulkan, but does not gate its pixel suite.
VULKANis built and run in the platform matrix (the SDL3 platform on private X11 and Wayland displays) and in the glTF conformance matrix, but no workflow gates its complete renderer pixel suite on every push, and the recorded Vulkan glTF pixel set (rendered on thelavapipesoftware driver) is a record of that renderer's own past output, not a comparison against a reference renderer.
Vulkan on Android
Android supports Vulkan from API level 24 (Android 7.0). Add uses-feature android:name="android.hardware.vulkan.level" android:version="0" to AndroidManifest.xml. CNA's Android support is code paths and NDK backends with no workflow and no preset — the only Android build project is the Devices demo — and the Vulkan renderer is not validated there. A plain Android configure defaults to SDL_RENDERER (the default for every host except Linux and Emscripten). See Tutorial 82.
Consuming CNA from your own CMake project
The C++ framework has no general install/export package, so C++ consumers add the source tree as a subdirectory and normally switch off CNA's own tests and examples. Only the optional, experimental C layer installs a CNA CMake package, and no CI job builds it, so whether it builds at this snapshot has not been verified.
# CMakeLists.txt: build your game against the Vulkan renderer
cmake_minimum_required(VERSION 3.20)
project(MyGame CXX)
set(CMAKE_CXX_STANDARD 23)
set(CNA_GRAPHICS_RENDERER "VULKAN" CACHE STRING "CNA renderer")
set(CNA_BUILD_TESTS OFF CACHE BOOL "")
set(CNA_BUILD_EXAMPLES OFF CACHE BOOL "")
add_subdirectory(../cna ${CMAKE_BINARY_DIR}/cna)
add_executable(MyGame main.cpp)
target_link_libraries(MyGame PRIVATE CNA)
# You do not define the renderer macro yourself — CNA's own configure emits
# CNA_RENDERER_VULKAN for the whole build when VULKAN is selected.
Compiling GLSL to SPIR-V with glslc is an ordinary offline step:
glslc shaders/basic.vert -o shaders/basic.vert.spv
glslc shaders/basic.frag -o shaders/basic.frag.spv
Looking for a native Windows GPU API instead of cross-platform Vulkan? CNA ships native DIRECTX9 and DIRECTX11 renderers. This snapshot includes manual Wine/DXVK paths, while its native-Windows workflows are manual-dispatch; see Tutorial 72 and Tutorial 103. The other modern-API renderers have tutorials of their own: SDL_GPU (Tutorial 131) and WebGPU (Tutorial 132). For GPU-free CI runs alongside Vulkan development, that same tutorial covers HEADLESS (logic only) and SOFTWARE (a real CPU rasteriser you read back rather than present).
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Vulkan presentation, frame pacing and back-buffer readback — How CNA's VULKAN renderer picks its swapchain format and present mode, synchronises two frames in flight, and reads the back buffer without racing the presentation engine.