Skip to content

[XR] WebGPU-XR Phase 0: decouple WebXR from WebGL with API-agnostic seams#18645

Merged
RaananW merged 8 commits into
masterfrom
raananw-webgpu-xr-phase0-binding-seam
Jul 2, 2026
Merged

[XR] WebGPU-XR Phase 0: decouple WebXR from WebGL with API-agnostic seams#18645
RaananW merged 8 commits into
masterfrom
raananw-webgpu-xr-phase0-binding-seam

Conversation

@RaananW

@RaananW RaananW commented Jul 1, 2026

Copy link
Copy Markdown
Member

WebGPU support for WebXR — Phase 0 (behavior-preserving, WebGL-only refactor)

Part of #18637 (epic #18635).

Phase 0 introduces API-agnostic seams into Babylon WebXR so a future WebGPU-backed XR session (layers-only, XRGPUBinding, engine.wrapWebGPUTexture) can plug in later. It is a strict no-op for existing WebGL2 XR — no functional change, no WebGPU/XRGPUBinding code. Public API surface is unchanged (additive/backward-compatible only).

This PR consolidates what was originally staged as three stacked PRs (#18643#18644#18645). They were stacked onto feature branches, which meant the Azure snapshot pipeline (PR trigger is master-only) never produced a preview build for the combined change. Retargeting the tip branch to master yields a single testable, snapshot-backed, mergeable PR containing all of Phase 0.

1) Engine typing

  • Promotes framebufferDimensionsObject (public setter + protected backing field) from ThinEngine up to AbstractEngine. Additive and backward-compatible: ThinEngine/Engine keep consuming/overriding it (Engine keeps the onResize notification; ThinEngine keeps the _gl fallback in getRenderWidth/Height), WebGPU inherits harmless default storage. No new getter — the write-only read semantics are preserved exactly.
  • Retypes WebXRSessionManager._engine and WebXRLayerRenderTargetTextureProvider._engine from Engine to AbstractEngine; removes the scene.getEngine() as Engine casts, plus the leftover as ThinEngine cast in webXRExperienceHelper.ts.

2) RT-provider split

  • WebXRLayerRenderTargetTextureProvider (base) is now graphics-API-agnostic (no WebGL imports) and exposes two protected hooks:
    • _createRenderTargetTextureShell(width, height, multiview) — builds the RenderTargetTexture/MultiviewRenderTarget with the correct sample count.
    • _createRenderTargetTextureInternal(...) — assembles a RenderTargetTexture from already-wrapped InternalTextures with no graphics-API types. This is the seam a future GPU backend uses (wrapWebGPUTexture_createRenderTargetTextureInternal). Intentionally unused in Phase 0.
  • New WebGL-specific abstract provider WebXRWebGLRenderTargetTextureProvider owns the WebGL-typed _createRenderTargetTexture / _createInternalTexture (WebGLHardwareTexture via engine._gl). The exact original creation order is preserved (framebuffer assigned before setTexture).
  • All WebGL-backed subclasses repointed to the new base (WebGL-layer, Composition→Projection, Native).

3) Type generalization + graphics-binding seam

  • WebXRRenderTarget is now generic with WebGL defaults: WebXRRenderTarget<TContext = WebGLRenderingContext, TLayer extends XRLayer = XRWebGLLayer>. Used without type args it resolves to the exact previous shape (compile- and runtime-compatible). Stays public.
  • WebXRManagedOutputCanvas context/layer creation factored into overridable protected seams _createXRCompatibleRenderingContext() and _createXRLayer(); WebGL behavior is byte-for-byte unchanged. Kept off AbstractEngine's public surface deliberately (WebGPU XR is layers-only and may never use a managed-output baseLayer).
  • New side-effect-free WebXRGraphicsBinding abstraction (IWebXRGraphicsBinding + WebXRGraphicsBindingType + WebGL impl WebXRWebGLGraphicsBinding) hiding XRWebGLBinding vs a future XRGPUBinding, plus WebXRSessionManager._getGraphicsBinding(). Marked @internal and not barrel-exported — introduced but unconsumed until Phase 4 proves the per-op shape, so it stays off the public API surface.

