Skip to content

Release 3.0.0 - #531

Merged
doranteseduardo merged 234 commits into
mainfrom
develop
Sep 19, 2026
Merged

doranteseduardo merged 234 commits into
mainfrom
develop

Conversation

@doranteseduardo

@doranteseduardo doranteseduardo commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Summary

Release 3.0.0 of @reactvision/react-viro. Merges 193 commits from develop. main contains no commits absent from develop.

Features

Area Description
Co-location Shared coordinate frame plus a peer-state channel. Cloud anchors on iOS/Android, Meta shared spatial anchors on Quest, SharedCoordinateSpaceProvider on visionOS. Same-device-family sessions only.
Replication Server-authoritative replicated state with total ordering, ownership-gated mutation and optimistic concurrency via expectVersion.
Web platform .web.tsx implementations across the component surface, rendered by @reactvision/viro-web-renderer (WASM/WebGL2 build of virocore).
Quest Passthrough, plane detection via XR_EXT_plane_detection, head-locked UI, hand tracking, navigator reference operations.
visionOS Renderer support and ViroKit.xcframework distribution.
Studio parity Approximately forty rendering differences between the Studio editor and the native runtime resolved, covering transforms, materials, physics, animation and text layout.

Breaking changes

Change Impact
iOS deployment target raised to 15.1 Consuming applications must target iOS 15.1 or later. Required by React Native 0.86 and by Xcode 26+, which rejects lower targets.
Asset scale clamping removed Previously scales below 0.01 were substituted with 0.1 and scales above 10 with 2. Authored values now pass through unchanged. Scenes tuned against the former behaviour will render differently.
Android asset scale multiplier removed The 0.8 factor applied on Android is gone. Models render approximately 25% larger relative to prior releases.
Trigger-image rotation offsets removed Orientation no longer contributes a Z rotation to content. Marker-anchored content previously rendered rotated on non-Up orientations.
@reactvision/viro-web-renderer peer dependency Declared as ^1.0.0 and optional. Required only for the web platform.

Release order

This release spans three repositories. Merge in sequence; each produces the build input for the next.

  1. ReactVision/rvca#10
  2. Release 3.0.0 — renderer virocore#384
  3. This pull request

Pre-merge checklist

  • Feat/co location petko #529 (co-location) merged to develop, if 3.0.0 is to include it. This pull request tracks develop and will incorporate it automatically.
  • Confirm whether ios/dist/lib/libViroReact.a requires a rebuild. It was not included in 8b96433; all subsequent changes to develop are JavaScript-only.
  • Device validation of co-location on two devices of the same family.

Verification

  • npm run build reproduces the committed dist/ with no differences.
  • 187 unit tests pass across 19 suites.
  • Binary chain verified end to end: ReactVisionCCA archives, viro_renderer.so, ViroKit.framework, libViroReact.a, react_viro-release.aar and the JavaScript bundle each carry the co-location symbols.

doranteseduardo and others added 30 commits April 8, 2026 16:48
# Conflicts:
#	dist/index.d.ts
#	dist/index.js
#	index.ts
…anos, matching, onAnchor*/onTrackingUpdated, hit-test
PetkoKuzmanov and others added 23 commits September 18, 2026 11:43
It was committed unformatted, so the next edit to it would have carried the
reflow along with whatever it was actually changing.
Resetting a shared room by deleting entity by entity cannot work: an owned
entity refuses not-owner, so exactly the objects somebody is still holding
would survive the reset. clear is the one operation that ignores ownership.

It is not optimistic. The room is the authority on what a reset removed, and
the deletes come back with one sequence each, which is why a client that
applies them in order empties rather than stopping after the first.
room-too-large has existed on the relay since rooms became persistent and
org-too-large arrived with the per-organisation ceiling; neither was in the
client's union, so an app matching on the reason fell through on both.

They ask for different things, which is why the guide separates them:
room-too-large means this room is at the size its saved copy may be, while
org-too-large is not about this room at all and is answered by closing or
resetting one of the team's others.
VRTColocationModule was listed twice in both PBXBuildFile and PBXFileReference,
under the same object ids. Xcode tolerates it on a read and rewrites one copy
away on its next save, so it reappears as noise in whichever diff comes next.
Produced by the native and TypeScript builds behind the co-location work, not
by hand. The two aars and ViroKit carry the camera intrinsics, sensor pose and
resolve changes already committed in virocore and reactvisioncca; Podfile.lock
moved because the pods were reinstalled against the current Xcode.

