Files
mitch030504--Wiicompiled_VR…/runtime/src/vr/openxr_vulkan.cpp
T
iChris4andClaude Opus 5.5 c990c595f0 Render only the window in the Quest's immersive window
- The pacing thread aims each immersive window eye through the window itself (AimEyesThroughWindow):
  it keeps its position but looks square-on at the window's plane through an off-axis frustum just
  around it, so the eye image is the window, at the display's pixel density (about 680x380 per eye
  at render_scale 0.8 instead of 1344x1408), with a two-pixel border the mask leaves transparent.
  The frame's views carry that pose and field of view to the projection layer.
- vulkan_interop.cpp copies an eye smaller than its AHardwareBuffer into the buffer's corner, and
  the Quest layer's imageRect is the rendered part of the swapchain image.
- These eyes are not foveated: their field of view follows the head, which would rebuild the
  density map every frame.
- Quest only (kWindowShapedEyesSupported); the PC backends copy whole eyes and keep masking them.
  debug.wiicompiled.window_eyes 0 renders them whole and masked again for A/B timing.
- Quest 3, paused Retro Rewind race, render_scale 1.0: Immersive window runs the GPU at level 1
  (456 MHz, app GPU 11.0 ms, eyes 8.4 ms) where Immersive needs level 3 (599-640 MHz, 13.4 ms,
  10.3 ms), about 42% fewer GPU cycles. No edge artifacts, image as sharp. Docs: OPENXR.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:29:28 +02:00