Deliberate non-changes / scope

  • The graphics-binding seam and _createRenderTargetTextureInternal are introduced but not yet consumed. XR features (WebXRLayers, WebXRRawCameraAccess, WebXRDepthSensing, WebXRLightEstimation, WebXRSpaceWarp) keep constructing XRWebGLBinding directly — porting them is Phase 4.
  • WebXRLayerWrapper/WebXRLayerType were already API-agnostic; the layerType == "XRWebGLLayer" foveation guard stays for behavior preservation.
  • Multiview color/depth array path (_colorTextureArray/_depthStencilTextureArray) still lives in the WebGL _createRenderTargetTexture; the GPU-agnostic _createRenderTargetTextureInternal only handles the single-InternalTexture path today. Equivalent GPU path is tracked for Phase 2 ([WebGPU-XR][Phase 2] WebGPU projection layer + render target provider #18640).

Validation

  • tsc -b tsconfig.devpackages.json
  • format:check (prettier) ✅ / lint:check (eslint + ratchets) ✅
  • XR + Engines unit tests ✅
  • check:treeshaking (16 checks) ✅ / check:side-effects-sync (all 4 packages) ✅
  • Zero net public-API additions (binding seam is @internal; framebufferDimensionsObject is additive on AbstractEngine; WebXRRenderTarget generics are default-compatible).
  • WebGL2 XR desktop smoke passed (scene renders, session manager constructs under the AbstractEngine retype, context seam yields a real WebGL2RenderingContext). In-headset Quest Browser WebGL2 XR smoke is the final gate before merge.

RaananW and others added 4 commits July 1, 2026 14:11
…XR phase 0)

