Skip to content

[WebGPU-XR][Phase 3] WebXR projection matrix / NDC handling + upright projection-layer rendering - #18688

Draft
RaananW wants to merge 4 commits into
masterfrom
raananw-webgpu-xr-phase3-projection-ndc
Draft

[WebGPU-XR][Phase 3] WebXR projection matrix / NDC handling + upright projection-layer rendering#18688
RaananW wants to merge 4 commits into
masterfrom
raananw-webgpu-xr-phase3-projection-ndc

Conversation

@RaananW

@RaananW RaananW commented Jul 13, 2026

Copy link
Copy Markdown
Member

Phase 3 of WebGPU-for-WebXR (refs #18639)

DRAFT — pending in-headset validation on Meta Quest (WebGPU). Do not merge until RaananW confirms.

Builds on Phase 2 (#18655), which reached IN_XR and rendered both eyes on Quest via the WebGPU projection-layer RTT provider but left two in-headset symptoms: the image was vertically flipped, and geometry disappeared (meshes flash for ~1 frame on entry, then are gone while the clear color remains).

Root cause of the flip (proven)

The WebGPU engine draws every render target Y-flipped: it binds the inverting InternalsUBO (yFactor = -1, webgpuEngine.pure.ts:3750) so the vertex shader does position.y *= yFactor_ (webgpuShaderProcessorsWGSL.pure.ts:409), plus a compensating front-face winding flip (:3716). This exists so that a later-sampled RTT matches the WebGL texture-space convention.

XR projection-layer textures are presented directly by the XR compositor (top-left origin, per the WebXR-WebGPU spec's plain render pass) and are never re-sampled by Babylon. So the engine's RTT Y-flip makes the in-headset image upside-down. This matches the confirmed in-headset report ("down is up, up is down").

The depth-range concern from the issue is confirmed benign: per spec, a WebGPU XRSession's XRView.projectionMatrix is authored for a [0,1] clip-space depth range, matching WebGPUEngine.isNDCHalfZRange = true. It is consumed verbatim (webXRCamera.ts:323, and freezeProjectionMatrix bypasses the engine's depth remap), so there is no double-conversion and no range clipping.

On the disappearance — UNPROVEN hypothesis, with a known gap

This PR definitively fixes the flip. It may or may not fix the disappearance; that is an unproven hypothesis and the in-headset test is the sole decider — do not read this PR as a proven two-for-one.

The candidate mechanism: the same Y-flip flips the submitted depth relative to the [0,1] projection matrix, and the compositor uses that depth. Known gap: the reported symptom is geometry gone while motionless, and with zero head motion positional reprojection/timewarp is ~identity, so flipped depth would not clearly warp geometry off-view without a warp delta. The depth-reprojection story therefore does not cleanly explain the motionless case. Treated as an open question, not a claim.

Fix (WebGPU + XR only)

Add an @internal WebGPURenderTargetWrapper._disableEngineYFlip flag, set only by the WebGPU XR render-target provider, that makes the engine render that target upright — non-inverting InternalsUBO (yFactor = +1) and main-framebuffer winding, exactly like the canvas. Viewport/scissor stay in the top-left render-target branch because the XR sub-image viewport is already top-left.

  • webgpuRenderTargetWrapper.ts@internal _disableEngineYFlip = false
  • webXRWebGPURenderTargetTextureProvider.ts — set the flag on the created per-eye XR RTT
  • webgpuEngine.pure.ts — honor the flag at the two RTT Y-flip sites (yFactor bind + front-face winding); the derived invertYRenderTarget reproduces the original mapping exactly for canvas and non-XR RTTs

Constraints upheld

  • WebGL2 XR byte-identical: the flag is never set on the WebGL path or on non-XR targets; base RenderTargetWrapper, the WebGL provider, and webXRCamera are untouched.
  • Zero net public API: the flag is @internal on the WebGPU subclass; side-effects manifest and tree-shaking barrels are unchanged.

Gates (run locally, real deps)

  • tsc -b packages/dev/core/tsconfig.build.json → exit 0
  • vitest run --project=unit XR → 275 passed (incl. a new test asserting the flag is set on the XR RTT and left false on plain RTTs)
  • check:side-effects-sync → up-to-date
  • prettier --check clean; eslint 0 errors

In-headset read (Quest, WebGPU) — three branches

  1. Upright + geometry back ⇒ flip and disappearance were unified; ship.
  2. Upright but still gone ⇒ the disappearance is independent of the flip; the flip fix here is still correct and required, and the disappearance is chased separately next.
  3. Still flipped ⇒ the flag didn't take; debug wiring.

Refs #18639

…engine RTT Y-flip)

The WebGPU engine draws every render target Y-flipped (InternalsUBO yFactor = -1
at webgpuEngine.pure.ts, plus compensating front-face winding) so a later-sampled
RTT matches the WebGL texture-space convention. XR projection-layer textures are
presented directly by the XR compositor (top-left origin, per the WebXR/WebGPU
spec's plain render pass) and are never re-sampled by Babylon, so that flip makes
the in-headset image upside-down and its depth inconsistent with the [0,1]
projection matrix the compositor uses for reprojection.

Add an @internal WebGPURenderTargetWrapper._disableEngineYFlip flag, set only by
the WebGPU XR render-target provider, that makes the engine render the target
upright (non-inverting InternalsUBO + main-framebuffer winding), matching the
canvas. Viewport/scissor stay in the top-left render-target branch since the XR
sub-image viewport is already top-left.

WebGPU + XR only: the flag is never set on the WebGL2 path or on non-XR targets,
so WebGL2 XR stays byte-identical and non-XR WebGPU rendering is unchanged. Zero
net public API (flag is @internal on the WebGPU subclass).

Refs #18639

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

bjsplat commented Jul 13, 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 13, 2026

Copy link
Copy Markdown
Collaborator

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

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

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

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

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

bjsplat commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 13, 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 13, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

@bjsplat

bjsplat commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

…ebGPU) engines

On a WebGPU engine (isNDCHalfZRange === true) the clip-space depth range is
[0, 1], while WebGL/OpenGL uses [-1, 1]. WebXRCamera copies the XR binding's
projection matrix verbatim and the rig cameras freeze their projection, so the
engine's range-aware projection builders are bypassed. If the binding returns a
[-1, 1]-convention matrix while the engine clips at [0, 1], every fragment with
NDC z in [-1, 0) is clipped and all world geometry disappears (clear color still
shows) - matching the in-headset "meshes flash a frame then vanish" symptom.

Fix (WebGPU-gated, WebGL2 byte-identical, zero net public API):
- Add @internal Matrix.convertProjectionToHalfZRangeInPlace(), reusing the
  engine's existing mtxConvertNDCToHalfZRange remap (the same conversion the
  perspective/orthographic builders apply when halfZRange is set).
