diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp index c37a6f07..ab87c405 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp @@ -150,12 +150,30 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) { auto& frame = GetCurrent(); - if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout || - oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) { + if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) { return false; } - auto& commandBuffer = BeginCommandRecording(); + // The barrier belongs in the frame's own recording. Bailing out because + // something was already recorded (the previous behaviour) dropped the + // transition entirely for every frame that never ran a default-framebuffer + // render pass - the only other thing that carries the image to + // PRESENT_SRC_KHR, via that pass's finalLayout - so the swapchain image was + // handed to the WSI still in the layout it was acquired in. + // A closed-but-unsubmitted buffer can only come from a submit that already + // failed (SubmitPendingCommandBuffer leaves the flag set on error), and + // appending to it is illegal while reopening would reset the frame's own + // commands away. The device is gone on that path anyway - stay silent-safe + // rather than trade a lost device for a barrier into a closed buffer. + if (frame.hasCommandBufferRecorded) { + MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier"); + return false; + } + + // Reopening a recording here would vkResetCommandBuffer this frame's own + // commands away, so append to the open one and let the caller close it. + const Bool openedRecording = !frame.isCommandRecording; + VkCommandBuffer commandBuffer = openedRecording ? BeginCommandRecording() : frame.commandBuffer; VkImageMemoryBarrier presentBarrier{}; presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; @@ -174,7 +192,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier); - EndCommandRecording(); + if (openedRecording) { + EndCommandRecording(); + } return true; } @@ -227,12 +247,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence, &outImageIndex); - if (result != VK_SUCCESS) { + // VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and + // imageAvailableSemaphore *will* be signaled. Bailing out on it skipped both + // the consumed-flag reset (leaving a stale "already consumed", so the next + // submit never waited on the pending signal) and the fence reset (leaving + // the slot's fence signaled for the next submit to reuse). Only a genuine + // failure - VK_ERROR_OUT_OF_DATE_KHR and friends, where nothing is acquired + // and nothing is signaled - skips the bookkeeping. + if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { return result; } frame.imageAvailableSemaphoreConsumed = false; - return vkResetFences(device, 1, &frame.imageInFlightFence); + const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence); + // Hand the acquire's own code back so the caller can schedule a rebuild. + return resetResult == VK_SUCCESS ? result : resetResult; } Uint32 FrameContext::GetCurrentFrameIndex() const { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 63154d02..a367b1f7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -1016,7 +1016,18 @@ layout(location = 0) in vec2 vTexCoord; layout(location = 0) out vec4 outColor; void main() { - outColor = texture(uSource, vTexCoord); + // Explicit LOD, not texture(): a blit reads exactly the selected level, so + // derivative-based mip selection has no business here. It is also load-bearing: + // on Adreno 650 (driver 512.502) an implicit-LOD sample of this single-mip + // UBWC render target through the pre-rotation (ROTATE_90) mapping reads past + // the image's allocation - despite the sampler's maxLod=0 and a nominal 1:1 + // texel mapping whose LOD is 0, so the driver's implicit-LOD path itself is at + // fault - and page-faults the GPU once the neighbouring memory is returned to + // the kernel (frame 2 of Minecraft 26.2's resource reload; the kernel then + // invalidates the context and the next submit dies with EDEADLK -> + // VK_ERROR_DEVICE_LOST at Present). Verified on device: texture() faults on + // the second frame every run, textureLod survives with identical state. + outColor = textureLod(uSource, vTexCoord, 0.0); } )"; @@ -2563,11 +2574,22 @@ void main() { if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) { VkResult acquireResult = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); - if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) { + if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR) { + // Nothing was acquired and no semaphore signal was armed, so + // rebuilding and re-acquiring on the same semaphore is safe. MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult); RecreateSwapchain(); acquireResult = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); + } else if (acquireResult == VK_SUBOPTIMAL_KHR) { + // The image is usable, and its acquire signal is already armed on + // imageAvailableSemaphore. Re-acquiring here would arm a second + // signal on a binary semaphore whose first one nobody has waited on + // yet; keep the image and let Present rebuild after the frame that + // consumes the signal. + MGLOG_D("Initialize, vkAcquireNextImageKHR got VK_SUBOPTIMAL_KHR; deferring swapchain rebuild"); + m_swapchainResizeRequested = true; + acquireResult = VK_SUCCESS; } VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage"); } else { @@ -2693,6 +2715,7 @@ void main() { DestroyDebugMessenger(); m_debugMessenger = VK_NULL_HANDLE; } + DestroyDebugReportCallback(); if (m_instance != VK_NULL_HANDLE) { vkDestroyInstance(m_instance, nullptr); @@ -7542,7 +7565,11 @@ void main() { m_presentSuspended = false; const VkResult acquireResult = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); - if (acquireResult != VK_SUBOPTIMAL_KHR) { + if (acquireResult == VK_SUBOPTIMAL_KHR) { + // Usable image with its acquire signal already armed; rebuild only + // once a submit has consumed it (see step 4 at the end of Present). + m_swapchainResizeRequested = true; + } else { VK_VERIFY(acquireResult, "Present, deferred first WaitAndAcquireNextImage"); } } @@ -7570,16 +7597,24 @@ void main() { if (activeRenderPass) VkRenderPassManager::EndRenderPass(frame.commandBuffer); + // Transition while this frame's recording is still open. A frame that + // rendered only into FBOs has no default-framebuffer render pass, and that + // pass's finalLayout is the only other thing that carries the swapchain + // image to PRESENT_SRC_KHR - so closing the buffer first, which made + // TransitionToPresent refuse to record, handed the image to + // vkQueuePresentKHR in the layout it was acquired in (UNDEFINED on a fresh + // swapchain). The SetImageLayout below then made the tracker's + // disagreement with reality permanent for that image index. + const auto acquiredImageLayout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired); + m_frameContext.TransitionToPresent(m_swapchainObject.GetImage(m_imageIndexAcquired), acquiredImageLayout); + if (frame.isCommandRecording) { m_frameContext.EndCommandRecording(); frame.hasCommandBufferRecorded = true; m_lastPipelineValid = false; // command-buffer boundary: drop the pipeline memo } - const auto acquiredImageLayout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired); - const Bool needsLayoutTransitionForPresent = - m_frameContext.TransitionToPresent(m_swapchainObject.GetImage(m_imageIndexAcquired), acquiredImageLayout); - const Bool shouldSubmitCommandBuffer = frame.hasCommandBufferRecorded || needsLayoutTransitionForPresent; + const Bool shouldSubmitCommandBuffer = frame.hasCommandBufferRecorded; // 1) Submit current frame work. auto submitPacket = m_frameContext.GetSubmitInfo(shouldSubmitCommandBuffer, m_imageIndexAcquired); @@ -7623,7 +7658,17 @@ void main() { // 4) Wait/reset/acquire for next frame. result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); - if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { + if (result == VK_SUBOPTIMAL_KHR) { + // An image was acquired and its signal is armed on this slot's + // imageAvailableSemaphore. Rebuilding now would mean re-acquiring on + // that same binary semaphore, arming a second signal while the first is + // still unwaited. Keep the image: the rebuild runs at the top of the + // next Present's step 4 above, once this frame's submit has consumed it. + MGLOG_D("Present, vkAcquireNextImageKHR got VK_SUBOPTIMAL_KHR; deferring swapchain rebuild"); + m_swapchainResizeRequested = true; + result = VK_SUCCESS; + } else if (result == VK_ERROR_OUT_OF_DATE_KHR) { + // Nothing acquired, nothing signaled: safe to rebuild and re-acquire. MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result); if (!RecreateSwapchain()) { m_presentSuspended = true; @@ -7671,6 +7716,24 @@ void main() { m_validationLayersEnabled = m_config.EnableValidationLayers && validationLayerAvailable; + // The debug messenger is a VK_EXT_debug_utils object, but a driver can ship + // the validation layers while exposing only the older VK_EXT_debug_report + // (Adreno 650 / Vulkan 1.1.128 does exactly that). Requesting the extension + // unconditionally tripped the required-extension assert below, aborting every + // validation-enabled build in CreateInstance. Keep the layers - they still + // validate, and on Android they report to logcat on their own - and drop only + // the messenger. + const Bool debugUtilsAvailable = + m_validationLayersEnabled && IsExtensionSupported(m_extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + // Without a reporting channel the layers validate but say nothing, so fall + // back to VK_EXT_debug_report when debug_utils is missing. + const Bool debugReportAvailable = m_validationLayersEnabled && !debugUtilsAvailable && + IsExtensionSupported(m_extensions, VK_EXT_DEBUG_REPORT_EXTENSION_NAME); + if (m_validationLayersEnabled && !debugUtilsAvailable) { + MGLOG_I("%s not available; validation reports via %s instead.", VK_EXT_DEBUG_UTILS_EXTENSION_NAME, + debugReportAvailable ? VK_EXT_DEBUG_REPORT_EXTENSION_NAME : "(no channel)"); + } + // ---------------- App info ------------------- VkApplicationInfo appInfo = {}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; @@ -7721,8 +7784,10 @@ void main() { } #endif - if (m_validationLayersEnabled) { + if (debugUtilsAvailable) { exts.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } else if (debugReportAvailable) { + exts.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME); } MGLOG_I("Enabling %d Vulkan instance extensions:", exts.size()); @@ -7747,7 +7812,8 @@ void main() { MGLOG_I("Enabling validation layer..."); instanceInfo.enabledLayerCount = static_cast(std::size(s_validationLayerNames)); instanceInfo.ppEnabledLayerNames = s_validationLayerNames; - instanceInfo.pNext = &debugMessengerCreateInfo; + // Chaining the messenger create-info is only legal with the extension on. + instanceInfo.pNext = debugUtilsAvailable ? &debugMessengerCreateInfo : nullptr; } else { instanceInfo.enabledLayerCount = 0; instanceInfo.pNext = nullptr; @@ -7755,7 +7821,40 @@ void main() { VK_VERIFY(vkCreateInstance(&instanceInfo, nullptr, &m_instance), "vkCreateInstance failed"); - if (m_validationLayersEnabled) VK_VERIFY(SetupDebugMessenger()); + if (debugUtilsAvailable) { + VK_VERIFY(SetupDebugMessenger()); + } else if (debugReportAvailable) { + VK_VERIFY(SetupDebugReportCallback()); + } + } + + static VKAPI_ATTR VkBool32 VKAPI_CALL DebugReportCallback(VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT, + Uint64, size_t, Int32 messageCode, const char* pLayerPrefix, + const char* pMessage, void*) { + if ((flags & (VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT | + VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT)) != 0) { + MGLOG_F("[Vulkan %s %d] %s", pLayerPrefix ? pLayerPrefix : "?", messageCode, pMessage ? pMessage : ""); + } + return VK_FALSE; + } + + VkResult VulkanRenderer::SetupDebugReportCallback() { + auto vkCreateDebugReportCallbackEXT = + (PFN_vkCreateDebugReportCallbackEXT)vkGetInstanceProcAddr(m_instance, "vkCreateDebugReportCallbackEXT"); + if (!vkCreateDebugReportCallbackEXT) return VK_ERROR_EXTENSION_NOT_PRESENT; + VkDebugReportCallbackCreateInfoEXT createInfo{VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT}; + createInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT | + VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT; + createInfo.pfnCallback = &DebugReportCallback; + return vkCreateDebugReportCallbackEXT(m_instance, &createInfo, nullptr, &m_debugReportCallback); + } + + void VulkanRenderer::DestroyDebugReportCallback() { + if (m_debugReportCallback == VK_NULL_HANDLE) return; + auto func = (PFN_vkDestroyDebugReportCallbackEXT)vkGetInstanceProcAddr(m_instance, + "vkDestroyDebugReportCallbackEXT"); + if (func != nullptr) func(m_instance, m_debugReportCallback, nullptr); + m_debugReportCallback = VK_NULL_HANDLE; } VkResult VulkanRenderer::SetupDebugMessenger() { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 34f7be82..bf1172b7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -399,6 +399,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector m_extensions; VkInstance m_instance = VK_NULL_HANDLE; VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE; + // Fallback reporting channel for drivers that ship the validation layers but + // only expose the older VK_EXT_debug_report (Adreno 650 / Vulkan 1.1.128). + VkDebugReportCallbackEXT m_debugReportCallback = VK_NULL_HANDLE; PhysicalDevice m_physicalDevice; VkDevice m_device = VK_NULL_HANDLE; VmaAllocator m_allocator = nullptr; @@ -549,6 +552,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { void CreateInstance(); VkResult SetupDebugMessenger(); VkResult DestroyDebugMessenger(); + VkResult SetupDebugReportCallback(); + void DestroyDebugReportCallback(); VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo(); void CreateSurface(); void PickPhysicalDevice();