Skip to content

feat(XR): WebGPU-compatible XR session + XRGPUBinding plumbing (Phase 1, #18638)#18650

Merged
RaananW merged 5 commits into
masterfrom
raananw-webgpu-xr-phase1-binding-plumbing
Jul 3, 2026
Merged

feat(XR): WebGPU-compatible XR session + XRGPUBinding plumbing (Phase 1, #18638)#18650
RaananW merged 5 commits into
masterfrom
raananw-webgpu-xr-phase1-binding-plumbing

Conversation

@RaananW

@RaananW RaananW commented Jul 2, 2026

Copy link
Copy Markdown
Member

Phase 1 (#18638) — WebGPU-compatible XR session + XRGPUBinding plumbing

Closes #18638 (Phase 1 of the WebGPU-for-WebXR epic #18635 — the epic stays open for Phases 2–5).

Part of the WebGPU-for-WebXR effort (epic #18635). Plumbing only — no per-frame WebGPU XR rendering (Phase 2), no camera/NDC (Phase 3), no feature rewiring (Phase 4). WebGL2 XR behavior is byte-for-byte identical; every WebGPU path is gated behind AbstractEngine.isWebGPU and is unreachable for WebGL engines.

Consolidated from an initial 3-PR stack into this single PR (kept as focused commits for readability). Supersedes #18651 and #18652. Rebased onto master after Phase 0 (#18645) merged.

What's in here

1. TS typings + adapter opt-in

  • New companion packages/dev/core/src/LibDeclarations/webxr.webgpu.d.ts (mirrors the existing webxr.nativeextensions.d.ts split so the large community-maintained webxr.d.ts stays untouched). Declares XRGPUBinding, XRGPUSubImage, and the XRGPU*LayerInit dictionaries. Base XR layer/sub-image types come from webxr.d.ts; GPURequestAdapterOptions.xrCompatible stays in webgpu.d.ts (not duplicated).
  • Documented xrCompatible on WebGPUEngineOptions. It is inherited from GPURequestAdapterOptions and already flows end-to-end (initAsync passes _options straight to navigator.gpu.requestAdapter()), so this is JSDoc/discoverability only — it notes the flag must be set at adapter-request/engine-construction time (WebGPU has no post-hoc "make XR compatible" step).

2. @internal WebGPU graphics binding + wiring

  • WebXRWebGPUGraphicsBinding implementing the Phase-0 IWebXRGraphicsBinding seam, wrapping new XRGPUBinding(session, device), plus a WebXRGraphicsBindingType.WebGPU enum member. Minimal/introduced-but-unused — per-frame layer/sub-image ops are deferred to later phases.
  • WebXRSessionManager._getGraphicsBinding() branches on AbstractEngine.isWebGPU: WebGPU engine → XRGPUBinding-backed binding; everything else → the existing XRWebGLBinding-backed one. The GPUDevice is read from WebGPUEngine._device via a type-only import + cast (mirrors Phase 0's (engine as ThinEngine)._gl), so there is no runtime coupling/side effect. Kept @internal and out of index.ts/pure.ts.

3. WebGPU-compatible session gating

  • initializeSessionAsync requests the 'webgpu' feature descriptor as a required feature when the engine is WebGPU (a WebGPU engine cannot fall back to a WebGL XR session; the native requestSession rejection is left to propagate to the caller, not swallowed). The WebGL branch leaves the XRSessionInit object untouched (same reference passed through — asserted in a test).
  • webXRExperienceHelper.enterXRAsync skips the baseLayer/XRWebGLLayer path for WebGPU, since a WebGPU-compatible session is layers-only.

4. Doc-only clarification of the ENTERING_XR end-state (see below) — comment-only, no logic change.

✅ Hardware-validated Phase 1 end-state (not a regression)

Validated on Meta Quest Browser with the experimental XRGPUBinding flag, against this PR's snapshot build. The precise Phase 1 end-state is:

The native WebGPU XR session enters — an xrCompatible WebGPU engine is created, the 'webgpu' feature is accepted and appears in session.enabledFeatures, baseLayer is correctly skipped, and enterXRAsync resolves without throwing. But because Phase 1 attaches no layer (the XRProjectionLayer is Phase 2), the session receives no requestAnimationFrame callbacks, so onXRFrameObservable never fires and Babylon's WebXRState stays at ENTERING_XR and never reaches IN_XR (which is gated on the first frame). This is the correct Phase 1 result, not a regression.

This "no layer → no frame" behavior was confirmed both through Babylon and at the raw-browser level: a bare navigator.xr immersive-vr session requested with requiredFeatures: ['webgpu'] is granted 'webgpu', yet a requestAnimationFrame with no layer set yields zero callbacks. So the stall is inherent to the layers-only WebGPU-XR design and the WebXR spec, not a bug in this plumbing. Reaching IN_XR / actually rendering is Phase 2 (projection-layer RTT provider via wrapWebGPUTexture).

Exit is clean from ENTERING_XR: ending the session via its native end path (e.g. the headset system menu) fires onXRSessionEnded regardless of WebXRState, restoring the framebuffer/render loop/camera and returning WebXRState to NOT_IN_XR with no hang or leak (the @internal binding is nulled on end). Note: the programmatic exitXRAsync() is a no-op from ENTERING_XR (its pre-existing state !== IN_XR guard, unchanged by this PR) — a no-op, not a hang; the session remains exitable via the native path.

A unit test confirms updateRenderState with neither baseLayer nor layers does not throw. /cc @RaananW.

Spec grounding

Declarations/behavior are taken from the immersive-web/WebXR-WebGPU-Binding explainer + proposed IDL: https://github.com/immersive-web/WebXR-WebGPU-Binding/blob/main/explainer.md — including the 'webgpu' feature descriptor, the layers-only rule (no baseLayer), and the XRGPUBinding(session, device) shape requiring an xrCompatible adapter.

Constraints honored

  • WebGL2 XR byte-for-byte identical; all WebGPU logic gated behind isWebGPU.
  • No .pure.ts split, no top-level side effects (confirmed: no side-effect-manifest churn on any commit).
  • Zero net public-API additions — the binding is @internal and out of the barrels; xrCompatible is an inherited, now-documented option.

Testing

  • tsc -b tsconfig.devPackages.json exit 0.
  • npm run format:check ✅ ; npm run lint:check ✅ (eslint + all 16 tree-shaking checks + side-effects-sync across core/gui/loaders/serializers).
  • XR unit gate vitest run --project=unit -t "XR": 260 passed. webXRSessionManager.test.ts cases cover binding selection (WebGL vs WebGPU), binding caching, the uninitialized-session guard, 'webgpu' injected only for WebGPU engines (existing required features preserved, no duplicates), WebGL init passed through unchanged (same object ref), updateRenderState with no layer not throwing, and the native session-end handler cleaning up (nulls the graphics binding, clears inXRSession, notifies onXRSessionEnded) even when no frame ever arrived. New webXRExperienceHelper.test.ts covers the no-frame WebGPU lifecycle: state stays at ENTERING_XR with no frame, exitXRAsync() from ENTERING_XR is a clean no-op (does not touch the session manager, state unchanged), and a native session end from ENTERING_XR returns state to NOT_IN_XR with no hang or leak.
  • Hardware-validated on Meta Quest Browser (XRGPUBinding flag) against the PR snapshot — see the end-state section above.
  • Note: the unrelated smartFilterBlocks vitest suite fails to import in this environment (missing generated .block.js artifacts) — pre-existing, not touched by this PR.

@RaananW RaananW changed the title feat(XR): WebGPU XR phase 1 — XRGPUBinding typings + xrCompatible opt-in (PR1/3, #18638) feat(XR): WebGPU-compatible XR session + XRGPUBinding plumbing (Phase 1, #18638) Jul 2, 2026
Base automatically changed from raananw-webgpu-xr-phase0-binding-seam to master July 2, 2026 13:18
RaananW and others added 3 commits July 2, 2026 16:02
…ible opt-in

Phase 1 (#18638) plumbing, PR1 of 3. Adds a webxr.webgpu.d.ts companion with
ambient declarations for XRGPUBinding / XRGPUSubImage / XRGPU*LayerInit grounded
in the immersive-web/WebXR-WebGPU-Binding explainer + IDL, and documents the
inherited GPURequestAdapterOptions.xrCompatible opt-in on WebGPUEngineOptions.

No behavior change: xrCompatible already flows through _options to
navigator.gpu.requestAdapter(); this only surfaces/documents it. WebGL paths
untouched.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…lection

Phase 1 (#18638) plumbing, PR2 of 3. Adds the @internal WebGPU implementation of
IWebXRGraphicsBinding (wrapping XRGPUBinding) alongside the existing WebGL one, a
WebXRGraphicsBindingType.WebGPU enum member, and branches
WebXRSessionManager._getGraphicsBinding() on AbstractEngine.isWebGPU so a WebGPU
engine gets the XRGPUBinding-backed binding and everything else keeps the
XRWebGLBinding-backed one.

Introduced-but-unused: per-frame layer/sub-image ops are deferred to later phases.
Kept @internal and out of the XR barrels (mirrors webXRGraphicsBinding.ts). The
GPUDevice is read from WebGPUEngine._device via a type-only import (mirrors the
Phase 0 (engine as ThinEngine)._gl access), so no runtime coupling/side effects.
WebGL2 XR behavior is unchanged.

Adds unit coverage for the WebGL/WebGPU binding selection, caching, and the
uninitialized-session guard.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…only)

Phase 1 (#18638) plumbing, PR3 of 3. When the engine is a WebGPU engine:
- initializeSessionAsync requests the 'webgpu' feature descriptor as a REQUIRED
  feature (a WebGPU engine cannot fall back to a WebGL XR session; the native
  requestSession rejection is left to propagate to the caller, not swallowed).
- webXRExperienceHelper skips the baseLayer/XRWebGLLayer path, since a
  WebGPU-compatible XR session is layers-only per the WebXR/WebGPU binding spec.

The WebGPU XRProjectionLayer is added in a later phase, so a WebGPU XR session
currently ENTERS with no layer and renders nothing yet — the expected Phase 1
end-state, not a bug. WebGL2 XR behavior is byte-for-byte identical: the WebGL
branch leaves the session init object untouched and keeps the baseLayer path.

Adds unit coverage: 'webgpu' injected only for WebGPU engines, existing required
features preserved without duplicates, WebGL init passed through unchanged, and
updateRenderState with neither baseLayer nor layers does not throw.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@RaananW RaananW force-pushed the raananw-webgpu-xr-phase1-binding-plumbing branch from be37024 to fa08272 Compare July 2, 2026 14:06
@bjsplat

bjsplat commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.

@bjsplat

bjsplat commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Snapshot stored with reference name:
refs/pull/18650/merge

Test environment:
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18650/merge/index.html

To test a playground add it to the URL, for example:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18650/merge/index.html#WGZLGJ#4600

Links to test your changes to core in the published versions of the Babylon tools (does not contain changes you made to the tools themselves):

https://playground.babylonjs.com/?snapshot=refs/pull/18650/merge
https://sandbox.babylonjs.com/?snapshot=refs/pull/18650/merge
https://gui.babylonjs.com/?snapshot=refs/pull/18650/merge
https://nme.babylonjs.com/?snapshot=refs/pull/18650/merge

To test the snapshot in the playground with a playground ID add it after the snapshot query string:

https://playground.babylonjs.com/?snapshot=refs/pull/18650/merge#BCU1XR#0

If you made changes to the sandbox or playground in this PR, additional comments will be generated soon containing links to the dev versions of those tools.

@bjsplat

bjsplat commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

🟢 Memory Leak Test Results

4 passed, 0 leaked out of 4 scenarios

🟢 All memory leak tests passed — no leaks detected.

Passed Scenarios (4)
Scenario Package
Core Playground #2FDQT5#1508 @babylonjs/core
Core Playground #T90MQ4#14 @babylonjs/core
Core Playground #8EDB5N#2 @babylonjs/core
Core Playground #LL5BIQ#636 @babylonjs/core

@bjsplat

bjsplat commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

@RaananW RaananW marked this pull request as ready for review July 3, 2026 09:09
Copilot AI review requested due to automatic review settings July 3, 2026 09:09
…n layer

A layers-only WebGPU XR session attaches no layer in Phase 1, so it receives
no requestAnimationFrame callbacks and WebXRState remains ENTERING_XR (never
IN_XR, which is gated on the first frame) until the Phase 2 XRProjectionLayer
produces the first frame. Documents the hardware-confirmed Phase 1 end-state
so it is not mistaken for a regression. Comment-only, no logic change.

Refs #18638

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Implements Phase 1 of the WebGPU-for-WebXR effort by adding the minimal plumbing needed for a WebGPU engine to request a WebGPU-compatible XR session (via the "webgpu" required feature) and to construct an XRGPUBinding-backed graphics binding, while preserving byte-for-byte identical behavior for WebGL/WebGL2 XR paths.

Changes:

  • Added ambient TypeScript declarations for the WebXR/WebGPU Binding types (XRGPUBinding, XRGPUSubImage, and XRGPU*LayerInit dictionaries) without modifying the community webxr.d.ts.
  • Introduced an internal WebXRWebGPUGraphicsBinding and updated session manager binding selection based on engine.isWebGPU.
  • Gated XR session creation + enter flow for WebGPU (inject required "webgpu" feature; skip baseLayer/XRWebGLLayer path), with unit tests covering the new branching behavior.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/dev/core/test/unit/XR/webXRSessionManager.test.ts Adds unit tests for WebGPU vs WebGL binding selection, "webgpu" required-feature injection, pass-through behavior for WebGL, and Phase 1 “no layer” render state.
packages/dev/core/src/XR/webXRSessionManager.ts Selects WebGL vs WebGPU graphics binding based on isWebGPU; injects "webgpu" into requiredFeatures only for WebGPU engines.
packages/dev/core/src/XR/webXRGraphicsBinding.ts Adds WebXRGraphicsBindingType.WebGPU and internal WebXRWebGPUGraphicsBinding wrapping new XRGPUBinding(session, device).
packages/dev/core/src/XR/webXRExperienceHelper.ts Skips baseLayer initialization when running on a WebGPU engine (layers-only session path).
packages/dev/core/src/LibDeclarations/webxr.webgpu.d.ts New companion ambient typings for the proposed WebXR/WebGPU binding API surface.
packages/dev/core/src/Engines/webgpuEngine.pure.ts Documents xrCompatible on WebGPUEngineOptions for discoverability (inherited from GPURequestAdapterOptions).

@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.

Adds unit coverage for the hardware-observed WebGPU Phase 1 flow where a
layers-only session enters but produces no XR frame:
- webXRSessionManager: the native session 'end' handler cleans up (nulls the
  graphics binding, clears inXRSession, notifies onXRSessionEnded) even when no
  frame ever arrived.
- webXRExperienceHelper (new): with a WebGPU engine and no frame, state stays at
  ENTERING_XR; exitXRAsync() from ENTERING_XR is a clean no-op (does not touch
  the session manager, state unchanged); and a native session end from
  ENTERING_XR returns state to NOT_IN_XR with no hang or leak.

Refs #18638

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.

@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Snapshot stored with reference name:
refs/pull/18650/merge

Test environment:
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18650/merge/index.html

To test a playground add it to the URL, for example:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18650/merge/index.html#WGZLGJ#4600

Links to test your changes to core in the published versions of the Babylon tools (does not contain changes you made to the tools themselves):

https://playground.babylonjs.com/?snapshot=refs/pull/18650/merge
https://sandbox.babylonjs.com/?snapshot=refs/pull/18650/merge
https://gui.babylonjs.com/?snapshot=refs/pull/18650/merge
https://nme.babylonjs.com/?snapshot=refs/pull/18650/merge

To test the snapshot in the playground with a playground ID add it after the snapshot query string:

https://playground.babylonjs.com/?snapshot=refs/pull/18650/merge#BCU1XR#0

If you made changes to the sandbox or playground in this PR, additional comments will be generated soon containing links to the dev versions of those tools.

@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

🟢 Memory Leak Test Results

4 passed, 0 leaked out of 4 scenarios

🟢 All memory leak tests passed — no leaks detected.

Passed Scenarios (4)
Scenario Package
Core Playground #2FDQT5#1508 @babylonjs/core
Core Playground #T90MQ4#14 @babylonjs/core
Core Playground #8EDB5N#2 @babylonjs/core
Core Playground #LL5BIQ#636 @babylonjs/core

@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

@RaananW RaananW enabled auto-merge (squash) July 3, 2026 10:23
@RaananW RaananW merged commit 8844c93 into master Jul 3, 2026
22 checks passed
@RaananW RaananW deleted the raananw-webgpu-xr-phase1-binding-plumbing branch July 3, 2026 11:46
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.

[WebGPU-XR][Phase 1] WebGPU-compatible XR session + XRGPUBinding plumbing

4 participants