2167 lines
96 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#if defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
// OpenXR's Vulkan structures are selected when openxr_platform.h is parsed.
#define VK_USE_PLATFORM_ANDROID_KHR
#define XR_USE_GRAPHICS_API_VULKAN
#define XR_USE_PLATFORM_ANDROID
#include "vr/openxr_vulkan.h"
#include "vr/openxr_diagnostics.h"
#include "vr/openxr_passthrough.h"
#include <aurora/vulkan_interop.h>
#include <android/hardware_buffer.h>
#include <jni.h>
#include <unistd.h>
#include <vulkan/vulkan.h>
#include <openxr/openxr_platform.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <limits>
#include <mutex>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
namespace mkw::vr {
namespace {
// Two buffers per eye: Dawn writes one while this backend's queue copies the
// other into the compositor image. The sync-fd handshake orders the two
// devices on each buffer, so a deeper ring only adds latency.
constexpr uint32_t kSlotCount = 2;
// Images one frame copies into the compositor: the eyes, then the settings
// panel's layer image, in the order of Aurora's release entries.
constexpr uint32_t kMaxCopies = AURORA_VULKAN_STEREO_MAX_RELEASES;
static_assert(kMaxCopies == kOpenXREyeCount + 1);
// Copy command buffers in flight before the oldest fence is waited on.
constexpr uint32_t kSubmissionRingSize = 3;
constexpr uint64_t kFenceTimeoutNanos = 2'000'000'000ull;
int CopyFamily(VkFormat format) noexcept {
switch (format) {
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
return 1;
case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SRGB:
return 2;
case VK_FORMAT_R16G16B16A16_SFLOAT:
return 3;
default:
return 0;
}
}
bool SameCopyFamily(VkFormat left, VkFormat right) noexcept {
const int family = CopyFamily(left);
return family != 0 && family == CopyFamily(right);
}
VkFormat SrgbSibling(VkFormat format) noexcept {
switch (format) {
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
return VK_FORMAT_R8G8B8A8_SRGB;
case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SRGB:
return VK_FORMAT_B8G8R8A8_SRGB;
default:
return VK_FORMAT_UNDEFINED;
}
}
const char* BeginStatusOperation(OpenXRFrameStatus status) noexcept {
switch (status) {
case OpenXRFrameStatus::Ready:
return "ready";
case OpenXRFrameStatus::SessionNotRunning:
return "session is not running";
case OpenXRFrameStatus::ExitRequested:
return "runtime requested exit";
case OpenXRFrameStatus::Error:
return "xrWaitFrame failed";
}
return "unknown frame status";
}
std::vector<std::string> SplitExtensionList(const std::string& text) {
std::vector<std::string> names;
std::istringstream stream(text);
std::string name;
while (stream >> name) {
names.push_back(name);
}
return names;
}
void CloseFd(int& fd) noexcept {
if (fd >= 0) {
::close(fd);
}
fd = -1;
}
int DupFd(int fd) noexcept {
return fd >= 0 ? ::dup(fd) : -1;
}
std::string VkFailure(const char* what, VkResult result) {
std::ostringstream message;
message << what << " (VkResult " << static_cast<int32_t>(result) << ')';
return message.str();
}
} // namespace
class OpenXRVulkanBackend::Impl final {
public:
explicit Impl(OpenXRLogCallback logger) : logger_(std::move(logger)) {}
~Impl() { Shutdown(); }
struct EyeSwapchain {
XrSwapchain handle = XR_NULL_HANDLE;
uint32_t width = 0;
uint32_t height = 0;
std::vector<XrSwapchainImageVulkan2KHR> images;
uint32_t acquired_index = 0;
bool acquired = false;
bool waited = false;
bool release_forbidden = false;
};
// One AHardwareBuffer shared with Dawn, imported into this backend's own
// device. Dawn writes it, this device reads it into the compositor image.
struct EyeSlot {
AHardwareBuffer* buffer = nullptr;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
// Signalled by this device's copy and exported as the fence Dawn waits on.
VkSemaphore signal_semaphore = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
// The sync fd of this slot's last copy-out, kept until the next copy replaces it. Dawn
// gets a duplicate per frame, so a frame cancelled before encoding cannot lose it.
int pending_acquire_fd = -1;
uint32_t width = 0;
uint32_t height = 0;
};
struct Submission {
VkFence fence = VK_NULL_HANDLE;
VkCommandBuffer command_buffer = VK_NULL_HANDLE;
// Dawn's release fences are imported here, one semaphore per image. They belong to the
// submission rather than the slot: importing into a semaphore whose previous wait is
// still pending is invalid, and only the submission's fence proves that wait completed.
std::array<VkSemaphore, kMaxCopies> wait_semaphores{};
bool busy = false;
};
enum class CopyOutcome {
Submitted,
// Nothing reached the queue: the shared buffers and the compositor image are untouched.
Skipped,
// Work may have been queued with no completion marker to wait on.
Unsafe,
};
struct PendingCopy {
uint64_t token = 0;
uint32_t slot = 0;
uint32_t target_count = 0;
// The settings panel's layer image follows the eyes.
bool panel = false;
std::array<VkImage, kMaxCopies> swapchain_images{};
uint32_t Count() const noexcept { return target_count + (panel ? 1u : 0u); }
};
bool QueryGraphicsRequirements(OpenXRRuntime& runtime) {
ClearError();
if (!runtime.IsInitialized() || runtime.HasSession()) {
return Fail("OpenXR must own an instance, but no session, before querying Vulkan requirements");
}
if (requirements_queried_ && runtime_ != &runtime) {
return Fail("Vulkan graphics requirements were already queried from another OpenXR instance");
}
const auto& extensions = runtime.EnabledExtensions();
const bool enable2 = std::find(extensions.begin(), extensions.end(),
std::string(XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME)) != extensions.end();
const bool enable1 = std::find(extensions.begin(), extensions.end(),
std::string(XR_KHR_VULKAN_ENABLE_EXTENSION_NAME)) != extensions.end();
if (!enable2 && !enable1) {
return Fail("the OpenXR runtime offers neither XR_KHR_vulkan_enable2 nor XR_KHR_vulkan_enable");
}
XrGraphicsRequirementsVulkan2KHR requirements{XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN2_KHR};
XrResult result = XR_ERROR_RUNTIME_FAILURE;
if (enable2) {
PFN_xrGetVulkanGraphicsRequirements2KHR get_requirements = nullptr;
if (!runtime.LoadFunction("xrGetVulkanGraphicsRequirements2KHR", &get_requirements) ||
get_requirements == nullptr) {
return Fail("OpenXR runtime did not expose xrGetVulkanGraphicsRequirements2KHR");
}
result = get_requirements(runtime.Instance(), runtime.SystemId(), &requirements);
} else {
PFN_xrGetVulkanGraphicsRequirementsKHR get_requirements = nullptr;
if (!runtime.LoadFunction("xrGetVulkanGraphicsRequirementsKHR", &get_requirements) ||
get_requirements == nullptr) {
return Fail("OpenXR runtime did not expose xrGetVulkanGraphicsRequirementsKHR");
}
result = get_requirements(runtime.Instance(), runtime.SystemId(), &requirements);
}
runtime.ObserveResult(result);
if (XR_FAILED(result)) {
std::ostringstream message;
message << "xrGetVulkanGraphicsRequirements failed (" << result << ')';
return Fail(message.str());
}
runtime_ = &runtime;
{
std::lock_guard lock(submission_mutex_);
shutting_down_ = false;
submission_unsafe_ = false;
}
requirements_ = {
requirements.minApiVersionSupported,
requirements.maxApiVersionSupported,
enable2,
};
requirements_queried_ = true;
std::ostringstream message;
message << "OpenXR Vulkan requirements: API "
<< XR_VERSION_MAJOR(requirements_.min_api_version) << '.'
<< XR_VERSION_MINOR(requirements_.min_api_version) << " to "
<< XR_VERSION_MAJOR(requirements_.max_api_version) << '.'
<< XR_VERSION_MINOR(requirements_.max_api_version) << " via "
<< (enable2 ? XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME : XR_KHR_VULKAN_ENABLE_EXTENSION_NAME);
Log(OpenXRLogLevel::Info, message.str());
return true;
}
bool BindAurora(OpenXRRuntime& runtime) {
ClearError();
if (!requirements_queried_ || runtime_ != &runtime || runtime.HasSession()) {
return Fail("QueryGraphicsRequirements must succeed on this OpenXR instance before BindAurora");
}
if (bound_) {
return Fail("OpenXR Vulkan backend is already bound");
}
AuroraVulkanNativeHandles handles{};
if (!aurora_vulkan_get_native_handles(&handles)) {
return Fail("Aurora did not report a Dawn Vulkan device");
}
if (!handles.sharedTextureMemoryAHardwareBuffer || !handles.sharedFenceSyncFd) {
return Fail("Aurora's Dawn device lacks AHardwareBuffer shared memory or sync-fd fences");
}
aurora_format_ = static_cast<VkFormat>(handles.colorVkFormat);
if (CopyFamily(aurora_format_) == 2) {
return Fail("Aurora selected a BGRA colour format, which Android cannot share through AHardwareBuffer");
}
if (CopyFamily(aurora_format_) == 0) {
return Fail("Aurora's colour format cannot be copied into an OpenXR swapchain");
}
if (!CreateVulkanObjects()) {
DestroyVulkanObjects();
return false;
}
XrGraphicsBindingVulkan2KHR binding{XR_TYPE_GRAPHICS_BINDING_VULKAN2_KHR};
binding.instance = vk_instance_;
binding.physicalDevice = vk_physical_;
binding.device = vk_device_;
binding.queueFamilyIndex = queue_family_;
binding.queueIndex = 0;
if (!runtime.CreateSession(&binding)) {
DestroyVulkanObjects();
return Fail("OpenXR rejected the Vulkan device binding");
}
owns_session_ = true;
if (!SelectSwapchainFormat() || !CreateSwapchains() || !AllocateSlots()) {
DestroySwapchains();
DestroySlots();
runtime.DestroySession();
owns_session_ = false;
DestroyVulkanObjects();
return false;
}
if (runtime.ShouldExit()) {
DestroySwapchains();
DestroySlots();
runtime.DestroySession();
owns_session_ = false;
DestroyVulkanObjects();
return Fail("OpenXR session became loss-pending while creating Vulkan swapchains");
}
if (!aurora_vulkan_enable_stereo_bridge(&Impl::OnAuroraSubmitted, this)) {
DestroySwapchains();
DestroySlots();
runtime.DestroySession();
owns_session_ = false;
DestroyVulkanObjects();
return Fail("Aurora could not enable its AHardwareBuffer stereo bridge");
}
bridge_enabled_ = true;
bound_ = true;
std::ostringstream message;
message << "OpenXR Vulkan swapchains ready: VkFormat "
<< static_cast<int64_t>(swapchain_format_) << ", eyes "
<< eye_swapchains_[0].width << 'x' << eye_swapchains_[0].height << " / "
<< eye_swapchains_[1].width << 'x' << eye_swapchains_[1].height
<< ", shared buffers " << kSlotCount << " per eye";
Log(OpenXRLogLevel::Info, message.str());
return true;
}
OpenXRBeginStatus BeginFrame(const OpenXRPresentation& presentation, OpenXRBackendFrame& frame) {
frame = {};
frame.presentation = presentation;
if (!bound_ || runtime_ == nullptr) {
Fail("BeginFrame called before the Vulkan backend was bound");
return OpenXRBeginStatus::Error;
}
if (frame_active_) {
Fail("BeginFrame called while another OpenXR frame is active");
return OpenXRBeginStatus::Error;
}
const OpenXRFrameStatus status = runtime_->WaitFrame(frame.xr_frame);
if (status != OpenXRFrameStatus::Ready) {
if (status == OpenXRFrameStatus::Error) {
Fail(BeginStatusOperation(status));
}
switch (status) {
case OpenXRFrameStatus::SessionNotRunning:
return OpenXRBeginStatus::SessionNotRunning;
case OpenXRFrameStatus::ExitRequested:
return OpenXRBeginStatus::ExitRequested;
case OpenXRFrameStatus::Error:
return OpenXRBeginStatus::Error;
case OpenXRFrameStatus::Ready:
break;
}
}
passthrough_.SetRunning(*runtime_, presentation.passthrough);
if (!runtime_->BeginFrame(frame.xr_frame)) {
Fail("xrBeginFrame failed");
return OpenXRBeginStatus::Error;
}
frame_active_ = true;
active_frame_serial_ = frame.xr_frame.serial;
active_frame_ = frame.xr_frame;
render_session_serial_ = runtime_->SessionRunSerial();
render_space_serial_ = runtime_->LastReferenceSpaceChange().serial;
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
frame.render_width[eye] = eye_swapchains_[eye].width;
frame.render_height[eye] = eye_swapchains_[eye].height;
}
if (!frame.xr_frame.should_render) {
return OpenXRBeginStatus::Ready;
}
if (!runtime_->LocateViews(frame.xr_frame)) {
Fail("xrLocateViews failed");
EndActiveFrameWithoutLayers(frame.xr_frame);
return OpenXRBeginStatus::Error;
}
active_frame_ = frame.xr_frame;
if (!frame.xr_frame.views_valid) {
return OpenXRBeginStatus::Ready;
}
const uint32_t target_count =
presentation.mode == OpenXRFrameMode::VirtualScreen ? 1u : kOpenXREyeCount;
if (target_count == 1) {
frame.render_width[1] = frame.render_width[0];
frame.render_height[1] = frame.render_height[0];
}
// The settings panel's layer image, rendered with the eyes while it is open.
const bool panel = frame.presentation.panel.requested && EnsurePanelResources();
frame.presentation.panel.requested = panel;
const diagnostics::Stopwatch acquire_timer;
for (uint32_t eye = 0; eye < target_count; ++eye) {
if (!AcquireSwapchain(eye_swapchains_[eye])) {
ReleaseAcquiredSwapchains();
EndActiveFrameWithoutLayers(frame.xr_frame);
return OpenXRBeginStatus::Error;
}
}
if (panel && !AcquireSwapchain(panel_swapchain_)) {
ReleaseAcquiredSwapchains();
EndActiveFrameWithoutLayers(frame.xr_frame);
return OpenXRBeginStatus::Error;
}
diagnostics::OnSwapchainAcquire(acquire_timer);
std::array<AuroraVulkanStereoTarget, kOpenXREyeCount> targets{};
AuroraVulkanStereoTarget panel_target{};
const uint32_t slot = next_slot_;
{
std::lock_guard lock(vk_mutex_);
for (uint32_t eye = 0; eye < target_count; ++eye) {
targets[eye] = SlotTargetLocked(slots_[eye][slot]);
}
if (panel) {
panel_target = SlotTargetLocked(panel_slots_[slot]);
}
pending_copy_ = {frame.xr_frame.serial, slot, target_count, panel, {}};
for (uint32_t eye = 0; eye < target_count; ++eye) {
pending_copy_.swapchain_images[eye] =
eye_swapchains_[eye].images[eye_swapchains_[eye].acquired_index].image;
}
if (panel) {
pending_copy_.swapchain_images[target_count] =
panel_swapchain_.images[panel_swapchain_.acquired_index].image;
}
}
{
std::lock_guard lock(submission_mutex_);
awaiting_token_ = frame.xr_frame.serial;
submitted_token_ = 0;
submission_arrived_ = false;
submission_success_ = false;
submission_unsafe_ = false;
}
if (!aurora_vulkan_set_stereo_targets_with_panel(frame.xr_frame.serial, targets.data(), target_count,
panel ? &panel_target : nullptr)) {
// The duplicates were not taken; the slots keep their own descriptors.
for (uint32_t eye = 0; eye < target_count; ++eye) {
CloseFd(targets[eye].acquireFenceFd);
}
if (panel) {
CloseFd(panel_target.acquireFenceFd);
}
{
std::lock_guard lock(submission_mutex_);
awaiting_token_ = 0;
}
ReleaseAcquiredSwapchains();
Fail("Aurora rejected the AHardwareBuffer stereo targets");
EndActiveFrameWithoutLayers(frame.xr_frame);
return OpenXRBeginStatus::Error;
}
next_slot_ = (slot + 1) % kSlotCount;
frame.expects_gpu_submission = true;
return OpenXRBeginStatus::Ready;
}
OpenXRSubmissionStatus WaitForSubmission(const OpenXRBackendFrame& frame, uint32_t timeout_ms) {
if (!frame.expects_gpu_submission) {
return OpenXRSubmissionStatus::Success;
}
std::unique_lock lock(submission_mutex_);
const auto ready = [&] {
return shutting_down_ ||
(submission_arrived_ && submitted_token_ == frame.xr_frame.serial);
};
if (timeout_ms == std::numeric_limits<uint32_t>::max()) {
submission_cv_.wait(lock, ready);
} else if (!submission_cv_.wait_for(lock, std::chrono::milliseconds(timeout_ms), ready)) {
return OpenXRSubmissionStatus::Timeout;
}
if (shutting_down_) {
return OpenXRSubmissionStatus::ShuttingDown;
}
if (submission_success_) {
return OpenXRSubmissionStatus::Success;
}
return submission_unsafe_ ? OpenXRSubmissionStatus::Failed : OpenXRSubmissionStatus::Skipped;
}
bool TryCancelPendingFrame(OpenXRBackendFrame& frame) {
if (!frame_active_ || !frame.expects_gpu_submission ||
frame.xr_frame.serial != active_frame_serial_) {
return false;
}
if (!aurora_vulkan_cancel_stereo_targets(frame.xr_frame.serial)) {
return false;
}
std::lock_guard lock(submission_mutex_);
awaiting_token_ = 0;
submitted_token_ = 0;
submission_arrived_ = false;
submission_success_ = false;
submission_unsafe_ = false;
frame.expects_gpu_submission = false;
return true;
}
// ---- Render-first pacing (see openxr_vulkan.h) ---------------------------------------------
void DiscardPendingReleasesLocked() noexcept {
if (!have_pending_releases_) {
return;
}
for (auto& release : pending_releases_) {
CloseFd(release.releaseFenceFd);
release = {-1, VK_IMAGE_LAYOUT_UNDEFINED};
}
have_pending_releases_ = false;
}
OpenXRBeginStatus PreparePacket(const OpenXRPresentation& presentation, OpenXRBackendFrame& packet) {
packet = {};
packet.presentation = presentation;
if (!bound_ || runtime_ == nullptr) {
Fail("PreparePacket called before the Vulkan backend was bound");
return OpenXRBeginStatus::Error;
}
if (frame_active_) {
Fail("PreparePacket called while an OpenXR frame is active");
return OpenXRBeginStatus::Error;
}
if (runtime_->ShouldExit()) {
return OpenXRBeginStatus::ExitRequested;
}
if (!runtime_->IsSessionRunning()) {
return OpenXRBeginStatus::SessionNotRunning;
}
// Before any frame ends on this presentation, the priming cycle below included.
passthrough_.SetRunning(*runtime_, presentation.passthrough);
if (last_display_period_ <= 0) {
// No display timing yet: one compositor cycle learns it.
const OpenXRBeginStatus primed = KeepAliveCycle();
if (primed != OpenXRBeginStatus::Ready) {
return primed;
}
}
packet.xr_frame.serial = next_packet_serial_++;
// The eyes are ready after at most one game frame plus the encode and show at the first
// display slot after that: two periods past the last predicted display time.
packet.xr_frame.predicted_display_time = last_display_time_ + 2 * last_display_period_;
packet.xr_frame.predicted_display_period = last_display_period_;
packet.xr_frame.should_render = last_should_render_;
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
packet.render_width[eye] = eye_swapchains_[eye].width;
packet.render_height[eye] = eye_swapchains_[eye].height;
}
if (!packet.xr_frame.should_render) {
return OpenXRBeginStatus::Ready;
}
if (!runtime_->LocateViewsAt(packet.xr_frame.predicted_display_time, packet.xr_frame)) {
Fail("xrLocateViews failed for a packet");
return OpenXRBeginStatus::Error;
}
if (!packet.xr_frame.views_valid) {
return OpenXRBeginStatus::Ready;
}
const uint32_t target_count =
presentation.mode == OpenXRFrameMode::VirtualScreen ? 1u : kOpenXREyeCount;
if (target_count == 1) {
packet.render_width[1] = packet.render_width[0];
packet.render_height[1] = packet.render_height[0];
}
// The settings panel's layer image, rendered with the eyes while it is open.
const bool panel = packet.presentation.panel.requested && EnsurePanelResources();
packet.presentation.panel.requested = panel;
std::array<AuroraVulkanStereoTarget, kOpenXREyeCount> targets{};
AuroraVulkanStereoTarget panel_target{};
const uint32_t slot = next_slot_;
{
std::lock_guard lock(vk_mutex_);
DiscardPendingReleasesLocked();
for (uint32_t eye = 0; eye < target_count; ++eye) {
targets[eye] = SlotTargetLocked(slots_[eye][slot]);
}
if (panel) {
panel_target = SlotTargetLocked(panel_slots_[slot]);
}
// The compositor images are acquired by BeginFrameForPacket, once the eyes exist.
pending_copy_ = {packet.xr_frame.serial, slot, target_count, panel, {}};
deferred_copy_ = true;
}
{
std::lock_guard lock(submission_mutex_);
awaiting_token_ = packet.xr_frame.serial;
submitted_token_ = 0;
submission_arrived_ = false;
submission_success_ = false;
submission_unsafe_ = false;
}
if (!aurora_vulkan_set_stereo_targets_with_panel(packet.xr_frame.serial, targets.data(), target_count,
panel ? &panel_target : nullptr)) {
for (uint32_t eye = 0; eye < target_count; ++eye) {
CloseFd(targets[eye].acquireFenceFd);
}
if (panel) {
CloseFd(panel_target.acquireFenceFd);
}
{
std::lock_guard lock(submission_mutex_);
awaiting_token_ = 0;
}
{
std::lock_guard lock(vk_mutex_);
deferred_copy_ = false;
}
Fail("Aurora rejected the AHardwareBuffer stereo targets");
return OpenXRBeginStatus::Error;
}
next_slot_ = (slot + 1) % kSlotCount;
packet.expects_gpu_submission = true;
return OpenXRBeginStatus::Ready;
}
bool TryCancelPendingPacket(OpenXRBackendFrame& packet) {
if (!packet.expects_gpu_submission || !aurora_vulkan_cancel_stereo_targets(packet.xr_frame.serial)) {
return false;
}
{
std::lock_guard lock(submission_mutex_);
awaiting_token_ = 0;
submitted_token_ = 0;
submission_arrived_ = false;
submission_success_ = false;
submission_unsafe_ = false;
}
{
std::lock_guard lock(vk_mutex_);
DiscardPendingReleasesLocked();
deferred_copy_ = false;
}
packet.expects_gpu_submission = false;
return true;
}
bool FinishFrame(OpenXRBackendFrame& frame, bool submit_layer) {
if (!frame_active_ || runtime_ == nullptr ||
frame.xr_frame.serial != active_frame_serial_) {
return Fail("FinishFrame received a stale or inactive OpenXR frame token");
}
bool submission_unsafe = false;
{
std::lock_guard lock(submission_mutex_);
submission_unsafe = submission_arrived_ &&
submitted_token_ == frame.xr_frame.serial &&
submission_unsafe_;
}
if (submission_unsafe) {
AbandonAcquiredSwapchains();
Fail("Aurora's stereo submission failed after GPU work may have been queued");
}
const diagnostics::Stopwatch release_timer;
bool release_ok = ReleaseAcquiredSwapchains();
if (frame.xr_frame.should_render && frame.xr_frame.views_valid) {
diagnostics::OnSwapchainRelease(release_timer);
}
const bool position_valid =
(frame.xr_frame.view_state_flags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
const bool composition_pose_valid =
frame.presentation.mode == OpenXRFrameMode::VirtualScreen || position_valid;
const bool can_submit = submit_layer && release_ok && frame.xr_frame.should_render &&
frame.xr_frame.views_valid && frame.expects_gpu_submission &&
composition_pose_valid;
if (submit_layer && !can_submit) {
diagnostics::OnLayerRejected(diagnostics::ClassifyRejectedLayer(
release_ok, frame.xr_frame.should_render, frame.xr_frame.views_valid));
}
if (can_submit) {
// xrEndFrame references the most recently released image of a
// swapchain, so keep the displayed pair separate from the pair
// Aurora may write next.
std::swap(eye_swapchains_, retained_swapchains_);
if (frame.presentation.panel.requested) {
std::swap(panel_swapchain_, retained_panel_swapchain_);
}
retained_panel_valid_ = frame.presentation.panel.requested;
retained_frame_ = frame;
retained_session_serial_ = render_session_serial_;
retained_space_serial_ = render_space_serial_;
have_retained_frame_ = true;
}
const bool end_ok = EndRetainedFrame(can_submit);
frame_active_ = false;
active_frame_serial_ = 0;
active_frame_ = {};
frame.expects_gpu_submission = false;
{
// Eyes rendered for a packet that this frame did not copy are dropped with it.
std::lock_guard lock(vk_mutex_);
DiscardPendingReleasesLocked();
deferred_copy_ = false;
}
{
std::lock_guard lock(submission_mutex_);
awaiting_token_ = 0;
submission_arrived_ = false;
submission_success_ = false;
submission_unsafe_ = false;
}
return release_ok && end_ok;
}
OpenXRBeginStatus BeginFrameForPacket(const OpenXRBackendFrame& packet, OpenXRBackendFrame& frame) {
frame = {};
frame.presentation = packet.presentation;
frame.render_width = packet.render_width;
frame.render_height = packet.render_height;
if (!bound_ || runtime_ == nullptr) {
Fail("BeginFrameForPacket called before the Vulkan backend was bound");
return OpenXRBeginStatus::Error;
}
if (frame_active_) {
Fail("BeginFrameForPacket called while another OpenXR frame is active");
return OpenXRBeginStatus::Error;
}
const OpenXRFrameStatus status = runtime_->WaitFrame(frame.xr_frame);
if (status != OpenXRFrameStatus::Ready) {
if (status == OpenXRFrameStatus::Error) {
Fail(BeginStatusOperation(status));
}
return status == OpenXRFrameStatus::SessionNotRunning ? OpenXRBeginStatus::SessionNotRunning
: status == OpenXRFrameStatus::ExitRequested ? OpenXRBeginStatus::ExitRequested
: OpenXRBeginStatus::Error;
}
NoteDisplayTiming(frame.xr_frame);
if (!runtime_->BeginFrame(frame.xr_frame)) {
Fail("xrBeginFrame failed");
return OpenXRBeginStatus::Error;
}
frame_active_ = true;
active_frame_serial_ = frame.xr_frame.serial;
render_session_serial_ = runtime_->SessionRunSerial();
render_space_serial_ = runtime_->LastReferenceSpaceChange().serial;
// The layer shows the eyes as they were rendered: it carries the packet's located views.
frame.xr_frame.views = packet.xr_frame.views;
frame.xr_frame.view_state_flags = packet.xr_frame.view_state_flags;
frame.xr_frame.views_valid = packet.xr_frame.views_valid;
active_frame_ = frame.xr_frame;
frame.expects_gpu_submission = packet.expects_gpu_submission;
if (!frame.xr_frame.should_render || !frame.expects_gpu_submission || !frame.xr_frame.views_valid) {
// No swapchain image is acquired for this frame, so the rendered eyes cannot be
// copied; FinishFrame drops them with the frame.
frame.expects_gpu_submission = false;
return OpenXRBeginStatus::Ready;
}
const uint32_t target_count =
presentation_target_count(frame.presentation);
const bool panel = frame.presentation.panel.requested;
const diagnostics::Stopwatch acquire_timer;
for (uint32_t eye = 0; eye < target_count; ++eye) {
if (!AcquireSwapchain(eye_swapchains_[eye])) {
ReleaseAcquiredSwapchains();
EndActiveFrameWithoutLayers(frame.xr_frame);
return OpenXRBeginStatus::Error;
}
}
if (panel && !AcquireSwapchain(panel_swapchain_)) {
ReleaseAcquiredSwapchains();
EndActiveFrameWithoutLayers(frame.xr_frame);
return OpenXRBeginStatus::Error;
}
diagnostics::OnSwapchainAcquire(acquire_timer);
{
std::lock_guard lock(vk_mutex_);
for (uint32_t eye = 0; eye < target_count; ++eye) {
pending_copy_.swapchain_images[eye] =
eye_swapchains_[eye].images[eye_swapchains_[eye].acquired_index].image;
}
if (panel) {
pending_copy_.swapchain_images[target_count] =
panel_swapchain_.images[panel_swapchain_.acquired_index].image;
}
}
return OpenXRBeginStatus::Ready;
}
OpenXRSubmissionStatus CopyRenderedEyes(const OpenXRBackendFrame& frame) {
CopyOutcome outcome = CopyOutcome::Skipped;
{
std::lock_guard lock(vk_mutex_);
if (!frame_active_ || !have_pending_releases_ || !deferred_copy_) {
return OpenXRSubmissionStatus::Skipped;
}
outcome = RecordAndSubmitCopyLocked(pending_releases_);
have_pending_releases_ = false;
deferred_copy_ = false;
}
{
// FinishFrame judges the queue's safety by this frame's token.
std::lock_guard lock(submission_mutex_);
submitted_token_ = frame.xr_frame.serial;
submission_arrived_ = true;
submission_success_ = outcome == CopyOutcome::Submitted;
submission_unsafe_ = outcome == CopyOutcome::Unsafe;
}
return outcome == CopyOutcome::Submitted ? OpenXRSubmissionStatus::Success
: outcome == CopyOutcome::Unsafe ? OpenXRSubmissionStatus::Failed
: OpenXRSubmissionStatus::Skipped;
}
OpenXRBeginStatus KeepAliveCycle() {
if (!bound_ || runtime_ == nullptr || frame_active_) {
Fail("KeepAliveCycle called with a frame active or before binding");
return OpenXRBeginStatus::Error;
}
OpenXRFrame cycle{};
const OpenXRFrameStatus status = runtime_->WaitFrame(cycle);
if (status != OpenXRFrameStatus::Ready) {
if (status == OpenXRFrameStatus::Error) {
Fail(BeginStatusOperation(status));
}
return status == OpenXRFrameStatus::SessionNotRunning ? OpenXRBeginStatus::SessionNotRunning
: status == OpenXRFrameStatus::ExitRequested ? OpenXRBeginStatus::ExitRequested
: OpenXRBeginStatus::Error;
}
NoteDisplayTiming(cycle);
if (!runtime_->BeginFrame(cycle)) {
Fail("xrBeginFrame failed for a keep-alive cycle");
return OpenXRBeginStatus::Error;
}
active_frame_ = cycle;
frame_active_ = true;
const bool end_ok = EndRetainedFrame(false);
frame_active_ = false;
active_frame_ = {};
if (!end_ok) {
Fail("OpenXR could not resubmit the retained frame");
return OpenXRBeginStatus::Error;
}
return OpenXRBeginStatus::Ready;
}
void NoteDisplayTiming(const OpenXRFrame& frame) noexcept {
last_display_time_ = frame.predicted_display_time;
last_display_period_ = frame.predicted_display_period;
last_should_render_ = frame.should_render;
}
static uint32_t presentation_target_count(const OpenXRPresentation& presentation) noexcept {
return presentation.mode == OpenXRFrameMode::VirtualScreen ? 1u : kOpenXREyeCount;
}
bool RepeatFrame(const OpenXRBackendFrame& frame) {
if (!frame_active_ || runtime_ == nullptr ||
frame.xr_frame.serial != active_frame_serial_) {
return Fail("RepeatFrame received a stale or inactive render token");
}
const bool end_ok = EndRetainedFrame(false);
frame_active_ = false;
if (!end_ok) {
return Fail("OpenXR could not resubmit the retained frame");
}
if (runtime_->PollEvents() != OpenXREventStatus::Continue ||
!runtime_->IsSessionRunning() || runtime_->ShouldExit()) {
return Fail("OpenXR session stopped while waiting for stereo rendering");
}
if (runtime_->WaitFrame(active_frame_) != OpenXRFrameStatus::Ready ||
!runtime_->BeginFrame(active_frame_)) {
return Fail("OpenXR could not start a retained-frame compositor cycle");
}
frame_active_ = true;
return true;
}
// fresh: the retained layer was completed for this call rather than repeated.
bool EndRetainedFrame(bool fresh) {
if (!runtime_->IsSessionRunning()) {
return true;
}
const bool session_changed = retained_session_serial_ != runtime_->SessionRunSerial();
if (session_changed || retained_space_serial_ != runtime_->LastReferenceSpaceChange().serial) {
if (have_retained_frame_) {
diagnostics::OnRetainedLayerDiscarded(session_changed
? diagnostics::DiscardReason::SessionRestarted
: diagnostics::DiscardReason::ReferenceSpaceChanged);
}
have_retained_frame_ = false;
}
if (!have_retained_frame_ || !active_frame_.should_render) {
diagnostics::OnEmptyFrame(!active_frame_.should_render ? diagnostics::EmptyFrameReason::ShouldRenderOff
: diagnostics::EmptyFrameReason::NoRetainedLayer);
// Outside a race the room stays in view until there is an image to show (at start
// and after a recenter), rather than flashing black.
if (const XrCompositionLayerBaseHeader* passthrough = passthrough_.Layer()) {
return runtime_->EndFrame(active_frame_, &passthrough, 1);
}
return runtime_->EndFrameWithoutLayers(active_frame_);
}
diagnostics::OnLayer(fresh);
const auto& frame = retained_frame_;
if (frame.presentation.mode == OpenXRFrameMode::VirtualScreen) {
XrCompositionLayerQuad quad{XR_TYPE_COMPOSITION_LAYER_QUAD};
quad.layerFlags = 0;
quad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
quad.subImage.swapchain = retained_swapchains_[0].handle;
// Only the picture, not the black bands letterboxing it into the eye-sized image: they
// would frame it against the passthrough view.
const uint32_t image_width = retained_swapchains_[0].width;
quad.subImage.imageRect = OpenXRVirtualScreenContentRect(
image_width, retained_swapchains_[0].height, frame.presentation.quad_content_aspect);
quad.subImage.imageArrayIndex = 0;
if (frame.presentation.quad_anchored) {
quad.space = runtime_->AppSpace();
quad.pose = frame.presentation.quad_pose;
} else {
quad.space = runtime_->ViewSpace();
quad.pose.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
quad.pose.position = {
0.0f, 0.0f, -std::max(0.25f, frame.presentation.quad_distance_meters)};
}
// The whole image would be quad_width_meters across: the crop keeps that size per pixel,
// so the picture stays exactly where the pointer and the settings panel expect it.
const float meters_per_pixel =
std::max(0.25f, frame.presentation.quad_width_meters) / static_cast<float>(image_width);
quad.size.width = meters_per_pixel * static_cast<float>(quad.subImage.imageRect.extent.width);
quad.size.height = meters_per_pixel * static_cast<float>(quad.subImage.imageRect.extent.height);
return EndFrameWithPanel(frame, reinterpret_cast<const XrCompositionLayerBaseHeader*>(&quad));
}
std::array<XrCompositionLayerProjectionView, kOpenXREyeCount> views{};
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
views[eye] = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW};
views[eye].pose.orientation = frame.xr_frame.views[eye].pose.orientation;
views[eye].pose.position = frame.xr_frame.views[eye].pose.position;
views[eye].fov = frame.xr_frame.views[eye].fov;
views[eye].subImage.swapchain = retained_swapchains_[eye].handle;
// The part of the image the eye was rendered into: all of it, except for the immersive
// window's eyes, which are only the window (OpenXRPresentation::window_eyes).
views[eye].subImage.imageRect = {
{0, 0},
{static_cast<int32_t>(std::min(frame.render_width[eye], retained_swapchains_[eye].width)),
static_cast<int32_t>(std::min(frame.render_height[eye], retained_swapchains_[eye].height))}};
views[eye].subImage.imageArrayIndex = 0;
}
XrCompositionLayerProjection projection{XR_TYPE_COMPOSITION_LAYER_PROJECTION};
// The immersive window's eyes are transparent outside the window (premultiplied alpha), so
// the room shows around it; otherwise the race covers the whole view and alpha is ignored.
projection.layerFlags =
frame.presentation.immersive_window ? XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT : 0;
projection.space = runtime_->AppSpace();
projection.viewCount = kOpenXREyeCount;
projection.views = views.data();
return EndFrameWithPanel(frame, reinterpret_cast<const XrCompositionLayerBaseHeader*>(&projection));
}
// Ends the compositor frame with the scene's layer: over the room's camera
// view while that runs and the scene is the virtual screen or the immersive
// window (never under a fully immersive race's projection, which covers the
// whole view), and, while the retained frame rendered it, under the settings
// panel's layer.
bool EndFrameWithPanel(const OpenXRBackendFrame& frame, const XrCompositionLayerBaseHeader* scene) {
const auto& panel = frame.presentation.panel;
XrCompositionLayerQuad panel_quad{};
const XrCompositionLayerBaseHeader* layers[3] = {};
uint32_t count = 0;
if (const XrCompositionLayerBaseHeader* passthrough = passthrough_.Layer();
passthrough != nullptr && (frame.presentation.mode == OpenXRFrameMode::VirtualScreen ||
frame.presentation.immersive_window)) {
layers[count++] = passthrough;
}
layers[count++] = scene;
if (retained_panel_valid_ && panel.requested && panel.placed) {
panel_quad = OpenXRPanelQuadLayer(panel, runtime_->AppSpace(), retained_panel_swapchain_.handle);
layers[count++] = reinterpret_cast<const XrCompositionLayerBaseHeader*>(&panel_quad);
}
return runtime_->EndFrame(active_frame_, layers, count);
}
bool Shutdown() {
if (shutdown_unsafe_) {
return false;
}
{
std::lock_guard lock(submission_mutex_);
shutting_down_ = true;
}
submission_cv_.notify_all();
bool bridge_drained = true;
if (bridge_enabled_) {
bridge_drained = aurora_vulkan_disable_stereo_bridge();
bridge_enabled_ = false;
}
bool device_idle = true;
if (vk_device_ != VK_NULL_HANDLE) {
std::lock_guard lock(vk_mutex_);
device_idle = vkDeviceWaitIdle(vk_device_) == VK_SUCCESS;
}
if (!bridge_drained || !device_idle) {
AbandonAcquiredSwapchains();
shutdown_unsafe_ = true;
Fail("Vulkan queue completion is unknown; retaining the OpenXR session and graphics owners");
return false;
}
AllowAcquiredSwapchainsAfterGpuDrain();
ReleaseAcquiredSwapchains();
if (frame_active_ && runtime_ != nullptr) {
if (runtime_->IsSessionRunning()) {
runtime_->EndFrameWithoutLayers(active_frame_);
}
frame_active_ = false;
active_frame_serial_ = 0;
active_frame_ = {};
}
DestroySwapchains();
DestroySlots();
// Its handles belong to the session.
passthrough_.Destroy();
if (owns_session_ && runtime_ != nullptr) {
runtime_->DestroySession();
owns_session_ = false;
}
DestroyVulkanObjects();
bound_ = false;
requirements_queried_ = false;
runtime_ = nullptr;
return true;
}
bool IsBound() const { return bound_; }
bool PanelLayerAvailable() const { return !panel_layer_failed_; }
const OpenXRVulkanGraphicsRequirements& GraphicsRequirements() const { return requirements_; }
int64_t SwapchainFormat() const { return static_cast<int64_t>(swapchain_format_); }
const std::string& LastError() const { return last_error_; }
private:
// ---- Vulkan device owned by the OpenXR side ------------------------------
bool CreateVulkanObjects() {
// Vulkan 1.1 brings external memory/semaphores and dedicated allocation
// into core; every Quest ships at least that. XR_KHR_vulkan_enable
// runtimes have been seen reporting max 1.0 while accepting 1.1.
uint32_t api_version = VK_API_VERSION_1_1;
if (requirements_.max_api_version != 0 &&
XR_VERSION_MAJOR(requirements_.max_api_version) == 1 &&
XR_VERSION_MINOR(requirements_.max_api_version) == 0 && requirements_.uses_enable2) {
api_version = VK_API_VERSION_1_1;
}
VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO};
application.pApplicationName = "WiiCompiled";
application.applicationVersion = 1;
application.pEngineName = "Aurora OpenXR bridge";
application.engineVersion = 1;
application.apiVersion = api_version;
std::vector<std::string> instance_extension_storage;
std::vector<const char*> instance_extensions;
if (!requirements_.uses_enable2) {
PFN_xrGetVulkanInstanceExtensionsKHR get_instance_extensions = nullptr;
if (!runtime_->LoadFunction("xrGetVulkanInstanceExtensionsKHR", &get_instance_extensions) ||
get_instance_extensions == nullptr) {
return Fail("xrGetVulkanInstanceExtensionsKHR is unavailable");
}
uint32_t length = 0;
get_instance_extensions(runtime_->Instance(), runtime_->SystemId(), 0, &length, nullptr);
std::string text(length, '\0');
if (length != 0) {
get_instance_extensions(runtime_->Instance(), runtime_->SystemId(), length, &length, text.data());
}
instance_extension_storage = SplitExtensionList(text);
}
for (const std::string& name : instance_extension_storage) {
instance_extensions.push_back(name.c_str());
}
VkInstanceCreateInfo instance_info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
instance_info.pApplicationInfo = &application;
instance_info.enabledExtensionCount = static_cast<uint32_t>(instance_extensions.size());
instance_info.ppEnabledExtensionNames = instance_extensions.data();
if (requirements_.uses_enable2) {
PFN_xrCreateVulkanInstanceKHR create_instance = nullptr;
if (!runtime_->LoadFunction("xrCreateVulkanInstanceKHR", &create_instance) ||
create_instance == nullptr) {
return Fail("xrCreateVulkanInstanceKHR is unavailable");
}
XrVulkanInstanceCreateInfoKHR info{XR_TYPE_VULKAN_INSTANCE_CREATE_INFO_KHR};
info.systemId = runtime_->SystemId();
info.createFlags = 0;
info.pfnGetInstanceProcAddr = &vkGetInstanceProcAddr;
info.vulkanCreateInfo = &instance_info;
info.vulkanAllocator = nullptr;
VkResult vk_result = VK_SUCCESS;
const XrResult result = create_instance(runtime_->Instance(), &info, &vk_instance_, &vk_result);
ObserveResult(result);
if (XR_FAILED(result) || vk_result != VK_SUCCESS || vk_instance_ == VK_NULL_HANDLE) {
std::ostringstream message;
message << "xrCreateVulkanInstanceKHR failed (" << result << ", VkResult " << vk_result << ')';
return Fail(message.str());
}
} else {
const VkResult vk_result = vkCreateInstance(&instance_info, nullptr, &vk_instance_);
if (vk_result != VK_SUCCESS || vk_instance_ == VK_NULL_HANDLE) {
std::ostringstream message;
message << "vkCreateInstance failed (VkResult " << vk_result << ')';
return Fail(message.str());
}
}
if (requirements_.uses_enable2) {
PFN_xrGetVulkanGraphicsDevice2KHR get_device = nullptr;
if (!runtime_->LoadFunction("xrGetVulkanGraphicsDevice2KHR", &get_device) || get_device == nullptr) {
return Fail("xrGetVulkanGraphicsDevice2KHR is unavailable");
}
XrVulkanGraphicsDeviceGetInfoKHR info{XR_TYPE_VULKAN_GRAPHICS_DEVICE_GET_INFO_KHR};
info.systemId = runtime_->SystemId();
info.vulkanInstance = vk_instance_;
const XrResult result = get_device(runtime_->Instance(), &info, &vk_physical_);
ObserveResult(result);
if (XR_FAILED(result) || vk_physical_ == VK_NULL_HANDLE) {
return Fail("xrGetVulkanGraphicsDevice2KHR failed");
}
} else {
PFN_xrGetVulkanGraphicsDeviceKHR get_device = nullptr;
if (!runtime_->LoadFunction("xrGetVulkanGraphicsDeviceKHR", &get_device) || get_device == nullptr) {
return Fail("xrGetVulkanGraphicsDeviceKHR is unavailable");
}
const XrResult result =
get_device(runtime_->Instance(), runtime_->SystemId(), vk_instance_, &vk_physical_);
ObserveResult(result);
if (XR_FAILED(result) || vk_physical_ == VK_NULL_HANDLE) {
return Fail("xrGetVulkanGraphicsDeviceKHR failed");
}
}
uint32_t family_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(vk_physical_, &family_count, nullptr);
std::vector<VkQueueFamilyProperties> families(family_count);
vkGetPhysicalDeviceQueueFamilyProperties(vk_physical_, &family_count, families.data());
queue_family_ = UINT32_MAX;
for (uint32_t index = 0; index < family_count; ++index) {
if ((families[index].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0 && families[index].queueCount > 0) {
queue_family_ = index;
break;
}
}
if (queue_family_ == UINT32_MAX) {
return Fail("the OpenXR physical device exposes no graphics queue family");
}
// Extensions this backend needs on top of whatever the runtime adds.
// Everything promoted to core in 1.1 is still requested by name when
// the driver lists it, which keeps a 1.0-only report working too.
uint32_t available_count = 0;
vkEnumerateDeviceExtensionProperties(vk_physical_, nullptr, &available_count, nullptr);
std::vector<VkExtensionProperties> available(available_count);
vkEnumerateDeviceExtensionProperties(vk_physical_, nullptr, &available_count, available.data());
const auto has_extension = [&](const char* name) {
return std::any_of(available.begin(), available.end(), [&](const VkExtensionProperties& e) {
return std::strcmp(e.extensionName, name) == 0;
});
};
const std::array<const char*, 2> mandatory{
VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME,
VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME,
};
const std::array<const char*, 8> desirable{
VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME,
VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME,
VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME,
VK_KHR_EXTERNAL_SEMAPHORE_EXTENSION_NAME,
VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME,
VK_KHR_BIND_MEMORY_2_EXTENSION_NAME,
VK_KHR_MAINTENANCE1_EXTENSION_NAME,
};
std::vector<std::string> device_extension_storage;
for (const char* name : mandatory) {
if (!has_extension(name)) {
return Fail(std::string("the OpenXR physical device lacks ") + name);
}
device_extension_storage.emplace_back(name);
}
for (const char* name : desirable) {
if (has_extension(name)) {
device_extension_storage.emplace_back(name);
}
}
if (!requirements_.uses_enable2) {
PFN_xrGetVulkanDeviceExtensionsKHR get_device_extensions = nullptr;
if (!runtime_->LoadFunction("xrGetVulkanDeviceExtensionsKHR", &get_device_extensions) ||
get_device_extensions == nullptr) {
return Fail("xrGetVulkanDeviceExtensionsKHR is unavailable");
}
uint32_t length = 0;
get_device_extensions(runtime_->Instance(), runtime_->SystemId(), 0, &length, nullptr);
std::string text(length, '\0');
if (length != 0) {
get_device_extensions(runtime_->Instance(), runtime_->SystemId(), length, &length, text.data());
}
for (const std::string& name : SplitExtensionList(text)) {
if (std::find(device_extension_storage.begin(), device_extension_storage.end(), name) ==
device_extension_storage.end()) {
device_extension_storage.push_back(name);
}
}
}
std::vector<const char*> device_extensions;
for (const std::string& name : device_extension_storage) {
device_extensions.push_back(name.c_str());
}
const float priority = 1.0f;
VkDeviceQueueCreateInfo queue_info{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
queue_info.queueFamilyIndex = queue_family_;
queue_info.queueCount = 1;
queue_info.pQueuePriorities = &priority;
VkPhysicalDeviceFeatures features{};
VkDeviceCreateInfo device_info{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
device_info.queueCreateInfoCount = 1;
device_info.pQueueCreateInfos = &queue_info;
device_info.enabledExtensionCount = static_cast<uint32_t>(device_extensions.size());
device_info.ppEnabledExtensionNames = device_extensions.data();
device_info.pEnabledFeatures = &features;
if (requirements_.uses_enable2) {
PFN_xrCreateVulkanDeviceKHR create_device = nullptr;
if (!runtime_->LoadFunction("xrCreateVulkanDeviceKHR", &create_device) || create_device == nullptr) {
return Fail("xrCreateVulkanDeviceKHR is unavailable");
}
XrVulkanDeviceCreateInfoKHR info{XR_TYPE_VULKAN_DEVICE_CREATE_INFO_KHR};
info.systemId = runtime_->SystemId();
info.createFlags = 0;
info.pfnGetInstanceProcAddr = &vkGetInstanceProcAddr;
info.vulkanPhysicalDevice = vk_physical_;
info.vulkanCreateInfo = &device_info;
info.vulkanAllocator = nullptr;
VkResult vk_result = VK_SUCCESS;
const XrResult result = create_device(runtime_->Instance(), &info, &vk_device_, &vk_result);
ObserveResult(result);
if (XR_FAILED(result) || vk_result != VK_SUCCESS || vk_device_ == VK_NULL_HANDLE) {
std::ostringstream message;
message << "xrCreateVulkanDeviceKHR failed (" << result << ", VkResult " << vk_result << ')';
return Fail(message.str());
}
} else {
const VkResult vk_result = vkCreateDevice(vk_physical_, &device_info, nullptr, &vk_device_);
if (vk_result != VK_SUCCESS || vk_device_ == VK_NULL_HANDLE) {
std::ostringstream message;
message << "vkCreateDevice failed (VkResult " << vk_result << ')';
return Fail(message.str());
}
}
vkGetDeviceQueue(vk_device_, queue_family_, 0, &vk_queue_);
pfn_import_semaphore_fd_ = reinterpret_cast<PFN_vkImportSemaphoreFdKHR>(
vkGetDeviceProcAddr(vk_device_, "vkImportSemaphoreFdKHR"));
pfn_get_semaphore_fd_ = reinterpret_cast<PFN_vkGetSemaphoreFdKHR>(
vkGetDeviceProcAddr(vk_device_, "vkGetSemaphoreFdKHR"));
pfn_get_ahb_properties_ = reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(
vkGetDeviceProcAddr(vk_device_, "vkGetAndroidHardwareBufferPropertiesANDROID"));
if (pfn_import_semaphore_fd_ == nullptr || pfn_get_semaphore_fd_ == nullptr ||
pfn_get_ahb_properties_ == nullptr) {
return Fail("the OpenXR Vulkan device did not resolve the sync-fd or AHardwareBuffer entry points");
}
VkCommandPoolCreateInfo pool_info{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pool_info.queueFamilyIndex = queue_family_;
if (vkCreateCommandPool(vk_device_, &pool_info, nullptr, &command_pool_) != VK_SUCCESS) {
return Fail("vkCreateCommandPool failed");
}
for (Submission& submission : submissions_) {
VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
allocate.commandPool = command_pool_;
allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocate.commandBufferCount = 1;
VkFenceCreateInfo fence_info{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
if (vkAllocateCommandBuffers(vk_device_, &allocate, &submission.command_buffer) != VK_SUCCESS ||
vkCreateFence(vk_device_, &fence_info, nullptr, &submission.fence) != VK_SUCCESS) {
return Fail("could not allocate the OpenXR copy command buffers");
}
VkSemaphoreCreateInfo semaphore_info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
for (VkSemaphore& semaphore : submission.wait_semaphores) {
if (vkCreateSemaphore(vk_device_, &semaphore_info, nullptr, &semaphore) != VK_SUCCESS) {
return Fail("vkCreateSemaphore failed for a copy wait semaphore");
}
}
}
return true;
}
void DestroyVulkanObjects() {
std::lock_guard lock(vk_mutex_);
if (vk_device_ != VK_NULL_HANDLE) {
vkDeviceWaitIdle(vk_device_);
for (Submission& submission : submissions_) {
if (submission.fence != VK_NULL_HANDLE) {
vkDestroyFence(vk_device_, submission.fence, nullptr);
}
for (VkSemaphore semaphore : submission.wait_semaphores) {
if (semaphore != VK_NULL_HANDLE) {
vkDestroySemaphore(vk_device_, semaphore, nullptr);
}
}
submission = {};
}
if (command_pool_ != VK_NULL_HANDLE) {
vkDestroyCommandPool(vk_device_, command_pool_, nullptr);
command_pool_ = VK_NULL_HANDLE;
}
vkDestroyDevice(vk_device_, nullptr);
vk_device_ = VK_NULL_HANDLE;
}
if (vk_instance_ != VK_NULL_HANDLE) {
vkDestroyInstance(vk_instance_, nullptr);
vk_instance_ = VK_NULL_HANDLE;
}
vk_physical_ = VK_NULL_HANDLE;
vk_queue_ = VK_NULL_HANDLE;
queue_family_ = UINT32_MAX;
pfn_import_semaphore_fd_ = nullptr;
pfn_get_semaphore_fd_ = nullptr;
pfn_get_ahb_properties_ = nullptr;
}
// ---- Shared eye buffers ---------------------------------------------------
bool AllocateSlots() {
std::lock_guard lock(vk_mutex_);
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
for (uint32_t slot = 0; slot < kSlotCount; ++slot) {
if (!AllocateSlot(slots_[eye][slot], eye_swapchains_[eye].width, eye_swapchains_[eye].height)) {
return false;
}
}
}
return true;
}
// Aurora's view of a shared buffer: it waits on the slot's last copy-out.
AuroraVulkanStereoTarget SlotTargetLocked(const EyeSlot& slot) const noexcept {
return {
slot.buffer,
slot.width,
slot.height,
static_cast<int64_t>(aurora_format_),
DupFd(slot.pending_acquire_fd),
static_cast<int32_t>(slot.layout),
};
}
// The settings panel's swapchain pair and shared buffers, made the first
// time it opens and kept for the session.
bool EnsurePanelResources() {
if (!EnsurePanelSwapchains()) {
return false;
}
std::lock_guard lock(vk_mutex_);
if (panel_slots_ready_) {
return true;
}
for (EyeSlot& slot : panel_slots_) {
if (!AllocateSlot(slot, kOpenXRPanelLayerWidth, kOpenXRPanelLayerHeight)) {
for (EyeSlot& allocated : panel_slots_) {
DestroySlotLocked(allocated);
}
DestroyPanelSwapchains();
panel_layer_failed_ = true;
Log(OpenXRLogLevel::Warning,
"the settings panel's shared buffers could not be allocated; drawing it into the eyes");
return false;
}
}
panel_slots_ready_ = true;
return true;
}
bool AllocateSlot(EyeSlot& slot, uint32_t width, uint32_t height) {
AHardwareBuffer_Desc desc{};
desc.width = width;
desc.height = height;
desc.layers = 1;
desc.format = CopyFamily(aurora_format_) == 3 ? AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT
: AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
// SAMPLED lets Dawn read (copy source) and FRAMEBUFFER lets it write
// (copy destination / render attachment); both sides transfer.
desc.usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER;
if (AHardwareBuffer_allocate(&desc, &slot.buffer) != 0 || slot.buffer == nullptr) {
return Fail("AHardwareBuffer_allocate failed for an eye buffer");
}
slot.width = width;
slot.height = height;
VkAndroidHardwareBufferFormatPropertiesANDROID format_properties{
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID};
VkAndroidHardwareBufferPropertiesANDROID properties{
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID};
properties.pNext = &format_properties;
if (pfn_get_ahb_properties_(vk_device_, slot.buffer, &properties) != VK_SUCCESS) {
return Fail("vkGetAndroidHardwareBufferPropertiesANDROID failed");
}
if (format_properties.format == VK_FORMAT_UNDEFINED ||
!SameCopyFamily(format_properties.format, aurora_format_)) {
return Fail("the AHardwareBuffer's Vulkan format does not match Aurora's colour format");
}
slot.format = format_properties.format;
VkExternalMemoryImageCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
VkImageCreateInfo image_info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
image_info.pNext = &external;
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.format = slot.format;
image_info.extent = {width, height, 1};
image_info.mipLevels = 1;
image_info.arrayLayers = 1;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (vkCreateImage(vk_device_, &image_info, nullptr, &slot.image) != VK_SUCCESS) {
return Fail("vkCreateImage failed for an imported eye buffer");
}
uint32_t memory_type = UINT32_MAX;
for (uint32_t bit = 0; bit < 32; ++bit) {
if ((properties.memoryTypeBits & (1u << bit)) != 0) {
memory_type = bit;
break;
}
}
if (memory_type == UINT32_MAX) {
return Fail("the AHardwareBuffer reports no usable memory type");
}
VkImportAndroidHardwareBufferInfoANDROID import_info{
VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID};
import_info.buffer = slot.buffer;
VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
dedicated.pNext = &import_info;
dedicated.image = slot.image;
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocate.pNext = &dedicated;
allocate.allocationSize = properties.allocationSize;
allocate.memoryTypeIndex = memory_type;
if (vkAllocateMemory(vk_device_, &allocate, nullptr, &slot.memory) != VK_SUCCESS) {
return Fail("vkAllocateMemory failed while importing an eye buffer");
}
if (vkBindImageMemory(vk_device_, slot.image, slot.memory, 0) != VK_SUCCESS) {
return Fail("vkBindImageMemory failed for an imported eye buffer");
}
VkExportSemaphoreCreateInfo export_info{VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO};
export_info.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
VkSemaphoreCreateInfo exportable{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
exportable.pNext = &export_info;
if (vkCreateSemaphore(vk_device_, &exportable, nullptr, &slot.signal_semaphore) != VK_SUCCESS) {
return Fail("vkCreateSemaphore failed for the exportable eye semaphore");
}
slot.layout = VK_IMAGE_LAYOUT_UNDEFINED;
slot.pending_acquire_fd = -1;
return true;
}
void DestroySlots() {
std::lock_guard lock(vk_mutex_);
DiscardPendingReleasesLocked();
if (vk_device_ != VK_NULL_HANDLE) {
vkDeviceWaitIdle(vk_device_);
}
for (auto& eye : slots_) {
for (EyeSlot& slot : eye) {
DestroySlotLocked(slot);
}
}
for (EyeSlot& slot : panel_slots_) {
DestroySlotLocked(slot);
}
panel_slots_ready_ = false;
}
void DestroySlotLocked(EyeSlot& slot) noexcept {
CloseFd(slot.pending_acquire_fd);
if (vk_device_ != VK_NULL_HANDLE) {
if (slot.signal_semaphore != VK_NULL_HANDLE) {
vkDestroySemaphore(vk_device_, slot.signal_semaphore, nullptr);
}
if (slot.image != VK_NULL_HANDLE) {
vkDestroyImage(vk_device_, slot.image, nullptr);
}
if (slot.memory != VK_NULL_HANDLE) {
vkFreeMemory(vk_device_, slot.memory, nullptr);
}
}
if (slot.buffer != nullptr) {
AHardwareBuffer_release(slot.buffer);
}
slot = {};
}
// ---- The copy into the compositor image -----------------------------------
static void OnAuroraSubmitted(uint64_t token, bool success, bool gpu_work_queued,
const AuroraVulkanStereoRelease* releases, uint32_t release_count,
void* userdata) {
auto* self = static_cast<Impl*>(userdata);
std::array<AuroraVulkanStereoRelease, kMaxCopies> owned{};
for (auto& release : owned) {
release = {-1, VK_IMAGE_LAYOUT_UNDEFINED};
}
for (uint32_t i = 0; i < release_count && i < owned.size(); ++i) {
owned[i] = releases[i];
}
if (self == nullptr) {
for (auto& release : owned) {
CloseFd(release.releaseFenceFd);
}
return;
}
CopyOutcome outcome = CopyOutcome::Skipped;
{
std::lock_guard lock(self->vk_mutex_);
bool expected = false;
{
std::lock_guard submission_lock(self->submission_mutex_);
expected = token == self->awaiting_token_ && token == self->pending_copy_.token;
}
if (expected && success && release_count >= self->pending_copy_.Count()) {
if (self->deferred_copy_) {
// No compositor frame is open yet: keep Dawn's release fences for
// CopyRenderedEyes, which records the copy once the frame is begun.
self->DiscardPendingReleasesLocked();
self->pending_releases_ = owned;
self->have_pending_releases_ = true;
outcome = CopyOutcome::Submitted;
} else {
outcome = self->RecordAndSubmitCopyLocked(owned);
}
} else {
for (auto& release : owned) {
CloseFd(release.releaseFenceFd);
}
// Aurora failing after it queued GPU work may have written the shared buffer
// with no completion marker to wait on; failing before that touched nothing.
if (expected && !success && gpu_work_queued) {
outcome = CopyOutcome::Unsafe;
}
}
if (!expected) {
return;
}
}
{
std::lock_guard lock(self->submission_mutex_);
if (token != self->awaiting_token_) {
return;
}
self->submitted_token_ = token;
self->submission_success_ = outcome == CopyOutcome::Submitted;
self->submission_arrived_ = true;
self->submission_unsafe_ = outcome == CopyOutcome::Unsafe;
}
self->submission_cv_.notify_all();
}
// The slot image `n` of a copy reads: an eye's, or after the eyes the panel's.
EyeSlot& CopySlotLocked(const PendingCopy& copy, uint32_t n) noexcept {
return n < copy.target_count ? slots_[n][copy.slot] : panel_slots_[copy.slot];
}
CopyOutcome RecordAndSubmitCopyLocked(std::array<AuroraVulkanStereoRelease, kMaxCopies>& releases) {
const auto close_releases = [&releases] {
for (auto& release : releases) {
CloseFd(release.releaseFenceFd);
}
};
CopyOutcome acquire_failure = CopyOutcome::Skipped;
Submission* submission = AcquireSubmissionLocked(acquire_failure);
if (submission == nullptr) {
close_releases();
return acquire_failure;
}
const PendingCopy copy = pending_copy_;
VkCommandBuffer cmd = submission->command_buffer;
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
const VkResult begun = vkBeginCommandBuffer(cmd, &begin);
if (begun != VK_SUCCESS) {
close_releases();
Fail(VkFailure("vkBeginCommandBuffer failed for the eye copy", begun));
return CopyOutcome::Skipped;
}
std::array<VkSemaphore, kMaxCopies> waits{};
std::array<VkPipelineStageFlags, kMaxCopies> wait_stages{};
std::array<VkSemaphore, kMaxCopies> signals{};
uint32_t wait_count = 0;
uint32_t signal_count = 0;
constexpr VkImageSubresourceRange kColorRange{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
for (uint32_t n = 0; n < copy.Count(); ++n) {
EyeSlot& slot = CopySlotLocked(copy, n);
AuroraVulkanStereoRelease& release = releases[n];
if (release.releaseFenceFd >= 0) {
const VkSemaphore wait_semaphore = submission->wait_semaphores[n];
VkImportSemaphoreFdInfoKHR import{VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR};
import.semaphore = wait_semaphore;
import.flags = VK_SEMAPHORE_IMPORT_TEMPORARY_BIT;
import.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
import.fd = release.releaseFenceFd;
const VkResult imported = pfn_import_semaphore_fd_(vk_device_, &import);
if (imported == VK_SUCCESS) {
// Ownership of the descriptor moved to Vulkan.
release.releaseFenceFd = -1;
waits[wait_count] = wait_semaphore;
wait_stages[wait_count] = VK_PIPELINE_STAGE_TRANSFER_BIT;
++wait_count;
} else {
close_releases();
vkEndCommandBuffer(cmd);
Fail(VkFailure("vkImportSemaphoreFdKHR rejected Dawn's release fence", imported));
return CopyOutcome::Skipped;
}
}
// Dawn released the buffer with an ownership transfer whose old/new
// layouts it reported; the acquire here must repeat that pair before
// the image can be transitioned for reading.
VkImageLayout released = static_cast<VkImageLayout>(release.releasedImageLayout);
if (released == VK_IMAGE_LAYOUT_UNDEFINED) {
released = VK_IMAGE_LAYOUT_GENERAL;
}
VkImageMemoryBarrier acquire{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
acquire.srcAccessMask = 0;
acquire.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
acquire.oldLayout = released;
acquire.newLayout = released;
acquire.srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
acquire.dstQueueFamilyIndex = queue_family_;
acquire.image = slot.image;
acquire.subresourceRange = kColorRange;
VkImageMemoryBarrier to_source = acquire;
to_source.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
to_source.oldLayout = released;
to_source.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
to_source.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_source.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
VkImageMemoryBarrier to_destination{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
to_destination.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
to_destination.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
to_destination.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
to_destination.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
to_destination.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_destination.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_destination.image = copy.swapchain_images[n];
to_destination.subresourceRange = kColorRange;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0,
nullptr, 0, nullptr, 1, &acquire);
const std::array pre{to_source, to_destination};
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr,
static_cast<uint32_t>(pre.size()), pre.data());
VkImageCopy region{};
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.extent = {slot.width, slot.height, 1};
vkCmdCopyImage(cmd, slot.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, copy.swapchain_images[n],
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
// Back to the layout the compositor expects, and hand the shared
// buffer back to Dawn in GENERAL without a transition inside the
// ownership release so Dawn's acquire can mirror it exactly.
VkImageMemoryBarrier to_attachment = to_destination;
to_attachment.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
to_attachment.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
to_attachment.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
to_attachment.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkImageMemoryBarrier to_general = to_source;
to_general.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
to_general.dstAccessMask = 0;
to_general.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
to_general.newLayout = VK_IMAGE_LAYOUT_GENERAL;
const std::array post{to_attachment, to_general};
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
0, nullptr, 0, nullptr, static_cast<uint32_t>(post.size()), post.data());
VkImageMemoryBarrier release_barrier = acquire;
release_barrier.srcAccessMask = 0;
release_barrier.dstAccessMask = 0;
release_barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
release_barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
release_barrier.srcQueueFamilyIndex = queue_family_;
release_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
nullptr, 0, nullptr, 1, &release_barrier);
slot.layout = VK_IMAGE_LAYOUT_GENERAL;
signals[signal_count++] = slot.signal_semaphore;
}
const VkResult ended = vkEndCommandBuffer(cmd);
if (ended != VK_SUCCESS) {
Fail(VkFailure("vkEndCommandBuffer failed for the eye copy", ended));
return CopyOutcome::Skipped;
}
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.waitSemaphoreCount = wait_count;
submit.pWaitSemaphores = waits.data();
submit.pWaitDstStageMask = wait_stages.data();
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
submit.signalSemaphoreCount = signal_count;
submit.pSignalSemaphores = signals.data();
const VkResult submitted = vkQueueSubmit(vk_queue_, 1, &submit, submission->fence);
if (submitted != VK_SUCCESS) {
Fail(VkFailure("vkQueueSubmit failed for the eye copy", submitted));
// A memory failure leaves every referenced resource untouched, so the frame merely
// has no copy; only a lost device leaves the queue's state unknown.
return submitted == VK_ERROR_DEVICE_LOST ? CopyOutcome::Unsafe : CopyOutcome::Skipped;
}
submission->busy = true;
// Exporting a sync fd from a binary semaphore resets it, so the same
// semaphore serves the next copy. Dawn waits on this before it writes
// the buffer again.
for (uint32_t n = 0; n < copy.Count(); ++n) {
EyeSlot& slot = CopySlotLocked(copy, n);
VkSemaphoreGetFdInfoKHR get_fd{VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR};
get_fd.semaphore = slot.signal_semaphore;
get_fd.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
int fd = -1;
CloseFd(slot.pending_acquire_fd);
if (pfn_get_semaphore_fd_(vk_device_, &get_fd, &fd) == VK_SUCCESS) {
slot.pending_acquire_fd = fd;
} else {
// Without the fence Dawn would write while the copy may still
// read; wait for this submission on the CPU instead.
vkWaitForFences(vk_device_, 1, &submission->fence, VK_TRUE, kFenceTimeoutNanos);
Log(OpenXRLogLevel::Warning, "vkGetSemaphoreFdKHR failed; the eye copy was waited on the CPU");
}
}
return CopyOutcome::Submitted;
}
Submission* AcquireSubmissionLocked(CopyOutcome& failure) {
Submission& submission = submissions_[next_submission_];
next_submission_ = (next_submission_ + 1) % kSubmissionRingSize;
if (submission.busy) {
const VkResult waited = vkWaitForFences(vk_device_, 1, &submission.fence, VK_TRUE, kFenceTimeoutNanos);
if (waited != VK_SUCCESS) {
// An older copy is still outstanding, so the queue's state is unknown.
failure = CopyOutcome::Unsafe;
Fail(VkFailure("a previous eye copy did not complete in time", waited));
return nullptr;
}
submission.busy = false;
}
vkResetFences(vk_device_, 1, &submission.fence);
const VkResult reset = vkResetCommandBuffer(submission.command_buffer, 0);
if (reset != VK_SUCCESS) {
failure = CopyOutcome::Skipped;
Fail(VkFailure("vkResetCommandBuffer failed for the eye copy", reset));
return nullptr;
}
return &submission;
}
// ---- OpenXR swapchains ----------------------------------------------------
bool SelectSwapchainFormat() {
const auto& formats = runtime_->SwapchainFormats();
// Aurora's UNORM target holds gamma-encoded bytes; declaring the sRGB
// sibling makes the compositor decode them instead of treating them as
// linear light. vkCmdCopyImage between UNORM and SRGB siblings is legal.
const VkFormat srgb = SrgbSibling(aurora_format_);
if (srgb != VK_FORMAT_UNDEFINED &&
std::find(formats.begin(), formats.end(), static_cast<int64_t>(srgb)) != formats.end()) {
swapchain_format_ = srgb;
return true;
}
if (std::find(formats.begin(), formats.end(), static_cast<int64_t>(aurora_format_)) != formats.end()) {
swapchain_format_ = aurora_format_;
return true;
}
const auto compatible = std::find_if(formats.begin(), formats.end(), [&](int64_t format) {
return SameCopyFamily(aurora_format_, static_cast<VkFormat>(format));
});
if (compatible == formats.end()) {
return Fail("OpenXR offered no swapchain format copy-compatible with Aurora's Vulkan colour format");
}
swapchain_format_ = static_cast<VkFormat>(*compatible);
return true;
}
bool CreateSwapchains() {
return CreateSwapchainPair(eye_swapchains_) && CreateSwapchainPair(retained_swapchains_);
}
bool CreateSwapchainPair(std::array<EyeSwapchain, kOpenXREyeCount>& pair) {
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
const auto& view = runtime_->ViewConfiguration()[eye];
if (!CreateSwapchain(pair[eye], view.render_width, view.render_height,
eye == 0 ? "left eye" : "right eye")) {
return false;
}
}
return true;
}
bool CreateSwapchain(EyeSwapchain& swapchain, uint32_t width, uint32_t height, const char* what) {
swapchain.width = width;
swapchain.height = height;
XrSwapchainCreateInfo create{XR_TYPE_SWAPCHAIN_CREATE_INFO};
create.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
create.format = static_cast<int64_t>(swapchain_format_);
create.sampleCount = 1;
create.width = swapchain.width;
create.height = swapchain.height;
create.faceCount = 1;
create.arraySize = 1;
create.mipCount = 1;
XrResult result = xrCreateSwapchain(runtime_->Session(), &create, &swapchain.handle);
ObserveResult(result);
if (XR_FAILED(result)) {
std::ostringstream message;
message << "xrCreateSwapchain failed for the Vulkan " << what << " swapchain (" << result << ')';
return Fail(message.str());
}
uint32_t count = 0;
result = xrEnumerateSwapchainImages(swapchain.handle, 0, &count, nullptr);
ObserveResult(result);
if (XR_FAILED(result) || count == 0) {
return Fail("OpenXR returned no Vulkan swapchain images");
}
swapchain.images.resize(count);
for (auto& image : swapchain.images) {
image = {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN2_KHR};
}
result = xrEnumerateSwapchainImages(
swapchain.handle, count, &count,
reinterpret_cast<XrSwapchainImageBaseHeader*>(swapchain.images.data()));
ObserveResult(result);
if (XR_FAILED(result)) {
return Fail("xrEnumerateSwapchainImages failed for a Vulkan eye swapchain");
}
return true;
}
bool AcquireSwapchain(EyeSwapchain& swapchain) {
XrSwapchainImageAcquireInfo acquire{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
XrResult result = xrAcquireSwapchainImage(swapchain.handle, &acquire, &swapchain.acquired_index);
ObserveResult(result);
if (XR_FAILED(result)) {
return Fail("xrAcquireSwapchainImage failed for a Vulkan eye swapchain");
}
swapchain.acquired = true;
swapchain.waited = false;
swapchain.release_forbidden = false;
XrSwapchainImageWaitInfo wait{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
wait.timeout = XR_INFINITE_DURATION;
result = xrWaitSwapchainImage(swapchain.handle, &wait);
ObserveResult(result);
if (result == XR_TIMEOUT_EXPIRED) {
return Fail("xrWaitSwapchainImage unexpectedly timed out for a Vulkan eye swapchain");
}
if (XR_FAILED(result)) {
return Fail("xrWaitSwapchainImage failed for a Vulkan eye swapchain");
}
swapchain.waited = true;
if (swapchain.acquired_index >= swapchain.images.size()) {
return Fail("OpenXR returned an out-of-range Vulkan swapchain image index");
}
return true;
}
bool ReleaseAcquiredSwapchains() {
bool success = true;
for (auto& swapchain : eye_swapchains_) {
success = ReleaseSwapchain(swapchain) && success;
}
return ReleaseSwapchain(panel_swapchain_) && success;
}
bool ReleaseSwapchain(EyeSwapchain& swapchain) {
if (!swapchain.acquired || swapchain.handle == XR_NULL_HANDLE) {
return true;
}
if (!swapchain.waited) {
// OpenXR only permits release after a successful wait. Keep the
// image acquired and let session teardown destroy the child.
Log(OpenXRLogLevel::Warning, "cannot release an OpenXR Vulkan image whose wait did not complete");
return false;
}
if (swapchain.release_forbidden) {
// Aurora reported a failed submission after it may already have
// queued GPU work. Without a trustworthy fence the release could race
// that work, so leave the image acquired for xrDestroySession.
Log(OpenXRLogLevel::Warning, "deferring an OpenXR Vulkan image after an unsafe GPU submission");
return false;
}
XrSwapchainImageReleaseInfo release{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
const XrResult result = xrReleaseSwapchainImage(swapchain.handle, &release);
ObserveResult(result);
if (XR_FAILED(result)) {
return Fail("xrReleaseSwapchainImage failed for an Vulkan swapchain");
}
swapchain.acquired = false;
swapchain.waited = false;
return true;
}
void AbandonAcquiredSwapchains() noexcept {
for (auto* swapchain : {&eye_swapchains_[0], &eye_swapchains_[1], &panel_swapchain_}) {
if (swapchain->acquired) {
swapchain->release_forbidden = true;
}
}
}
void AllowAcquiredSwapchainsAfterGpuDrain() noexcept {
for (auto* swapchain : {&eye_swapchains_[0], &eye_swapchains_[1], &panel_swapchain_}) {
if (swapchain->acquired) {
swapchain->release_forbidden = false;
}
}
}
// The settings panel's swapchain pair, made the first time the panel opens.
// A failure is logged once and the panel is drawn into the eyes again.
bool EnsurePanelSwapchains() {
if (panel_swapchains_ready_) {
return true;
}
if (panel_layer_failed_) {
return false;
}
if (CreateSwapchain(panel_swapchain_, kOpenXRPanelLayerWidth, kOpenXRPanelLayerHeight, "settings panel") &&
CreateSwapchain(retained_panel_swapchain_, kOpenXRPanelLayerWidth, kOpenXRPanelLayerHeight,
"settings panel")) {
panel_swapchains_ready_ = true;
Log(OpenXRLogLevel::Info, "OpenXR settings panel layer ready");
return true;
}
DestroyPanelSwapchains();
panel_layer_failed_ = true;
Log(OpenXRLogLevel::Warning, "the settings panel could not get its own OpenXR layer; drawing it into the eyes");
return false;
}
void DestroyPanelSwapchains() {
for (auto* swapchain : {&panel_swapchain_, &retained_panel_swapchain_}) {
if (swapchain->handle != XR_NULL_HANDLE && !swapchain->acquired) {
xrDestroySwapchain(swapchain->handle);
} else if (swapchain->acquired) {
Log(OpenXRLogLevel::Warning,
"Vulkan panel swapchain still owns an acquired image; deferring its destruction to xrDestroySession");
}
*swapchain = {};
}
panel_swapchains_ready_ = false;
retained_panel_valid_ = false;
}
void DestroySwapchains() {
DestroyPanelSwapchains();
DestroySwapchainPair(eye_swapchains_);
DestroySwapchainPair(retained_swapchains_);
have_retained_frame_ = false;
retained_frame_ = {};
swapchain_format_ = VK_FORMAT_UNDEFINED;
}
void DestroySwapchainPair(std::array<EyeSwapchain, kOpenXREyeCount>& pair) {
for (auto& swapchain : pair) {
if (swapchain.handle != XR_NULL_HANDLE && !swapchain.acquired) {
xrDestroySwapchain(swapchain.handle);
} else if (swapchain.acquired) {
Log(OpenXRLogLevel::Warning,
"Vulkan swapchain still owns an acquired image; deferring its destruction to xrDestroySession");
}
swapchain = {};
}
}
void EndActiveFrameWithoutLayers(const OpenXRFrame& frame) {
if (runtime_ != nullptr && runtime_->IsSessionRunning()) {
runtime_->EndFrameWithoutLayers(frame);
}
frame_active_ = false;
active_frame_serial_ = 0;
active_frame_ = {};
}
void ObserveResult(XrResult result) noexcept {
if (runtime_ != nullptr) {
runtime_->ObserveResult(result);
}
}
bool Fail(std::string message) {
last_error_ = std::move(message);
Log(OpenXRLogLevel::Error, last_error_);
return false;
}
void ClearError() { last_error_.clear(); }
void Log(OpenXRLogLevel level, std::string_view message) const noexcept {
if (!logger_) {
return;
}
try {
logger_(level, message);
} catch (...) {
}
}
OpenXRRuntime* runtime_ = nullptr;
OpenXRLogCallback logger_;
// The room around the virtual screen, started and paused as each presentation arrives.
OpenXRPassthrough passthrough_{logger_};
OpenXRVulkanGraphicsRequirements requirements_{};
std::array<EyeSwapchain, kOpenXREyeCount> eye_swapchains_{};
std::array<EyeSwapchain, kOpenXREyeCount> retained_swapchains_{};
// The settings panel's layer: written like the eyes into panel_swapchain_,
// shown from retained_panel_swapchain_ (see FinishFrame).
EyeSwapchain panel_swapchain_{};
EyeSwapchain retained_panel_swapchain_{};
bool panel_swapchains_ready_ = false;
bool panel_layer_failed_ = false;
// The retained frame rendered the panel's image into retained_panel_swapchain_.
bool retained_panel_valid_ = false;
OpenXRBackendFrame retained_frame_{};
uint64_t retained_session_serial_ = 0;
uint64_t retained_space_serial_ = 0;
bool have_retained_frame_ = false;
VkFormat aurora_format_ = VK_FORMAT_UNDEFINED;
VkFormat swapchain_format_ = VK_FORMAT_UNDEFINED;
std::string last_error_;
// Vulkan objects owned here, guarded by vk_mutex_ because Aurora's worker
// records the copy while the pacing thread prepares the next frame.
std::mutex vk_mutex_;
VkInstance vk_instance_ = VK_NULL_HANDLE;
VkPhysicalDevice vk_physical_ = VK_NULL_HANDLE;
VkDevice vk_device_ = VK_NULL_HANDLE;
VkQueue vk_queue_ = VK_NULL_HANDLE;
uint32_t queue_family_ = UINT32_MAX;
VkCommandPool command_pool_ = VK_NULL_HANDLE;
PFN_vkImportSemaphoreFdKHR pfn_import_semaphore_fd_ = nullptr;
PFN_vkGetSemaphoreFdKHR pfn_get_semaphore_fd_ = nullptr;
PFN_vkGetAndroidHardwareBufferPropertiesANDROID pfn_get_ahb_properties_ = nullptr;
std::array<std::array<EyeSlot, kSlotCount>, kOpenXREyeCount> slots_{};
// The settings panel's shared buffers, allocated when it first opens.
std::array<EyeSlot, kSlotCount> panel_slots_{};
bool panel_slots_ready_ = false;
std::array<Submission, kSubmissionRingSize> submissions_{};
uint32_t next_submission_ = 0;
uint32_t next_slot_ = 0;
PendingCopy pending_copy_{};
// Render-first pacing (PreparePacket): Aurora's release fences arrive while no compositor
// frame is active, so the copy is recorded later by CopyRenderedEyes.
bool deferred_copy_ = false;
std::array<AuroraVulkanStereoRelease, kMaxCopies> pending_releases_{};
bool have_pending_releases_ = false;
uint64_t next_packet_serial_ = 1ull << 40; // never collides with the runtime's frame serials
XrTime last_display_time_ = 0;
XrDuration last_display_period_ = 0;
bool last_should_render_ = false;
std::mutex submission_mutex_;
std::condition_variable submission_cv_;
uint64_t awaiting_token_ = 0;
uint64_t submitted_token_ = 0;
bool submission_arrived_ = false;
bool submission_success_ = false;
bool submission_unsafe_ = false;
bool shutting_down_ = false;
uint64_t active_frame_serial_ = 0;
uint64_t render_session_serial_ = 0;
uint64_t render_space_serial_ = 0;
OpenXRFrame active_frame_{};
bool requirements_queried_ = false;
bool owns_session_ = false;
bool bridge_enabled_ = false;
bool bound_ = false;
bool frame_active_ = false;
bool shutdown_unsafe_ = false;
};
OpenXRVulkanBackend::OpenXRVulkanBackend(OpenXRLogCallback logger)
: m_impl(std::make_unique<Impl>(std::move(logger))) {}
OpenXRVulkanBackend::~OpenXRVulkanBackend() = default;
bool OpenXRVulkanBackend::QueryGraphicsRequirements(OpenXRRuntime& runtime) {
return m_impl->QueryGraphicsRequirements(runtime);
}
bool OpenXRVulkanBackend::BindAurora(OpenXRRuntime& runtime) {
return m_impl->BindAurora(runtime);
}
OpenXRBeginStatus OpenXRVulkanBackend::BeginFrame(const OpenXRPresentation& presentation,
OpenXRBackendFrame& frame) {
return m_impl->BeginFrame(presentation, frame);
}
OpenXRSubmissionStatus OpenXRVulkanBackend::WaitForSubmission(const OpenXRBackendFrame& frame,
uint32_t timeout_ms) {
return m_impl->WaitForSubmission(frame, timeout_ms);
}
OpenXRBeginStatus OpenXRVulkanBackend::PreparePacket(const OpenXRPresentation& presentation,
OpenXRBackendFrame& packet) {
return m_impl->PreparePacket(presentation, packet);
}
bool OpenXRVulkanBackend::TryCancelPendingPacket(OpenXRBackendFrame& packet) {
return m_impl->TryCancelPendingPacket(packet);
}
OpenXRBeginStatus OpenXRVulkanBackend::BeginFrameForPacket(const OpenXRBackendFrame& packet,
OpenXRBackendFrame& frame) {
return m_impl->BeginFrameForPacket(packet, frame);
}
OpenXRSubmissionStatus OpenXRVulkanBackend::CopyRenderedEyes(const OpenXRBackendFrame& frame) {
return m_impl->CopyRenderedEyes(frame);
}
OpenXRBeginStatus OpenXRVulkanBackend::KeepAliveCycle() { return m_impl->KeepAliveCycle(); }
bool OpenXRVulkanBackend::TryCancelPendingFrame(OpenXRBackendFrame& frame) {
return m_impl->TryCancelPendingFrame(frame);
}
bool OpenXRVulkanBackend::FinishFrame(OpenXRBackendFrame& frame, bool submit_layer) {
return m_impl->FinishFrame(frame, submit_layer);
}
bool OpenXRVulkanBackend::RepeatFrame(const OpenXRBackendFrame& frame) {
return m_impl->RepeatFrame(frame);
}
bool OpenXRVulkanBackend::Shutdown() { return m_impl->Shutdown(); }
bool OpenXRVulkanBackend::IsBound() const { return m_impl->IsBound(); }
bool OpenXRVulkanBackend::PanelLayerAvailable() const { return m_impl->PanelLayerAvailable(); }
const OpenXRVulkanGraphicsRequirements& OpenXRVulkanBackend::GraphicsRequirements() const {
return m_impl->GraphicsRequirements();
}
int64_t OpenXRVulkanBackend::SwapchainFormat() const { return m_impl->SwapchainFormat(); }
const std::string& OpenXRVulkanBackend::LastError() const { return m_impl->LastError(); }
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)