diff --git a/test_apps/common/CMakeLists.txt b/test_apps/common/CMakeLists.txt index aa9e40edb..008925e25 100644 --- a/test_apps/common/CMakeLists.txt +++ b/test_apps/common/CMakeLists.txt @@ -76,9 +76,8 @@ target_link_libraries(sr_common PUBLIC dwrite ) -# D3D11 atlas readback. Pulled into sr_common (not sr_common_base) so -# only D3D11-linking targets get it — the rest of the cube_handle apps -# add their respective atlas_capture_.cpp directly. -target_sources(sr_common PRIVATE - atlas_capture_d3d11.cpp -) +# Atlas capture is runtime-owned now (XR_EXT_atlas_capture); the cube_handle +# apps call dxr_capture::RequestRuntimeAtlasCapture (in sr_common_base) instead +# of a per-API readback. The Windows-only D3D11/D3D12 readback TUs were deleted; +# atlas_capture_{gl,vk}.cpp + atlas_capture_metal.mm remain for the macOS apps, +# which haven't migrated yet. diff --git a/test_apps/common/atlas_capture.cpp b/test_apps/common/atlas_capture.cpp index e3204057d..f21868415 100644 --- a/test_apps/common/atlas_capture.cpp +++ b/test_apps/common/atlas_capture.cpp @@ -22,6 +22,8 @@ #include "stb_image_write.h" #include "atlas_capture.h" +#include "xr_session_common.h" // XrSessionManager + XR_EXT_atlas_capture +#include "logging.h" #pragma comment(lib, "shell32.lib") #pragma comment(lib, "ole32.lib") @@ -159,4 +161,39 @@ void TickCaptureFlash(HWND parent) { SetLayeredWindowAttributes(g_flashHwnd, 0, (BYTE)g_flashAlpha, LWA_ALPHA); } +// --------------------------------------------------------------------------- +// Runtime-owned atlas capture (XR_EXT_atlas_capture) — the unified path. +// --------------------------------------------------------------------------- + +bool RequestRuntimeAtlasCapture(const ::XrSessionManager& xr, + const char* appName, + uint32_t tileColumns, + uint32_t tileRows, + HWND flashHwnd) { + if (xr.pfnCaptureAtlasEXT == nullptr || xr.session == XR_NULL_HANDLE) { + LOG_WARN("Atlas capture unavailable: XR_EXT_atlas_capture not active"); + return false; + } + // Captures the app's projection atlas; meaningless for a mono (1×1) layout. + if (tileColumns <= 1 && tileRows <= 1) { + LOG_INFO("Capture skipped: need 3D mode with cols/rows > 1"); + return false; + } + + std::string prefix = MakeCaptureAtlasPrefix(appName ? appName : "capture", tileColumns, tileRows); + XrAtlasCaptureInfoEXT info = {XR_TYPE_ATLAS_CAPTURE_INFO_EXT}; + info.next = nullptr; + info.stage = XR_ATLAS_CAPTURE_STAGE_PROJECTION_ONLY_EXT; + strncpy_s(info.pathPrefix, prefix.c_str(), _TRUNCATE); + + XrResult cr = xr.pfnCaptureAtlasEXT(xr.session, &info, nullptr); + if (XR_SUCCEEDED(cr)) { + LOG_INFO("Atlas capture requested -> %s_atlas.png", prefix.c_str()); + PostFlashRequest(flashHwnd); + return true; + } + LOG_WARN("xrCaptureAtlasEXT failed: 0x%x", (unsigned)cr); + return false; +} + } // namespace dxr_capture diff --git a/test_apps/common/atlas_capture.h b/test_apps/common/atlas_capture.h index 90837e146..52e5d922f 100644 --- a/test_apps/common/atlas_capture.h +++ b/test_apps/common/atlas_capture.h @@ -70,6 +70,11 @@ typedef struct VkCommandPool_T* VkCommandPool; typedef struct VkImage_T* VkImage; #endif +// Forward-declared (global scope) so the runtime-capture helper below can take +// it without this header pulling in xr_session_common.h; atlas_capture.cpp +// includes that header for the full definition. +struct XrSessionManager; + namespace dxr_capture { // --------------------------------------------------------------------------- @@ -130,6 +135,23 @@ void TickCaptureFlash(HWND parent); inline void PostFlashRequest(HWND hwnd) { PostMessageW(hwnd, kFlashUserMsg, 0, 0); } + +// --------------------------------------------------------------------------- +// Runtime-owned atlas capture (XR_EXT_atlas_capture). The single, graphics- +// API-agnostic capture path: the runtime does the GPU readback, so apps no +// longer need a per-API CaptureAtlasRegion* helper. Handles the 3D-mode guard, +// filename numbering (MakeCaptureAtlasPrefix), the xrCaptureAtlasEXT call +// (PROJECTION_ONLY = the app's own projection atlas), the flash overlay, and +// logging. Call from the render loop when the 'I' key flag is set. +// +// Returns true iff a capture was requested. No-ops (returns false) when the +// runtime didn't expose the extension (pfn NULL) or for mono/1×1 layouts. +// --------------------------------------------------------------------------- +bool RequestRuntimeAtlasCapture(const ::XrSessionManager& xr, + const char* appName, + uint32_t tileColumns, + uint32_t tileRows, + HWND flashHwnd); #endif #ifdef __APPLE__ @@ -171,30 +193,10 @@ bool CaptureAtlasRegionVk(VkDevice device, const std::string& outPath, bool linearBytesInSrgbImage = false); -#ifdef _WIN32 -bool CaptureAtlasRegionD3D11(ID3D11Device* device, - ID3D11DeviceContext* context, - ID3D11Texture2D* srcTex, - uint32_t rectX, - uint32_t rectY, - uint32_t rectW, - uint32_t rectH, - const std::string& outPath); - -bool CaptureAtlasRegionD3D12(ID3D12Device* device, - ID3D12CommandQueue* queue, - ID3D12Resource* srcTex, - uint32_t srcImageWidth, - uint32_t srcImageHeight, - // Resource state on entry; we transition back - // to it before returning (caller's lifecycle). - int /*D3D12_RESOURCE_STATES*/ entryState, - uint32_t rectX, - uint32_t rectY, - uint32_t rectW, - uint32_t rectH, - const std::string& outPath); -#endif +// NB: the D3D11/D3D12 per-API readback helpers were removed — those apps use +// the runtime-owned dxr_capture::RequestRuntimeAtlasCapture (XR_EXT_atlas_capture) +// above. The VK / GL / Metal readbacks below remain for the macOS apps, which +// have not migrated yet. // OpenGL helper. Available on both Windows and macOS — the caller must have // a current GL context bound. Loads its own FBO/blit function pointers diff --git a/test_apps/common/atlas_capture_d3d11.cpp b/test_apps/common/atlas_capture_d3d11.cpp deleted file mode 100644 index b0c499ada..000000000 --- a/test_apps/common/atlas_capture_d3d11.cpp +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2026, Leia Inc. -// SPDX-License-Identifier: BSL-1.0 -/*! - * @file - * @brief D3D11 host-readback path for the 'I' key atlas capture. - * - * Same staging-texture pattern used by the service compositor's - * `comp_d3d11_service_capture_frame()` (see - * src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp:10331), generalised - * to (a) take an arbitrary swapchain texture from the caller and (b) handle - * BGRA→RGBA byte-swap (the service atlas is hard-coded RGBA8; cube-app - * swapchains may be either). - */ - -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include -#include -#include - -#include -#include -#include -#include - -#include "stb_image_write.h" -#include "atlas_capture.h" - -namespace dxr_capture { - -namespace { - -bool IsBgraFormat(DXGI_FORMAT f) { - return f == DXGI_FORMAT_B8G8R8A8_UNORM || - f == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB || - f == DXGI_FORMAT_B8G8R8A8_TYPELESS; -} - -} // namespace - -bool CaptureAtlasRegionD3D11(ID3D11Device* device, - ID3D11DeviceContext* context, - ID3D11Texture2D* srcTex, - uint32_t rectX, - uint32_t rectY, - uint32_t rectW, - uint32_t rectH, - const std::string& outPath) { - if (device == nullptr || context == nullptr || srcTex == nullptr) return false; - if (rectW == 0 || rectH == 0) return false; - - D3D11_TEXTURE2D_DESC desc; - srcTex->GetDesc(&desc); - if ((uint64_t)rectX + rectW > desc.Width) return false; - if ((uint64_t)rectY + rectH > desc.Height) return false; - - // Staging texture matching the source — STAGING + CPU_READ. - D3D11_TEXTURE2D_DESC sd = desc; - sd.Usage = D3D11_USAGE_STAGING; - sd.BindFlags = 0; - sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; - sd.MiscFlags = 0; - sd.SampleDesc = {1, 0}; // staging textures must be non-multisampled - sd.ArraySize = 1; - sd.MipLevels = 1; - ID3D11Texture2D* staging = nullptr; - if (FAILED(device->CreateTexture2D(&sd, nullptr, &staging)) || !staging) { - return false; - } - - // CopySubresourceRegion to copy just the requested sub-rect into the - // staging texture's top-left. (CopyResource would copy the whole image, - // including any black padding outside the active atlas region.) - D3D11_BOX box; - box.left = rectX; - box.top = rectY; - box.front = 0; - box.right = rectX + rectW; - box.bottom = rectY + rectH; - box.back = 1; - context->CopySubresourceRegion(staging, 0, 0, 0, 0, srcTex, 0, &box); - - D3D11_MAPPED_SUBRESOURCE m{}; - if (FAILED(context->Map(staging, 0, D3D11_MAP_READ, 0, &m))) { - staging->Release(); - return false; - } - - // Tightly-pack RGBA into a contiguous buffer (RowPitch may exceed rectW*4). - std::vector rgba((size_t)rectW * rectH * 4u); - const uint8_t* src = static_cast(m.pData); - for (uint32_t y = 0; y < rectH; y++) { - std::memcpy(rgba.data() + (size_t)y * rectW * 4u, - src + (size_t)y * m.RowPitch, - (size_t)rectW * 4u); - } - context->Unmap(staging, 0); - staging->Release(); - - if (IsBgraFormat(desc.Format)) { - for (size_t i = 0; i < rgba.size(); i += 4) { - std::swap(rgba[i + 0], rgba[i + 2]); - } - } - - int ok = stbi_write_png(outPath.c_str(), (int)rectW, (int)rectH, 4, - rgba.data(), (int)rectW * 4); - return ok != 0; -} - -} // namespace dxr_capture diff --git a/test_apps/common/atlas_capture_d3d12.cpp b/test_apps/common/atlas_capture_d3d12.cpp deleted file mode 100644 index 3bf5cb827..000000000 --- a/test_apps/common/atlas_capture_d3d12.cpp +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2026, Leia Inc. -// SPDX-License-Identifier: BSL-1.0 -/*! - * @file - * @brief D3D12 host-readback path for the 'I' key atlas capture. - * - * One-shot allocator/list/fence on the caller's command queue: transition - * srcTex → COPY_SOURCE, CopyTextureRegion to a readback buffer, transition - * back, signal fence, wait. Tightly-pack the readback rows (D3D12 requires - * a 256-byte aligned row pitch) into RGBA, swap BGRA→RGBA if needed, and - * write a PNG via stb_image_write. - * - * Caller passes `entryState` so we can return the resource to whatever - * state the runtime expects after the capture (typically COMMON or - * RENDER_TARGET — passed as `int` to keep the header DXGI-free). - */ - -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include -#include -#include - -#include -#include -#include -#include - -#include "stb_image_write.h" -#include "atlas_capture.h" - -namespace dxr_capture { - -namespace { - -bool IsBgraFormat(DXGI_FORMAT f) { - return f == DXGI_FORMAT_B8G8R8A8_UNORM || - f == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB || - f == DXGI_FORMAT_B8G8R8A8_TYPELESS; -} - -constexpr uint32_t kRowAlign = D3D12_TEXTURE_DATA_PITCH_ALIGNMENT; // 256 - -uint32_t AlignedRowPitch(uint32_t bytes) { - return (bytes + kRowAlign - 1) & ~(kRowAlign - 1); -} - -} // namespace - -bool CaptureAtlasRegionD3D12(ID3D12Device* device, - ID3D12CommandQueue* queue, - ID3D12Resource* srcTex, - uint32_t srcImageWidth, - uint32_t srcImageHeight, - int entryState, - uint32_t rectX, - uint32_t rectY, - uint32_t rectW, - uint32_t rectH, - const std::string& outPath) { - if (device == nullptr || queue == nullptr || srcTex == nullptr) return false; - if (rectW == 0 || rectH == 0) return false; - if ((uint64_t)rectX + rectW > srcImageWidth) return false; - if ((uint64_t)rectY + rectH > srcImageHeight) return false; - - D3D12_RESOURCE_DESC desc = srcTex->GetDesc(); - DXGI_FORMAT format = desc.Format; - - const uint32_t rowBytes = rectW * 4u; - const uint32_t rowPitch = AlignedRowPitch(rowBytes); - const uint64_t bufBytes = (uint64_t)rowPitch * rectH; - - // Readback heap + buffer for the GPU→CPU copy. - D3D12_HEAP_PROPERTIES rbHeap{}; - rbHeap.Type = D3D12_HEAP_TYPE_READBACK; - rbHeap.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; - rbHeap.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; - rbHeap.CreationNodeMask = 1; - rbHeap.VisibleNodeMask = 1; - - D3D12_RESOURCE_DESC rbDesc{}; - rbDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; - rbDesc.Alignment = 0; - rbDesc.Width = bufBytes; - rbDesc.Height = 1; - rbDesc.DepthOrArraySize = 1; - rbDesc.MipLevels = 1; - rbDesc.Format = DXGI_FORMAT_UNKNOWN; - rbDesc.SampleDesc = {1, 0}; - rbDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; - rbDesc.Flags = D3D12_RESOURCE_FLAG_NONE; - - ID3D12Resource* readback = nullptr; - if (FAILED(device->CreateCommittedResource( - &rbHeap, D3D12_HEAP_FLAG_NONE, &rbDesc, - D3D12_RESOURCE_STATE_COPY_DEST, nullptr, - IID_PPV_ARGS(&readback)))) { - return false; - } - - // One-shot allocator / list on the same queue type (DIRECT — copies are - // legal on direct queues; matches the caller's render queue). - ID3D12CommandAllocator* alloc = nullptr; - if (FAILED(device->CreateCommandAllocator( - D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&alloc))) || !alloc) { - readback->Release(); - return false; - } - ID3D12GraphicsCommandList* list = nullptr; - if (FAILED(device->CreateCommandList( - 0, D3D12_COMMAND_LIST_TYPE_DIRECT, alloc, nullptr, - IID_PPV_ARGS(&list))) || !list) { - alloc->Release(); - readback->Release(); - return false; - } - - auto barrier = [](ID3D12Resource* r, D3D12_RESOURCE_STATES before, - D3D12_RESOURCE_STATES after) { - D3D12_RESOURCE_BARRIER b{}; - b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; - b.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; - b.Transition.pResource = r; - b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; - b.Transition.StateBefore = before; - b.Transition.StateAfter = after; - return b; - }; - - const auto entry = static_cast(entryState); - - if (entry != D3D12_RESOURCE_STATE_COPY_SOURCE) { - D3D12_RESOURCE_BARRIER toCopy = barrier(srcTex, entry, D3D12_RESOURCE_STATE_COPY_SOURCE); - list->ResourceBarrier(1, &toCopy); - } - - D3D12_TEXTURE_COPY_LOCATION dst{}; - dst.pResource = readback; - dst.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; - dst.PlacedFootprint.Offset = 0; - dst.PlacedFootprint.Footprint.Format = format; - dst.PlacedFootprint.Footprint.Width = rectW; - dst.PlacedFootprint.Footprint.Height = rectH; - dst.PlacedFootprint.Footprint.Depth = 1; - dst.PlacedFootprint.Footprint.RowPitch = rowPitch; - - D3D12_TEXTURE_COPY_LOCATION src{}; - src.pResource = srcTex; - src.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; - src.SubresourceIndex = 0; - - D3D12_BOX box; - box.left = rectX; - box.top = rectY; - box.front = 0; - box.right = rectX + rectW; - box.bottom = rectY + rectH; - box.back = 1; - list->CopyTextureRegion(&dst, 0, 0, 0, &src, &box); - - if (entry != D3D12_RESOURCE_STATE_COPY_SOURCE) { - D3D12_RESOURCE_BARRIER toEntry = barrier(srcTex, D3D12_RESOURCE_STATE_COPY_SOURCE, entry); - list->ResourceBarrier(1, &toEntry); - } - list->Close(); - - ID3D12CommandList* lists[] = {list}; - queue->ExecuteCommandLists(1, lists); - - // Fence + wait for completion. - ID3D12Fence* fence = nullptr; - if (FAILED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence))) || !fence) { - list->Release(); - alloc->Release(); - readback->Release(); - return false; - } - HANDLE evt = CreateEventW(nullptr, FALSE, FALSE, nullptr); - queue->Signal(fence, 1); - fence->SetEventOnCompletion(1, evt); - WaitForSingleObject(evt, INFINITE); - CloseHandle(evt); - fence->Release(); - - // Map readback buffer and pack rows into a tight RGBA buffer. - D3D12_RANGE readRange{0, (SIZE_T)bufBytes}; - void* mapped = nullptr; - if (FAILED(readback->Map(0, &readRange, &mapped)) || !mapped) { - list->Release(); - alloc->Release(); - readback->Release(); - return false; - } - std::vector rgba((size_t)rowBytes * rectH); - const uint8_t* mp = static_cast(mapped); - for (uint32_t y = 0; y < rectH; y++) { - std::memcpy(rgba.data() + (size_t)y * rowBytes, - mp + (size_t)y * rowPitch, - rowBytes); - } - D3D12_RANGE wroteNothing{0, 0}; - readback->Unmap(0, &wroteNothing); - - list->Release(); - alloc->Release(); - readback->Release(); - - if (IsBgraFormat(format)) { - for (size_t i = 0; i < rgba.size(); i += 4) { - std::swap(rgba[i + 0], rgba[i + 2]); - } - } - - int ok = stbi_write_png(outPath.c_str(), (int)rectW, (int)rectH, 4, - rgba.data(), (int)rowBytes); - return ok != 0; -} - -} // namespace dxr_capture diff --git a/test_apps/cube_handle_d3d11_win/main.cpp b/test_apps/cube_handle_d3d11_win/main.cpp index d228c11ad..202ee6720 100644 --- a/test_apps/cube_handle_d3d11_win/main.cpp +++ b/test_apps/cube_handle_d3d11_win/main.cpp @@ -767,53 +767,12 @@ static void RenderOneFrame(RenderState& rs) { if (rtv) rtv->Release(); - // 'I' key: snapshot the multi-view atlas to a PNG. - // Skipped for mono (1×1) layouts. + // 'I' key: snapshot the multi-view atlas to a PNG via the + // runtime (XR_EXT_atlas_capture). Skipped for mono (1×1). if (g_inputState.captureAtlasRequested) { g_inputState.captureAtlasRequested = false; - if (!monoMode && (tileColumns > 1 || tileRows > 1)) { - if (xr.pfnCaptureAtlasEXT && xr.session != XR_NULL_HANDLE) { - // XR_EXT_atlas_capture (W6 of #396): the runtime owns - // the readback — no app-side staging texture. The latch - // is consumed by this iteration's xrEndFrame below, so it - // captures the current frame. The prefix has no ".png"; - // the runtime appends "_atlas.png". - std::string prefix = dxr_capture::MakeCaptureAtlasPrefix( - APP_NAME, tileColumns, tileRows); - XrAtlasCaptureInfoEXT info = {XR_TYPE_ATLAS_CAPTURE_INFO_EXT}; - info.next = nullptr; - info.stage = XR_ATLAS_CAPTURE_STAGE_PROJECTION_ONLY_EXT; - strncpy_s(info.pathPrefix, prefix.c_str(), _TRUNCATE); - XrResult cr = xr.pfnCaptureAtlasEXT(xr.session, &info, nullptr); - if (XR_SUCCEEDED(cr)) { - LOG_INFO("Atlas capture requested -> %s_atlas.png", - prefix.c_str()); - dxr_capture::PostFlashRequest(rs.hwnd); - } else { - LOG_WARN("xrCaptureAtlasEXT failed: 0x%x", (unsigned)cr); - } - } else { - // Fallback: legacy app-side readback when the runtime - // does not expose XR_EXT_atlas_capture. - std::string outPath = dxr_capture::MakeCapturePath( - APP_NAME, tileColumns, tileRows); - uint32_t atlasW = tileColumns * renderW; - uint32_t atlasH = tileRows * renderH; - if (atlasW <= xr.swapchain.width && atlasH <= xr.swapchain.height) { - bool ok = dxr_capture::CaptureAtlasRegionD3D11( - renderer.device.Get(), renderer.context.Get(), - swapchainTexture, - 0, 0, atlasW, atlasH, outPath); - if (ok) { - LOG_INFO("Captured atlas %ux%u -> %s", - atlasW, atlasH, outPath.c_str()); - dxr_capture::PostFlashRequest(rs.hwnd); - } - } - } - } else { - LOG_INFO("Capture skipped: need 3D mode with cols/rows > 1"); - } + dxr_capture::RequestRuntimeAtlasCapture( + xr, APP_NAME, tileColumns, tileRows, rs.hwnd); } ReleaseSwapchainImage(xr); diff --git a/test_apps/cube_handle_d3d12_win/CMakeLists.txt b/test_apps/cube_handle_d3d12_win/CMakeLists.txt index d7951f8e7..165f2adbb 100644 --- a/test_apps/cube_handle_d3d12_win/CMakeLists.txt +++ b/test_apps/cube_handle_d3d12_win/CMakeLists.txt @@ -76,8 +76,8 @@ add_executable(cube_handle_d3d12_win WIN32 ${CMAKE_CURRENT_SOURCE_DIR}/../common/hud_renderer.h ${CMAKE_CURRENT_SOURCE_DIR}/../common/text_overlay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../common/text_overlay.h - # 'I' key atlas capture: D3D12 readback (filename + flash come via sr_common_base) - ${CMAKE_CURRENT_SOURCE_DIR}/../common/atlas_capture_d3d12.cpp + # 'I' key atlas capture is runtime-owned now (XR_EXT_atlas_capture); the + # shared dxr_capture::RequestRuntimeAtlasCapture lives in sr_common_base. resource.rc ) diff --git a/test_apps/cube_handle_d3d12_win/main.cpp b/test_apps/cube_handle_d3d12_win/main.cpp index bff81d93e..20837ef85 100644 --- a/test_apps/cube_handle_d3d12_win/main.cpp +++ b/test_apps/cube_handle_d3d12_win/main.cpp @@ -539,28 +539,8 @@ static void RenderThreadFunc( std::lock_guard lk(g_inputMutex); g_inputState.captureAtlasRequested = false; } - if (!monoMode && (tileColumns > 1 || tileRows > 1)) { - uint32_t atlasW = tileColumns * renderW; - uint32_t atlasH = tileRows * renderH; - if (atlasW <= xr->swapchain.width && atlasH <= xr->swapchain.height) { - std::string outPath = dxr_capture::MakeCapturePath( - APP_NAME, tileColumns, tileRows); - bool ok = dxr_capture::CaptureAtlasRegionD3D12( - renderer->device.Get(), - renderer->commandQueue.Get(), - swapchainTexture, - xr->swapchain.width, xr->swapchain.height, - (int)D3D12_RESOURCE_STATE_COMMON, - 0, 0, atlasW, atlasH, outPath); - if (ok) { - LOG_INFO("Captured atlas %ux%u -> %s", - atlasW, atlasH, outPath.c_str()); - dxr_capture::PostFlashRequest(hwnd); - } - } - } else { - LOG_INFO("Capture skipped: need 3D mode with cols/rows > 1"); - } + dxr_capture::RequestRuntimeAtlasCapture( + *xr, APP_NAME, tileColumns, tileRows, hwnd); } ReleaseSwapchainImage(*xr); diff --git a/test_apps/cube_handle_d3d12_win/xr_session.cpp b/test_apps/cube_handle_d3d12_win/xr_session.cpp index 6bd6ace16..b041eefd1 100644 --- a/test_apps/cube_handle_d3d12_win/xr_session.cpp +++ b/test_apps/cube_handle_d3d12_win/xr_session.cpp @@ -78,6 +78,9 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (strcmp(ext.extensionName, XR_EXT_DISPLAY_INFO_EXTENSION_NAME) == 0) { xr.hasDisplayInfoExt = true; } + if (strcmp(ext.extensionName, XR_EXT_ATLAS_CAPTURE_EXTENSION_NAME) == 0) { + xr.hasAtlasCaptureExt = true; + } } LOG_INFO("XR_KHR_D3D12_enable: %s", hasD3D12 ? "AVAILABLE" : "NOT FOUND"); @@ -97,6 +100,9 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (xr.hasDisplayInfoExt) { enabledExtensions.push_back(XR_EXT_DISPLAY_INFO_EXTENSION_NAME); } + if (xr.hasAtlasCaptureExt) { + enabledExtensions.push_back(XR_EXT_ATLAS_CAPTURE_EXTENSION_NAME); + } LOG_INFO("Enabling %zu extensions", enabledExtensions.size()); for (const auto& ext : enabledExtensions) { @@ -179,6 +185,13 @@ bool InitializeOpenXR(XrSessionManager& xr) { xr.pfnRequestDisplayRenderingModeEXT ? "available" : "not available"); } + // XR_EXT_atlas_capture (#396 W6): resolve the runtime-owned capture entry. + if (xr.hasAtlasCaptureExt) { + xrGetInstanceProcAddr(xr.instance, "xrCaptureAtlasEXT", + (PFN_xrVoidFunction*)&xr.pfnCaptureAtlasEXT); + LOG_INFO("xrCaptureAtlasEXT: %s", xr.pfnCaptureAtlasEXT ? "resolved" : "NULL"); + } + LOG_INFO("Enumerating view configuration views..."); uint32_t viewCount = 0; XR_CHECK(xrEnumerateViewConfigurationViews(xr.instance, xr.systemId, xr.viewConfigType, 0, &viewCount, nullptr)); diff --git a/test_apps/cube_handle_gl_win/CMakeLists.txt b/test_apps/cube_handle_gl_win/CMakeLists.txt index 1a4772815..b169752e4 100644 --- a/test_apps/cube_handle_gl_win/CMakeLists.txt +++ b/test_apps/cube_handle_gl_win/CMakeLists.txt @@ -78,8 +78,9 @@ add_executable(cube_handle_gl_win WIN32 ${CMAKE_CURRENT_SOURCE_DIR}/../common/hud_renderer.h ${CMAKE_CURRENT_SOURCE_DIR}/../common/text_overlay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../common/text_overlay.h - # 'I' key atlas capture: GL readback (filename + flash come via sr_common_base) - ${CMAKE_CURRENT_SOURCE_DIR}/../common/atlas_capture_gl.cpp + # 'I' key atlas capture is runtime-owned now (XR_EXT_atlas_capture); the + # shared dxr_capture::RequestRuntimeAtlasCapture lives in sr_common_base. + # (atlas_capture_gl.cpp stays in the tree for the macOS GL app.) resource.rc ) diff --git a/test_apps/cube_handle_gl_win/main.cpp b/test_apps/cube_handle_gl_win/main.cpp index 2ac893bff..706f1b35f 100644 --- a/test_apps/cube_handle_gl_win/main.cpp +++ b/test_apps/cube_handle_gl_win/main.cpp @@ -612,25 +612,8 @@ static void RenderThreadFunc( std::lock_guard lk(g_inputMutex); g_inputState.captureAtlasRequested = false; } - if (!monoMode && (tileColumns > 1 || tileRows > 1)) { - uint32_t atlasW = tileColumns * renderW; - uint32_t atlasH = tileRows * renderH; - if (atlasW <= xr->swapchain.width && atlasH <= xr->swapchain.height) { - std::string outPath = dxr_capture::MakeCapturePath( - APP_NAME, tileColumns, tileRows); - bool ok = dxr_capture::CaptureAtlasRegionGL( - (uint32_t)(*swapchainImages)[imageIndex].image, - xr->swapchain.width, xr->swapchain.height, - 0, 0, atlasW, atlasH, outPath); - if (ok) { - LOG_INFO("Captured atlas %ux%u -> %s", - atlasW, atlasH, outPath.c_str()); - dxr_capture::PostFlashRequest(hwnd); - } - } - } else { - LOG_INFO("Capture skipped: need 3D mode with cols/rows > 1"); - } + dxr_capture::RequestRuntimeAtlasCapture( + *xr, APP_NAME, tileColumns, tileRows, hwnd); } ReleaseSwapchainImage(*xr); diff --git a/test_apps/cube_handle_gl_win/xr_session.cpp b/test_apps/cube_handle_gl_win/xr_session.cpp index 9ff5e16d2..04fcdcd01 100644 --- a/test_apps/cube_handle_gl_win/xr_session.cpp +++ b/test_apps/cube_handle_gl_win/xr_session.cpp @@ -81,6 +81,9 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (strcmp(ext.extensionName, XR_EXT_DISPLAY_INFO_EXTENSION_NAME) == 0) { xr.hasDisplayInfoExt = true; } + if (strcmp(ext.extensionName, XR_EXT_ATLAS_CAPTURE_EXTENSION_NAME) == 0) { + xr.hasAtlasCaptureExt = true; + } } LOG_INFO("XR_KHR_opengl_enable: %s", hasOpenGL ? "AVAILABLE" : "NOT FOUND"); @@ -100,6 +103,9 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (xr.hasDisplayInfoExt) { enabledExtensions.push_back(XR_EXT_DISPLAY_INFO_EXTENSION_NAME); } + if (xr.hasAtlasCaptureExt) { + enabledExtensions.push_back(XR_EXT_ATLAS_CAPTURE_EXTENSION_NAME); + } XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO}; strcpy_s(createInfo.applicationInfo.applicationName, "SRCubeOpenXRExtGL"); @@ -182,6 +188,13 @@ bool InitializeOpenXR(XrSessionManager& xr) { xr.pfnRequestDisplayRenderingModeEXT ? "available" : "not available"); } + // XR_EXT_atlas_capture (#396 W6): resolve the runtime-owned capture entry. + if (xr.hasAtlasCaptureExt) { + xrGetInstanceProcAddr(xr.instance, "xrCaptureAtlasEXT", + (PFN_xrVoidFunction*)&xr.pfnCaptureAtlasEXT); + LOG_INFO("xrCaptureAtlasEXT: %s", xr.pfnCaptureAtlasEXT ? "resolved" : "NULL"); + } + uint32_t viewCount = 0; XR_CHECK(xrEnumerateViewConfigurationViews(xr.instance, xr.systemId, xr.viewConfigType, 0, &viewCount, nullptr)); xr.configViews.resize(viewCount, {XR_TYPE_VIEW_CONFIGURATION_VIEW}); diff --git a/test_apps/cube_handle_vk_win/CMakeLists.txt b/test_apps/cube_handle_vk_win/CMakeLists.txt index b50c65d61..106e037b4 100644 --- a/test_apps/cube_handle_vk_win/CMakeLists.txt +++ b/test_apps/cube_handle_vk_win/CMakeLists.txt @@ -84,8 +84,9 @@ add_executable(cube_handle_vk_win WIN32 ${CMAKE_CURRENT_SOURCE_DIR}/../common/hud_renderer.h ${CMAKE_CURRENT_SOURCE_DIR}/../common/text_overlay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../common/text_overlay.h - # 'I' key atlas capture: VK readback (filename + flash come via sr_common_base) - ${CMAKE_CURRENT_SOURCE_DIR}/../common/atlas_capture_vk.cpp + # 'I' key atlas capture is runtime-owned now (XR_EXT_atlas_capture); the + # shared dxr_capture::RequestRuntimeAtlasCapture lives in sr_common_base. + # (atlas_capture_vk.cpp stays in the tree for the macOS VK app.) resource.rc ) diff --git a/test_apps/cube_handle_vk_win/main.cpp b/test_apps/cube_handle_vk_win/main.cpp index bb72420c3..a80c402bc 100644 --- a/test_apps/cube_handle_vk_win/main.cpp +++ b/test_apps/cube_handle_vk_win/main.cpp @@ -590,29 +590,8 @@ static void RenderThreadFunc( std::lock_guard lk(g_inputMutex); g_inputState.captureAtlasRequested = false; } - if (!monoMode && (tileColumns > 1 || tileRows > 1) && - g_vkSwapchainImages != nullptr) { - uint32_t atlasW = tileColumns * renderW; - uint32_t atlasH = tileRows * renderH; - if (atlasW <= xr->swapchain.width && atlasH <= xr->swapchain.height) { - std::string outPath = dxr_capture::MakeCapturePath( - APP_NAME, tileColumns, tileRows); - bool ok = dxr_capture::CaptureAtlasRegionVk( - renderer->device, g_vkPhysDevice, - renderer->graphicsQueue, renderer->commandPool, - (*g_vkSwapchainImages)[imageIndex].image, - (int)g_vkColorFormat, - xr->swapchain.width, xr->swapchain.height, - 0, 0, atlasW, atlasH, outPath); - if (ok) { - LOG_INFO("Captured atlas %ux%u -> %s", - atlasW, atlasH, outPath.c_str()); - dxr_capture::PostFlashRequest(hwnd); - } - } - } else { - LOG_INFO("Capture skipped: need 3D mode with cols/rows > 1"); - } + dxr_capture::RequestRuntimeAtlasCapture( + *xr, APP_NAME, tileColumns, tileRows, hwnd); } LOG_INFO("[FRAME] ReleaseSwapchainImage..."); diff --git a/test_apps/cube_handle_vk_win/xr_session.cpp b/test_apps/cube_handle_vk_win/xr_session.cpp index 14d62f92a..5b43b1eaf 100644 --- a/test_apps/cube_handle_vk_win/xr_session.cpp +++ b/test_apps/cube_handle_vk_win/xr_session.cpp @@ -50,6 +50,9 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (strcmp(ext.extensionName, XR_EXT_DISPLAY_INFO_EXTENSION_NAME) == 0) { xr.hasDisplayInfoExt = true; } + if (strcmp(ext.extensionName, XR_EXT_ATLAS_CAPTURE_EXTENSION_NAME) == 0) { + xr.hasAtlasCaptureExt = true; + } } LOG_INFO("XR_KHR_vulkan_enable: %s", hasVulkan ? "AVAILABLE" : "NOT FOUND"); @@ -69,6 +72,9 @@ bool InitializeOpenXR(XrSessionManager& xr) { if (xr.hasDisplayInfoExt) { enabledExtensions.push_back(XR_EXT_DISPLAY_INFO_EXTENSION_NAME); } + if (xr.hasAtlasCaptureExt) { + enabledExtensions.push_back(XR_EXT_ATLAS_CAPTURE_EXTENSION_NAME); + } XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO}; strcpy_s(createInfo.applicationInfo.applicationName, "SRCubeOpenXRExtVK"); @@ -142,6 +148,13 @@ bool InitializeOpenXR(XrSessionManager& xr) { (PFN_xrVoidFunction*)&xr.pfnEnumerateDisplayRenderingModesEXT); } + // XR_EXT_atlas_capture (#396 W6): resolve the runtime-owned capture entry. + if (xr.hasAtlasCaptureExt) { + xrGetInstanceProcAddr(xr.instance, "xrCaptureAtlasEXT", + (PFN_xrVoidFunction*)&xr.pfnCaptureAtlasEXT); + LOG_INFO("xrCaptureAtlasEXT: %s", xr.pfnCaptureAtlasEXT ? "resolved" : "NULL"); + } + uint32_t viewCount = 0; XR_CHECK(xrEnumerateViewConfigurationViews(xr.instance, xr.systemId, xr.viewConfigType, 0, &viewCount, nullptr)); xr.configViews.resize(viewCount, {XR_TYPE_VIEW_CONFIGURATION_VIEW});