Skip to content

Add Meta Quest OpenXR VR support for Android#2

Open
RobertoD91 wants to merge 4 commits into
mainfrom
feature/quest-vr
Open

Add Meta Quest OpenXR VR support for Android#2
RobertoD91 wants to merge 4 commits into
mainfrom
feature/quest-vr

Conversation

@RobertoD91

Copy link
Copy Markdown
Owner

Summary

Adds OpenXR-based VR support for Meta Quest headsets (Quest 2/Pro/3) via a new Android Gradle product flavor. The regular Android build remains unchanged (phone flavor, VR off). This is an experimental feature—the build system is in place, but rendering is still being ported to GLES3.

Key Changes

Build System & Configuration

  • Added quest Gradle product flavor alongside existing phone flavor; both use the same application ID so only one can be installed at a time
  • Integrated Khronos OpenXR loader AAR (org.khronos.openxr:openxr_loader_for_android:1.1.49) via Gradle prefab support, exposed to CMake as OpenXR::openxr_loader
  • CMake now accepts -DPICASIM_QUEST=ON to enable VR on Android; sets PICASIM_VR_SUPPORT=1, PICASIM_QUEST=1, and links GLESv3 in addition to GLESv2
  • Updated CI workflow to build both flavors and upload quest APK as separate artifact

Android Manifest & Metadata

  • Added Quest manifest overlay (android/app/src/quest/AndroidManifest.xml) with:
    • Head-tracking feature requirement
    • VR launcher category (com.oculus.intent.category.VR)
    • Device metadata for Horizon OS store/sideload
    • Removal of phone-oriented screen orientation lock
    • Minimum SDK bumped to 29 for quest flavor (OpenXR loader requirement)

OpenXR Runtime Implementation (OpenXRRuntime.cpp)

  • Android-specific includes: EGL, GLES3, JNI headers; conditional compilation for OpenGL vs. OpenGL ES
  • CreateInstance(): Retrieves JavaVM and activity from SDL, initializes Android OpenXR loader via xrInitializeLoaderKHR before any other OpenXR calls
  • CreateInstance(): Selects XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME on Android vs. XR_KHR_OPENGL_ENABLE_EXTENSION_NAME on desktop; includes XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME in enabled extensions
  • CreateInstance(): Passes JavaVM and activity to runtime via XrInstanceCreateInfoAndroidKHR chained struct
  • CreateSessionInternal(): Android path retrieves EGL display/context from SDL, finds matching EGLConfig via GetEGLConfigForContext() helper, creates graphics binding with XrGraphicsBindingOpenGLESAndroidKHR
  • Swapchain image type: XrSwapchainImageOpenGLESKHR on Android vs. XrSwapchainImageOpenGLKHR on desktop

