Blog

Fixing Vulkan HDR on virtual displays: a Vulkan layer that does what the GPU driver won’t

NVIDIA/AMD Vulkan drivers hide HDR on indirect (virtual) displays, so Vulkan games refuse HDR when streaming. We proved the driver still presents a forced HDR swapchain — and shipped a tiny Vulkan layer that injects the missing formats. The full engineering story.

Doom: The Dark Ages, Indiana Jones and the Great Circle, and other Vulkan games refuse HDR when you stream them through a virtual display — the kind every streaming host creates. The streaming community (Sunshine, Apollo, the Virtual-Display-Driver project) had written this off as unfixable, “something in the Windows kernel.” It isn’t. Here’s how we fixed it.

Two different HDR questions

Whether Windows thinks a display is HDR-capable and whether a Vulkan game thinks so are answered by two different parts of the stack. The Windows “Use HDR” toggle and the desktop compositor read HDR from the display’s metadata and the OS — and an indirect (IddCx) virtual display reports that correctly, which is why the toggle works, the desktop is 10-bit, and DirectX 12 games are usually fine. Vulkan games ask the GPU’s Vulkan driver instead, by enumerating swapchain surface formats and looking for VK_COLOR_SPACE_HDR10_ST2084_EXT. On a virtual display the NVIDIA/AMD Vulkan driver doesn’t return that color space — so the game sees “no HDR.”

The key experiment

Before writing any code we tested the obvious question: is the HDR pipeline actually broken on a virtual display, or only the advertisement? With a small Vulkan probe we force-created an HDR swapchain (HDR10_ST2084, 10-bit) on the virtual display — a format the driver never listed — set HDR metadata, and presented frames. It all succeeded. The driver presents HDR there perfectly; it simply refuses to put the format in the list a game reads. So the entire fix is to add the missing entry back.

A tiny idea, wrapped in a Vulkan layer

Vulkan has a clean extension point for exactly this: an implicit layer that sits between the game and the driver. Our layer intercepts the two surface-format queries (vkGetPhysicalDeviceSurfaceFormatsKHR and its 2KHR variant), calls down to the driver, and appends the HDR formats — HDR10/ST2084 and scRGB — to the list. The game then asks for an HDR swapchain, and (as the experiment showed) the driver honors it. No game patches, no per-title tweaks.

Two details make it production-safe:

  • Self-gating. The layer only injects when the target display *actually* has HDR enabled right now — it checks the Windows advanced-color state for the surface’s monitor. On an SDR session, or on a real monitor that already advertises HDR, it does nothing, so it can’t wash out an SDR game.
  • Always-on, but inert. It’s registered globally so it works no matter how a game is launched (env-based scoping silently fails for games started by an already-running Steam). Because of the self-gate it’s a no-op everywhere except an HDR virtual-display session, with an off-switch and an anti-cheat exclude list.

One gotcha for implementers

If you build your own: the Vulkan loader’s private create-info structs (VK_STRUCTURE_TYPE_LOADER_INSTANCE/DEVICE_CREATE_INFO) use the enum values 47 and 48, not the documented 1000211xxx range — get that wrong and vkCreateInstance fails inside your layer. Everything else is standard layer dispatch chaining.

The result

Doom: The Dark Ages now enables HDR over the virtual display, end to end, with no per-game work — and punktfunk ships the layer in its Windows host. If you just want it to work, see the short version; to diagnose your own setup, see DirectX vs Vulkan HDR on a virtual display.