- In WebXRCamera._updateFromXRSession, on a half-Z engine, empirically detect the
  convention the binding used (project the view-space near point through the raw
  matrix: NDC z ~= -1 => [-1, 1], ~= 0 => [0, 1]) and convert only a [-1, 1]
  matrix, so a spec-compliant [0, 1] binding is never double-converted. Guards
  minZ <= 0 (avoids divide-by-zero) and assumes non-reverse-Z. WebGL2
  (isNDCHalfZRange === false) never enters the block.

Adds 6 unit tests: [-1,1]->[0,1] conversion, [0,1] left unchanged, WebGL2
byte-identical, asymmetric off-center frusta, minZ<=0 guard, and converter
equivalence to the engine's half-Z builder.

Refs #18639

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

bjsplat commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 13, 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 13, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

@bjsplat

bjsplat commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 15, 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 15, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 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 17, 2026

Copy link
Copy Markdown
Collaborator

@RaananW
RaananW force-pushed the raananw-webgpu-xr-phase3-projection-ndc branch from ccd3a34 to 292e86f Compare July 17, 2026 12:27
@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 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 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 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 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

RaananW added a commit that referenced this pull request Jul 17, 2026
Phase 3 (#18639, PR #18688) diagnostic — NOT a shipping change; reverted before
Phase 3 lands. Discriminates whether a raw draw into the compositor sub-image
presents when it is the SOLE writer of that sub-image, submitted on its OWN
command encoder + OWN queue.submit (mirroring the working bare WebGPU-WebXR
sample), run inside Babylon's XR frame loop.

Confined to the WebGPU-XR provider (WebXRWebGPUCompositionLayer.ts); WebGL2
provider untouched. @internal, zero net public API. Distinct from the NDC and
Y-flip commits.

- Divert Babylon's per-eye scene render into a per-eye throwaway GPUTexture so the
  real compositor sub-image is never touched by Babylon this frame (no clear/draw),
  killing the endFrame single-submit overwrite confound.
- Own-encoder + own-submit green-on-blue triangle into the real sub-image
  (createView only, never destroys the compositor texture).
- Gated on a real WebGPU _device; NullEngine path is inert passthrough (exact
  repoint baseline), so the XR unit suite is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@RaananW
RaananW force-pushed the raananw-webgpu-xr-phase3-projection-ndc branch from 3f303ab to ee019c9 Compare July 17, 2026 16:23
@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 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 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

@matthargett

Copy link
Copy Markdown
Contributor

@RaananW I opened #18711 for the separate capability-order prerequisite found during broader WebGPU runtime testing. WebXRLayers.isCompatible() currently rejects native sessions before checking whether a WebGPU engine exposes XRGPUBinding.createProjectionLayer.

#18711 contains only the minimal reorder and five browser/native, WebGPU/WebGL compatibility tests. It makes the existing WebGPU layer provider reachable; this draft can remain focused on projection/NDC and upright rendering once that path is active. There is no file overlap.

@RaananW
RaananW force-pushed the raananw-webgpu-xr-phase3-projection-ndc branch from ee019c9 to 8bee2b9 Compare July 20, 2026 13:04
…rotating-pool fix)