Graphics & Rendering (RenderManager.cpp, Window.cpp, VRManager.cpp, VRMenuRenderer.cpp)

  • GLES3 header includes for Quest to access glBlitFramebuffer, glRenderbufferStorageMultisample, sized internal formats
  • Window initialization: Requests OpenGL ES 3.0 context on Quest (backward compatible with ES2 usage elsewhere)
  • Depth range: Uses glDepthRangef() (float variant) on GLES, glDepthRange() on desktop
  • MSAA renderbuffer: Uses GL_SRGB8_ALPHA8 on Quest (matches OpenXR runtime preference) vs. GL_RGBA8 on desktop
  • Depth texture: Uses GL_UNSIGNED_INT type on GLES3 (required for GL_DEPTH_COMPONENT24) vs. GL_FLOAT on desktop
  • Depth copy: Uses glBlitFramebuffer on GLES3 (since glCopyTexImage2D cannot copy from depth buffer) vs. glCopyTexImage2D on desktop
  • Desktop mirror window: Disabled on Quest (no desktop display, would stall OpenXR frame

https://claude.ai/code/session_0123xeZJrveacZTdK8bCv7YX

claude added 3 commits July 8, 2026 07:29
Android gains phone/quest product flavors: phone is today's build,
quest passes PICASIM_QUEST=ON to CMake, links the Khronos OpenXR
Android loader (org.khronos.openxr:openxr_loader_for_android:1.1.49
via prefab, minSdk 29 for this flavor as the loader requires 24+) and
merges a manifest overlay with the Quest metadata (vr.headtracking,
com.oculus.intent.category.VR, supportedDevices, no orientation lock).

CMake: new PICASIM_QUEST option; on Android it enables VR, finds the
OpenXR prefab package, defines PICASIM_VR_SUPPORT/PICASIM_QUEST and
links GLESv3 + openxr_loader. Non-quest Android keeps VR forced off.

CI builds both flavors and uploads a separate quest APK artifact;
APK paths updated for the flavored output layout. QUEST.md documents
building, sideloading and input (RC transmitter as SDL gamepad, no VR
controller support).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123xeZJrveacZTdK8bCv7YX
Adds the Android path to the OpenXR layer, all behind PICASIM_ANDROID
with the Win32/desktop-GL path unchanged:

- XR_USE_GRAPHICS_API_OPENGL_ES + XR_USE_PLATFORM_ANDROID, with EGL,
  GLES3 and JNI headers ahead of openxr_platform.h
- one-time xrInitializeLoaderKHR with the JavaVM and activity obtained
  from SDL, before any other OpenXR call (mandatory on Android)
- instance extensions XR_KHR_opengl_es_enable and
  XR_KHR_android_create_instance, with XrInstanceCreateInfoAndroidKHR
  chained into instance creation
- session creation via XrGraphicsBindingOpenGLESAndroidKHR built from
  the current EGL display/context, with the EGLConfig recovered from
  the context's EGL_CONFIG_ID
- swapchain images typed XrSwapchainImageOpenGLESKHR

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123xeZJrveacZTdK8bCv7YX
The VR rendering code was written against desktop GL; this makes it
compile and behave on an ES 3.0 context (Quest), with every change
inert unless PICASIM_ANDROID/PICASIM_QUEST is defined:

- Window: request an ES 3.0 context and GLES3 headers for the quest
  flavor (non-quest Android stays on ES 2.0)
- glDepthRange -> glDepthRangef on GLES (double variant doesn't exist)
- MSAA color renderbuffer uses GL_SRGB8_ALPHA8 on Android: GLES3
  requires matching internal formats on multisample resolve blits and
  the swapchain prefers sRGB
- panorama depth copy: GLES cannot glCopyTexImage2D depth, so it is
  replaced by a depth blit through a temporary FBO; depth texture
  allocated with GL_UNSIGNED_INT as GLES3 rejects DEPTH_COMPONENT24 +
  GL_FLOAT
- desktop mirror-window rendering and per-eye mirror blits compiled
  out on Android (no mirror on device; swapping the invisible SDL
  surface could stall the XR frame loop), and the eye mirror FBOs are
  skipped entirely, saving GPU memory

Known runtime follow-ups documented in QUEST.md territory: swapchain
format fallback if the runtime lacks SRGB8_ALPHA8, and possible double
sRGB encoding to tune in the swapchain choice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123xeZJrveacZTdK8bCv7YX
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@RobertoD91, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 767ffb05-1c3f-45f7-a771-5fff45fa25e9

📥 Commits

Reviewing files that changed from the base of the PR and between d53ee90 and 83f531c.

📒 Files selected for processing (13)
  • .github/workflows/android-build.yml
  • .github/workflows/github-release.yml
  • CMakeLists.txt
  • QUEST.md
  • android/app/build.gradle
  • android/app/src/quest/AndroidManifest.xml
  • source/Framework/RenderManager.cpp
  • source/PicaSim/PicaSim.cpp
  • source/Platform/OpenXRRuntime.cpp
  • source/Platform/OpenXRRuntime.h
  • source/Platform/VRManager.cpp
  • source/Platform/VRMenuRenderer.cpp
  • source/Platform/Window.cpp
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/quest-vr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

PicaSim.cpp's VR block included glad unconditionally; it had only ever
compiled on Windows. Use GLES3 headers on Android like the rest of the
VR path. First compile failure caught by CI on the quest flavor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123xeZJrveacZTdK8bCv7YX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants