Direct-to-Display Rendering with VK_KHR_display

Up to this point, we have used GLFW to create a window that we can render into. This means that GLFW and a window manager or compositor have quietly done a lot of work on our behalf to make rendering possible: it decided where the window sits on screen, negotiated the pixel format with the display, handed us input events, and composited our output together with everything else running on the machine. VK_KHR_display is Vulkan’s answer to a different situation: what if there is no window manager at all, and your application is the only thing that will ever touch this screen? That’s the normal situation for a kiosk terminal, an embedded device, a piece of digital signage, an industrial control panel, or the very first frame a VR runtime draws before anything resembling a desktop exists. In that situation you don’t want a window; you want to talk to the physical display directly, and VK_KHR_display together with VK_KHR_display_swapchain is exactly the mechanism Vulkan provides for that.

The price of that directness is that everything a window manager used to do for you is now your problem. There’s no resizing, because there’s no window to resize. There’s no way to run two applications side by side, because the extension hands one application exclusive control of a display’s scanout hardware. And, as you’ll see in a moment, you don’t get to pick a physical device and then ask it for a surface the way you normally would - the display is the surface, in a much more literal sense than a window ever is.

Why the engine treats this as another Platform implementation

Earlier in this chapter we introduced the engine’s Platform abstraction: one interface, several implementations, one per way of getting pixels onto a screen and input off a keyboard. DesktopPlatform wraps GLFW. AndroidPlatform wraps the NDK’s windowing APIs. Direct-to-display fits the same shape - it still needs to hand the renderer a VkSurfaceKHR, still needs to report a width and height, still needs some notion of "should I keep running" - so it becomes a third implementation, DirectDisplayPlatform, rather than a special case bolted onto the renderer. That’s really the point of having the abstraction in the first place: adding a fundamentally different way of getting an image on screen doesn’t require touching the rendering code at all, only CreatePlatform().

Because this mode has to coexist with the ordinary windowed build rather than replace it, the engine chooses between them at startup rather than at compile time. Setting the environment variable SIMPLE_ENGINE_DIRECT_DISPLAY=1 before launching the binary is enough: CreatePlatform() checks for it and constructs a DirectDisplayPlatform instead of a DesktopPlatform. The same flag has to be checked one more time, earlier in the startup sequence than you might expect - when the Vulkan instance is created. A windowed build asks GLFW which instance extensions it needs (glfwGetRequiredInstanceExtensions()), which on Linux typically means VK_KHR_surface plus whichever of VK_KHR_xcb_surface or VK_KHR_wayland_surface matches the session you’re running under. None of that applies here, because there is no GLFW involved at all in direct-to-display mode. Instead, Renderer::createInstance() requests VK_KHR_surface and VK_KHR_display directly whenever the environment variable is set.

The ordering problem that makes this Platform implementation different

Every other Platform implementation you’ll write follows the same sequence: create a window, hand its native handle to the windowing API’s surface-creation function, get back a VkSurfaceKHR, and only afterward does the renderer enumerate physical devices and ask each one "can you present to this surface?" That ordering works because a windowed surface belongs to the windowing system, not to any particular GPU - any physical device that supports presentation in general is a candidate.

A VK_KHR_display surface breaks that assumption, because there is no windowing system standing between your application and the GPU. The surface is created from a specific physical device’s own enumeration of the displays wired into it, using vkGetPhysicalDeviceDisplayPropertiesKHR. That means the physical device has to be chosen as part of creating the surface, not afterward the way every other backend does it. DirectDisplayPlatform::CreateVulkanSurface() has to do a job the renderer normally does for it: walk every physical device the instance can see, and for each one, ask whether it has any displays attached at all. Most setups will only find displays on one device - typically whichever GPU has cables actually plugged into it - and it’s the first one that answers with a non-zero display count that direct-to-display mode commits to using.

Once a device with at least one display has been found, the code has three more decisions to make before it can call vkCreateDisplayPlaneSurfaceKHR, and each one exists for a real reason rather than being an arbitrary formality the API insists on.

