Files
simplysanatori--KartFrame/aurora-main/lib/webgpu/vulkan_win32_interop.cpp
T
simplysanatoriandClaude Opus 5.5 dac9971ecc Eye pose checks: frustum word, GPU uniform probe, cheap stamp; mock bookkeeping before the wait
The design already snapshots each set's packet once (ProvideStereoFrame copies it under
published_mutex_; every eye path uses that copy). This makes the remaining ways one eye
could differ measurable on the headset too:
- the stamp's fourth pose word: the frustum tangents each eye's fill used; xrmock compares
  them with the layer's submitted fov (and the other eye's);
- a GPU uniform probe (KARTFRAME_FRAME_LOG=1 or KARTFRAME_UNIFORM_PROBE=1): after both eyes,
  each eye's first perspective range is read back from g_uniformBuffer (now CopySrc) and
  compared with the bytes its fill wrote, counted per 10 s;
- the XRP fields ride on the XRS line (one log call per set).
The test stamp is 64x15 pixels, one pixel a bit (was 4x4 cells), and xrmock does its
read-back bookkeeping before xrWaitFrame's sleep instead of after the wake-up it measures
from: with the larger stamp that work alone moved 'ends later than 1 ms' by 2 %.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:35:43 -04:00

504 lines
26 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#include <aurora/vulkan_win32_interop.h>
#include "../internal.hpp"
#include "../stereo.hpp"
#include "../stereo_overlay.hpp"
#include "gpu.hpp"
#if (defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__))) && defined(WEBGPU_DAWN) && defined(DAWN_ENABLE_BACKEND_VULKAN)
#if defined(_WIN32)
#include <windows.h>
#else
// The patched Dawn is linked statically on Linux; its Aurora Vulkan ABI is resolved at link time.
extern "C" {
uint32_t AuroraDawnVulkanVersion();
int AuroraDawnVulkanConfigure(const AuroraDawnVulkanHooks* hooks);
int AuroraDawnVulkanGetHandles(void* handle, AuroraDawnVulkanHandles* out);
void* AuroraDawnVulkanWrap(void* handle, const void* desc, uint64_t image);
int AuroraDawnVulkanRelease(void* handle, void* const* textures, uint32_t count);
void* AuroraDawnVulkanLock(void* handle);
void AuroraDawnVulkanUnlock(void* guard);
int AuroraDawnVulkanDrain(void* handle);
}
#endif
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <string_view>
#include <vector>
namespace aurora::vulkan_win32 {
namespace {
Module Log("aurora::vulkan_win32");
// KARTFRAME_FRAME_STAMP=1 (tests only): each eye image gets a 64x15 pixel block in its top left
// corner naming the display time its poses were located for and the eye, written after the eye
// copy. The mock runtime (MOCK_XR_STAMP) reads it back from the image a projection layer submits
// and compares it with the locate the layer's poses came from. Layout, one pixel a cell, white = 1:
// row 0 the 64 bits of the display time (steady clock ns, bit 0 first), row 1 their complement,
// row 2: cell 0 the eye (1 = right), cell 1 its complement, cells 2-5 the marker 1 0 1 0.
// Lane L, the scene state this eye's draws used (stereo::EyeScene), rows 3 and 5 two 64-bit words, rows 4
// and 6 their complements:
// row 3: bits 0-15 weight x 65535, 16-31 previous game frame (low 16 bits), 32-47 current game frame,
// 48 valid, 49 late (filled at the set's sample time), 50 anchored, 51 continuous, 56-63 set (low 8);
// row 5: perspective draws filled from the pair (bits 0-15), filled current-only (16-31), drawn with the
// recorded uniform (32-47), drawn with a range outside this set's fill (48-63).
// Lane P, the head pose this eye's uniform fill wrote (stereo::EyeScene::pose*), rows 7, 9 and 11 three
// 64-bit words, rows 8, 10 and 12 their complements:
// row 7: the display time (steady ns) of the packet whose eye transform the fill wrote (0: none);
// row 9: the eye's orientation recovered from that transform, x, y, z, w as signed 16-bit (x 32767);
// row 11: bits 0-15 draws of this eye filled with another packet or transform, 16-47 the packet's token
// (low 32 bits), 48 a pose was recorded;
// row 13: the tangents of that fill's eye frustum, left, right, down, up as signed 16-bit (x 8192); row 14
// its complement.
// Lane P: one pixel a cell (the block is 64x15 pixels; it was 4x4-pixel cells, 256x28, before): the mock
// reads the image raw, and the smaller block keeps the test stamp's cost out of the frame's timing.
constexpr uint32_t kStampCell = 1;
constexpr uint32_t kStampRows = 15;
std::array<uint64_t, 2> SceneWords(const stereo::EyeScene& scene) {
if (!scene.valid) return {0, 0};
const uint64_t weight = static_cast<uint64_t>(std::lround(std::clamp(scene.weight, 0.f, 1.f) * 65535.f));
const uint64_t a = weight | (static_cast<uint64_t>(scene.previousFrame & 0xffffu) << 16) |
(static_cast<uint64_t>(scene.currentFrame & 0xffffu) << 32) | (1ull << 48) |
(scene.late ? 1ull << 49 : 0) | (scene.anchored ? 1ull << 50 : 0) |
(scene.continuous ? 1ull << 51 : 0) | (static_cast<uint64_t>(scene.set & 0xffu) << 56);
const uint64_t b = static_cast<uint64_t>(scene.interpolated) | (static_cast<uint64_t>(scene.current) << 16) |
(static_cast<uint64_t>(scene.recorded) << 32) | (static_cast<uint64_t>(scene.outside) << 48);
return {a, b};
}
std::array<uint64_t, 4> PoseWords(const stereo::EyeScene& scene) {
if (!scene.valid || scene.poseTime == 0) return {0, 0, 0, 0};
uint64_t q = 0;
for (uint32_t k = 0; k < 4; ++k) {
const auto v = static_cast<int16_t>(std::lround(std::clamp(scene.poseOrientation[k], -1.f, 1.f) * 32767.f));
q |= static_cast<uint64_t>(static_cast<uint16_t>(v)) << (16 * k);
}
const uint64_t e = static_cast<uint64_t>(scene.poseMixed) | ((scene.poseToken & 0xffffffffull) << 16) | (1ull << 48);
uint64_t t = 0;
for (uint32_t k = 0; k < 4; ++k) {
const auto v = static_cast<int16_t>(std::lround(std::clamp(scene.poseTangents[k], -3.99f, 3.99f) * 8192.f));
t |= static_cast<uint64_t>(static_cast<uint16_t>(v)) << (16 * k);
}
return {scene.poseTime, q, e, t};
}
bool StampEnabled() {
static const bool enabled = [] {
const char* env = std::getenv("KARTFRAME_FRAME_STAMP");
return env != nullptr && env[0] == '1';
}();
return enabled;
}
// The whole block is built on the CPU and written into a small texture per eye (queue.WriteTexture runs
// before this set's submission and after the last one's, so reusing it per eye is safe), then copied into
// the eye image with one copy. One pixel a bit (lane P; 4x4 cells before): 64x15 pixels, 3.8 KB an eye.
void WriteStamp(wgpu::CommandEncoder& encoder, const wgpu::Texture& target, wgpu::TextureFormat format, uint64_t value,
uint32_t eye, const stereo::EyeScene& scene) {
constexpr uint32_t kWidth = 64 * kStampCell, kHeight = kStampRows * kStampCell;
static std::array<wgpu::Texture, 2> blocks;
static std::array<wgpu::TextureFormat, 2> blockFormats{};
const uint32_t slot = eye == 1 ? 1 : 0;
const wgpu::Extent3D size{kWidth, kHeight, 1};
if (!blocks[slot] || blockFormats[slot] != format) {
const wgpu::TextureDescriptor descriptor{.label = "Frame stamp block",
.usage = wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst,
.dimension = wgpu::TextureDimension::e2D,
.size = size,
.format = format,
.mipLevelCount = 1,
.sampleCount = 1};
blocks[slot] = webgpu::g_device.CreateTexture(&descriptor);
blockFormats[slot] = format;
}
std::array<uint8_t, size_t{kWidth} * kHeight * 4> pixels;
const auto cell = [&](uint32_t column, uint32_t row, bool one) {
for (uint32_t y = 0; y < kStampCell; ++y)
for (uint32_t x = 0; x < kStampCell; ++x) {
auto* p = &pixels[((row * kStampCell + y) * kWidth + column * kStampCell + x) * 4];
p[0] = p[1] = p[2] = one ? 255 : 0;
p[3] = 255;
}
};
const auto word = [&](uint32_t row, uint64_t bits) {
for (uint32_t bit = 0; bit < 64; ++bit) {
const bool one = ((bits >> bit) & 1u) != 0;
cell(bit, row, one);
cell(bit, row + 1, !one);
}
};
word(0, value);
for (uint32_t column = 0; column < 64; ++column)
cell(column, 2, column == 0 ? eye == 1 : column == 1 ? eye != 1 : column < 6 ? ((column - 2) & 1u) == 0 : false);
const auto words = SceneWords(scene);
word(3, words[0]);
word(5, words[1]);
const auto pose = PoseWords(scene);
for (uint32_t k = 0; k < pose.size(); ++k) word(7 + 2 * k, pose[k]);
const wgpu::TexelCopyTextureInfo block{.texture = blocks[slot]};
const wgpu::TexelCopyBufferLayout layout{.bytesPerRow = kWidth * 4, .rowsPerImage = kHeight};
webgpu::g_queue.WriteTexture(&block, pixels.data(), pixels.size(), &layout, &size);
const wgpu::TexelCopyTextureInfo destination{.texture = target};
encoder.CopyTextureToTexture(&block, &destination, &size);
}
// Set on the frame worker around the submitted callback (see aurora_vulkan_win32_submitting_retained).
thread_local bool submittingRetained = false;
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() {
#if defined(_WIN32)
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;
#else
if (AuroraDawnVulkanVersion() != AURORA_DAWN_VULKAN_ABI) return false;
configure = &AuroraDawnVulkanConfigure;
handles = &AuroraDawnVulkanGetHandles;
wrap = &AuroraDawnVulkanWrap;
release = &AuroraDawnVulkanRelease;
lock = &AuroraDawnVulkanLock;
unlock = &AuroraDawnVulkanUnlock;
drain = &AuroraDawnVulkanDrain;
return true;
#endif
}
} 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; };
// The eyes, then the settings panel's layer image after them.
constexpr uint32_t kMaxImages = 3;
class Bridge {
public:
std::mutex mutex;
std::vector<Import> imports;
std::array<AuroraD3D12StereoTarget, kMaxImages> targets{};
std::array<wgpu::Texture, kMaxImages> active{};
uint64_t token = 0;
uint32_t count = 0;
// Images this frame copies: `count` eyes, plus the panel when `panel` is set.
uint32_t images = 0;
bool panel = false;
bool encoded = false;
// The eyes render straight into this set's images (DirectTargets): it can no longer be canceled.
bool claimed = 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, const AuroraD3D12StereoTarget* panelTarget) {
if (!next || !data || !n || n > 2) return false;
std::lock_guard guard(mutex);
if (token) return false;
const auto valid = [](const AuroraD3D12StereoTarget& target) {
return target.resource && target.width && target.height;
};
for (uint32_t i = 0; i < n; ++i) {
if (!valid(data[i])) return false;
}
if (panelTarget && !valid(*panelTarget)) return false;
for (uint32_t i = 0; i < n; ++i) targets[i] = data[i];
panel = panelTarget != nullptr;
if (panel) targets[n] = *panelTarget;
token = next; count = n; images = n + (panel ? 1u : 0u); return true;
}
bool Cancel(uint64_t wanted) {
std::unique_lock guard(mutex, std::try_to_lock);
if (!guard.owns_lock() || encoded || claimed || !token || token != wanted) return false;
token = 0; return true;
}
// How the eyes reach the swapchain images (lane I). Default srgb: a full-screen draw that decodes the
// eye's sRGB-encoded bytes and writes them through an sRGB view of the sRGB image (the same bytes; the
// stamps read back exactly). The image then needs no MUTABLE_FORMAT, so the driver may compress it
// (Turnip: UBWC), and the eyes' way into it cost 0.47 ms a set instead of the copy's 2.3 ms (mock, GP
// race, compositor running; RACE120.md). KARTFRAME_SINK=copy is the old texture copy into a UNORM view
// of a MUTABLE_FORMAT image, =blit the same through a full-screen draw (the backend asks for
// MUTABLE_FORMAT for both). Not for a UNORM swapchain (no sRGB format offered): that takes the copy.
enum class Sink { Copy, Blit, Srgb };
static Sink SinkMode() {
static const Sink mode = [] {
const char* env = std::getenv("KARTFRAME_SINK");
const std::string_view v = env != nullptr ? env : "";
return v == "blit" ? Sink::Blit : v == "copy" ? Sink::Copy : Sink::Srgb;
}();
return mode;
}
static bool SinkBlit() { return SinkMode() != Sink::Copy; }
// The sRGB format of an sRGB swapchain image (VkFormat 43 or 50), for the draw that writes it.
static wgpu::TextureFormat TargetSrgb(int64_t vk) {
return vk == 43 ? wgpu::TextureFormat::RGBA8UnormSrgb : vk == 50 ? wgpu::TextureFormat::BGRA8UnormSrgb
: wgpu::TextureFormat::Undefined;
}
static wgpu::TextureFormat SrgbOf(wgpu::TextureFormat f) {
return f == wgpu::TextureFormat::BGRA8Unorm ? wgpu::TextureFormat::BGRA8UnormSrgb
: f == wgpu::TextureFormat::RGBA8Unorm ? wgpu::TextureFormat::RGBA8UnormSrgb
: f;
}
wgpu::RenderPipeline srgbPipeline;
wgpu::TextureFormat srgbPipelineFormat = wgpu::TextureFormat::Undefined;
const wgpu::RenderPipeline& SrgbPipeline(wgpu::TextureFormat format) {
if (srgbPipeline && srgbPipelineFormat == format) return srgbPipeline;
wgpu::ShaderSourceWGSL code{};
code.code = R"(
@group(0) @binding(0) var s: sampler;
@group(0) @binding(1) var t: texture_2d<f32>;
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
let p = array(vec2f(-1.0, 1.0), vec2f(-1.0, -3.0), vec2f(3.0, 1.0));
return vec4f(p[i], 0.0, 1.0);
}
fn decode(c: f32) -> f32 {
return select(pow((c + 0.055) / 1.055, 2.4), c / 12.92, c <= 0.04045);
}
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
let c = textureLoad(t, vec2i(p.xy), 0);
return vec4f(decode(c.r), decode(c.g), decode(c.b), 1.0);
}
)";
wgpu::ShaderModuleDescriptor md{};
md.nextInChain = &code;
const auto module = webgpu::g_device.CreateShaderModule(&md);
const auto layout = webgpu::g_CopyPipeline.GetBindGroupLayout(0);
const wgpu::PipelineLayoutDescriptor ld{.bindGroupLayoutCount = 1, .bindGroupLayouts = &layout};
const wgpu::ColorTargetState target{.format = format};
const wgpu::FragmentState fs{.module = module, .entryPoint = "fs", .targetCount = 1, .targets = &target};
wgpu::RenderPipelineDescriptor pd{};
pd.layout = webgpu::g_device.CreatePipelineLayout(&ld);
pd.vertex = {.module = module, .entryPoint = "vs"};
pd.fragment = &fs;
srgbPipeline = webgpu::g_device.CreateRenderPipeline(&pd);
srgbPipelineFormat = format;
return srgbPipeline;
}
std::vector<std::pair<WGPUTextureView, wgpu::BindGroup>> blitSources;
std::vector<std::pair<WGPUTexture, wgpu::TextureView>> blitTargets;
wgpu::Sampler blitSampler;
void Blit(wgpu::CommandEncoder& encoder, const wgpu::TextureView& source, const wgpu::Texture& target) {
if (!blitSampler) {
const wgpu::SamplerDescriptor sd{.magFilter = wgpu::FilterMode::Nearest, .minFilter = wgpu::FilterMode::Nearest};
blitSampler = webgpu::g_device.CreateSampler(&sd);
}
auto src = std::find_if(blitSources.begin(), blitSources.end(), [&](const auto& e) { return e.first == source.Get(); });
if (src == blitSources.end()) {
if (blitSources.size() >= 8) blitSources.erase(blitSources.begin());
blitSources.emplace_back(source.Get(), webgpu::create_copy_bind_group(source, blitSampler));
src = blitSources.end() - 1;
}
auto dst = std::find_if(blitTargets.begin(), blitTargets.end(), [&](const auto& e) { return e.first == target.Get(); });
if (dst == blitTargets.end()) {
if (blitTargets.size() >= 16) blitTargets.erase(blitTargets.begin());
blitTargets.emplace_back(target.Get(), target.CreateView());
dst = blitTargets.end() - 1;
}
const wgpu::RenderPassColorAttachment ca{.view = dst->second, .loadOp = wgpu::LoadOp::Clear,
.storeOp = wgpu::StoreOp::Store};
const wgpu::RenderPassDescriptor rd{.label = "VR eye to swapchain", .colorAttachmentCount = 1, .colorAttachments = &ca};
auto pass = encoder.BeginRenderPass(&rd);
pass.SetPipeline(SinkMode() == Sink::Srgb ? SrgbPipeline(target.GetFormat()) : webgpu::g_CopyPipeline);
pass.SetBindGroup(0, src->second, 0, nullptr);
pass.Draw(3);
pass.End();
}
// The wrapped texture of target i as `format`, imported on first use (mutex held).
wgpu::Texture* ImportTarget(uint32_t i, wgpu::TextureFormat format) {
const auto& target = targets[i];
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() && (it->width != target.width || it->height != target.height || it->format != format)) {
// The runtime reuses VkImage handles across swapchain recreation, and Aurora's eye format can
// change with the surface: re-wrap.
imports.erase(it);
it = imports.end();
}
if (it == imports.end()) {
wgpu::TextureDescriptor descriptor;
descriptor.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::RenderAttachment;
descriptor.size = {target.width, target.height, 1};
descriptor.format = format;
void* wrapped = api.wrap(webgpu::g_device.Get(), &descriptor, image);
if (!wrapped) return nullptr;
imports.push_back({image, target.width, target.height, format,
wgpu::Texture::Acquire(static_cast<WGPUTexture>(wrapped))});
it = imports.end() - 1;
}
return &it->texture;
}
bool DirectTargets(uint64_t wanted, wgpu::TextureFormat format, uint32_t width, uint32_t height,
std::array<wgpu::Texture, AURORA_STEREO_EYE_COUNT>& out) {
std::lock_guard guard(mutex);
if (!token || token != wanted || encoded || count != AURORA_STEREO_EYE_COUNT || SinkMode() == Sink::Srgb) return false;
for (uint32_t i = 0; i < count; ++i) {
if (targets[i].width != width || targets[i].height != height ||
!CopyCompatible(VkFormat(format), targets[i].dxgiFormat)) return false;
}
for (uint32_t i = 0; i < count; ++i) {
auto* texture = ImportTarget(i, format);
if (!texture) return false;
out[i] = *texture;
}
claimed = true;
return true;
}
bool Encode(wgpu::CommandEncoder& encoder, const stereo::SinkFrame& frame) {
std::lock_guard guard(mutex);
if (!token || encoded || frame.frameToken != token) return false;
std::array<stereo::EyeImage, kMaxImages> sources{};
for (uint32_t i = 0; i < count; ++i) sources[i] = frame.eyes[i];
if (panel && !stereo_overlay::layer_source(encoder, targets[count].width, targets[count].height, sources[count]))
return false;
// Validate/import every target before recording any copy.
for (uint32_t i = 0; i < images; ++i) {
const auto& eye = sources[i];
const auto& target = targets[i];
if (!eye.texture || eye.size.width != target.width || eye.size.height != target.height) return false;
// Lane O: an eye swapchain in the other channel order (R8G8B8A8_SRGB against Aurora's BGRA, as
// GalaxyFrame's) can only be written by the sRGB draw, which writes any order; that includes the
// menus' screen image (the left image of a pair), which the copy wrote before.
const bool swizzle = !CopyCompatible(VkFormat(eye.format), target.dxgiFormat);
const bool srgbTarget = TargetSrgb(target.dxgiFormat) != wgpu::TextureFormat::Undefined;
const bool srgbEye = SinkMode() == Sink::Srgb && srgbTarget && SrgbOf(eye.format) != eye.format &&
((i < count && frame.mode == AURORA_STEREO_FRAME_IMMERSIVE_REPLAY) || swizzle);
if (swizzle && !(srgbEye && eye.view != nullptr && *eye.view)) return false;
auto* texture = ImportTarget(i, srgbEye ? TargetSrgb(target.dxgiFormat) : eye.format);
if (!texture) return false;
active[i] = *texture;
}
for (uint32_t i = 0; i < images; ++i) {
wgpu::TexelCopyTextureInfo source, destination;
source.texture = *sources[i].texture;
destination.texture = active[i];
wgpu::Extent3D size{targets[i].width, targets[i].height, 1};
// An eye rendered straight into its swapchain image (DirectTargets) needs no copy.
if (source.texture.Get() == destination.texture.Get()) {
} else if (SinkBlit() && sources[i].view != nullptr && *sources[i].view &&
(SinkMode() == Sink::Blit || active[i].GetFormat() != sources[i].format)) {
Blit(encoder, *sources[i].view, active[i]);
} else {
encoder.CopyTextureToTexture(&source, &destination, &size);
}
if (StampEnabled() && frame.mode == AURORA_STEREO_FRAME_IMMERSIVE_REPLAY && i < count && count == 2 &&
targets[i].width >= 64 * kStampCell &&
targets[i].height >= kStampRows * kStampCell)
WriteStamp(encoder, active[i], active[i].GetFormat(), frame.displayTimeNanos, i, frame.scene[i]);
}
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*, kMaxImages> textures{};
for (uint32_t i = 0; i < images; ++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(), images) != 0;
const auto completed = token;
token = 0; encoded = false; claimed = false;
submittingRetained = frame.retained;
callback(completed, success, userdata);
submittingRetained = false;
}
};
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());
aurora::stereo::set_direct_targets(
[](uint64_t token, wgpu::TextureFormat format, uint32_t width, uint32_t height,
std::array<wgpu::Texture, AURORA_STEREO_EYE_COUNT>& out, void* self) noexcept {
return static_cast<Bridge*>(self)->DirectTargets(token, format, width, height, out);
}, 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, nullptr);
}
bool aurora_vulkan_win32_set_targets_with_panel(uint64_t token, const AuroraD3D12StereoTarget* targets, uint32_t count,
const AuroraD3D12StereoTarget* panel) {
using namespace aurora::vulkan_win32;
return bridge && bridge->Set(token, targets, count, panel);
}
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);
aurora::stereo::set_direct_targets(nullptr, nullptr);
if (!api.drain(aurora::webgpu::g_device.Get())) { bridge.release(); return false; }
bridge.reset();
return true;
}
bool aurora_vulkan_win32_submitting_retained() { return aurora::vulkan_win32::submittingRetained; }
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_set_targets_with_panel(uint64_t, const AuroraD3D12StereoTarget*, uint32_t,
const AuroraD3D12StereoTarget*) { return false; }
bool aurora_vulkan_win32_cancel(uint64_t) { return false; }
bool aurora_vulkan_win32_disable() { return true; }
bool aurora_vulkan_win32_submitting_retained() { return false; }
void* aurora_vulkan_win32_lock_queue() { return nullptr; }
void aurora_vulkan_win32_unlock_queue(void*) {}
#endif