mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 08:00:25 +02:00
752 lines
27 KiB
C++
752 lines
27 KiB
C++
#include <aurora/d3d12_interop.h>
|
|
|
|
#include "../internal.hpp"
|
|
#include "../stereo.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;
|
|
};
|
|
|
|
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>, AURORA_D3D12_STEREO_MAX_TARGETS> sources;
|
|
std::array<ComPtr<ID3D12Resource>, AURORA_D3D12_STEREO_MAX_TARGETS> 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) noexcept {
|
|
if (token == 0 || targets == nullptr || targetCount == 0 ||
|
|
targetCount > AURORA_D3D12_STEREO_MAX_TARGETS) {
|
|
return false;
|
|
}
|
|
std::lock_guard lock(m_mutex);
|
|
if (m_framePending || m_encoded) {
|
|
return false;
|
|
}
|
|
for (uint32_t eye = 0; eye < targetCount; ++eye) {
|
|
if (targets[eye].resource == nullptr || targets[eye].width == 0 ||
|
|
targets[eye].height == 0 || targets[eye].dxgiFormat == DXGI_FORMAT_UNKNOWN) {
|
|
return false;
|
|
}
|
|
auto* resource = static_cast<ID3D12Resource*>(targets[eye].resource);
|
|
const D3D12_RESOURCE_DESC desc = resource->GetDesc();
|
|
if (desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D ||
|
|
desc.Width < targets[eye].width || desc.Height < targets[eye].height ||
|
|
desc.DepthOrArraySize != 1 || desc.MipLevels != 1 || desc.SampleDesc.Count != 1 ||
|
|
!same_copy_family(desc.Format, static_cast<DXGI_FORMAT>(targets[eye].dxgiFormat))) {
|
|
return false;
|
|
}
|
|
m_targets[eye] = {
|
|
.resource = resource,
|
|
.width = targets[eye].width,
|
|
.height = targets[eye].height,
|
|
.format = static_cast<DXGI_FORMAT>(targets[eye].dxgiFormat),
|
|
};
|
|
}
|
|
for (uint32_t eye = targetCount; eye < m_targets.size(); ++eye) {
|
|
m_targets[eye] = {};
|
|
}
|
|
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 eye {} 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" : "OpenXR right eye 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" : "OpenXR right eye 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();
|
|
for (uint32_t eye = 0; eye < m_targetCount; ++eye) {
|
|
if (!EnsureIntermediate(eye, frame.eyes[eye])) {
|
|
return false;
|
|
}
|
|
}
|
|
for (uint32_t eye = 0; eye < m_targetCount; ++eye) {
|
|
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 begunEye = 0; begunEye < eye; ++begunEye) {
|
|
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 eye = 0; eye < m_targetCount; ++eye) {
|
|
const auto& intermediate = m_intermediates[eye];
|
|
const wgpu::TexelCopyTextureInfo source{
|
|
.texture = *frame.eyes[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 eye = 0; eye < m_targetCount; ++eye) {
|
|
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>, AURORA_D3D12_STEREO_MAX_TARGETS> sources;
|
|
std::array<ComPtr<ID3D12Resource>, AURORA_D3D12_STEREO_MAX_TARGETS> 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 eye = 0; eye < m_targetCount; ++eye) {
|
|
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_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, AURORA_D3D12_STEREO_MAX_TARGETS> m_intermediates{};
|
|
std::array<PendingTarget, AURORA_D3D12_STEREO_MAX_TARGETS> m_targets{};
|
|
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);
|
|
}
|
|
|
|
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_cancel_stereo_targets(uint64_t) { return false; }
|
|
|
|
bool aurora_d3d12_disable_stereo_bridge() { return true; }
|
|
|
|
#endif
|