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

implementation reference

Need the exact API surface, compatibility boundaries, errors, lifecycle, and source links? Read the matching gendn reference ↗