Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
789f212
Add Windows VR support
TheReal-Flo Aug 22, 2026
c067030
Improve headset VR launch support
TheReal-Flo Aug 22, 2026
c8b58c3
Speed up VR frame production: optimal-tiling render targets, buffered…
Aug 22, 2026
a3a3a09
Skip the Windows-only XR payload scripts on non-Windows hosts
Aug 22, 2026
914bc7f
Keep the VR control connection alive: drop the per-connection request…
Aug 22, 2026
336c60a
Collapse per-frame control traffic into one FRAME_SYNC round trip
Aug 22, 2026
9b3f002
Log the consumer buffer path and swapchain sample count
Aug 22, 2026
566393e
Relay device: GPU dma-buf to AHardwareBuffer copies when the game dev…
Aug 22, 2026
7d068a8
Try every relay driver candidate until one has the needed extensions;…
Aug 22, 2026
2fac1a0
Reach past the rootfs Mesa: absolute system loader and adrenotools tu…
Aug 22, 2026
106a1a8
Open adrenotools-packaged drivers through the Android HAL module inte…
Aug 22, 2026
16d590c
Ship frames from a worker thread so xrEndFrame stops draining the GPU…
Aug 22, 2026
3a13461
Route the control plane through the unixbridge: winsock's 40ms winese…
Aug 22, 2026
568b9e4
Keep the unix fast path retrying through transient refusals; raise th…
Aug 22, 2026
3961d5d
Block image reuse until the relay copy has executed
Aug 22, 2026
eef0e1f
Quad-buffer the eye swapchains so acquire stops stalling the frame loop
Aug 22, 2026
b4a3039
Strip frame-phase instrumentation; raise render scale to 0.85
Aug 23, 2026
632a55c
Per-container VR render scale, default native
Aug 23, 2026
55bd729
Render scale as a slider (25-100%, 5% steps)
Aug 23, 2026
4bd1aa2
Log the EGL error and extension support when dma-buf import fails
Aug 23, 2026
6c92c47
One-command payload build for macOS
Aug 27, 2026
db3b05d
Render into compressed (UBWC) targets when the relay carries the frames
Aug 27, 2026
d9b3885
Merge remote-tracking branch 'origin/vr-support-perf' into vr-support…
Sep 3, 2026
5c2f1ee
Make Windows VR opt-in per container and fail soft when the payload i…
Sep 3, 2026
65f1e52
Expose the OpenComposite toggle in container graphics settings
Sep 3, 2026
161bb7f
Add translations for the VR mode and OpenComposite settings
Sep 3, 2026
65ab7d9
Merge remote-tracking branch 'origin/master' into vr-support-perf-merge
Sep 3, 2026
2bc6cb1
Move the VR mode toggle next to the immersive launch toggle on the ga…
Sep 3, 2026
afe8db6
Keep the game in the foreground when the launcher pauses behind the i…
Sep 3, 2026
51abcd8
Ship the Windows VR payload in the modernXr assets
Sep 3, 2026
c21206d
Ship libxrimmersive.so built from the branch and keep the modernXr jn…
Sep 3, 2026
65dfc7d
Ship the VR payload and rebuilt libxrimmersive.so in legacyXr too
Sep 3, 2026
4d8b309
Revert "Render into compressed (UBWC) targets when the relay carries …
Sep 3, 2026
a3e2704
Rebuild the VR payload without compressed render targets
Sep 3, 2026
4d4dc75
Address review findings: harden the immersive compositor transport, s…
Sep 3, 2026
4a7c05c
Follow-up review fixes: submit overlays during stereo, serialize cont…
Sep 3, 2026
4402fc9
Merge remote-tracking branch 'origin/master' into vr-support-perf-merge
Sep 4, 2026
8ab4ca7
Track Windows VR in LOCAL_FLOOR so recentering follows the headset, s…
Sep 4, 2026
96e8be1
Send OpenXR thumbstick Y with its native sign to the Windows runtime …
Sep 4, 2026
99bcd3a
Forward headset recenter events to the Windows runtime, level the emu…
Sep 4, 2026
5d6cac8
Skip non-x64 openvr_api.dll helpers when installing OpenComposite and…
Sep 4, 2026
25e13ea
Pack controller buttons per hand for the Windows runtime, make the im…
Sep 4, 2026
3e28a09
Count only tracking-space recenters, drop the runtime pose-jump heuri…
Sep 4, 2026
6dfef2b
Merge remote-tracking branch 'origin/master' into vr-support-perf-merge
Sep 4, 2026
ede1736
Add translations for the immersive quick menu's Windows VR strings
Sep 4, 2026
e19802d
Add a bounded per-session frame trace to the Windows OpenXR runtime a…
Sep 4, 2026
553cdb0
Pin OpenComposite to upstream a27e7e6a, accept flipped eye rectangles…
Sep 4, 2026
862da24
Reset the head-velocity history when a headset recenter serial arrives
Sep 4, 2026
91013ec
Bump the unixlib ABI version and mirror flip_y in the single-image su…
Sep 4, 2026
656fef4
Restore shared payload recovery records only when they name the expec…
Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ android {
srcDirs("src/modern/assets", "src/main/assets")
}
jniLibs {
srcDirs("src/modern/jniLibs")
setSrcDirs(listOf("src/modern/jniLibs", "src/modernXr/jniLibs"))
}
}
getByName("debug") {
Expand All @@ -283,6 +283,76 @@ android {
ignoreFormatFailures = false
}

val hostCanRunXrPayloadScripts = System.getProperty("os.name").startsWith("Windows")

tasks.register<Exec>("buildModernXrNative") {
enabled = hostCanRunXrPayloadScripts
commandLine(
"powershell",
"-ExecutionPolicy",
"Bypass",
"-File",
rootProject.file("tools/build-xr-native.ps1").absolutePath,
)
}

tasks.register<Exec>("buildWindowsXrRuntime") {
enabled = hostCanRunXrPayloadScripts
commandLine(
"powershell",
"-ExecutionPolicy",
"Bypass",
"-File",
rootProject.file("tools/build-windows-xr-runtime.ps1").absolutePath,
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
)
}

tasks.register<Exec>("stageWineXrBridge") {
enabled = hostCanRunXrPayloadScripts
dependsOn("buildModernXrNative")
val companion = providers.environmentVariable("GAMENATIVE_WINE_XR_BRIDGE")
doFirst {
check(companion.isPresent) { "GAMENATIVE_WINE_XR_BRIDGE must point to the ARM64X Wine builtin companion" }
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}
commandLine(
"powershell",
"-ExecutionPolicy",
"Bypass",
"-File",
rootProject.file("tools/stage-wine-xr-bridge.ps1").absolutePath,
"-CompanionPath",
companion.getOrElse(""),
)
}

tasks.register<Exec>("stageOpenComposite") {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
enabled = hostCanRunXrPayloadScripts
commandLine(
"powershell",
"-ExecutionPolicy",
"Bypass",
"-File",
rootProject.file("tools/stage-opencomposite.ps1").absolutePath,
)
}

tasks.register<Exec>("verifyModernXrPayload") {
enabled = hostCanRunXrPayloadScripts
dependsOn("buildModernXrNative", "buildWindowsXrRuntime", "stageWineXrBridge", "stageOpenComposite")
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
commandLine(
"powershell",
"-ExecutionPolicy",
"Bypass",
"-File",
rootProject.file("tools/verify-xr-payload.ps1").absolutePath,
)
}

tasks.register("prepareModernXrPayload") {
dependsOn("verifyModernXrPayload")
}


// externalNativeBuild {
// cmake {
// path = file("src/main/cpp/asurfacerenderer/CMakeLists.txt")
Expand Down
3 changes: 3 additions & 0 deletions app/src/legacyXr/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
<meta-data
android:name="com.oculus.vrshell.supports_free_resizing"
android:value="true" />
<meta-data
android:name="pvr.app.type"
android:value="vr" />
</activity>
</application>

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
77eed5501e077f5f0422d063b171a3a4491f52a6
Binary file not shown.
Binary file added app/src/legacyXr/assets/opencomposite_x64.dll
Binary file not shown.
1 change: 1 addition & 0 deletions app/src/legacyXr/assets/payload.version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3 fb4792ecc21725bec26e3e1754af6c954b39e0f641192d257e578629dea9c92c eb77fcd88b7a93c1fd28e8aaa05b22d0d454a11ebf488743cb4008a058d93e8e
Binary file modified app/src/legacyXr/jniLibs/arm64-v8a/libxrimmersive.so
Binary file not shown.
9 changes: 9 additions & 0 deletions app/src/main/cpp/xrimmersive/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ project(xrimmersive)
add_library(xrimmersive SHARED
xr_immersive.cpp
jni_bridge.cpp
xr_windows_projection.cpp
xr_windows_transport.cpp
)

find_package(OpenXR REQUIRED CONFIG)
Expand All @@ -30,4 +32,11 @@ target_link_libraries(xrimmersive
log
)

target_compile_definitions(xrimmersive PRIVATE
XR_USE_PLATFORM_ANDROID=1
XR_USE_GRAPHICS_API_OPENGL_ES=1
EGL_EGLEXT_PROTOTYPES=1
GL_GLEXT_PROTOTYPES=1
)

target_compile_options(xrimmersive PRIVATE -Wall -Wextra -Werror=return-type -Wno-missing-field-initializers)
132 changes: 130 additions & 2 deletions app/src/main/cpp/xrimmersive/jni_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <android/hardware_buffer_jni.h>
#include <android/log.h>

#include <atomic>
#include <condition_variable>
#include <mutex>
#include <unordered_set>

Expand All @@ -13,13 +15,18 @@ namespace {
struct NativeHandle {
xrimmersive::XrImmersiveSession *session;
jobject activityGlobalRef;
std::atomic<int> waiters{0};
};

// Lock ordering: gHandleMutex is the outermost lock — it may be held while calling into
// XrImmersiveSession (which takes its own inner mutexes), never the other way round, and it is
// never held across XrImmersiveSession::join() or AndroidBitmap_lockPixels/unlockPixels. Every
// entry point must hold it for the WHOLE call into the session, not just the lookup.
// entry point must hold it for the WHOLE call into the session, not just the lookup. The one
// exception is nativeWaitWindowsFrame, which pins the handle with NativeHandle::waiters instead so
// the (up to 1 s) wait does not block every other entry point; nativeJoinAndDestroy waits for
// waiters to drain before freeing the session.
std::mutex gHandleMutex;
std::condition_variable gHandleCondition;
std::unordered_set<NativeHandle *> gLiveHandles;

NativeHandle *LiveHandle(jlong handlePtr) {
Expand Down Expand Up @@ -68,8 +75,10 @@ Java_app_gamenative_ui_screen_xr_XrNative_nativeJoinAndDestroy(JNIEnv *env, jcla
{
// Retiring the handle under the lock means every in-flight caller has already left its
// critical section, and no new one can enter, before anything is destroyed.
std::lock_guard<std::mutex> lock(gHandleMutex);
std::unique_lock<std::mutex> lock(gHandleMutex);
if (gLiveHandles.erase(handle) == 0) return;
handle->session->requestStop();
gHandleCondition.wait(lock, [handle] { return handle->waiters.load() == 0; });
}
handle->session->join();
delete handle->session;
Expand Down Expand Up @@ -127,6 +136,125 @@ Java_app_gamenative_ui_screen_xr_XrNative_nativePollSnapshot(JNIEnv *env, jclass
return snapshot.quickMenuClicked ? JNI_TRUE : JNI_FALSE;
}

JNIEXPORT jboolean JNICALL
Java_app_gamenative_ui_screen_xr_XrNative_nativeWaitWindowsFrame(
JNIEnv *env, jclass, jlong handlePtr, jlong afterSerial, jint timeoutMs,
jlongArray outTiming, jfloatArray outViews, jfloatArray outInput, jintArray outFlags) {
if (env->GetArrayLength(outTiming) < 12 || env->GetArrayLength(outViews) < 22 ||
env->GetArrayLength(outInput) < 36 || env->GetArrayLength(outFlags) < 3) return JNI_FALSE;
xrimmersive::WindowsRuntimeSnapshot snapshot;
xrimmersive::XrImmersiveSession *session = nullptr;
NativeHandle *handle = nullptr;
{
std::lock_guard<std::mutex> lock(gHandleMutex);
handle = LiveHandle(handlePtr);
if (handle == nullptr) return JNI_FALSE;
session = handle->session;
handle->waiters.fetch_add(1);
}
const bool got = session->waitWindowsRuntimeSnapshot(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
static_cast<uint64_t>(afterSerial), static_cast<uint32_t>(timeoutMs), &snapshot);
{
std::lock_guard<std::mutex> lock(gHandleMutex);
handle->waiters.fetch_sub(1);
}
gHandleCondition.notify_all();
if (!got) return JNI_FALSE;
const jlong timing[12] = {
static_cast<jlong>(snapshot.frameSerial),
static_cast<jlong>(snapshot.predictedDisplayTime),
static_cast<jlong>(snapshot.predictedDisplayPeriod),
static_cast<jlong>(snapshot.sessionState),
static_cast<jlong>(snapshot.shouldRender ? 1 : 0),
static_cast<jlong>(snapshot.recommendedWidth),
static_cast<jlong>(snapshot.recommendedHeight),
static_cast<jlong>(snapshot.stageAvailable ? 1 : 0),
static_cast<jlong>(snapshot.stageBounds.width * 1000000.0f),
static_cast<jlong>(snapshot.stageBounds.height * 1000000.0f),
static_cast<jlong>(snapshot.stageSpaceActive ? 1 : 0),
static_cast<jlong>(snapshot.recenterSerial),
};
env->SetLongArrayRegion(outTiming, 0, 12, timing);
jfloat views[22]{};
for (size_t eye = 0; eye < 2; ++eye) {
const XrView &view = snapshot.views[eye];
const size_t base = eye * 11;
views[base] = view.pose.orientation.x;
views[base + 1] = view.pose.orientation.y;
views[base + 2] = view.pose.orientation.z;
views[base + 3] = view.pose.orientation.w;
views[base + 4] = view.pose.position.x;
views[base + 5] = view.pose.position.y;
views[base + 6] = view.pose.position.z;
views[base + 7] = view.fov.angleLeft;
views[base + 8] = view.fov.angleRight;
views[base + 9] = view.fov.angleUp;
views[base + 10] = view.fov.angleDown;
}
env->SetFloatArrayRegion(outViews, 0, 22, views);
const auto &input = snapshot.input;
jfloat inputValues[36]{};
for (size_t hand = 0; hand < 2; ++hand) {
const size_t base = hand * 18;
const XrPosef &grip = input.gripPoses[hand];
const XrPosef &aim = input.aimPoses[hand];
inputValues[base] = hand == 0 ? input.triggerL : input.triggerR;
inputValues[base + 1] = hand == 0 ? input.squeezeL : input.squeezeR;
inputValues[base + 2] = hand == 0 ? input.leftX : input.rightX;
inputValues[base + 3] = -(hand == 0 ? input.leftY : input.rightY);
inputValues[base + 4] = grip.orientation.x;
inputValues[base + 5] = grip.orientation.y;
inputValues[base + 6] = grip.orientation.z;
inputValues[base + 7] = grip.orientation.w;
inputValues[base + 8] = grip.position.x;
inputValues[base + 9] = grip.position.y;
inputValues[base + 10] = grip.position.z;
inputValues[base + 11] = aim.orientation.x;
inputValues[base + 12] = aim.orientation.y;
inputValues[base + 13] = aim.orientation.z;
inputValues[base + 14] = aim.orientation.w;
inputValues[base + 15] = aim.position.x;
inputValues[base + 16] = aim.position.y;
inputValues[base + 17] = aim.position.z;
}
env->SetFloatArrayRegion(outInput, 0, 36, inputValues);
const jint flags[3] = {
static_cast<jint>(snapshot.viewStateFlags),
static_cast<jint>(input.buttons),
static_cast<jint>((input.aimPoseValid[0] && input.gripPoseValid[0] ? 1 : 0) |
(input.aimPoseValid[1] && input.gripPoseValid[1] ? 2 : 0)),
};
env->SetIntArrayRegion(outFlags, 0, 3, flags);
return JNI_TRUE;
}

JNIEXPORT jboolean JNICALL
Java_app_gamenative_ui_screen_xr_XrNative_nativeIsWindowsStereoActive(
JNIEnv *, jclass, jlong handlePtr) {
std::lock_guard<std::mutex> lock(gHandleMutex);
auto *handle = LiveHandle(handlePtr);
return handle != nullptr && handle->session->windowsStereoActive() ? JNI_TRUE : JNI_FALSE;
}

JNIEXPORT jboolean JNICALL
Java_app_gamenative_ui_screen_xr_XrNative_nativeApplyWindowsHaptic(
JNIEnv *, jclass, jlong handlePtr, jint hand, jfloat amplitude, jlong duration,
jfloat frequency) {
std::lock_guard<std::mutex> lock(gHandleMutex);
auto *handle = LiveHandle(handlePtr);
return handle != nullptr && handle->session->applyWindowsHaptic(
static_cast<uint32_t>(hand), amplitude, static_cast<XrDuration>(duration), frequency)
? JNI_TRUE : JNI_FALSE;
}

JNIEXPORT void JNICALL
Java_app_gamenative_ui_screen_xr_XrNative_nativeSetWindowsOverlayVisible(
JNIEnv *, jclass, jlong handlePtr, jboolean visible) {
std::lock_guard<std::mutex> lock(gHandleMutex);
auto *handle = LiveHandle(handlePtr);
if (handle != nullptr) handle->session->setWindowsOverlayVisible(visible == JNI_TRUE);
}

// Called from ImmersiveXrActivity's PixelCopy capture loop with the game's actual rendered
// frame (ARGB_8888 bitmap). Copies the pixels into the session's pending-frame buffer; the
// render thread uploads them to the GPU and draws them into the quad layer on its own.
Expand Down
Loading
Loading