Skip to content

Add Meta Quest OpenXR VR support for Android#2

Merged
RobertoD91 merged 1 commit into
mainfrom
feature/quest-vr
Jul 16, 2026
Merged

Add Meta Quest OpenXR VR support for Android#2
RobertoD91 merged 1 commit into
mainfrom
feature/quest-vr

Conversation

@RobertoD91

@RobertoD91 RobertoD91 commented Jul 8, 2026

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

Summary by CodeRabbit

  • New Features
    • Added a Meta Quest Android build with OpenXR-based VR support (GLES3-enabled).
    • Added separate Phone and Quest Android variants and CI artifacts for each.
    • Added Quest APK build and run helpers (local and fast-iteration Docker workflow) plus ADB-based install/launch logging.
  • Documentation
    • Updated Quest documentation with exact build/CI artifact naming, sideload steps, and signing options.
  • Bug Fixes
    • Improved Android/Quest rendering behavior (VR depth handling and reduced Android-only mirror steps).
    • Improved build versioning for CI and updated smoke testing to a dedicated script.

@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: 45 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: bffc96d7-c48b-4851-84fa-56b879ed2ad5

📥 Commits

Reviewing files that changed from the base of the PR and between a6a5382 and 8b08e0c.

📒 Files selected for processing (20)
  • .github/workflows/android-build.yml
  • .github/workflows/github-release.yml
  • .github/workflows/quest-build.yml
  • CMakeLists.txt
  • QUEST.md
  • android/app/build.gradle
  • android/app/src/quest/AndroidManifest.xml
  • android/debug.keystore
  • docker/android-build.Dockerfile
  • quest_docker_build.sh
  • quest_run.sh
  • source/Framework/RenderManager.cpp
  • source/PicaSim/Main.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
  • tools/android_smoke_test.sh
📝 Walkthrough

Walkthrough

Adds a Meta Quest Android flavor with OpenXR and GLES3 support, Quest-specific packaging and startup behavior, Android VR rendering adaptations, local ADB/Docker tooling, and separate phone and Quest APK CI workflows.

Changes

Quest Android VR flavor