The first is which mode to drive the display in. A single physical display can report several supported combinations of resolution and refresh rate through vkGetDisplayModePropertiesKHR, the same way a monitor’s OSD lets you pick between 1080p60 and 4K30. CreateVulkanSurface() walks that list and keeps whichever mode has the highest pixel count, which in practice means it lands on the display’s native resolution - the same default you’d expect a compositor to choose for you if one existed here.

The second is which plane to present through. Display hardware exposes one or more overlay/scanout planes, and not every plane is wired up to every display - a plane meant for a secondary output won’t help you if you’re targeting the primary one. vkGetPhysicalDeviceDisplayPlanePropertiesKHR lists the planes; vkGetDisplayPlaneSupportedDisplaysKHR tells you, for a given plane, which displays it’s actually capable of driving. The code checks that list for the display it already picked and skips any plane that doesn’t support it, rather than assuming plane zero will always work.

The third is alpha blending. vkGetDisplayPlaneCapabilitiesKHR reports which of opaque, global, and per-pixel alpha modes a given plane/mode combination actually supports, and they aren’t universally available - a plane might not support opaque blending at all, in which case asking for it would simply fail surface creation. The code asks for opaque first, since that’s what you almost always want for a full-screen application with nothing behind it, and only falls back to global or per-pixel alpha if opaque genuinely isn’t on offer.

With a display, a mode, a plane, and an alpha mode all pinned down, filling in VkDisplaySurfaceCreateInfoKHR and calling vkCreateDisplayPlaneSurfaceKHR is the easy part - everything before it exists to make sure that call has a combination of parameters the driver will actually accept.

One more difference worth calling out: DesktopPlatform::ProcessEvents() calls glfwPollEvents() every frame, because GLFW needs regular pumping to deliver window and input events. There is no equivalent here - no window system to poll - so DirectDisplayPlatform::ProcessEvents() does nothing but check a flag. That flag is set by a SIGINT/SIGTERM handler installed during Initialize(), which is the only way this mode has of being told to shut down cleanly, since there’s no close button and no window manager to send a close event in the first place.

Why this won’t present anything while you’re sitting at a normal desktop, and why that’s correct

The principle behind VK_KHR_display is the same on every platform that supports it: an application using it is asking to become the sole owner of a display’s scanout hardware, and an operating system will only ever grant that ownership to one process at a time. Whatever is already drawing your desktop - a Wayland or X11 compositor on Linux, the desktop compositor on Windows, the window server on macOS where it’s supported at all - already holds that ownership, simply by virtue of being the thing currently putting pixels on your monitor. There is no OS where a second, unrelated application can walk in and take over a display out from under the process already driving it; if that were possible, any application could hijack your screen away from whatever you were doing, which is exactly the kind of thing every desktop operating system’s display model is designed to prevent.

Linux makes the mechanics of this particularly easy to see, so it’s worth naming concretely even though the underlying principle isn’t Linux-specific: the kernel’s DRM/KMS subsystem hands out a single "master" lease per display, and whichever process holds it - your compositor, in the normal case - is the only one allowed to change display modes or present directly to the hardware. In testing this implementation on Linux, the effect was visible in two different ways depending on the driver: the proprietary NVIDIA driver didn’t even export the vkGetPhysicalDeviceDisplayPropertiesKHR entry point while a Wayland compositor held the session, while Mesa’s Intel driver exported the function normally but reported zero displays for the same underlying reason. Either way, listing physical devices, displays, modes, and planes doesn’t require owning the display and will generally still work; it’s the surface-creation call itself, and everything that would follow it - swapchain creation, presentation - that fails, because the compositor sitting in front of you isn’t going to hand over control of your monitor to a second application just because it asked.