dist/ is one build behind source: it has the auth-revoked close handling but
not clear(), which needs another `npm run build` before anything downstream
can import it.
The default was the platform API, which does not speak this protocol. The relay
is a different host, and the doc comment now says so, because
ViroColocationRooms keeps the platform URL on purpose: join codes are a REST
call and cannot share one endpoint with the socket.
Every device in a relay log looks alike, so a refusal or a regression cannot be
tied to a client version. The replication socket now sends x-rv-client beside
its credentials, which the relay logs and never uses for a decision.

Native only, where React Native forwards a third constructor argument to the
native module. The web branch authenticates in the query string and is a
separate decision.

The test matches the shape rather than a number, so a release bump does not
break it.
The doc said every failure was retried. A handshake answered 400, 401 or 403
now reports failed at once, as does a close with reason auth-revoked, while
everything else keeps the five-attempt backoff: the other 1008 reasons, and
the 404 a relay answers while it is restarting.

Replicated state fails fast on auth-revoked only. The WebSocket API hides the
HTTP status of a failed upgrade from JavaScript, so it cannot see the other
three.
Peers are polled rather than pushed, and the poll is not synchronised with the
10 Hz they arrive at, so reading every 100 ms made a pose wait 50 ms on average
for the next read. That is latency on top of the network, and it shows up as a
peer marker lagging the person it marks, which is a safety marker rather than
decoration.

Reading three times as often would otherwise triple the re-renders for nothing,
since most reads return the same list. A read that finds no change now returns
the previous array so React bails out. The timestamp moves with the pose that
carried the position and rotation, so comparing it stands in for comparing
those.
Metro reads dist rather than the sources, so nothing downstream sees the relay
endpoint, the client header or the peer poll interval until this is rebuilt.

useViroReplicatedState is in here for clear(), which landed earlier without a
rebuild.
publishPose takes 16 column-major floats and nothing shipped here built them,
so every app assembled the matrix itself. The camera looks down its own -Z, so
the +Z basis is the negated forward, and getting that backwards points an
avatar away from where its device is pointing while still rendering a
plausible scene. poseCsv holds that, and the degenerate case where someone
looks straight up, where up and forward are parallel and the cross product
collapses.

transformDirection is the other half. locationToWorld moves a point, so a
forward vector through it comes back displaced by the frame's origin, which
drifts as the frame does.

The matrix is built from two basis vectors rather than from Euler angles,
because a triple of angles needs a convention to mean anything and the
convention differs between the editor, the renderer and the scene graph.

VIRO_POSE_INTERVAL_MS mirrors the native poseSendHz default, which drops
anything faster, so an app driving from a 90 Hz camera callback stops
formatting 16 floats for frames that never reach the socket.
The rate lives in RVCCAColocationSession::Config and JS cannot reach it, so
this is the documentation half of the native change. 10 Hz assumed the
receiver interpolates, which nothing did until useViroSmoothedPeers, and it
left a peer marker visibly staler than a replicated object written at 30 Hz on
the other socket.

The poll interval bullet documents a change that already landed; the comment
on pollMs still described the old arrival rate.
The relay broadcasts to the sender as well, so a drag writing every 33 ms over
a 63 ms round trip has two more writes outstanding by the time the first comes
back. Applying that echo puts the object where the hand was two writes ago and
the next write pulls it forward again, which on the device doing the dragging
reads as the object trailing the finger and snapping back, worse the faster
the drag.

The fields already shown are kept while one of this device's own writes is
still in flight; the version and owner the server assigned are adopted either
way, so a field only the server knows about is not lost. Another peer's write
is authoritative as before, since an unowned entity is last-writer-wins and
the server has just said who won.

A delta carries no ref, but the relay applies one connection's ops in the
order they were sent and broadcasts them in that order, so the nth echo
settles the nth write. That is also what now bounds pending, which used to be
cleared wholesale on any delta: doing that dropped the record a later
rejection of an unrelated write needed to roll back.
A peer marker driven straight from peers moves in 50 ms jumps and a replicated
object in 33 ms ones. Both rates assume the receiver fills in between samples
and nothing here did, so a marker read as unreliable, which is the one thing a
safety cue cannot be.

Smoothing is a half-life rather than a per-frame fraction, so it behaves the
same on a 90 Hz headset and a 45 Hz phone, which is how this gets tuned on one
device and looks wrong on the other. A frame long enough to cover several
half-lives snaps, so an app coming back from the background does not glide in
from wherever the room was when it left.

