diff --git a/test_apps/cube_handle_vk_linux/CMakeLists.txt b/test_apps/cube_handle_vk_linux/CMakeLists.txt index e0ea1d9c89..d97c5d814b 100644 --- a/test_apps/cube_handle_vk_linux/CMakeLists.txt +++ b/test_apps/cube_handle_vk_linux/CMakeLists.txt @@ -88,7 +88,10 @@ FetchContent_Declare( # v2.27.0: window-space / HUD swapchains prefer the _SRGB sibling of # R8G8B8A8 (dxr::ChooseWindowSpaceSwapchainFormat; ADR-044 §7, # displayxr-common#72) — a UNORM HUD of encoded bytes was encoded twice. - GIT_TAG v2.27.0 + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. + # v2.29.0: dxr::CursorDepth (common/cursor_depth.h, ADR-046 Amendment 2). + GIT_TAG v2.29.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) diff --git a/test_apps/cube_handle_vk_linux/main.cpp b/test_apps/cube_handle_vk_linux/main.cpp index 75be878b2c..60bf83ea7b 100644 --- a/test_apps/cube_handle_vk_linux/main.cpp +++ b/test_apps/cube_handle_vk_linux/main.cpp @@ -47,6 +47,7 @@ #include #include +#include #define XR_USE_GRAPHICS_API_VULKAN #include @@ -1013,9 +1014,10 @@ struct VkRenderer { uint32_t fbWidth = 0; uint32_t fbHeight = 0; - VkCommandPool commandPool = VK_NULL_HANDLE; - VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - VkFence frameFence = VK_NULL_HANDLE; + VkCommandPool commandPool = VK_NULL_HANDLE; // init-time texture uploads only + // INV-4.10 / #1917: N frames in flight, one submit per frame, never a wait + // on the frame just submitted (replaces the old single cmd + frameFence). + dxr::VkFrameRing ring; float cubeRotation = 0.0f; @@ -1115,12 +1117,32 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; + // The single depth image is shared by every frame; with N frames in + // flight (INV-4.10) the next frame's depth clear must wait for the + // previous frame's depth writes (WAW), and the colour layout + // transition must wait for colour output. + VkSubpassDependency dep = {}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpInfo = {}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpInfo.attachmentCount = 2; rpInfo.pAttachments = attachments; rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderer.renderPass) != VK_SUCCESS) { LOG_ERROR("Failed to create render pass"); @@ -1476,7 +1498,7 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic memcpy(data, gridVerts.data(), (size_t)gridBufSize); vkUnmapMemory(device, renderer.gridVertexMemory); - // Command pool + command buffer + // Command pool (init-time texture uploads) { VkCommandPoolCreateInfo poolInfo = {}; poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; @@ -1487,27 +1509,13 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic LOG_ERROR("Failed to create command pool"); return false; } - - VkCommandBufferAllocateInfo allocInfo = {}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.commandPool = renderer.commandPool; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandBufferCount = 1; - - if (vkAllocateCommandBuffers(device, &allocInfo, &renderer.commandBuffer) != VK_SUCCESS) { - LOG_ERROR("Failed to allocate command buffer"); - return false; - } } - // Frame fence + // Frame ring (per-frame command buffers + fences) { - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - - if (vkCreateFence(device, &fenceInfo, nullptr, &renderer.frameFence) != VK_SUCCESS) { - LOG_ERROR("Failed to create fence"); + dxr::VkFrameRing::Config ringCfg; // default 3 frames in flight; push constants, no UBO + if (!renderer.ring.init(device, physDevice, queue, queueFamilyIndex, ringCfg)) { + LOG_ERROR("Failed to create frame ring"); return false; } } @@ -1701,19 +1709,12 @@ static void RenderScene( VkRenderer& renderer, uint32_t imageIndex, const EyeRenderParams* eyes, int eyeCount) { - VkDevice device = renderer.device; - - // Wait for previous frame - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &renderer.frameFence); - - // Begin command buffer - VkCommandBuffer cmd = renderer.commandBuffer; - vkResetCommandBuffer(cmd, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &beginInfo); + // Waits only the slot used N frames ago (INV-4.10), resets + begins its cmd + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + LOG_ERROR("Frame ring beginFrame failed"); + return; + } // Begin render pass with full SBS framebuffer VkClearValue clearValues[2] = {}; @@ -1819,23 +1820,20 @@ static void RenderScene( } vkCmdEndRenderPass(cmd); - vkEndCommandBuffer(cmd); - - // Submit - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cmd; - vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - - // Wait for completion before returning (runtime needs the image ready) - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); + // ONE submit with the slot fence, no wait. The caller releases the + // swapchain image after this; the runtime composes on the same queue, so + // this work is ordered ahead of it. + VkResult submitResult = renderer.ring.endFrame(); + if (submitResult != VK_SUCCESS) { + LOG_ERROR("Frame submit failed: %d", (int)submitResult); + } } static void CleanupVkRenderer(VkRenderer& renderer) { if (!renderer.device) return; + renderer.ring.waitAll(); vkDeviceWaitIdle(renderer.device); for (auto fb : renderer.framebuffers) @@ -1859,10 +1857,7 @@ static void CleanupVkRenderer(VkRenderer& renderer) { renderer.depthMemory = VK_NULL_HANDLE; } - if (renderer.frameFence) { - vkDestroyFence(renderer.device, renderer.frameFence, nullptr); - renderer.frameFence = VK_NULL_HANDLE; - } + renderer.ring.destroy(); if (renderer.commandPool) { vkDestroyCommandPool(renderer.device, renderer.commandPool, nullptr); renderer.commandPool = VK_NULL_HANDLE; @@ -3083,6 +3078,7 @@ int main(int argc, char** argv) { VkRenderer vkRenderer = {}; if (!InitializeVkRenderer(vkRenderer, vkDevice, physDevice, graphicsQueue, queueFamilyIndex, colorFormat)) { LOG_ERROR("Vulkan renderer initialization failed"); + vkRenderer.ring.destroy(); // before vkDestroyDevice (dtor would run after) CleanupOpenXR(xr); vkDestroyDevice(vkDevice, nullptr); vkDestroyInstance(vkInstance, nullptr); diff --git a/test_apps/cube_zones_vk_linux/CMakeLists.txt b/test_apps/cube_zones_vk_linux/CMakeLists.txt index c383052dfe..14e6b99225 100644 --- a/test_apps/cube_zones_vk_linux/CMakeLists.txt +++ b/test_apps/cube_zones_vk_linux/CMakeLists.txt @@ -89,7 +89,10 @@ FetchContent_Declare( # v2.27.0: window-space / HUD swapchains prefer the _SRGB sibling of # R8G8B8A8 (dxr::ChooseWindowSpaceSwapchainFormat; ADR-044 §7, # displayxr-common#72) — a UNORM HUD of encoded bytes was encoded twice. - GIT_TAG v2.27.0 + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. + # v2.29.0: dxr::CursorDepth (common/cursor_depth.h, ADR-046 Amendment 2). + GIT_TAG v2.29.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) diff --git a/test_apps/cube_zones_vk_linux/main.cpp b/test_apps/cube_zones_vk_linux/main.cpp index 14d4a3be98..4403049183 100644 --- a/test_apps/cube_zones_vk_linux/main.cpp +++ b/test_apps/cube_zones_vk_linux/main.cpp @@ -45,6 +45,7 @@ #include "dxr_weave_snap.h" #include +#include #define XR_USE_GRAPHICS_API_VULKAN #include @@ -1074,9 +1075,10 @@ struct VkRenderer { uint32_t fbWidth = 0; uint32_t fbHeight = 0; - VkCommandPool commandPool = VK_NULL_HANDLE; - VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - VkFence frameFence = VK_NULL_HANDLE; + VkCommandPool commandPool = VK_NULL_HANDLE; // init-time texture uploads only + // INV-4.10 / #1917: N frames in flight, one submit per frame, never a wait + // on the frame just submitted (replaces the old single cmd + frameFence). + dxr::VkFrameRing ring; float cubeRotation = 0.0f; @@ -1290,12 +1292,32 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; + // Each framebuffer set's single depth image (base + per-zone) is shared + // by every frame; with N frames in flight (INV-4.10) the next frame's + // depth clear must wait for the previous frame's depth writes (WAW), + // and the colour layout transition must wait for colour output. + VkSubpassDependency dep = {}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpInfo = {}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpInfo.attachmentCount = 2; rpInfo.pAttachments = attachments; rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderer.renderPass) != VK_SUCCESS) { LOG_ERROR("Failed to create render pass"); @@ -1651,7 +1673,7 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic memcpy(data, gridVerts.data(), (size_t)gridBufSize); vkUnmapMemory(device, renderer.gridVertexMemory); - // Command pool + command buffer + // Command pool (init-time texture uploads) { VkCommandPoolCreateInfo poolInfo = {}; poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; @@ -1662,27 +1684,13 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic LOG_ERROR("Failed to create command pool"); return false; } - - VkCommandBufferAllocateInfo allocInfo = {}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.commandPool = renderer.commandPool; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandBufferCount = 1; - - if (vkAllocateCommandBuffers(device, &allocInfo, &renderer.commandBuffer) != VK_SUCCESS) { - LOG_ERROR("Failed to allocate command buffer"); - return false; - } } - // Frame fence + // Frame ring (per-frame command buffers + fences) { - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - - if (vkCreateFence(device, &fenceInfo, nullptr, &renderer.frameFence) != VK_SUCCESS) { - LOG_ERROR("Failed to create fence"); + dxr::VkFrameRing::Config ringCfg; // default 3 frames in flight; push constants, no UBO + if (!renderer.ring.init(device, physDevice, queue, queueFamilyIndex, ringCfg)) { + LOG_ERROR("Failed to create frame ring"); return false; } } @@ -1876,19 +1884,12 @@ static void RenderScene( VkRenderer& renderer, uint32_t imageIndex, const EyeRenderParams* eyes, int eyeCount) { - VkDevice device = renderer.device; - - // Wait for previous frame - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &renderer.frameFence); - - // Begin command buffer - VkCommandBuffer cmd = renderer.commandBuffer; - vkResetCommandBuffer(cmd, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &beginInfo); + // Waits only the slot used N frames ago (INV-4.10), resets + begins its cmd + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + LOG_ERROR("Frame ring beginFrame failed"); + return; + } // Begin render pass with full SBS framebuffer VkClearValue clearValues[2] = {}; @@ -1994,18 +1995,14 @@ static void RenderScene( } vkCmdEndRenderPass(cmd); - vkEndCommandBuffer(cmd); - // Submit - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cmd; - - vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - - // Wait for completion before returning (runtime needs the image ready) - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); + // ONE submit with the slot fence, no wait. The caller releases the + // swapchain image after this; the runtime composes on the same queue, so + // this work is ordered ahead of it. + VkResult submitResult = renderer.ring.endFrame(); + if (submitResult != VK_SUCCESS) { + LOG_ERROR("Frame submit failed: %d", (int)submitResult); + } } // XR_DXR_display_zones: generalized scene render — the same cube + grid as @@ -2014,22 +2011,13 @@ static void RenderScene( // swapchain with its own spin phase and premultiplied background). Identical // draw path to the base RenderScene (single CLEAR pass, negative-viewport // Y-flip, cube then grid); only the framebuffer/clear/rotation are parameters. +// Records into `cmd` (the frame ring's open command buffer) — every zone of a +// frame goes into the ONE per-frame submit (INV-4.10); no submit here. static void RenderSceneToFramebuffer( - VkRenderer& renderer, SwapchainFramebuffers& fb, uint32_t imageIndex, + VkRenderer& renderer, VkCommandBuffer cmd, SwapchainFramebuffers& fb, uint32_t imageIndex, const EyeRenderParams* eyes, int eyeCount, const float clearColor[4], float rotationOffset) { - VkDevice device = renderer.device; - - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &renderer.frameFence); - - VkCommandBuffer cmd = renderer.commandBuffer; - vkResetCommandBuffer(cmd, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &beginInfo); VkClearValue clearValues[2] = {}; clearValues[0].color = {{clearColor[0], clearColor[1], clearColor[2], clearColor[3]}}; @@ -2124,22 +2112,12 @@ static void RenderSceneToFramebuffer( } vkCmdEndRenderPass(cmd); - vkEndCommandBuffer(cmd); - - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cmd; - vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - - // Wait for completion before returning (runtime needs the image ready - // before we release it to the compositor) — matches the base RenderScene. - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); } static void CleanupVkRenderer(VkRenderer& renderer) { if (!renderer.device) return; + renderer.ring.waitAll(); vkDeviceWaitIdle(renderer.device); for (auto fb : renderer.framebuffers) @@ -2163,10 +2141,7 @@ static void CleanupVkRenderer(VkRenderer& renderer) { renderer.depthMemory = VK_NULL_HANDLE; } - if (renderer.frameFence) { - vkDestroyFence(renderer.device, renderer.frameFence, nullptr); - renderer.frameFence = VK_NULL_HANDLE; - } + renderer.ring.destroy(); if (renderer.commandPool) { vkDestroyCommandPool(renderer.device, renderer.commandPool, nullptr); renderer.commandPool = VK_NULL_HANDLE; @@ -2985,56 +2960,37 @@ static void GetWindowSize(uint32_t& w, uint32_t& h) { // One-shot graphics-queue submit clearing a whole VK image to `color` and // leaving it in COLOR_ATTACHMENT_OPTIMAL (the steady-state layout the // compositor's zone pass transitions from). oldLayout UNDEFINED is always valid -// here — the clear overwrites every pixel. -static void ClearImageToColor(VkDevice device, VkCommandPool pool, VkQueue queue, - VkImage image, const float color[4]) { - VkCommandBufferAllocateInfo ai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; - ai.commandPool = pool; - ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - ai.commandBufferCount = 1; - VkCommandBuffer cmd = VK_NULL_HANDLE; - if (vkAllocateCommandBuffers(device, &ai, &cmd) != VK_SUCCESS) { - return; - } - - VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); - - VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = image; - barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - barrier.srcAccessMask = 0; - barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &barrier); - - VkClearColorValue cv; - for (int i = 0; i < 4; i++) { - cv.float32[i] = color[i]; - } - VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - vkCmdClearColorImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &cv, 1, &range); - - barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - 0, 0, nullptr, 0, nullptr, 1, &barrier); - - vkEndCommandBuffer(cmd); - VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - vkQueueSubmit(queue, 1, &si, VK_NULL_HANDLE); - vkQueueWaitIdle(queue); - vkFreeCommandBuffers(device, pool, 1, &cmd); +// here — the clear overwrites every pixel. Runs once at zones activation via the +// frame ring's sanctioned blocking path (submitImmediate, INV-4.10). +static void ClearImageToColor(dxr::VkFrameRing& ring, VkImage image, const float color[4]) { + ring.submitImmediate([&](VkCommandBuffer cmd) { + VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier); + + VkClearColorValue cv; + for (int i = 0; i < 4; i++) { + cv.float32[i] = color[i]; + } + VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdClearColorImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &cv, 1, &range); + + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier); + }); } // Create one zone's swapchain, sized per xrGetDisplayZoneRecommendedViewSizeDXR, @@ -3094,7 +3050,7 @@ static bool CreateZoneResources(AppXrSession& xr, VkRenderer& renderer, uint32_t } // Create the always-on Local2D strip and fill it once with solid amber. -static bool CreateAndFillStrip(AppXrSession& xr, VkDevice device, VkCommandPool pool, VkQueue queue) { +static bool CreateAndFillStrip(AppXrSession& xr, VkRenderer& renderer) { const uint32_t w = (uint32_t)g_stripRect.extent.width; const uint32_t h = (uint32_t)g_stripRect.extent.height; if (w == 0 || h == 0) { @@ -3143,7 +3099,7 @@ static bool CreateAndFillStrip(AppXrSession& xr, VkDevice device, VkCommandPool // what encodes; nothing is drawn into it). Alpha is never converted. const float amber[4] = {1.0f, 0.667f, 0.0f, 1.0f}; const VkClearColorValue amberClear = dxr::VkDisplayReferredClearColor((VkFormat)sci.format, amber); - ClearImageToColor(device, pool, queue, imgs[idx].image, amberClear.float32); + ClearImageToColor(renderer.ring, imgs[idx].image, amberClear.float32); XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; xrReleaseSwapchainImage(g_strip.swapchain, &ri); @@ -3204,8 +3160,7 @@ static void HandleZoneKeys(AppXrSession& xr) { } // One-time zones activation: capabilities check + per-zone swapchains + strip. -static void TryActivateZones(AppXrSession& xr, VkRenderer& renderer, - VkDevice device, VkCommandPool pool, VkQueue queue) { +static void TryActivateZones(AppXrSession& xr, VkRenderer& renderer) { uint32_t winW = 0, winH = 0; GetWindowSize(winW, winH); const int32_t W = (int32_t)winW; @@ -3272,7 +3227,7 @@ static void TryActivateZones(AppXrSession& xr, VkRenderer& renderer, return; } } - if (!CreateAndFillStrip(xr, device, pool, queue)) { + if (!CreateAndFillStrip(xr, renderer)) { g_hasDisplayZonesExt = false; return; } @@ -3298,6 +3253,11 @@ static void RenderZonesFrame(AppXrSession& xr, VkRenderer& renderer, const XrFra XrDisplayRigDXR rigStructs[kNumZones]; std::vector projViews[kNumZones]; uint32_t submitViewCounts[kNumZones] = {}; + // INV-4.10: acquire every zone swapchain first, record all zones into ONE + // frame-ring command buffer, submit once, then release all. + std::vector zoneEyes[kNumZones]; + uint32_t zoneImageIndex[kNumZones] = {}; + bool zoneAcquired[kNumZones] = {}; for (uint32_t zi = 0; zi < kNumZones; zi++) { DisplayZone& z = g_zonesArr[zi]; @@ -3348,7 +3308,8 @@ static void RenderZonesFrame(AppXrSession& xr, VkRenderer& renderer, const XrFra // far = ez + 1000*vH (identity rig here, so ez = pose z). Same recipe as // the base app's rig-consume path: GL-convention fov matrix remapped to // Vulkan's [0,1] clip via convert_projection_gl_to_zero_to_one. - std::vector eyes(n); + std::vector& eyes = zoneEyes[zi]; + eyes.resize(n); for (uint32_t vi = 0; vi < n; vi++) { const XrView& v = zoneViews[vi]; const float ez = RigLocalEyeZ(rigStructs[zi].pose, v.pose.position); @@ -3384,12 +3345,38 @@ static void RenderZonesFrame(AppXrSession& xr, VkRenderer& renderer, const XrFra XrSwapchainImageWaitInfo wi = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; wi.timeout = XR_INFINITE_DURATION; xrWaitSwapchainImage(z.swapchain, &wi); - - RenderSceneToFramebuffer(renderer, g_zoneFbs[zi], imageIndex, eyes.data(), (int)n, - z.clearColor, z.spinPhase); - - XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; - xrReleaseSwapchainImage(z.swapchain, &ri); + zoneImageIndex[zi] = imageIndex; + zoneAcquired[zi] = true; + } + + // One command buffer for every zone, ONE submit, no wait (INV-4.10). Each + // zone renders into its own swapchain + own depth image, so no barrier is + // needed between zones. + bool anyAcquired = false; + for (uint32_t zi = 0; zi < kNumZones; zi++) anyAcquired = anyAcquired || zoneAcquired[zi]; + if (anyAcquired) { + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd != VK_NULL_HANDLE) { + for (uint32_t zi = 0; zi < kNumZones; zi++) { + if (!zoneAcquired[zi]) continue; + const DisplayZone& z = g_zonesArr[zi]; + RenderSceneToFramebuffer(renderer, cmd, g_zoneFbs[zi], zoneImageIndex[zi], + zoneEyes[zi].data(), (int)zoneEyes[zi].size(), + z.clearColor, z.spinPhase); + } + VkResult submitResult = renderer.ring.endFrame(); + if (submitResult != VK_SUCCESS) { + LOG_ERROR("[zones] frame submit failed: %d", (int)submitResult); + } + } else { + LOG_ERROR("[zones] frame ring beginFrame failed"); + } + // Release AFTER endFrame: the runtime composes on the same queue. + for (uint32_t zi = 0; zi < kNumZones; zi++) { + if (!zoneAcquired[zi]) continue; + XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; + xrReleaseSwapchainImage(g_zonesArr[zi].swapchain, &ri); + } } // Layer list: [projA (zone A chained), projB (zone B chained), strip]. @@ -3741,6 +3728,7 @@ int main(int argc, char** argv) { VkRenderer vkRenderer = {}; if (!InitializeVkRenderer(vkRenderer, vkDevice, physDevice, graphicsQueue, queueFamilyIndex, colorFormat)) { LOG_ERROR("Vulkan renderer initialization failed"); + vkRenderer.ring.destroy(); // before vkDestroyDevice (dtor would run after) CleanupOpenXR(xr); vkDestroyDevice(vkDevice, nullptr); vkDestroyInstance(vkInstance, nullptr); @@ -3816,7 +3804,7 @@ int main(int argc, char** argv) { g_zonesFrameCounter++; if (g_hasDisplayZonesExt && !g_zonesActive && !g_zonesAttempted && g_zonesFrameCounter >= kZonesActivationFrame) { - TryActivateZones(xr, vkRenderer, vkDevice, vkRenderer.commandPool, graphicsQueue); + TryActivateZones(xr, vkRenderer); } if (g_zonesActive && g_hasDisplayZonesExt) { HandleZoneKeys(xr); @@ -4013,6 +4001,7 @@ int main(int argc, char** argv) { } // Zone framebuffer sets hold views onto the zone swapchain images — // destroy them BEFORE the swapchains release those images. + vkRenderer.ring.waitAll(); vkDeviceWaitIdle(vkDevice); for (uint32_t zi = 0; zi < kNumZones; zi++) { DestroyFramebuffers(vkDevice, g_zoneFbs[zi]); diff --git a/test_apps/handle/cube_handle_metal_macos/CMakeLists.txt b/test_apps/handle/cube_handle_metal_macos/CMakeLists.txt index 056fb9599a..07ac3255eb 100644 --- a/test_apps/handle/cube_handle_metal_macos/CMakeLists.txt +++ b/test_apps/handle/cube_handle_metal_macos/CMakeLists.txt @@ -64,6 +64,8 @@ FetchContent_Declare( # R8G8B8A8 (ADR-044 §7, displayxr-common#72) — the Metal compositor is # format-honest, so a UNORM HUD of CPU-rasterized (encoded) bytes would be # encoded twice. Same pin as the VK macOS cube apps. + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. # v2.29.0: dxr::CursorDepth (common/cursor_depth.h), the XR_DXR_cursor_depth # app helper (ADR-046 Amendment 2) - same pin as the aggregate test_apps build. GIT_TAG v2.29.0 diff --git a/test_apps/handle/cube_handle_vk_android/src/main/cpp/CMakeLists.txt b/test_apps/handle/cube_handle_vk_android/src/main/cpp/CMakeLists.txt index 4cad3a0657..9f5812dca7 100644 --- a/test_apps/handle/cube_handle_vk_android/src/main/cpp/CMakeLists.txt +++ b/test_apps/handle/cube_handle_vk_android/src/main/cpp/CMakeLists.txt @@ -60,6 +60,22 @@ target_include_directories(cube_handle_vk_android BEFORE PRIVATE target_include_directories(cube_handle_vk_android PRIVATE "${CMAKE_SOURCE_DIR}/../../../../../common") +# displayxr-common — header-only vk_frame_ring.h (dxr::VkFrameRing: N frames in +# flight, ONE vkQueueSubmit per frame, no wait on the frame just submitted — +# INV-4.10, runtime #1917 / displayxr-demo-modelviewer#170). Sources only: don't +# run its CMake (the STATIC lib carries desktop platform sources); its common/ +# dir is appended LAST to the include path so no local header is shadowed. +include(FetchContent) +FetchContent_Declare(displayxr_common_rules + GIT_REPOSITORY https://github.com/DisplayXR/displayxr-common.git + GIT_TAG v2.29.0 + GIT_SHALLOW TRUE) +FetchContent_GetProperties(displayxr_common_rules) +if(NOT displayxr_common_rules_POPULATED) + FetchContent_Populate(displayxr_common_rules) +endif() +target_include_directories(cube_handle_vk_android PRIVATE "${displayxr_common_rules_SOURCE_DIR}/common") + # -u ANativeActivity_onCreate forces the linker to keep the symbol the # Android runtime resolves dynamically on Activity launch — without it, # the shared lib loads but exposes nothing. diff --git a/test_apps/handle/cube_handle_vk_android/src/main/cpp/hud_font.cpp b/test_apps/handle/cube_handle_vk_android/src/main/cpp/hud_font.cpp index 3d1464284c..87af894d7c 100644 --- a/test_apps/handle/cube_handle_vk_android/src/main/cpp/hud_font.cpp +++ b/test_apps/handle/cube_handle_vk_android/src/main/cpp/hud_font.cpp @@ -440,13 +440,16 @@ hud_font_init(HudFont &f, VkPhysicalDevice phys, VkDevice device, VkQueue queue, } // ── 5. Mapped quad vertex buffer. ── - if (!make_buffer(phys, device, sizeof(TextVtx) * kMaxVerts, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, + // One region per frame-ring slot (see HudFont::slot). + if (!make_buffer(phys, device, sizeof(TextVtx) * kMaxVerts * HUD_FONT_MAX_SLOTS, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, f.vbuf, f.vbuf_mem)) { HFLOGE("hud_font: vertex buffer failed"); return false; } - vkMapMemory(device, f.vbuf_mem, 0, sizeof(TextVtx) * kMaxVerts, 0, &f.vbuf_mapped); + vkMapMemory(device, f.vbuf_mem, 0, sizeof(TextVtx) * kMaxVerts * HUD_FONT_MAX_SLOTS, 0, + &f.vbuf_mapped); // ── 6. SDF rounded-rect panel pipeline (smooth corners, no texture). Same // {vec2 pos; vec2 c} vertex layout + alpha blend + no-depth as the text @@ -548,13 +551,15 @@ hud_font_init(HudFont &f, VkPhysicalDevice phys, VkDevice device, VkQueue queue, HFLOGE("hud_font: panel pipeline failed (%d)", (int)ppr); return false; } - if (!make_buffer(phys, device, sizeof(TextVtx) * 6, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, + if (!make_buffer(phys, device, sizeof(TextVtx) * 6 * HUD_FONT_MAX_SLOTS, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, f.panel_vbuf, f.panel_vbuf_mem)) { HFLOGE("hud_font: panel vbuf failed"); return false; } - vkMapMemory(device, f.panel_vbuf_mem, 0, sizeof(TextVtx) * 6, 0, &f.panel_vbuf_mapped); + vkMapMemory(device, f.panel_vbuf_mem, 0, sizeof(TextVtx) * 6 * HUD_FONT_MAX_SLOTS, 0, + &f.panel_vbuf_mapped); } f.ready = true; @@ -573,7 +578,7 @@ hud_font_build(HudFont &f, const char *text, float ox, float oy, float ndc_per_p if (!f.ready || !f.vbuf_mapped || !text) { return 0; } - TextVtx *v = (TextVtx *)f.vbuf_mapped; + TextVtx *v = (TextVtx *)f.vbuf_mapped + (f.slot % HUD_FONT_MAX_SLOTS) * kMaxVerts; uint32_t n = 0; const stbtt_bakedchar *cd = (const stbtt_bakedchar *)f.chars; float bx0 = 1e9f, by0 = 1e9f, bx1 = -1e9f, by1 = -1e9f; @@ -639,7 +644,7 @@ hud_font_draw(HudFont &f, VkCommandBuffer cmd, const float mvp[16], const float memcpy(push + 16, color, sizeof(float) * 4); vkCmdPushConstants(cmd, f.pipe_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), push); - VkDeviceSize off = 0; + VkDeviceSize off = sizeof(TextVtx) * kMaxVerts * (f.slot % HUD_FONT_MAX_SLOTS); vkCmdBindVertexBuffers(cmd, 0, 1, &f.vbuf, &off); vkCmdDraw(cmd, vert_count, 1, 0, 0); } @@ -653,7 +658,8 @@ hud_font_draw_panel(HudFont &f, VkCommandBuffer cmd, const float mvp[16], float } const float hx = 0.5f * (x1 - x0), hy = 0.5f * (y1 - y0); // 6 verts: NDC corner + centered coord (corner relative to panel center). - TextVtx *v = (TextVtx *)f.panel_vbuf_mapped; + const uint32_t slot = f.slot % HUD_FONT_MAX_SLOTS; + TextVtx *v = (TextVtx *)f.panel_vbuf_mapped + slot * 6; v[0] = {{x0, y0}, {-hx, -hy}}; v[1] = {{x1, y0}, {hx, -hy}}; v[2] = {{x1, y1}, {hx, hy}}; @@ -672,7 +678,7 @@ hud_font_draw_panel(HudFont &f, VkCommandBuffer cmd, const float mvp[16], float vkCmdPushConstants(cmd, f.panel_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), push); - VkDeviceSize off = 0; + VkDeviceSize off = sizeof(TextVtx) * 6 * slot; vkCmdBindVertexBuffers(cmd, 0, 1, &f.panel_vbuf, &off); vkCmdDraw(cmd, 6, 1, 0, 0); } diff --git a/test_apps/handle/cube_handle_vk_android/src/main/cpp/hud_font.h b/test_apps/handle/cube_handle_vk_android/src/main/cpp/hud_font.h index 1119f92bdc..d73217a91d 100644 --- a/test_apps/handle/cube_handle_vk_android/src/main/cpp/hud_font.h +++ b/test_apps/handle/cube_handle_vk_android/src/main/cpp/hud_font.h @@ -21,6 +21,7 @@ #define HUD_FONT_FIRST_CHAR 32 #define HUD_FONT_NUM_CHARS 96 // ASCII 32..127 #define HUD_FONT_MAX_QUADS 256 // plenty for a few short lines +#define HUD_FONT_MAX_SLOTS 8 // >= dxr::VkFrameRing frames in flight (clamped 1..8) struct HudFontBakedChar { // mirrors stbtt_bakedchar (avoids leaking stb in the header) uint16_t x0, y0, x1, y1; @@ -56,6 +57,10 @@ struct HudFont { void *panel_vbuf_mapped = nullptr; // Persistently-mapped glyph quad vertex buffer ({vec2 pos; vec2 uv}). + // Both vertex buffers hold HUD_FONT_MAX_SLOTS regions; build/draw use the + // region of `slot` — the host sets it to its frame-ring slot each frame so + // a rebuild never overwrites vertices an in-flight frame still reads. + uint32_t slot = 0; VkBuffer vbuf = VK_NULL_HANDLE; VkDeviceMemory vbuf_mem = VK_NULL_HANDLE; void *vbuf_mapped = nullptr; diff --git a/test_apps/handle/cube_handle_vk_android/src/main/cpp/main.cpp b/test_apps/handle/cube_handle_vk_android/src/main/cpp/main.cpp index a91f24aa05..25fd2cf59e 100644 --- a/test_apps/handle/cube_handle_vk_android/src/main/cpp/main.cpp +++ b/test_apps/handle/cube_handle_vk_android/src/main/cpp/main.cpp @@ -63,6 +63,7 @@ #include #include "crate_scene.h" #include "hud_font.h" +#include #define LOG_TAG "cube_handle_vk_android" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) @@ -401,7 +402,11 @@ srgb_to_linear(float c) { return c <= 0.04045f ? c / 12.92f : powf((c + 0.055f) / 1.055f, 2.4f); } -VkCommandPool g_app_cmd_pool = VK_NULL_HANDLE; +// Frame submission ring (dxr::VkFrameRing, displayxr-common): 3 frames in +// flight, ONE submit per frame, never a wait on the frame just submitted — +// INV-4.10 / #1917 (a per-frame submit + vkQueueWaitIdle parks the Adreno +// governor at ~1/3 clock). Owns the per-frame command buffers + fences. +dxr::VkFrameRing g_ring; // Graphics pipeline state for the triangle renderer. VkRenderPass g_render_pass = VK_NULL_HANDLE; @@ -420,10 +425,13 @@ constexpr uint32_t kCubeVertexCount = 36; // In-frame HUD: a persistently-mapped host-visible vertex buffer of CubeVertex // quads, rebuilt each frame and drawn with an identity MVP (screen-space NDC). // Android widgets can't overlay the runtime's display window, so the HUD lives -// inside the rendered frame. +// inside the rendered frame. One kHudMaxVerts region PER FRAME-RING SLOT: with +// several frames in flight, rebuilding a single shared region would overwrite +// vertices a still-executing frame is reading. VkBuffer g_hud_vbuf = VK_NULL_HANDLE; VkDeviceMemory g_hud_vbuf_mem = VK_NULL_HANDLE; void *g_hud_mapped = nullptr; +uint32_t g_hud_slots = 1; constexpr uint32_t kHudMaxVerts = 8192; // Authoritative 4-way display rotation, pushed from MainActivity (Surface @@ -1341,18 +1349,16 @@ create_scene() g_render_pass, g_asset_manager, g_scene_linear); } -// Standalone command pool for the test app's per-frame cmd buffers. -// Created lazily once the session + swapchains exist. +// Frame ring for the per-frame command buffers (INV-4.10). Created lazily once +// the session + swapchains exist. Android processes have no usable environment, +// so the frames-in-flight count is passed explicitly. bool -create_cmd_pool() +create_frame_ring() { - VkCommandPoolCreateInfo ci = {}; - ci.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; - ci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - ci.queueFamilyIndex = g_vk_queue_family; - VkResult res = vkCreateCommandPool(g_vk_device, &ci, nullptr, &g_app_cmd_pool); - if (res != VK_SUCCESS) { - LOGE("vkCreateCommandPool failed: %d", res); + dxr::VkFrameRing::Config cfg; + cfg.framesInFlight = 3; + if (!g_ring.init(g_vk_device, g_vk_phys_device, g_vk_queue, g_vk_queue_family, cfg)) { + LOGE("frame ring init failed"); return false; } return true; @@ -1381,12 +1387,9 @@ destroy_reference_space() } void -destroy_cmd_pool() +destroy_frame_ring() { - if (g_app_cmd_pool != VK_NULL_HANDLE && g_vk_device != VK_NULL_HANDLE) { - vkDestroyCommandPool(g_vk_device, g_app_cmd_pool, nullptr); - g_app_cmd_pool = VK_NULL_HANDLE; - } + g_ring.destroy(); } // Record + submit a clear-to-color cmd buffer on the swapchain image @@ -1629,6 +1632,20 @@ create_render_pass() subpass.pColorAttachments = &color_ref; subpass.pDepthStencilAttachment = &depth_ref; + // The single atlas depth image is shared by every frame, and with the frame + // ring (INV-4.10) several frames are in flight — order this frame's depth + // clear/writes after the previous frame's (WAW across submits). + VkSubpassDependency depth_dep = {}; + depth_dep.srcSubpass = VK_SUBPASS_EXTERNAL; + depth_dep.dstSubpass = 0; + depth_dep.srcStageMask = + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + depth_dep.dstStageMask = + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + depth_dep.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + depth_dep.dstAccessMask = + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + VkAttachmentDescription attachments[2] = {color, depth}; VkRenderPassCreateInfo rpci = {}; rpci.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; @@ -1636,6 +1653,8 @@ create_render_pass() rpci.pAttachments = attachments; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; + rpci.dependencyCount = 1; + rpci.pDependencies = &depth_dep; VkResult res = vkCreateRenderPass(g_vk_device, &rpci, nullptr, &g_render_pass); if (res != VK_SUCCESS) { @@ -2050,7 +2069,8 @@ build_hud(CubeVertex *v) static bool create_hud_buffer() { - const VkDeviceSize size = sizeof(CubeVertex) * kHudMaxVerts; + g_hud_slots = g_ring.framesInFlight() > 0 ? g_ring.framesInFlight() : 1; + const VkDeviceSize size = sizeof(CubeVertex) * kHudMaxVerts * g_hud_slots; VkBufferCreateInfo bci = {}; bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bci.size = size; @@ -2074,12 +2094,13 @@ create_hud_buffer() return false; } vkBindBufferMemory(g_vk_device, g_hud_vbuf, g_hud_vbuf_mem, 0); - // Persistently mapped (host-coherent): rebuilt each frame in record_atlas. + // Persistently mapped (host-coherent): rebuilt each frame in record_atlas, + // into the current ring slot's region. if (vkMapMemory(g_vk_device, g_hud_vbuf_mem, 0, size, 0, &g_hud_mapped) != VK_SUCCESS) { LOGE("map HUD vertex buffer failed"); return false; } - DXR_HW_DBG("HUD vertex buffer created (%u verts max)", kHudMaxVerts); + DXR_HW_DBG("HUD vertex buffer created (%u verts max x %u slots)", kHudMaxVerts, g_hud_slots); return true; } @@ -2241,20 +2262,13 @@ record_atlas(uint32_t image_idx, const XrView *views, uint32_t view_count, uint3 uint32_t render_h, uint32_t cols, uint32_t rows, bool swap_lr, const XrPosef &rig_pose) { (void)rows; - VkCommandBufferAllocateInfo ai = {}; - ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - ai.commandPool = g_app_cmd_pool; - ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - ai.commandBufferCount = 1; - VkCommandBuffer cmd = VK_NULL_HANDLE; - if (vkAllocateCommandBuffers(g_vk_device, &ai, &cmd) != VK_SUCCESS) { + // The ring waits only the fence of the frame that used this slot N frames + // ago, then hands back its (reset, begun) command buffer. + VkCommandBuffer cmd = g_ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { return false; } - - VkCommandBufferBeginInfo bi = {}; - bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); + const uint32_t slot = g_ring.slotIndex(); // LOAD_OP_CLEAR fills the WHOLE atlas (every tile) once at render-pass // begin. Dark blue, matching cube_handle_vk_win's scene background. @@ -2329,14 +2343,16 @@ record_atlas(uint32_t image_idx, const XrView *views, uint32_t view_count, uint3 uint32_t hud_n = 0; const Mat4 hud_mvp = mat4_identity(); const bool hud_ok = (g_hud_vbuf != VK_NULL_HANDLE && g_hud_mapped != nullptr); + const uint32_t hud_slot = slot % g_hud_slots; if (hud_ok) { - hud_n = build_hud(reinterpret_cast(g_hud_mapped)); + hud_n = build_hud(reinterpret_cast(g_hud_mapped) + hud_slot * kHudMaxVerts); } // Build the crisp stb_truetype text once per frame (drawn per tile below, // on top of the translucent panel). The panel top-left is (-0.975,-0.800). uint32_t hud_text_n = 0; float hud_bbox[4] = {0, 0, 0, 0}; // tight text bounds (NDC) → snug panel + g_hud_font.slot = slot; // per-slot glyph/panel vertex regions (frames in flight) if (g_hud_font.ready) { char xb[16], yb[16], zb[16], block[80]; hud_ftoa(g_eye_x.load(std::memory_order_relaxed), xb, sizeof xb); @@ -2398,7 +2414,7 @@ record_atlas(uint32_t image_idx, const XrView *views, uint32_t view_count, uint3 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, g_pipeline); vkCmdPushConstants(cmd, g_pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(hud_mvp), &hud_mvp); - VkDeviceSize hud_off = 0; + VkDeviceSize hud_off = sizeof(CubeVertex) * kHudMaxVerts * hud_slot; vkCmdBindVertexBuffers(cmd, 0, 1, &g_hud_vbuf, &hud_off); // Pass 1: translucent panel (blend-constant alpha < 1). if (g_hud_panel_n > 0) { @@ -2427,19 +2443,14 @@ record_atlas(uint32_t image_idx, const XrView *views, uint32_t view_count, uint3 } vkCmdEndRenderPass(cmd); - vkEndCommandBuffer(cmd); - VkSubmitInfo si = {}; - si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - VkResult res = vkQueueSubmit(g_vk_queue, 1, &si, VK_NULL_HANDLE); - if (res == VK_SUCCESS) { - // Host stall — acceptable for this test app; xrEndFrame needs the - // atlas image ready and there's no per-frame fence wired yet. - vkQueueWaitIdle(g_vk_queue); + // ONE submit with the slot fence and NO wait. The runtime composes on the + // same VkQueue, so the caller releases the swapchain image only after this + // (queue order puts our work ahead of the compose). + VkResult res = g_ring.endFrame(); + if (res != VK_SUCCESS) { + LOGE("record_atlas: frame submit failed: %d", (int)res); } - vkFreeCommandBuffers(g_vk_device, g_app_cmd_pool, 1, &cmd); DXR_HW_DBG_ONCE("record_atlas: first successful tiled-atlas submit"); return res == VK_SUCCESS; @@ -2768,6 +2779,7 @@ render_frame() res = xrWaitSwapchainImage(g_atlas.swapchain, &wait_img); } if (res == XR_SUCCESS) { + // Submits the frame (ring endFrame) BEFORE the release below. record_atlas(img_idx, views, submit_views, render_w, render_h, cols, rows, swap_lr, rig_pose); @@ -2885,10 +2897,12 @@ destroy_vulkan() void destroy_instance() { - // Tear down in reverse-creation order: cmd_pool → space → swapchains + // Tear down in reverse-creation order: frame ring → space → swapchains // → session → Vulkan → instance. Doing it any other way invalidates // handles still referenced by the runtime / loader. Drain GPU - // before per-view image destruction (audit B6). + // before per-view image destruction (audit B6): the frame ring's + // in-flight frames first, then the whole device. + g_ring.waitAll(); if (g_vk_device != VK_NULL_HANDLE) { vkDeviceWaitIdle(g_vk_device); } @@ -2905,7 +2919,7 @@ destroy_instance() } destroy_pipeline(); destroy_render_pass(); - destroy_cmd_pool(); + destroy_frame_ring(); destroy_reference_space(); destroy_swapchains(); destroy_session(); @@ -2950,7 +2964,7 @@ handle_cmd(struct android_app *app, int32_t cmd) query_display_info_and_modes() && create_swapchains() && create_reference_space() && - create_cmd_pool() && + create_frame_ring() && create_render_pass() && create_pipeline() && create_scene() && diff --git a/test_apps/handle/cube_handle_vk_macos/CMakeLists.txt b/test_apps/handle/cube_handle_vk_macos/CMakeLists.txt index b73484186a..b55b0d8f68 100644 --- a/test_apps/handle/cube_handle_vk_macos/CMakeLists.txt +++ b/test_apps/handle/cube_handle_vk_macos/CMakeLists.txt @@ -70,6 +70,8 @@ FetchContent_Declare( # v2.27.0: window-space / HUD swapchains prefer the _SRGB sibling of # R8G8B8A8 (dxr::ChooseWindowSpaceSwapchainFormat; ADR-044 §7, # displayxr-common#72) — a UNORM HUD of encoded bytes was encoded twice. + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. # v2.29.0: dxr::CursorDepth (common/cursor_depth.h), the XR_DXR_cursor_depth # app helper (ADR-046 Amendment 2) - same pin as the aggregate test_apps build. GIT_TAG v2.29.0 diff --git a/test_apps/handle/cube_handle_vk_macos/main.mm b/test_apps/handle/cube_handle_vk_macos/main.mm index 414e338080..f335ee055a 100644 --- a/test_apps/handle/cube_handle_vk_macos/main.mm +++ b/test_apps/handle/cube_handle_vk_macos/main.mm @@ -60,6 +60,7 @@ #include "stb_image.h" #include "color_policy.h" +#include // dxr::VkFrameRing — INV-4.10 frame-submit discipline // ============================================================================ // Logging @@ -900,9 +901,11 @@ static XrQuaternionf quat_multiply(XrQuaternionf a, XrQuaternionf b) { VkBuffer gridVertexBuffer = VK_NULL_HANDLE; VkDeviceMemory gridVertexMemory = VK_NULL_HANDLE; int gridVertexCount = 0; - VkCommandPool commandPool = VK_NULL_HANDLE; - VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - VkFence frameFence = VK_NULL_HANDLE; + VkCommandPool commandPool = VK_NULL_HANDLE; // init-time uploads only + // INV-4.10 / #1917: N frames in flight, ONE vkQueueSubmit per frame, never + // a wait on the frame just submitted. Owns the per-frame command buffers + // and fences (and the blocking one-off path, submitImmediate()). + dxr::VkFrameRing ring; float cubeRotation = 0.0f; // ADR-021 / INV-4.6: does this renderer emit SCENE-LINEAR? Latched once, at @@ -1309,12 +1312,31 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; + // One depth image is shared by every frame, and with several frames in + // flight (VkFrameRing) the previous frame's pass may still be writing it: + // order this pass's attachment writes after any earlier ones. The old + // per-frame fence wait gave this ordering implicitly. + VkSubpassDependency dep = {}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + dep.dstStageMask = dep.srcStageMask; + dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpInfo = {}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpInfo.attachmentCount = 2; rpInfo.pAttachments = attachments; rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; VK_CHECK(vkCreateRenderPass(device, &rpInfo, nullptr, &renderer.renderPass)); // Create second render pass with LOAD instead of CLEAR (for second eye in SBS) @@ -1631,17 +1653,13 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic poolInfo.queueFamilyIndex = queueFamilyIndex; VK_CHECK(vkCreateCommandPool(device, &poolInfo, nullptr, &renderer.commandPool)); - VkCommandBufferAllocateInfo allocInfo = {}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.commandPool = renderer.commandPool; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandBufferCount = 1; - VK_CHECK(vkAllocateCommandBuffers(device, &allocInfo, &renderer.commandBuffer)); - - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - VK_CHECK(vkCreateFence(device, &fenceInfo, nullptr, &renderer.frameFence)); + // Frame ring (default 3 in flight; DXR_VK_FRAMES_IN_FLIGHT overrides). + // Grid / cube / cursor MVPs are push constants, so the ring owns no UBO. + if (!renderer.ring.init(device, physDevice, graphicsQueue, queueFamilyIndex, + dxr::VkFrameRing::Config{})) { + LOG_ERROR("VkFrameRing init failed"); + return false; + } // Load textures and create descriptor set { @@ -1810,18 +1828,11 @@ static void CursorSpriteModel(float *out) static void RenderScene(VkRenderer& renderer, uint32_t imageIndex, const EyeRenderParams* eyes, int eyeCount) { - VkDevice device = renderer.device; - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &renderer.frameFence); - auto& fb = renderer.swapchainFBs; - VkCommandBuffer cmd = renderer.commandBuffer; - vkResetCommandBuffer(cmd, 0); - - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &beginInfo); + // INV-4.10: waits only the fence of the frame that used this ring slot N + // frames ago (normally long signalled), then resets + begins its buffer. + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) return; // Transparent-background mode (DISPLAYXR_TRANSPARENT_BG=1) clears RGBA(0,0,0,0) // so the desktop shows through everywhere the cube isn't drawn. Pairs with @@ -1932,13 +1943,11 @@ static void RenderScene(VkRenderer& renderer, uint32_t imageIndex, } vkCmdEndRenderPass(cmd); - vkEndCommandBuffer(cmd); - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cmd; - vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); + // ONE submit for every view, no wait. Must precede xrReleaseSwapchainImage + // (the runtime composes on the same queue). + VkResult r = renderer.ring.endFrame(); + if (r != VK_SUCCESS) LOG_WARN("VkFrameRing::endFrame: vkQueueSubmit failed (%d)", (int)r); } // Workspace chrome self-test (#48): one-shot clear of a chrome swapchain image @@ -2023,7 +2032,7 @@ static void CleanupVkRenderer(VkRenderer& renderer) { if (fb.depthMemory) vkFreeMemory(renderer.device, fb.depthMemory, nullptr); } - if (renderer.frameFence) vkDestroyFence(renderer.device, renderer.frameFence, nullptr); + renderer.ring.destroy(); // after vkDeviceWaitIdle above; frees its pool/fences if (renderer.commandPool) vkDestroyCommandPool(renderer.device, renderer.commandPool, nullptr); if (renderer.gridVertexBuffer) vkDestroyBuffer(renderer.device, renderer.gridVertexBuffer, nullptr); if (renderer.gridVertexMemory) vkFreeMemory(renderer.device, renderer.gridVertexMemory, nullptr); @@ -3195,7 +3204,7 @@ static bool ReleaseSwapchainImage(AppXrSession& xr) { VkDevice device = VK_NULL_HANDLE; VkPhysicalDevice physDevice = VK_NULL_HANDLE; VkQueue queue = VK_NULL_HANDLE; - VkCommandPool cmdPool = VK_NULL_HANDLE; + dxr::VkFrameRing* ring = nullptr; // uploads go through ring->submitImmediate() VkBuffer stagingBuffer = VK_NULL_HANDLE; VkDeviceMemory stagingMemory = VK_NULL_HANDLE; uint8_t* stagingMapped = nullptr; @@ -3221,13 +3230,13 @@ static bool VkFindMemoryTypeForHud(VkPhysicalDevice phys, uint32_t typeBits, } static bool InitializeHudVkUploader(HudVkUploader& up, - VkDevice device, VkPhysicalDevice phys, VkQueue queue, VkCommandPool pool, + VkDevice device, VkPhysicalDevice phys, VkQueue queue, dxr::VkFrameRing* ring, uint32_t width, uint32_t height) { up.device = device; up.physDevice = phys; up.queue = queue; - up.cmdPool = pool; + up.ring = ring; up.width = width; up.height = height; up.stagingSize = (VkDeviceSize)width * (VkDeviceSize)height * 4; @@ -3281,54 +3290,38 @@ static bool UploadHudPixels(HudVkUploader& up, VkImage hudImage, } } - VkCommandBufferAllocateInfo ca = {}; - ca.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - ca.commandPool = up.cmdPool; - ca.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - ca.commandBufferCount = 1; - VkCommandBuffer cmd = VK_NULL_HANDLE; - if (vkAllocateCommandBuffers(up.device, &ca, &cmd) != VK_SUCCESS) return false; - - VkCommandBufferBeginInfo bi = {}; - bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); - - VkImageMemoryBarrier b = {}; - b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - b.image = hudImage; - b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - b.srcAccessMask = 0; - b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &b); + // A HUD upload is a one-off (only when the HUD text changes — see the + // caller), so it takes the ring's one sanctioned blocking path: its own + // command buffer + fence, allocated once. The wait also keeps the single + // staging buffer safe to rewrite next time. + VkResult r = up.ring->submitImmediate([&](VkCommandBuffer cmd) { + VkImageMemoryBarrier b = {}; + b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.image = hudImage; + b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + b.srcAccessMask = 0; + b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &b); - VkBufferImageCopy region = {}; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - region.imageExtent = {up.width, up.height, 1}; - vkCmdCopyBufferToImage(cmd, up.stagingBuffer, hudImage, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - - b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &b); + VkBufferImageCopy region = {}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {up.width, up.height, 1}; + vkCmdCopyBufferToImage(cmd, up.stagingBuffer, hudImage, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - vkEndCommandBuffer(cmd); - VkSubmitInfo si = {}; - si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - vkQueueSubmit(up.queue, 1, &si, VK_NULL_HANDLE); - vkQueueWaitIdle(up.queue); - vkFreeCommandBuffers(up.device, up.cmdPool, 1, &cmd); - return true; + b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &b); + }); + return r == VK_SUCCESS; } // ============================================================================ @@ -3369,7 +3362,7 @@ static bool UploadHudPixels(HudVkUploader& up, VkImage hudImage, // samples the last released image every frame, as STRAIGHT alpha. `_SRGB` // when offered: the bytes are display-referred. Row 0 = the top (Vulkan). static bool CreateCursorSprite(AppXrSession& xr, VkDevice device, VkPhysicalDevice phys, VkQueue queue, - VkCommandPool pool) + dxr::VkFrameRing* ring) { const uint32_t S = 64; uint32_t formatCount = 0; @@ -3437,7 +3430,7 @@ static bool CreateCursorSprite(AppXrSession& xr, VkDevice device, VkPhysicalDevi } HudVkUploader up = {}; - bool ok = InitializeHudVkUploader(up, device, phys, queue, pool, S, S) && + bool ok = InitializeHudVkUploader(up, device, phys, queue, ring, S, S) && UploadHudPixels(up, imgs[idx].image, buf.data(), S * 4); CleanupHudVkUploader(up); @@ -4013,12 +4006,13 @@ int main() { VkRenderer vkRenderer = {}; if (!InitializeVkRenderer(vkRenderer, vkDevice, physDevice, graphicsQueue, queueFamilyIndex, colorFormat)) { CleanupOpenXR(xr); + vkRenderer.ring.destroy(); // a partial init may have created it; free before the device vkDestroyDevice(vkDevice, nullptr); vkDestroyInstance(vkInstance, nullptr); return 1; } if (xr.cursorDepthRuntimeDraw && xr.cursorDepthCustomSprite && - !CreateCursorSprite(xr, vkDevice, physDevice, graphicsQueue, vkRenderer.commandPool)) { + !CreateCursorSprite(xr, vkDevice, physDevice, graphicsQueue, &vkRenderer.ring)) { LOG_WARN("Cursor sprite swapchain creation failed - using the runtime's default look"); xr.cursorDepthCustomSprite = false; } @@ -4081,7 +4075,7 @@ int main() { } if (InitializeHudRenderer(hudRenderer, HUD_PIXEL_WIDTH, HUD_PIXEL_HEIGHT) && InitializeHudVkUploader(hudUploader, vkDevice, physDevice, - graphicsQueue, vkRenderer.commandPool, + graphicsQueue, &vkRenderer.ring, HUD_PIXEL_WIDTH, HUD_PIXEL_HEIGHT)) { hudReady = true; LOG_INFO("HUD window-space layer ready (%ux%u, %u images, format %lld)", @@ -4826,10 +4820,23 @@ int main() { } // Render HUD into the window-space layer swapchain (when visible). + // INV-4.10: re-render + upload only when the HUD text changed. + // Otherwise the layer keeps showing the swapchain's last + // released image (OpenXR composes the most recently released + // one), so an unchanged HUD costs no acquire, no upload and no + // blocking submit. bool hudSubmitted = false; if (hudReady && g_input.hudVisible && rendered && frameState.shouldRender) { + static std::wstring s_hudUploadedKey; + static bool s_hudHasImage = false; + std::wstring hudKey = g_hudSessionText + L'\x1f' + g_hudModeText + L'\x1f' + + g_hudPerfText + L'\x1f' + g_hudDisplayText + L'\x1f' + g_hudEyeText + + L'\x1f' + g_hudCameraText + L'\x1f' + g_hudStereoText + L'\x1f' + + g_hudHelpText; uint32_t hudIndex = 0; - if (AcquireHudSwapchainImage(hudSwapchain, hudIndex)) { + if (s_hudHasImage && hudKey == s_hudUploadedKey) { + hudSubmitted = true; + } else if (AcquireHudSwapchainImage(hudSwapchain, hudIndex)) { uint32_t rowPitch = 0; const void* pixels = RenderHudAndMap(hudRenderer, &rowPitch, g_hudSessionText, g_hudModeText, g_hudPerfText, g_hudDisplayText, @@ -4837,6 +4844,8 @@ int main() { if (pixels) { if (UploadHudPixels(hudUploader, hudVkImages[hudIndex], pixels, rowPitch)) { hudSubmitted = true; + s_hudHasImage = true; + s_hudUploadedKey = std::move(hudKey); } UnmapHud(hudRenderer); } @@ -4999,8 +5008,8 @@ int main() { LOG_INFO("=== Shutting down ==="); if (hudReady) { - // HUD uploader uses vkRenderer.commandPool — clean up before - // CleanupVkRenderer destroys the pool. + // HUD uploader submits through vkRenderer.ring — clean up before + // CleanupVkRenderer destroys it. CleanupHudVkUploader(hudUploader); CleanupHudRenderer(hudRenderer); } diff --git a/test_apps/handle/cube_handle_vk_win/main.cpp b/test_apps/handle/cube_handle_vk_win/main.cpp index 156f672987..2b4c94a66a 100644 --- a/test_apps/handle/cube_handle_vk_win/main.cpp +++ b/test_apps/handle/cube_handle_vk_win/main.cpp @@ -393,7 +393,7 @@ static int64_t PickPanelFormat(XrSession session, int64_t srgb, int64_t unorm) { return unorm; } -static bool CreateAndFillL2DPanel(XrSessionManager& xr, VkRenderer* renderer, VkCommandPool cmdPool, +static bool CreateAndFillL2DPanel(XrSessionManager& xr, VkRenderer* renderer, uint32_t w, uint32_t h, int variant, L2DPanel& out) { if (w == 0 || h == 0) { return false; @@ -515,49 +515,34 @@ static bool CreateAndFillL2DPanel(XrSessionManager& xr, VkRenderer* renderer, Vk vkUnmapMemory(renderer->device, stagingMem); // Copy staging → image, then transition to SHADER_READ_ONLY for sampling. - VkCommandBufferAllocateInfo cbai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; - cbai.commandPool = cmdPool; - cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - cbai.commandBufferCount = 1; - VkCommandBuffer cmd = VK_NULL_HANDLE; - vkAllocateCommandBuffers(renderer->device, &cbai, &cmd); - VkCommandBufferBeginInfo cbi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; - cbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &cbi); - - VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - b.srcAccessMask = 0; - b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - b.image = img; - b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, - nullptr, 0, nullptr, 1, &b); - - VkBufferImageCopy region = {}; - region.bufferRowLength = w; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - region.imageOffset = {0, 0, 0}; - region.imageExtent = {w, h, 1}; - vkCmdCopyBufferToImage(cmd, staging, img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - - b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, - nullptr, 0, nullptr, 1, &b); - - vkEndCommandBuffer(cmd); - VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - vkQueueSubmit(renderer->graphicsQueue, 1, &si, VK_NULL_HANDLE); - vkQueueWaitIdle(renderer->graphicsQueue); - vkFreeCommandBuffers(renderer->device, cmdPool, 1, &cmd); + // One-off (static content, filled once): the ring's sanctioned blocking path. + renderer->ring.submitImmediate([&](VkCommandBuffer cmd) { + VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + b.srcAccessMask = 0; + b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.image = img; + b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, + nullptr, 0, nullptr, 1, &b); + + VkBufferImageCopy region = {}; + region.bufferRowLength = w; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageOffset = {0, 0, 0}; + region.imageExtent = {w, h, 1}; + vkCmdCopyBufferToImage(cmd, staging, img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, + nullptr, 0, nullptr, 1, &b); + }); vkDestroyBuffer(renderer->device, staging, nullptr); vkFreeMemory(renderer->device, stagingMem, nullptr); @@ -575,7 +560,6 @@ static void RenderThreadFunc( uint32_t hudHeight, VkBuffer hudStagingBuffer, void* hudStagingMapped, - VkCommandPool hudCmdPool, std::vector* hudSwapchainImages) { LOG_INFO("[RenderThread] Started"); @@ -1195,68 +1179,51 @@ static void RenderThreadFunc( UnmapHud(*hud); } - // Copy staging buffer to HUD swapchain image - VkCommandBufferAllocateInfo allocInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; - allocInfo.commandPool = hudCmdPool; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandBufferCount = 1; - - VkCommandBuffer cmdBuf; - vkAllocateCommandBuffers(renderer->device, &allocInfo, &cmdBuf); - - VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmdBuf, &beginInfo); - + // Copy staging buffer to HUD swapchain image. The HUD + // text (perf line) changes every frame and the shared + // staging buffer is rewritten next frame, so this stays + // a blocking upload — via the ring's one sanctioned + // blocking path (no per-frame cmd alloc). HUD is hidden + // by default (Shift+Tab). VkImage hudImg = (*hudSwapchainImages)[hudImageIndex].image; - - // Barrier: UNDEFINED -> TRANSFER_DST - VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - barrier.srcAccessMask = 0; - barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = hudImg; - barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - vkCmdPipelineBarrier(cmdBuf, - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_TRANSFER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &barrier); - - // Copy buffer to image - VkBufferImageCopy region = {}; - region.bufferRowLength = hudWidth; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - region.imageOffset = {0, 0, 0}; - region.imageExtent = {hudWidth, hudHeight, 1}; - vkCmdCopyBufferToImage(cmdBuf, hudStagingBuffer, hudImg, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - - // Barrier: TRANSFER_DST -> COLOR_ATTACHMENT - barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - vkCmdPipelineBarrier(cmdBuf, - VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - 0, 0, nullptr, 0, nullptr, 1, &barrier); - - vkEndCommandBuffer(cmdBuf); - - VkSubmitInfo submitInfo = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cmdBuf; LOG_INFO("[FRAME] HUD: Submitting GPU commands..."); - vkQueueSubmit(renderer->graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); - LOG_INFO("[FRAME] HUD: vkQueueWaitIdle..."); - vkQueueWaitIdle(renderer->graphicsQueue); + renderer->ring.submitImmediate([&](VkCommandBuffer cmdBuf) { + // Barrier: UNDEFINED -> TRANSFER_DST + VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = hudImg; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(cmdBuf, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier); + + // Copy buffer to image + VkBufferImageCopy region = {}; + region.bufferRowLength = hudWidth; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageOffset = {0, 0, 0}; + region.imageExtent = {hudWidth, hudHeight, 1}; + vkCmdCopyBufferToImage(cmdBuf, hudStagingBuffer, hudImg, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + // Barrier: TRANSFER_DST -> COLOR_ATTACHMENT + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + vkCmdPipelineBarrier(cmdBuf, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier); + }); LOG_INFO("[FRAME] HUD: GPU done"); - vkFreeCommandBuffers(renderer->device, hudCmdPool, 1, &cmdBuf); - LOG_INFO("[FRAME] HUD: Releasing swapchain image..."); ReleaseHudSwapchainImage(*xr); LOG_INFO("[FRAME] HUD: Released"); @@ -1302,7 +1269,7 @@ static void RenderThreadFunc( g_panel1Rect.offset = {(int32_t)(winW / 16), (int32_t)(winH * 9 / 16)}; } g_panel1Rect.extent = {(int32_t)pw, (int32_t)ph}; - bool ok = CreateAndFillL2DPanel(*xr, renderer, hudCmdPool, pw, ph, p1variant, g_panel1); + bool ok = CreateAndFillL2DPanel(*xr, renderer, pw, ph, p1variant, g_panel1); // #491 part 3 — a large backdrop submitted BEFORE the // projection (an UNDER layer): the flat 2D plane the @@ -1314,7 +1281,7 @@ static void RenderThreadFunc( g_backdropRect.offset = {(int32_t)(winW / 2 - bw / 2), (int32_t)(winH / 2 - bh / 2)}; g_backdropRect.extent = {(int32_t)bw, (int32_t)bh}; - ok = CreateAndFillL2DPanel(*xr, renderer, hudCmdPool, bw, bh, + ok = CreateAndFillL2DPanel(*xr, renderer, bw, bh, g_l2dBackdropVariant, g_backdrop); } @@ -1324,7 +1291,7 @@ static void RenderThreadFunc( g_panel2Rect.offset = {g_panel1Rect.offset.x + (int32_t)(pw / 2), g_panel1Rect.offset.y - (int32_t)(ph / 4)}; g_panel2Rect.extent = {(int32_t)pw, (int32_t)ph}; - ok = CreateAndFillL2DPanel(*xr, renderer, hudCmdPool, pw, ph, 1, g_panel2); + ok = CreateAndFillL2DPanel(*xr, renderer, pw, ph, 1, g_panel2); } if (ok && g_l2dMask && g_zone.available && g_zone.pfnCreate && @@ -1704,7 +1671,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine VkBuffer hudStagingBuffer = VK_NULL_HANDLE; VkDeviceMemory hudStagingMemory = VK_NULL_HANDLE; void* hudStagingMapped = nullptr; - VkCommandPool hudCmdPool = VK_NULL_HANDLE; if (hudOk) { // Staging buffer @@ -1749,17 +1715,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine } } - // Command pool for HUD copy operations - if (hudOk) { - VkCommandPoolCreateInfo poolInfo = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; - poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - poolInfo.queueFamilyIndex = queueFamilyIndex; - if (vkCreateCommandPool(vkDevice, &poolInfo, nullptr, &hudCmdPool) != VK_SUCCESS) { - LOG_WARN("Failed to create HUD command pool"); - hudOk = false; - } - } - if (hudOk) { LOG_INFO("HUD Vulkan resources created (%ux%u)", hudWidth, hudHeight); } @@ -1780,7 +1735,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine std::thread renderThread(RenderThreadFunc, hwnd, &xr, &vkRenderer, hudOk ? &hudRenderer : nullptr, hudWidth, hudHeight, - hudStagingBuffer, hudStagingMapped, hudCmdPool, + hudStagingBuffer, hudStagingMapped, hudOk ? &hudSwapImages : nullptr); MSG msg = {}; @@ -1797,7 +1752,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine LOG_INFO(""); LOG_INFO("=== Shutting down ==="); - if (hudCmdPool != VK_NULL_HANDLE) vkDestroyCommandPool(vkDevice, hudCmdPool, nullptr); + // Drain in-flight ring frames before freeing anything they may reference. + vkRenderer.ring.waitAll(); if (hudStagingBuffer != VK_NULL_HANDLE) { vkUnmapMemory(vkDevice, hudStagingMemory); vkDestroyBuffer(vkDevice, hudStagingBuffer, nullptr); diff --git a/test_apps/handle/cube_handle_vk_win/vk_renderer.cpp b/test_apps/handle/cube_handle_vk_win/vk_renderer.cpp index c75bdceba8..5621b04d4d 100644 --- a/test_apps/handle/cube_handle_vk_win/vk_renderer.cpp +++ b/test_apps/handle/cube_handle_vk_win/vk_renderer.cpp @@ -586,12 +586,27 @@ bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysicalDevic subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; + // With several frames in flight (dxr::VkFrameRing) the per-eye depth + // image is shared by consecutive frames: order this frame's depth + // clear/writes after the previous frame's (WAW), and colour likewise. + VkSubpassDependency dep = {}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstStageMask = dep.srcStageMask; + dep.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpInfo = {}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpInfo.attachmentCount = 2; rpInfo.pAttachments = attachments; rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderer.renderPass) != VK_SUCCESS) { LOG_ERROR("Failed to create render pass"); @@ -1214,16 +1229,10 @@ bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysicalDevic renderer.texturesLoaded = true; } - // Frame fence - { - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - - if (vkCreateFence(device, &fenceInfo, nullptr, &renderer.frameFence) != VK_SUCCESS) { - LOG_ERROR("Failed to create fence"); - return false; - } + // Frame ring (INV-4.10 / #1917): per-frame command buffers + fences. + if (!renderer.ring.init(device, physDevice, queue, queueFamilyIndex, dxr::VkFrameRing::Config{})) { + LOG_ERROR("Failed to create frame ring"); + return false; } LOG_INFO("Vulkan renderer initialized"); @@ -1343,20 +1352,13 @@ void RenderScene( const EyeRenderParams* eyes, int eyeCount, float zoomScale) { - VkDevice device = renderer.device; - - // Wait for previous frame's fence - LOG_INFO("[RenderScene] imageIndex=%u: vkWaitForFences (pre-render)...", imageIndex); - VkResult fenceResult = vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - LOG_INFO("[RenderScene] vkWaitForFences returned %d", (int)fenceResult); - vkResetFences(device, 1, &renderer.frameFence); - - // Begin command buffer - vkResetCommandBuffer(renderer.commandBuffer, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(renderer.commandBuffer, &beginInfo); + // Begin this frame's ring slot (waits only the fence of the frame that + // used this slot N frames ago). + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + LOG_ERROR("[RenderScene] imageIndex=%u: ring.beginFrame failed", imageIndex); + return; + } // Single render pass with CLEAR covering full framebuffer. // Transparent-background mode (DISPLAYXR_TRANSPARENT_BG=1): clear to @@ -1399,7 +1401,7 @@ void RenderScene( rpBegin.pClearValues = clearValues; __try { - vkCmdBeginRenderPass(renderer.commandBuffer, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); } __except(EXCEPTION_EXECUTE_HANDLER) { LOG_ERROR("[RenderScene] CRASH in vkCmdBeginRenderPass! exception=0x%08X", GetExceptionCode()); return; @@ -1425,12 +1427,12 @@ void RenderScene( viewport.height = -(float)h; viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; - vkCmdSetViewport(renderer.commandBuffer, 0, 1, &viewport); + vkCmdSetViewport(cmd, 0, 1, &viewport); VkRect2D scissor = {}; scissor.offset = {(int32_t)vpX, (int32_t)vpY}; scissor.extent = {w, h}; - vkCmdSetScissor(renderer.commandBuffer, 0, 1, &scissor); + vkCmdSetScissor(cmd, 0, 1, &scissor); // Draw cube - base rests on grid at y=0 { @@ -1446,19 +1448,19 @@ void RenderScene( XMMATRIX cubeModel = cubeRot * cubeScale; XMStoreFloat4x4(&pc.model, cubeModel); - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.cubePipelineLayout, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipeline); + vkCmdPushConstants(cmd, renderer.cubePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc); if (renderer.texturesLoaded) { - vkCmdBindDescriptorSets(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipelineLayout, 0, 1, &renderer.descriptorSet, 0, nullptr); } VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(renderer.commandBuffer, 0, 1, &renderer.cubeVertexBuffer, &offset); - vkCmdBindIndexBuffer(renderer.commandBuffer, renderer.cubeIndexBuffer, 0, VK_INDEX_TYPE_UINT16); - vkCmdDrawIndexed(renderer.commandBuffer, 36, 1, 0, 0, 0); + vkCmdBindVertexBuffers(cmd, 0, 1, &renderer.cubeVertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, renderer.cubeIndexBuffer, 0, VK_INDEX_TYPE_UINT16); + vkCmdDrawIndexed(cmd, 36, 1, 0, 0, 0); } // Draw grid floor @@ -1472,36 +1474,30 @@ void RenderScene( XMStoreFloat4x4(&pc.transform, gridWVP); pc.color[0] = 0.3f; pc.color[1] = 0.3f; pc.color[2] = 0.35f; pc.color[3] = 1.0f; - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.gridPipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.pipelineLayout, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.gridPipeline); + vkCmdPushConstants(cmd, renderer.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(renderer.commandBuffer, 0, 1, &renderer.gridVertexBuffer, &offset); - vkCmdDraw(renderer.commandBuffer, renderer.gridVertexCount, 1, 0, 0); + vkCmdBindVertexBuffers(cmd, 0, 1, &renderer.gridVertexBuffer, &offset); + vkCmdDraw(cmd, renderer.gridVertexCount, 1, 0, 0); } } - vkCmdEndRenderPass(renderer.commandBuffer); - vkEndCommandBuffer(renderer.commandBuffer); - - // Submit - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &renderer.commandBuffer; + vkCmdEndRenderPass(cmd); - VkResult submitResult = vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - LOG_INFO("[RenderScene] vkQueueSubmit returned %d", (int)submitResult); - - // Wait for completion before returning (runtime needs the image ready) - VkResult waitResult = vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - LOG_INFO("[RenderScene] vkWaitForFences returned %d - DONE", (int)waitResult); + // ONE submit, no wait: the runtime composes on the same VkQueue, so this + // work is ordered ahead of it as long as the image is released after. + VkResult submitResult = renderer.ring.endFrame(); + if (submitResult != VK_SUCCESS) { + LOG_ERROR("[RenderScene] ring.endFrame (vkQueueSubmit) failed: %d", (int)submitResult); + } } void CleanupVkRenderer(VkRenderer& renderer) { if (!renderer.device) return; + renderer.ring.waitAll(); vkDeviceWaitIdle(renderer.device); // Cleanup texture resources @@ -1559,10 +1555,7 @@ void CleanupVkRenderer(VkRenderer& renderer) { } } - if (renderer.frameFence) { - vkDestroyFence(renderer.device, renderer.frameFence, nullptr); - renderer.frameFence = VK_NULL_HANDLE; - } + renderer.ring.destroy(); if (renderer.commandPool) { vkDestroyCommandPool(renderer.device, renderer.commandPool, nullptr); renderer.commandPool = VK_NULL_HANDLE; diff --git a/test_apps/handle/cube_handle_vk_win/vk_renderer.h b/test_apps/handle/cube_handle_vk_win/vk_renderer.h index 86b7faac0d..0f077e5973 100644 --- a/test_apps/handle/cube_handle_vk_win/vk_renderer.h +++ b/test_apps/handle/cube_handle_vk_win/vk_renderer.h @@ -11,6 +11,7 @@ #include #include #include +#include struct VkRenderer { VkDevice device = VK_NULL_HANDLE; @@ -57,12 +58,13 @@ struct VkRenderer { std::vector swapchainImageViews[2]; std::vector framebuffers[2]; - // Command pool and buffers + // Command pool + buffer for the init-time texture upload only. VkCommandPool commandPool = VK_NULL_HANDLE; VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - // Fence for frame synchronization - VkFence frameFence = VK_NULL_HANDLE; + // INV-4.10 / #1917: N frames in flight, one submit per frame, never a wait + // on the frame just submitted. Owns the per-frame command buffers + fences. + dxr::VkFrameRing ring; // ADR-021 / INV-4.6: does this renderer emit SCENE-LINEAR? Latched once, at // InitializeVkRenderer(), from dxr::RenderSceneLinear() -- the pipelines bake diff --git a/test_apps/handle/cube_zones_metal_macos/CMakeLists.txt b/test_apps/handle/cube_zones_metal_macos/CMakeLists.txt index e61fd145bf..3606432b78 100644 --- a/test_apps/handle/cube_zones_metal_macos/CMakeLists.txt +++ b/test_apps/handle/cube_zones_metal_macos/CMakeLists.txt @@ -64,6 +64,8 @@ FetchContent_Declare( # R8G8B8A8 (ADR-044 §7, displayxr-common#72) — the Metal compositor is # format-honest, so a UNORM HUD of CPU-rasterized (encoded) bytes would be # encoded twice. Same pin as the VK macOS cube apps. + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. # v2.29.0: dxr::CursorDepth (common/cursor_depth.h) for the zones cursor (runtime#1949). GIT_TAG v2.29.0 GIT_SHALLOW TRUE diff --git a/test_apps/handle/cube_zones_vk_macos/CMakeLists.txt b/test_apps/handle/cube_zones_vk_macos/CMakeLists.txt index 5edd930431..64c84afc1f 100644 --- a/test_apps/handle/cube_zones_vk_macos/CMakeLists.txt +++ b/test_apps/handle/cube_zones_vk_macos/CMakeLists.txt @@ -70,6 +70,8 @@ FetchContent_Declare( # v2.27.0: window-space / HUD swapchains prefer the _SRGB sibling of # R8G8B8A8 (dxr::ChooseWindowSpaceSwapchainFormat; ADR-044 §7, # displayxr-common#72) — a UNORM HUD of encoded bytes was encoded twice. + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. # v2.29.0: dxr::CursorDepth (common/cursor_depth.h) for the zones cursor (runtime#1949). GIT_TAG v2.29.0 GIT_SHALLOW TRUE diff --git a/test_apps/handle/cube_zones_vk_macos/main.mm b/test_apps/handle/cube_zones_vk_macos/main.mm index 22a2f41c25..c68c095502 100644 --- a/test_apps/handle/cube_zones_vk_macos/main.mm +++ b/test_apps/handle/cube_zones_vk_macos/main.mm @@ -69,6 +69,7 @@ #include "stb_image.h" #include "color_policy.h" +#include // dxr::VkFrameRing — INV-4.10 frame-submit discipline #include "vk_clear.h" // ============================================================================ @@ -881,9 +882,12 @@ static XrQuaternionf quat_multiply(XrQuaternionf a, XrQuaternionf b) { VkBuffer gridVertexBuffer = VK_NULL_HANDLE; VkDeviceMemory gridVertexMemory = VK_NULL_HANDLE; int gridVertexCount = 0; - VkCommandPool commandPool = VK_NULL_HANDLE; - VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - VkFence frameFence = VK_NULL_HANDLE; + VkCommandPool commandPool = VK_NULL_HANDLE; // init-time uploads only + // INV-4.10 / #1917: N frames in flight, ONE vkQueueSubmit per frame (every + // zone of a frame goes into the same command buffer), never a wait on the + // frame just submitted. Owns the per-frame command buffers and fences + // (and the blocking one-off path, submitImmediate()). + dxr::VkFrameRing ring; float cubeRotation = 0.0f; // ADR-021 / INV-4.6: does this renderer emit SCENE-LINEAR? Latched once, at @@ -1317,12 +1321,31 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; + // A framebuffer set's one depth image is shared by every frame, and with + // several frames in flight (VkFrameRing) the previous frame's pass may + // still be writing it: order this pass's attachment writes after any + // earlier ones. The old per-frame fence wait gave this ordering implicitly. + VkSubpassDependency dep = {}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + dep.dstStageMask = dep.srcStageMask; + dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpInfo = {}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpInfo.attachmentCount = 2; rpInfo.pAttachments = attachments; rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; VK_CHECK(vkCreateRenderPass(device, &rpInfo, nullptr, &renderer.renderPass)); // Create second render pass with LOAD instead of CLEAR (for second eye in SBS) @@ -1610,17 +1633,13 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic poolInfo.queueFamilyIndex = queueFamilyIndex; VK_CHECK(vkCreateCommandPool(device, &poolInfo, nullptr, &renderer.commandPool)); - VkCommandBufferAllocateInfo allocInfo = {}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.commandPool = renderer.commandPool; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandBufferCount = 1; - VK_CHECK(vkAllocateCommandBuffers(device, &allocInfo, &renderer.commandBuffer)); - - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - VK_CHECK(vkCreateFence(device, &fenceInfo, nullptr, &renderer.frameFence)); + // Frame ring (default 3 in flight; DXR_VK_FRAMES_IN_FLIGHT overrides). + // Grid / cube MVPs are push constants, so the ring owns no UBO. + if (!renderer.ring.init(device, physDevice, graphicsQueue, queueFamilyIndex, + dxr::VkFrameRing::Config{})) { + LOG_ERROR("VkFrameRing init failed"); + return false; + } // Load textures and create descriptor set { @@ -1713,22 +1732,15 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic // Generalized scene render: cube + grid into any framebuffer set with an // explicit clear color and a per-call rotation offset (XR_DXR_display_zones: // each zone renders into its own swapchain with its own spin phase and -// premultiplied background). +// premultiplied background). RECORDS ONLY, into the ring's open frame — the +// caller brackets every pass of a frame with ring.beginFrame()/endFrame(), so a +// frame is one vkQueueSubmit however many zones it draws (INV-4.10). static void RenderSceneToFramebuffer(VkRenderer& renderer, SwapchainFramebuffers& fb, uint32_t imageIndex, const EyeRenderParams* eyes, int eyeCount, const float clearColor[4], float rotationOffset) { - VkDevice device = renderer.device; - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &renderer.frameFence); - - VkCommandBuffer cmd = renderer.commandBuffer; - vkResetCommandBuffer(cmd, 0); - - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &beginInfo); + VkCommandBuffer cmd = renderer.ring.current(); + if (cmd == VK_NULL_HANDLE) return; VkClearValue clearValues[2] = {}; clearValues[0].color = VkClearColorValue{{clearColor[0], clearColor[1], clearColor[2], clearColor[3]}}; @@ -1814,13 +1826,14 @@ static void RenderSceneToFramebuffer(VkRenderer& renderer, SwapchainFramebuffers } vkCmdEndRenderPass(cmd); - vkEndCommandBuffer(cmd); +} - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cmd; - vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); +// ONE submit for the frame, no wait. Must precede xrReleaseSwapchainImage (the +// runtime composes on the same queue). +static void EndRingFrame(VkRenderer& renderer) +{ + VkResult r = renderer.ring.endFrame(); + if (r != VK_SUCCESS) LOG_WARN("VkFrameRing::endFrame: vkQueueSubmit failed (%d)", (int)r); } static void RenderScene(VkRenderer& renderer, uint32_t imageIndex, @@ -1835,8 +1848,10 @@ static void RenderScene(VkRenderer& renderer, uint32_t imageIndex, }(); const float clearTransparent[4] = {0.0f, 0.0f, 0.0f, 0.0f}; const float clearOpaque[4] = {0.05f, 0.05f, 0.1f, 1.0f}; + if (renderer.ring.beginFrame() == VK_NULL_HANDLE) return; RenderSceneToFramebuffer(renderer, renderer.swapchainFBs, imageIndex, eyes, eyeCount, transparent_bg ? clearTransparent : clearOpaque, 0.0f); + EndRingFrame(renderer); } static void CleanupVkRenderer(VkRenderer& renderer) { @@ -1852,7 +1867,7 @@ static void CleanupVkRenderer(VkRenderer& renderer) { if (fb.depthMemory) vkFreeMemory(renderer.device, fb.depthMemory, nullptr); } - if (renderer.frameFence) vkDestroyFence(renderer.device, renderer.frameFence, nullptr); + renderer.ring.destroy(); // after vkDeviceWaitIdle above; frees its pool/fences if (renderer.commandPool) vkDestroyCommandPool(renderer.device, renderer.commandPool, nullptr); if (renderer.gridVertexBuffer) vkDestroyBuffer(renderer.device, renderer.gridVertexBuffer, nullptr); if (renderer.gridVertexMemory) vkFreeMemory(renderer.device, renderer.gridVertexMemory, nullptr); @@ -3000,7 +3015,7 @@ static bool ReleaseSwapchainImage(AppXrSession& xr) { VkDevice device = VK_NULL_HANDLE; VkPhysicalDevice physDevice = VK_NULL_HANDLE; VkQueue queue = VK_NULL_HANDLE; - VkCommandPool cmdPool = VK_NULL_HANDLE; + dxr::VkFrameRing* ring = nullptr; // uploads go through ring->submitImmediate() VkBuffer stagingBuffer = VK_NULL_HANDLE; VkDeviceMemory stagingMemory = VK_NULL_HANDLE; uint8_t* stagingMapped = nullptr; @@ -3026,13 +3041,13 @@ static bool VkFindMemoryTypeForHud(VkPhysicalDevice phys, uint32_t typeBits, } static bool InitializeHudVkUploader(HudVkUploader& up, - VkDevice device, VkPhysicalDevice phys, VkQueue queue, VkCommandPool pool, + VkDevice device, VkPhysicalDevice phys, VkQueue queue, dxr::VkFrameRing* ring, uint32_t width, uint32_t height) { up.device = device; up.physDevice = phys; up.queue = queue; - up.cmdPool = pool; + up.ring = ring; up.width = width; up.height = height; up.stagingSize = (VkDeviceSize)width * (VkDeviceSize)height * 4; @@ -3086,54 +3101,38 @@ static bool UploadHudPixels(HudVkUploader& up, VkImage hudImage, } } - VkCommandBufferAllocateInfo ca = {}; - ca.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - ca.commandPool = up.cmdPool; - ca.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - ca.commandBufferCount = 1; - VkCommandBuffer cmd = VK_NULL_HANDLE; - if (vkAllocateCommandBuffers(up.device, &ca, &cmd) != VK_SUCCESS) return false; - - VkCommandBufferBeginInfo bi = {}; - bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); - - VkImageMemoryBarrier b = {}; - b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - b.image = hudImage; - b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - b.srcAccessMask = 0; - b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &b); - - VkBufferImageCopy region = {}; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - region.imageExtent = {up.width, up.height, 1}; - vkCmdCopyBufferToImage(cmd, up.stagingBuffer, hudImage, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - - b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &b); - - vkEndCommandBuffer(cmd); - VkSubmitInfo si = {}; - si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - vkQueueSubmit(up.queue, 1, &si, VK_NULL_HANDLE); - vkQueueWaitIdle(up.queue); - vkFreeCommandBuffers(up.device, up.cmdPool, 1, &cmd); - return true; + // A HUD upload is a one-off (only when the HUD text changes — see the + // caller), so it takes the ring's one sanctioned blocking path: its own + // command buffer + fence, allocated once. The wait also keeps the single + // staging buffer safe to rewrite next time. + VkResult r = up.ring->submitImmediate([&](VkCommandBuffer cmd) { + VkImageMemoryBarrier b = {}; + b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.image = hudImage; + b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + b.srcAccessMask = 0; + b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &b); + + VkBufferImageCopy region = {}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {up.width, up.height, 1}; + vkCmdCopyBufferToImage(cmd, up.stagingBuffer, hudImage, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &b); + }); + return r == VK_SUCCESS; } static bool EndFrame(AppXrSession& xr, XrTime displayTime, @@ -3513,6 +3512,11 @@ static void RenderZonesFrame(AppXrSession& xr, VkRenderer& renderer, const XrFra g_zoneCursor.begin((int32_t)g_windowW, (int32_t)g_windowH, rects, rots, kNumZones); } + // INV-4.10: every zone's pass is recorded into ONE ring frame and submitted + // once; each zone swapchain is released only after that submit. + bool zoneAcquired[kNumZones] = {}; + renderer.ring.beginFrame(); + for (uint32_t zi = 0; zi < kNumZones; zi++) { DisplayZone& z = g_zonesArr[zi]; @@ -3605,9 +3609,14 @@ static void RenderZonesFrame(AppXrSession& xr, VkRenderer& renderer, const XrFra RenderSceneToFramebuffer(renderer, g_zoneFbs[zi], imageIndex, eyes.data(), (int)n, z.clearColor, z.spinPhase); + zoneAcquired[zi] = true; + } + EndRingFrame(renderer); + for (uint32_t zi = 0; zi < kNumZones; zi++) { + if (!zoneAcquired[zi]) continue; XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; - xrReleaseSwapchainImage(z.swapchain, &ri); + xrReleaseSwapchainImage(g_zonesArr[zi].swapchain, &ri); } g_zoneCursor.end(); // pointer over no 3D zone: the OS cursor is the cursor @@ -3811,6 +3820,7 @@ int main() { VkRenderer vkRenderer = {}; if (!InitializeVkRenderer(vkRenderer, vkDevice, physDevice, graphicsQueue, queueFamilyIndex, colorFormat)) { CleanupOpenXR(xr); + vkRenderer.ring.destroy(); // a partial init may have created it; free before the device vkDestroyDevice(vkDevice, nullptr); vkDestroyInstance(vkInstance, nullptr); return 1; @@ -3852,7 +3862,7 @@ int main() { } if (InitializeHudRenderer(hudRenderer, HUD_PIXEL_WIDTH, HUD_PIXEL_HEIGHT) && InitializeHudVkUploader(hudUploader, vkDevice, physDevice, - graphicsQueue, vkRenderer.commandPool, + graphicsQueue, &vkRenderer.ring, HUD_PIXEL_WIDTH, HUD_PIXEL_HEIGHT)) { hudReady = true; LOG_INFO("HUD window-space layer ready (%ux%u, %u images, format %lld)", @@ -4240,10 +4250,23 @@ int main() { } // Render HUD into the window-space layer swapchain (when visible). + // INV-4.10: re-render + upload only when the HUD text changed. + // Otherwise the layer keeps showing the swapchain's last + // released image (OpenXR composes the most recently released + // one), so an unchanged HUD costs no acquire, no upload and no + // blocking submit. bool hudSubmitted = false; if (hudReady && g_input.hudVisible && rendered && frameState.shouldRender) { + static std::wstring s_hudUploadedKey; + static bool s_hudHasImage = false; + std::wstring hudKey = g_hudSessionText + L'\x1f' + g_hudModeText + L'\x1f' + + g_hudPerfText + L'\x1f' + g_hudDisplayText + L'\x1f' + g_hudEyeText + + L'\x1f' + g_hudCameraText + L'\x1f' + g_hudStereoText + L'\x1f' + + g_hudHelpText; uint32_t hudIndex = 0; - if (AcquireHudSwapchainImage(hudSwapchain, hudIndex)) { + if (s_hudHasImage && hudKey == s_hudUploadedKey) { + hudSubmitted = true; + } else if (AcquireHudSwapchainImage(hudSwapchain, hudIndex)) { uint32_t rowPitch = 0; const void* pixels = RenderHudAndMap(hudRenderer, &rowPitch, g_hudSessionText, g_hudModeText, g_hudPerfText, g_hudDisplayText, @@ -4251,6 +4274,8 @@ int main() { if (pixels) { if (UploadHudPixels(hudUploader, hudVkImages[hudIndex], pixels, rowPitch)) { hudSubmitted = true; + s_hudHasImage = true; + s_hudUploadedKey = std::move(hudKey); } UnmapHud(hudRenderer); } @@ -4407,8 +4432,8 @@ int main() { LOG_INFO("=== Shutting down ==="); if (hudReady) { - // HUD uploader uses vkRenderer.commandPool — clean up before - // CleanupVkRenderer destroys the pool. + // HUD uploader submits through vkRenderer.ring — clean up before + // CleanupVkRenderer destroys it. CleanupHudVkUploader(hudUploader); CleanupHudRenderer(hudRenderer); } diff --git a/test_apps/handle/cube_zones_vk_win/main.cpp b/test_apps/handle/cube_zones_vk_win/main.cpp index 8430cea214..2dc64a0f6c 100644 --- a/test_apps/handle/cube_zones_vk_win/main.cpp +++ b/test_apps/handle/cube_zones_vk_win/main.cpp @@ -527,17 +527,8 @@ static bool UploadStripPixels(VkRenderer& renderer, VkImage image, uint32_t w, u memcpy(mapped, pixels, size); vkUnmapMemory(renderer.device, stagingMem); - VkCommandBufferAllocateInfo cai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; - cai.commandPool = renderer.commandPool; - cai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - cai.commandBufferCount = 1; - VkCommandBuffer cmd = VK_NULL_HANDLE; - bool ok = vkAllocateCommandBuffers(renderer.device, &cai, &cmd) == VK_SUCCESS; - if (ok) { - VkCommandBufferBeginInfo cbi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; - cbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &cbi); - + // One-off (filled once at activation): the ring's sanctioned blocking path. + const bool ok = renderer.ring.submitImmediate([&](VkCommandBuffer cmd) { VkImageMemoryBarrier toDst = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; toDst.srcAccessMask = 0; toDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -564,18 +555,7 @@ static bool UploadStripPixels(VkRenderer& renderer, VkImage image, uint32_t w, u vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toRead); - - vkEndCommandBuffer(cmd); - - VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - ok = vkQueueSubmit(renderer.graphicsQueue, 1, &si, VK_NULL_HANDLE) == VK_SUCCESS; - if (ok) { - vkQueueWaitIdle(renderer.graphicsQueue); - } - vkFreeCommandBuffers(renderer.device, renderer.commandPool, 1, &cmd); - } + }) == VK_SUCCESS; vkFreeMemory(renderer.device, stagingMem, nullptr); vkDestroyBuffer(renderer.device, staging, nullptr); @@ -769,6 +749,13 @@ static void RenderZonesFrame(XrSessionManager& xr, VkRenderer& renderer, uint32_t submitViewCounts[kNumZones] = {}; // ADR-041: what each zone LAYER carries (>= submitViewCounts[zi]). uint32_t locatedViewCounts[kNumZones] = {}; + // Zone swapchains acquired this frame; released only after the ONE ring + // submit below (INV-4.10: every zone's rendering shares one submit). + bool acquired[kNumZones] = {}; + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + LOG_ERROR("[zones] ring.beginFrame failed"); + } for (uint32_t zi = 0; zi < kNumZones; zi++) { DisplayZone& z = g_zonesArr[zi]; @@ -859,10 +846,11 @@ static void RenderZonesFrame(XrSessionManager& xr, VkRenderer& renderer, // Acquire this zone's array swapchain image. XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; uint32_t imageIndex = 0; - if (XR_FAILED(xrAcquireSwapchainImage(z.swapchain, &ai, &imageIndex))) { + if (cmd == VK_NULL_HANDLE || XR_FAILED(xrAcquireSwapchainImage(z.swapchain, &ai, &imageIndex))) { submitViewCounts[zi] = 0; continue; } + acquired[zi] = true; XrSwapchainImageWaitInfo wi = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; wi.timeout = XR_INFINITE_DURATION; xrWaitSwapchainImage(z.swapchain, &wi); @@ -877,7 +865,7 @@ static void RenderZonesFrame(XrSessionManager& xr, VkRenderer& renderer, ep.viewMatrix = ColumnMajorToXMMatrix(rigViews[vi].view_matrix); ep.projMatrix = ColumnMajorToXMMatrix(rigViews[vi].projection_matrix); - RenderSceneToZoneLayer(renderer, z.fb, imageIndex, vi, ep, z.clearColor, z.spinPhase); + RenderSceneToZoneLayer(renderer, cmd, z.fb, imageIndex, vi, ep, z.clearColor, z.spinPhase); projViews[zi][vi].subImage.swapchain = z.swapchain; projViews[zi][vi].subImage.imageRect.offset = {0, 0}; @@ -890,9 +878,20 @@ static void RenderZonesFrame(XrSessionManager& xr, VkRenderer& renderer, // ADR-041: this zone rendered only `n` views — point the inactive tail // at this zone's view-0 layer so the layer can carry `locatedCount`. DxrAliasInactiveViews(projViews[zi].data(), zoneViews, locatedCount, n); + } + // ONE submit for every zone layer, no wait; then release the zone images + // (the runtime composes on the same VkQueue, so release AFTER the submit). + if (cmd != VK_NULL_HANDLE) { + const VkResult sr = renderer.ring.endFrame(); + if (sr != VK_SUCCESS) { + LOG_ERROR("[zones] ring.endFrame (vkQueueSubmit) failed: %d", (int)sr); + } + } + for (uint32_t zi = 0; zi < kNumZones; zi++) { + if (!acquired[zi]) continue; XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; - xrReleaseSwapchainImage(z.swapchain, &ri); + xrReleaseSwapchainImage(g_zonesArr[zi].swapchain, &ri); } // Layer list: [projA (zone A chained), projB (zone B chained)]. @@ -1176,6 +1175,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine LOG_INFO("Main thread: render thread joined"); LOG_INFO("=== Shutting down ==="); + vkRenderer.ring.waitAll(); if (vkDevice != VK_NULL_HANDLE) vkDeviceWaitIdle(vkDevice); for (uint32_t zi = 0; zi < kNumZones; zi++) { DestroyZoneArrayFramebuffers(vkRenderer, g_zonesArr[zi].fb); diff --git a/test_apps/handle/cube_zones_vk_win/vk_renderer.cpp b/test_apps/handle/cube_zones_vk_win/vk_renderer.cpp index 00f21a021c..f938666e07 100644 --- a/test_apps/handle/cube_zones_vk_win/vk_renderer.cpp +++ b/test_apps/handle/cube_zones_vk_win/vk_renderer.cpp @@ -586,12 +586,27 @@ bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysicalDevic subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; + // With several frames in flight (dxr::VkFrameRing) the per-eye depth + // image is shared by consecutive frames: order this frame's depth + // clear/writes after the previous frame's (WAW), and colour likewise. + VkSubpassDependency dep = {}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstStageMask = dep.srcStageMask; + dep.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpInfo = {}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpInfo.attachmentCount = 2; rpInfo.pAttachments = attachments; rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderer.renderPass) != VK_SUCCESS) { LOG_ERROR("Failed to create render pass"); @@ -1214,16 +1229,10 @@ bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysicalDevic renderer.texturesLoaded = true; } - // Frame fence - { - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - - if (vkCreateFence(device, &fenceInfo, nullptr, &renderer.frameFence) != VK_SUCCESS) { - LOG_ERROR("Failed to create fence"); - return false; - } + // Frame ring (INV-4.10 / #1917): per-frame command buffers + fences. + if (!renderer.ring.init(device, physDevice, queue, queueFamilyIndex, dxr::VkFrameRing::Config{})) { + LOG_ERROR("Failed to create frame ring"); + return false; } LOG_INFO("Vulkan renderer initialized"); @@ -1343,20 +1352,13 @@ void RenderScene( const EyeRenderParams* eyes, int eyeCount, float zoomScale) { - VkDevice device = renderer.device; - - // Wait for previous frame's fence - LOG_INFO("[RenderScene] imageIndex=%u: vkWaitForFences (pre-render)...", imageIndex); - VkResult fenceResult = vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - LOG_INFO("[RenderScene] vkWaitForFences returned %d", (int)fenceResult); - vkResetFences(device, 1, &renderer.frameFence); - - // Begin command buffer - vkResetCommandBuffer(renderer.commandBuffer, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(renderer.commandBuffer, &beginInfo); + // Begin this frame's ring slot (waits only the fence of the frame that + // used this slot N frames ago). + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + LOG_ERROR("[RenderScene] imageIndex=%u: ring.beginFrame failed", imageIndex); + return; + } // Single render pass with CLEAR covering full framebuffer. // Transparent-background mode (DISPLAYXR_TRANSPARENT_BG=1): clear to @@ -1399,7 +1401,7 @@ void RenderScene( rpBegin.pClearValues = clearValues; __try { - vkCmdBeginRenderPass(renderer.commandBuffer, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); } __except(EXCEPTION_EXECUTE_HANDLER) { LOG_ERROR("[RenderScene] CRASH in vkCmdBeginRenderPass! exception=0x%08X", GetExceptionCode()); return; @@ -1425,12 +1427,12 @@ void RenderScene( viewport.height = -(float)h; viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; - vkCmdSetViewport(renderer.commandBuffer, 0, 1, &viewport); + vkCmdSetViewport(cmd, 0, 1, &viewport); VkRect2D scissor = {}; scissor.offset = {(int32_t)vpX, (int32_t)vpY}; scissor.extent = {w, h}; - vkCmdSetScissor(renderer.commandBuffer, 0, 1, &scissor); + vkCmdSetScissor(cmd, 0, 1, &scissor); // Draw cube - base rests on grid at y=0 { @@ -1446,19 +1448,19 @@ void RenderScene( XMMATRIX cubeModel = cubeRot * cubeScale; XMStoreFloat4x4(&pc.model, cubeModel); - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.cubePipelineLayout, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipeline); + vkCmdPushConstants(cmd, renderer.cubePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc); if (renderer.texturesLoaded) { - vkCmdBindDescriptorSets(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipelineLayout, 0, 1, &renderer.descriptorSet, 0, nullptr); } VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(renderer.commandBuffer, 0, 1, &renderer.cubeVertexBuffer, &offset); - vkCmdBindIndexBuffer(renderer.commandBuffer, renderer.cubeIndexBuffer, 0, VK_INDEX_TYPE_UINT16); - vkCmdDrawIndexed(renderer.commandBuffer, 36, 1, 0, 0, 0); + vkCmdBindVertexBuffers(cmd, 0, 1, &renderer.cubeVertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, renderer.cubeIndexBuffer, 0, VK_INDEX_TYPE_UINT16); + vkCmdDrawIndexed(cmd, 36, 1, 0, 0, 0); } // Draw grid floor @@ -1472,36 +1474,30 @@ void RenderScene( XMStoreFloat4x4(&pc.transform, gridWVP); pc.color[0] = 0.3f; pc.color[1] = 0.3f; pc.color[2] = 0.35f; pc.color[3] = 1.0f; - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.gridPipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.pipelineLayout, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.gridPipeline); + vkCmdPushConstants(cmd, renderer.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(renderer.commandBuffer, 0, 1, &renderer.gridVertexBuffer, &offset); - vkCmdDraw(renderer.commandBuffer, renderer.gridVertexCount, 1, 0, 0); + vkCmdBindVertexBuffers(cmd, 0, 1, &renderer.gridVertexBuffer, &offset); + vkCmdDraw(cmd, renderer.gridVertexCount, 1, 0, 0); } } - vkCmdEndRenderPass(renderer.commandBuffer); - vkEndCommandBuffer(renderer.commandBuffer); - - // Submit - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &renderer.commandBuffer; - - VkResult submitResult = vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - LOG_INFO("[RenderScene] vkQueueSubmit returned %d", (int)submitResult); + vkCmdEndRenderPass(cmd); - // Wait for completion before returning (runtime needs the image ready) - VkResult waitResult = vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - LOG_INFO("[RenderScene] vkWaitForFences returned %d - DONE", (int)waitResult); + // ONE submit, no wait: the runtime composes on the same VkQueue, so this + // work is ordered ahead of it as long as the image is released after. + VkResult submitResult = renderer.ring.endFrame(); + if (submitResult != VK_SUCCESS) { + LOG_ERROR("[RenderScene] ring.endFrame (vkQueueSubmit) failed: %d", (int)submitResult); + } } void CleanupVkRenderer(VkRenderer& renderer) { if (!renderer.device) return; + renderer.ring.waitAll(); vkDeviceWaitIdle(renderer.device); // Cleanup texture resources @@ -1559,10 +1555,7 @@ void CleanupVkRenderer(VkRenderer& renderer) { } } - if (renderer.frameFence) { - vkDestroyFence(renderer.device, renderer.frameFence, nullptr); - renderer.frameFence = VK_NULL_HANDLE; - } + renderer.ring.destroy(); if (renderer.commandPool) { vkDestroyCommandPool(renderer.device, renderer.commandPool, nullptr); renderer.commandPool = VK_NULL_HANDLE; @@ -1628,8 +1621,10 @@ bool CreateZoneArrayFramebuffers(VkRenderer& renderer, ZoneArrayFramebuffers& fb fb.framebuffers.resize((size_t)imageCount * layerCount); // One shared, single-layer depth image (per-view tile sized). The render - // pass CLEARs it at the start of every layer submission, so serial reuse - // across (image, layer) is safe. + // pass CLEARs it at the start of every layer pass, and its + // VK_SUBPASS_EXTERNAL dependency orders those depth writes after the + // previous pass's — including passes of earlier, still in-flight frames + // (VkFrameRing) — so reuse across (image, layer) is safe. { VkImageCreateInfo imageInfo = {}; imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; @@ -1719,20 +1714,14 @@ bool CreateZoneArrayFramebuffers(VkRenderer& renderer, ZoneArrayFramebuffers& fb return true; } -void RenderSceneToZoneLayer(VkRenderer& renderer, ZoneArrayFramebuffers& fb, +void RenderSceneToZoneLayer(VkRenderer& renderer, VkCommandBuffer cmd, ZoneArrayFramebuffers& fb, uint32_t imageIndex, uint32_t layer, const EyeRenderParams& eye, const float clearColor[4], float rotationOffset) { - VkDevice device = renderer.device; - - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &renderer.frameFence); - - vkResetCommandBuffer(renderer.commandBuffer, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(renderer.commandBuffer, &beginInfo); + // Records into the frame's ring command buffer (the caller owns + // beginFrame/endFrame); every zone layer of the frame shares ONE submit. + // The shared depth image is ordered between layers by the render pass's + // EXTERNAL subpass dependency. // Per-zone premultiplied clear (zone A opaque tint; zone B fully transparent // -> desktop show-through; matches the D3D11/D3D12/macOS zones apps). @@ -1762,7 +1751,7 @@ void RenderSceneToZoneLayer(VkRenderer& renderer, ZoneArrayFramebuffers& fb, rpBegin.renderArea.extent = {fb.width, fb.height}; rpBegin.clearValueCount = 2; rpBegin.pClearValues = clearValues; - vkCmdBeginRenderPass(renderer.commandBuffer, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); const float zoneRotation = renderer.cubeRotation + rotationOffset; @@ -1779,12 +1768,12 @@ void RenderSceneToZoneLayer(VkRenderer& renderer, ZoneArrayFramebuffers& fb, viewport.height = -(float)h; viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; - vkCmdSetViewport(renderer.commandBuffer, 0, 1, &viewport); + vkCmdSetViewport(cmd, 0, 1, &viewport); VkRect2D scissor = {}; scissor.offset = {0, 0}; scissor.extent = {w, h}; - vkCmdSetScissor(renderer.commandBuffer, 0, 1, &scissor); + vkCmdSetScissor(cmd, 0, 1, &scissor); // Draw cube - base rests on grid at y=0 (per-zone spin phase applied). { @@ -1800,17 +1789,17 @@ void RenderSceneToZoneLayer(VkRenderer& renderer, ZoneArrayFramebuffers& fb, XMMATRIX cubeModel = cubeRot * cubeScale; XMStoreFloat4x4(&pc.model, cubeModel); - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.cubePipelineLayout, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipeline); + vkCmdPushConstants(cmd, renderer.cubePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc); if (renderer.texturesLoaded) { - vkCmdBindDescriptorSets(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipelineLayout, 0, 1, &renderer.descriptorSet, 0, nullptr); } VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(renderer.commandBuffer, 0, 1, &renderer.cubeVertexBuffer, &offset); - vkCmdBindIndexBuffer(renderer.commandBuffer, renderer.cubeIndexBuffer, 0, VK_INDEX_TYPE_UINT16); - vkCmdDrawIndexed(renderer.commandBuffer, 36, 1, 0, 0, 0); + vkCmdBindVertexBuffers(cmd, 0, 1, &renderer.cubeVertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, renderer.cubeIndexBuffer, 0, VK_INDEX_TYPE_UINT16); + vkCmdDrawIndexed(cmd, 36, 1, 0, 0, 0); } // Draw grid floor. @@ -1824,26 +1813,15 @@ void RenderSceneToZoneLayer(VkRenderer& renderer, ZoneArrayFramebuffers& fb, XMStoreFloat4x4(&pc.transform, gridWVP); pc.color[0] = 0.3f; pc.color[1] = 0.3f; pc.color[2] = 0.35f; pc.color[3] = 1.0f; - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.gridPipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.pipelineLayout, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.gridPipeline); + vkCmdPushConstants(cmd, renderer.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(renderer.commandBuffer, 0, 1, &renderer.gridVertexBuffer, &offset); - vkCmdDraw(renderer.commandBuffer, renderer.gridVertexCount, 1, 0, 0); + vkCmdBindVertexBuffers(cmd, 0, 1, &renderer.gridVertexBuffer, &offset); + vkCmdDraw(cmd, renderer.gridVertexCount, 1, 0, 0); } - vkCmdEndRenderPass(renderer.commandBuffer); - vkEndCommandBuffer(renderer.commandBuffer); - - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &renderer.commandBuffer; - vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - - // Wait for completion before returning - the runtime needs the zone image - // ready when the projection layer referencing it is submitted in xrEndFrame. - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); + vkCmdEndRenderPass(cmd); } void DestroyZoneArrayFramebuffers(VkRenderer& renderer, ZoneArrayFramebuffers& fb) { diff --git a/test_apps/handle/cube_zones_vk_win/vk_renderer.h b/test_apps/handle/cube_zones_vk_win/vk_renderer.h index 11f6d8f4c7..e54b63f3dc 100644 --- a/test_apps/handle/cube_zones_vk_win/vk_renderer.h +++ b/test_apps/handle/cube_zones_vk_win/vk_renderer.h @@ -19,6 +19,7 @@ #include #include #include +#include struct VkRenderer { VkDevice device = VK_NULL_HANDLE; @@ -65,12 +66,13 @@ struct VkRenderer { std::vector swapchainImageViews[2]; std::vector framebuffers[2]; - // Command pool and buffers + // Command pool + buffer for the init-time texture upload only. VkCommandPool commandPool = VK_NULL_HANDLE; VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - // Fence for frame synchronization - VkFence frameFence = VK_NULL_HANDLE; + // INV-4.10 / #1917: N frames in flight, one submit per frame, never a wait + // on the frame just submitted. Owns the per-frame command buffers + fences. + dxr::VkFrameRing ring; // ADR-021 / INV-4.6: does this renderer emit SCENE-LINEAR? Latched once, at // InitializeVkRenderer(), from dxr::RenderSceneLinear() -- the pipelines bake @@ -148,7 +150,8 @@ struct ZoneArrayFramebuffers { std::vector colorViews; std::vector framebuffers; // Single shared depth image/view (per-view tile sized); the render pass - // CLEARs it at the start of every layer submission, so serial reuse is safe. + // CLEARs it at the start of every layer, and its EXTERNAL subpass + // dependency orders that clear after the previous layer's/frame's writes. VkImage depthImage = VK_NULL_HANDLE; VkDeviceMemory depthMemory = VK_NULL_HANDLE; VkImageView depthView = VK_NULL_HANDLE; @@ -164,7 +167,8 @@ bool CreateZoneArrayFramebuffers(VkRenderer& renderer, ZoneArrayFramebuffers& fb // Render the cube + grid scene for ONE view into (imageIndex, layer) with an // explicit premultiplied clear color and a per-zone rotation offset (added to // renderer.cubeRotation for this submission only). Full-viewport into the tile. -void RenderSceneToZoneLayer(VkRenderer& renderer, ZoneArrayFramebuffers& fb, +// Records into @p cmd (the frame's renderer.ring command buffer); no submit. +void RenderSceneToZoneLayer(VkRenderer& renderer, VkCommandBuffer cmd, ZoneArrayFramebuffers& fb, uint32_t imageIndex, uint32_t layer, const EyeRenderParams& eye, const float clearColor[4], float rotationOffset); diff --git a/test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt b/test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt index a32b4d152c..7dbaed2abc 100644 --- a/test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt +++ b/test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt @@ -62,7 +62,10 @@ FetchContent_Declare( # R8G8B8A8 (ADR-044 §7, displayxr-common#72) — the Metal compositor is # format-honest, so a UNORM HUD of CPU-rasterized (encoded) bytes would be # encoded twice. Same pin as the VK macOS cube apps. - GIT_TAG v2.27.0 + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. + # v2.29.0: dxr::CursorDepth (common/cursor_depth.h, ADR-046 Amendment 2). + GIT_TAG v2.29.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) diff --git a/test_apps/legacy/cube_hosted_legacy_metal_macos/CMakeLists.txt b/test_apps/legacy/cube_hosted_legacy_metal_macos/CMakeLists.txt index ae8ea1c471..b5ebcd2eb6 100644 --- a/test_apps/legacy/cube_hosted_legacy_metal_macos/CMakeLists.txt +++ b/test_apps/legacy/cube_hosted_legacy_metal_macos/CMakeLists.txt @@ -62,7 +62,10 @@ FetchContent_Declare( # R8G8B8A8 (ADR-044 §7, displayxr-common#72) — the Metal compositor is # format-honest, so a UNORM HUD of CPU-rasterized (encoded) bytes would be # encoded twice. Same pin as the VK macOS cube apps. - GIT_TAG v2.27.0 + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. + # v2.29.0: dxr::CursorDepth (common/cursor_depth.h, ADR-046 Amendment 2). + GIT_TAG v2.29.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) diff --git a/test_apps/legacy/cube_hosted_legacy_vk_linux/CMakeLists.txt b/test_apps/legacy/cube_hosted_legacy_vk_linux/CMakeLists.txt index 76cb754e6b..6c78b67c9a 100644 --- a/test_apps/legacy/cube_hosted_legacy_vk_linux/CMakeLists.txt +++ b/test_apps/legacy/cube_hosted_legacy_vk_linux/CMakeLists.txt @@ -71,7 +71,10 @@ FetchContent_Declare( # v2.27.0: window-space / HUD swapchains prefer the _SRGB sibling of # R8G8B8A8 (dxr::ChooseWindowSpaceSwapchainFormat; ADR-044 §7, # displayxr-common#72) — a UNORM HUD of encoded bytes was encoded twice. - GIT_TAG v2.27.0 + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. + # v2.29.0: dxr::CursorDepth (common/cursor_depth.h, ADR-046 Amendment 2). + GIT_TAG v2.29.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) diff --git a/test_apps/legacy/cube_hosted_legacy_vk_linux/main.cpp b/test_apps/legacy/cube_hosted_legacy_vk_linux/main.cpp index 6e6fd2cccc..603b9b95f6 100644 --- a/test_apps/legacy/cube_hosted_legacy_vk_linux/main.cpp +++ b/test_apps/legacy/cube_hosted_legacy_vk_linux/main.cpp @@ -16,6 +16,7 @@ */ #include +#include #define XR_USE_GRAPHICS_API_VULKAN #include @@ -890,9 +891,10 @@ struct VkRenderer { uint32_t fbWidth = 0; uint32_t fbHeight = 0; - VkCommandPool commandPool = VK_NULL_HANDLE; - VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - VkFence frameFence = VK_NULL_HANDLE; + VkCommandPool commandPool = VK_NULL_HANDLE; // init-time texture uploads only + // INV-4.10 / #1917: N frames in flight, one submit per frame, never a wait + // on the frame just submitted (replaces the old single cmd + frameFence). + dxr::VkFrameRing ring; float cubeRotation = 0.0f; @@ -992,12 +994,32 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; + // The single depth image is shared by every frame; with N frames in + // flight (INV-4.10) the next frame's depth clear must wait for the + // previous frame's depth writes (WAW), and the colour layout + // transition must wait for colour output. + VkSubpassDependency dep = {}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpInfo = {}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpInfo.attachmentCount = 2; rpInfo.pAttachments = attachments; rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderer.renderPass) != VK_SUCCESS) { LOG_ERROR("Failed to create render pass"); @@ -1353,7 +1375,7 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic memcpy(data, gridVerts.data(), (size_t)gridBufSize); vkUnmapMemory(device, renderer.gridVertexMemory); - // Command pool + command buffer + // Command pool (init-time texture uploads) { VkCommandPoolCreateInfo poolInfo = {}; poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; @@ -1364,27 +1386,13 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic LOG_ERROR("Failed to create command pool"); return false; } - - VkCommandBufferAllocateInfo allocInfo = {}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.commandPool = renderer.commandPool; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandBufferCount = 1; - - if (vkAllocateCommandBuffers(device, &allocInfo, &renderer.commandBuffer) != VK_SUCCESS) { - LOG_ERROR("Failed to allocate command buffer"); - return false; - } } - // Frame fence + // Frame ring (per-frame command buffers + fences) { - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - - if (vkCreateFence(device, &fenceInfo, nullptr, &renderer.frameFence) != VK_SUCCESS) { - LOG_ERROR("Failed to create fence"); + dxr::VkFrameRing::Config ringCfg; // default 3 frames in flight; push constants, no UBO + if (!renderer.ring.init(device, physDevice, queue, queueFamilyIndex, ringCfg)) { + LOG_ERROR("Failed to create frame ring"); return false; } } @@ -1578,19 +1586,12 @@ static void RenderScene( VkRenderer& renderer, uint32_t imageIndex, const EyeRenderParams* eyes, int eyeCount) { - VkDevice device = renderer.device; - - // Wait for previous frame - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &renderer.frameFence); - - // Begin command buffer - VkCommandBuffer cmd = renderer.commandBuffer; - vkResetCommandBuffer(cmd, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &beginInfo); + // Waits only the slot used N frames ago (INV-4.10), resets + begins its cmd + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + LOG_ERROR("Frame ring beginFrame failed"); + return; + } // Begin render pass with full SBS framebuffer VkClearValue clearValues[2] = {}; @@ -1694,23 +1695,20 @@ static void RenderScene( } vkCmdEndRenderPass(cmd); - vkEndCommandBuffer(cmd); - // Submit - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cmd; - - vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - - // Wait for completion before returning (runtime needs the image ready) - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); + // ONE submit with the slot fence, no wait. The caller releases the + // swapchain image after this; the runtime composes on the same queue, so + // this work is ordered ahead of it. + VkResult submitResult = renderer.ring.endFrame(); + if (submitResult != VK_SUCCESS) { + LOG_ERROR("Frame submit failed: %d", (int)submitResult); + } } static void CleanupVkRenderer(VkRenderer& renderer) { if (!renderer.device) return; + renderer.ring.waitAll(); vkDeviceWaitIdle(renderer.device); for (auto fb : renderer.framebuffers) @@ -1734,10 +1732,7 @@ static void CleanupVkRenderer(VkRenderer& renderer) { renderer.depthMemory = VK_NULL_HANDLE; } - if (renderer.frameFence) { - vkDestroyFence(renderer.device, renderer.frameFence, nullptr); - renderer.frameFence = VK_NULL_HANDLE; - } + renderer.ring.destroy(); if (renderer.commandPool) { vkDestroyCommandPool(renderer.device, renderer.commandPool, nullptr); renderer.commandPool = VK_NULL_HANDLE; @@ -2417,6 +2412,7 @@ int main() { VkRenderer vkRenderer = {}; if (!InitializeVkRenderer(vkRenderer, vkDevice, physDevice, graphicsQueue, queueFamilyIndex, colorFormat)) { LOG_ERROR("Vulkan renderer initialization failed"); + vkRenderer.ring.destroy(); // before vkDestroyDevice (dtor would run after) CleanupOpenXR(xr); vkDestroyDevice(vkDevice, nullptr); vkDestroyInstance(vkInstance, nullptr); diff --git a/test_apps/legacy/cube_hosted_legacy_vk_macos/CMakeLists.txt b/test_apps/legacy/cube_hosted_legacy_vk_macos/CMakeLists.txt index ed3851dbcb..9420c000c9 100644 --- a/test_apps/legacy/cube_hosted_legacy_vk_macos/CMakeLists.txt +++ b/test_apps/legacy/cube_hosted_legacy_vk_macos/CMakeLists.txt @@ -68,7 +68,10 @@ FetchContent_Declare( # v2.27.0: window-space / HUD swapchains prefer the _SRGB sibling of # R8G8B8A8 (dxr::ChooseWindowSpaceSwapchainFormat; ADR-044 §7, # displayxr-common#72) — a UNORM HUD of encoded bytes was encoded twice. - GIT_TAG v2.27.0 + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. + # v2.29.0: dxr::CursorDepth (common/cursor_depth.h, ADR-046 Amendment 2). + GIT_TAG v2.29.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) diff --git a/test_apps/legacy/cube_hosted_legacy_vk_macos/main.cpp b/test_apps/legacy/cube_hosted_legacy_vk_macos/main.cpp index 6511b40b7f..59bee67fa2 100644 --- a/test_apps/legacy/cube_hosted_legacy_vk_macos/main.cpp +++ b/test_apps/legacy/cube_hosted_legacy_vk_macos/main.cpp @@ -45,6 +45,7 @@ #include "stb_image.h" #include "color_policy.h" +#include // dxr::VkFrameRing — INV-4.10 frame-submit discipline // ============================================================================ // Logging @@ -890,9 +891,10 @@ struct VkRenderer { uint32_t fbWidth = 0; uint32_t fbHeight = 0; - VkCommandPool commandPool = VK_NULL_HANDLE; - VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - VkFence frameFence = VK_NULL_HANDLE; + VkCommandPool commandPool = VK_NULL_HANDLE; // init-time uploads only + // INV-4.10 / #1917: N frames in flight, ONE vkQueueSubmit per frame, never + // a wait on the frame just submitted (owns the per-frame cmd buffers/fences). + dxr::VkFrameRing ring; float cubeRotation = 0.0f; @@ -999,6 +1001,25 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; + // The one depth image is shared by every frame, and with several frames + // in flight (VkFrameRing) the previous frame's pass may still be writing + // it: order this pass's attachment writes after any earlier ones. The + // old per-frame fence wait gave this ordering implicitly. + VkSubpassDependency dep = {}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + dep.dstStageMask = dep.srcStageMask; + dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; + if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderer.renderPass) != VK_SUCCESS) { LOG_ERROR("Failed to create render pass"); return false; @@ -1353,7 +1374,7 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic memcpy(data, gridVerts.data(), (size_t)gridBufSize); vkUnmapMemory(device, renderer.gridVertexMemory); - // Command pool + command buffer + // Command pool (init-time uploads) + frame ring { VkCommandPoolCreateInfo poolInfo = {}; poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; @@ -1365,26 +1386,11 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic return false; } - VkCommandBufferAllocateInfo allocInfo = {}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.commandPool = renderer.commandPool; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandBufferCount = 1; - - if (vkAllocateCommandBuffers(device, &allocInfo, &renderer.commandBuffer) != VK_SUCCESS) { - LOG_ERROR("Failed to allocate command buffer"); - return false; - } - } - - // Frame fence - { - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - - if (vkCreateFence(device, &fenceInfo, nullptr, &renderer.frameFence) != VK_SUCCESS) { - LOG_ERROR("Failed to create fence"); + // Default 3 in flight (DXR_VK_FRAMES_IN_FLIGHT overrides). MVPs are + // push constants, so the ring owns no UBO. + if (!renderer.ring.init(device, physDevice, queue, queueFamilyIndex, + dxr::VkFrameRing::Config{})) { + LOG_ERROR("Failed to create the Vulkan frame ring"); return false; } } @@ -1578,19 +1584,10 @@ static void RenderScene( VkRenderer& renderer, uint32_t imageIndex, const EyeRenderParams* eyes, int eyeCount) { - VkDevice device = renderer.device; - - // Wait for previous frame - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &renderer.frameFence); - - // Begin command buffer - VkCommandBuffer cmd = renderer.commandBuffer; - vkResetCommandBuffer(cmd, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &beginInfo); + // INV-4.10: waits only the fence of the frame that used this ring slot N + // frames ago (normally long signalled), then resets + begins its buffer. + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) return; // Begin render pass with full SBS framebuffer VkClearValue clearValues[2] = {}; @@ -1694,18 +1691,12 @@ static void RenderScene( } vkCmdEndRenderPass(cmd); - vkEndCommandBuffer(cmd); - // Submit - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cmd; - - vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - - // Wait for completion before returning (runtime needs the image ready) - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); + // ONE submit, no wait. The caller releases the swapchain image after this: + // the runtime composes on the same queue, so queue order (not a CPU wait) + // is what makes the image ready for it. + VkResult r = renderer.ring.endFrame(); + if (r != VK_SUCCESS) LOG_WARN("VkFrameRing::endFrame: vkQueueSubmit failed (%d)", (int)r); } static void CleanupVkRenderer(VkRenderer& renderer) { @@ -1734,10 +1725,7 @@ static void CleanupVkRenderer(VkRenderer& renderer) { renderer.depthMemory = VK_NULL_HANDLE; } - if (renderer.frameFence) { - vkDestroyFence(renderer.device, renderer.frameFence, nullptr); - renderer.frameFence = VK_NULL_HANDLE; - } + renderer.ring.destroy(); // after vkDeviceWaitIdle above; frees its pool/fences if (renderer.commandPool) { vkDestroyCommandPool(renderer.device, renderer.commandPool, nullptr); renderer.commandPool = VK_NULL_HANDLE; @@ -2461,6 +2449,7 @@ int main() { if (!InitializeVkRenderer(vkRenderer, vkDevice, physDevice, graphicsQueue, queueFamilyIndex, colorFormat)) { LOG_ERROR("Vulkan renderer initialization failed"); CleanupOpenXR(xr); + vkRenderer.ring.destroy(); // a partial init may have created it; free before the device vkDestroyDevice(vkDevice, nullptr); vkDestroyInstance(vkInstance, nullptr); return 1; diff --git a/test_apps/legacy/cube_hosted_legacy_vk_win/main.cpp b/test_apps/legacy/cube_hosted_legacy_vk_win/main.cpp index aac87c1042..137a9232c8 100644 --- a/test_apps/legacy/cube_hosted_legacy_vk_win/main.cpp +++ b/test_apps/legacy/cube_hosted_legacy_vk_win/main.cpp @@ -16,6 +16,7 @@ #define VK_USE_PLATFORM_WIN32_KHR #include +#include #define XR_USE_GRAPHICS_API_VULKAN #include @@ -858,9 +859,11 @@ struct VkRenderer { uint32_t fbWidth = 0; uint32_t fbHeight = 0; - VkCommandPool commandPool = VK_NULL_HANDLE; - VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - VkFence frameFence = VK_NULL_HANDLE; + VkCommandPool commandPool = VK_NULL_HANDLE; // init-time texture uploads only + + // INV-4.10 / #1917: N frames in flight, one submit per frame, never a wait + // on the frame just submitted. Owns the per-frame command buffers + fences. + dxr::VkFrameRing ring; float cubeRotation = 0.0f; @@ -953,12 +956,27 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; + // With several frames in flight (dxr::VkFrameRing) the depth image is + // shared by consecutive frames: order this frame's depth clear/writes + // after the previous frame's (WAW), and colour likewise. + VkSubpassDependency dep = {}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstStageMask = dep.srcStageMask; + dep.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpInfo = {}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpInfo.attachmentCount = 2; rpInfo.pAttachments = attachments; rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderer.renderPass) != VK_SUCCESS) { LOG_ERROR("Failed to create render pass"); @@ -1267,7 +1285,7 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic memcpy(data, gridVerts.data(), (size_t)gridBufSize); vkUnmapMemory(device, renderer.gridVertexMemory); - // Command pool + buffer + fence + // Command pool (init-time texture uploads) + frame ring (INV-4.10 / #1917) { VkCommandPoolCreateInfo poolInfo = {}; poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; @@ -1275,17 +1293,10 @@ static bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysic poolInfo.queueFamilyIndex = queueFamilyIndex; if (vkCreateCommandPool(device, &poolInfo, nullptr, &renderer.commandPool) != VK_SUCCESS) return false; - VkCommandBufferAllocateInfo allocInfo = {}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.commandPool = renderer.commandPool; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandBufferCount = 1; - if (vkAllocateCommandBuffers(device, &allocInfo, &renderer.commandBuffer) != VK_SUCCESS) return false; - - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - if (vkCreateFence(device, &fenceInfo, nullptr, &renderer.frameFence) != VK_SUCCESS) return false; + if (!renderer.ring.init(device, physDevice, queue, queueFamilyIndex, dxr::VkFrameRing::Config{})) { + LOG_ERROR("Failed to create frame ring"); + return false; + } } // Load textures and create descriptor set @@ -1440,16 +1451,13 @@ static bool CreateSwapchainFramebuffers(VkRenderer& renderer, static void RenderScene(VkRenderer& renderer, uint32_t imageIndex, const EyeRenderParams* eyes, int eyeCount) { - VkDevice device = renderer.device; - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &renderer.frameFence); - - VkCommandBuffer cmd = renderer.commandBuffer; - vkResetCommandBuffer(cmd, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &beginInfo); + // Begin this frame's ring slot (waits only the fence of the frame that + // used this slot N frames ago). + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + LOG_ERROR("RenderScene: ring.beginFrame failed"); + return; + } VkClearValue clearValues[2] = {}; clearValues[0].color = {{0.05f, 0.05f, 0.25f, 1.0f}}; @@ -1545,14 +1553,12 @@ static void RenderScene(VkRenderer& renderer, uint32_t imageIndex, } vkCmdEndRenderPass(cmd); - vkEndCommandBuffer(cmd); - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cmd; - vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); + // ONE submit, no wait; the caller releases the swapchain image after this. + VkResult submitResult = renderer.ring.endFrame(); + if (submitResult != VK_SUCCESS) { + LOG_ERROR("RenderScene: ring.endFrame (vkQueueSubmit) failed: %d", (int)submitResult); + } } // ============================================================================ @@ -1561,6 +1567,7 @@ static void RenderScene(VkRenderer& renderer, uint32_t imageIndex, static void CleanupVkRenderer(VkRenderer& renderer) { if (!renderer.device) return; + renderer.ring.waitAll(); vkDeviceWaitIdle(renderer.device); for (auto fb : renderer.framebuffers) vkDestroyFramebuffer(renderer.device, fb, nullptr); @@ -1569,7 +1576,7 @@ static void CleanupVkRenderer(VkRenderer& renderer) { if (renderer.depthView) vkDestroyImageView(renderer.device, renderer.depthView, nullptr); if (renderer.depthImage) vkDestroyImage(renderer.device, renderer.depthImage, nullptr); if (renderer.depthMemory) vkFreeMemory(renderer.device, renderer.depthMemory, nullptr); - if (renderer.frameFence) vkDestroyFence(renderer.device, renderer.frameFence, nullptr); + renderer.ring.destroy(); if (renderer.commandPool) vkDestroyCommandPool(renderer.device, renderer.commandPool, nullptr); if (renderer.gridVertexBuffer) vkDestroyBuffer(renderer.device, renderer.gridVertexBuffer, nullptr); if (renderer.gridVertexMemory) vkFreeMemory(renderer.device, renderer.gridVertexMemory, nullptr); diff --git a/test_apps/texture/cube_zones_texture_metal_macos/CMakeLists.txt b/test_apps/texture/cube_zones_texture_metal_macos/CMakeLists.txt index fc42e7cece..a31057101e 100644 --- a/test_apps/texture/cube_zones_texture_metal_macos/CMakeLists.txt +++ b/test_apps/texture/cube_zones_texture_metal_macos/CMakeLists.txt @@ -64,7 +64,10 @@ FetchContent_Declare( # R8G8B8A8 (ADR-044 §7, displayxr-common#72) — the Metal compositor is # format-honest, so a UNORM HUD of CPU-rasterized (encoded) bytes would be # encoded twice. Same pin as the VK macOS cube apps. - GIT_TAG v2.27.0 + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. + # v2.29.0: dxr::CursorDepth (common/cursor_depth.h, ADR-046 Amendment 2). + GIT_TAG v2.29.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) diff --git a/test_apps/texture/cube_zones_texture_vk_win/main.cpp b/test_apps/texture/cube_zones_texture_vk_win/main.cpp index 6a1e09bfc6..86b0aa697a 100644 --- a/test_apps/texture/cube_zones_texture_vk_win/main.cpp +++ b/test_apps/texture/cube_zones_texture_vk_win/main.cpp @@ -768,49 +768,35 @@ static bool UploadBgraToVkImage(VkRenderer& vk, VkImage img, uint32_t w, uint32_ memcpy(mapped, buf.data(), buf.size()); vkUnmapMemory(vk.device, stagingMem); - VkCommandBufferAllocateInfo cbai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; - cbai.commandPool = vk.commandPool; - cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - cbai.commandBufferCount = 1; - VkCommandBuffer cmd = VK_NULL_HANDLE; - vkAllocateCommandBuffers(vk.device, &cbai, &cmd); - VkCommandBufferBeginInfo cbi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; - cbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &cbi); - - VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - b.srcAccessMask = 0; - b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - b.image = img; - b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, - nullptr, 0, nullptr, 1, &b); - - VkBufferImageCopy region = {}; - region.bufferRowLength = w; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - region.imageOffset = {0, 0, 0}; - region.imageExtent = {w, h, 1}; - vkCmdCopyBufferToImage(cmd, staging, img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - - b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, - nullptr, 0, nullptr, 1, &b); - - vkEndCommandBuffer(cmd); - VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - vkQueueSubmit(vk.graphicsQueue, 1, &si, VK_NULL_HANDLE); - vkQueueWaitIdle(vk.graphicsQueue); - vkFreeCommandBuffers(vk.device, vk.commandPool, 1, &cmd); + // One-off (strip filled once at activation): the ring's sanctioned + // blocking path. + vk.ring.submitImmediate([&](VkCommandBuffer cmd) { + VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + b.srcAccessMask = 0; + b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.image = img; + b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, + nullptr, 0, nullptr, 1, &b); + + VkBufferImageCopy region = {}; + region.bufferRowLength = w; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageOffset = {0, 0, 0}; + region.imageExtent = {w, h, 1}; + vkCmdCopyBufferToImage(cmd, staging, img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, + nullptr, 0, nullptr, 1, &b); + }); vkDestroyBuffer(vk.device, staging, nullptr); vkFreeMemory(vk.device, stagingMem, nullptr); return true; @@ -1099,6 +1085,13 @@ static void RenderZonesFrame(RenderState& rs, const XrFrameState& frameState) { uint32_t submitViewCounts[kNumZones] = {}; // ADR-041: what each zone LAYER carries (>= submitViewCounts[zi]). uint32_t locatedViewCounts[kNumZones] = {}; + // Zone swapchains acquired this frame; released only after the ONE ring + // submit below (INV-4.10: every zone's rendering shares one submit). + bool acquired[kNumZones] = {}; + VkCommandBuffer cmd = vk.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + LOG_ERROR("[zones] ring.beginFrame failed"); + } for (uint32_t zi = 0; zi < kNumZones; zi++) { DisplayZone& z = g_zonesArr[zi]; @@ -1207,20 +1200,35 @@ static void RenderZonesFrame(RenderState& rs, const XrFrameState& frameState) { // Acquire this zone's swapchain image and render every view tile. XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; uint32_t imageIndex = 0; - if (XR_FAILED(xrAcquireSwapchainImage(z.swapchain, &ai, &imageIndex))) { + if (cmd == VK_NULL_HANDLE || XR_FAILED(xrAcquireSwapchainImage(z.swapchain, &ai, &imageIndex))) { submitViewCounts[zi] = 0; continue; } + acquired[zi] = true; XrSwapchainImageWaitInfo wi = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; wi.timeout = XR_INFINITE_DURATION; xrWaitSwapchainImage(z.swapchain, &wi); // Per-zone clear (premultiplied RGBA) + spin phase via RenderSceneToZone. - RenderSceneToZone(vk, z.fb, imageIndex, eyes.data(), (int)n, + RenderSceneToZone(vk, cmd, z.fb, imageIndex, eyes.data(), (int)n, z.clearColor, z.spinPhase); + } + // ONE submit for every zone, no wait; then release the zone images (the + // runtime composes on the same VkQueue, so release AFTER the submit). No + // app-side wait is needed for the texture-mode present either: the app + // never reads its own Vulkan output — it presents the runtime-composited + // shared texture after xrEndFrame, which the runtime finishes/synchronises. + if (cmd != VK_NULL_HANDLE) { + const VkResult sr = vk.ring.endFrame(); + if (sr != VK_SUCCESS) { + LOG_ERROR("[zones] ring.endFrame (vkQueueSubmit) failed: %d", (int)sr); + } + } + for (uint32_t zi = 0; zi < kNumZones; zi++) { + if (!acquired[zi]) continue; XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; - xrReleaseSwapchainImage(z.swapchain, &ri); + xrReleaseSwapchainImage(g_zonesArr[zi].swapchain, &ri); } // Layer list: [projA (zone A chained), projB (zone B chained), strip]. @@ -1920,6 +1928,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine LOG_INFO(""); LOG_INFO("=== Shutting down ==="); + // Drain in-flight ring frames before destroying zone framebuffers/swapchains. + vkRenderer.ring.waitAll(); CleanupZones(vkRenderer); // Release RenderState ComPtrs first (they hold the swapchain refs). diff --git a/test_apps/texture/cube_zones_texture_vk_win/vk_renderer.cpp b/test_apps/texture/cube_zones_texture_vk_win/vk_renderer.cpp index 1db2cf9cb2..c91541c249 100644 --- a/test_apps/texture/cube_zones_texture_vk_win/vk_renderer.cpp +++ b/test_apps/texture/cube_zones_texture_vk_win/vk_renderer.cpp @@ -705,12 +705,27 @@ bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysicalDevic subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; + // With several frames in flight (dxr::VkFrameRing) the per-eye depth + // image is shared by consecutive frames: order this frame's depth + // clear/writes after the previous frame's (WAW), and colour likewise. + VkSubpassDependency dep = {}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstStageMask = dep.srcStageMask; + dep.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpInfo = {}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpInfo.attachmentCount = 2; rpInfo.pAttachments = attachments; rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderer.renderPass) != VK_SUCCESS) { LOG_ERROR("Failed to create render pass"); @@ -1444,16 +1459,10 @@ bool InitializeVkRenderer(VkRenderer& renderer, VkDevice device, VkPhysicalDevic renderer.texturesLoaded = true; } - // Frame fence - { - VkFenceCreateInfo fenceInfo = {}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - - if (vkCreateFence(device, &fenceInfo, nullptr, &renderer.frameFence) != VK_SUCCESS) { - LOG_ERROR("Failed to create fence"); - return false; - } + // Frame ring (INV-4.10 / #1917): per-frame command buffers + fences. + if (!renderer.ring.init(device, physDevice, queue, queueFamilyIndex, dxr::VkFrameRing::Config{})) { + LOG_ERROR("Failed to create frame ring"); + return false; } LOG_INFO("Vulkan renderer initialized"); @@ -1573,20 +1582,13 @@ void RenderScene( const EyeRenderParams* eyes, int eyeCount, float zoomScale) { - VkDevice device = renderer.device; - - // Wait for previous frame's fence - LOG_INFO("[RenderScene] imageIndex=%u: vkWaitForFences (pre-render)...", imageIndex); - VkResult fenceResult = vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - LOG_INFO("[RenderScene] vkWaitForFences returned %d", (int)fenceResult); - vkResetFences(device, 1, &renderer.frameFence); - - // Begin command buffer - vkResetCommandBuffer(renderer.commandBuffer, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(renderer.commandBuffer, &beginInfo); + // Begin this frame's ring slot (waits only the fence of the frame that + // used this slot N frames ago). + VkCommandBuffer cmd = renderer.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + LOG_ERROR("[RenderScene] imageIndex=%u: ring.beginFrame failed", imageIndex); + return; + } // Single render pass with CLEAR covering full framebuffer. // Transparent-background mode (DISPLAYXR_TRANSPARENT_BG=1): clear to @@ -1629,7 +1631,7 @@ void RenderScene( rpBegin.pClearValues = clearValues; __try { - vkCmdBeginRenderPass(renderer.commandBuffer, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); } __except(EXCEPTION_EXECUTE_HANDLER) { LOG_ERROR("[RenderScene] CRASH in vkCmdBeginRenderPass! exception=0x%08X", GetExceptionCode()); return; @@ -1655,12 +1657,12 @@ void RenderScene( viewport.height = -(float)h; viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; - vkCmdSetViewport(renderer.commandBuffer, 0, 1, &viewport); + vkCmdSetViewport(cmd, 0, 1, &viewport); VkRect2D scissor = {}; scissor.offset = {(int32_t)vpX, (int32_t)vpY}; scissor.extent = {w, h}; - vkCmdSetScissor(renderer.commandBuffer, 0, 1, &scissor); + vkCmdSetScissor(cmd, 0, 1, &scissor); // Draw cube - base rests on grid at y=0 { @@ -1676,19 +1678,19 @@ void RenderScene( XMMATRIX cubeModel = cubeRot * cubeScale; XMStoreFloat4x4(&pc.model, cubeModel); - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.cubePipelineLayout, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipeline); + vkCmdPushConstants(cmd, renderer.cubePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc); if (renderer.texturesLoaded) { - vkCmdBindDescriptorSets(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipelineLayout, 0, 1, &renderer.descriptorSet, 0, nullptr); } VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(renderer.commandBuffer, 0, 1, &renderer.cubeVertexBuffer, &offset); - vkCmdBindIndexBuffer(renderer.commandBuffer, renderer.cubeIndexBuffer, 0, VK_INDEX_TYPE_UINT16); - vkCmdDrawIndexed(renderer.commandBuffer, 36, 1, 0, 0, 0); + vkCmdBindVertexBuffers(cmd, 0, 1, &renderer.cubeVertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, renderer.cubeIndexBuffer, 0, VK_INDEX_TYPE_UINT16); + vkCmdDrawIndexed(cmd, 36, 1, 0, 0, 0); } // Draw grid floor @@ -1702,36 +1704,30 @@ void RenderScene( XMStoreFloat4x4(&pc.transform, gridWVP); pc.color[0] = 0.3f; pc.color[1] = 0.3f; pc.color[2] = 0.35f; pc.color[3] = 1.0f; - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.gridPipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.pipelineLayout, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.gridPipeline); + vkCmdPushConstants(cmd, renderer.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(renderer.commandBuffer, 0, 1, &renderer.gridVertexBuffer, &offset); - vkCmdDraw(renderer.commandBuffer, renderer.gridVertexCount, 1, 0, 0); + vkCmdBindVertexBuffers(cmd, 0, 1, &renderer.gridVertexBuffer, &offset); + vkCmdDraw(cmd, renderer.gridVertexCount, 1, 0, 0); } } - vkCmdEndRenderPass(renderer.commandBuffer); - vkEndCommandBuffer(renderer.commandBuffer); - - // Submit - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &renderer.commandBuffer; - - VkResult submitResult = vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - LOG_INFO("[RenderScene] vkQueueSubmit returned %d", (int)submitResult); + vkCmdEndRenderPass(cmd); - // Wait for completion before returning (runtime needs the image ready) - VkResult waitResult = vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - LOG_INFO("[RenderScene] vkWaitForFences returned %d - DONE", (int)waitResult); + // ONE submit, no wait: the runtime composes on the same VkQueue, so this + // work is ordered ahead of it as long as the image is released after. + VkResult submitResult = renderer.ring.endFrame(); + if (submitResult != VK_SUCCESS) { + LOG_ERROR("[RenderScene] ring.endFrame (vkQueueSubmit) failed: %d", (int)submitResult); + } } void CleanupVkRenderer(VkRenderer& renderer) { if (!renderer.device) return; + renderer.ring.waitAll(); vkDeviceWaitIdle(renderer.device); // Cleanup texture resources @@ -1789,10 +1785,7 @@ void CleanupVkRenderer(VkRenderer& renderer) { } } - if (renderer.frameFence) { - vkDestroyFence(renderer.device, renderer.frameFence, nullptr); - renderer.frameFence = VK_NULL_HANDLE; - } + renderer.ring.destroy(); if (renderer.commandPool) { vkDestroyCommandPool(renderer.device, renderer.commandPool, nullptr); renderer.commandPool = VK_NULL_HANDLE; @@ -1942,20 +1935,14 @@ bool CreateZoneFramebuffers(VkRenderer& renderer, ZoneFramebuffers& fb, return true; } -void RenderSceneToZone(VkRenderer& renderer, ZoneFramebuffers& fb, +void RenderSceneToZone(VkRenderer& renderer, VkCommandBuffer cmd, ZoneFramebuffers& fb, uint32_t imageIndex, const EyeRenderParams* eyes, int eyeCount, const float clearColor[4], float rotationOffset) { - VkDevice device = renderer.device; - - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &renderer.frameFence); - - vkResetCommandBuffer(renderer.commandBuffer, 0); - VkCommandBufferBeginInfo beginInfo = {}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(renderer.commandBuffer, &beginInfo); + // Records into the frame's ring command buffer (the caller owns + // beginFrame/endFrame); every zone of the frame shares ONE submit. The + // zone's depth image is ordered across frames by the render pass's + // EXTERNAL subpass dependency. // Per-zone premultiplied clear (zone A opaque tint; zone B fully // transparent → desktop show-through; matches the D3D11/macOS zones apps). @@ -1984,7 +1971,7 @@ void RenderSceneToZone(VkRenderer& renderer, ZoneFramebuffers& fb, rpBegin.renderArea.extent = {fb.width, fb.height}; rpBegin.clearValueCount = 2; rpBegin.pClearValues = clearValues; - vkCmdBeginRenderPass(renderer.commandBuffer, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); const float zoneRotation = renderer.cubeRotation + rotationOffset; @@ -2005,12 +1992,12 @@ void RenderSceneToZone(VkRenderer& renderer, ZoneFramebuffers& fb, viewport.height = -(float)h; viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; - vkCmdSetViewport(renderer.commandBuffer, 0, 1, &viewport); + vkCmdSetViewport(cmd, 0, 1, &viewport); VkRect2D scissor = {}; scissor.offset = {(int32_t)vpX, (int32_t)vpY}; scissor.extent = {w, h}; - vkCmdSetScissor(renderer.commandBuffer, 0, 1, &scissor); + vkCmdSetScissor(cmd, 0, 1, &scissor); // Draw cube — base rests on grid at y=0 (per-zone spin phase applied). { @@ -2026,17 +2013,17 @@ void RenderSceneToZone(VkRenderer& renderer, ZoneFramebuffers& fb, XMMATRIX cubeModel = cubeRot * cubeScale; XMStoreFloat4x4(&pc.model, cubeModel); - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.cubePipelineLayout, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipeline); + vkCmdPushConstants(cmd, renderer.cubePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc); if (renderer.texturesLoaded) { - vkCmdBindDescriptorSets(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.cubePipelineLayout, 0, 1, &renderer.descriptorSet, 0, nullptr); } VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(renderer.commandBuffer, 0, 1, &renderer.cubeVertexBuffer, &offset); - vkCmdBindIndexBuffer(renderer.commandBuffer, renderer.cubeIndexBuffer, 0, VK_INDEX_TYPE_UINT16); - vkCmdDrawIndexed(renderer.commandBuffer, 36, 1, 0, 0, 0); + vkCmdBindVertexBuffers(cmd, 0, 1, &renderer.cubeVertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, renderer.cubeIndexBuffer, 0, VK_INDEX_TYPE_UINT16); + vkCmdDrawIndexed(cmd, 36, 1, 0, 0, 0); } // Draw grid floor. @@ -2050,12 +2037,12 @@ void RenderSceneToZone(VkRenderer& renderer, ZoneFramebuffers& fb, XMStoreFloat4x4(&pc.transform, gridWVP); pc.color[0] = 0.3f; pc.color[1] = 0.3f; pc.color[2] = 0.35f; pc.color[3] = 1.0f; - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.gridPipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.pipelineLayout, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.gridPipeline); + vkCmdPushConstants(cmd, renderer.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(renderer.commandBuffer, 0, 1, &renderer.gridVertexBuffer, &offset); - vkCmdDraw(renderer.commandBuffer, renderer.gridVertexCount, 1, 0, 0); + vkCmdBindVertexBuffers(cmd, 0, 1, &renderer.gridVertexBuffer, &offset); + vkCmdDraw(cmd, renderer.gridVertexCount, 1, 0, 0); } // Zone content-alpha edge fade (#613 parity with the D3D11 zone app): @@ -2073,32 +2060,21 @@ void RenderSceneToZone(VkRenderer& renderer, ZoneFramebuffers& fb, fvp.height = (float)h; fvp.minDepth = 0.0f; fvp.maxDepth = 1.0f; - vkCmdSetViewport(renderer.commandBuffer, 0, 1, &fvp); + vkCmdSetViewport(cmd, 0, 1, &fvp); ZoneFeatherPush fp = {}; fp.tile_px[0] = (float)w; fp.tile_px[1] = (float)h; fp.feather_px = ZoneEdgeFadePx(); - vkCmdBindPipeline(renderer.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.zoneFeatherPipeline); - vkCmdPushConstants(renderer.commandBuffer, renderer.zoneFeatherPipelineLayout, + vkCmdPushConstants(cmd, renderer.zoneFeatherPipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(fp), &fp); - vkCmdDraw(renderer.commandBuffer, 3, 1, 0, 0); + vkCmdDraw(cmd, 3, 1, 0, 0); } } - vkCmdEndRenderPass(renderer.commandBuffer); - vkEndCommandBuffer(renderer.commandBuffer); - - VkSubmitInfo submitInfo = {}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &renderer.commandBuffer; - vkQueueSubmit(renderer.graphicsQueue, 1, &submitInfo, renderer.frameFence); - - // Wait for completion before returning — the runtime needs the zone image - // ready when the projection layer referencing it is submitted in xrEndFrame. - vkWaitForFences(device, 1, &renderer.frameFence, VK_TRUE, UINT64_MAX); + vkCmdEndRenderPass(cmd); } void DestroyZoneFramebuffers(VkRenderer& renderer, ZoneFramebuffers& fb) { diff --git a/test_apps/texture/cube_zones_texture_vk_win/vk_renderer.h b/test_apps/texture/cube_zones_texture_vk_win/vk_renderer.h index ccea7daef1..e011327ba2 100644 --- a/test_apps/texture/cube_zones_texture_vk_win/vk_renderer.h +++ b/test_apps/texture/cube_zones_texture_vk_win/vk_renderer.h @@ -18,6 +18,7 @@ #include #include #include +#include struct VkRenderer { VkDevice device = VK_NULL_HANDLE; @@ -69,12 +70,13 @@ struct VkRenderer { std::vector swapchainImageViews[2]; std::vector framebuffers[2]; - // Command pool and buffers + // Command pool + buffer for the init-time texture upload only. VkCommandPool commandPool = VK_NULL_HANDLE; VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - // Fence for frame synchronization - VkFence frameFence = VK_NULL_HANDLE; + // INV-4.10 / #1917: N frames in flight, one submit per frame, never a wait + // on the frame just submitted. Owns the per-frame command buffers + fences. + dxr::VkFrameRing ring; // ADR-021 / INV-4.6: does this renderer emit SCENE-LINEAR? Latched once, at // InitializeVkRenderer(), from dxr::RenderSceneLinear() -- the pipelines bake @@ -161,8 +163,9 @@ bool CreateZoneFramebuffers(VkRenderer& renderer, ZoneFramebuffers& fb, // Render the cube + grid scene into one zone framebuffer with an explicit // premultiplied clear color and a per-zone rotation offset (added to // renderer.cubeRotation for this submission only — the caller does NOT need to -// save/restore the shared rotation). -void RenderSceneToZone(VkRenderer& renderer, ZoneFramebuffers& fb, +// save/restore the shared rotation). Records into @p cmd (the frame's +// renderer.ring command buffer); no submit. +void RenderSceneToZone(VkRenderer& renderer, VkCommandBuffer cmd, ZoneFramebuffers& fb, uint32_t imageIndex, const EyeRenderParams* eyes, int eyeCount, const float clearColor[4], float rotationOffset); diff --git a/test_apps/weave/weave_client_vk_android/src/main/cpp/CMakeLists.txt b/test_apps/weave/weave_client_vk_android/src/main/cpp/CMakeLists.txt index c8382a304a..2e11118233 100644 --- a/test_apps/weave/weave_client_vk_android/src/main/cpp/CMakeLists.txt +++ b/test_apps/weave/weave_client_vk_android/src/main/cpp/CMakeLists.txt @@ -24,6 +24,22 @@ add_library(weave_client_vk_android SHARED main.cpp) target_include_directories(weave_client_vk_android BEFORE PRIVATE "${CMAKE_SOURCE_DIR}/../../../../../../src/external/openxr_includes") +# displayxr-common — header-only vk_frame_ring.h (dxr::VkFrameRing: N frames in +# flight, ONE vkQueueSubmit per frame, no wait on the frame just submitted — +# INV-4.10, runtime #1917 / displayxr-demo-modelviewer#170). Sources only: don't +# run its CMake (the STATIC lib carries desktop platform sources); its common/ +# dir is appended LAST to the include path so no local header is shadowed. +include(FetchContent) +FetchContent_Declare(displayxr_common_rules + GIT_REPOSITORY https://github.com/DisplayXR/displayxr-common.git + GIT_TAG v2.29.0 + GIT_SHALLOW TRUE) +FetchContent_GetProperties(displayxr_common_rules) +if(NOT displayxr_common_rules_POPULATED) + FetchContent_Populate(displayxr_common_rules) +endif() +target_include_directories(weave_client_vk_android PRIVATE "${displayxr_common_rules_SOURCE_DIR}/common") + target_link_libraries(weave_client_vk_android OpenXR::openxr_loader vulkan diff --git a/test_apps/weave/weave_client_vk_android/src/main/cpp/main.cpp b/test_apps/weave/weave_client_vk_android/src/main/cpp/main.cpp index a6f1f27f16..1a6a473e65 100644 --- a/test_apps/weave/weave_client_vk_android/src/main/cpp/main.cpp +++ b/test_apps/weave/weave_client_vk_android/src/main/cpp/main.cpp @@ -59,6 +59,8 @@ #include #include +#include + #define LOG_TAG "weave_client_vk_android" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) @@ -123,9 +125,10 @@ struct WeaveApp VkDevice vk_device = VK_NULL_HANDLE; uint32_t vk_queue_family = UINT32_MAX; VkQueue vk_queue = VK_NULL_HANDLE; - VkCommandPool cmd_pool = VK_NULL_HANDLE; - VkCommandBuffer cmd = VK_NULL_HANDLE; - VkFence fence = VK_NULL_HANDLE; + // Frame submission ring (dxr::VkFrameRing, displayxr-common): the present + // blit is ONE submit per frame with no wait on it — INV-4.10 / #1917. Also + // owns the one-off submitImmediate() path (input upload, debug dump). + dxr::VkFrameRing ring; VkSemaphore acquire_sem = VK_NULL_HANDLE; VkSemaphore present_sem = VK_NULL_HANDLE; PFN_vkGetAndroidHardwareBufferPropertiesANDROID get_ahb_props = nullptr; @@ -483,21 +486,14 @@ vk_create_instance_and_device() return false; } - // Command pool / buffer / sync. - VkCommandPoolCreateInfo pool_ci = {}; - pool_ci.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; - pool_ci.queueFamilyIndex = g_app.vk_queue_family; - pool_ci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - VK_CHECK(vkCreateCommandPool(g_app.vk_device, &pool_ci, nullptr, &g_app.cmd_pool), "vkCreateCommandPool"); - VkCommandBufferAllocateInfo cb_ai = {}; - cb_ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - cb_ai.commandPool = g_app.cmd_pool; - cb_ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - cb_ai.commandBufferCount = 1; - VK_CHECK(vkAllocateCommandBuffers(g_app.vk_device, &cb_ai, &g_app.cmd), "vkAllocateCommandBuffers"); - VkFenceCreateInfo fence_ci = {}; - fence_ci.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - VK_CHECK(vkCreateFence(g_app.vk_device, &fence_ci, nullptr, &g_app.fence), "vkCreateFence"); + // Frame ring (command buffers + fences) and present semaphores. Android + // processes have no usable environment: pass frames-in-flight explicitly. + dxr::VkFrameRing::Config ring_cfg; + ring_cfg.framesInFlight = 3; + if (!g_app.ring.init(g_app.vk_device, g_app.vk_phys, g_app.vk_queue, g_app.vk_queue_family, ring_cfg)) { + LOGE("frame ring init failed"); + return false; + } VkSemaphoreCreateInfo sem_ci = {}; sem_ci.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; VK_CHECK(vkCreateSemaphore(g_app.vk_device, &sem_ci, nullptr, &g_app.acquire_sem), "vkCreateSemaphore"); @@ -930,41 +926,30 @@ barrier(VkCommandBuffer cmd, //! Upload the CPU pattern into the input AHardwareBuffer and WAIT for it. The //! weave contract is "caller's writes are complete before xrWeaveSubmitDXR" //! (there is no acquire fence on this platform yet, #1036 follow-up), so this -//! wait is the contract, not laziness. +//! wait is the contract, not laziness — hence the ring's one sanctioned +//! blocking path, submitImmediate(). bool upload_input() { - vkResetCommandBuffer(g_app.cmd, 0); - VkCommandBufferBeginInfo bi = {}; - bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK_CHECK(vkBeginCommandBuffer(g_app.cmd, &bi), "vkBeginCommandBuffer(upload)"); - - barrier(g_app.cmd, g_app.in_image, - g_app.in_first_use ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_GENERAL, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT, - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); - g_app.in_first_use = false; - - VkBufferImageCopy copy = {}; - copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - copy.imageExtent = {g_app.in_w, g_app.in_h, 1}; - vkCmdCopyBufferToImage(g_app.cmd, g_app.staging, g_app.in_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, - ©); - - // GENERAL is what the runtime's importer expects to find across frames. - barrier(g_app.cmd, g_app.in_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, - VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); - - VK_CHECK(vkEndCommandBuffer(g_app.cmd), "vkEndCommandBuffer(upload)"); - VkSubmitInfo si = {}; - si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - si.commandBufferCount = 1; - si.pCommandBuffers = &g_app.cmd; - VK_CHECK(vkQueueSubmit(g_app.vk_queue, 1, &si, g_app.fence), "vkQueueSubmit(upload)"); - VK_CHECK(vkWaitForFences(g_app.vk_device, 1, &g_app.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences(upload)"); - vkResetFences(g_app.vk_device, 1, &g_app.fence); + const VkResult r = g_app.ring.submitImmediate([](VkCommandBuffer cmd) { + barrier(cmd, g_app.in_image, + g_app.in_first_use ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + g_app.in_first_use = false; + + VkBufferImageCopy copy = {}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {g_app.in_w, g_app.in_h, 1}; + vkCmdCopyBufferToImage(cmd, g_app.staging, g_app.in_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, + ©); + + // GENERAL is what the runtime's importer expects to find across frames. + barrier(cmd, g_app.in_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); + }); + VK_CHECK(r, "submitImmediate(upload)"); return true; } @@ -992,11 +977,11 @@ present_woven() LOGW("vkAcquireNextImageKHR: SUBOPTIMAL (presenting anyway)"); } - vkResetCommandBuffer(g_app.cmd, 0); - VkCommandBufferBeginInfo bi = {}; - bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK_CHECK(vkBeginCommandBuffer(g_app.cmd, &bi), "vkBeginCommandBuffer(present)"); + VkCommandBuffer cmd = g_app.ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + LOGE("frame ring beginFrame failed"); + return false; + } // Passthrough (debug.dxr.weave.passthrough) blits the pre-weave input, which // is the A/B for "is my present path or the weave at fault?". @@ -1005,12 +990,12 @@ present_woven() const uint32_t src_w = passthrough ? g_app.in_w : g_app.out_w; const uint32_t src_h = passthrough ? g_app.in_h : g_app.out_h; - barrier(g_app.cmd, src_image, g_app.out_first_use && !passthrough ? VK_IMAGE_LAYOUT_UNDEFINED + barrier(cmd, src_image, g_app.out_first_use && !passthrough ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); g_app.out_first_use = false; - barrier(g_app.cmd, g_app.swapchain_images[index], VK_IMAGE_LAYOUT_UNDEFINED, + barrier(cmd, g_app.swapchain_images[index], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); @@ -1021,30 +1006,21 @@ present_woven() blit.dstOffsets[1] = {(int32_t)g_app.swapchain_w, (int32_t)g_app.swapchain_h, 1}; // NEAREST: the woven output is an interlaced lattice — any filtering here // would smear neighbouring views into each other and destroy the 3D. - vkCmdBlitImage(g_app.cmd, src_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + vkCmdBlitImage(cmd, src_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, g_app.swapchain_images[index], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_NEAREST); - barrier(g_app.cmd, g_app.swapchain_images[index], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + barrier(cmd, g_app.swapchain_images[index], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_TRANSFER_WRITE_BIT, 0, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT); - barrier(g_app.cmd, src_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, + barrier(cmd, src_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); - VK_CHECK(vkEndCommandBuffer(g_app.cmd), "vkEndCommandBuffer(present)"); - + // ONE submit with the ring slot's fence and NO wait on it (INV-4.10). VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_TRANSFER_BIT; - VkSubmitInfo si = {}; - si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - si.waitSemaphoreCount = 1; - si.pWaitSemaphores = &g_app.acquire_sem; - si.pWaitDstStageMask = &wait_stage; - si.commandBufferCount = 1; - si.pCommandBuffers = &g_app.cmd; - si.signalSemaphoreCount = 1; - si.pSignalSemaphores = &g_app.present_sem; - VK_CHECK(vkQueueSubmit(g_app.vk_queue, 1, &si, g_app.fence), "vkQueueSubmit(present)"); + VK_CHECK(g_app.ring.endFrame(&g_app.acquire_sem, &wait_stage, 1, &g_app.present_sem, 1), + "frame ring endFrame(present)"); VkPresentInfoKHR pi = {}; pi.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; @@ -1059,9 +1035,6 @@ present_woven() logged_present = true; LOGW("vkQueuePresentKHR: %d", (int)present_res); } - - VK_CHECK(vkWaitForFences(g_app.vk_device, 1, &g_app.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences(present)"); - vkResetFences(g_app.vk_device, 1, &g_app.fence); return true; } @@ -1113,29 +1086,19 @@ dump_image(VkImage image, uint32_t w, uint32_t h, const char *path) vkAllocateMemory(g_app.vk_device, &mai, nullptr, &memory); vkBindBufferMemory(g_app.vk_device, buffer, memory, 0); - vkResetCommandBuffer(g_app.cmd, 0); - VkCommandBufferBeginInfo bi = {}; - bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(g_app.cmd, &bi); - barrier(g_app.cmd, image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - VK_PIPELINE_STAGE_TRANSFER_BIT); - VkBufferImageCopy copy = {}; - copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - copy.imageExtent = {w, h, 1}; - vkCmdCopyImageToBuffer(g_app.cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buffer, 1, ©); - barrier(g_app.cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, - VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); - vkEndCommandBuffer(g_app.cmd); - VkSubmitInfo si = {}; - si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - si.commandBufferCount = 1; - si.pCommandBuffers = &g_app.cmd; - vkQueueSubmit(g_app.vk_queue, 1, &si, g_app.fence); - vkWaitForFences(g_app.vk_device, 1, &g_app.fence, VK_TRUE, UINT64_MAX); - vkResetFences(g_app.vk_device, 1, &g_app.fence); + // Debug one-off (on demand, once): the ring's sanctioned blocking path. + g_app.ring.submitImmediate([&](VkCommandBuffer cmd) { + barrier(cmd, image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + VkBufferImageCopy copy = {}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {w, h, 1}; + vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buffer, 1, ©); + barrier(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, + VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); + }); uint8_t *mapped = nullptr; if (vkMapMemory(g_app.vk_device, memory, 0, VK_WHOLE_SIZE, 0, reinterpret_cast(&mapped)) == @@ -1190,6 +1153,12 @@ render_frame() } paint_frame(g_app.frame, rects, kRectCount); + // The previous frame's present blit may still be reading the woven output + // (and, under passthrough, the input). The weave contract is that the + // caller has finished with both before xrWeaveSubmitDXR — the runtime + // rewrites the one shared output and has no acquire fence on Android — so + // drain the ring here. The CPU paint above still overlaps that GPU work. + g_app.ring.waitAll(); if (!upload_input()) { return false; } @@ -1339,6 +1308,7 @@ reconcile_size() LOGI("WEAVE_CLIENT: surface is now %ux%u (was %ux%u) — rebuilding", w, h, g_app.swapchain_w, g_app.swapchain_h); + g_app.ring.waitAll(); vkDeviceWaitIdle(g_app.vk_device); destroy_input(); destroy_swapchain(); @@ -1415,10 +1385,12 @@ render_thread(void *) } } + g_app.ring.waitAll(); vkDeviceWaitIdle(g_app.vk_device); destroy_output(); destroy_input(); destroy_swapchain(); + g_app.ring.destroy(); if (g_app.xr_session != XR_NULL_HANDLE) { xrDestroySession(g_app.xr_session); } diff --git a/test_apps/weave/weave_present_vk_linux/CMakeLists.txt b/test_apps/weave/weave_present_vk_linux/CMakeLists.txt index d0b117cfcf..bbcc1901b2 100644 --- a/test_apps/weave/weave_present_vk_linux/CMakeLists.txt +++ b/test_apps/weave/weave_present_vk_linux/CMakeLists.txt @@ -62,7 +62,10 @@ FetchContent_Declare( # v2.27.0: window-space / HUD swapchains prefer the _SRGB sibling of # R8G8B8A8 (dxr::ChooseWindowSpaceSwapchainFormat; ADR-044 §7, # displayxr-common#72) — a UNORM HUD of encoded bytes was encoded twice. - GIT_TAG v2.27.0 + # v2.28.0: dxr::VkFrameRing (common/vk_frame_ring.h) — N frames in flight, + # one vkQueueSubmit per frame (INV-4.10, #1917). The Vulkan cubes use it. + # v2.29.0: dxr::CursorDepth (common/cursor_depth.h, ADR-046 Amendment 2). + GIT_TAG v2.29.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) diff --git a/test_apps/zones/cube_zones_vk_android/src/main/cpp/CMakeLists.txt b/test_apps/zones/cube_zones_vk_android/src/main/cpp/CMakeLists.txt index 832af84609..fc726a28e6 100644 --- a/test_apps/zones/cube_zones_vk_android/src/main/cpp/CMakeLists.txt +++ b/test_apps/zones/cube_zones_vk_android/src/main/cpp/CMakeLists.txt @@ -60,6 +60,22 @@ target_include_directories(cube_zones_vk_android BEFORE PRIVATE target_include_directories(cube_zones_vk_android PRIVATE "${CMAKE_SOURCE_DIR}/../../../../../common") +# displayxr-common — header-only vk_frame_ring.h (dxr::VkFrameRing: N frames in +# flight, ONE vkQueueSubmit per frame, no wait on the frame just submitted — +# INV-4.10, runtime #1917 / displayxr-demo-modelviewer#170). Sources only: don't +# run its CMake (the STATIC lib carries desktop platform sources); its common/ +# dir is appended LAST to the include path so no local header is shadowed. +include(FetchContent) +FetchContent_Declare(displayxr_common_rules + GIT_REPOSITORY https://github.com/DisplayXR/displayxr-common.git + GIT_TAG v2.29.0 + GIT_SHALLOW TRUE) +FetchContent_GetProperties(displayxr_common_rules) +if(NOT displayxr_common_rules_POPULATED) + FetchContent_Populate(displayxr_common_rules) +endif() +target_include_directories(cube_zones_vk_android PRIVATE "${displayxr_common_rules_SOURCE_DIR}/common") + # -u ANativeActivity_onCreate forces the linker to keep the symbol the # Android runtime resolves dynamically on Activity launch — without it, # the shared lib loads but exposes nothing. diff --git a/test_apps/zones/cube_zones_vk_android/src/main/cpp/hud_font.cpp b/test_apps/zones/cube_zones_vk_android/src/main/cpp/hud_font.cpp index 5194764552..78a676cf08 100644 --- a/test_apps/zones/cube_zones_vk_android/src/main/cpp/hud_font.cpp +++ b/test_apps/zones/cube_zones_vk_android/src/main/cpp/hud_font.cpp @@ -440,13 +440,16 @@ hud_font_init(HudFont &f, VkPhysicalDevice phys, VkDevice device, VkQueue queue, } // ── 5. Mapped quad vertex buffer. ── - if (!make_buffer(phys, device, sizeof(TextVtx) * kMaxVerts, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, + // One region per frame-ring slot (see HudFont::slot). + if (!make_buffer(phys, device, sizeof(TextVtx) * kMaxVerts * HUD_FONT_MAX_SLOTS, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, f.vbuf, f.vbuf_mem)) { HFLOGE("hud_font: vertex buffer failed"); return false; } - vkMapMemory(device, f.vbuf_mem, 0, sizeof(TextVtx) * kMaxVerts, 0, &f.vbuf_mapped); + vkMapMemory(device, f.vbuf_mem, 0, sizeof(TextVtx) * kMaxVerts * HUD_FONT_MAX_SLOTS, 0, + &f.vbuf_mapped); // ── 6. SDF rounded-rect panel pipeline (smooth corners, no texture). Same // {vec2 pos; vec2 c} vertex layout + alpha blend + no-depth as the text @@ -548,13 +551,15 @@ hud_font_init(HudFont &f, VkPhysicalDevice phys, VkDevice device, VkQueue queue, HFLOGE("hud_font: panel pipeline failed (%d)", (int)ppr); return false; } - if (!make_buffer(phys, device, sizeof(TextVtx) * 6, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, + if (!make_buffer(phys, device, sizeof(TextVtx) * 6 * HUD_FONT_MAX_SLOTS, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, f.panel_vbuf, f.panel_vbuf_mem)) { HFLOGE("hud_font: panel vbuf failed"); return false; } - vkMapMemory(device, f.panel_vbuf_mem, 0, sizeof(TextVtx) * 6, 0, &f.panel_vbuf_mapped); + vkMapMemory(device, f.panel_vbuf_mem, 0, sizeof(TextVtx) * 6 * HUD_FONT_MAX_SLOTS, 0, + &f.panel_vbuf_mapped); } f.ready = true; @@ -573,7 +578,7 @@ hud_font_build(HudFont &f, const char *text, float ox, float oy, float ndc_per_p if (!f.ready || !f.vbuf_mapped || !text) { return 0; } - TextVtx *v = (TextVtx *)f.vbuf_mapped; + TextVtx *v = (TextVtx *)f.vbuf_mapped + (f.slot % HUD_FONT_MAX_SLOTS) * kMaxVerts; uint32_t n = 0; const stbtt_bakedchar *cd = (const stbtt_bakedchar *)f.chars; float bx0 = 1e9f, by0 = 1e9f, bx1 = -1e9f, by1 = -1e9f; @@ -639,7 +644,7 @@ hud_font_draw(HudFont &f, VkCommandBuffer cmd, const float mvp[16], const float memcpy(push + 16, color, sizeof(float) * 4); vkCmdPushConstants(cmd, f.pipe_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), push); - VkDeviceSize off = 0; + VkDeviceSize off = sizeof(TextVtx) * kMaxVerts * (f.slot % HUD_FONT_MAX_SLOTS); vkCmdBindVertexBuffers(cmd, 0, 1, &f.vbuf, &off); vkCmdDraw(cmd, vert_count, 1, 0, 0); } @@ -653,7 +658,8 @@ hud_font_draw_panel(HudFont &f, VkCommandBuffer cmd, const float mvp[16], float } const float hx = 0.5f * (x1 - x0), hy = 0.5f * (y1 - y0); // 6 verts: NDC corner + centered coord (corner relative to panel center). - TextVtx *v = (TextVtx *)f.panel_vbuf_mapped; + const uint32_t slot = f.slot % HUD_FONT_MAX_SLOTS; + TextVtx *v = (TextVtx *)f.panel_vbuf_mapped + slot * 6; v[0] = {{x0, y0}, {-hx, -hy}}; v[1] = {{x1, y0}, {hx, -hy}}; v[2] = {{x1, y1}, {hx, hy}}; @@ -672,7 +678,7 @@ hud_font_draw_panel(HudFont &f, VkCommandBuffer cmd, const float mvp[16], float vkCmdPushConstants(cmd, f.panel_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), push); - VkDeviceSize off = 0; + VkDeviceSize off = sizeof(TextVtx) * 6 * slot; vkCmdBindVertexBuffers(cmd, 0, 1, &f.panel_vbuf, &off); vkCmdDraw(cmd, 6, 1, 0, 0); } diff --git a/test_apps/zones/cube_zones_vk_android/src/main/cpp/hud_font.h b/test_apps/zones/cube_zones_vk_android/src/main/cpp/hud_font.h index 3436ff71c0..ebddd82c32 100644 --- a/test_apps/zones/cube_zones_vk_android/src/main/cpp/hud_font.h +++ b/test_apps/zones/cube_zones_vk_android/src/main/cpp/hud_font.h @@ -21,6 +21,7 @@ #define HUD_FONT_FIRST_CHAR 32 #define HUD_FONT_NUM_CHARS 96 // ASCII 32..127 #define HUD_FONT_MAX_QUADS 256 // plenty for a few short lines +#define HUD_FONT_MAX_SLOTS 8 // >= dxr::VkFrameRing frames in flight (clamped 1..8) struct HudFontBakedChar { // mirrors stbtt_bakedchar (avoids leaking stb in the header) uint16_t x0, y0, x1, y1; @@ -56,6 +57,10 @@ struct HudFont { void *panel_vbuf_mapped = nullptr; // Persistently-mapped glyph quad vertex buffer ({vec2 pos; vec2 uv}). + // Both vertex buffers hold HUD_FONT_MAX_SLOTS regions; build/draw use the + // region of `slot` — the host sets it to its frame-ring slot each frame so + // a rebuild never overwrites vertices an in-flight frame still reads. + uint32_t slot = 0; VkBuffer vbuf = VK_NULL_HANDLE; VkDeviceMemory vbuf_mem = VK_NULL_HANDLE; void *vbuf_mapped = nullptr; diff --git a/test_apps/zones/cube_zones_vk_android/src/main/cpp/main.cpp b/test_apps/zones/cube_zones_vk_android/src/main/cpp/main.cpp index 6b0ba580fe..51f9f1ba68 100644 --- a/test_apps/zones/cube_zones_vk_android/src/main/cpp/main.cpp +++ b/test_apps/zones/cube_zones_vk_android/src/main/cpp/main.cpp @@ -118,6 +118,7 @@ #include #include "crate_scene.h" #include "hud_font.h" +#include #define LOG_TAG "cube_zones_vk_android" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) @@ -342,6 +343,7 @@ struct Local2DSwapchain uint32_t height{0}; XrSwapchainImageVulkanKHR images[8]{}; uint32_t image_count{0}; + uint32_t uploaded_mask{0}; //!< bit i = image i already holds the pattern VkBuffer staging{VK_NULL_HANDLE}; VkDeviceMemory staging_mem{VK_NULL_HANDLE}; }; @@ -389,7 +391,12 @@ srgb_to_linear(float c) { return c <= 0.04045f ? c / 12.92f : powf((c + 0.055f) / 1.055f, 2.4f); } -VkCommandPool g_app_cmd_pool = VK_NULL_HANDLE; +// Frame submission ring (dxr::VkFrameRing, displayxr-common): 3 frames in +// flight, ONE submit per frame, never a wait on the frame just submitted — +// INV-4.10 / #1917 (a per-frame submit + vkQueueWaitIdle parks the Adreno +// governor at ~1/3 clock). Owns the per-frame command buffers + fences, and +// the one-off submitImmediate() path the Local2D upload uses. +dxr::VkFrameRing g_ring; // Graphics pipeline state for the triangle renderer. VkRenderPass g_render_pass = VK_NULL_HANDLE; @@ -408,10 +415,13 @@ constexpr uint32_t kCubeVertexCount = 36; // In-frame HUD: a persistently-mapped host-visible vertex buffer of CubeVertex // quads, rebuilt each frame and drawn with an identity MVP (screen-space NDC). // Android widgets can't overlay the runtime's display window, so the HUD lives -// inside the rendered frame. +// inside the rendered frame. One kHudMaxVerts region PER FRAME-RING SLOT: with +// several frames in flight, rebuilding a single shared region would overwrite +// vertices a still-executing frame is reading. VkBuffer g_hud_vbuf = VK_NULL_HANDLE; VkDeviceMemory g_hud_vbuf_mem = VK_NULL_HANDLE; void *g_hud_mapped = nullptr; +uint32_t g_hud_slots = 1; constexpr uint32_t kHudMaxVerts = 8192; // Authoritative 4-way display rotation, pushed from MainActivity (Surface @@ -1463,6 +1473,7 @@ create_local2d_swapchain() } fill_local2d_pattern(static_cast(mapped), kLocal2DWidth, kLocal2DHeight); vkUnmapMemory(g_vk_device, g_local2d.staging_mem); + g_local2d.uploaded_mask = 0; LOGI("[zones] Local2D swapchain: %ux%u, %u images", kLocal2DWidth, kLocal2DHeight, n); return true; @@ -1470,60 +1481,46 @@ create_local2d_swapchain() //! Copy the staged pattern into Local2D image @p idx. The runtime hands these //! out in GENERAL (AHardwareBuffer-imported), so bracket the copy accordingly. +//! The pattern is static, so each image is uploaded ONCE (uploaded_mask) via +//! the ring's one-off submitImmediate() — not a blocking submit every frame +//! (INV-4.10). static bool upload_local2d_image(uint32_t idx) { - VkCommandBufferAllocateInfo ai = {}; - ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - ai.commandPool = g_app_cmd_pool; - ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - ai.commandBufferCount = 1; - VkCommandBuffer cmd = VK_NULL_HANDLE; - if (vkAllocateCommandBuffers(g_vk_device, &ai, &cmd) != VK_SUCCESS) { - return false; + if (idx < 32 && (g_local2d.uploaded_mask & (1u << idx)) != 0) { + return true; + } + VkResult r = g_ring.submitImmediate([&](VkCommandBuffer cmd) { + VkImage img = g_local2d.images[idx].image; + VkImageMemoryBarrier to_dst = {}; + to_dst.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + to_dst.srcAccessMask = 0; + to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + to_dst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + to_dst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + to_dst.image = img; + to_dst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, + nullptr, 0, nullptr, 1, &to_dst); + + VkBufferImageCopy copy = {}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {kLocal2DWidth, kLocal2DHeight, 1}; + vkCmdCopyBufferToImage(cmd, g_local2d.staging, img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + + VkImageMemoryBarrier to_gen = to_dst; + to_gen.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + to_gen.dstAccessMask = 0; + to_gen.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + to_gen.newLayout = VK_IMAGE_LAYOUT_GENERAL; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, + 0, nullptr, 0, nullptr, 1, &to_gen); + }); + if (r == VK_SUCCESS && idx < 32) { + g_local2d.uploaded_mask |= 1u << idx; } - VkCommandBufferBeginInfo bi = {}; - bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); - - VkImage img = g_local2d.images[idx].image; - VkImageMemoryBarrier to_dst = {}; - to_dst.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - to_dst.srcAccessMask = 0; - to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - to_dst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - to_dst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - to_dst.image = img; - to_dst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, - nullptr, 0, nullptr, 1, &to_dst); - - VkBufferImageCopy copy = {}; - copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - copy.imageExtent = {kLocal2DWidth, kLocal2DHeight, 1}; - vkCmdCopyBufferToImage(cmd, g_local2d.staging, img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); - - VkImageMemoryBarrier to_gen = to_dst; - to_gen.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - to_gen.dstAccessMask = 0; - to_gen.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - to_gen.newLayout = VK_IMAGE_LAYOUT_GENERAL; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, - 0, nullptr, 0, nullptr, 1, &to_gen); - - vkEndCommandBuffer(cmd); - VkSubmitInfo si = {}; - si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - VkResult r = vkQueueSubmit(g_vk_queue, 1, &si, VK_NULL_HANDLE); - if (r == VK_SUCCESS) { - vkQueueWaitIdle(g_vk_queue); - } - vkFreeCommandBuffers(g_vk_device, g_app_cmd_pool, 1, &cmd); return r == VK_SUCCESS; } @@ -1581,6 +1578,7 @@ destroy_local2d_swapchain() g_local2d.staging_mem = VK_NULL_HANDLE; } g_local2d.image_count = 0; + g_local2d.uploaded_mask = 0; } // One reference space for the app — projection layer poses are expressed @@ -1607,18 +1605,16 @@ create_scene() g_render_pass, g_asset_manager, g_scene_linear); } -// Standalone command pool for the test app's per-frame cmd buffers. -// Created lazily once the session + swapchains exist. +// Frame ring for the per-frame command buffers (INV-4.10). Created lazily once +// the session + swapchains exist. Android processes have no usable environment, +// so the frames-in-flight count is passed explicitly. bool -create_cmd_pool() +create_frame_ring() { - VkCommandPoolCreateInfo ci = {}; - ci.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; - ci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - ci.queueFamilyIndex = g_vk_queue_family; - VkResult res = vkCreateCommandPool(g_vk_device, &ci, nullptr, &g_app_cmd_pool); - if (res != VK_SUCCESS) { - LOGE("vkCreateCommandPool failed: %d", res); + dxr::VkFrameRing::Config cfg; + cfg.framesInFlight = 3; + if (!g_ring.init(g_vk_device, g_vk_phys_device, g_vk_queue, g_vk_queue_family, cfg)) { + LOGE("frame ring init failed"); return false; } return true; @@ -1648,12 +1644,9 @@ destroy_reference_space() } void -destroy_cmd_pool() +destroy_frame_ring() { - if (g_app_cmd_pool != VK_NULL_HANDLE && g_vk_device != VK_NULL_HANDLE) { - vkDestroyCommandPool(g_vk_device, g_app_cmd_pool, nullptr); - g_app_cmd_pool = VK_NULL_HANDLE; - } + g_ring.destroy(); } // Record + submit a clear-to-color cmd buffer on the swapchain image @@ -1896,6 +1889,20 @@ create_render_pass() subpass.pColorAttachments = &color_ref; subpass.pDepthStencilAttachment = &depth_ref; + // The single atlas depth image is shared by every frame, and with the frame + // ring (INV-4.10) several frames are in flight — order this frame's depth + // clear/writes after the previous frame's (WAW across submits). + VkSubpassDependency depth_dep = {}; + depth_dep.srcSubpass = VK_SUBPASS_EXTERNAL; + depth_dep.dstSubpass = 0; + depth_dep.srcStageMask = + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + depth_dep.dstStageMask = + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + depth_dep.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + depth_dep.dstAccessMask = + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + VkAttachmentDescription attachments[2] = {color, depth}; VkRenderPassCreateInfo rpci = {}; rpci.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; @@ -1903,6 +1910,8 @@ create_render_pass() rpci.pAttachments = attachments; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; + rpci.dependencyCount = 1; + rpci.pDependencies = &depth_dep; VkResult res = vkCreateRenderPass(g_vk_device, &rpci, nullptr, &g_render_pass); if (res != VK_SUCCESS) { @@ -2317,7 +2326,8 @@ build_hud(CubeVertex *v) static bool create_hud_buffer() { - const VkDeviceSize size = sizeof(CubeVertex) * kHudMaxVerts; + g_hud_slots = g_ring.framesInFlight() > 0 ? g_ring.framesInFlight() : 1; + const VkDeviceSize size = sizeof(CubeVertex) * kHudMaxVerts * g_hud_slots; VkBufferCreateInfo bci = {}; bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bci.size = size; @@ -2341,12 +2351,13 @@ create_hud_buffer() return false; } vkBindBufferMemory(g_vk_device, g_hud_vbuf, g_hud_vbuf_mem, 0); - // Persistently mapped (host-coherent): rebuilt each frame in record_atlas. + // Persistently mapped (host-coherent): rebuilt each frame in record_atlas, + // into the current ring slot's region. if (vkMapMemory(g_vk_device, g_hud_vbuf_mem, 0, size, 0, &g_hud_mapped) != VK_SUCCESS) { LOGE("map HUD vertex buffer failed"); return false; } - DXR_HW_DBG("HUD vertex buffer created (%u verts max)", kHudMaxVerts); + DXR_HW_DBG("HUD vertex buffer created (%u verts max x %u slots)", kHudMaxVerts, g_hud_slots); return true; } @@ -2510,20 +2521,13 @@ record_atlas(uint32_t image_idx, const XrView *views, uint32_t view_count, uint3 (void)rows; // DIAG (#1074): live-tunable flat per-view tint (see the clear below). const bool tint_views = get_prop_float("debug.dxr.tintviews", 0.0f) != 0.0f; - VkCommandBufferAllocateInfo ai = {}; - ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - ai.commandPool = g_app_cmd_pool; - ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - ai.commandBufferCount = 1; - VkCommandBuffer cmd = VK_NULL_HANDLE; - if (vkAllocateCommandBuffers(g_vk_device, &ai, &cmd) != VK_SUCCESS) { + // The ring waits only the fence of the frame that used this slot N frames + // ago, then hands back its (reset, begun) command buffer. + VkCommandBuffer cmd = g_ring.beginFrame(); + if (cmd == VK_NULL_HANDLE) { return false; } - - VkCommandBufferBeginInfo bi = {}; - bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); + const uint32_t slot = g_ring.slotIndex(); // LOAD_OP_CLEAR fills the WHOLE atlas (every tile) once at render-pass // begin. Dark blue, matching cube_handle_vk_win's scene background. @@ -2598,14 +2602,16 @@ record_atlas(uint32_t image_idx, const XrView *views, uint32_t view_count, uint3 uint32_t hud_n = 0; const Mat4 hud_mvp = mat4_identity(); const bool hud_ok = (g_hud_vbuf != VK_NULL_HANDLE && g_hud_mapped != nullptr); + const uint32_t hud_slot = slot % g_hud_slots; if (hud_ok) { - hud_n = build_hud(reinterpret_cast(g_hud_mapped)); + hud_n = build_hud(reinterpret_cast(g_hud_mapped) + hud_slot * kHudMaxVerts); } // Build the crisp stb_truetype text once per frame (drawn per tile below, // on top of the translucent panel). The panel top-left is (-0.975,-0.800). uint32_t hud_text_n = 0; float hud_bbox[4] = {0, 0, 0, 0}; // tight text bounds (NDC) → snug panel + g_hud_font.slot = slot; // per-slot glyph/panel vertex regions (frames in flight) if (g_hud_font.ready) { char xb[16], yb[16], zb[16], block[80]; hud_ftoa(g_eye_x.load(std::memory_order_relaxed), xb, sizeof xb); @@ -2667,7 +2673,7 @@ record_atlas(uint32_t image_idx, const XrView *views, uint32_t view_count, uint3 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, g_pipeline); vkCmdPushConstants(cmd, g_pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(hud_mvp), &hud_mvp); - VkDeviceSize hud_off = 0; + VkDeviceSize hud_off = sizeof(CubeVertex) * kHudMaxVerts * hud_slot; vkCmdBindVertexBuffers(cmd, 0, 1, &g_hud_vbuf, &hud_off); // Pass 1: translucent panel (blend-constant alpha < 1). if (g_hud_panel_n > 0) { @@ -2722,19 +2728,14 @@ record_atlas(uint32_t image_idx, const XrView *views, uint32_t view_count, uint3 } vkCmdEndRenderPass(cmd); - vkEndCommandBuffer(cmd); - VkSubmitInfo si = {}; - si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - VkResult res = vkQueueSubmit(g_vk_queue, 1, &si, VK_NULL_HANDLE); - if (res == VK_SUCCESS) { - // Host stall — acceptable for this test app; xrEndFrame needs the - // atlas image ready and there's no per-frame fence wired yet. - vkQueueWaitIdle(g_vk_queue); + // ONE submit with the slot fence and NO wait. The runtime composes on the + // same VkQueue, so the caller releases the swapchain image only after this + // (queue order puts our work ahead of the compose). + VkResult res = g_ring.endFrame(); + if (res != VK_SUCCESS) { + LOGE("record_atlas: frame submit failed: %d", (int)res); } - vkFreeCommandBuffers(g_vk_device, g_app_cmd_pool, 1, &cmd); DXR_HW_DBG_ONCE("record_atlas: first successful tiled-atlas submit"); return res == VK_SUCCESS; @@ -3078,6 +3079,7 @@ render_frame() res = xrWaitSwapchainImage(g_atlas.swapchain, &wait_img); } if (res == XR_SUCCESS) { + // Submits the frame (ring endFrame) BEFORE the release below. record_atlas(img_idx, views, submit_views, render_w, render_h, cols, rows, swap_lr, rig_pose); @@ -3255,10 +3257,12 @@ destroy_vulkan() void destroy_instance() { - // Tear down in reverse-creation order: cmd_pool → space → swapchains + // Tear down in reverse-creation order: frame ring → space → swapchains // → session → Vulkan → instance. Doing it any other way invalidates // handles still referenced by the runtime / loader. Drain GPU - // before per-view image destruction (audit B6). + // before per-view image destruction (audit B6): the frame ring's + // in-flight frames first, then the whole device. + g_ring.waitAll(); if (g_vk_device != VK_NULL_HANDLE) { vkDeviceWaitIdle(g_vk_device); } @@ -3275,7 +3279,7 @@ destroy_instance() } destroy_pipeline(); destroy_render_pass(); - destroy_cmd_pool(); + destroy_frame_ring(); destroy_reference_space(); destroy_swapchains(); destroy_session(); @@ -3321,7 +3325,7 @@ handle_cmd(struct android_app *app, int32_t cmd) // the plain full-canvas path, so this must not gate startup. (activate_zones(), true) && create_reference_space() && - create_cmd_pool() && + create_frame_ring() && create_render_pass() && create_pipeline() && create_scene() &&