Part of the WebGPU-for-WebXR decoupling (#18637, epic #18635). Behavior-
preserving, WebGL-only: no functional change for existing WebGL2 XR.

- Promote framebufferDimensionsObject (setter + backing field) from ThinEngine
  up to AbstractEngine so the XR render loop can drive any engine backend.
- Retype WebXRSessionManager._engine and the WebXR RT-provider _engine from
  Engine to AbstractEngine and drop the "as Engine" casts.
- The remaining WebGL _gl access in the RT provider is temporarily narrowed via
  ThinEngine and relocated to a WebGL-specific provider in phase 0 PR 2.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…sionsObject (#18637)

Now that framebufferDimensionsObject lives on AbstractEngine, the spectator-camera
reset in webXRExperienceHelper no longer needs to cast the engine to ThinEngine.
Remove the cast and the now-unused ThinEngine import. No behavior change.

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

Phase 0 of the WebGPU-for-WebXR epic. Behavior-preserving, WebGL-only.

Keep WebXRLayerRenderTargetTextureProvider API-agnostic: it no longer
imports any WebGL type. It exposes two protected hooks:
- _createRenderTargetTextureShell(width, height, multiview): builds the
  RenderTargetTexture/MultiviewRenderTarget with the right sample count.
- _createRenderTargetTextureInternal(...): assembles a RenderTargetTexture
  from already-wrapped InternalTextures, with no graphics-API types. This
  is the seam a future GPU backend (XRGPUBinding + wrapWebGPUTexture) uses.

Move all WebGL framebuffer/texture wiring into a new WebGL-specific
abstract provider, WebXRWebGLRenderTargetTextureProvider, which owns the
WebGL-typed _createRenderTargetTexture and _createInternalTexture
(WebGLHardwareTexture via engine._gl). The exact original creation order
is preserved (framebuffer assigned before setTexture).

Repoint all WebGL-backed subclasses to the new base:
WebXRWebGLLayerRenderTargetTextureProvider, NativeXRLayerRenderTargetTextureProvider,
and WebXRCompositionLayerRenderTargetTextureProvider (and thus the
projection-layer provider that extends it).

No public API removed or renamed. No WebGPU code added.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ics-binding seam (#18637)

Phase 0 of the WebGPU-for-WebXR epic. Behavior-preserving, WebGL-only.

WebXRRenderTarget is now generic with WebGL defaults:
WebXRRenderTarget<TContext = WebGLRenderingContext, TLayer extends XRLayer = XRWebGLLayer>.
Used without type arguments it resolves to the exact previous shape, so this is a
strict compile- and runtime-compatible change. A future non-WebGL target can
specialize the context/layer types.

WebXRManagedOutputCanvas: factor the WebGL context creation (getContext webgl2/webgl)
and the XRWebGLLayer construction into overridable protected seams
(_createXRCompatibleRenderingContext / _createXRLayer). A future non-WebGL managed
output canvas can override them; WebGL behavior is unchanged.

Introduce a WebXRGraphicsBinding abstraction (new side-effect-free file) hiding
XRWebGLBinding vs a future XRGPUBinding: IWebXRGraphicsBinding + WebXRGraphicsBindingType
+ a WebGL implementation (WebXRWebGLGraphicsBinding) whose CreateFromEngine factory
localizes the WebGL context access. WebXRSessionManager exposes a typed accessor
(_getGraphicsBinding) that lazily creates and caches it and resets it on session end.
The seam is introduced but not yet consumed; the XR features keep using XRWebGLBinding
directly and are migrated in a later phase.

No public API removed or renamed. No WebGPU/XRGPUBinding code added.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@RaananW RaananW force-pushed the raananw-webgpu-xr-phase0-rtt-split branch from 3ca1a38 to 8bc6ac9 Compare July 1, 2026 12:38
@RaananW RaananW force-pushed the raananw-webgpu-xr-phase0-binding-seam branch from 212399b to 90635fd Compare July 1, 2026 12:38
…#18637)

The graphics-binding abstraction has no consumer until the features are ported
(Phase 4), and its per-operation shape will be designed then. Avoid committing it
as public API now: mark IWebXRGraphicsBinding, WebXRWebGLGraphicsBinding and
WebXRGraphicsBindingType @internal and drop them from the XR barrels (index.ts /
pure.ts). WebXRSessionManager keeps importing them via the direct relative path,
so nothing internal breaks and the public API surface is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@RaananW RaananW changed the base branch from raananw-webgpu-xr-phase0-rtt-split to master July 2, 2026 09:36
@RaananW RaananW changed the title WebGPU-XR Phase 0 (PR 3/3): generalize XR types + graphics-binding seam [XR] WebGPU-XR Phase 0: decouple WebXR from WebGL with API-agnostic seams Jul 2, 2026
@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/18645/merge

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

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

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18645/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/18645/merge
https://sandbox.babylonjs.com/?snapshot=refs/pull/18645/merge
https://gui.babylonjs.com/?snapshot=refs/pull/18645/merge
https://nme.babylonjs.com/?snapshot=refs/pull/18645/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/18645/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

@RaananW RaananW marked this pull request as ready for review July 2, 2026 10:24
Copilot AI review requested due to automatic review settings July 2, 2026 10:24
@bjsplat

bjsplat commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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

Refactors the WebXR rendering pipeline to introduce graphics-API-agnostic seams (preparing for a future WebGPU-backed XR path) while preserving current WebGL XR behavior, primarily by generalizing engine typings and splitting WebGL-specific render-target wiring into dedicated abstractions.

Changes:

  • Promotes framebufferDimensionsObject storage to AbstractEngine and updates WebXR engine references to AbstractEngine.
  • Splits XR render-target-texture provider into an API-agnostic base plus a WebGL-specific provider to isolate WebGL framebuffer/texture wiring.
  • Generalizes WebXRRenderTarget typing and introduces internal seams for managed-output-canvas context/layer creation and a (currently unused) graphics-binding abstraction.

Reviewed changes

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

Show a summary per file
File Description
packages/dev/core/src/XR/webXRWebGLRenderTargetTextureProvider.ts New WebGL-specific XR RTT provider that owns WebGL framebuffer/texture wiring.
packages/dev/core/src/XR/webXRWebGLLayer.ts Switches WebGL layer RTT provider to extend the new WebGL-specific base provider.
packages/dev/core/src/XR/webXRTypes.ts Makes WebXRRenderTarget generic with WebGL-defaulted type parameters.
packages/dev/core/src/XR/webXRSessionManager.ts Retypes engine to AbstractEngine and adds an internal lazy graphics-binding accessor.
packages/dev/core/src/XR/webXRRenderTargetTextureProvider.ts Makes the base RTT provider API-agnostic and adds shell/internal RTT creation hooks.
packages/dev/core/src/XR/webXRManagedOutputCanvas.ts Adds protected seams for XR-compatible context creation and XR layer creation.
packages/dev/core/src/XR/webXRGraphicsBinding.ts Introduces an internal graphics-binding abstraction with a WebGL implementation.
packages/dev/core/src/XR/webXRExperienceHelper.ts Removes ThinEngine cast for framebufferDimensionsObject usage.
packages/dev/core/src/XR/native/nativeXRRenderTarget.ts Repivots native provider to the WebGL-specific provider base.
packages/dev/core/src/XR/features/Layers/WebXRCompositionLayer.ts Repivots composition-layer provider to the WebGL-specific provider base.
packages/dev/core/src/Engines/thinEngine.pure.ts Removes framebufferDimensionsObject storage from ThinEngine (now inherited).
packages/dev/core/src/Engines/abstractEngine.pure.ts Adds framebufferDimensionsObject storage/setter to AbstractEngine.

Comment thread packages/dev/core/src/XR/webXRWebGLRenderTargetTextureProvider.ts
Comment thread packages/dev/core/src/XR/webXRGraphicsBinding.ts
Comment thread packages/dev/core/src/XR/webXRSessionManager.ts
Comment thread packages/dev/core/src/XR/webXRRenderTargetTextureProvider.ts
Comment thread packages/dev/core/src/Engines/abstractEngine.pure.ts
Comment thread packages/dev/core/src/XR/webXRManagedOutputCanvas.ts
@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

- Guard the WebGL `_gl` access in WebXRWebGLRenderTargetTextureProvider and
  WebXRWebGLGraphicsBinding.CreateFromEngine so a non-WebGL engine fails with a
  clear, targeted error instead of a confusing downstream one.
- Guard WebXRSessionManager._getGraphicsBinding against a disposed engine and a
  not-yet-initialized session (also drops the non-null assertion).
- Explicitly disallow multiview in the API-agnostic
  _createRenderTargetTextureInternal hook, since only the single-texture path is
  wired there today (the WebGL provider still owns the multiview array path).
- Fix the framebufferDimensionsObject doc comment moved onto AbstractEngine
  (no gl fallback at that level).

No WebGL2 XR behavior change; guards only fire on paths unreachable in Phase 0.

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

@sebavan sebavan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@RaananW would be great to add tests if possible to prevent future regressions

@RaananW

RaananW commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

@RaananW would be great to add tests if possible to prevent future regressions

The last phase of this work is documentation and full tests. It's going to be all in all 5 phases.
Adding some unit tests now to this PR as well.

@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.

Add unit tests locking in the guard contracts introduced by the Phase 0 seams,
all using NullEngine (no real XR session / WebGL context required):

- WebXRSessionManager._getGraphicsBinding throws before the session is
  initialized and after the engine is disposed.
- WebXRWebGLGraphicsBinding.CreateFromEngine throws for a non-WebGL engine.
- WebXRLayerRenderTargetTextureProvider._createRenderTargetTextureInternal
  throws for multiview (owned by the WebGL provider path).

Behavior-preserving: these assert already-shipped guards; no source change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@RaananW RaananW enabled auto-merge (squash) July 2, 2026 12:23
@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 merged commit ac8318c into master Jul 2, 2026
22 checks passed
@RaananW RaananW deleted the raananw-webgpu-xr-phase0-binding-seam branch July 2, 2026 13:18
RaananW added a commit that referenced this pull request Jul 3, 2026
… 1, #18638) (#18650)

## 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.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

4 participants