Timing comes from the frame callback and never from a pose's timestampMs,
which is the sender's clock: differencing that against the local one makes the
device with the longer uptime flicker while the other never settles.

useViroSmoothedEntities touches only the fields named, and only where the
value matches the declared shape, so a score or a step index is never averaged
on the way to the screen. Pass localPeerId wherever things are dragged: what
this device owns is passed straight through, because the renderer is already
moving that node under the finger and blending it only puts the rendered
object behind the hand.

A settled list hands back the same references and stops re-rendering, so a
still room costs a frame of arithmetic and nothing else.
Drag callbacks arrive every frame, 72 to 90 a second on a headset, against a
relay budget of 120 a second per peer that closes the socket at 1008 above it
and then refuses that key for 30 seconds. One dragged object exceeds it on its
own and two exceed it twice over, so this is not a ceiling to feel your way up
to.

Plain throttling drops the final position and leaves the object a frame short
of where the hand let go on every other device, so a value arriving inside the
interval is held and sent on the trailing edge, and anything pending is
flushed on unmount.

The deadband is where most of the saving is: placing something precisely is
mostly slow movement, and skipping those writes costs no latency. It compares
against the last value actually sent rather than the previous sample, so a
slow drag cannot creep any distance one sub-threshold step at a time.

CO_LOCATION also gains the pinned-position pattern, which is the other half of
the same problem: the renderer moves a dragged node itself every frame, so
writing position back gives that node two authors and the one arriving through
React is always the older.
Metro reads dist rather than the sources, so nothing downstream can import the
smoothing hooks, the throttled write or the pose helpers until this is rebuilt.
The replication echo fix is in here too.
Rebuilt against the ReactVisionCCA prebuilts virocore now carries, so a device
gets the relay endpoint default, the x-rv-client header, the fail-fast on a
refused handshake, the client address the relay counts against and the 20 Hz
pose rate. Both apps need reinstalling; an OTA update cannot carry this.
Two comments naming the face the web text pipeline renders in. The font moved
in virocore — Helvetica could not be redistributed inside viro-web.data — and a
comment that names the old one sends the next reader looking for a file that is
not there.
The range was ">=0.0.1", which accepts every future major of a package that has
never been published — including one whose C API has moved out from under the
components/Web layer, which is exactly the failure this release just spent a day
tracking down.

It stays optional. Nothing outside the .web.tsx files and components/Web
imports it, in source or in dist, so a native-only install never reaches for it
and most people never will.
Its first release is 1.0.0, not 0.1.0 — the version in the repo was a
placeholder from before anyone had decided. ^0.1.0 would have refused the only
version that will ever exist, and neither git nor npm would have said so: both
PRs merge clean and leave an optional peer that nothing can satisfy.
web: Roboto, and pin the renderer peer to the release it needs
Two files drifted when the Roboto comment landed in ViroText.web.tsx: tsc emits
comments, so a doc change in source is a change in dist. Small, but the whole
point of checking is that dist and source say the same thing.
doranteseduardo and others added 4 commits September 18, 2026 14:46
Built from develop with every co-location branch merged: rvca 4e938d5, virocore
d07a5cea, viro 1ed7463. The renderer AAR and libViroReact.a pick up what landed
after the last rebuild; ViroKit is unchanged in substance and differs only in
the timestamps its resource bundles carry.

Chain verified with scripts/verify_colocation_binaries.sh: 9 of 9 links carry
the feature, from the ReactVisionCCA archives up to the JavaScript bundle.
The release notes and the co-location guide both told readers there is no
persistence and that a restart loses the room. The relay saves a room when its
last peer leaves and reloads it when the room is next opened, so what remains
true is narrower: two instances holding the same room would diverge, and room
homing does not exist yet.

Also records that the web renderer is an optional peer at ^1.0.0, so a project
shipping only to native never needs it installed.
The platform table listed iOS, Android and Horizon OS. visionOS and web both
ship in 3.0.0 and neither was there, so the package's front door undersold two
of its five targets — and the web one carries a condition worth stating, since
its renderer is an optional peer.

Co-location was missing from the features list for the release it headlines.
@doranteseduardo
doranteseduardo merged commit c732a6b into main Sep 19, 2026
1 check passed
@doranteseduardo
doranteseduardo deleted the develop branch September 19, 2026 04:01
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.

5 participants