From b6f2ab46b2061bbfebc583f26efdeaf4d7166941 Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 17 Jul 2026 05:57:24 +0300 Subject: [PATCH] Opt out of Windows fullscreen exclusive via VK_EXT_full_screen_exclusive Fixes black screen capture, stream lag on Discord/OBS, and occasional black screens when running the game in fullscreen or borderless fullscreen on Windows. Windows automatically promotes fullscreen-sized Vulkan swapchains to independent flip mode, bypassing the Desktop Window Manager (DWM) compositor that screen capture software reads from. This change opts out of exclusive fullscreen behavior when supported by the driver: * Checks for and enables VK_KHR_get_surface_capabilities2 during Vulkan instance creation on Windows. * Checks for and optionally enables VK_EXT_full_screen_exclusive during logical device creation without breaking Linux, macOS, or older drivers. * Chains VkSurfaceFullScreenExclusiveInfoEXT with VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT to VkSwapchainCreateInfoKHR during swapchain creation and recreation. --- .../java/net/vulkanmod/vulkan/Vulkan.java | 38 +++++++++++++++++-- .../vulkan/device/DeviceManager.java | 18 ++++++++- .../vulkan/framebuffer/SwapChain.java | 7 ++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/vulkanmod/vulkan/Vulkan.java b/src/main/java/net/vulkanmod/vulkan/Vulkan.java index cec7af77e..447c640ac 100644 --- a/src/main/java/net/vulkanmod/vulkan/Vulkan.java +++ b/src/main/java/net/vulkanmod/vulkan/Vulkan.java @@ -137,6 +137,7 @@ public static long getAllocator() { private static final StagingBuffers stagingBuffers = new StagingBuffers(); public static boolean use24BitsDepthFormat = true; + public static boolean surfaceCapabilities2Supported = false; private static int DEFAULT_DEPTH_FORMAT = 0; public static void initVulkan(long window) { @@ -203,6 +204,7 @@ private static void createInstance() { } try (MemoryStack stack = stackPush()) { + checkSurfaceCapabilities2Support(stack); // Use calloc to initialize the structs with 0s. Otherwise, the program can crash due to random values @@ -357,18 +359,48 @@ private static void createCommandPool() { } } + private static void checkSurfaceCapabilities2Support(MemoryStack stack) { + if (org.lwjgl.system.Platform.get() != org.lwjgl.system.Platform.WINDOWS) { + return; + } + + IntBuffer propertyCount = stack.ints(0); + vkEnumerateInstanceExtensionProperties((String) null, propertyCount, null); + + if (propertyCount.get(0) == 0) { + return; + } + + VkExtensionProperties.Buffer availableExtensions = VkExtensionProperties.malloc(propertyCount.get(0), stack); + vkEnumerateInstanceExtensionProperties((String) null, propertyCount, availableExtensions); + + Set availableExtensionNames = availableExtensions.stream() + .map(VkExtensionProperties::extensionNameString) + .collect(toSet()); + + if (availableExtensionNames.contains(KHRGetSurfaceCapabilities2.VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME)) { + surfaceCapabilities2Supported = true; + } + } + private static PointerBuffer getRequiredInstanceExtensions() { PointerBuffer glfwExtensions = glfwGetRequiredInstanceExtensions(); - if (ENABLE_VALIDATION_LAYERS) { + int extraCount = (ENABLE_VALIDATION_LAYERS ? 1 : 0) + (surfaceCapabilities2Supported ? 1 : 0); + if (extraCount > 0) { MemoryStack stack = stackGet(); - PointerBuffer extensions = stack.mallocPointer(glfwExtensions.capacity() + 1); + PointerBuffer extensions = stack.mallocPointer(glfwExtensions.capacity() + extraCount); extensions.put(glfwExtensions); - extensions.put(stack.UTF8(VK_EXT_DEBUG_UTILS_EXTENSION_NAME)); + if (ENABLE_VALIDATION_LAYERS) { + extensions.put(stack.UTF8(VK_EXT_DEBUG_UTILS_EXTENSION_NAME)); + } + if (surfaceCapabilities2Supported) { + extensions.put(stack.UTF8(KHRGetSurfaceCapabilities2.VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME)); + } // Rewind the buffer before returning it to reset its position back to 0 return extensions.rewind(); diff --git a/src/main/java/net/vulkanmod/vulkan/device/DeviceManager.java b/src/main/java/net/vulkanmod/vulkan/device/DeviceManager.java index 39a8ccef8..6c0cc95f6 100644 --- a/src/main/java/net/vulkanmod/vulkan/device/DeviceManager.java +++ b/src/main/java/net/vulkanmod/vulkan/device/DeviceManager.java @@ -12,6 +12,7 @@ import java.nio.IntBuffer; import java.util.ArrayList; import java.util.List; +import java.util.Set; import static java.util.stream.Collectors.toSet; import static net.vulkanmod.vulkan.queue.Queue.findQueueFamilies; @@ -37,6 +38,7 @@ public abstract class DeviceManager { public static VkPhysicalDeviceMemoryProperties memoryProperties; public static SurfaceProperties surfaceProperties; + public static boolean fullScreenExclusiveControl = false; static GraphicsQueue graphicsQueue; static PresentQueue presentQueue; @@ -209,7 +211,21 @@ public static void createLogicalDevice() { // deviceVulkan13Features.pNext(deviceVulkan11Features.address()); } - createInfo.ppEnabledExtensionNames(asPointerBuffer(Vulkan.REQUIRED_EXTENSION)); + fullScreenExclusiveControl = false; + List enabledExtensions = new ArrayList<>(Vulkan.REQUIRED_EXTENSION); + if (Vulkan.surfaceCapabilities2Supported) { + Set availableExtensions = getAvailableExtension(stack, physicalDevice).stream() + .map(VkExtensionProperties::extensionNameString) + .collect(toSet()); + + if (availableExtensions.contains(EXTFullScreenExclusive.VK_EXT_FULL_SCREEN_EXCLUSIVE_EXTENSION_NAME)) { + enabledExtensions.add(EXTFullScreenExclusive.VK_EXT_FULL_SCREEN_EXCLUSIVE_EXTENSION_NAME); + fullScreenExclusiveControl = true; + Initializer.LOGGER.info("VK_EXT_full_screen_exclusive enabled: opting out of full screen exclusive control"); + } + } + + createInfo.ppEnabledExtensionNames(asPointerBuffer(enabledExtensions)); // Configuration.DEBUG_FUNCTIONS.set(true); diff --git a/src/main/java/net/vulkanmod/vulkan/framebuffer/SwapChain.java b/src/main/java/net/vulkanmod/vulkan/framebuffer/SwapChain.java index ef7191c1c..e0dab5173 100644 --- a/src/main/java/net/vulkanmod/vulkan/framebuffer/SwapChain.java +++ b/src/main/java/net/vulkanmod/vulkan/framebuffer/SwapChain.java @@ -127,6 +127,13 @@ private void createSwapChain() { createInfo.presentMode(presentMode); createInfo.clipped(true); + if (DeviceManager.fullScreenExclusiveControl) { + VkSurfaceFullScreenExclusiveInfoEXT fullScreenExclusiveInfo = VkSurfaceFullScreenExclusiveInfoEXT.calloc(stack); + fullScreenExclusiveInfo.sType(EXTFullScreenExclusive.VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_INFO_EXT); + fullScreenExclusiveInfo.fullScreenExclusive(EXTFullScreenExclusive.VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT); + createInfo.pNext(fullScreenExclusiveInfo.address()); + } + if (this.swapChainId != VK_NULL_HANDLE) { this.swapChainImages.forEach(image -> vkDestroyImageView(device, image.getImageView(), null)); vkDestroySwapchainKHR(device, this.swapChainId, null);