mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 07:00:30 +02:00
The panel was drawn into the eye images, so below a 1.00x OpenXR resolution its 1440x1080 canvas was minified into a small eye region and the text became hard to read. It is now submitted as a quad layer of its own over the scene's projection or menu quad, which the compositor samples directly at any render scale. Each backend (D3D12, Windows Vulkan, Quest) makes the panel's swapchain pair (plus two shared buffers on the Quest) the first time the panel opens. While it is open, the frame hands Aurora one more stereo target after the eyes, which the bridge fills with the panel texture or a transparent image. The panel image follows the eyes' displayed/retained pairing, so a cancelled frame never shows an unwritten panel. The quad hangs where the pointer's hits are tested. Aurora leaves the panel out of the eyes in layer mode, and a backend that cannot make the layer falls back to drawing it into the eyes. On the Quest, debug.wiicompiled.panel_layer 0 selects the old path at run time. Measured there at a race start (render_scale 0.8), the layer costs nothing while the panel is closed; while it is open, app GPU time is 10.5 ms against 9.7 ms drawn into the eyes, with unchanged frame rates. The replay tests cover lazy creation, cancelled frames, closed and unplaced panels, and render-first pacing on D3D12 and Windows Vulkan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
804 lines
29 KiB
C++
804 lines
29 KiB
C++
#include <aurora/d3d12_interop.h>
|
|
|
|
#include "../internal.hpp"
|
|
#include "../stereo.hpp"
|
|
#include "../stereo_overlay.hpp"
|
|
#include "gpu.hpp"
|
|
|
|
#if defined(_WIN32) && defined(WEBGPU_DAWN) && defined(DAWN_ENABLE_BACKEND_D3D12)
|
|
|
|
#include <dawn/native/D3D12Backend.h>
|
|
|
|
#include <d3d12.h>
|
|
#include <dxgi1_4.h>
|
|
#include <windows.h>
|
|
#include <wrl/client.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace aurora::d3d12_interop {
|
|
namespace {
|
|
|
|
using Microsoft::WRL::ComPtr;
|
|
|
|
Module Log("aurora::d3d12_interop");
|
|
|
|
constexpr uint64_t kUnfencedSubmission = (std::numeric_limits<uint64_t>::max)();
|
|
|
|
constexpr char kGetDeviceExport[] =
|
|
"?GetD3D12Device@d3d12@native@dawn@@YA?AV?$ComPtr@UID3D12Device@@@WRL@Microsoft@@PEAUWGPUDeviceImpl@@@Z";
|
|
constexpr char kGetQueueExport[] =
|
|
"?GetD3D12CommandQueue@d3d12@native@dawn@@YA?AV?$ComPtr@UID3D12CommandQueue@@@WRL@Microsoft@@PEAUWGPUDeviceImpl@@@Z";
|
|
|
|
struct NativeObjects {
|
|
ComPtr<ID3D12Device> device;
|
|
ComPtr<ID3D12CommandQueue> queue;
|
|
};
|
|
|
|
int64_t to_dxgi_format(wgpu::TextureFormat format) noexcept {
|
|
switch (format) {
|
|
case wgpu::TextureFormat::RGBA8Unorm:
|
|
return DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
case wgpu::TextureFormat::RGBA8UnormSrgb:
|
|
return DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
|
|
case wgpu::TextureFormat::BGRA8Unorm:
|
|
return DXGI_FORMAT_B8G8R8A8_UNORM;
|
|
case wgpu::TextureFormat::BGRA8UnormSrgb:
|
|
return DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
|
|
case wgpu::TextureFormat::RGBA16Float:
|
|
return DXGI_FORMAT_R16G16B16A16_FLOAT;
|
|
default:
|
|
return DXGI_FORMAT_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
bool same_copy_family(DXGI_FORMAT left, DXGI_FORMAT right) noexcept {
|
|
const auto family = [](DXGI_FORMAT format) {
|
|
switch (format) {
|
|
case DXGI_FORMAT_R8G8B8A8_TYPELESS:
|
|
case DXGI_FORMAT_R8G8B8A8_UNORM:
|
|
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
|
|
return 1;
|
|
case DXGI_FORMAT_B8G8R8A8_TYPELESS:
|
|
case DXGI_FORMAT_B8G8R8A8_UNORM:
|
|
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
|
|
return 2;
|
|
case DXGI_FORMAT_R16G16B16A16_TYPELESS:
|
|
case DXGI_FORMAT_R16G16B16A16_FLOAT:
|
|
return 3;
|
|
default:
|
|
return 0;
|
|
}
|
|
};
|
|
const int leftFamily = family(left);
|
|
return leftFamily != 0 && leftFamily == family(right);
|
|
}
|
|
|
|
bool get_native_objects(NativeObjects& objects) noexcept {
|
|
if (!webgpu::g_device || webgpu::g_backendType != wgpu::BackendType::D3D12) {
|
|
return false;
|
|
}
|
|
|
|
#if defined(__MINGW32__)
|
|
// The distributed Dawn DLL is built by MSVC. LLVM-MinGW uses a different
|
|
// C++ symbol spelling, but Win64's calling ABI is identical. Resolve the two
|
|
// pinned native exports explicitly and keep all public interop C-compatible.
|
|
static_assert(sizeof(ComPtr<ID3D12Device>) == sizeof(void*));
|
|
HMODULE dawnModule = GetModuleHandleW(L"webgpu_dawn.dll");
|
|
if (dawnModule == nullptr) {
|
|
Log.error("webgpu_dawn.dll is not loaded; native D3D12 interop is unavailable");
|
|
return false;
|
|
}
|
|
using GetDeviceFn = ComPtr<ID3D12Device> (*)(WGPUDevice);
|
|
using GetQueueFn = ComPtr<ID3D12CommandQueue> (*)(WGPUDevice);
|
|
const auto getDevice = reinterpret_cast<GetDeviceFn>(GetProcAddress(dawnModule, kGetDeviceExport));
|
|
const auto getQueue = reinterpret_cast<GetQueueFn>(GetProcAddress(dawnModule, kGetQueueExport));
|
|
if (getDevice == nullptr || getQueue == nullptr) {
|
|
Log.error("Pinned Dawn native D3D12 exports are unavailable");
|
|
return false;
|
|
}
|
|
objects.device = getDevice(webgpu::g_device.Get());
|
|
objects.queue = getQueue(webgpu::g_device.Get());
|
|
#else
|
|
objects.device = dawn::native::d3d12::GetD3D12Device(webgpu::g_device.Get());
|
|
objects.queue = dawn::native::d3d12::GetD3D12CommandQueue(webgpu::g_device.Get());
|
|
#endif
|
|
return objects.device != nullptr && objects.queue != nullptr;
|
|
}
|
|
|
|
// Dawn exposes this descriptor only through a native C++ type. Its ABI is a
|
|
// chained header followed by a ComPtr, so spell the wire layout locally and
|
|
// enter Dawn through the ordinary WebGPU C API instead of linking a C++ ctor.
|
|
struct SharedTextureMemoryD3D12ResourceWire {
|
|
wgpu::ChainedStruct chain{};
|
|
ComPtr<ID3D12Resource> resource;
|
|
};
|
|
|
|
struct SharedFenceDxgiHandleWire {
|
|
wgpu::ChainedStruct chain{};
|
|
void* handle = nullptr;
|
|
};
|
|
|
|
// The eyes, then the settings panel's layer image in a slot of its own so the
|
|
// eye intermediates are never resized for it.
|
|
constexpr uint32_t kPanelIndex = AURORA_D3D12_STEREO_MAX_TARGETS;
|
|
constexpr uint32_t kMaxImages = AURORA_D3D12_STEREO_MAX_TARGETS + 1;
|
|
|
|
struct IntermediateEye {
|
|
ComPtr<ID3D12Resource> resource;
|
|
wgpu::SharedTextureMemory memory;
|
|
wgpu::Texture texture;
|
|
wgpu::TextureFormat webgpuFormat = wgpu::TextureFormat::Undefined;
|
|
DXGI_FORMAT dxgiFormat = DXGI_FORMAT_UNKNOWN;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
bool initialized = false;
|
|
bool accessBegun = false;
|
|
};
|
|
|
|
struct PendingTarget {
|
|
ComPtr<ID3D12Resource> resource;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
DXGI_FORMAT format = DXGI_FORMAT_UNKNOWN;
|
|
};
|
|
|
|
struct InFlightCommand {
|
|
uint64_t fenceValue = 0;
|
|
ComPtr<ID3D12CommandAllocator> allocator;
|
|
ComPtr<ID3D12GraphicsCommandList> list;
|
|
// D3D12 command lists do not retain application resource references. Keep
|
|
// both sides of every copy alive until this submission's fence completes;
|
|
// an eye-size change may otherwise replace the bridge intermediate while
|
|
// the GPU is still reading it.
|
|
std::array<ComPtr<ID3D12Resource>, kMaxImages> sources;
|
|
std::array<ComPtr<ID3D12Resource>, kMaxImages> destinations;
|
|
};
|
|
|
|
class StereoBridge final {
|
|
public:
|
|
StereoBridge(NativeObjects objects, AuroraD3D12StereoSubmittedCallback callback,
|
|
void* userdata) noexcept
|
|
: m_device(std::move(objects.device)), m_queue(std::move(objects.queue)),
|
|
m_callback(callback), m_userdata(userdata) {}
|
|
|
|
bool Initialize() noexcept {
|
|
if (!webgpu::g_device.HasFeature(wgpu::FeatureName::SharedTextureMemoryD3D12Resource)) {
|
|
Log.error("Dawn device lacks SharedTextureMemoryD3D12Resource");
|
|
return false;
|
|
}
|
|
if (FAILED(m_device->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_PPV_ARGS(&m_fence)))) {
|
|
Log.error("Could not create the D3D12 interop fence");
|
|
return false;
|
|
}
|
|
|
|
if (webgpu::g_device.HasFeature(wgpu::FeatureName::SharedFenceDXGISharedHandle)) {
|
|
HANDLE handle = nullptr;
|
|
if (SUCCEEDED(m_device->CreateSharedHandle(m_fence.Get(), nullptr, GENERIC_ALL, nullptr,
|
|
&handle))) {
|
|
SharedFenceDxgiHandleWire wire{};
|
|
wire.chain.sType = wgpu::SType::SharedFenceDXGISharedHandleDescriptor;
|
|
wire.handle = handle;
|
|
const wgpu::SharedFenceDescriptor descriptor{
|
|
.nextInChain = &wire.chain,
|
|
.label = "Aurora D3D12 stereo interop fence",
|
|
};
|
|
m_webgpuFence = webgpu::g_device.ImportSharedFence(&descriptor);
|
|
CloseHandle(handle);
|
|
}
|
|
}
|
|
if (!m_webgpuFence) {
|
|
// This is still ordered correctly because both APIs submit to the exact
|
|
// same D3D12 queue. The explicit shared fence additionally describes the
|
|
// dependency to Dawn when that optional feature is available.
|
|
Log.warn("Dawn shared-fence import is unavailable; using same-queue ordering");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
~StereoBridge() {
|
|
if (!m_gpuIdle) {
|
|
(void)WaitForGpuLocked();
|
|
}
|
|
}
|
|
|
|
bool PrepareForDestruction() noexcept {
|
|
std::lock_guard lock(m_mutex);
|
|
return WaitForGpuLocked();
|
|
}
|
|
|
|
bool SetTargets(uint64_t token, const AuroraD3D12StereoTarget* targets, uint32_t targetCount,
|
|
const AuroraD3D12StereoTarget* panel) noexcept {
|
|
if (token == 0 || targets == nullptr || targetCount == 0 ||
|
|
targetCount > AURORA_D3D12_STEREO_MAX_TARGETS) {
|
|
return false;
|
|
}
|
|
const auto valid = [](const AuroraD3D12StereoTarget& target) {
|
|
if (target.resource == nullptr || target.width == 0 || target.height == 0 ||
|
|
target.dxgiFormat == DXGI_FORMAT_UNKNOWN) {
|
|
return false;
|
|
}
|
|
const D3D12_RESOURCE_DESC desc = static_cast<ID3D12Resource*>(target.resource)->GetDesc();
|
|
return desc.Dimension == D3D12_RESOURCE_DIMENSION_TEXTURE2D && desc.Width >= target.width &&
|
|
desc.Height >= target.height && desc.DepthOrArraySize == 1 && desc.MipLevels == 1 &&
|
|
desc.SampleDesc.Count == 1 &&
|
|
same_copy_family(desc.Format, static_cast<DXGI_FORMAT>(target.dxgiFormat));
|
|
};
|
|
std::lock_guard lock(m_mutex);
|
|
if (m_framePending || m_encoded) {
|
|
return false;
|
|
}
|
|
for (uint32_t eye = 0; eye < targetCount; ++eye) {
|
|
if (!valid(targets[eye])) {
|
|
return false;
|
|
}
|
|
}
|
|
if (panel != nullptr && !valid(*panel)) {
|
|
return false;
|
|
}
|
|
m_targets = {};
|
|
m_imageCount = 0;
|
|
const auto add = [&](uint32_t index, const AuroraD3D12StereoTarget& target) {
|
|
m_targets[index] = {
|
|
.resource = static_cast<ID3D12Resource*>(target.resource),
|
|
.width = target.width,
|
|
.height = target.height,
|
|
.format = static_cast<DXGI_FORMAT>(target.dxgiFormat),
|
|
};
|
|
m_images[m_imageCount++] = index;
|
|
};
|
|
for (uint32_t eye = 0; eye < targetCount; ++eye) {
|
|
add(eye, targets[eye]);
|
|
}
|
|
if (panel != nullptr) {
|
|
add(kPanelIndex, *panel);
|
|
}
|
|
m_frameToken = token;
|
|
m_targetCount = targetCount;
|
|
m_framePending = true;
|
|
return true;
|
|
}
|
|
|
|
bool Encode(wgpu::CommandEncoder& encoder, const stereo::SinkFrame& frame) noexcept {
|
|
std::lock_guard lock(m_mutex);
|
|
if (!m_framePending || m_encoded || frame.frameToken != m_frameToken) {
|
|
return false;
|
|
}
|
|
if (EncodeLocked(encoder, frame)) {
|
|
m_encoded = true;
|
|
return true;
|
|
}
|
|
PublishAndClearFrameLocked(frame.frameToken, false);
|
|
return false;
|
|
}
|
|
|
|
void Submitted(const stereo::SinkFrame& frame) noexcept {
|
|
std::lock_guard lock(m_mutex);
|
|
if (!m_framePending || !m_encoded || frame.frameToken != m_frameToken) {
|
|
return;
|
|
}
|
|
const bool success = EndAccessLocked() && EnqueueNativeCopyLocked();
|
|
PublishAndClearFrameLocked(frame.frameToken, success);
|
|
}
|
|
|
|
void CancelPending() noexcept {
|
|
uint64_t token = 0;
|
|
{
|
|
std::lock_guard lock(m_mutex);
|
|
if (!m_framePending) {
|
|
return;
|
|
}
|
|
token = m_frameToken;
|
|
if (m_encoded) {
|
|
EndAccessLocked();
|
|
}
|
|
PublishAndClearFrameLocked(token, false);
|
|
}
|
|
}
|
|
|
|
bool CancelBeforeEncode(uint64_t token) noexcept {
|
|
// Never make the XR pacing thread wait behind an in-progress Encode. A
|
|
// failed try-lock means Aurora may already own GPU-relevant work, so the
|
|
// submitted callback remains authoritative.
|
|
std::unique_lock lock(m_mutex, std::try_to_lock);
|
|
if (!lock.owns_lock()) {
|
|
return false;
|
|
}
|
|
if (token == 0 || !m_framePending || m_encoded || token != m_frameToken) {
|
|
return false;
|
|
}
|
|
ClearFrameLocked();
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
bool EnsureIntermediate(uint32_t eye, const stereo::EyeImage& source) noexcept {
|
|
auto& intermediate = m_intermediates[eye];
|
|
const DXGI_FORMAT sourceFormat = static_cast<DXGI_FORMAT>(to_dxgi_format(source.format));
|
|
if (source.texture == nullptr || sourceFormat == DXGI_FORMAT_UNKNOWN ||
|
|
source.size.width != m_targets[eye].width || source.size.height != m_targets[eye].height ||
|
|
!same_copy_family(sourceFormat, m_targets[eye].format)) {
|
|
Log.error("Stereo image {} does not match its OpenXR D3D12 target", eye);
|
|
return false;
|
|
}
|
|
if (intermediate.texture && intermediate.width == source.size.width &&
|
|
intermediate.height == source.size.height && intermediate.webgpuFormat == source.format) {
|
|
return true;
|
|
}
|
|
if (intermediate.accessBegun) {
|
|
return false;
|
|
}
|
|
|
|
intermediate = {};
|
|
const D3D12_HEAP_PROPERTIES heap{
|
|
.Type = D3D12_HEAP_TYPE_DEFAULT,
|
|
.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
|
|
.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN,
|
|
.CreationNodeMask = 1,
|
|
.VisibleNodeMask = 1,
|
|
};
|
|
const D3D12_RESOURCE_DESC resourceDescriptor{
|
|
.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D,
|
|
.Alignment = 0,
|
|
.Width = source.size.width,
|
|
.Height = source.size.height,
|
|
.DepthOrArraySize = 1,
|
|
.MipLevels = 1,
|
|
.Format = sourceFormat,
|
|
.SampleDesc = {1, 0},
|
|
.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN,
|
|
.Flags = D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS,
|
|
};
|
|
if (FAILED(m_device->CreateCommittedResource(
|
|
&heap, D3D12_HEAP_FLAG_NONE, &resourceDescriptor, D3D12_RESOURCE_STATE_COMMON,
|
|
nullptr, IID_PPV_ARGS(&intermediate.resource)))) {
|
|
Log.error("Could not create D3D12 stereo intermediate for eye {}", eye);
|
|
return false;
|
|
}
|
|
|
|
SharedTextureMemoryD3D12ResourceWire wire{};
|
|
wire.chain.sType = wgpu::SType::SharedTextureMemoryD3D12ResourceDescriptor;
|
|
wire.resource = intermediate.resource;
|
|
const wgpu::SharedTextureMemoryDescriptor memoryDescriptor{
|
|
.nextInChain = &wire.chain,
|
|
.label = eye == 0 ? "OpenXR left eye intermediate"
|
|
: eye == 1 ? "OpenXR right eye intermediate"
|
|
: "OpenXR panel intermediate",
|
|
};
|
|
intermediate.memory = webgpu::g_device.ImportSharedTextureMemory(&memoryDescriptor);
|
|
if (!intermediate.memory) {
|
|
Log.error("Dawn rejected D3D12 stereo intermediate for eye {}", eye);
|
|
intermediate = {};
|
|
return false;
|
|
}
|
|
wgpu::SharedTextureMemoryProperties properties{};
|
|
if (intermediate.memory.GetProperties(&properties) != wgpu::Status::Success ||
|
|
properties.size.width != source.size.width || properties.size.height != source.size.height ||
|
|
properties.format != source.format ||
|
|
(properties.usage & wgpu::TextureUsage::CopyDst) == wgpu::TextureUsage::None) {
|
|
Log.error("Dawn reported incompatible D3D12 shared-texture properties for eye {}", eye);
|
|
intermediate = {};
|
|
return false;
|
|
}
|
|
const wgpu::TextureDescriptor textureDescriptor{
|
|
.label = eye == 0 ? "OpenXR left eye shared texture"
|
|
: eye == 1 ? "OpenXR right eye shared texture"
|
|
: "OpenXR panel shared texture",
|
|
.usage = wgpu::TextureUsage::CopyDst,
|
|
.dimension = wgpu::TextureDimension::e2D,
|
|
.size = {source.size.width, source.size.height, 1},
|
|
.format = source.format,
|
|
.mipLevelCount = 1,
|
|
.sampleCount = 1,
|
|
};
|
|
intermediate.texture = intermediate.memory.CreateTexture(&textureDescriptor);
|
|
if (!intermediate.texture) {
|
|
Log.error("Dawn could not wrap D3D12 stereo intermediate for eye {}", eye);
|
|
intermediate = {};
|
|
return false;
|
|
}
|
|
intermediate.webgpuFormat = source.format;
|
|
intermediate.dxgiFormat = sourceFormat;
|
|
intermediate.width = source.size.width;
|
|
intermediate.height = source.size.height;
|
|
return true;
|
|
}
|
|
|
|
bool EncodeLocked(wgpu::CommandEncoder& encoder, const stereo::SinkFrame& frame) noexcept {
|
|
CollectCompletedCommandsLocked();
|
|
std::array<stereo::EyeImage, kMaxImages> sources{};
|
|
for (uint32_t n = 0; n < m_imageCount; ++n) {
|
|
const uint32_t eye = m_images[n];
|
|
if (eye == kPanelIndex) {
|
|
if (!stereo_overlay::layer_source(encoder, m_targets[eye].width, m_targets[eye].height, sources[eye])) {
|
|
return false;
|
|
}
|
|
} else {
|
|
sources[eye] = frame.eyes[eye];
|
|
}
|
|
if (!EnsureIntermediate(eye, sources[eye])) {
|
|
return false;
|
|
}
|
|
}
|
|
for (uint32_t n = 0; n < m_imageCount; ++n) {
|
|
const uint32_t eye = m_images[n];
|
|
auto& intermediate = m_intermediates[eye];
|
|
const std::array fences{m_webgpuFence};
|
|
const std::array values{m_lastExternalFenceValue};
|
|
wgpu::SharedTextureMemoryBeginAccessDescriptor begin{};
|
|
begin.initialized = intermediate.initialized;
|
|
if (m_webgpuFence && m_lastExternalFenceValue != 0) {
|
|
begin.fenceCount = 1;
|
|
begin.fences = fences.data();
|
|
begin.signaledValueCount = 1;
|
|
begin.signaledValues = values.data();
|
|
}
|
|
if (intermediate.memory.BeginAccess(intermediate.texture, &begin) != wgpu::Status::Success) {
|
|
Log.error("Dawn BeginAccess failed for stereo eye {}", eye);
|
|
for (uint32_t m = 0; m < n; ++m) {
|
|
const uint32_t begunEye = m_images[m];
|
|
wgpu::SharedTextureMemoryEndAccessState end{};
|
|
m_intermediates[begunEye].memory.EndAccess(m_intermediates[begunEye].texture, &end);
|
|
m_intermediates[begunEye].initialized = end.initialized;
|
|
m_intermediates[begunEye].accessBegun = false;
|
|
}
|
|
return false;
|
|
}
|
|
intermediate.accessBegun = true;
|
|
}
|
|
// Acquire every shared texture before recording any command that refers
|
|
// to one. If a later BeginAccess fails, the rollback above can therefore
|
|
// end the earlier accesses without leaving an unsubmitted copy that uses
|
|
// a texture after its access interval.
|
|
for (uint32_t n = 0; n < m_imageCount; ++n) {
|
|
const uint32_t eye = m_images[n];
|
|
const auto& intermediate = m_intermediates[eye];
|
|
const wgpu::TexelCopyTextureInfo source{
|
|
.texture = *sources[eye].texture,
|
|
.mipLevel = 0,
|
|
.origin = {},
|
|
.aspect = wgpu::TextureAspect::All,
|
|
};
|
|
const wgpu::TexelCopyTextureInfo destination{
|
|
.texture = intermediate.texture,
|
|
.mipLevel = 0,
|
|
.origin = {},
|
|
.aspect = wgpu::TextureAspect::All,
|
|
};
|
|
const wgpu::Extent3D extent{intermediate.width, intermediate.height, 1};
|
|
encoder.CopyTextureToTexture(&source, &destination, &extent);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool EndAccessLocked() noexcept {
|
|
bool success = true;
|
|
for (uint32_t n = 0; n < m_imageCount; ++n) {
|
|
const uint32_t eye = m_images[n];
|
|
auto& intermediate = m_intermediates[eye];
|
|
if (!intermediate.accessBegun) {
|
|
success = false;
|
|
continue;
|
|
}
|
|
wgpu::SharedTextureMemoryEndAccessState end{};
|
|
if (intermediate.memory.EndAccess(intermediate.texture, &end) != wgpu::Status::Success) {
|
|
Log.error("Dawn EndAccess failed for stereo eye {}", eye);
|
|
success = false;
|
|
} else {
|
|
intermediate.initialized = end.initialized;
|
|
}
|
|
intermediate.accessBegun = false;
|
|
}
|
|
return success;
|
|
}
|
|
|
|
bool EnqueueNativeCopyLocked() noexcept {
|
|
ComPtr<ID3D12CommandAllocator> allocator;
|
|
ComPtr<ID3D12GraphicsCommandList> list;
|
|
std::array<ComPtr<ID3D12Resource>, kMaxImages> sources;
|
|
std::array<ComPtr<ID3D12Resource>, kMaxImages> destinations;
|
|
if (FAILED(m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
|
|
IID_PPV_ARGS(&allocator))) ||
|
|
FAILED(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator.Get(),
|
|
nullptr, IID_PPV_ARGS(&list)))) {
|
|
Log.error("Could not create the D3D12 stereo copy command list");
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t n = 0; n < m_imageCount; ++n) {
|
|
const uint32_t eye = m_images[n];
|
|
const auto& source = m_intermediates[eye];
|
|
const auto& destination = m_targets[eye];
|
|
sources[eye] = source.resource;
|
|
destinations[eye] = destination.resource;
|
|
const std::array barriers{
|
|
D3D12_RESOURCE_BARRIER{
|
|
.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION,
|
|
.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE,
|
|
.Transition = {source.resource.Get(), D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
|
|
D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_SOURCE},
|
|
},
|
|
D3D12_RESOURCE_BARRIER{
|
|
.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION,
|
|
.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE,
|
|
.Transition = {destination.resource.Get(), D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
|
|
D3D12_RESOURCE_STATE_RENDER_TARGET,
|
|
D3D12_RESOURCE_STATE_COPY_DEST},
|
|
},
|
|
};
|
|
list->ResourceBarrier(static_cast<UINT>(barriers.size()), barriers.data());
|
|
const D3D12_TEXTURE_COPY_LOCATION sourceLocation{
|
|
.pResource = source.resource.Get(),
|
|
.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX,
|
|
.SubresourceIndex = 0,
|
|
};
|
|
const D3D12_TEXTURE_COPY_LOCATION destinationLocation{
|
|
.pResource = destination.resource.Get(),
|
|
.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX,
|
|
.SubresourceIndex = 0,
|
|
};
|
|
const D3D12_BOX sourceBox{0, 0, 0, source.width, source.height, 1};
|
|
list->CopyTextureRegion(&destinationLocation, 0, 0, 0, &sourceLocation, &sourceBox);
|
|
const std::array restore{
|
|
D3D12_RESOURCE_BARRIER{
|
|
.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION,
|
|
.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE,
|
|
.Transition = {source.resource.Get(), D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
|
|
D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_COMMON},
|
|
},
|
|
D3D12_RESOURCE_BARRIER{
|
|
.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION,
|
|
.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE,
|
|
.Transition = {destination.resource.Get(), D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
|
|
D3D12_RESOURCE_STATE_COPY_DEST,
|
|
D3D12_RESOURCE_STATE_RENDER_TARGET},
|
|
},
|
|
};
|
|
list->ResourceBarrier(static_cast<UINT>(restore.size()), restore.data());
|
|
}
|
|
if (FAILED(list->Close())) {
|
|
Log.error("Could not close the D3D12 stereo copy command list");
|
|
return false;
|
|
}
|
|
ID3D12CommandList* lists[]{list.Get()};
|
|
m_queue->ExecuteCommandLists(1, lists);
|
|
m_gpuIdle = false;
|
|
const uint64_t fenceValue = ++m_nextFenceValue;
|
|
const HRESULT signalResult = m_queue->Signal(m_fence.Get(), fenceValue);
|
|
m_commands.push_back({FAILED(signalResult) ? kUnfencedSubmission : fenceValue,
|
|
std::move(allocator), std::move(list),
|
|
std::move(sources), std::move(destinations)});
|
|
if (FAILED(signalResult)) {
|
|
// ExecuteCommandLists has already transferred work to the queue. Keep
|
|
// every command/resource reference alive even though there is no usable
|
|
// completion value; shutdown will retry with a queue-tail fence and leak
|
|
// this small bridge on an unrecoverable device/queue failure.
|
|
Log.error("Could not signal the D3D12 stereo copy fence");
|
|
return false;
|
|
}
|
|
m_lastExternalFenceValue = fenceValue;
|
|
return true;
|
|
}
|
|
|
|
void CollectCompletedCommandsLocked() noexcept {
|
|
const uint64_t completed = m_fence ? m_fence->GetCompletedValue() : 0;
|
|
std::erase_if(m_commands, [completed](const InFlightCommand& command) {
|
|
return command.fenceValue != kUnfencedSubmission && command.fenceValue <= completed;
|
|
});
|
|
}
|
|
|
|
void ClearFrameLocked() noexcept {
|
|
for (auto& target : m_targets) {
|
|
target = {};
|
|
}
|
|
m_frameToken = 0;
|
|
m_targetCount = 0;
|
|
m_imageCount = 0;
|
|
m_framePending = false;
|
|
m_encoded = false;
|
|
}
|
|
|
|
void PublishAndClearFrameLocked(uint64_t token, bool success) noexcept {
|
|
// Publication is part of the bridge state transition: once another thread
|
|
// can observe that this token is no longer cancellable, its submission
|
|
// result must already be visible. The OpenXR callback only takes the
|
|
// backend submission mutex; no backend path holds that mutex while entering
|
|
// this bridge, so keeping m_mutex here preserves the lock order.
|
|
Notify(token, success);
|
|
ClearFrameLocked();
|
|
}
|
|
|
|
void Notify(uint64_t token, bool success) noexcept {
|
|
if (m_callback != nullptr) {
|
|
m_callback(token, success, m_userdata);
|
|
}
|
|
}
|
|
|
|
bool WaitForGpuLocked() noexcept {
|
|
if (m_gpuIdle) {
|
|
return true;
|
|
}
|
|
if (!m_queue || !m_fence) {
|
|
return m_commands.empty();
|
|
}
|
|
const uint64_t value = ++m_nextFenceValue;
|
|
if (FAILED(m_queue->Signal(m_fence.Get(), value))) {
|
|
Log.error("Could not signal a D3D12 queue-tail fence during stereo bridge shutdown");
|
|
return false;
|
|
}
|
|
if (m_fence->GetCompletedValue() >= value) {
|
|
m_commands.clear();
|
|
m_gpuIdle = true;
|
|
return true;
|
|
}
|
|
HANDLE event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
|
if (event == nullptr) {
|
|
Log.error("Could not create the D3D12 stereo shutdown fence event");
|
|
return false;
|
|
}
|
|
bool complete = false;
|
|
if (SUCCEEDED(m_fence->SetEventOnCompletion(value, event))) {
|
|
complete = WaitForSingleObject(event, 5000) == WAIT_OBJECT_0;
|
|
}
|
|
CloseHandle(event);
|
|
if (!complete) {
|
|
Log.error("Timed out waiting for the D3D12 stereo queue to become idle");
|
|
return false;
|
|
}
|
|
m_commands.clear();
|
|
m_gpuIdle = true;
|
|
return true;
|
|
}
|
|
|
|
std::mutex m_mutex;
|
|
ComPtr<ID3D12Device> m_device;
|
|
ComPtr<ID3D12CommandQueue> m_queue;
|
|
ComPtr<ID3D12Fence> m_fence;
|
|
wgpu::SharedFence m_webgpuFence;
|
|
std::array<IntermediateEye, kMaxImages> m_intermediates{};
|
|
std::array<PendingTarget, kMaxImages> m_targets{};
|
|
// The slots of m_targets this frame copies into, eyes first.
|
|
std::array<uint32_t, kMaxImages> m_images{};
|
|
uint32_t m_imageCount = 0;
|
|
std::vector<InFlightCommand> m_commands;
|
|
AuroraD3D12StereoSubmittedCallback m_callback = nullptr;
|
|
void* m_userdata = nullptr;
|
|
uint64_t m_frameToken = 0;
|
|
uint64_t m_nextFenceValue = 0;
|
|
uint64_t m_lastExternalFenceValue = 0;
|
|
uint32_t m_targetCount = 0;
|
|
bool m_framePending = false;
|
|
bool m_encoded = false;
|
|
bool m_gpuIdle = true;
|
|
};
|
|
|
|
std::unique_ptr<StereoBridge> g_bridge;
|
|
|
|
bool sink_encode(wgpu::CommandEncoder& encoder, const stereo::SinkFrame& frame,
|
|
void* userdata) noexcept {
|
|
return static_cast<StereoBridge*>(userdata)->Encode(encoder, frame);
|
|
}
|
|
|
|
void sink_submitted(const stereo::SinkFrame& frame, void* userdata) noexcept {
|
|
static_cast<StereoBridge*>(userdata)->Submitted(frame);
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace aurora::d3d12_interop
|
|
|
|
bool aurora_d3d12_get_native_handles(AuroraD3D12NativeHandles* handles) {
|
|
if (handles == nullptr) {
|
|
return false;
|
|
}
|
|
*handles = {};
|
|
aurora::d3d12_interop::NativeObjects objects;
|
|
if (!aurora::d3d12_interop::get_native_objects(objects)) {
|
|
return false;
|
|
}
|
|
const int64_t colorFormat =
|
|
aurora::d3d12_interop::to_dxgi_format(aurora::webgpu::g_graphicsConfig.surfaceConfiguration.format);
|
|
if (colorFormat == DXGI_FORMAT_UNKNOWN) {
|
|
return false;
|
|
}
|
|
const LUID luid = objects.device->GetAdapterLuid();
|
|
*handles = {
|
|
.device = objects.device.Get(),
|
|
.queue = objects.queue.Get(),
|
|
.colorDxgiFormat = colorFormat,
|
|
.adapterLuidLow = luid.LowPart,
|
|
.adapterLuidHigh = luid.HighPart,
|
|
};
|
|
return true;
|
|
}
|
|
|
|
bool aurora_d3d12_enable_stereo_bridge(AuroraD3D12StereoSubmittedCallback submitted,
|
|
void* userdata) {
|
|
using namespace aurora::d3d12_interop;
|
|
if (g_bridge || submitted == nullptr) {
|
|
return false;
|
|
}
|
|
NativeObjects objects;
|
|
if (!get_native_objects(objects)) {
|
|
return false;
|
|
}
|
|
auto bridge = std::make_unique<StereoBridge>(std::move(objects), submitted, userdata);
|
|
if (!bridge->Initialize()) {
|
|
return false;
|
|
}
|
|
aurora::stereo::set_sink(sink_encode, sink_submitted, bridge.get());
|
|
g_bridge = std::move(bridge);
|
|
return true;
|
|
}
|
|
|
|
bool aurora_d3d12_set_stereo_targets(uint64_t frameToken,
|
|
const AuroraD3D12StereoTarget* targets,
|
|
uint32_t targetCount) {
|
|
using namespace aurora::d3d12_interop;
|
|
return g_bridge && g_bridge->SetTargets(frameToken, targets, targetCount, nullptr);
|
|
}
|
|
|
|
bool aurora_d3d12_set_stereo_targets_with_panel(uint64_t frameToken, const AuroraD3D12StereoTarget* targets,
|
|
uint32_t targetCount, const AuroraD3D12StereoTarget* panel) {
|
|
using namespace aurora::d3d12_interop;
|
|
return g_bridge && g_bridge->SetTargets(frameToken, targets, targetCount, panel);
|
|
}
|
|
|
|
bool aurora_d3d12_cancel_stereo_targets(uint64_t frameToken) {
|
|
using namespace aurora::d3d12_interop;
|
|
return g_bridge && g_bridge->CancelBeforeEncode(frameToken);
|
|
}
|
|
|
|
bool aurora_d3d12_disable_stereo_bridge() {
|
|
using namespace aurora::d3d12_interop;
|
|
if (!g_bridge) {
|
|
return true;
|
|
}
|
|
aurora::stereo::set_sink(nullptr, nullptr, nullptr);
|
|
g_bridge->CancelPending();
|
|
if (!g_bridge->PrepareForDestruction()) {
|
|
// An already-enqueued command has no trustworthy completion marker. Keep
|
|
// the bridge, queue, command lists and resource references alive for the
|
|
// rest of the process rather than freeing memory the GPU may still touch.
|
|
(void)g_bridge.release();
|
|
return false;
|
|
}
|
|
g_bridge.reset();
|
|
return true;
|
|
}
|
|
|
|
#else
|
|
|
|
bool aurora_d3d12_get_native_handles(AuroraD3D12NativeHandles* handles) {
|
|
if (handles != nullptr) {
|
|
*handles = {};
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool aurora_d3d12_enable_stereo_bridge(AuroraD3D12StereoSubmittedCallback, void*) {
|
|
return false;
|
|
}
|
|
|
|
bool aurora_d3d12_set_stereo_targets(uint64_t, const AuroraD3D12StereoTarget*, uint32_t) {
|
|
return false;
|
|
}
|
|
|
|
bool aurora_d3d12_set_stereo_targets_with_panel(uint64_t, const AuroraD3D12StereoTarget*, uint32_t,
|
|
const AuroraD3D12StereoTarget*) {
|
|
return false;
|
|
}
|
|
|
|
bool aurora_d3d12_cancel_stereo_targets(uint64_t) { return false; }
|
|
|
|
bool aurora_d3d12_disable_stereo_bridge() { return true; }
|
|
|
|
#endif
|