Understanding how this works concretely for Windows is also important, so it’s worth being specific about how the same story plays out there, because the mechanics look different even though the outcome is the same. Windows has not allowed its desktop compositor, DWM, to be disabled since Windows 8 - unlike Windows 7, where you could turn Aero off, there is no user-facing way to get a standard Windows desktop, Pro, or Enterprise session down to "nothing is compositing this display" the way a Linux console lets you switch away from a compositor entirely. DWM is simply always the thing that owns your monitor on those editions. Because of that, Windows developers who want tight control over a display’s presentation almost always reach for VK_EXT_full_screen_exclusive rather than VK_KHR_display: it’s a negotiation with DWM, where your swapchain asks for exclusive fullscreen access and DWM steps aside for it, rather than an attempt to seize raw scanout control the way VK_KHR_display does. VK_KHR_display itself isn’t tied to any particular platform in the Vulkan spec, so a Windows driver is free to expose it, but on an ordinary Windows desktop session you’ll hit the same wall this code hit against the Wayland compositor above - DWM was there first, and it isn’t giving up ownership of your screen because a second application asked nicely. Where VK_KHR_display genuinely applies on Windows is closer to embedded and IoT deployments that don’t run the full desktop shell at all, which is a much smaller slice of the Windows world than the equivalent headless-Linux story.

Android is worth calling out for the opposite reason: it isn’t that a compositor happens to be running and getting in the way, it’s that Android’s display model doesn’t have a "no compositor" mode to switch to in the first place. Every pixel any Android app produces, fullscreen or not, is composited by SurfaceFlinger before it reaches the screen, and the only Vulkan surface type Android exposes is VK_KHR_android_surface, tied to an ANativeWindow that SurfaceFlinger owns end to end. There is no code path on stock Android for an application to ask for raw display-plane access the way this chapter’s CreateVulkanSurface() does. That’s exactly why DirectDisplayPlatform lives in the same #else branch as DesktopPlatform in platform.h, compiled only when PLATFORM_ANDROID is not defined - it isn’t an oversight or a missing feature to add later, it reflects that VK_KHR_display genuinely has nowhere to attach on that platform.

This isn’t a bug you should try to work around from inside the engine. It’s the mechanism working as designed. To actually watch this code present a frame, you need to run it somewhere nothing else is holding exclusive ownership of the display. On a Linux workstation that means a bare virtual terminal with no compositor running on it. The far more common real-world case is single-board hardware that was never going to run a desktop environment at all - a Raspberry Pi or an NVIDIA Jetson flashed with a minimal, headless Linux image and set to launch your application directly at boot is the textbook target for this mode, whether that’s Mesa’s V3D driver on a Pi 4 or 5 or NVIDIA’s own Tegra/L4T driver on Jetson. In that setup nothing ever starts a compositor in the first place, so there’s no session to fight with: the very first call this code makes to vkCreateDisplayPlaneSurfaceKHR is also the first thing that has ever asked the display for anything, and it gets what it asks for.

Where the code lives

The DirectDisplayPlatform class and the runtime switch that selects it live in platform.h and platform.cpp, alongside DesktopPlatform and AndroidPlatform. The instance-extension branch that swaps GLFW’s extension list for VK_KHR_surface plus VK_KHR_display lives in Renderer::createInstance(), in renderer_core.cpp.

Where this could go next

The engine’s swapchain setup after surface creation is currently the same generic path every Platform implementation shares; a more complete implementation would add a VK_KHR_display_swapchain-aware presentation-mode negotiation step, since a display swapchain has different presentation-mode tradeoffs than a windowed one. It would also be worth letting the environment-variable switch carry more than an on/off flag, so a multi-monitor kiosk rig could specify which display and which mode to target instead of always taking the highest-resolution mode on the first display found. And because the enumeration steps are safe to run even when a compositor owns the display, they’d make a reasonable standalone CI check, separate from the full engine binary, that at least confirms the VK_KHR_display code paths still compile and run without crashing on whatever hardware a build runs on - even on machines where actually presenting a frame will never be possible.

For the general shape of the Platform abstraction this builds on, see Architectural Patterns; for another example of the engine adapting to a very different platform surface, see Mobile Development.