From be71caab8cbdb8e279227eca4e36385c4c943831 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 02:05:00 -0700 Subject: [PATCH 1/6] build(test_apps): pin displayxr-common v2.28.0 (dxr::VkFrameRing) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v2.28.0 adds common/vk_frame_ring.h — the shared N-frames-in-flight, one-submit-per-frame Vulkan ring (INV-4.10, #1917). Every test app that fetches displayxr-common at v2.27.0 moves to it; the three GL apps pinned at v2.0.0 are left alone (no Vulkan, and a 28-minor jump is out of scope). Co-Authored-By: Claude Fable 5.1 --- test_apps/cube_handle_vk_linux/CMakeLists.txt | 4 +++- test_apps/cube_zones_vk_linux/CMakeLists.txt | 4 +++- test_apps/handle/cube_handle_metal_macos/CMakeLists.txt | 2 ++ test_apps/handle/cube_handle_vk_macos/CMakeLists.txt | 2 ++ test_apps/handle/cube_zones_metal_macos/CMakeLists.txt | 2 ++ test_apps/handle/cube_zones_vk_macos/CMakeLists.txt | 2 ++ test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt | 4 +++- .../legacy/cube_hosted_legacy_metal_macos/CMakeLists.txt | 4 +++- test_apps/legacy/cube_hosted_legacy_vk_linux/CMakeLists.txt | 4 +++- test_apps/legacy/cube_hosted_legacy_vk_macos/CMakeLists.txt | 4 +++- .../texture/cube_zones_texture_metal_macos/CMakeLists.txt | 4 +++- test_apps/weave/weave_present_vk_linux/CMakeLists.txt | 4 +++- 12 files changed, 32 insertions(+), 8 deletions(-) diff --git a/test_apps/cube_handle_vk_linux/CMakeLists.txt b/test_apps/cube_handle_vk_linux/CMakeLists.txt index e0ea1d9c89..3dcd7f1af2 100644 --- a/test_apps/cube_handle_vk_linux/CMakeLists.txt +++ b/test_apps/cube_handle_vk_linux/CMakeLists.txt @@ -88,7 +88,9 @@ 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. + GIT_TAG v2.28.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) diff --git a/test_apps/cube_zones_vk_linux/CMakeLists.txt b/test_apps/cube_zones_vk_linux/CMakeLists.txt index c383052dfe..635368a649 100644 --- a/test_apps/cube_zones_vk_linux/CMakeLists.txt +++ b/test_apps/cube_zones_vk_linux/CMakeLists.txt @@ -89,7 +89,9 @@ 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. + GIT_TAG v2.28.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) 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_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_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/hosted/cube_hosted_metal_macos/CMakeLists.txt b/test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt index a32b4d152c..85582fd9cb 100644 --- a/test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt +++ b/test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt @@ -62,7 +62,9 @@ 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. + GIT_TAG v2.28.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..80d0d5fcc6 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,9 @@ 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. + GIT_TAG v2.28.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..1f383cfa40 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,9 @@ 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. + GIT_TAG v2.28.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) 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..3b85bc5943 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,9 @@ 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. + GIT_TAG v2.28.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) 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..1bf7477cfd 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,9 @@ 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. + GIT_TAG v2.28.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) diff --git a/test_apps/weave/weave_present_vk_linux/CMakeLists.txt b/test_apps/weave/weave_present_vk_linux/CMakeLists.txt index d0b117cfcf..96a978f68d 100644 --- a/test_apps/weave/weave_present_vk_linux/CMakeLists.txt +++ b/test_apps/weave/weave_present_vk_linux/CMakeLists.txt @@ -62,7 +62,9 @@ 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. + GIT_TAG v2.28.0 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(displayxr_common) From c132d1b41d4f42a7453c188cfc7dbe54fd7291f5 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 02:19:06 -0700 Subject: [PATCH 2/6] perf(test_apps): Linux + macOS Vulkan cubes on dxr::VkFrameRing (INV-4.10) cube_handle_vk_{linux,macos} and cube_zones_vk_{linux,macos} submitted, then waited the fence they had just submitted (or waited a single fence at the top of the next frame): zero / one frame in flight, no CPU-GPU overlap. They are what /new-displayxr-app clones, so every new app inherited it. - The single command buffer + frame fence become a dxr::VkFrameRing (displayxr-common v2.28.0; default 3 in flight, DXR_VK_FRAMES_IN_FLIGHT overrides). Per frame: beginFrame() -> record -> endFrame() (ONE submit, no wait) -> xrReleaseSwapchainImage. - Zones: every zone's pass of a frame is recorded into the one ring frame; all zone swapchains are released after the single submit. - Each render pass gains a VK_SUBPASS_EXTERNAL dependency: the shared depth image is now written by overlapping frames, which the per-frame fence wait used to order implicitly. - MVPs stay push constants (no per-frame host-written UBO to protect). - macOS HUD: uploads go through ring.submitImmediate() (no per-call alloc/free/vkQueueWaitIdle) and only when the HUD text changes; an unchanged HUD reuses the swapchain's last released image. - Linux zones: the strip clear goes through submitImmediate(). - Teardown destroys the ring before the device, including on the renderer-init failure path. Co-Authored-By: Claude Fable 5.1 --- test_apps/cube_handle_vk_linux/main.cpp | 96 +++---- test_apps/cube_zones_vk_linux/main.cpp | 261 +++++++++--------- test_apps/handle/cube_handle_vk_macos/main.mm | 185 +++++++------ test_apps/handle/cube_zones_vk_macos/main.mm | 203 ++++++++------ 4 files changed, 382 insertions(+), 363 deletions(-) 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/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_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_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); } From ff20c06c24c91ebc64943067a02dccd854cf3eaf Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 02:35:03 -0700 Subject: [PATCH 3/6] perf(test_apps): Windows Vulkan cubes on dxr::VkFrameRing (INV-4.10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cube_handle_vk_win, cube_zones_vk_win and cube_zones_texture_vk_win waited the fence of the frame they had just submitted (one submit + wait per ZONE in the zones apps), and cube_handle_vk_win's HUD upload allocated/freed a command buffer and vkQueueWaitIdle'd on the frame path. - Single command buffer + frame fence -> dxr::VkFrameRing (default 3 in flight). Per frame: beginFrame() -> record -> endFrame() (ONE submit, no wait) -> release swapchain image(s) -> xrEndFrame. - Zones / texture zones: all zone layers record into the one ring frame; zone swapchains are released after the single submit. - Render passes gain a VK_SUBPASS_EXTERNAL dependency for the shared depth image (overlapping frames now write it). - One-off uploads (HUD, Local2D panel, zone strip, BGRA uploads) go through ring.submitImmediate(); cube_handle_vk_win's now-unused hudCmdPool is gone. - texture zones: ring.waitAll() before CleanupZones at shutdown (it relied on every frame being synchronous). The shared-texture D3D11 present path is unchanged — the app never reads its own Vulkan output. - MVPs stay push constants. Co-Authored-By: Claude Fable 5.1 --- test_apps/handle/cube_handle_vk_win/main.cpp | 196 +++++++----------- .../handle/cube_handle_vk_win/vk_renderer.cpp | 103 +++++---- .../handle/cube_handle_vk_win/vk_renderer.h | 8 +- test_apps/handle/cube_zones_vk_win/main.cpp | 52 ++--- .../handle/cube_zones_vk_win/vk_renderer.cpp | 164 +++++++-------- .../handle/cube_zones_vk_win/vk_renderer.h | 14 +- .../cube_zones_texture_vk_win/main.cpp | 102 +++++---- .../cube_zones_texture_vk_win/vk_renderer.cpp | 166 +++++++-------- .../cube_zones_texture_vk_win/vk_renderer.h | 13 +- 9 files changed, 370 insertions(+), 448 deletions(-) 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_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/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); From 41cdb923a19c3e438b00bf541db5a2c6e433e895 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 02:46:51 -0700 Subject: [PATCH 4/6] build(test_apps): move the displayxr-common pins to v2.29.0 with main main moved test_apps/common to v2.29.0 (dxr::CursorDepth, #1943) while this branch was open; v2.29.0 is v2.28.0 (dxr::VkFrameRing) plus that one additive helper. Keep every test app on the same tag rather than leave the standalone-fetching apps one tag behind. This also gives cube_handle_metal_macos the cursor_depth.h it already includes (main pins it at v2.27.0, which has no such header). Co-Authored-By: Claude Fable 5.1 --- test_apps/cube_handle_vk_linux/CMakeLists.txt | 3 ++- test_apps/cube_zones_vk_linux/CMakeLists.txt | 3 ++- test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt | 3 ++- test_apps/legacy/cube_hosted_legacy_metal_macos/CMakeLists.txt | 3 ++- test_apps/legacy/cube_hosted_legacy_vk_linux/CMakeLists.txt | 3 ++- test_apps/legacy/cube_hosted_legacy_vk_macos/CMakeLists.txt | 3 ++- .../texture/cube_zones_texture_metal_macos/CMakeLists.txt | 3 ++- test_apps/weave/weave_present_vk_linux/CMakeLists.txt | 3 ++- 8 files changed, 16 insertions(+), 8 deletions(-) diff --git a/test_apps/cube_handle_vk_linux/CMakeLists.txt b/test_apps/cube_handle_vk_linux/CMakeLists.txt index 3dcd7f1af2..d97c5d814b 100644 --- a/test_apps/cube_handle_vk_linux/CMakeLists.txt +++ b/test_apps/cube_handle_vk_linux/CMakeLists.txt @@ -90,7 +90,8 @@ FetchContent_Declare( # 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. - GIT_TAG v2.28.0 + # 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/CMakeLists.txt b/test_apps/cube_zones_vk_linux/CMakeLists.txt index 635368a649..14e6b99225 100644 --- a/test_apps/cube_zones_vk_linux/CMakeLists.txt +++ b/test_apps/cube_zones_vk_linux/CMakeLists.txt @@ -91,7 +91,8 @@ FetchContent_Declare( # 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. - GIT_TAG v2.28.0 + # 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/hosted/cube_hosted_metal_macos/CMakeLists.txt b/test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt index 85582fd9cb..7dbaed2abc 100644 --- a/test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt +++ b/test_apps/hosted/cube_hosted_metal_macos/CMakeLists.txt @@ -64,7 +64,8 @@ FetchContent_Declare( # 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. - GIT_TAG v2.28.0 + # 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 80d0d5fcc6..b5ebcd2eb6 100644 --- a/test_apps/legacy/cube_hosted_legacy_metal_macos/CMakeLists.txt +++ b/test_apps/legacy/cube_hosted_legacy_metal_macos/CMakeLists.txt @@ -64,7 +64,8 @@ FetchContent_Declare( # 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. - GIT_TAG v2.28.0 + # 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 1f383cfa40..6c78b67c9a 100644 --- a/test_apps/legacy/cube_hosted_legacy_vk_linux/CMakeLists.txt +++ b/test_apps/legacy/cube_hosted_legacy_vk_linux/CMakeLists.txt @@ -73,7 +73,8 @@ FetchContent_Declare( # 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. - GIT_TAG v2.28.0 + # 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/CMakeLists.txt b/test_apps/legacy/cube_hosted_legacy_vk_macos/CMakeLists.txt index 3b85bc5943..9420c000c9 100644 --- a/test_apps/legacy/cube_hosted_legacy_vk_macos/CMakeLists.txt +++ b/test_apps/legacy/cube_hosted_legacy_vk_macos/CMakeLists.txt @@ -70,7 +70,8 @@ FetchContent_Declare( # 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. - GIT_TAG v2.28.0 + # 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_metal_macos/CMakeLists.txt b/test_apps/texture/cube_zones_texture_metal_macos/CMakeLists.txt index 1bf7477cfd..a31057101e 100644 --- a/test_apps/texture/cube_zones_texture_metal_macos/CMakeLists.txt +++ b/test_apps/texture/cube_zones_texture_metal_macos/CMakeLists.txt @@ -66,7 +66,8 @@ FetchContent_Declare( # 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. - GIT_TAG v2.28.0 + # 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/weave/weave_present_vk_linux/CMakeLists.txt b/test_apps/weave/weave_present_vk_linux/CMakeLists.txt index 96a978f68d..bbcc1901b2 100644 --- a/test_apps/weave/weave_present_vk_linux/CMakeLists.txt +++ b/test_apps/weave/weave_present_vk_linux/CMakeLists.txt @@ -64,7 +64,8 @@ FetchContent_Declare( # 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. - GIT_TAG v2.28.0 + # 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) From 686930c1d21da0ee40223b0adbeb34d8e595e8f3 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 02:58:24 -0700 Subject: [PATCH 5/6] perf(test_apps): Android Vulkan cubes on dxr::VkFrameRing (INV-4.10, #1917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cube_handle_vk_android and cube_zones_vk_android allocated a command buffer, submitted, vkQueueWaitIdle'd and freed it in record_atlas() every frame ("no per-frame fence wired yet") — the serialised submit->wait loop that parks an Adreno GPU at ~1/3 clock (#1917; modelviewer#170 went 13 -> 30 fps on the same fix). - displayxr-common v2.29.0 fetched sources-only (its STATIC lib carries desktop sources), common/ appended last to the include path — the modelviewer pattern. - One dxr::VkFrameRing (framesInFlight = 3 explicitly; Android has no env): beginFrame() -> record every tile -> endFrame() (ONE submit, no wait) -> release swapchain image. - Render pass gains a VK_SUBPASS_EXTERNAL depth dependency (the one atlas depth image is now written by overlapping frames). - CPU-rewritten HUD vertex data gets one region per ring slot (fallback HUD buffer sized by framesInFlight; HudFont glyph/panel buffers by HUD_FONT_MAX_SLOTS) so a frame in flight never reads the next frame's vertices. - zones: the static Local2D band upload goes through submitImmediate() once per swapchain image (uploaded_mask, reset on swapchain create/destroy) instead of a blocking submit every frame. - Teardown: waitAll() before freeing app resources, ring.destroy() before the device. Startup-only uploads in crate_scene / hud_font unchanged. Built locally with Gradle + NDK 26.3 (no device touched). Co-Authored-By: Claude Fable 5.1 --- .../src/main/cpp/CMakeLists.txt | 16 ++ .../src/main/cpp/hud_font.cpp | 22 +- .../src/main/cpp/hud_font.h | 5 + .../src/main/cpp/main.cpp | 112 +++++----- .../src/main/cpp/CMakeLists.txt | 16 ++ .../src/main/cpp/hud_font.cpp | 22 +- .../src/main/cpp/hud_font.h | 5 + .../src/main/cpp/main.cpp | 202 +++++++++--------- 8 files changed, 236 insertions(+), 164 deletions(-) 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/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() && From 517375c85cbcd7019e66a09b81e5af34d957ffd3 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 02:58:25 -0700 Subject: [PATCH 6/6] perf(test_apps): legacy hosted VK cubes + weave_client_vk_android on dxr::VkFrameRing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same INV-4.10 treatment for the small apps — even tiny ones use the shared helper rather than a second hand-rolled fence scheme. - cube_hosted_legacy_vk_{win,linux,macos}: RenderScene() submitted then waited its own fence ("runtime needs the image ready"). The runtime composes on the same queue, so queue order — not a CPU wait — is what makes the image ready. Now beginFrame()/endFrame() (default 3 in flight), a VK_SUBPASS_EXTERNAL depth dependency for the shared depth image, and ring teardown before the device (incl. the renderer-init failure path). - weave_client_vk_android: the command pool/buffer/fence become a ring; present_woven() is beginFrame()/endFrame() with the acquire/present semaphores passed through endFrame() — no wait on the frame just submitted. upload_input() and the debug dump use submitImmediate(). One waitAll() per frame REMAINS, deliberately and commented: the runtime rewrites the single shared woven output on every xrWeaveSubmitDXR and Android has no acquire fence, so the previous present blit must finish first. CPU painting now overlaps that blit; true overlap needs a sync_file-style acquire fence on Android (weave spec v10 is desktop-Linux only). Co-Authored-By: Claude Fable 5.1 --- .../cube_hosted_legacy_vk_linux/main.cpp | 96 +++++----- .../cube_hosted_legacy_vk_macos/main.cpp | 93 +++++----- .../legacy/cube_hosted_legacy_vk_win/main.cpp | 73 ++++---- .../src/main/cpp/CMakeLists.txt | 16 ++ .../src/main/cpp/main.cpp | 168 ++++++++---------- 5 files changed, 213 insertions(+), 233 deletions(-) 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/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/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); }