Added Android Meta Quest Support

This commit is contained in:
iChris4 committed 2026-09-16 22:06:34 +02:00
1 parent 49b8602543
commit e52227baf5
67 files changed
+5662 -176

No files matched your search

+24
View File
@@ -2,6 +2,7 @@
#include "settings_overlay.h"
#include "runtime_config.h"
#include "fiber_manager.h"
#include <aurora/aurora.h>
#include <aurora/event.h>
@@ -136,7 +137,30 @@ inline bool BeginAuroraFrame() {
return true;
}
#if defined(__ANDROID__)
inline std::atomic_bool g_androidAuroraPollPending{false};
#endif
// Poll Aurora events and update cached window/framebuffer dimensions.
inline void UpdateAuroraAndProcessEvents() {
#if defined(__ANDROID__)
// aurora_update() pumps SDL, which can call into Java; ART only tolerates
// that on the thread's real stack. A guest thread on its own fiber stack
// leaves the poll for the scheduler to service after the next switch.
if (!Fiber::GuestFiberManager::IsOnSchedulerFiber()) {
g_androidAuroraPollPending.store(true, std::memory_order_release);
return;
}
g_androidAuroraPollPending.store(false, std::memory_order_release);
#endif
ProcessAuroraEvents(aurora_update());
}
inline void ServicePendingAuroraEventsOnScheduler() {
#if defined(__ANDROID__)
if (Fiber::GuestFiberManager::IsOnSchedulerFiber() &&
g_androidAuroraPollPending.exchange(false, std::memory_order_acq_rel)) {
ProcessAuroraEvents(aurora_update());
}
#endif
}
+5
View File
@@ -59,6 +59,11 @@ public:
// Check if fiber system is initialized
static bool IsInitialized();
// True when the caller runs on the scheduler's own host context, i.e. the
// original thread stack (also true before the fiber system exists). Code
// that may enter Java through JNI on Android must only run there.
static bool IsOnSchedulerFiber();
// Create a fiber for a guest thread (called from OSCreateThread HLE).
// OSCreateThread's stackSize/priority are not passed: the host fiber models
+13
View File
@@ -15,6 +15,8 @@
#ifdef _WIN32
#include <windows.h>
#else
#include <cerrno>
#include <cstdio>
#include <unistd.h>
#endif
@@ -202,6 +204,17 @@ inline bool Ensure(const std::filesystem::path& root, std::string& error,
published = MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH) != 0;
#else
published = ::link(temporary.c_str(), path.c_str()) == 0;
#if defined(__ANDROID__)
// Android's shared storage (the FUSE-backed Android/data tree) has no
// hard links; on a Quest 3 link() fails with EACCES, not EPERM, so the
// errno is not a usable signal. The no-replace publish exists for
// competing desktop launchers; an Android app runs one process, so an
// existence check followed by an atomic rename keeps the "never
// overwrite" guarantee. A real permission problem fails the rename too.
if (!published && !std::filesystem::exists(path)) {
published = ::rename(temporary.c_str(), path.c_str()) == 0;
}
#endif
#endif
}
std::filesystem::remove(temporary, ec);
+8 -3
View File
@@ -131,7 +131,9 @@ inline std::filesystem::path PathFromUtf8(std::string_view text) {
}
inline constexpr const char* kConfigFileName = "Config.toml";
#ifdef _WIN32
#if defined(_WIN32) || defined(__ANDROID__)
// The Quest activity (android/.../QuestActivity.kt) creates this directory
// under the app's external files dir and writes Config.toml into it.
inline constexpr const char* kApplicationDirectoryName = "WiiCompiledOpenXRVR";
#else
inline constexpr const char* kApplicationDirectoryName = "WiiCompiled";
@@ -269,7 +271,10 @@ inline std::optional<std::filesystem::path> ExecutableDirectory() {
}
buffer.resize(buffer.size() * 2);
}
#elif defined(__APPLE__)
#elif defined(__APPLE__) || defined(__ANDROID__)
// Android has no executable next to which assets could sit; the platform
// layer answers with the directory the activity unpacked the bundled
// runtime resources into, so the adjacent-file lookups below keep working.
return RuntimePlatform::ExecutableDirectory();
#else
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink()
@@ -328,7 +333,7 @@ inline std::filesystem::path ApplicationDataDirectory() {
CoTaskMemFree(rawPath);
return directory;
}
#elif defined(__APPLE__)
#elif defined(__APPLE__) || defined(__ANDROID__)
return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName);
#else
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
+41
View File
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#if defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
#include "vr/openxr_runtime.h"
#include <string>
namespace mkw::vr {
// Android-specific OpenXR bootstrap.
//
// The Khronos loader on Android must be told which JavaVM and Context it lives
// in before any other OpenXR entry point is called (xrInitializeLoaderKHR), and
// xrCreateInstance must chain an XrInstanceCreateInfoAndroidKHR naming the
// activity. Both come from SDL's Android glue (the SDLActivity that hosts the
// runtime), so no extra JNI surface is needed. DolphinXR found that the loader
// context must be the *activity*, not the application context: with a plain
// application context the Quest runtime never leaves XR_SESSION_STATE_IDLE.
//
// Call order: OpenXRAndroidInitializeLoader() once, before OpenXRRuntime::Initialize;
// then pass OpenXRAndroidInstanceCreateNext() as OpenXRConfig::instance_create_next.
bool OpenXRAndroidInitializeLoader(OpenXRLogCallback logger, std::string* error);
const void* OpenXRAndroidInstanceCreateNext();
// Optional XR_KHR_android_thread_settings hint for the calling thread. Failure
// is not an error: some Quest runtime builds advertise the extension but reject
// particular thread types, so the caller only logs the outcome.
enum class OpenXRAndroidThreadType {
ApplicationMain,
ApplicationWorker,
RendererMain,
RendererWorker,
};
bool OpenXRAndroidRegisterThread(OpenXRRuntime& runtime, OpenXRAndroidThreadType type);
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
+66
View File
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#if defined(MKW_ENABLE_OPENXR)
#include "vr/openxr_runtime.h"
#include <array>
#include <cstdint>
namespace mkw::vr {
// Backend-neutral frame vocabulary shared by every graphics binding.
//
// The OpenXR pacing thread in openxr_integration.cpp is written against these
// types and the method surface documented on OpenXRD3D12Backend; each concrete
// backend (D3D12 on Windows, Vulkan on Android) implements that same surface so
// the pacing, retained-layer and policy logic is compiled once for both.
enum class OpenXRFrameMode {
ImmersiveProjection,
VirtualScreen,
};
enum class OpenXRBeginStatus {
Ready,
SessionNotRunning,
ExitRequested,
Error,
};
enum class OpenXRSubmissionStatus {
Success,
Failed,
Timeout,
ShuttingDown,
};
struct OpenXRPresentation {
OpenXRFrameMode mode = OpenXRFrameMode::ImmersiveProjection;
// Used only by VirtualScreen.
float quad_distance_meters = 2.0f;
float quad_width_meters = 2.4f;
// When quad_anchored is set, the quad is placed at quad_pose in the
// application reference space and stays put as the player looks around.
// Otherwise it falls back to being head-locked in XR_VIEW_SPACE, centered
// straight ahead at -Z, which is what happens until tracking has produced a
// head pose good enough to anchor against.
bool quad_anchored = false;
XrPosef quad_pose{{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}};
};
struct OpenXRBackendFrame {
OpenXRFrame xr_frame;
OpenXRPresentation presentation;
std::array<uint32_t, kOpenXREyeCount> render_width{};
std::array<uint32_t, kOpenXREyeCount> render_height{};
bool expects_gpu_submission = false;
};
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR)
+4
View File
@@ -46,6 +46,10 @@ struct OpenXRConfig {
// Destruction never waits indefinitely for a runtime to acknowledge an exit.
uint32_t shutdown_timeout_ms = 500;
// Platform structure chained into XrInstanceCreateInfo::next (Android needs
// XrInstanceCreateInfoAndroidKHR). Must outlive Initialize(); null elsewhere.
const void* instance_create_next = nullptr;
};
} // namespace mkw::vr
+9 -42
View File
@@ -4,6 +4,7 @@
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#include "vr/openxr_backend.h"
#include "vr/openxr_runtime.h"
#include <array>
@@ -13,24 +14,14 @@
namespace mkw::vr {
enum class OpenXRD3D12FrameMode {
ImmersiveProjection,
VirtualScreen,
};
enum class OpenXRD3D12BeginStatus {
Ready,
SessionNotRunning,
ExitRequested,
Error,
};
enum class OpenXRD3D12SubmissionStatus {
Success,
Failed,
Timeout,
ShuttingDown,
};
// The frame vocabulary is shared with the Vulkan backend (vr/openxr_backend.h).
// These aliases keep the D3D12 spelling that the replay tests and the original
// integration code were written against.
using OpenXRD3D12FrameMode = OpenXRFrameMode;
using OpenXRD3D12BeginStatus = OpenXRBeginStatus;
using OpenXRD3D12SubmissionStatus = OpenXRSubmissionStatus;
using OpenXRD3D12Presentation = OpenXRPresentation;
using OpenXRD3D12Frame = OpenXRBackendFrame;
struct OpenXRD3D12GraphicsRequirements {
uint32_t adapter_luid_low = 0;
@@ -38,30 +29,6 @@ struct OpenXRD3D12GraphicsRequirements {
uint32_t minimum_feature_level = 0;
};
struct OpenXRD3D12Presentation {
OpenXRD3D12FrameMode mode = OpenXRD3D12FrameMode::ImmersiveProjection;
// Used only by VirtualScreen.
float quad_distance_meters = 2.0f;
float quad_width_meters = 2.4f;
// When quad_anchored is set, the quad is placed at quad_pose in the
// application reference space and stays put as the player looks around.
// Otherwise it falls back to being head-locked in XR_VIEW_SPACE, centered
// straight ahead at -Z, which is what happens until tracking has produced a
// head pose good enough to anchor against.
bool quad_anchored = false;
XrPosef quad_pose{{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}};
};
struct OpenXRD3D12Frame {
OpenXRFrame xr_frame;
OpenXRD3D12Presentation presentation;
std::array<uint32_t, kOpenXREyeCount> render_width{};
std::array<uint32_t, kOpenXREyeCount> render_height{};
bool expects_gpu_submission = false;
};
// Same-device Dawn/OpenXR D3D12 backend.
//
// Startup is deliberately split in two. QueryGraphicsRequirements() runs
+94
View File
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#if defined(MKW_ENABLE_OPENXR)
#include "vr/openxr_runtime.h"
#include <cstdint>
#include <string>
namespace mkw::vr {
// OpenXR action-based controller input, surfaced to the rest of the runtime as
// one ordinary SDL gamepad.
//
// Quest Touch controllers are not visible to SDL's joystick layer (the OS does
// not expose them as HID gamepads), so a standalone headset build would have no
// input at all. Rather than adding a second input path through the PAD/WPAD
// HLE, this module syncs an OpenXR action set on the pacing thread and feeds a
// virtual SDL joystick (SDL_AttachVirtualJoystick) that Aurora's existing
// controller code opens, maps and assigns to player 1 exactly like a physical
// pad. Every binding the settings overlay already offers keeps working.
//
// Mapping (Oculus Touch profile; the same actions are also bound for
// khr/simple_controller so an unknown runtime still gets A/menu):
// right A / B -> gamepad South / East (GameCube A / B)
// left X / Y -> gamepad West / North (GameCube X / Y)
// index triggers -> left / right trigger axes
// grip squeezes -> left / right shoulder buttons
// left / right thumbstick-> left / right stick axes, clicks -> stick buttons
// left menu -> Start
//
// Lifetime: OpenXRInputCreate after the session exists (attaches the action
// set, which OpenXR permits once per session), OpenXRInputSync once per
// xrWaitFrame when the session is focused, OpenXRInputDestroy before the
// session is destroyed. All three run on the XR pacing thread; SDL's virtual
// joystick setters are internally locked, so the game thread may read the pad
// concurrently.
class OpenXRInput final {
public:
explicit OpenXRInput(OpenXRLogCallback logger = {});
~OpenXRInput();
OpenXRInput(const OpenXRInput&) = delete;
OpenXRInput& operator=(const OpenXRInput&) = delete;
// Creates the action set/actions/spaces and attaches them to the session.
// Returns false (with LastError set) if the runtime rejects the action set;
// the caller continues without controller input rather than failing VR.
bool Create(OpenXRRuntime& runtime);
void Destroy();
// xrSyncActions + state reads, then publishes to the virtual gamepad.
// predicted_display_time is the frame's XrTime for pose-based lookups.
void Sync(XrTime predicted_display_time);
// Rumble for the given hand (0 = left, 1 = right); amplitude 0..1.
void ApplyHaptic(uint32_t hand, float amplitude, XrDuration duration);
bool IsCreated() const noexcept { return m_created; }
bool HasVirtualGamepad() const noexcept { return m_joystick_id != 0; }
const std::string& LastError() const noexcept { return m_last_error; }
private:
bool CreateActions();
bool SuggestBindings();
bool AttachVirtualGamepad();
void DetachVirtualGamepad();
bool Check(XrResult result, const char* operation);
void Log(OpenXRLogLevel level, const std::string& message) const noexcept;
OpenXRLogCallback m_logger;
OpenXRRuntime* m_runtime = nullptr;
XrActionSet m_action_set = XR_NULL_HANDLE;
XrAction m_thumbstick = XR_NULL_HANDLE;
XrAction m_thumbstick_click = XR_NULL_HANDLE;
XrAction m_trigger = XR_NULL_HANDLE;
XrAction m_squeeze = XR_NULL_HANDLE;
XrAction m_button_primary = XR_NULL_HANDLE; // A / X
XrAction m_button_secondary = XR_NULL_HANDLE; // B / Y
XrAction m_menu = XR_NULL_HANDLE;
XrAction m_haptic = XR_NULL_HANDLE;
XrPath m_hand_paths[2]{};
uint32_t m_joystick_id = 0; // SDL_JoystickID; 0 when detached
void* m_joystick = nullptr; // SDL_Joystick*
bool m_created = false;
bool m_logged_sync_failure = false;
std::string m_last_error;
};
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR)
+78
View File
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#if defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
#include "vr/openxr_backend.h"
#include "vr/openxr_runtime.h"
#include <cstdint>
#include <memory>
#include <string>
namespace mkw::vr {
struct OpenXRVulkanGraphicsRequirements {
// XR_MAKE_VERSION-encoded bounds reported by xrGetVulkanGraphicsRequirements2KHR.
XrVersion min_api_version = 0;
XrVersion max_api_version = 0;
// Which binding extension was negotiated: XR_KHR_vulkan_enable2 when the
// runtime offers it, otherwise the original XR_KHR_vulkan_enable.
bool uses_enable2 = false;
};
// Two-device Dawn/OpenXR Vulkan backend for Android (Meta Quest).
//
// Method surface and threading rules are identical to OpenXRD3D12Backend so the
// pacing thread in openxr_integration.cpp is shared. The difference is behind
// BindAurora: instead of binding Dawn's device to the session, this backend
// creates its own VkInstance/VkDevice through the OpenXR runtime, owns a small
// ring of AHardwareBuffers per eye, and hands them to Aurora's Vulkan stereo
// bridge (aurora/vulkan_interop.h). When Aurora reports a completed copy, the
// backend copies the buffer into the acquired XrSwapchain image on the
// session's queue, so the compositor sees ordinary same-queue rendering.
//
// QueryGraphicsRequirements() runs after OpenXRRuntime::Initialize() and before
// aurora_initialize(); BindAurora() runs afterwards, while Aurora's frame worker
// is idle. Everything from BeginFrame() through FinishFrame() belongs to one XR
// pacing thread; the Aurora callback only records a small copy on this
// backend's private queue and publishes a token.
class OpenXRVulkanBackend final {
public:
explicit OpenXRVulkanBackend(OpenXRLogCallback logger = {});
~OpenXRVulkanBackend();
OpenXRVulkanBackend(const OpenXRVulkanBackend&) = delete;
OpenXRVulkanBackend& operator=(const OpenXRVulkanBackend&) = delete;
OpenXRVulkanBackend(OpenXRVulkanBackend&&) = delete;
OpenXRVulkanBackend& operator=(OpenXRVulkanBackend&&) = delete;
bool QueryGraphicsRequirements(OpenXRRuntime& runtime);
bool BindAurora(OpenXRRuntime& runtime);
OpenXRBeginStatus BeginFrame(const OpenXRPresentation& presentation, OpenXRBackendFrame& frame);
OpenXRSubmissionStatus WaitForSubmission(const OpenXRBackendFrame& frame,
uint32_t timeout_ms = UINT32_MAX);
bool TryCancelPendingFrame(OpenXRBackendFrame& frame);
bool RepeatFrame(const OpenXRBackendFrame& frame);
bool FinishFrame(OpenXRBackendFrame& frame, bool submit_layer);
// Drains this backend's own queue before tearing down. Returns false only
// when the private device could not be waited on, in which case the caller
// retains the backend and runtime for the process lifetime.
bool Shutdown();
bool IsBound() const;
const OpenXRVulkanGraphicsRequirements& GraphicsRequirements() const;
int64_t SwapchainFormat() const;
const std::string& LastError() const;
private:
class Impl;
std::unique_ptr<Impl> m_impl;
};
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)