v147 · WebXR · demo
Compositor Model
The traditional WebXR rendering path composites all content into a single WebGL framebuffer in JavaScript. WebXR Layers hands each surface to the device compositor directly — cutting out a full re-projection pass and letting the GPU process each layer at its native resolution.
Without Layers — single framebuffer
1. JavaScript render loop
App draws 3D scene, UI panels, video, and backgrounds into one WebGL framebuffer.
↓
2. Submit to browser
The single texture is handed to the browser compositor.
↓
3. Re-projection warp
The compositor applies lens distortion correction to the entire frame — including text and UI that was already rendered at framebuffer resolution.
↓
4. Display
Text appears blurry because it was warped twice. All layers share the same resolution regardless of how much screen area they occupy.
With Layers — compositor-managed
1. JavaScript render loop
App renders 3D scene to XRProjectionLayer. UI, video, and backgrounds each go to their own XRQuadLayer / XREquirectLayer etc.
↓
2. Submit layers separately
Each layer is submitted independently. The compositor knows the 3D pose of each layer and can handle it natively.
↓
3. Native compositor pass
The system compositor applies lens correction once, directly to each layer's source pixels — the same path native VR apps use.
↓
4. Display
Text is sharp. Each layer renders at the exact resolution it occupies. Unchanged layers skip re-rendering entirely.
Interactive compositor budget + live layer API probe
0 ms
Single framebuffer: scene + UI + video + one full-frame warp.
0 ms
WebXR Layers: XRProjectionLayer, XRQuadLayer, and media surfaces submitted separately.
Use the controls to estimate compositor cost, then run the live probe to check navigator.xr and layer globals in this browser.
| Aspect | Single framebuffer | WebXR Layers |
|---|---|---|
| Text / UI quality | Blurry (double-warped) | Sharp (native resolution) |
| Static content re-render | Every frame | Only when changed |
| Resolution control | Shared — one size fits all | Per-layer (match use case) |
| Video playback | Decoded then re-encoded into framebuffer | Direct hardware decode path available |
| Compositor passes | 2 (JS + system) | 1 (system only) |
| Depth texture | Not surfaced to compositor | Native depth texture supported |
Creating and using layers — setup code
// Request layers support when creating the session
const session = await navigator.xr.requestSession("immersive-vr", {
requiredFeatures: ["layers"]
});
// Create the WebGL binding that vends layer objects
const binding = new XRWebGLBinding(session, gl);
// Base 3D scene layer (replaces the default framebuffer)
const projLayer = binding.createProjectionLayer({
textureType: "texture-array",
colorFormat: gl.RGBA8,
});
// A crisp UI panel — renders at its own native resolution
const uiLayer = binding.createQuadLayer({
pixelWidth: 1024, pixelHeight: 512,
space: refSpace,
viewPixelWidth: 1024, viewPixelHeight: 512,
});
// Order matters: first in array = rendered first (back to front)
session.updateRenderState({ layers: [projLayer, uiLayer] });
// Each frame: draw into each layer's framebuffer separately
session.requestAnimationFrame((time, frame) => {
const glLayer = binding.getSubImage(projLayer, frame);
gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
draw3DScene();
const uiGl = binding.getSubImage(uiLayer, frame, "none");
gl.bindFramebuffer(gl.FRAMEBUFFER, uiGl.framebuffer);
drawUI(); // sharp at full 1024×512, no warp artefacts
});
see also
- Layer Type Explorer — all five layer types
- Back to feature index
- ChromeStatus entry
implementation reference
Need the exact API surface, compatibility boundaries, errors, lifecycle, and source links? Read the matching gendn reference ↗