Layer / File(s) Summary
CMake and Gradle Quest flavor configuration
CMakeLists.txt, android/app/build.gradle, android/debug.keystore
Adds Quest-specific CMake linkage and definitions, phone/Quest Gradle flavors, prefab support, versioning, signing configuration, and the Quest OpenXR loader dependency.
Android OpenXR instance and session support
source/Platform/OpenXRRuntime.*
Initializes the Android OpenXR loader, selects Android extensions, passes Java/SDL activity data, resolves EGL configuration, creates an OpenGLES session, and uses Android swapchain image types.
Quest GLES3 and VR rendering paths
source/Platform/Window.cpp, source/PicaSim/*, source/Platform/VR*.cpp, source/Framework/RenderManager.cpp
Requests GLES3 for Quest, forces Quest VR startup, selects GLES3 headers, updates Android VR depth/MSAA handling, and disables desktop mirror rendering.
Quest manifest and local deployment workflow
android/app/src/quest/AndroidManifest.xml, QUEST.md, docker/*, quest_docker_build.sh, quest_run.sh
Adds Quest device and launcher metadata, documents builds and sideloading, provides Docker-based builds, and adds ADB installation, launch, PID polling, and filtered logging.
Phone and Quest APK CI workflows
.github/workflows/android-build.yml, .github/workflows/github-release.yml, .github/workflows/quest-build.yml, tools/android_smoke_test.sh
Adds optional signing, builds flavor-specific APKs, uploads renamed Quest artifacts, uses the phone APK for emulator smoke tests, and adds a manual Quest build workflow.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant Gradle
  participant CMake
  participant PicaSim
  participant OpenXRRuntime
  participant Quest

  Developer->>Gradle: assembleQuestDebug
  Gradle->>CMake: Configure PICASIM_QUEST
  CMake->>Gradle: Link GLESv3 and OpenXR loader
  Gradle->>Quest: Install Quest APK
  Quest->>PicaSim: Launch Quest activity
  PicaSim->>OpenXRRuntime: CreateInstance()
  OpenXRRuntime->>OpenXRRuntime: Create OpenGLES session
  OpenXRRuntime->>Quest: Render VR frames
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding Meta Quest OpenXR VR support on Android.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
.github/workflows/github-release.yml (1)

159-164: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include the Quest APK in GitHub releases.

The PR objectives mention updating the release workflows to "build flavor-specific APKs, upload renamed Quest artifacts". However, this workflow currently only builds and packages the phone flavor. If you intend to distribute the experimental Quest build via GitHub releases, you should add assembleQuestDebug and copy its APK here as well.

📦 Proposed fix to include the Quest APK
-      - name: Build debug APK
+      - name: Build debug APKs
         working-directory: android
         run: |
-          ./gradlew assemblePhoneDebug
+          ./gradlew assemblePhoneDebug assembleQuestDebug

-      - name: Rename APK
+      - name: Rename APKs
         run: |
           VERSION=$(grep -oE 'Version [0-9.]+' VERSIONS.txt | head -1 | sed 's/Version //')
           cp android/app/build/outputs/apk/phone/debug/app-phone-debug.apk "PicaSim-${VERSION}-android-debug.apk"
+          cp android/app/build/outputs/apk/quest/debug/app-quest-debug.apk "PicaSim-${VERSION}-quest-debug.apk"

       - name: Upload artifact
         uses: actions/upload-artifact@v4
         with:
           name: release-android
-          path: PicaSim-*-android-debug.apk
+          path: PicaSim-*-debug.apk
           retention-days: 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/github-release.yml around lines 159 - 164, Update the
release workflow’s Android build and packaging steps around assemblePhoneDebug
and the Rename APK step to also run assembleQuestDebug and copy the generated
Quest debug APK into a clearly named PicaSim-${VERSION}-quest-android-debug.apk
release artifact, while preserving the existing phone artifact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@android/app/build.gradle`:
- Around line 143-144: Update the quest flavor dependency declaration for
org.khronos.openxr:openxr_loader_for_android from version 1.1.49 to 1.1.60,
leaving its questImplementation configuration unchanged.

In `@QUEST.md`:
- Line 67: Update the hardware-testing status in QUEST.md to document the Quest
headset and build tested, reflecting that device testing exposed the
invisible-menu startup issue. Retain caveats for any Quest models or
configurations that remain untested.

In `@source/Framework/RenderManager.cpp`:
- Around line 697-704: Update the MSAA renderbuffer setup in the RenderManager
initialization path to use the same internal format selected by
OpenXRRuntime::CreateSwapchainsInternal, rather than always forcing
GL_SRGB8_ALPHA8 on Android. Expose or reuse the selected swapchain format so the
renderbuffer and swapchain attachment match for both GL_RGBA8 and
GL_SRGB8_ALPHA8, preserving the existing platform-specific setup.

---

Nitpick comments:
In @.github/workflows/github-release.yml:
- Around line 159-164: Update the release workflow’s Android build and packaging
steps around assemblePhoneDebug and the Rename APK step to also run
assembleQuestDebug and copy the generated Quest debug APK into a clearly named
PicaSim-${VERSION}-quest-android-debug.apk release artifact, while preserving
the existing phone artifact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efe706a8-5579-4839-a3f6-f7479da8e5a9

📥 Commits

Reviewing files that changed from the base of the PR and between d53ee90 and 582c4b5.

📒 Files selected for processing (15)
  • .github/workflows/android-build.yml
  • .github/workflows/github-release.yml
  • CMakeLists.txt
  • QUEST.md
  • android/app/build.gradle
  • android/app/src/quest/AndroidManifest.xml
  • quest_run.sh
  • source/Framework/RenderManager.cpp
  • source/PicaSim/Main.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

Comment thread android/app/build.gradle
Comment on lines +143 to +144
// Khronos OpenXR loader (AAR with prefab support) — quest flavor only
questImplementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the latest stable version of the org.khronos.openxr:openxr_loader_for_android package on Maven Central?

💡 Result:

The latest stable version of the org.khronos.openxr:openxr_loader_for_android package available on Maven Central is 1.1.60 [1]. Although a hotfix version 1.1.59.1 was previously released [2], the 1.1.60 release, published on May 20, 2026, serves as a subsequent stable update containing support for a new multi-vendor extension along with various fixes and cleanups [1]. Users can include this version in their Android project's build.gradle file as follows: implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.60' [1]

Citations:


Bump org.khronos.openxr:openxr_loader_for_android to 1.1.60
1.1.49 is behind the current stable Maven Central release.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/app/build.gradle` around lines 143 - 144, Update the quest flavor
dependency declaration for org.khronos.openxr:openxr_loader_for_android from
version 1.1.49 to 1.1.60, leaving its questImplementation configuration
unchanged.

Comment thread QUEST.md

- **Experimental** — the build-system support is in place, but the rendering
path is still being ported to GLES3 for the Quest's OpenXR runtime.
- Not yet tested on real hardware.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the hardware-testing status.

“Not yet tested on real hardware” contradicts the PR history, which says Quest device testing exposed the invisible-menu startup issue. Document the tested headset/build and retain any untested-device caveats.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@QUEST.md` at line 67, Update the hardware-testing status in QUEST.md to
document the Quest headset and build tested, reflecting that device testing
exposed the invisible-menu startup issue. Retain caveats for any Quest models or
configurations that remain untested.

Comment on lines +697 to +704
#if defined(PICASIM_ANDROID) || defined(__ANDROID__)
// GLES3 requires the multisample resolve source and destination to
// have identical internal formats. The OpenXR runtime prefers an
// SRGB8_ALPHA8 swapchain (which Quest provides), so match it here.
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaaSamples, GL_SRGB8_ALPHA8, eyeWidth, eyeHeight);
#else
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaaSamples, GL_RGBA8, eyeWidth, eyeHeight);
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the swapchain fallback and MSAA resolve sites remain coupled.
rg -n -C4 'selectedFormat|GL_SRGB8_ALPHA8|glRenderbufferStorageMultisample|glBlitFramebuffer' \
  source/Platform/OpenXRRuntime.cpp source/Framework/RenderManager.cpp

Repository: RobertoD91/PicaSim

Length of output: 9518


🌐 Web query:

OpenGL ES 3.0 glBlitFramebuffer multisample resolve identical internal formats GL_INVALID_OPERATION official spec

💡 Result:

In OpenGL ES 3.0, the glBlitFramebuffer specification explicitly requires that if either the read or draw framebuffer is multisampled (i.e., SAMPLE_BUFFERS is greater than zero), the internal formats of the corresponding attachments must be identical [1][2][3]. If they are not identical, an GL_INVALID_OPERATION error is generated [1][2][3]. This restriction is a formal requirement of the OpenGL ES 3.0 specification for multisample resolve operations [1][2]. While later versions of the desktop OpenGL specification (starting with 4.4) were updated to relax this constraint and allow format conversion during multisample blits, the OpenGL ES 3.0 core specification retains the requirement that formats must be identical for multisample resolve operations to ensure conformance [4][1][2]. In addition to the identical format requirement, glBlitFramebuffer also mandates the following for multisample blits (when either the read or draw buffer is multisampled): * The source and destination rectangle dimensions (X0, Y0, X1, Y1) must be identical [1][2][3]. * If both framebuffers are multisampled, their sample counts (SAMPLES) must be identical [5][6][3]. Failure to meet these conditions—including the identical internal format requirement—will result in a GL_INVALID_OPERATION error [5][1][2][3].

Citations:


Match the MSAA renderbuffer format to the selected swapchain format
OpenXRRuntime::CreateSwapchainsInternal can select GL_RGBA8, but the Android MSAA buffer is still created as GL_SRGB8_ALPHA8. In OpenGL ES 3.0, multisample blits require identical internal formats, so the resolve can fail with GL_INVALID_OPERATION when the fallback is used. Expose the chosen swapchain format or derive the renderbuffer format from the swapchain attachment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/Framework/RenderManager.cpp` around lines 697 - 704, Update the MSAA
renderbuffer setup in the RenderManager initialization path to use the same
internal format selected by OpenXRRuntime::CreateSwapchainsInternal, rather than
always forcing GL_SRGB8_ALPHA8 on Android. Expose or reuse the selected
swapchain format so the renderbuffer and swapchain attachment match for both
GL_RGBA8 and GL_SRGB8_ALPHA8, preserving the existing platform-specific setup.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/android-build.yml (1)

63-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use printf instead of echo -n for decoding base64 secrets.

Different implementations of echo handle the -n flag inconsistently, which can sometimes corrupt the decoded output or print -n directly into the stream. printf is POSIX-compliant and more reliable for handling base64 secrets.

  • .github/workflows/android-build.yml#L63-L63: replace echo -n "$ANDROID_KEYSTORE_BASE64" with printf "%s" "$ANDROID_KEYSTORE_BASE64".
  • .github/workflows/github-release.yml#L165-L165: replace echo -n "$ANDROID_KEYSTORE_BASE64" with printf "%s" "$ANDROID_KEYSTORE_BASE64".
  • .github/workflows/quest-build.yml#L62-L62: replace echo -n "$ANDROID_KEYSTORE_BASE64" with printf "%s" "$ANDROID_KEYSTORE_BASE64".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/android-build.yml at line 63, Replace echo -n with printf
"%s" when piping ANDROID_KEYSTORE_BASE64 into base64 decoding in
.github/workflows/android-build.yml:63-63,
.github/workflows/github-release.yml:165-165, and
.github/workflows/quest-build.yml:62-62; preserve the existing output paths and
decoding flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/android-build.yml:
- Line 63: Replace echo -n with printf "%s" when piping ANDROID_KEYSTORE_BASE64
into base64 decoding in .github/workflows/android-build.yml:63-63,
.github/workflows/github-release.yml:165-165, and
.github/workflows/quest-build.yml:62-62; preserve the existing output paths and
decoding flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6a1767f-70cf-4e5e-8588-be2cfde53ae2

📥 Commits

Reviewing files that changed from the base of the PR and between 582c4b5 and a6a5382.

📒 Files selected for processing (11)
  • .github/workflows/android-build.yml
  • .github/workflows/github-release.yml
  • .github/workflows/quest-build.yml
  • QUEST.md
  • android/app/build.gradle
  • android/debug.keystore
  • docker/android-build.Dockerfile
  • quest_docker_build.sh
  • quest_run.sh
  • source/PicaSim/Main.cpp
  • tools/android_smoke_test.sh
🚧 Files skipped from review as they are similar to previous changes (2)
  • quest_run.sh
  • QUEST.md

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

Quest: Android/OpenGL ES backend for the OpenXR runtime

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

Quest: port the VR render path to OpenGL ES 3.0

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

Quest: fix glad include in PicaSim.cpp for Android VR builds

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

Quest: enable VR at startup so menus render in the headset

On-device testing showed the app healthy (assets extracted, GL up,
OpenXR instance created on the Meta runtime) but stuck on the system
loading interstitial: the menus were rendering to the invisible SDL
surface with zero XR frames submitted. The menus already go through
VRMenuRenderer, which is gated on mOptions.mEnableVR - default false
and only togglable from the desktop settings UI. On the quest flavor,
force it on at startup: the SDL window is invisible, the app only
exists in VR.

Also from the first device-test round:
- quest_run.sh: one-step adb install + launch + PID-filtered logcat
  (drops the Quest system noise, keeps PicaSim/SDL/OpenXR output),
  saving to picasim_quest_run.log with a crash-report dump on Ctrl-C
- CI names the quest APK com.rowlhouse.picasim-quest-v<version>-b<run>
  so sideloaded builds are identifiable

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

Quest: skip menus at startup, stable debug signing, quest-only CI workflow [skip ci]

Second device-test round: the XR session comes up and the menus render
in the headset, so now the quest flavor goes straight to flying via the
existing mFreeFlyOnStartup path (menu navigation needs a Bluetooth
mouse for now and is a later step).

Sideload fixes:
- debug builds are now signed with a keystore committed to the repo
  (standard Android debug credentials): every CI runner used to
  generate its own debug key, so each new APK failed to install with
  INSTALL_FAILED_UPDATE_INCOMPATIBLE
- CI passes -PpicasimBuildNumber=<run number>: versionCode becomes
  <2MMNNPP>000+build, strictly increasing across CI builds so updates
  install cleanly; quest_run.sh installs with -r -d (downgrade allowed
  for local builds) and auto-uninstalls on a signature mismatch

quest-build.yml is a new workflow_dispatch-only workflow that builds
just the quest flavor, to iterate on the port without burning CI
minutes on the full multi-platform suite; combined with [skip ci] on
work-in-progress pushes.

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

Android CI: optional custom signing keystore via secrets [skip ci]

Debug builds can now be signed with a user-provided key: the workflows
(quest-build, android-build, github-release) decode
ANDROID_KEYSTORE_BASE64 and export it with its credentials
(ANDROID_KEYSTORE_PASSWORD/KEY_ALIAS/KEY_PASSWORD) as PICASIM_KEYSTORE_*
environment variables, which build.gradle's debug signing config picks
up. With any secret missing - e.g. on fork PRs, where secrets are
unavailable - the build falls back to the debug keystore committed in
android/, preserving the stable-signature behavior.

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

Quest: local Docker build environment for fast iteration [skip ci]

quest_docker_build.sh builds the APK in a container instead of waiting
for CI: docker/android-build.Dockerfile bakes JDK 17 + Android SDK,
NDK 27.0.12077973 (the version AGP actually resolves on CI) and cmake
3.22.1, with licenses pre-accepted so AGP can fetch anything else.
Gradle caches persist in the picasim-gradle named volume and native
build outputs live in the repo bind-mount, so incremental builds are
fast. A host android/local.properties is shadowed inside the container
so it can't point gradle at the host SDK path. Runs as linux/amd64
(the NDK has no linux/arm64 host toolchain); on Apple Silicon enable
Rosetta emulation in Docker Desktop.

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

quest_run.sh: add -u/--uninstall option for clean installs [skip ci]

./quest_run.sh -u <apk> uninstalls the old app before installing
(clean install, drops app data); ./quest_run.sh -u alone just
uninstalls and exits. The automatic uninstall-on-signature-mismatch
fallback stays.

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

Quest: start with the powered trainer in the 3D field; fix emulator smoke test [skip ci]

The quest flavor now always starts flying the Jackdaw (powered, with
wheels) in RecreationGround3D - the same combination as the built-in
powered-trainer scenario but with the full-3D environment instead of
the panoramic one - until the menus are usable in the headset.

The test-android job had been broken since its introduction: the
android-emulator-runner action executes its script input line by line,
so the multi-line polling loop died with a shell syntax error. The
logic now lives in tools/android_smoke_test.sh, called as one line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123xeZJrveacZTdK8bCv7YX
@RobertoD91
RobertoD91 merged commit 8b08e0c into main Jul 16, 2026
1 check passed
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