Added Vulkan PC OpenXR Support

This commit is contained in:
iChris4 committed 2026-09-21 00:31:46 +02:00
1 parent fbc9ce7217
commit e7b670b2cc
23 files changed
+2174 -22

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
# Builds the pinned Dawn DLL with Aurora's native Windows Vulkan/OpenXR bridge.
# Run on a maintainer machine with VS 2022 C++ tools, CMake, Git and Python 3.
[CmdletBinding()]
param(
[string]$WorkDirectory = (Join-Path $PSScriptRoot 'artifacts\dawn-vulkan-build'),
[string]$Destination = (Join-Path $PSScriptRoot 'artifacts\dawn-vulkan'),
[string]$Python = 'python',
[int]$Jobs = 8
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3.0
$revision = '13abc3bc8ea2d3c2050f9e77a12d012108ceee24'
$archiveHash = '713bea5b92d4f6c5175752fd7cbf1c3c5ce36598ff5dd98685d8a1216614ebba'
$WorkDirectory = [IO.Path]::GetFullPath($WorkDirectory)
$Destination = [IO.Path]::GetFullPath($Destination)
[IO.Directory]::CreateDirectory($WorkDirectory) | Out-Null
$archive = Join-Path $WorkDirectory 'dawn-source.tar.gz'
if (-not (Test-Path -LiteralPath $archive)) {
Invoke-WebRequest "https://github.com/google/dawn/archive/$revision.tar.gz" -OutFile $archive -UseBasicParsing
}
if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $archiveHash) {
throw 'Dawn source archive does not match the pinned SHA-256.'
}
$source = Join-Path $WorkDirectory "dawn-$revision"
if (-not (Test-Path -LiteralPath $source)) {
& $Python -c 'import sys,tarfile; tarfile.open(sys.argv[1]).extractall(sys.argv[2], filter="data")' $archive $WorkDirectory
if ($LASTEXITCODE -ne 0) { throw 'Dawn source extraction failed (Python 3.12+ required).' }
}
$patch = Join-Path $PSScriptRoot '..\aurora-main\patches\dawn\apply.py'
& $Python $patch $source
if ($LASTEXITCODE -ne 0) { throw 'Applying the Aurora Dawn bridge failed.' }
$build = Join-Path $WorkDirectory 'build'
& cmake -S $source -B $build -G 'Visual Studio 17 2022' -A x64 `
"-DPython3_EXECUTABLE=$Python" -DDAWN_FETCH_DEPENDENCIES=ON `
-DDAWN_BUILD_MONOLITHIC_LIBRARY=SHARED -DDAWN_ENABLE_INSTALL=ON `
-DDAWN_BUILD_SAMPLES=OFF -DDAWN_BUILD_TESTS=OFF -DDAWN_BUILD_BENCHMARKS=OFF `
-DTINT_BUILD_TESTS=OFF -DTINT_BUILD_CMD_TOOLS=OFF -DDAWN_USE_GLFW=OFF `
-DDAWN_ENABLE_D3D11=OFF -DDAWN_ENABLE_D3D12=ON -DDAWN_ENABLE_VULKAN=ON `
-DDAWN_ENABLE_DESKTOP_GL=OFF -DDAWN_ENABLE_OPENGLES=OFF "-DCMAKE_INSTALL_PREFIX=$Destination"
if ($LASTEXITCODE -ne 0) { throw 'Dawn configuration failed.' }
& cmake --build $build --config Release --parallel $Jobs
if ($LASTEXITCODE -ne 0) { throw 'Dawn build failed.' }
& cmake --install $build --config Release
if ($LASTEXITCODE -ne 0) { throw 'Dawn installation failed.' }
$dll = Join-Path $Destination 'bin\webgpu_dawn.dll'
if (-not (Test-Path -LiteralPath $dll)) { throw "Dawn DLL missing: $dll" }
[ordered]@{
SourceRevision = $revision
AuroraVulkanAbi = 1
DllSha256 = (Get-FileHash -LiteralPath $dll -Algorithm SHA256).Hash.ToLowerInvariant()
} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Destination 'aurora-vulkan.json') -Encoding UTF8
Write-Host "Custom Dawn package ready: $Destination"
+1 -1
View File
@@ -99,7 +99,7 @@ Assert-File (Join-Path $portableTools 'Ninja\ninja.exe') 'Portable Ninja'
# to compile (launcher/Prepare-NativePrebuilt.ps1).
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host
# refuses to call a toolkit complete unless every one of these directories is present.
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','openxr','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','openxr','png','SDL','sqlite3','tracy','vulkan_headers','xxhash','zlib','zstd')
$missingSources = @($requiredDependencies | Where-Object { $_ -ne 'native_prebuilt' } |
Where-Object { -not (Test-Path -LiteralPath (Join-Path $dependencySources $_) -PathType Container) })
if ($missingSources.Count -gt 0) {
+24 -1
View File
@@ -9,7 +9,9 @@ param(
[string]$Destination,
# Windows metadata source for cppwinrt.exe: 'local' (this machine's WinMetadata), 'sdk', or an
# installed SDK version such as 10.0.26100.0.
[string]$CppWinRtInput = 'local'
[string]$CppWinRtInput = 'local',
# Output of Build-DawnVulkan.ps1. Rebuild native_prebuilt after changing Dawn.
[string]$DawnVulkanPackage
)
$ErrorActionPreference = 'Stop'
@@ -27,6 +29,11 @@ $runtimeCMake = Join-Path $repoRoot 'runtime\CMakeLists.txt'
# FETCHCONTENT_SOURCE_DIR_<UPPERCASE NAME> (NativeBuildFlags.ps1), so the names are the declared
# FetchContent names, not the upstream project names.
$packages = @(
[pscustomobject]@{
Name = 'vulkan_headers'; File = 'vulkan-headers-015e25c3c91b70eb1a754d36fb14c4ba6ad9b0b9.tar.gz'
Uris = @('https://github.com/KhronosGroup/Vulkan-Headers/archive/015e25c3c91b70eb1a754d36fb14c4ba6ad9b0b9.tar.gz')
Pins = @(@{ File = $runtimeCMake; Text = 'Vulkan-Headers/archive/015e25c3c91b70eb1a754d36fb14c4ba6ad9b0b9.tar.gz' })
},
[pscustomobject]@{
Name = 'SDL'; File = 'SDL3-3.4.4.tar.gz'
Uris = @('https://github.com/libsdl-org/SDL/releases/download/release-3.4.4/SDL3-3.4.4.tar.gz')
@@ -175,6 +182,22 @@ function Expand-Package([string]$Archive, [string]$Target) {
foreach ($package in $packages) {
Assert-Pinned $package
$target = Join-Path $Destination $package.Name
if ($package.Name -eq 'dawn_prebuilt' -and $DawnVulkanPackage) {
$custom = [IO.Path]::GetFullPath($DawnVulkanPackage)
$manifest = Get-Content -LiteralPath (Join-Path $custom 'aurora-vulkan.json') -Raw | ConvertFrom-Json
$dll = Join-Path $custom 'bin\webgpu_dawn.dll'
if ($manifest.SourceRevision -ne '13abc3bc8ea2d3c2050f9e77a12d012108ceee24' -or
$manifest.AuroraVulkanAbi -ne 1 -or
(Get-FileHash -LiteralPath $dll -Algorithm SHA256).Hash.ToLowerInvariant() -ne $manifest.DllSha256) {
throw 'Custom Dawn package provenance or DLL hash does not match.'
}
if (Test-Path -LiteralPath $target) {
throw "Use a fresh dependency destination for custom Dawn: $target already exists."
}
Copy-Item -LiteralPath $custom -Destination $target -Recurse
Write-Host 'Prepared custom Dawn with Windows Vulkan OpenXR support'
continue
}
if (Test-Path -LiteralPath $target -PathType Container) { continue }
Expand-Package (Get-Archive $package) $target
Write-Host "Prepared $($package.Name)"
@@ -27,7 +27,7 @@ internal static class InstalledLayout
public static readonly string[] DependencyNames =
[
"abseil-cpp", "cppwinrt", "dawn_prebuilt", "fmt", "freetype", "imgui", "libusb", "native_prebuilt",
"openxr", "png", "SDL", "sqlite3", "tracy", "xxhash", "zlib", "zstd"
"openxr", "png", "SDL", "sqlite3", "tracy", "vulkan_headers", "xxhash", "zlib", "zstd"
];
}
+62 -9
View File
@@ -3,7 +3,10 @@
WiiCompiled has an opt-in OpenXR rendering path. The first functional backend is Windows D3D12.
It asks the OpenXR runtime for the required GPU before Aurora creates Dawn, then copies each eye
on that same D3D12 device and queue into the acquired OpenXR swapchain images. Eye submission
stays on the GPU; there is no CPU texture readback and no second graphics device.
stays on the GPU; there is no CPU texture readback and no second graphics device. Windows Vulkan is
an opt-in second binding built on the same design: the OpenXR runtime creates Dawn's Vulkan instance
and device (`XR_KHR_vulkan_enable2`) and eyes are copied on that same queue. It needs a custom Dawn
build; see [Windows Vulkan](#windows-vulkan).
This is an experimental renderer, not yet a release-ready VR mode.
@@ -11,14 +14,16 @@ This is an experimental renderer, not yet a release-ready VR mode.
- A Windows OpenXR runtime selected as the system's active runtime.
- A connected headset supported by that runtime.
- A D3D12-capable GPU and driver accepted by both OpenXR and Dawn.
- A D3D12-capable GPU and driver accepted by both OpenXR and Dawn, or for the opt-in Vulkan
binding a Vulkan 1.1+ driver plus the custom Dawn described under [Windows Vulkan](#windows-vulkan).
- A build made with `MKW_ENABLE_OPENXR=ON`, which defaults on for Windows and off elsewhere while
the Vulkan bridge remains capability-gated.
For managed installation, use [WheelWizard VR](https://github.com/iChris4/WheelWizard_VR/releases/latest)
and enable **Settings → Other → WiiCompiled (beta) → Enable WiiCompiled OpenXR VR (beta)**.
The launcher sets `vr.enabled=true`, `vr.required=false`, and `video.graphics_api="d3d12"` before
each VR launch, preserving other preferences. Its portable configuration lives at
each VR launch, preserving other preferences. Setting `video.graphics_api = "vulkan"` in that
configuration selects the Vulkan binding instead. Its portable configuration lives at
`RecompVR/UserData/Config.toml` beneath WheelWizard's data folder. Normal graphics settings remain
in `Recomp/UserData/Config.toml`. Both backends use the normal installation's effective NAND.
@@ -320,7 +325,7 @@ short-lived immutable stereo packet. Each sealed GX frame and immersive packet c
policy-generation tag; a mismatch is rendered in mono and the acquired XR frame is canceled, so an
asynchronous menu/race transition cannot replay race transforms over unsafe content.
With interpolation off, both PC (D3D12) and standalone (Vulkan) pace render-first:
With interpolation off, PC (D3D12 and Windows Vulkan) and standalone (Android Vulkan) pace render-first:
the pacing thread locates views for an estimated display time (two periods past the last
prediction), hands Aurora a packet without leaving a compositor frame open, and waits for
rendering. A 50 ms stall repeats the retained layer; cancellation also advances a keep-alive
@@ -328,16 +333,17 @@ cycle to refresh timing. Once rendering is submitted, the thread calls xrWaitFra
xrBeginFrame, completes backend-specific copy/release work, and ends the frame using the
packet's original render poses with the current compositor display time.
Vulkan renders into shared buffers and copies them into newly acquired XR images afterward.
D3D12 acquires images from its non-retained swapchain pair before rendering; Aurora queues the
copy on the session's D3D12 queue before reporting completion. PC therefore needs no additional
Android Vulkan renders into shared buffers and copies them into newly acquired XR images afterward.
Both PC bindings acquire images from their non-retained swapchain pair before rendering; Aurora
queues the copy on the session's queue before reporting completion. PC therefore needs no additional
copy in the short compositor cycle. Pending images remain acquired and separate from the
retained pair until completion or confirmed cancellation before encoding. GPU failure still
requires the existing queue-drain teardown. Rendered poses keep the session/reference-space
serials recorded when the packet was prepared, so changes during rendering invalidate them.
VR interpolation keeps the frame-first order on both backends because it renders for the
frame's own predicted display time. The log announces `OpenXR D3D12 pacing: render-first` or
frame's own predicted display time. The log announces `OpenXR D3D12 pacing: render-first` (or
`OpenXR Vulkan pacing: …` on the Vulkan binding) or
`frame-first (VR interpolation)` on each transition. For PC testing, disable **VR** frame
interpolation for a race capture; changing desktop interpolation alone does not select this
path. Menus use render-first even when VR interpolation is configured for races. Compare the
@@ -456,7 +462,7 @@ When it is on, `console.log` receives lines tagged `[runtime] [xr-diag]`
| `poll-events`, `begin-call`, `locate-views` | Event polling, the xrBeginFrame call itself, and xrLocateViews. |
| `input-sync`, `sync-actions` | Complete input update and its xrSyncActions call. |
| `publish`, `withdraw` | Packet construction/publication and withdrawal, including mutex waits. |
| `set-targets` | D3D12 bridge target registration, including its mutex wait. |
| `set-targets` | D3D12/Vulkan bridge target registration, including its mutex wait. |
| `submission-wait` | Actual time waiting for a render result, including timeout paths; compare against the requested 50 ms. |
| `cancel` | Bridge cancellation attempt, whether it succeeds or fails. |
| `cancel-age` | Publication to cancellation, including packets never picked up. |
@@ -491,11 +497,58 @@ The current `console.log` is copied through a shared-read stream while it is sti
The copy runs on SDL's dialog thread (`runtime/src/log_export.cpp`), and the outcome is shown under
the button. `mkw_openxr_diagnostics_tests` and `mkw_log_export_tests` cover both without a headset.
## Windows Vulkan
`video.graphics_api = "vulkan"` selects a second Windows binding, `runtime/src/vr/openxr_vulkan_win32.cpp`,
with the same pacing thread, retained-layer protocol and policy as D3D12
(`runtime/include/vr/openxr_windows.h` picks the backend at startup). It is opt-in. It has raced on
a headset (SteamVR/OpenXR with a PlayStation VR2): immersive projection held the headset's full
90 Hz with no skipped display slots, and a 646-second session recorded no rejected or discarded
layers and no failed submissions. Other runtimes are still unexercised.
**Why a custom Dawn.** The pinned prebuilt Dawn DLL exposes no native Vulkan device, so
`aurora-main/patches/dawn` adds a small versioned C ABI to the pinned Dawn source
(`aurora_dawn_vulkan_abi.h`, `AURORA_DAWN_VULKAN_ABI = 1`): hooks that let the OpenXR runtime
create Dawn's `VkInstance`, choose the physical device and create the `VkDevice`; wrapping of a
borrowed `VkImage` as a Dawn texture; the release barrier back to `COLOR_ATTACHMENT_OPTIMAL`; a
device-guard lock; and a queue drain. `Launcher/Build-DawnVulkan.ps1` builds that DLL from the pinned
revision on a machine with Visual Studio 2022, Python 3.12+ and CMake, and writes `aurora-vulkan.json`
(revision, ABI, DLL hash). `Launcher/Prepare-Dependencies.ps1 -DawnVulkanPackage <dir>` installs it as
`dawn_prebuilt` in a fresh dependency destination after checking that provenance; re-harvest
`native_prebuilt` afterwards because the archives are pinned to the Dawn DLL hash. The runtime
build also fetches Vulkan headers (`vulkan_headers` dependency).
**Startup.** `QueryGraphicsRequirements` loads `xrGetVulkanGraphicsRequirements2KHR`,
`xrCreateVulkanInstanceKHR`, `xrCreateVulkanDeviceKHR` and `xrGetVulkanGraphicsDevice2KHR`, then
installs the hooks in Dawn. Without the custom DLL it fails with *"requires the custom Dawn library
with Aurora Vulkan ABI 1"* and the game continues on the desktop renderer. During
`aurora_initialize` the runtime creates Dawn's instance (Vulkan 1.2 is requested when the loader
and runtime allow it, so that timeline semaphores, which PC runtimes create on the application's
device, are a core feature the device hook can enable) and device on the runtime's physical GPU.
`BindAurora` confirms that Dawn's physical device is the one the runtime selected, creates the
session on Dawn's graphics queue, picks the sRGB sibling of Aurora's UNORM colour format (with
`XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT`) so the compositor decodes the gamma-encoded bytes, and
enables the bridge.
**Frames.** Each acquired XR image is wrapped once as a Dawn texture and reused. Aurora's frame
worker records the eye copies into its own command buffer; immediately after `queue.Submit`, still
under Aurora's submit mutex, the bridge appends the release barrier through Dawn's queue and
publishes the token that the pacing thread's `WaitForSubmission` consumes. The runtime may use
the VkQueue only inside `xrBeginFrame`, `xrEndFrame`, `xrAcquireSwapchainImage` and
`xrReleaseSwapchainImage`, so `OpenXRRuntime::LockGraphicsQueue` holds Dawn's device guard around
exactly those four calls and never across `xrWaitFrame` or `xrWaitSwapchainImage`.
**Tests.** `mkw_openxr_vulkan_replay_tests` compiles the real backend against the deterministic
compositor of the D3D12 replay tests, including the queue-guard requirement on acquire and release.
`vulkan_native_bridge_smoke` (aurora, `AURORA_GPU_SMOKE_TESTS=ON`, real GPU, no headset) drives the
custom DLL's ABI through three borrowed-image copy/readback cycles; run it with that DLL beside it.
## Backend status
| Backend | Status |
| --- | --- |
| Windows D3D12 | Implemented: same-adapter, same-device asynchronous OpenXR submission. |
| Windows Vulkan | Implemented, opt-in (`video.graphics_api = "vulkan"`): the runtime creates Dawn's Vulkan instance and device through `XR_KHR_vulkan_enable2`, eyes are copied on the same queue, and Dawn's device guard is held around the four queue-touching OpenXR calls. Needs the custom Dawn from `Launcher/Build-DawnVulkan.ps1`. Raced on SteamVR/PSVR2 at the headset's full rate; other runtimes unexercised. See [Windows Vulkan](#windows-vulkan). |
| Android Vulkan (Meta Quest) | Implemented and running on a Quest 3: the OpenXR side owns its own Vulkan device (`XR_KHR_vulkan_enable2`, `XR_KHR_vulkan_enable` fallback) and shares eyes with Dawn through `AHardwareBuffer`s ordered by sync-fd fences. Controllers arrive through OpenXR actions as a virtual SDL gamepad. See `docs/quest-port.md`. |
| Linux Vulkan | Not wired. The pinned Dawn package does not expose a native Vulkan device, and the AHardwareBuffer bridge is Android-only; a dma-buf/opaque-fd variant of the same design would cover desktop Linux. |
| Other platforms | Not wired yet. |
+4 -2
View File
@@ -50,7 +50,8 @@ The graphics layer is built on
Play at several times the console's resolution.
**Experimental OpenXR VR.**
Windows builds can render through a D3D12 OpenXR runtime without CPU readback. Menus and
Windows builds can render through an OpenXR runtime on D3D12, or on Vulkan with a custom Dawn
build, without CPU readback. Menus and
unsupported scenes appear as a head-locked virtual screen; a validated single-camera race switches
to immersive stereo rendering. VR is opt-in and falls back to the normal desktop renderer if the
runtime or headset is unavailable. See [`OPENXR.md`](OPENXR.md) for setup, configuration, and the
@@ -131,7 +132,8 @@ Saves and Miis use the normal installation's effective NAND; Retro Rewind retain
XML-directed saves and ghosts. Graphics, VR preferences, caches, and compiled binaries stay separate.
Uninstalling either backend in WheelWizard VR preserves configuration and shared progress.
Managed VR launches enable OpenXR with D3D12. If the runtime or headset is unavailable, the game
Managed VR launches enable OpenXR with D3D12; the Vulkan binding is opt-in through
`video.graphics_api` (see [OPENXR.md](OPENXR.md)). If the runtime or headset is unavailable, the game
continues on the desktop and displays the failure briefly; **F10 → VR** retains the explanation.
See [OpenXR configuration](OPENXR.md) and [distribution and validation](DISTRIBUTION.md).
+1 -1
View File
@@ -38,7 +38,7 @@ if (AURORA_ENABLE_GX)
# OpenXR-enabled runtime can fail back to desktop mode at runtime instead
# of producing unresolved interop references.
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
target_sources(aurora_core PRIVATE lib/webgpu/d3d12_interop.cpp)
target_sources(aurora_core PRIVATE lib/webgpu/d3d12_interop.cpp lib/webgpu/vulkan_win32_interop.cpp)
endif ()
# Android/Vulkan counterpart: the AHardwareBuffer stereo bridge. The file
# compiles to C ABI stubs on every other platform so the runtime's OpenXR
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <stdint.h>
// Versioned C ABI: no STL objects or compiler-specific C++ symbols cross the
// MSVC Dawn DLL / LLVM-MinGW runtime boundary. Vulkan handles are borrowed.
#define AURORA_DAWN_VULKAN_ABI 1
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
void* userdata;
int32_t (*createInstance)(void*, void* getProc, const void* info, const void* allocator, void** instance);
int32_t (*createDevice)(void*, void* getProc, void* physical, const void* info, const void* allocator, void** device);
int32_t (*getPhysicalDevice)(void*, void* instance, void** physical);
} AuroraDawnVulkanHooks;
typedef struct {
void* instance;
void* physicalDevice;
void* device;
uint32_t queueFamily;
uint32_t queueIndex;
} AuroraDawnVulkanHandles;
typedef uint32_t (*AuroraDawnVulkanVersionFn)(void);
typedef int (*AuroraDawnVulkanConfigureFn)(const AuroraDawnVulkanHooks*);
typedef int (*AuroraDawnVulkanHandlesFn)(void* device, AuroraDawnVulkanHandles*);
typedef void* (*AuroraDawnVulkanWrapFn)(void* device, const void* textureDescriptor, uint64_t image);
typedef int (*AuroraDawnVulkanReleaseFn)(void* device, void* const* textures, uint32_t count);
typedef void* (*AuroraDawnVulkanLockFn)(void* device);
typedef void (*AuroraDawnVulkanUnlockFn)(void* guard);
typedef int (*AuroraDawnVulkanDrainFn)(void* device);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <aurora/dawn_vulkan_abi.h>
#include <aurora/d3d12_interop.h>
#ifdef __cplusplus
extern "C" {
#endif
// Same pending-target/callback contract as D3D12. resource carries a VkImage
// encoded as a pointer-sized value; colorDxgiFormat carries a VkFormat.
bool aurora_vulkan_win32_configure(const AuroraDawnVulkanHooks* hooks);
bool aurora_vulkan_win32_get_handles(AuroraDawnVulkanHandles* handles, int64_t* colorFormat);
bool aurora_vulkan_win32_enable(AuroraD3D12StereoSubmittedCallback submitted, void* userdata);
bool aurora_vulkan_win32_set_targets(uint64_t token, const AuroraD3D12StereoTarget* targets, uint32_t count);
bool aurora_vulkan_win32_cancel(uint64_t token);
bool aurora_vulkan_win32_disable();
void* aurora_vulkan_win32_lock_queue();
void aurora_vulkan_win32_unlock_queue(void* guard);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,197 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <aurora/vulkan_win32_interop.h>
#include "../internal.hpp"
#include "../stereo.hpp"
#include "gpu.hpp"
#if defined(_WIN32) && defined(WEBGPU_DAWN) && defined(DAWN_ENABLE_BACKEND_VULKAN)
#include <windows.h>
#include <algorithm>
#include <array>
#include <memory>
#include <mutex>
#include <vector>
namespace aurora::vulkan_win32 {
namespace {
Module Log("aurora::vulkan_win32");
struct Api {
AuroraDawnVulkanConfigureFn configure = nullptr;
AuroraDawnVulkanHandlesFn handles = nullptr;
AuroraDawnVulkanWrapFn wrap = nullptr;
AuroraDawnVulkanReleaseFn release = nullptr;
AuroraDawnVulkanLockFn lock = nullptr;
AuroraDawnVulkanUnlockFn unlock = nullptr;
AuroraDawnVulkanDrainFn drain = nullptr;
bool Load() {
HMODULE module = GetModuleHandleW(L"webgpu_dawn.dll");
if (!module) return false;
auto version = reinterpret_cast<AuroraDawnVulkanVersionFn>(GetProcAddress(module, "AuroraDawnVulkanVersion"));
if (!version || version() != AURORA_DAWN_VULKAN_ABI) return false;
#define LOAD(member, type, name) member = reinterpret_cast<type>(GetProcAddress(module, name)); if (!member) return false
LOAD(configure, AuroraDawnVulkanConfigureFn, "AuroraDawnVulkanConfigure");
LOAD(handles, AuroraDawnVulkanHandlesFn, "AuroraDawnVulkanGetHandles");
LOAD(wrap, AuroraDawnVulkanWrapFn, "AuroraDawnVulkanWrap");
LOAD(release, AuroraDawnVulkanReleaseFn, "AuroraDawnVulkanRelease");
LOAD(lock, AuroraDawnVulkanLockFn, "AuroraDawnVulkanLock");
LOAD(unlock, AuroraDawnVulkanUnlockFn, "AuroraDawnVulkanUnlock");
LOAD(drain, AuroraDawnVulkanDrainFn, "AuroraDawnVulkanDrain");
#undef LOAD
return true;
}
} api;
int64_t VkFormat(wgpu::TextureFormat format) {
switch (format) {
case wgpu::TextureFormat::RGBA8Unorm: return 37;
case wgpu::TextureFormat::RGBA8UnormSrgb: return 43;
case wgpu::TextureFormat::BGRA8Unorm: return 44;
case wgpu::TextureFormat::BGRA8UnormSrgb: return 50;
case wgpu::TextureFormat::RGBA16Float: return 97;
default: return 0;
}
}
bool CopyCompatible(int64_t a, int64_t b) {
return a == b || ((a == 37 || a == 43) && (b == 37 || b == 43)) ||
((a == 44 || a == 50) && (b == 44 || b == 50));
}
struct Import { uint64_t image; uint32_t width, height; wgpu::TextureFormat format; wgpu::Texture texture; };
class Bridge {
public:
std::mutex mutex;
std::vector<Import> imports;
std::array<AuroraD3D12StereoTarget, 2> targets{};
std::array<wgpu::Texture, 2> active{};
uint64_t token = 0;
uint32_t count = 0;
bool encoded = false;
AuroraD3D12StereoSubmittedCallback callback;
void* userdata;
Bridge(AuroraD3D12StereoSubmittedCallback cb, void* data) : callback(cb), userdata(data) {}
bool Set(uint64_t next, const AuroraD3D12StereoTarget* data, uint32_t n) {
if (!next || !data || !n || n > 2) return false;
std::lock_guard guard(mutex);
if (token) return false;
for (uint32_t i = 0; i < n; ++i) {
if (!data[i].resource || !data[i].width || !data[i].height) return false;
targets[i] = data[i];
}
token = next; count = n; return true;
}
bool Cancel(uint64_t wanted) {
std::unique_lock guard(mutex, std::try_to_lock);
if (!guard.owns_lock() || encoded || !token || token != wanted) return false;
token = 0; return true;
}
bool Encode(wgpu::CommandEncoder& encoder, const stereo::SinkFrame& frame) {
std::lock_guard guard(mutex);
if (!token || encoded || frame.frameToken != token) return false;
// Validate/import every target before recording any copy.
for (uint32_t i = 0; i < count; ++i) {
const auto& eye = frame.eyes[i];
const auto& target = targets[i];
if (!eye.texture || eye.size.width != target.width || eye.size.height != target.height ||
!CopyCompatible(VkFormat(eye.format), target.dxgiFormat)) return false;
const uint64_t image = reinterpret_cast<uintptr_t>(target.resource);
auto it = std::find_if(imports.begin(), imports.end(), [&](const Import& entry) { return entry.image == image; });
if (it == imports.end()) {
wgpu::TextureDescriptor descriptor;
descriptor.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::RenderAttachment;
descriptor.size = {target.width, target.height, 1};
descriptor.format = eye.format;
void* wrapped = api.wrap(webgpu::g_device.Get(), &descriptor, image);
if (!wrapped) return false;
imports.push_back({image, target.width, target.height, eye.format,
wgpu::Texture::Acquire(static_cast<WGPUTexture>(wrapped))});
it = imports.end() - 1;
}
if (it->width != target.width || it->height != target.height || it->format != eye.format) {
// The runtime reuses VkImage handles across swapchain recreation, and
// Aurora's eye format can change with the surface. Re-wrap rather than
// rejecting every future frame for this image.
imports.erase(it);
--i;
continue;
}
active[i] = it->texture;
}
for (uint32_t i = 0; i < count; ++i) {
wgpu::TexelCopyTextureInfo source, destination;
source.texture = *frame.eyes[i].texture;
destination.texture = active[i];
wgpu::Extent3D size{targets[i].width, targets[i].height, 1};
encoder.CopyTextureToTexture(&source, &destination, &size);
}
encoded = true;
return true;
}
void Submitted(const stereo::SinkFrame& frame) {
std::lock_guard guard(mutex);
if (!token || !encoded || token != frame.frameToken) return;
std::array<void*, 2> textures{};
for (uint32_t i = 0; i < count; ++i) textures[i] = active[i].Get();
// Append the COLOR_ATTACHMENT_OPTIMAL release barriers to Dawn's queue,
// flush them under its device guard, then allow the XR thread to release.
const bool success = api.release(webgpu::g_device.Get(), textures.data(), count) != 0;
const auto completed = token;
token = 0; encoded = false;
callback(completed, success, userdata);
}
};
std::unique_ptr<Bridge> bridge;
}
}
bool aurora_vulkan_win32_configure(const AuroraDawnVulkanHooks* hooks) {
using namespace aurora::vulkan_win32;
return api.Load() && api.configure(hooks);
}
bool aurora_vulkan_win32_get_handles(AuroraDawnVulkanHandles* handles, int64_t* format) {
using namespace aurora;
if (!webgpu::g_device || webgpu::g_backendType != wgpu::BackendType::Vulkan || !vulkan_win32::api.Load()) return false;
*format = vulkan_win32::VkFormat(webgpu::g_graphicsConfig.surfaceConfiguration.format);
return *format && vulkan_win32::api.handles(webgpu::g_device.Get(), handles);
}
bool aurora_vulkan_win32_enable(AuroraD3D12StereoSubmittedCallback cb, void* data) {
using namespace aurora::vulkan_win32;
if (bridge || !cb || !api.Load()) return false;
bridge = std::make_unique<Bridge>(cb, data);
aurora::stereo::set_sink(
[](wgpu::CommandEncoder& encoder, const aurora::stereo::SinkFrame& frame, void* self) noexcept {
return static_cast<Bridge*>(self)->Encode(encoder, frame);
}, [](const aurora::stereo::SinkFrame& frame, void* self) noexcept { static_cast<Bridge*>(self)->Submitted(frame); }, bridge.get());
return true;
}
bool aurora_vulkan_win32_set_targets(uint64_t token, const AuroraD3D12StereoTarget* targets, uint32_t count) {
using namespace aurora::vulkan_win32;
return bridge && bridge->Set(token, targets, count);
}
bool aurora_vulkan_win32_cancel(uint64_t token) {
using namespace aurora::vulkan_win32;
return bridge && bridge->Cancel(token);
}
bool aurora_vulkan_win32_disable() {
using namespace aurora::vulkan_win32;
if (!bridge) return true;
aurora::stereo::set_sink(nullptr, nullptr, nullptr);
if (!api.drain(aurora::webgpu::g_device.Get())) { bridge.release(); return false; }
bridge.reset();
return true;
}
void* aurora_vulkan_win32_lock_queue() {
return aurora::vulkan_win32::api.lock(aurora::webgpu::g_device.Get());
}
void aurora_vulkan_win32_unlock_queue(void* guard) { aurora::vulkan_win32::api.unlock(guard); }
#else
// C ABI stubs keep the runtime's OpenXR integration linkable on Windows GX
// builds whose Dawn has no Vulkan backend; the backend then reports that the
// bridge is unavailable and the game falls back to the desktop renderer.
bool aurora_vulkan_win32_configure(const AuroraDawnVulkanHooks*) { return false; }
bool aurora_vulkan_win32_get_handles(AuroraDawnVulkanHandles* handles, int64_t* format) {
if (handles) *handles = {};
if (format) *format = 0;
return false;
}
bool aurora_vulkan_win32_enable(AuroraD3D12StereoSubmittedCallback, void*) { return false; }
bool aurora_vulkan_win32_set_targets(uint64_t, const AuroraD3D12StereoTarget*, uint32_t) { return false; }
bool aurora_vulkan_win32_cancel(uint64_t) { return false; }
bool aurora_vulkan_win32_disable() { return true; }
void* aurora_vulkan_win32_lock_queue() { return nullptr; }
void aurora_vulkan_win32_unlock_queue(void*) {}
#endif
+38
View File
@@ -0,0 +1,38 @@
"""Apply the versioned Aurora native Vulkan ABI to the pinned Dawn source tree."""
from pathlib import Path
import shutil, sys
root = Path(sys.argv[1]).resolve()
here = Path(__file__).resolve().parent
for name in ("aurora_vulkan_hooks.h", "aurora_vulkan_interop.inc"):
shutil.copyfile(here / name, root / "src/dawn/native/vulkan" / name)
shutil.copyfile(here.parent.parent / "include/aurora/dawn_vulkan_abi.h",
root / "src/dawn/native/vulkan/aurora_dawn_vulkan_abi.h")
p = root / "src/dawn/native/vulkan/VulkanBackend.cpp"
s = p.read_text()
if '#include "aurora_vulkan_interop.inc"' not in s:
s += '\n#include "aurora_vulkan_interop.inc"\n'
p.write_text(s)
p = root / "src/dawn/common/DynamicLib.cpp"
s = p.read_text()
# LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR requires an absolute filename. Vulkan's
# system loader is also tried by bare name; preserve the restricted default
# search directories for that case instead of failing with ERROR_INVALID_PARAMETER.
old = 'LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS;'
new = '''((filename.size() > 2 && filename[1] == ':') || filename.starts_with("\\\\\\\\")
? LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR : 0) | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS;'''
if old in s:
s = s.replace(old, new, 1)
p.write_text(s)
p = root / "src/dawn/native/vulkan/VulkanFunctions.cpp"
s = p.read_text()
if '#include "aurora_vulkan_hooks.h"' not in s:
marker = 'namespace dawn::native::vulkan {'
assert marker in s
s = s.replace(marker, '#include "aurora_vulkan_hooks.h"\n\n' + marker, 1)
for old, new in [
('GET_GLOBAL_PROC(CreateInstance);', 'CreateInstance = AuroraCreateInstance(GetInstanceProcAddr);'),
('GET_INSTANCE_PROC(CreateDevice);', 'CreateDevice = AuroraCreateDevice(GetInstanceProcAddr, instance);'),
('GET_INSTANCE_PROC(EnumeratePhysicalDevices);', 'EnumeratePhysicalDevices = AuroraEnumeratePhysicalDevices(GetInstanceProcAddr, instance);')]:
assert old in s, old
s = s.replace(old, old + '\n ' + new, 1)
p.write_text(s)
@@ -0,0 +1,9 @@
// Included only by the pinned Dawn source build.
#pragma once
#include "dawn/native/DawnNative.h"
#include "aurora_dawn_vulkan_abi.h"
namespace dawn::native::vulkan {
PFN_vkCreateInstance AuroraCreateInstance(PFN_vkGetInstanceProcAddr proc);
PFN_vkCreateDevice AuroraCreateDevice(PFN_vkGetInstanceProcAddr proc, VkInstance instance);
PFN_vkEnumeratePhysicalDevices AuroraEnumeratePhysicalDevices(PFN_vkGetInstanceProcAddr proc, VkInstance instance);
}
@@ -0,0 +1,118 @@
// Compiled inside VulkanBackend.cpp, using the pinned Dawn implementation.
#include "aurora_vulkan_hooks.h"
#include "src/dawn/native/ChainUtils.h"
#include "src/dawn/native/vulkan/PhysicalDeviceVk.h"
#include "src/dawn/native/vulkan/QueueVk.h"
#include <mutex>
namespace dawn::native::vulkan {
namespace {
AuroraDawnVulkanHooks auroraHooks{};
std::recursive_mutex auroraHooksMutex;
PFN_vkGetInstanceProcAddr auroraGetProc = nullptr;
VkInstance auroraInstance = VK_NULL_HANDLE;
::VkResult VKAPI_CALL AuroraCreateInstanceImpl(const VkInstanceCreateInfo* info,
const VkAllocationCallbacks* allocator, VkInstance* instance) {
std::lock_guard lock(auroraHooksMutex);
if (auroraHooks.createInstance) return static_cast<::VkResult>(auroraHooks.createInstance(
auroraHooks.userdata, reinterpret_cast<void*>(auroraGetProc), info, allocator,
reinterpret_cast<void**>(instance)));
return reinterpret_cast<PFN_vkCreateInstance>(auroraGetProc(nullptr, "vkCreateInstance"))(info, allocator, instance);
}
::VkResult VKAPI_CALL AuroraCreateDeviceImpl(VkPhysicalDevice physical, const VkDeviceCreateInfo* info,
const VkAllocationCallbacks* allocator, VkDevice* device) {
std::lock_guard lock(auroraHooksMutex);
if (auroraHooks.createDevice) return static_cast<::VkResult>(auroraHooks.createDevice(
auroraHooks.userdata, reinterpret_cast<void*>(auroraGetProc), physical, info, allocator,
reinterpret_cast<void**>(device)));
return reinterpret_cast<PFN_vkCreateDevice>(auroraGetProc(auroraInstance, "vkCreateDevice"))(physical, info, allocator, device);
}
::VkResult VKAPI_CALL AuroraEnumerateImpl(VkInstance instance, uint32_t* count, VkPhysicalDevice* devices) {
std::lock_guard lock(auroraHooksMutex);
if (!auroraHooks.getPhysicalDevice) return reinterpret_cast<PFN_vkEnumeratePhysicalDevices>(
auroraGetProc(instance, "vkEnumeratePhysicalDevices"))(instance, count, devices);
void* physical = nullptr;
const auto result = static_cast<::VkResult>(auroraHooks.getPhysicalDevice(auroraHooks.userdata, instance, &physical));
if (result != VK_SUCCESS) return result;
if (!devices) { *count = 1; return VK_SUCCESS; }
if (*count == 0) return VK_INCOMPLETE;
devices[0] = static_cast<VkPhysicalDevice>(physical);
*count = 1;
return VK_SUCCESS;
}
struct AuroraDeviceGuard { decltype(std::declval<Device*>()->GetGuard()) guard; explicit AuroraDeviceGuard(Device* device) : guard(device->GetGuard()) {} };
}
PFN_vkCreateInstance AuroraCreateInstance(PFN_vkGetInstanceProcAddr proc) {
std::lock_guard lock(auroraHooksMutex);
if (!auroraHooks.createInstance) return reinterpret_cast<PFN_vkCreateInstance>(proc(nullptr, "vkCreateInstance"));
auroraGetProc = proc;
return AuroraCreateInstanceImpl;
}
PFN_vkCreateDevice AuroraCreateDevice(PFN_vkGetInstanceProcAddr proc, VkInstance instance) {
std::lock_guard lock(auroraHooksMutex);
if (!auroraHooks.createDevice) return reinterpret_cast<PFN_vkCreateDevice>(proc(instance, "vkCreateDevice"));
auroraGetProc = proc; auroraInstance = instance;
return AuroraCreateDeviceImpl;
}
PFN_vkEnumeratePhysicalDevices AuroraEnumeratePhysicalDevices(PFN_vkGetInstanceProcAddr proc, VkInstance instance) {
std::lock_guard lock(auroraHooksMutex);
if (!auroraHooks.getPhysicalDevice) return reinterpret_cast<PFN_vkEnumeratePhysicalDevices>(proc(instance, "vkEnumeratePhysicalDevices"));
auroraGetProc = proc; auroraInstance = instance;
return AuroraEnumerateImpl;
}
extern "C" DAWN_NATIVE_EXPORT uint32_t AuroraDawnVulkanVersion() { return AURORA_DAWN_VULKAN_ABI; }
extern "C" DAWN_NATIVE_EXPORT int AuroraDawnVulkanConfigure(const AuroraDawnVulkanHooks* hooks) {
std::lock_guard lock(auroraHooksMutex);
// Called before adapter discovery, or cleared after Aurora shutdown. The
// runtime and hook storage must outlive all device creation calls.
auroraHooks = hooks ? *hooks : AuroraDawnVulkanHooks{};
return 1;
}
extern "C" DAWN_NATIVE_EXPORT int AuroraDawnVulkanGetHandles(void* handle, AuroraDawnVulkanHandles* out) {
auto* device = ToBackend(FromAPI(static_cast<WGPUDevice>(handle)));
auto guard = device->GetGuard();
if (!device->HasFeature(Feature::ImplicitDeviceSynchronization)) return 0;
*out = {device->GetVkInstance(), ToBackend(device->GetPhysicalDevice())->GetVkPhysicalDevice(),
device->GetVkDevice(), device->GetGraphicsQueueFamily(), 0};
return 1;
}
extern "C" DAWN_NATIVE_EXPORT void* AuroraDawnVulkanWrap(void* handle, const void* desc, uint64_t image) {
auto* device = ToBackend(FromAPI(static_cast<WGPUDevice>(handle)));
auto guard = device->GetGuard();
// Mirror DeviceBase::CreateTexture: fill the trivial frontend defaults
// (dimension, mip and sample counts) and validate before the backend reads
// them. An Undefined dimension would otherwise abort the first copy.
TextureDescriptor raw = reinterpret_cast<const TextureDescriptor*>(desc)->WithTrivialFrontendDefaults();
UnpackedPtr<TextureDescriptor> descriptor;
if (device->ConsumedError(ValidateAndUnpack(&raw), &descriptor)) return nullptr;
if (device->ConsumedError(ValidateTextureDescriptor(device, descriptor, AllowMultiPlanarTextureFormat::No))) return nullptr;
auto texture = SwapChainTexture::Create(device, descriptor, VkImage::CreateFromHandle(reinterpret_cast<::VkImage>(image)));
texture->SetIsSubresourceContentInitialized(true, texture->GetAllSubresources());
texture->UpdateUsage(wgpu::TextureUsage::RenderAttachment, wgpu::ShaderStage::None, texture->GetAllSubresources());
return ToAPI(ReturnToAPI(std::move(texture)));
}
extern "C" DAWN_NATIVE_EXPORT int AuroraDawnVulkanRelease(void* handle, void* const* textures, uint32_t count) {
auto* device = ToBackend(FromAPI(static_cast<WGPUDevice>(handle)));
auto guard = device->GetGuard();
auto* queue = ToBackend(device->GetQueue());
auto* context = queue->GetPendingRecordingContext();
for (uint32_t i = 0; i < count; ++i) {
auto* texture = ToBackend(FromAPI(static_cast<WGPUTexture>(textures[i])));
texture->TransitionUsageNow(context, wgpu::TextureUsage::RenderAttachment,
wgpu::ShaderStage::None, texture->GetAllSubresources());
}
return !device->ConsumedError(queue->SubmitPendingCommands());
}
extern "C" DAWN_NATIVE_EXPORT void* AuroraDawnVulkanLock(void* handle) {
auto* device = ToBackend(FromAPI(static_cast<WGPUDevice>(handle)));
return new AuroraDeviceGuard(device);
}
extern "C" DAWN_NATIVE_EXPORT void AuroraDawnVulkanUnlock(void* guard) {
delete static_cast<AuroraDeviceGuard*>(guard);
}
extern "C" DAWN_NATIVE_EXPORT int AuroraDawnVulkanDrain(void* handle) {
auto* device = ToBackend(FromAPI(static_cast<WGPUDevice>(handle)));
auto guard = device->GetGuard();
if (device->ConsumedError(ToBackend(device->GetQueue())->SubmitPendingCommands())) return 0;
return device->fn.QueueWaitIdle(ToBackend(device->GetQueue())->GetVkQueue()) == VK_SUCCESS;
}
}
+12
View File
@@ -3,6 +3,18 @@ include(GoogleTest)
option(AURORA_GPU_SMOKE_TESTS "Build opt-in tests requiring a desktop GPU" OFF)
if (AURORA_GPU_SMOKE_TESTS AND AURORA_ENABLE_GX AND WIN32)
# Exercises the custom Dawn DLL's Aurora Vulkan ABI (patches/dawn). Run it with that
# webgpu_dawn.dll beside the executable; the stock prebuilt Dawn lacks the exports and the
# test reports that at startup. Vulkan headers come from the SDK or any tree on
# AURORA_TEST_VULKAN_INCLUDE (the runtime's FetchContent copy works).
find_path(AURORA_TEST_VULKAN_INCLUDE vulkan/vulkan.h HINTS "$ENV{VULKAN_SDK}/Include")
if (AURORA_TEST_VULKAN_INCLUDE)
add_executable(vulkan_native_bridge_smoke vulkan_native_bridge_smoke.cpp)
target_include_directories(vulkan_native_bridge_smoke PRIVATE ../include "${AURORA_TEST_VULKAN_INCLUDE}")
target_link_libraries(vulkan_native_bridge_smoke PRIVATE dawn::webgpu_dawn dawn::dawncpp_headers)
else ()
message(STATUS "vulkan_native_bridge_smoke skipped: no vulkan/vulkan.h (set VULKAN_SDK or AURORA_TEST_VULKAN_INCLUDE)")
endif ()
add_executable(stereo_frame_worker_smoke stereo_frame_worker_smoke.cpp)
target_include_directories(stereo_frame_worker_smoke PRIVATE ../lib)
target_link_libraries(stereo_frame_worker_smoke PRIVATE aurora::core aurora::gx aurora::main aurora::vi
@@ -0,0 +1,162 @@
// Opt-in real GPU test for the custom Dawn DLL. No headset required.
// Exercises the MinGW/MSVC C ABI, borrowed-image lifetime and repeated layout
// transitions. Run with VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation as well.
#define NOMINMAX
#include <windows.h>
#include <vulkan/vulkan.h>
#include <dawn/webgpu_cpp.h>
#include <aurora/dawn_vulkan_abi.h>
#include <array>
#include <cstdio>
#include <cstdlib>
static void Check(bool ok) { if (!ok) { std::fputs("Native Vulkan bridge smoke failed\n", stderr); std::abort(); } }
static PFN_vkGetInstanceProcAddr hookProc;
static VkInstance hookInstance;
static int instances = 0, devices = 0, selections = 0;
static int32_t CreateInstance(void*, void* proc, const void* info, const void* allocator, void** out) {
++instances; hookProc = reinterpret_cast<PFN_vkGetInstanceProcAddr>(proc);
auto create = reinterpret_cast<PFN_vkCreateInstance>(hookProc(nullptr, "vkCreateInstance"));
auto result = create(static_cast<const VkInstanceCreateInfo*>(info), static_cast<const VkAllocationCallbacks*>(allocator), &hookInstance);
*out = hookInstance; return result;
}
static int32_t CreateDevice(void*, void*, void* physical, const void* info, const void* allocator, void** out) {
++devices;
auto create = reinterpret_cast<PFN_vkCreateDevice>(hookProc(hookInstance, "vkCreateDevice"));
return create(static_cast<VkPhysicalDevice>(physical), static_cast<const VkDeviceCreateInfo*>(info),
static_cast<const VkAllocationCallbacks*>(allocator), reinterpret_cast<VkDevice*>(out));
}
static int32_t SelectPhysical(void*, void* instance, void** out) {
++selections;
auto enumerate = reinterpret_cast<PFN_vkEnumeratePhysicalDevices>(hookProc(static_cast<VkInstance>(instance), "vkEnumeratePhysicalDevices"));
uint32_t count = 1;
auto result = enumerate(static_cast<VkInstance>(instance), &count, reinterpret_cast<VkPhysicalDevice*>(out));
return result == VK_INCOMPLETE ? VK_SUCCESS : result;
}
int main() {
auto dll = LoadLibraryW(L"webgpu_dawn.dll"); Check(dll != nullptr);
#define API(name, type) auto name = reinterpret_cast<type>(GetProcAddress(dll, "AuroraDawnVulkan" #name)); Check(name != nullptr)
API(Version, AuroraDawnVulkanVersionFn);
API(Configure, AuroraDawnVulkanConfigureFn);
API(GetHandles, AuroraDawnVulkanHandlesFn);
API(Wrap, AuroraDawnVulkanWrapFn);
API(Release, AuroraDawnVulkanReleaseFn);
API(Lock, AuroraDawnVulkanLockFn);
API(Unlock, AuroraDawnVulkanUnlockFn);
API(Drain, AuroraDawnVulkanDrainFn);
Check(Version() == AURORA_DAWN_VULKAN_ABI);
AuroraDawnVulkanHooks hooks{nullptr, CreateInstance, CreateDevice, SelectPhysical};
Check(Configure(&hooks));
wgpu::InstanceFeatureName feature = wgpu::InstanceFeatureName::TimedWaitAny;
wgpu::InstanceDescriptor instanceDesc;
instanceDesc.requiredFeatureCount = 1; instanceDesc.requiredFeatures = &feature;
std::fputs("Creating WebGPU instance\n", stderr);
auto instance = wgpu::CreateInstance(&instanceDesc);
wgpu::RequestAdapterOptions options; options.backendType = wgpu::BackendType::Vulkan;
wgpu::Adapter adapter;
std::fputs("Requesting Vulkan adapter\n", stderr);
auto future = instance.RequestAdapter(&options, wgpu::CallbackMode::WaitAnyOnly,
[&](wgpu::RequestAdapterStatus status, wgpu::Adapter value, wgpu::StringView) {
Check(status == wgpu::RequestAdapterStatus::Success); adapter = std::move(value);
});
Check(instance.WaitAny(future, 10'000'000'000) == wgpu::WaitStatus::Success);
wgpu::FeatureName sync = wgpu::FeatureName::ImplicitDeviceSynchronization;
wgpu::DeviceDescriptor deviceDesc;
deviceDesc.requiredFeatureCount = 1; deviceDesc.requiredFeatures = &sync;
deviceDesc.SetUncapturedErrorCallback([](const wgpu::Device&, wgpu::ErrorType, wgpu::StringView message) {
std::fprintf(stderr, "Dawn: %.*s\n", static_cast<int>(message.length), message.data); Check(false);
});
std::fputs("Creating Vulkan device\n", stderr);
auto device = adapter.CreateDevice(&deviceDesc); Check(!!device);
Check(instances > 0 && devices > 0 && selections > 0);
std::fputs("Getting native device\n", stderr);
AuroraDawnVulkanHandles handles{}; Check(GetHandles(device.Get(), &handles));
VkDevice vkDevice = static_cast<VkDevice>(handles.device);
VkPhysicalDevice physical = static_cast<VkPhysicalDevice>(handles.physicalDevice);
auto loader = LoadLibraryW(L"vulkan-1.dll"); Check(loader != nullptr);
auto getProc = reinterpret_cast<PFN_vkGetInstanceProcAddr>(GetProcAddress(loader, "vkGetInstanceProcAddr"));
#define VK(name) auto name = reinterpret_cast<PFN_##name>(getProc(static_cast<VkInstance>(handles.instance), #name)); Check(name != nullptr)
VK(vkCreateImage); VK(vkGetImageMemoryRequirements); VK(vkGetPhysicalDeviceMemoryProperties);
VK(vkAllocateMemory); VK(vkBindImageMemory); VK(vkCreateCommandPool); VK(vkAllocateCommandBuffers);
VK(vkBeginCommandBuffer); VK(vkCmdPipelineBarrier); VK(vkEndCommandBuffer); VK(vkGetDeviceQueue);
VK(vkQueueSubmit); VK(vkQueueWaitIdle); VK(vkDestroyCommandPool); VK(vkDestroyImage); VK(vkFreeMemory);
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
imageInfo.imageType = VK_IMAGE_TYPE_2D; imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
imageInfo.extent = {64, 16, 1}; imageInfo.mipLevels = 1; imageInfo.arrayLayers = 1;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
std::fputs("Creating borrowed Vulkan image\n", stderr);
VkImage image; Check(vkCreateImage(vkDevice, &imageInfo, nullptr, &image) == VK_SUCCESS);
VkMemoryRequirements requirements; vkGetImageMemoryRequirements(vkDevice, image, &requirements);
VkPhysicalDeviceMemoryProperties properties; vkGetPhysicalDeviceMemoryProperties(physical, &properties);
uint32_t memoryType = 0;
while (!(requirements.memoryTypeBits & (1u << memoryType))) ++memoryType;
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = memoryType;
VkDeviceMemory memory; Check(vkAllocateMemory(vkDevice, &allocation, nullptr, &memory) == VK_SUCCESS);
Check(vkBindImageMemory(vkDevice, image, memory, 0) == VK_SUCCESS);
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; poolInfo.queueFamilyIndex = handles.queueFamily;
VkCommandPool pool; Check(vkCreateCommandPool(vkDevice, &poolInfo, nullptr, &pool) == VK_SUCCESS);
VkCommandBufferAllocateInfo alloc{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
alloc.commandPool = pool; alloc.commandBufferCount = 1; alloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
VkCommandBuffer commands; Check(vkAllocateCommandBuffers(vkDevice, &alloc, &commands) == VK_SUCCESS);
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
Check(vkBeginCommandBuffer(commands, &begin) == VK_SUCCESS);
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image; barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
0, 0, nullptr, 0, nullptr, 1, &barrier);
Check(vkEndCommandBuffer(commands) == VK_SUCCESS);
VkQueue queue; vkGetDeviceQueue(vkDevice, handles.queueFamily, handles.queueIndex, &queue);
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; submit.pCommandBuffers = &commands;
std::fputs("Locking native queue\n", stderr);
auto guard = Lock(device.Get());
Check(vkQueueSubmit(queue, 1, &submit, VK_NULL_HANDLE) == VK_SUCCESS);
Check(vkQueueWaitIdle(queue) == VK_SUCCESS); Unlock(guard);
wgpu::TextureDescriptor textureDesc;
textureDesc.size = {64, 16, 1}; textureDesc.format = wgpu::TextureFormat::RGBA8Unorm;
textureDesc.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::RenderAttachment;
std::fputs("Wrapping borrowed image\n", stderr);
auto borrowed = wgpu::Texture::Acquire(static_cast<WGPUTexture>(Wrap(device.Get(), &textureDesc, reinterpret_cast<uint64_t>(image))));
Check(!!borrowed);
std::fputs("Creating copy resources\n", stderr);
auto source = device.CreateTexture(&textureDesc);
wgpu::BufferDescriptor bufferDesc; bufferDesc.size = 4096;
bufferDesc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead;
auto readback = device.CreateBuffer(&bufferDesc);
std::fputs("Running GPU copy\n", stderr);
for (uint8_t value : {17, 99, 201}) {
std::array<uint8_t, 4096> pixels; pixels.fill(value);
wgpu::TexelCopyTextureInfo sourceInfo; sourceInfo.texture = source;
wgpu::TexelCopyTextureInfo targetInfo; targetInfo.texture = borrowed;
wgpu::TexelCopyBufferLayout layout; layout.bytesPerRow = 256; layout.rowsPerImage = 16;
wgpu::Extent3D extent{64, 16, 1};
device.GetQueue().WriteTexture(&sourceInfo, pixels.data(), pixels.size(), &layout, &extent);
std::fputs("WriteTexture done\n", stderr);
auto encoder = device.CreateCommandEncoder(); encoder.CopyTextureToTexture(&sourceInfo, &targetInfo, &extent);
auto copy = encoder.Finish(); device.GetQueue().Submit(1, &copy);
std::fputs("Copy submitted\n", stderr);
void* textures[] = {borrowed.Get()}; Check(Release(device.Get(), textures, 1));
std::fputs("Release transition done\n", stderr);
encoder = device.CreateCommandEncoder();
wgpu::TexelCopyBufferInfo destination; destination.buffer = readback; destination.layout = layout;
encoder.CopyTextureToBuffer(&targetInfo, &destination, &extent);
copy = encoder.Finish(); device.GetQueue().Submit(1, &copy);
Check(Release(device.Get(), textures, 1));
auto mapped = readback.MapAsync(wgpu::MapMode::Read, 0, 4096, wgpu::CallbackMode::WaitAnyOnly,
[](wgpu::MapAsyncStatus status, wgpu::StringView) { Check(status == wgpu::MapAsyncStatus::Success); });
Check(instance.WaitAny(mapped, 10'000'000'000) == wgpu::WaitStatus::Success);
const auto* bytes = static_cast<const uint8_t*>(readback.GetConstMappedRange());
for (size_t i = 0; i < pixels.size(); ++i) Check(bytes[i] == value);
readback.Unmap();
}
Check(Drain(device.Get())); borrowed = nullptr;
// The wrapper must not free the runtime-owned image or its memory.
vkDestroyImage(vkDevice, image, nullptr); vkFreeMemory(vkDevice, memory, nullptr);
vkDestroyCommandPool(vkDevice, pool, nullptr);
Check(Configure(nullptr));
std::puts("Native Vulkan bridge: three GPU copy/readback cycles passed");
}
+20
View File
@@ -286,6 +286,15 @@ if(MKW_PROJECT_COMPILE_DEFINITIONS)
add_compile_definitions(${MKW_PROJECT_COMPILE_DEFINITIONS})
endif()
if(MKW_PLATFORM_WINDOWS AND MKW_ENABLE_OPENXR)
include(FetchContent)
FetchContent_Declare(vulkan_headers
URL "https://github.com/KhronosGroup/Vulkan-Headers/archive/015e25c3c91b70eb1a754d36fb14c4ba6ad9b0b9.tar.gz"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
FetchContent_MakeAvailable(vulkan_headers)
include_directories("${vulkan_headers_SOURCE_DIR}/include")
endif()
set(MKW_OPENXR_TARGET "")
if(MKW_ENABLE_OPENXR)
if(NOT MKW_PLATFORM_WINDOWS AND NOT MKW_PLATFORM_LINUX AND NOT MKW_PLATFORM_ANDROID)
@@ -340,6 +349,7 @@ endif()
# a registration file is silently never compiled and never errors. The stale-glob
# failure mode is worth far more than the milliseconds.
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
set_source_files_properties(src/vr/openxr_vulkan_win32.cpp PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON)
if(MKW_PLATFORM_MACOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
# HostContext's Apple Silicon backend is implemented in a small assembly
@@ -453,6 +463,16 @@ if(MKW_ENABLE_OPENXR AND MKW_PLATFORM_WINDOWS)
target_compile_features(mkw_openxr_replay_tests PRIVATE cxx_std_17)
set_target_properties(mkw_openxr_replay_tests PROPERTIES UNITY_BUILD OFF)
add_test(NAME mkw_openxr_replay_tests COMMAND mkw_openxr_replay_tests)
add_executable(mkw_openxr_vulkan_replay_tests
tests/openxr_d3d12_replay_tests.cpp src/vr/openxr_vulkan_win32.cpp src/vr/openxr_diagnostics.cpp)
target_include_directories(mkw_openxr_vulkan_replay_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include"
"$<TARGET_PROPERTY:${MKW_OPENXR_TARGET},INTERFACE_INCLUDE_DIRECTORIES>")
target_compile_definitions(mkw_openxr_vulkan_replay_tests PRIVATE MKW_ENABLE_OPENXR=1 TEST_WINDOWS_VULKAN=1)
target_compile_features(mkw_openxr_vulkan_replay_tests PRIVATE cxx_std_17)
set_target_properties(mkw_openxr_vulkan_replay_tests PROPERTIES UNITY_BUILD OFF)
add_test(NAME mkw_openxr_vulkan_replay_tests COMMAND mkw_openxr_vulkan_replay_tests)
endif()
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
+19
View File
@@ -106,6 +106,23 @@ public:
OpenXRRuntime(OpenXRRuntime&&) = delete;
OpenXRRuntime& operator=(OpenXRRuntime&&) = delete;
// Same-device Vulkan runtimes may touch Dawn's queue in frame/swapchain
// calls. Share Dawn's device guard; never hold it across xrWaitFrame.
struct GraphicsQueueGuard {
GraphicsQueueGuard(void* value, void (*release)(void*)) : token(value), unlock(release) {}
GraphicsQueueGuard(const GraphicsQueueGuard&) = delete;
GraphicsQueueGuard& operator=(const GraphicsQueueGuard&) = delete;
void* token;
void (*unlock)(void*);
~GraphicsQueueGuard() { if (token && unlock) unlock(token); }
};
void SetGraphicsQueueGuard(void* (*lock)(), void (*unlock)(void*)) {
m_queue_lock = lock; m_queue_unlock = unlock;
}
GraphicsQueueGuard LockGraphicsQueue() const {
return {m_queue_lock ? m_queue_lock() : nullptr, m_queue_unlock};
}
// Creates the instance, resolves the HMD system, and enumerates the stereo
// view configuration. Returns false without terminating the application;
// the caller should continue in non-VR mode.
@@ -214,6 +231,8 @@ public:
const OpenXRError& LastError() const { return m_last_error; }
private:
void* (*m_queue_lock)() = nullptr;
void (*m_queue_unlock)(void*) = nullptr;
enum class FramePhase {
Idle,
Waited,
+109
View File
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#include "vr/openxr_backend.h"
#include "vr/openxr_runtime.h"
#include <array>
#include <cstdint>
#include <memory>
#include <string>
namespace mkw::vr {
// Shared frame vocabulary for the Windows pacing and replay paths.
using OpenXRWindowsVulkanFrameMode = OpenXRFrameMode;
using OpenXRWindowsVulkanBeginStatus = OpenXRBeginStatus;
using OpenXRWindowsVulkanSubmissionStatus = OpenXRSubmissionStatus;
using OpenXRWindowsVulkanPresentation = OpenXRPresentation;
using OpenXRWindowsVulkanFrame = OpenXRBackendFrame;
struct OpenXRWindowsVulkanGraphicsRequirements {
XrVersion min_api_version = 0;
XrVersion max_api_version = 0;
};
// Same-device Dawn/OpenXR Vulkan backend.
//
// Startup is deliberately split in two. QueryGraphicsRequirements() runs
// after OpenXRRuntime::Initialize() but before aurora_initialize(), allowing
// OpenXR to create Dawn's Vulkan instance/device and select the physical GPU.
// BindAurora() runs afterwards and checks the device before creating a session.
//
// All methods from BeginFrame() through FinishFrame(), plus PollEvents on the
// associated OpenXRRuntime, belong to one XR pacing thread. Aurora's frame
// worker never calls OpenXR: its post-submit callback only publishes a token
// that WaitForSubmission() consumes. This is the synchronization boundary
// required by the asynchronous sealed-frame renderer.
class OpenXRWindowsVulkanBackend final {
public:
explicit OpenXRWindowsVulkanBackend(OpenXRLogCallback logger = {});
~OpenXRWindowsVulkanBackend();
OpenXRWindowsVulkanBackend(const OpenXRWindowsVulkanBackend&) = delete;
OpenXRWindowsVulkanBackend& operator=(const OpenXRWindowsVulkanBackend&) = delete;
OpenXRWindowsVulkanBackend(OpenXRWindowsVulkanBackend&&) = delete;
OpenXRWindowsVulkanBackend& operator=(OpenXRWindowsVulkanBackend&&) = delete;
bool QueryGraphicsRequirements(OpenXRRuntime& runtime);
bool BindAurora(OpenXRRuntime& runtime);
OpenXRWindowsVulkanBeginStatus BeginFrame(const OpenXRWindowsVulkanPresentation& presentation,
OpenXRWindowsVulkanFrame& frame);
// timeout_ms == UINT32_MAX waits until Aurora publishes this token or
// Shutdown() interrupts the wait. A timeout does not release XR images;
// the caller may keep pacing with RepeatFrame while Aurora still owns them.
OpenXRWindowsVulkanSubmissionStatus WaitForSubmission(const OpenXRWindowsVulkanFrame& frame,
uint32_t timeout_ms = UINT32_MAX);
// Withdraws this token only if Aurora has not encoded it. On success no GPU
// command can reference the acquired images, and FinishFrame(frame, false)
// is required to release them and close the compositor frame.
bool TryCancelPendingFrame(OpenXRWindowsVulkanFrame& frame);
// Ends the current compositor cycle with the last completed layer and starts
// another, without releasing or changing Aurora's pending images/render token.
// The original render poses remain attached to the pending and retained images.
bool RepeatFrame(const OpenXRWindowsVulkanFrame& frame);
// Releases acquired images and calls xrEndFrame. submit_layer must only be
// true after WaitForSubmission returned Success. Immersive frames submit
// XrCompositionLayerProjection; virtual-screen frames submit an
// XrCompositionLayerQuad using the single mono target, placed as the
// presentation's quad_anchored/quad_pose describe. If no new usable layer is
// available, resubmits the retained layer using the current display time.
bool FinishFrame(OpenXRWindowsVulkanFrame& frame, bool submit_layer);
// Render-first path when interpolation is off. Aurora renders into acquired
// non-retained XR images while no compositor frame is open. BeginFrameForPacket
// accepts only a completed packet; CopyRenderedEyes verifies the already queued
// bridge copy. FinishFrame releases the images and submits their original poses.
OpenXRBeginStatus PreparePacket(const OpenXRPresentation& presentation, OpenXRBackendFrame& packet);
bool TryCancelPendingPacket(OpenXRBackendFrame& packet);
OpenXRBeginStatus BeginFrameForPacket(const OpenXRBackendFrame& packet, OpenXRBackendFrame& frame);
OpenXRSubmissionStatus CopyRenderedEyes(const OpenXRBackendFrame& frame);
OpenXRBeginStatus KeepAliveCycle();
// Call on the XR owner thread after Aurora's worker is idle and before
// aurora_shutdown(). Safe to repeat. False means a submitted Vulkan command
// could not be fenced; the caller must retain this backend and its runtime
// for the process lifetime instead of destroying possibly live resources.
bool Shutdown();
bool IsBound() const;
const OpenXRWindowsVulkanGraphicsRequirements& GraphicsRequirements() const;
int64_t SwapchainFormat() const;
const std::string& LastError() const;
private:
class Impl;
std::unique_ptr<Impl> m_impl;
};
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
+64
View File
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "vr/openxr_d3d12.h"
#include "vr/openxr_vulkan_win32.h"
#include <variant>
namespace mkw::vr {
// Runtime API selection; both backends expose the same pacing/ownership contract.
class OpenXRWindowsBackend {
std::variant<std::unique_ptr<OpenXRD3D12Backend>, std::unique_ptr<OpenXRWindowsVulkanBackend>> backend_;
public:
OpenXRWindowsBackend(OpenXRLogCallback logger, bool vulkan) {
if (vulkan) backend_ = std::make_unique<OpenXRWindowsVulkanBackend>(logger);
else backend_ = std::make_unique<OpenXRD3D12Backend>(logger);
}
const OpenXRD3D12GraphicsRequirements& GraphicsRequirements() {
return std::get<0>(backend_)->GraphicsRequirements();
}
bool QueryGraphicsRequirements(OpenXRRuntime& runtime) {
return std::visit([&](auto& backend) -> bool { return backend->QueryGraphicsRequirements(runtime); }, backend_);
}
bool BindAurora(OpenXRRuntime& runtime) {
return std::visit([&](auto& backend) -> bool { return backend->BindAurora(runtime); }, backend_);
}
OpenXRBeginStatus BeginFrame(const OpenXRPresentation& presentation, OpenXRBackendFrame& frame) {
return std::visit([&](auto& backend) -> OpenXRBeginStatus { return backend->BeginFrame(presentation, frame); }, backend_);
}
OpenXRSubmissionStatus WaitForSubmission(const OpenXRBackendFrame& frame, uint32_t timeout) {
return std::visit([&](auto& backend) -> OpenXRSubmissionStatus { return backend->WaitForSubmission(frame, timeout); }, backend_);
}
bool TryCancelPendingFrame(OpenXRBackendFrame& frame) {
return std::visit([&](auto& backend) -> bool { return backend->TryCancelPendingFrame(frame); }, backend_);
}
bool RepeatFrame(const OpenXRBackendFrame& frame) {
return std::visit([&](auto& backend) -> bool { return backend->RepeatFrame(frame); }, backend_);
}
bool FinishFrame(OpenXRBackendFrame& frame, bool submit) {
return std::visit([&](auto& backend) -> bool { return backend->FinishFrame(frame, submit); }, backend_);
}
OpenXRBeginStatus PreparePacket(const OpenXRPresentation& presentation, OpenXRBackendFrame& frame) {
return std::visit([&](auto& backend) -> OpenXRBeginStatus { return backend->PreparePacket(presentation, frame); }, backend_);
}
bool TryCancelPendingPacket(OpenXRBackendFrame& frame) {
return std::visit([&](auto& backend) -> bool { return backend->TryCancelPendingPacket(frame); }, backend_);
}
OpenXRBeginStatus BeginFrameForPacket(const OpenXRBackendFrame& packet, OpenXRBackendFrame& frame) {
return std::visit([&](auto& backend) -> OpenXRBeginStatus { return backend->BeginFrameForPacket(packet, frame); }, backend_);
}
OpenXRSubmissionStatus CopyRenderedEyes(const OpenXRBackendFrame& frame) {
return std::visit([&](auto& backend) -> OpenXRSubmissionStatus { return backend->CopyRenderedEyes(frame); }, backend_);
}
OpenXRBeginStatus KeepAliveCycle() {
return std::visit([&](auto& backend) -> OpenXRBeginStatus { return backend->KeepAliveCycle(); }, backend_);
}
bool Shutdown() {
return std::visit([&](auto& backend) -> bool { return backend->Shutdown(); }, backend_);
}
int64_t SwapchainFormat() {
return std::visit([&](auto& backend) -> int64_t { return backend->SwapchainFormat(); }, backend_);
}
const std::string& LastError() {
return std::visit([&](auto& backend) -> const std::string& { return backend->LastError(); }, backend_);
}
};
}
+15 -5
View File
@@ -34,7 +34,7 @@
#include "vr/openxr_input.h"
#include "vr/openxr_runtime.h"
#if defined(_WIN32)
#include "vr/openxr_d3d12.h"
#include "vr/openxr_windows.h"
#define MKW_OPENXR_GRAPHICS_BACKEND 1
#elif defined(__ANDROID__)
#include "vr/openxr_android.h"
@@ -78,8 +78,8 @@ void ConfigurePolicy(bool enabled) noexcept {
#if MKW_OPENXR_GRAPHICS_BACKEND
#if defined(_WIN32)
using GraphicsBackend = OpenXRD3D12Backend;
inline constexpr const char* kGraphicsBackendName = "D3D12";
using GraphicsBackend = OpenXRWindowsBackend;
#else
using GraphicsBackend = OpenXRVulkanBackend;
inline constexpr const char* kGraphicsBackendName = "Vulkan";
@@ -360,7 +360,11 @@ public:
}
#endif
runtime_ = std::make_unique<OpenXRRuntime>(logger_);
#if defined(_WIN32)
backend_ = std::make_unique<GraphicsBackend>(logger_, kRequiredAuroraBackend == BACKEND_VULKAN);
#else
backend_ = std::make_unique<GraphicsBackend>(logger_);
#endif
OpenXRConfig config{};
config.application_name = aurora_config.appName != nullptr ? aurora_config.appName
@@ -368,7 +372,7 @@ public:
config.engine_name = "Aurora";
config.resolution_scale = RuntimeConfigFile::VrRenderScale();
#if defined(_WIN32)
config.required_extensions = {"XR_KHR_D3D12_enable"};
config.required_extensions = {kRequiredAuroraBackend == BACKEND_VULKAN ? "XR_KHR_vulkan_enable2" : "XR_KHR_D3D12_enable"};
config.optional_extensions = {"XR_KHR_win32_convert_performance_counter_time",
"XR_FB_display_refresh_rate", "XR_EXT_performance_settings"};
#else
@@ -567,7 +571,8 @@ private:
};
#if defined(_WIN32)
static constexpr AuroraBackend kRequiredAuroraBackend = BACKEND_D3D12;
AuroraBackend kRequiredAuroraBackend = BACKEND_D3D12;
const char* kGraphicsBackendName = "D3D12";
#else
static constexpr AuroraBackend kRequiredAuroraBackend = BACKEND_VULKAN;
#endif
@@ -576,6 +581,10 @@ private:
static constexpr uint32_t kMaxConsecutiveSkips = 300;
bool BackendMatchesConfiguredGraphicsApi(const AuroraConfig& aurora_config) {
#if defined(_WIN32)
kRequiredAuroraBackend = aurora_config.desiredBackend == BACKEND_VULKAN ? BACKEND_VULKAN : BACKEND_D3D12;
kGraphicsBackendName = kRequiredAuroraBackend == BACKEND_VULKAN ? "Vulkan" : "D3D12";
#endif
if (aurora_config.desiredBackend == BACKEND_AUTO ||
aurora_config.desiredBackend == kRequiredAuroraBackend) {
return true;
@@ -589,6 +598,7 @@ private:
aurora_config.desiredBackend = kRequiredAuroraBackend;
aurora_config.xrInterop = true;
#if defined(_WIN32)
if (kRequiredAuroraBackend != BACKEND_D3D12) return;
const auto& requirements = backend_->GraphicsRequirements();
aurora_config.hasD3D12AdapterLuid = true;
aurora_config.d3d12AdapterLuidLow = requirements.adapter_luid_low;
+5 -1
View File
@@ -724,6 +724,7 @@ bool OpenXRRuntime::BeginFrame(const OpenXRFrame& frame) {
XrFrameBeginInfo begin_info{XR_TYPE_FRAME_BEGIN_INFO};
const XrResult result = diagnostics::Measure(diagnostics::Stage::BeginCall, [&] {
const auto queue_guard = LockGraphicsQueue();
return xrBeginFrame(m_session, &begin_info);
});
if (XR_FAILED(result)) {
@@ -816,7 +817,10 @@ bool OpenXRRuntime::EndFrame(
end_info.layerCount = layer_count;
end_info.layers = layers;
const diagnostics::Stopwatch end_timer;
const XrResult result = xrEndFrame(m_session, &end_info);
const XrResult result = [&] {
const auto queue_guard = LockGraphicsQueue();
return xrEndFrame(m_session, &end_info);
}();
diagnostics::OnEndFrame(end_timer);
m_frame_phase = FramePhase::Idle;
m_active_frame_serial = 0;
File diff suppressed because it is too large. Load diff
+80 -1
View File
@@ -1,6 +1,23 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Exercise the real backend against a deterministic compositor and Aurora sink.
// No headset, graphics driver, OpenXR loader, or translated game is required.
#if defined(TEST_WINDOWS_VULKAN)
#define XR_USE_GRAPHICS_API_VULKAN
#include <vulkan/vulkan.h>
#include <openxr/openxr_platform.h>
#include <aurora/vulkan_win32_interop.h>
#include "vr/openxr_vulkan_win32.h"
#define OpenXRD3D12Backend OpenXRWindowsVulkanBackend
#define OpenXRD3D12BeginStatus OpenXRWindowsVulkanBeginStatus
#define OpenXRD3D12SubmissionStatus OpenXRWindowsVulkanSubmissionStatus
#define OpenXRD3D12Frame OpenXRWindowsVulkanFrame
#define OpenXRD3D12Presentation OpenXRWindowsVulkanPresentation
#define OpenXRD3D12FrameMode OpenXRWindowsVulkanFrameMode
#define aurora_d3d12_enable_stereo_bridge aurora_vulkan_win32_enable
#define aurora_d3d12_set_stereo_targets aurora_vulkan_win32_set_targets
#define aurora_d3d12_cancel_stereo_targets aurora_vulkan_win32_cancel
#define aurora_d3d12_disable_stereo_bridge aurora_vulkan_win32_disable
#else
#define CINTERFACE
#define XR_USE_GRAPHICS_API_D3D12
#ifndef NOMINMAX
@@ -10,8 +27,10 @@
#include <openxr/openxr_platform.h>
#include <aurora/d3d12_interop.h>
#include "vr/openxr_d3d12.h"
#endif
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
@@ -65,6 +84,26 @@ void Complete(bool success = true) {
encoded = false;
}
#if defined(TEST_WINDOWS_VULKAN)
AuroraDawnVulkanHooks hooks{};
bool queue_locked = false;
XrResult XRAPI_CALL Requirements(XrInstance, XrSystemId, XrGraphicsRequirementsVulkanKHR* out) {
out->minApiVersionSupported = XR_MAKE_VERSION(1, 1, 0);
out->maxApiVersionSupported = XR_MAKE_VERSION(1, 3, 0);
return XR_SUCCESS;
}
XrResult XRAPI_CALL Physical(XrInstance, const XrVulkanGraphicsDeviceGetInfoKHR*, VkPhysicalDevice* out) {
*out = reinterpret_cast<VkPhysicalDevice>(2); return XR_SUCCESS;
}
XrResult XRAPI_CALL CreateInstance(XrInstance, const XrVulkanInstanceCreateInfoKHR* info, VkInstance* out, VkResult* result) {
Require(info->vulkanCreateInfo->pApplicationInfo->apiVersion >= VK_API_VERSION_1_1);
*out = reinterpret_cast<VkInstance>(1); *result = VK_SUCCESS; return XR_SUCCESS;
}
XrResult XRAPI_CALL CreateDevice(XrInstance, const XrVulkanDeviceCreateInfoKHR* info, VkDevice* out, VkResult* result) {
Require(info->vulkanPhysicalDevice == reinterpret_cast<VkPhysicalDevice>(2));
*out = reinterpret_cast<VkDevice>(3); *result = VK_SUCCESS; return XR_SUCCESS;
}
#else
HRESULT STDMETHODCALLTYPE FeatureSupport(ID3D12Device*, D3D12_FEATURE,
void* data, UINT) {
static_cast<D3D12_FEATURE_DATA_FEATURE_LEVELS*>(data)->MaxSupportedFeatureLevel =
@@ -77,10 +116,28 @@ XrResult XRAPI_CALL Requirements(XrInstance, XrSystemId,
requirements->minFeatureLevel = D3D_FEATURE_LEVEL_11_0;
return XR_SUCCESS;
}
#endif
}
// A COM vtable in its C representation supplies the single device operation
// used by BindAurora. The production backend is compiled normally as C++.
#if defined(TEST_WINDOWS_VULKAN)
bool aurora_vulkan_win32_configure(const AuroraDawnVulkanHooks* value) {
hooks = value ? *value : AuroraDawnVulkanHooks{}; return true;
}
bool aurora_vulkan_win32_get_handles(AuroraDawnVulkanHandles* handles, int64_t* format) {
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo instance{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; instance.pApplicationInfo = &app;
Require(hooks.createInstance(hooks.userdata, nullptr, &instance, nullptr, &handles->instance) == VK_SUCCESS);
Require(hooks.getPhysicalDevice(hooks.userdata, handles->instance, &handles->physicalDevice) == VK_SUCCESS);
VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
Require(hooks.createDevice(hooks.userdata, nullptr, handles->physicalDevice, &device, nullptr, &handles->device) == VK_SUCCESS);
*format = VK_FORMAT_R8G8B8A8_UNORM; return true;
}
void* aurora_vulkan_win32_lock_queue() { Require(!queue_locked); queue_locked = true; return &queue_locked; }
void aurora_vulkan_win32_unlock_queue(void*) { Require(queue_locked); queue_locked = false; }
#else
bool aurora_d3d12_get_native_handles(AuroraD3D12NativeHandles* handles) {
static ID3D12DeviceVtbl vtable{};
vtable.CheckFeatureSupport = FeatureSupport;
@@ -88,6 +145,7 @@ bool aurora_d3d12_get_native_handles(AuroraD3D12NativeHandles* handles) {
*handles = {&device, &device, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 0};
return true;
}
#endif
bool aurora_d3d12_enable_stereo_bridge(AuroraD3D12StereoSubmittedCallback cb, void* data) {
callback = cb;
callback_data = data;
@@ -119,13 +177,21 @@ XrResult XRAPI_CALL xrCreateSwapchain(XrSession, const XrSwapchainCreateInfo*, X
XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain handle, uint32_t capacity,
uint32_t* count, XrSwapchainImageBaseHeader* images) {
*count = 1; // Single-image swapchains also require a separate retained pair.
#if defined(TEST_WINDOWS_VULKAN)
if (capacity) reinterpret_cast<XrSwapchainImageVulkanKHR*>(images)->image =
reinterpret_cast<VkImage>(&reinterpret_cast<Swapchain*>(handle)->image);
#else
if (capacity) reinterpret_cast<XrSwapchainImageD3D12KHR*>(images)->texture =
reinterpret_cast<ID3D12Resource*>(&reinterpret_cast<Swapchain*>(handle)->image);
#endif
return XR_SUCCESS;
}
XrResult XRAPI_CALL xrAcquireSwapchainImage(XrSwapchain handle,
const XrSwapchainImageAcquireInfo*, uint32_t* index) {
auto& chain = *reinterpret_cast<Swapchain*>(handle);
#if defined(TEST_WINDOWS_VULKAN)
Require(queue_locked);
#endif
Require(!chain.acquired);
chain.acquired = true;
chain.waited = false;
@@ -141,6 +207,9 @@ XrResult XRAPI_CALL xrWaitSwapchainImage(XrSwapchain handle, const XrSwapchainIm
XrResult XRAPI_CALL xrReleaseSwapchainImage(XrSwapchain handle,
const XrSwapchainImageReleaseInfo*) {
auto& chain = *reinterpret_cast<Swapchain*>(handle);
#if defined(TEST_WINDOWS_VULKAN)
Require(queue_locked); // The runtime may touch Dawn's VkQueue here.
#endif
Require(chain.acquired && chain.waited);
chain.acquired = false;
chain.released = true;
@@ -158,14 +227,24 @@ XrResult XRAPI_CALL xrDestroySwapchain(XrSwapchain handle) {
namespace mkw::vr {
OpenXRRuntime::OpenXRRuntime(OpenXRLogCallback) {
m_instance = reinterpret_cast<XrInstance>(this);
#if defined(TEST_WINDOWS_VULKAN)
m_swapchain_formats = {VK_FORMAT_R8G8B8A8_SRGB};
#else
m_swapchain_formats = {DXGI_FORMAT_R8G8B8A8_UNORM_SRGB};
#endif
for (auto& view : m_view_configuration) {
view.render_width = 100;
view.render_height = 80;
}
}
OpenXRRuntime::~OpenXRRuntime() = default;
bool OpenXRRuntime::GetInstanceProcAddress(const char*, PFN_xrVoidFunction* out) {
bool OpenXRRuntime::GetInstanceProcAddress(const char* name, PFN_xrVoidFunction* out) {
#if defined(TEST_WINDOWS_VULKAN)
if (std::strcmp(name, "xrCreateVulkanInstanceKHR") == 0) *out = reinterpret_cast<PFN_xrVoidFunction>(::CreateInstance);
else if (std::strcmp(name, "xrCreateVulkanDeviceKHR") == 0) *out = reinterpret_cast<PFN_xrVoidFunction>(CreateDevice);
else if (std::strcmp(name, "xrGetVulkanGraphicsDevice2KHR") == 0) *out = reinterpret_cast<PFN_xrVoidFunction>(Physical);
else
#endif
*out = reinterpret_cast<PFN_xrVoidFunction>(Requirements);
return true;
}