mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Fix] (DirectVulkan): explicit-LOD blit sampling and present-path hardening - the default-framebuffer blit shader now samples with textureLod 0 (a blit reads exactly the selected level; Adreno 650's implicit-LOD path reads past a single-mip UBWC render target's allocation despite maxLod=0, page-faulting the GPU on MC 26.2's second startup frame once the neighbouring startup staging memory is returned - the invalidated context then failed the next Present submit with EDEADLK/DEVICE_LOST), TransitionToPresent appends the present barrier into the frame's open recording instead of silently dropping it whenever anything was recorded (frames without a default-FBO render pass presented images stuck in their acquired layout), VK_SUBOPTIMAL_KHR acquires are treated as the success they are (image acquired, semaphore signal armed - the early return skipped the fence reset and consumed-flag clear, and callers re-acquired on the same binary semaphore; rebuilds now defer to after the signal is consumed), and validation builds report through VK_EXT_debug_report when VK_EXT_debug_utils is absent instead of aborting instance creation
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<uint32_t>(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() {
|
||||
|
||||
@@ -399,6 +399,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkExtensionProperties> 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();
|
||||
|
||||
Reference in New Issue
Block a user