From b78c997e872b96f2f3765af0919a5e947f0cb133 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 19 Aug 2026 15:48:15 -0700 Subject: [PATCH 1/2] Add direct-to-display (VK_KHR_display) support to Simple Engine (#252) Implemented Direct-to-Display toggle it on with ENV var SIMPLE_ENGINE_DIRECT_DISPLAY=1. Adds a tutorial chapter covering it. --- antora/modules/ROOT/nav.adoc | 1 + attachments/simple_engine/platform.cpp | 150 ++++++++++++++++++ attachments/simple_engine/platform.h | 68 ++++++++ attachments/simple_engine/renderer_core.cpp | 14 +- .../Advanced_Topics/01_introduction.adoc | 1 + .../Advanced_Topics/Direct_To_Display.adoc | 51 ++++++ 6 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Direct_To_Display.adoc diff --git a/antora/modules/ROOT/nav.adoc b/antora/modules/ROOT/nav.adoc index 23b622c44..ee0b42479 100644 --- a/antora/modules/ROOT/nav.adoc +++ b/antora/modules/ROOT/nav.adoc @@ -156,6 +156,7 @@ *** xref:Building_a_Simple_Engine/Advanced_Topics/Ray_Query_Reflections_and_Transparency.adoc[Ray query: reflections & transparency] *** xref:Building_a_Simple_Engine/Advanced_Topics/Dynamic_Rendering_Local_Read.adoc[Dynamic rendering local read] *** xref:Building_a_Simple_Engine/Advanced_Topics/Robustness2.adoc[Robustness2] +*** xref:Building_a_Simple_Engine/Advanced_Topics/Direct_To_Display.adoc[Direct-to-Display Rendering (VK_KHR_display)] ** Appendix *** xref:Building_a_Simple_Engine/Appendix/appendix.adoc[Appendix] diff --git a/attachments/simple_engine/platform.cpp b/attachments/simple_engine/platform.cpp index a09852f16..1d71ef212 100644 --- a/attachments/simple_engine/platform.cpp +++ b/attachments/simple_engine/platform.cpp @@ -16,7 +16,10 @@ */ #include "platform.h" +#include +#include #include +#include #if defined(PLATFORM_ANDROID) # include @@ -547,4 +550,151 @@ void DesktopPlatform::CharCallback(GLFWwindow* window, unsigned int codepoint) { platform->charCallback(codepoint); } } + +// Direct-to-display platform implementation + +DirectDisplayPlatform* DirectDisplayPlatform::activeInstance = nullptr; + +bool DirectDisplayPlatform::IsRequested() { + const char* env = std::getenv("SIMPLE_ENGINE_DIRECT_DISPLAY"); + return env != nullptr && env[0] != '0' && env[0] != '\0'; +} + +void DirectDisplayPlatform::SignalHandler(int /*signal*/) { + if (activeInstance) { + activeInstance->shouldClose = true; + } +} + +bool DirectDisplayPlatform::Initialize(const std::string& /*appName*/, int requestedWidth, int requestedHeight) { + // The real resolution is only known once CreateVulkanSurface() has picked an + // actual display mode; until then, fall back to what was requested. + width = requestedWidth; + height = requestedHeight; + + activeInstance = this; + std::signal(SIGINT, SignalHandler); + std::signal(SIGTERM, SignalHandler); + + return true; +} + +void DirectDisplayPlatform::Cleanup() { + if (activeInstance == this) { + activeInstance = nullptr; + } +} + +bool DirectDisplayPlatform::ProcessEvents() { + // There is no window system to pump events for; SIGINT/SIGTERM (via + // SignalHandler) are the only way to ask this loop to stop. + return !shouldClose; +} + +bool DirectDisplayPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) { + // VK_KHR_display surfaces are created from a specific physical device's + // display outputs, so - unlike a windowed surface - we have to pick a + // physical device here rather than after surface creation. + uint32_t deviceCount = 0; + vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); + if (deviceCount == 0) { + LOGE("Direct-to-display: no Vulkan physical devices found"); + return false; + } + std::vector physicalDevices(deviceCount); + vkEnumeratePhysicalDevices(instance, &deviceCount, physicalDevices.data()); + + for (VkPhysicalDevice physicalDevice : physicalDevices) { + uint32_t displayCount = 0; + vkGetPhysicalDeviceDisplayPropertiesKHR(physicalDevice, &displayCount, nullptr); + if (displayCount == 0) { + continue; + } + std::vector displayProperties(displayCount); + vkGetPhysicalDeviceDisplayPropertiesKHR(physicalDevice, &displayCount, displayProperties.data()); + + const VkDisplayPropertiesKHR& chosenDisplay = displayProperties[0]; + LOGI("Direct-to-display: candidate display '%s' (%ux%u physical)", + chosenDisplay.displayName ? chosenDisplay.displayName : "", + chosenDisplay.physicalResolution.width, chosenDisplay.physicalResolution.height); + + uint32_t modeCount = 0; + vkGetDisplayModePropertiesKHR(physicalDevice, chosenDisplay.display, &modeCount, nullptr); + if (modeCount == 0) { + continue; + } + std::vector modeProperties(modeCount); + vkGetDisplayModePropertiesKHR(physicalDevice, chosenDisplay.display, &modeCount, modeProperties.data()); + + // Prefer the mode with the highest pixel count (typically the display's native resolution). + VkDisplayModePropertiesKHR bestMode = modeProperties[0]; + for (const VkDisplayModePropertiesKHR& mode : modeProperties) { + const uint64_t bestPixels = static_cast(bestMode.parameters.visibleRegion.width) * bestMode.parameters.visibleRegion.height; + const uint64_t pixels = static_cast(mode.parameters.visibleRegion.width) * mode.parameters.visibleRegion.height; + if (pixels > bestPixels) { + bestMode = mode; + } + } + + // Find a display plane that both supports this display and can be given a + // compatible surface, then create the surface on it. + uint32_t planeCount = 0; + vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physicalDevice, &planeCount, nullptr); + std::vector planeProperties(planeCount); + vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physicalDevice, &planeCount, planeProperties.data()); + + for (uint32_t planeIndex = 0; planeIndex < planeCount; ++planeIndex) { + uint32_t supportedDisplayCount = 0; + vkGetDisplayPlaneSupportedDisplaysKHR(physicalDevice, planeIndex, &supportedDisplayCount, nullptr); + if (supportedDisplayCount == 0) { + continue; + } + std::vector supportedDisplays(supportedDisplayCount); + vkGetDisplayPlaneSupportedDisplaysKHR(physicalDevice, planeIndex, &supportedDisplayCount, supportedDisplays.data()); + + bool planeSupportsDisplay = false; + for (VkDisplayKHR supportedDisplay : supportedDisplays) { + if (supportedDisplay == chosenDisplay.display) { + planeSupportsDisplay = true; + break; + } + } + if (!planeSupportsDisplay) { + continue; + } + + VkDisplayPlaneCapabilitiesKHR planeCapabilities{}; + vkGetDisplayPlaneCapabilitiesKHR(physicalDevice, bestMode.displayMode, planeIndex, &planeCapabilities); + + VkDisplayPlaneAlphaFlagBitsKHR alphaMode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR; + if (!(planeCapabilities.supportedAlpha & VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR)) { + if (planeCapabilities.supportedAlpha & VK_DISPLAY_PLANE_ALPHA_GLOBAL_BIT_KHR) { + alphaMode = VK_DISPLAY_PLANE_ALPHA_GLOBAL_BIT_KHR; + } else if (planeCapabilities.supportedAlpha & VK_DISPLAY_PLANE_ALPHA_PER_PIXEL_BIT_KHR) { + alphaMode = VK_DISPLAY_PLANE_ALPHA_PER_PIXEL_BIT_KHR; + } + } + + VkDisplaySurfaceCreateInfoKHR createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR; + createInfo.displayMode = bestMode.displayMode; + createInfo.planeIndex = planeIndex; + createInfo.planeStackIndex = planeProperties[planeIndex].currentStackIndex; + createInfo.transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; + createInfo.globalAlpha = 1.0f; + createInfo.alphaMode = alphaMode; + createInfo.imageExtent = bestMode.parameters.visibleRegion; + + if (vkCreateDisplayPlaneSurfaceKHR(instance, &createInfo, nullptr, surface) == VK_SUCCESS) { + width = static_cast(bestMode.parameters.visibleRegion.width); + height = static_cast(bestMode.parameters.visibleRegion.height); + LOGI("Direct-to-display: created VK_KHR_display surface at %dx%d on plane %u", width, height, planeIndex); + return true; + } + } + } + + LOGE("Direct-to-display: failed to find a display/plane combination that supports surface creation"); + return false; +} #endif \ No newline at end of file diff --git a/attachments/simple_engine/platform.h b/attachments/simple_engine/platform.h index e14b617cd..321c64fed 100644 --- a/attachments/simple_engine/platform.h +++ b/attachments/simple_engine/platform.h @@ -531,6 +531,71 @@ class DesktopPlatform final : public Platform { return window; } }; + +/** + * @brief Direct-to-display implementation of the Platform interface. + * + * Bypasses the window manager entirely and presents straight to a physical + * display output via VK_KHR_display / VK_KHR_display_swapchain, for kiosk, + * embedded, or headless-compositor scenarios. There is no window, so input + * and resize callbacks are accepted but never invoked, and the surface is + * created against whichever physical device exposes a usable display - see + * CreateVulkanSurface() for how that device and display/plane/mode are chosen. + * + * @see en/Building_a_Simple_Engine/Engine_Architecture chapter for background + * on how this fits into the wider Platform abstraction. + */ +class DirectDisplayPlatform final : public Platform { + private: + int width = 0; + int height = 0; + bool shouldClose = false; + std::function resizeCallback; + std::function mouseCallback; + std::function keyboardCallback; + std::function charCallback; + + static DirectDisplayPlatform* activeInstance; + static void SignalHandler(int signal); + + public: + DirectDisplayPlatform() = default; + + /** + * @brief Whether the SIMPLE_ENGINE_DIRECT_DISPLAY environment variable + * requests direct-to-display mode instead of a windowed surface. + */ + static bool IsRequested(); + + bool Initialize(const std::string& appName, int width, int height) override; + void Cleanup() override; + bool ProcessEvents() override; + bool HasWindowResized() override { + return false; + } + int GetWindowWidth() const override { + return width; + } + int GetWindowHeight() const override { + return height; + } + bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) override; + void SetResizeCallback(std::function callback) override { + resizeCallback = std::move(callback); + } + void SetMouseCallback(std::function callback) override { + mouseCallback = std::move(callback); + } + void SetKeyboardCallback(std::function callback) override { + keyboardCallback = std::move(callback); + } + void SetCharCallback(std::function callback) override { + charCallback = std::move(callback); + } + void SetWindowTitle(const std::string&) override { + // No window to title. + } +}; #endif /** @@ -543,6 +608,9 @@ std::unique_ptr CreatePlatform(Args&&... args) { #if defined(PLATFORM_ANDROID) return std::make_unique(std::forward(args)...); #else + if (DirectDisplayPlatform::IsRequested()) { + return std::make_unique(); + } return std::make_unique(); #endif } \ No newline at end of file diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index 6088067ad..1e7b55e92 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -650,9 +650,17 @@ bool Renderer::createInstance(const std::string& appName, bool enableValidationL // Add required extensions for GLFW #if defined(PLATFORM_DESKTOP) - uint32_t glfwExtensionCount = 0; - const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); - extensions.insert(extensions.end(), glfwExtensions, glfwExtensions + glfwExtensionCount); + if (DirectDisplayPlatform::IsRequested()) { + // No windowing system involved: just the base surface extension plus + // VK_KHR_display, which is what actually lets us enumerate and present + // to a physical display output directly. + extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME); + extensions.push_back(VK_KHR_DISPLAY_EXTENSION_NAME); + } else { + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + extensions.insert(extensions.end(), glfwExtensions, glfwExtensions + glfwExtensionCount); + } #elif defined(PLATFORM_ANDROID) extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME); extensions.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/01_introduction.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/01_introduction.adoc index 40e306bd0..d93dab7e9 100644 --- a/en/Building_a_Simple_Engine/Advanced_Topics/01_introduction.adoc +++ b/en/Building_a_Simple_Engine/Advanced_Topics/01_introduction.adoc @@ -23,5 +23,6 @@ Start anywhere that matches your interest: * xref:Building_a_Simple_Engine/Advanced_Topics/Robustness2.adoc[VK_EXT_robustness2] * xref:Building_a_Simple_Engine/Advanced_Topics/Dynamic_Rendering_Local_Read.adoc[Dynamic Rendering Local Read] * xref:Building_a_Simple_Engine/Advanced_Topics/Shader_Tile_Image.adoc[Shader Tile Image] +* xref:Building_a_Simple_Engine/Advanced_Topics/Direct_To_Display.adoc[Direct-to-Display Rendering (VK_KHR_display)] xref:Building_a_Simple_Engine/introduction.adoc[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Direct_To_Display.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Direct_To_Display.adoc new file mode 100644 index 000000000..91ddebac1 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Direct_To_Display.adoc @@ -0,0 +1,51 @@ += 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 xref:Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc[Architectural Patterns]; for another example of the engine adapting to a very different platform surface, see xref:Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc[Mobile Development]. From 9ace2f4091863bae0d6284d2a372bebfc69e19cc Mon Sep 17 00:00:00 2001 From: swinston Date: Thu, 20 Aug 2026 11:25:53 -0700 Subject: [PATCH 2/2] Address review comments on VK_KHR_display surface creation --- attachments/simple_engine/platform.cpp | 65 ++++++++++++------- .../Advanced_Topics/Direct_To_Display.adoc | 2 +- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/attachments/simple_engine/platform.cpp b/attachments/simple_engine/platform.cpp index 1d71ef212..e88eb461c 100644 --- a/attachments/simple_engine/platform.cpp +++ b/attachments/simple_engine/platform.cpp @@ -16,6 +16,8 @@ */ #include "platform.h" +#include +#include #include #include #include @@ -602,7 +604,10 @@ bool DirectDisplayPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKH return false; } std::vector physicalDevices(deviceCount); - vkEnumeratePhysicalDevices(instance, &deviceCount, physicalDevices.data()); + if (vkEnumeratePhysicalDevices(instance, &deviceCount, physicalDevices.data()) != VK_SUCCESS) { + LOGE("Direct-to-display: failed to enumerate Vulkan physical devices"); + return false; + } for (VkPhysicalDevice physicalDevice : physicalDevices) { uint32_t displayCount = 0; @@ -611,7 +616,10 @@ bool DirectDisplayPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKH continue; } std::vector displayProperties(displayCount); - vkGetPhysicalDeviceDisplayPropertiesKHR(physicalDevice, &displayCount, displayProperties.data()); + if (vkGetPhysicalDeviceDisplayPropertiesKHR(physicalDevice, &displayCount, displayProperties.data()) != VK_SUCCESS) { + LOGE("Direct-to-display: failed to get display properties"); + continue; + } const VkDisplayPropertiesKHR& chosenDisplay = displayProperties[0]; LOGI("Direct-to-display: candidate display '%s' (%ux%u physical)", @@ -624,15 +632,19 @@ bool DirectDisplayPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKH continue; } std::vector modeProperties(modeCount); - vkGetDisplayModePropertiesKHR(physicalDevice, chosenDisplay.display, &modeCount, modeProperties.data()); + if (vkGetDisplayModePropertiesKHR(physicalDevice, chosenDisplay.display, &modeCount, modeProperties.data()) != VK_SUCCESS) { + LOGE("Direct-to-display: failed to get display mode properties"); + continue; + } // Prefer the mode with the highest pixel count (typically the display's native resolution). VkDisplayModePropertiesKHR bestMode = modeProperties[0]; + uint64_t bestPixels = static_cast(bestMode.parameters.visibleRegion.width) * bestMode.parameters.visibleRegion.height; for (const VkDisplayModePropertiesKHR& mode : modeProperties) { - const uint64_t bestPixels = static_cast(bestMode.parameters.visibleRegion.width) * bestMode.parameters.visibleRegion.height; const uint64_t pixels = static_cast(mode.parameters.visibleRegion.width) * mode.parameters.visibleRegion.height; if (pixels > bestPixels) { bestMode = mode; + bestPixels = pixels; } } @@ -640,8 +652,14 @@ bool DirectDisplayPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKH // compatible surface, then create the surface on it. uint32_t planeCount = 0; vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physicalDevice, &planeCount, nullptr); + if (planeCount == 0) { + continue; + } std::vector planeProperties(planeCount); - vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physicalDevice, &planeCount, planeProperties.data()); + if (vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physicalDevice, &planeCount, planeProperties.data()) != VK_SUCCESS) { + LOGE("Direct-to-display: failed to get display plane properties"); + continue; + } for (uint32_t planeIndex = 0; planeIndex < planeCount; ++planeIndex) { uint32_t supportedDisplayCount = 0; @@ -650,30 +668,33 @@ bool DirectDisplayPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKH continue; } std::vector supportedDisplays(supportedDisplayCount); - vkGetDisplayPlaneSupportedDisplaysKHR(physicalDevice, planeIndex, &supportedDisplayCount, supportedDisplays.data()); - - bool planeSupportsDisplay = false; - for (VkDisplayKHR supportedDisplay : supportedDisplays) { - if (supportedDisplay == chosenDisplay.display) { - planeSupportsDisplay = true; - break; - } + if (vkGetDisplayPlaneSupportedDisplaysKHR(physicalDevice, planeIndex, &supportedDisplayCount, supportedDisplays.data()) != VK_SUCCESS) { + LOGE("Direct-to-display: failed to get displays supported by plane %u", planeIndex); + continue; } + + const bool planeSupportsDisplay = std::ranges::find(supportedDisplays, chosenDisplay.display) != supportedDisplays.end(); if (!planeSupportsDisplay) { continue; } VkDisplayPlaneCapabilitiesKHR planeCapabilities{}; - vkGetDisplayPlaneCapabilitiesKHR(physicalDevice, bestMode.displayMode, planeIndex, &planeCapabilities); - - VkDisplayPlaneAlphaFlagBitsKHR alphaMode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR; - if (!(planeCapabilities.supportedAlpha & VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR)) { - if (planeCapabilities.supportedAlpha & VK_DISPLAY_PLANE_ALPHA_GLOBAL_BIT_KHR) { - alphaMode = VK_DISPLAY_PLANE_ALPHA_GLOBAL_BIT_KHR; - } else if (planeCapabilities.supportedAlpha & VK_DISPLAY_PLANE_ALPHA_PER_PIXEL_BIT_KHR) { - alphaMode = VK_DISPLAY_PLANE_ALPHA_PER_PIXEL_BIT_KHR; - } + if (vkGetDisplayPlaneCapabilitiesKHR(physicalDevice, bestMode.displayMode, planeIndex, &planeCapabilities) != VK_SUCCESS) { + LOGE("Direct-to-display: failed to get display plane capabilities for plane %u", planeIndex); + continue; + } + + const std::array alphaModes{ + VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR, VK_DISPLAY_PLANE_ALPHA_GLOBAL_BIT_KHR, + VK_DISPLAY_PLANE_ALPHA_PER_PIXEL_BIT_KHR}; + const auto alphaModeIt = std::ranges::find_if(alphaModes, [supportedAlpha = planeCapabilities.supportedAlpha](VkDisplayPlaneAlphaFlagBitsKHR mode) { + return supportedAlpha & mode; + }); + if (alphaModeIt == alphaModes.end()) { + LOGE("Direct-to-display: plane %u supports no known alpha mode", planeIndex); + continue; } + const VkDisplayPlaneAlphaFlagBitsKHR alphaMode = *alphaModeIt; VkDisplaySurfaceCreateInfoKHR createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR; diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Direct_To_Display.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Direct_To_Display.adoc index 91ddebac1..5f8ddf376 100644 --- a/en/Building_a_Simple_Engine/Advanced_Topics/Direct_To_Display.adoc +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Direct_To_Display.adoc @@ -22,7 +22,7 @@ The first is which *mode* to drive the display in. A single physical display can 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. +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.