feat(xr)!: consumer-owned WebXR — decouple the frame loop from core; add createXR/useXR#62
Merged
Merged
Conversation
commit: |
Hand XR loop ownership to the consumer so core depends only on the stable cross-renderer contract (setAnimationLoop-before-setSession + xr.isPresenting), insulating it from the diverging, still-moving WebGLRenderer vs WebGPURenderer XR semantics. (cherry picked from commit 6035ad8)
Spec: one sessionend resume listener + self-stopping isPresenting guard (was two listeners); add moving-target motivation and per-renderer consumer contract. Plan: TDD task breakdown. (cherry picked from commit e06a2f4)
(cherry picked from commit 53ab6db)
(cherry picked from commit 0765607)
Core no longer drives the WebXR frame loop. It keeps its own window loop out of the way via an isPresenting guard + one sessionend resume listener, and depends only on the stable cross-renderer contract (setAnimationLoop-before-setSession + xr.isPresenting). The consumer wires setAnimationLoop/enabled/setSession. Fixes WebGPURenderer XR (the old sessionstart toggle ran too late for the WebGPU manager's setSession snapshot) and the latent frameloop=always + XR double-render. (cherry picked from commit e8b434c)
(cherry picked from commit fc24a4d)
(cherry picked from commit 6a84ebc)
Code-review follow-ups: - canvas.tsx resize no longer issues a window-driven render while an XR session is presenting (the third window driver that bypassed the guard). - extract a single isPresenting() predicate used by loop + requestRender. - sessionend resume repaints only in frameloop="demand", not "never". - useThree docs: correct render signature, drop removed xr member, add a consumer-driven WebXR section. - add coverage for a renderer whose xr manager lacks addEventListener. (cherry picked from commit 8786785)
Design for a createXR() primitive that encapsulates the three sharp edges of the decoupled XR contract (setAnimationLoop-before-setSession snapshot order, the post-exit double-drive, and transient-activation request-first) plus the structural Canvas-context-to-DOM-button gap. Also wires and widens Canvas's currently-dead ref<Context> to support a cleanup return. (cherry picked from commit 99e52ec)
(cherry picked from commit d773f15)
(cherry picked from commit ee763aa)
(cherry picked from commit a6e312f)
(cherry picked from commit 16cb7da)
…overload (cherry picked from commit 2c6c533)
(cherry picked from commit fcaef19)
(cherry picked from commit cb61f7a)
- New API page site/src/routes/api/hooks/create-xr.mdx + sidebar entry - README: createXR section, feature bullet, TOC; fix stale Context.render signature and remove the removed Context.xr field - Canvas ref documented as a cleanup-returning Context ref (README + canvas.mdx) - use-three WebXR section now points at createXR as the recommended path (cherry picked from commit 412c73d)
Cut author-facing internals (snapshot timing, double-drive, window-loop handoff, WebGL-vs-WebGPU parity) from the createXR page and README — those live in the design spec and the useThree manual-driving escape hatch. Keep the one rule a user must follow (call enter from the click handler) as an aside by the VR example. Collapse the Behavior section into the Returns table + a two-line Notes block; trim redundant parentheticals. (cherry picked from commit 2d768be)
…text
connect only ever reads gl and render off the Context, so type its parameter
as XRContext = Pick<Context, 'gl' | 'render'> and export it. A full Context
still satisfies it, so <Canvas ref={xr.connect}> is unchanged (locked by a
compile-time assertion), but the signature now states the real dependency and
a bare { gl, render } works — createXR is tied to neither <Canvas> nor its ref.
Docs + spec reworded to match.
(cherry picked from commit daa4786)
…ate) docs(xr): implementation plan for useXR + createXR().Provider (cherry picked from commit 981163e)
(cherry picked from commit ef5cd60)
(cherry picked from commit 0bd59bb)
(cherry picked from commit 67a2020)
(cherry picked from commit 6426e17)
(cherry picked from commit 9d0dc75)
(cherry picked from commit 64f5500)
(cherry picked from commit 4cd57c4)
4 tasks
bigmistqke
added a commit
to bigmistqke/solid-three
that referenced
this pull request
Jun 3, 2026
docs/superpowers is gitignored as internal process artifacts (specs + plans), but six files committed before that rule landed stayed tracked: the three design specs (via solidjs-community#62) and three plans (the commit that un-tracked plans lived on a deleted branch and never reached next-cleanup). Remove all six from the index (files stay on disk) so tracking matches the ignore rule.
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.
Reworks solid-three's WebXR integration so the consumer owns the session and core stays out of the frame loop while a session presents. Continues #58 on top of a cleaned-up next branch.
Summary
Previously core drove the WebXR loop itself: on the renderer's
sessionstartit toggledsetAnimationLoop(...)and setxr.enabled. That breaksWebGPURenderer— three's WebGPUXRManagersnapshots the renderer's animation loop atsetSessiontime, beforesessionstartfires, so toggling onsessionstartis too late and clobbers the manager's own frame driver. On a Quest this surfaced asgl.getContextAttributes is not a functionon the WebGPU backend, and a hung headset with endless "Not all layers submitted" warnings underforceWebGL. The same internal driver also caused a latentframeloop="always"+ XR double-render.Core now depends only on the stable, cross-renderer contract — the consumer installs
setAnimationLoop(render)beforesetSession, and core reads the read-onlyrenderer.xr.isPresentingto yield its window loop while presenting, resuming via a singlesessionendlistener. No WebGL-vs-WebGPU branching. The session wiring then lives behind one small primitive so consumers can't trip the sharp edges.What changed
createXR()— consumer-owned XR entryA Solid primitive called in a component body (it owns one reactive effect), built only on
context.gl,context.render, and the renderer's standardxrevent target — no core internals, no renderer-family branching. Members:connect(a cleanup-returningRef<XRContext>for<Canvas ref={xr.connect}>),enter(mode, init?)/enter(session),exit(),isSupported(mode),isPresenting(),session(), andProvider. It absorbs the three edges of the contract so the consumer never has to: snapshot order (setAnimationLoop(render)beforesetSession), post-exit double-drive (setAnimationLoop(null)onsessionend), and transient activation (requestSessionfirst, nothing awaited before it).useXR()+createXR().Provider— in-scene stateWrap the subtree (Canvas + its button) in
<xr.Provider>; scene components read{ isPresenting, session, exit }withuseXR()(which throws outside a provider). This serves in-world UI — e.g. a mesh whoseonClickcallsexit(), since the DOM exit button is not rendered while an immersive session presents.useXRis deliberately not the entry API:use*hooks read a provider from inside Canvas, whereascreateXR()is created outside it.Cleanup-returning refs
useRefnow runs a callback ref's returned cleanup viaonCleanup(the React-19 cleanup-ref shape), and<Canvas>'sreftype is widened to allow it (RefWithCleanup<T>). This is what makesxr.connecta one-liner that returns its own disconnect.API surface
New exports:
createXR,useXR, and typesXRContext(Pick<Context, "gl" | "render">) andXRState.connectis typedXRContext, not the fullContext, so its real dependency is explicit and any{ gl, render }works — it is tied to neither<Canvas>nor its ref.Docs
New API pages
create-xr.mdxanduse-xr.mdx;use-three.mdxdocuments the manual escape hatch and points atcreateXRas the recommended path;canvas.mdxdocuments the cleanup-returningref. README gainscreateXR/useXRsections, a feature bullet, and TOC entries. Design specs and TDD plans live underdocs/superpowers/.Breaking
Context.xr({ connect, disconnect }) — XR is now consumer-wired (viacreateXR, orgl.xrdirectly).Context.rendersignature changed (delta→timestamp, addedframe) so theXRFrameflows through touseFrame.Manual escape hatch
createXRis the recommended path, but the raw contract is small enough to wire by hand for both renderers:gl.setAnimationLoop(render)beforegl.xr.setSession(session)(withgl.xr.enabled = true), andgl.setAnimationLoop(null); gl.xr.enabled = falseto exit. For XR withWebGPURendereron three ≤ r184, construct it with{ forceWebGL: true }(WebGPU-backend XR is unreleased upstream).Test plan
pnpm test— 175 passed, 2 todo (12 files): core yields the window loop while presenting and resumes onsessionend; core leavesgl.xr.enabledalone;XRFrameforwarded touseFrame;createXRstate & wiring (snapshot order, post-exit loop-null, renderer-swap re-attach, no-crash on anxrlackingaddEventListener, connect/disconnect);enter(request-first, provided-session overload, error paths);exit/isSupported;useXRthrows outside provider;Providerbridges state across the Canvas boundary; cleanup-returninguseRef.pnpm lint:types— cleanpnpm lint:code— cleanWebGPURenderer({ forceWebGL: true })+ WebXR on Quest 3 (the original report).