How to do direct display scanout in Vulkan with VK_KHR_display and VK_EXT_direct_mode_display

Every window-system surface (xcb, wayland, …) routes your frames through a compositor — an extra copy (at best) or an extra refresh of latency (at worst). VK_KHR_display skips all of it: you enumerate the GPU’s own display controllers and planes and present directly to scanout hardware. This is how digital signage, VR compositors and camera walls get zero-compositor frames.

path.txt
normal:  app → swapchain → compositor → KMS → display
direct:  app → swapchain ──────────────► KMS → display   (VK_KHR_display)

Enumerating displays and modes

display.cpp
// Instance extensions: VK_KHR_surface, VK_KHR_display
// (+ VK_EXT_direct_mode_display, VK_EXT_acquire_drm_display on Linux)

uint32_t n = 0;
phys.getDisplayPropertiesKHR(&n, nullptr);
std::vector<vk::DisplayPropertiesKHR> dps(n);
phys.getDisplayPropertiesKHR(&n, dps.data());
// dp.display, dp.displayName, dp.physicalResolution,
// dp.physicalDimensions, dp.supportedTransforms,
// dp.planeReorderPossible, dp.persistentContent

vk::DisplayKHR display = dps[0].display;

phys.getDisplayModePropertiesKHR(display, &n, nullptr);
std::vector<vk::DisplayModePropertiesKHR> modes(n);
phys.getDisplayModePropertiesKHR(display, &n, modes.data());
// mode.parameters.visibleRegion + .refreshRate (mHz, e.g. 59940)

// Planes: the compositor-in-hardware. Each display uses a subset.
phys.getDisplayPlanePropertiesKHR(&n, nullptr);
std::vector<vk::DisplayPlanePropertiesKHR> planes(n);
phys.getDisplayPlanePropertiesKHR(&n, planes.data());
uint32_t planeIdx = findPlaneForDisplay(planes, display);

// What can this plane do with this mode?
vk::DisplayPlaneCapabilitiesKHR pcaps{};
phys.getDisplayPlaneCapabilitiesKHR(modes[0].displayMode,
                                    planeIdx, &pcaps);
// pcaps.supportedAlpha, .minSrcRect/maxSrcRect, .minDstRect/maxDstRect,
// pcaps.supportedDstPosition — can the plane place the image
// anywhere, or only at 0,0 covering the whole mode?

Creating the surface

surface.cpp
vk::DisplaySurfaceCreateInfoKHR sci{};
sci.displayMode     = modes[0].displayMode;
sci.planeIndex      = planeIdx;
sci.planeStackIndex = planes[planeIdx].currentStackIndex;
sci.transform       = vk::SurfaceTransformFlagBitsKHR::eIdentity;
sci.globalAlpha     = 1.0f;
sci.alphaMode       = vk::DisplayPlaneAlphaFlagBitsKHR::eOpaque;
sci.imageExtent     = modes[0].parameters.visibleRegion;

vk::UniqueSurfaceKHR surf = inst.createDisplayPlaneSurfaceKHRUnique(sci);
// From here: normal swapchain + present — straight to scanout.

Acquiring the display on Linux

The display is owned by DRM — if a compositor or getty holds it you must take control first:

acquire.cpp
// VK_EXT_acquire_drm_display — hand Vulkan the drm fd you opened/leased:
int drmFd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
// (or drmModeGetLease on a leased fd from logind/X)
phys.acquireDrmDisplayEXT(drmFd, display);

// VK_EXT_direct_mode_display — release it back on exit:
dev.releaseDisplayEXT(display);

Without VK_EXT_acquire_drm_display you can only use a display no one else owns — on a framebuffer-console-only box that often “just works”, on a desktop you’ll want the DRM lease path (drmModeCreateLease).

Why bother

wins.txt
compositor removed   → -1 copy or -1 frame of latency
predictable pacing   → no compositor scheduling jitter
fullscreen-only      → perfect for walls, kiosks, signage, VR runtimes
plane control        → alpha/positioning for overlays done in hardware

Practical rules

  • VK_KHR_display gives exclusive control — your app IS the compositor: you must handle mode-setting, power states and cleanup yourself (crash = black screen until re-acquired).
  • Check planeReorderPossible/supportedDstPosition: some planes are fullscreen-only — can’t do multi-window layouts on a single display plane.
  • alphaMode + planeStackIndex let you stack multiple surfaces (video under UI overlay) entirely in hardware — one plane each.
  • Not all drivers support it — NVIDIA + AMD/amdgpu DRM do; check vkEnumerateInstanceExtensionProperties for VK_KHR_display before designing for it. llvmpipe/lavapipe works for testing.
  • Combine with VK_EXT_present_timing for true end-to-end latency — with direct scanout the “presented” timestamp is real photons, not a compositor’s queue admission.

Check out similar posts by category: Vulkan C++