V2.55.0#321
Merged
doranteseduardo merged 14 commits intomainfrom Apr 27, 2026
Merged
Conversation
- Add OpenXR AAR (1.1.38) via local Maven repo and Khronos headers - Configure CMake with OpenXR::openxr_loader and c++_shared STL - Implement VROSceneRendererOpenXR: session, EGL, swapchains, frame loop - Implement VROInputControllerOpenXR: action set, Touch/Touch Plus bindings - Add VRODriverOpenGLAndroidOpenXR and VRODisplayOpenGLOpenXR (per-eye FBO) - Add ViroViewOpenXR (Java bootstrap) and nativeCreateRendererOpenXR JNI - Add standalone openxrtest/ module for on-device validation - Fix 7 on-device crashes: STL linkage, loader init order, session state, EGL context ownership, uninitialised renderer Result: stereo render at 90fps on Quest 3, no crashes.
…er events - VROSceneRendererOpenXR: replace POC color-clear with real VRORenderer pipeline; prepareFrame() + renderEye() per eye; fix _driver shadowing bug (_openxrDriver + base _driver both set in constructor); initPassthrough() loads 8 XR_FB_passthrough function pointers, creates XrPassthroughFB + XrPassthroughLayerFB; setPassthroughEnabled() start/pause subsystem and layer; proper destroySession() teardown - VROInputControllerOpenXR: wire right aim pose -> updateHitNode/onMove/processGazeEvent; right trigger -> ClickDown/Up; menu/B button -> BackButton ClickDown/Up; add file-local xrPoseToMatrix + xrAimForward helpers (fixes linkage bug); _prevMenuButton edge detection - VRORenderer_JNI: nativeSetPassthroughEnabled JNI method with dynamic_pointer_cast - Renderer.java / ViroViewOpenXR.java: setPassthroughEnabled() public API surface - openxrtest/MainActivity: dummy Viro scene (red box at 0,0,-2) for device validation
zero m[12..14] before passing to prepareFrame; per-eye view matrices (which need the full pose) are computed separately in renderEye.
… Quest 3 All renderer features confirmed working in stereo at 90fps on Quest 3. Two bugs found and fixed during M1 device validation: 1. Reference space: createReferenceSpace() was trying STAGE first. After the Quest Guardian was configured between M0 and M1 testing, STAGE succeeded — placing Y=0 at the floor with the eye at Y≈1.6m. Objects at Viro world origin appeared 39° below center, past the Quest 3's physical downward FOV, invisible with no error. Fixed: force XR_REFERENCE_SPACE_TYPE_LOCAL so the world origin matches eye level at session start, consistent with all other Viro platforms. 2. endFrame() never called: VROSceneRendererOpenXR::renderFrame() called prepareFrame() and renderEye() but never _renderer->endFrame(). endFrame() is where VROFrameScheduler::processTasks() runs — without it, texture hydration tasks queue forever. Models parsed and downloaded correctly but onObject3DLoaded never fired and nothing appeared, with zero error logs. Fixed: call _renderer->endFrame(_driver) after both eye renders, before xrEndFrame.
Dual-controller input (VROInputControllerOpenXR)
- Separate FLOAT trigger/grip actions per hand (threshold 0.5), edge-detected
- A/B/X/Y/Menu buttons as BOOLEAN actions; B+Menu both map to BackButton
- Thumbstick Vector2f actions → onScroll with dead zone 0.15
- Haptic output: xrApplyHapticFeedback per hand via triggerHaptic()
- New ViroOculus sources in VROInputType.h: LeftController=4, AButton=5,
XButton=6, YButton=7, LeftGrip=8, RightGrip=9, LeftThumbstick=10,
RightThumbstick=11
- recenterTracking() rewritten: VIEW space → head pose → yaw extraction →
new LOCAL reference space at head XZ; was broken (located space against itself)
- openxrtest scene updated with interactive hover+click nodes for validation
Hand tracking (XR_EXT_hand_tracking + XR_FB_hand_tracking_aim)
- initHandTracking() loads 3 EXT function pointers, creates left/right trackers
- processHands(): per-frame xrLocateHandJointsEXT (26 joints), chains
XrHandTrackingAimStateFB for aimPose + pinchStrengthIndex
- Pinch: pinchStrengthIndex >= 0.7 (FB aim) or thumb-tip<2cm (fallback)
- Grab: middle-tip to palm < 6cm → RightGrip/LeftGrip click events
- Right hand = primary pointer (updateHitNode + processGazeEvent)
- Left hand = pose-only (onMove, no hit test)
- Graceful no-op if extensions unavailable at runtime
Infrastructure fixes (blocked all input silently):
- VROInputControllerBase::processGazeEvent: add null guard on _hitResult;
updateHitNode returns early when _scene==nullptr, leaving _hitResult null,
previously causing SIGSEGV on first frames with hand tracking active
- VROPlatformUtil: add direct C++ renderer task queue
(VROPlatformSetUseDirectRendererQueue / VROPlatformDrainRendererQueue);
OpenXR has no GLSurfaceView so mRenderQueue==null and all
VROPlatformDispatchAsyncRenderer tasks were silently dropped — setScene
never executed, _scene never set, zero input events
- VROPlatformUtil: replace FindClass("com/viro/core/internal/PlatformUtil")
with GetObjectClass(sPlatformUtil) in 4 call sites; native threads attached
with AttachCurrentThread(nullptr) use bootstrap classloader, app classes
not found → null jclass → ART JNI abort in VROPlatformDispatchAsyncBackground
Device validated on Quest 3: trigger/grip/A/B/X/Y clicks and hover confirmed
in logcat; hand aim ray hover and grab events confirmed.
feat: add Meta Horizon OS support
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ReactVision ViroCore 2.55.0
This release lands the OpenXR backend for Meta Horizon OS,
a per-source input pipeline that lets multi-pointer backends like Quest
dispatch hover and click independently without breaking single-pointer backends,
and a new Studio scenes API in the bundled Cloud Anchor SDK.
It also ships three platform fixes — Android 2025 ABI compliance, an AR image-marker regression,
and continued iOS portal stability work.
Highlights
OpenXR backend for Meta Horizon OS
Native VR rendering on Meta Quest 3 / Quest Pro / Quest 2 / Quest 1 via
Khronos OpenXR. Validated end-to-end at 90 Hz on Quest 3 with App = 5–6 ms
frame time. The backend covers the full Activity / surface / session
lifecycle plus input and presentation:
ViroViewOpenXR.java— Android view selected by the bridge when thepackage is registered with
ReactViroPackage.ViroPlatform.QUEST. DefersRendererconstruction untilonAttachedToWindowsoxrCreateSessionbinds to the Activity that actually owns the surface — resolved via
window-token matching against live activities from
ActivityThread,the only signal that's stable under Android's multi-resumed mode.
Idempotent
onResume/onPause(mResumedboolean) tolerate themulti-source RN lifecycle (Application.ActivityLifecycleCallbacks +
RN LifecycleEventListener firing for the same Activity transition).
Null-guards every base-class setter / getter that touches the deferred
renderer.
VROSceneRendererOpenXR.cpp— C++ scene renderer.XR_SESSION_STATE_IDLE → READY → SYNCHRONIZED → VISIBLE → FOCUSED,per-frame
xrWaitFrame/xrBeginFrame/xrEndFrame,projection-layer compositor submission, swapchain image acquisition and
release.
onResumeandonPauseare idempotent (early-return whenalready in the target state) so the renderer survives lifecycle events
arriving from multiple sources without re-assigning the render
std::thread.VROInputControllerOpenXR.{h,cpp}— action-based input controller.Per-frame
xrSyncActionsfor Touch controllers (triggers, grips,A / B / X / Y, menu, thumbsticks, haptics) and per-hand
xrLocateHandJointsEXT+XR_FB_hand_tracking_aimfor fingertip-aimedpointing. Pinch-to-click via FB pinch strength with a thumb-to-index
joint-distance fallback; grip-to-grab via middle-tip-to-palm distance.
Two-pointer dispatch. Right and left aim poses are captured without
dispatching, then dispatched once per side via a
dispatchSidelambda:updateHitNode(source, …)→onMove(source, …)→processGazeEvent(source)→ laser update. Per-source pose hysteresis(~5-frame cache) keeps the aim source from flipping between controller
and synthesized hand pose on transient single-frame
xrLocateSpaceinvalidity.
VROInputPresenterOpenXR.h— visual presenter. World-space(non-headlocked) reticle plus a per-source
unordered_map<int, Laser>of cyanVROPolylinechildren under thepresenter
_rootNode, lazy-initialized on first dispatch and hiddencleanly when no aim is available. Both lasers can be visible
simultaneously when both hands are tracked.
OpenXR extensions in use:
XR_KHR_opengl_es_enable,XR_KHR_android_create_instance,XR_FB_passthrough,XR_FB_display_refresh_rate,XR_EXT_hand_tracking,XR_FB_hand_tracking_aim.com.oculus.feature.PASSTHROUGHmust bedeclared in the Android manifest for
XR_FB_passthroughto be advertisedby the OpenXR runtime — the Viro Expo plugin emits this automatically
when
xRModeincludes"QUEST".The integration goes deep into the dual-Activity story: surface mounted
inside the VR Activity,
xrCreateSessionbound to it (not to MainActivity),the OpenXR session machine cycling cleanly to FOCUSED, the input dispatcher
producing stable hover / click events across both pointers, and reflection-
based VR Activity discovery from the launcher module so
exitVRScenefinishes the right Activity even when the React context's
currentActivityremains pinned to MainActivity.
Per-source input state in
VROInputControllerBaseVROInputControllerBasepreviously carried a single_hitResult,_lastHoveredNodeand_lastClickedNode— fine for single-pointer backends(iOS / AR / Cardboard / OVR / Daydream / Wasm), wrong for multi-pointer
backends like OpenXR where two simultaneously tracked hands need
independent hover and click resolution.
This release adds additive per-source state without breaking any
existing backend:
_hitResultsBySource,_lastHoveredNodesBySource,_lastClickedNodesBySourcekeyed by input source ID.updateHitNode(int source, camera, origin, ray)writes boththe per-source map and the legacy
_hitResultfor backward compat.getHitResultForSource(source)falls back to the legacy_hitResultwhen no per-source entry exists.processGazeEventandonButtonEventuse the source-aware lookups whenan entry is present and dispatch hover / click events independently per
source. Otherwise they behave exactly as before.
iOS, AR, Cardboard, OVR, Daydream and Wasm — which all call
updateHitNode(camera, origin, ray)without a source parameter — see zerobehavior change. Only the OpenXR backend opts into per-source state by
calling the new overload.
Studio scenes API in
libreactvisionccaThe bundled Cloud Anchor SDK (
libreactvisioncca, shipped insidevirocore) gains two new endpoints that power the Studio scene fetcher in@reactvision/react-viro:getScene(sceneId, callback)— wrapsGET /functions/v1/scenes/{scene_id}. Returns the full scene response:metadata, asset list with placement and material configs, animations,
collision bindings, scene functions, project info.
getSceneAssets(sceneId, callback)— wrapsGET /functions/v1/scenes/{scene_id}/assets. Asset-list-only variantfor clients that already have scene metadata cached.
Both authenticate with the project API key and return their result inside
the existing
ApiResult<T>wrapper. The full asset shape (SceneAPIAsset)and scene-response shape (
SceneData) are added toRVCCAApiTypes.hfordirect C++ consumers.
The endpoints are exposed to JS via
ViroARSceneNavigator.rvGetScene(sceneId)andViroARSceneNavigator.rvGetSceneAssets(sceneId). Existing Cloud Anchor andGeospatial endpoints are unchanged.
Fixes
16 KB
.sopage alignment forlibvrapi.so(Issue A). The Android2025 ABI requires all shipped
.sofiles to align to 16 KB pages.libvrapi.sois now repackaged with-Wl,-z,max-page-size=16384,resolving load failures on devices with the new page size.
AR Image Markers — children fixed-on-screen after re-detection
(Android, GitHub viro #465).
Models parented to a
ViroARImageMarkerno longer pin to screencoordinates after the target was lost and re-acquired in v2.54.0. Markers
re-anchor cleanly to the detected world pose every time, including
subsequent re-detection.
iOS
ViroPortalSceneportal-tree stability(GitHub viro #452).
Continued portal-render-pass hardening on top of the v2.54.0 fix
(
VROPortalTreeRenderPass.cpp+VROPortal.cpp):before the alpha-discard modifier runs
(
_silhouetteMaterial->setAlphaCutoff(0.0f)).camera background drawn afterwards (
depth = 0.9999plus depth-writeenabled on the portal background sphere / cube).
when the user is outside a nested exit-frame portal (skip stencil
DECR for
isExit = true, useEqualstencil function whenanyChildIsExit = true).recursionLevel > 0so virtual content is no longer discarded bydepth-based occlusion in nested portals.
Compatibility
Backward compatible with all existing single-pointer backends. iOS,
AR (ARKit/ARCore), Cardboard, OVR, Daydream and Wasm continue to call the
unchanged
updateHitNode(camera, origin, ray)overload and observe nobehavior change in hover / click / drag dispatch.
Android 2025 ABI (16 KB-aligned
.sofiles) requires AndroidGradle Plugin 8.7+ in the consuming app. Minimum SDK unchanged.
Meta Quest support requires the consuming app to register
ReactViroPackage(ReactViroPackage.ViroPlatform.QUEST)inMainApplication.ktand declare the dual-Activity manifest entries(
VRActivity, thecom.oculus.intent.category.VRintent filter, Questhardware features, hand-tracking and passthrough permissions). The Viro
Expo plugin emits all of this automatically when
xRModeincludes"QUEST". Bare Android consumers will need to wire it manually.Install path
Bare React Native is not tested for this release. ViroCore should work but
will require a substantial amount of manual wiring on the Android side
(VRActivity, manifest features, package registration in
MainApplication.kt) and iOS Podfile entries. For now we recommendinstalling
@reactvision/react-virowith the Expo Dev Client, wherethe Viro Expo plugin emits all of the platform configuration
automatically. Bare RN support will be revisited in a follow-up release.