WebGPU composition-layer sub-image textures come from a small rotating pool that
the compositor reads shortly after each eye is rendered. Babylon's default flow
records both eyes into the single shared render encoder and submits it once, late,
at endFrame; on the rotating pool that late submit can land after the slot has
already been recycled, so the eye's contents never present (observed in-headset as
the scene flashing for ~2 frames then going black).

Fix: the WebGPU-XR composition-layer provider subscribes to
onAfterCameraRenderObservable (fires per eye, after that eye is rendered into its
sub-image target) and calls engine.flushFramebuffer() to submit that eye's recorded
work immediately, before the compositor recycles the slot. This mirrors the
reference WebXR/WebGPU sample's per-view submit timing.

Confined to the WebGPU-XR provider; the WebGL2 XR path uses a different provider and
is byte-identical. @internal, zero net public API. flushFramebuffer() is already used
mid-frame elsewhere in the engine, so per-eye use is a supported pattern.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@RaananW
RaananW force-pushed the raananw-webgpu-xr-phase3-projection-ndc branch from 8bee2b9 to 0c42466 Compare July 20, 2026 13:28
@bjsplat

bjsplat commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 20, 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 20, 2026

Copy link
Copy Markdown
Collaborator

⚡ Performance Test Results

🟢 All performance tests passed — no regressions detected.

@bjsplat

bjsplat commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

@bjsplat

bjsplat commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

sebavan pushed a commit that referenced this pull request Jul 21, 2026
## Background

Broader WebGPU runtime testing showed that `WebXRLayers.isCompatible()`
rejects every native session before checking the active engine and
`XRGPUBinding` capabilities. A standards-compatible WebGPU runtime can
therefore expose `XRGPUBinding.createProjectionLayer` and still be
prevented from selecting the existing WebGPU projection-layer path.

## Change

- Check WebGPU and `XRGPUBinding.createProjectionLayer` before the
native WebGL fallback.
- Keep native WebGL sessions on the existing `NativeXRRenderTarget`
path.
- Preserve browser WebGPU and WebGL behavior.
- Make no public API changes.

## Tests

Adds five explicit compatibility cases:

- native WebGPU with projection-layer support
- native WebGPU without `XRGPUBinding`
- native WebGL with `XRWebGLBinding`
- browser WebGPU with projection-layer support
- browser WebGL with projection-layer support

Local validation:

- focused fixture: 5/5 tests passed
- XR unit suite: 280/280 tests passed across 16 files
- exact-file ESLint and Prettier checks passed
- production core TypeScript build passed

## Relation to #18688

cc @RaananW. This is intentionally separate from #18688: this PR makes
the WebGPU layer provider reachable when the runtime advertises the
required capability, while #18688 addresses projection/NDC and upright
rendering after that layer path is active. The changes do not overlap
files.
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.

3 participants