Initial OpenXR configuration

This commit is contained in:
iChris4 committed 2026-08-30 21:47:12 +02:00
1 parent 9a22de1079
commit 0beb7d880c
18 files changed
+1904 -3

No files matched your search

+49
View File
@@ -77,6 +77,43 @@ typedef struct AuroraEvent AuroraEvent;
typedef void (*AuroraLogCallback)(AuroraLogLevel level, const char* module, const char* message, unsigned int len);
typedef void (*AuroraImGuiInitCallback)(const AuroraWindowSize* size);
enum { AURORA_STEREO_EYE_COUNT = 2 };
/**
* One eye of a stereo frame supplied by the host application.
*
* projection is a row-major, renderer-ready 4x4 projection matrix. It uses
* the same convention as the matrix uploaded by GXSetProjection after
* Aurora's depth-range adjustment.
*
* viewFromCenter is a row-major affine 3x4 transform from the game's
* recorded center-eye view space into this eye's view space. Identity keeps
* the recorded view and is useful when the game has already applied the eye
* transform before issuing GX commands.
*/
typedef struct {
uint32_t width;
uint32_t height;
float projection[16];
float viewFromCenter[12];
} AuroraStereoEye;
/**
* Stereo data for one sealed GX frame. frameToken is opaque to Aurora and is
* forwarded unchanged to the internal stereo output sink.
*/
typedef struct {
uint64_t frameToken;
AuroraStereoEye eyes[AURORA_STEREO_EYE_COUNT];
} AuroraStereoFrame;
/**
* Called on Aurora's frame worker immediately before a GX frame is sealed.
* Return false to render that logical frame in mono only. The callback must
* be non-blocking and must not call back into Aurora.
*/
typedef bool (*AuroraStereoFrameProvider)(uint32_t logicalFrame, AuroraStereoFrame* frame, void* userdata);
typedef struct {
const char* appName;
const char* userPath;
@@ -125,6 +162,15 @@ typedef struct {
// Optional directory for the portable GX pipeline database. When null, the
// database is stored in cachePath with Dawn's machine-specific cache.
const char* pipelineCachePath;
// Enables renderer features needed by an external XR compositor. The normal
// desktop path is unchanged when false.
bool xrInterop;
// Optional OpenXR-selected D3D adapter. Supplying the runtime's LUID before
// device creation keeps Dawn and the compositor on the same physical GPU.
bool hasD3D12AdapterLuid;
uint32_t d3d12AdapterLuidLow;
int32_t d3d12AdapterLuidHigh;
} AuroraConfig;
typedef struct {
@@ -144,6 +190,9 @@ typedef void (*AuroraFrameWorkerWaitCallback)();
// Called from the producer thread at bounded intervals while Aurora waits for
// the asynchronous frame worker. The callback must not enter Aurora.
void aurora_set_frame_worker_wait_callback(AuroraFrameWorkerWaitCallback callback);
// Registering nullptr restores the ordinary mono-only render path. Replace or
// unregister a provider only while Aurora's frame worker is idle.
void aurora_set_stereo_frame_provider(AuroraStereoFrameProvider provider, void* userdata);
void aurora_wait_for_frame_worker();
bool aurora_wait_for_frame_worker_for(uint32_t timeoutMicros);
// Absolute schedule for the next sealed frame, on steady_clock: baseNanos anchors the group and
+31
View File
@@ -281,7 +281,32 @@ void shutdown();
bool begin_frame();
bool resume_frame();
void abort_frame() noexcept;
struct ReplayTarget {
wgpu::TextureView colorView;
wgpu::TextureView resolveView;
wgpu::TextureView depthView;
wgpu::Texture copySourceTexture;
wgpu::TextureView copySourceView;
wgpu::TextureView copySourceDepthView;
wgpu::Extent3D size{};
uint32_t msaaSamples = 1;
};
struct StereoReplayEye {
ReplayTarget target;
Mat4x4<float> projection;
Mat3x4<float> viewFromCenter;
};
struct StereoReplayFrame {
std::array<StereoReplayEye, AURORA_STEREO_EYE_COUNT> eyes;
};
void end_frame(const wgpu::CommandEncoder& cmd);
// Prepares eye-specific uniform copies before unmapping the staging buffer.
// Returns false without modifying the mono path when the uniform buffer has
// insufficient room for the additional copies.
bool end_frame(const wgpu::CommandEncoder& cmd, const StereoReplayFrame& stereoFrame);
void end_batch(const wgpu::CommandEncoder& cmd);
uint32_t current_frame() noexcept;
@@ -314,6 +339,12 @@ void seal_frame(SealedFrame& out) noexcept;
// so this may run concurrently with the producer's FIFO drains.
void render(SealedFrame& frame, wgpu::CommandEncoder& cmd, int32_t interpolatedFrame = -1, bool finalize = true);
// Replays only main-EFB passes into one Aurora-owned eye target. Native
// offscreen/EFB-copy passes are consumed from the mono render and are not
// mutated by stereo replay.
void render_stereo_eye(SealedFrame& frame, wgpu::CommandEncoder& cmd,
const StereoReplayFrame& stereoFrame, uint32_t eye, bool finalize = false);
// Encode the frame that is still being recorded. Only for the synchronous
// EFB-readback split path, which runs on the producer thread.
void render(wgpu::CommandEncoder& cmd, int32_t interpolatedFrame = -1, bool finalize = true);
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include <aurora/math.hpp>
namespace aurora::gfx::stereo_replay {
// Aurora stores the GX 3x4 matrices row-major. The vertex shader consumes
// them as vec4 * mat3x4, which is equivalent to the original column-vector
// affine transform. Applying an eye-space delta therefore composes delta *
// objectToCenter in the ordinary row-major notation used below.
inline Mat3x4<float> compose_affine(const Mat3x4<float>& viewFromCenter,
const Mat3x4<float>& objectToCenter) noexcept {
Mat3x4<float> out{};
for (size_t row = 0; row < 3; ++row) {
auto& dst = *(&out.m0 + row);
const auto& view = *(&viewFromCenter.m0 + row);
for (size_t column = 0; column < 3; ++column) {
dst[column] = view[0] * objectToCenter.m0[column] + view[1] * objectToCenter.m1[column] +
view[2] * objectToCenter.m2[column];
}
dst[3] = view[3] + view[0] * objectToCenter.m0[3] + view[1] * objectToCenter.m1[3] +
view[2] * objectToCenter.m2[3];
}
return out;
}
// Normals receive only the eye transform's linear part. OpenXR view deltas
// are rigid transforms, so no inverse-transpose correction is needed here.
inline Mat3x4<float> compose_normal(const Mat3x4<float>& viewFromCenter,
const Mat3x4<float>& objectToCenter) noexcept {
Mat3x4<float> out{};
for (size_t row = 0; row < 3; ++row) {
auto& dst = *(&out.m0 + row);
const auto& view = *(&viewFromCenter.m0 + row);
for (size_t column = 0; column < 3; ++column) {
dst[column] = view[0] * objectToCenter.m0[column] + view[1] * objectToCenter.m1[column] +
view[2] * objectToCenter.m2[column];
}
dst[3] = 0.0f;
}
return out;
}
} // namespace aurora::gfx::stereo_replay
+4 -2
View File
@@ -2256,9 +2256,9 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount,
.matrixTopology = matrixTopologySignature,
};
}
const bool perspective = g_gxState.projType == GX_PERSPECTIVE;
const auto uniformRanges =
build_uniform(info, vertRange.offset, ranges, drawIdentity, interpolationIdentityActive,
usedPnMtxMask);
build_uniform(info, vertRange.offset, ranges, drawIdentity, perspective, usedPnMtxMask);
s_lastDrawRecordedInterpolation = interpolationIdentityActive;
uint32_t instanceCount = 1;
@@ -2275,6 +2275,8 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount,
.idxRange = idxRange,
.uniformRange = uniformRanges.current,
.interpolatedUniformRanges = uniformRanges.interpolated,
.stereoUniformRanges = {},
.uniformReplayLayout = uniformRanges.replayLayout,
.vtxCount = vtxCount,
.indexCount = numIndices,
.instanceCount = instanceCount,
+2
View File
@@ -10,6 +10,8 @@ struct DrawData {
gfx::Range idxRange;
gfx::Range uniformRange;
std::array<gfx::Range, MaxInterpolatedFrames> interpolatedUniformRanges;
std::array<gfx::Range, AURORA_STEREO_EYE_COUNT> stereoUniformRanges;
UniformReplayLayout uniformReplayLayout;
uint32_t vtxCount;
uint32_t indexCount;
uint32_t instanceCount;
+16
View File
@@ -613,10 +613,14 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou
stage(&effectiveProj, sizeof(effectiveProj));
const size_t positionOffset = stagedSize;
uint32_t positionMatrixMask = 0;
for (u32 i = 0; i < layout.postexCount; ++i) {
const u32 slot =
layout.postexSlots[i] == UniformMatrixLayout::kCurrentPnMtx ? currentPostexSlot
: layout.postexSlots[i];
if (slot < MaxPnMtx) {
positionMatrixMask |= 1u << i;
}
stage(slot < MaxPnMtx ? &g_gxState.pnMtx[slot].pos : &g_gxState.texMtxs[slot - MaxPnMtx],
sizeof(Mat3x4<float>));
}
@@ -707,11 +711,22 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou
buf.append(texture_size_bias(tex));
}
const UniformReplayLayout replayLayout{
.projectionOffset = static_cast<uint32_t>(projectionOffset),
.positionOffset = static_cast<uint32_t>(positionOffset),
.normalOffset = static_cast<uint32_t>(normalOffset),
.positionMatrixMask = positionMatrixMask,
.positionMatrixCount = layout.postexCount,
.normalMatrixCount = layout.nrmCount,
.perspective = perspective,
};
if (!perspective || frame_interpolation_fps() == 0) {
g_gxState.stateDirty = false;
return {
.current = range,
.interpolated = {},
.replayLayout = replayLayout,
};
}
@@ -733,6 +748,7 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou
return {
.current = range,
.interpolated = interpolatedRanges,
.replayLayout = replayLayout,
};
}
} // namespace aurora::gx
+11
View File
@@ -5,9 +5,20 @@
#include "frame_interpolation.hpp"
namespace aurora::gx {
struct UniformReplayLayout {
uint32_t projectionOffset = 0;
uint32_t positionOffset = 0;
uint32_t normalOffset = 0;
uint32_t positionMatrixMask = 0;
uint8_t positionMatrixCount = 0;
uint8_t normalMatrixCount = 0;
bool perspective = false;
};
struct UniformRanges {
gfx::Range current;
std::array<gfx::Range, MaxInterpolatedFrames> interpolated;
UniformReplayLayout replayLayout;
};
ShaderInfo build_shader_info(const ShaderConfig& config) noexcept;
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "webgpu/gpu.hpp"
#include <array>
#include <cstdint>
namespace aurora::stereo {
struct EyeImage {
// Borrowed for the duration of the sink callback. A sink may encode work
// that reads the texture, but must not retain these pointers.
const wgpu::Texture* texture = nullptr;
const wgpu::TextureView* view = nullptr;
wgpu::Extent3D size{};
wgpu::TextureFormat format = wgpu::TextureFormat::Undefined;
};
struct SinkFrame {
uint64_t frameToken = 0;
uint32_t logicalFrame = 0;
std::array<EyeImage, AURORA_STEREO_EYE_COUNT> eyes{};
};
// Runs synchronously on the frame worker after both eye replays have been
// encoded and before its command buffer is submitted. Backend interop code
// can append copies/import transitions to the same encoder here.
using SinkCallback = void (*)(wgpu::CommandEncoder& encoder, const SinkFrame& frame, void* userdata) noexcept;
// Internal Aurora hook: D3D12/Vulkan OpenXR interop owns this registration.
// Registration changes must happen while the frame worker is idle.
void set_sink(SinkCallback callback, void* userdata) noexcept;
} // namespace aurora::stereo
+32 -1
View File
@@ -43,6 +43,18 @@ void clear_offscreen_cache();
namespace aurora::webgpu {
static Module Log("aurora::gpu");
#if defined(WEBGPU_DAWN) && defined(_WIN32)
// RequestAdapterOptionsLUID is a Dawn-native extension whose exported C++
// constructor cannot be called safely by this project's LLVM-MinGW consumer.
// Its wire representation is deliberately just a WebGPU chained header plus
// the Win32 LUID, so describe that C ABI directly and let RequestAdapter
// consume it through the ordinary WebGPU entry point.
struct RequestAdapterOptionsLuidWire {
wgpu::ChainedStruct chain{};
LUID adapterLuid{};
};
#endif
wgpu::Device g_device;
wgpu::Queue g_queue;
wgpu::Surface g_surface;
@@ -558,11 +570,24 @@ bool initialize(AuroraBackend auroraBackend) {
}
}
{
const wgpu::RequestAdapterOptions options{
wgpu::RequestAdapterOptions options{
.powerPreference = wgpu::PowerPreference::HighPerformance,
.backendType = backend,
.compatibleSurface = g_surface,
};
#if defined(WEBGPU_DAWN) && defined(_WIN32)
RequestAdapterOptionsLuidWire luidOptions{};
if (backend == wgpu::BackendType::D3D12 && g_config.xrInterop &&
g_config.hasD3D12AdapterLuid) {
luidOptions.chain.sType = wgpu::SType::RequestAdapterOptionsLUID;
luidOptions.adapterLuid.LowPart = g_config.d3d12AdapterLuidLow;
luidOptions.adapterLuid.HighPart = g_config.d3d12AdapterLuidHigh;
options.nextInChain = &luidOptions.chain;
Log.info("Pinning D3D12 adapter to OpenXR LUID {:08x}:{:08x}",
static_cast<uint32_t>(luidOptions.adapterLuid.HighPart),
luidOptions.adapterLuid.LowPart);
}
#endif
const auto future = g_instance.RequestAdapter(
&options, wgpu::CallbackMode::WaitAnyOnly,
[](wgpu::RequestAdapterStatus status, wgpu::Adapter adapter, wgpu::StringView message) {
@@ -653,6 +678,12 @@ bool initialize(AuroraBackend auroraBackend) {
implicitDeviceSynchronizationSupported = true;
requiredFeatures.push_back(feature);
}
#if defined(WEBGPU_DAWN) && defined(_WIN32)
if (g_config.xrInterop && g_backendType == wgpu::BackendType::D3D12 &&
feature == wgpu::FeatureName::SharedTextureMemoryD3D12Resource) {
requiredFeatures.push_back(feature);
}
#endif
}
if (!implicitDeviceSynchronizationSupported) {
Log.warn(