Visitar URL original
Support WebXR DirectRenderPipeline on VisionOS by AdaRoseCannon · Pull Request #35028 · mrdoob/three.js · GitHub
Skip to content

Support WebXR DirectRenderPipeline on VisionOS - #35028

Open
AdaRoseCannon wants to merge 2 commits into
mrdoob:devfrom
AdaRoseCannon:webgpu-xr-visionos-depth
Open

AdaRoseCannon wants to merge 2 commits into
mrdoob:devfrom
AdaRoseCannon:webgpu-xr-visionos-depth

Conversation

@AdaRoseCannon

Copy link
Copy Markdown
Contributor

Support WebXR DirectRenderPipeline on VisionOS by sizing the XR render target from the projection layer, using its logical viewport, and supplying single-sample foveated depth for reprojection

Description

VisionOS can't expose the dimensions of the projection layer before the first frame, so the XR render target is resized when the size is available

VisionOS requires the depth buffer for reprojection so when the depth buffer is not ignored the WebXR provided depth texture is used

On visionOS the sub-image viewport is the logical (pre-foveation) size, which is larger than the projection layer texture size so that is used for rendering

…et from the projection layer, using its logical viewport, and supplying single-sample foveated depth for reprojection
@AdaRoseCannon

AdaRoseCannon commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

cc @cabanier (tested on quest and visionOS)

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

📦 Bundle size

Full ESM build including core, minified and gzipped.

Before After Diff
Core 391.30
102.65
391.30
102.65
+0 B
+0 B
WebGL 777.73
194.24
777.73
194.24
+0 B
+0 B
WebGPU 1099.90
296.20
1101.57
296.62
+1.66 kB
+423 B
WebGPU Nodes 1097.85
295.84
1099.51
296.27
+1.66 kB
+426 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 518.57
124.62
518.57
124.62
+0 B
+0 B
WebGPU 778.14
208.33
779.77
208.74
+1.63 kB
+405 B
WebGPU Nodes 725.26
195.14
726.89
195.54
+1.63 kB
+402 B

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

@AdaRoseCannon , your change disables MSAA in WebXR.
Are you planning on bringing it back in a follow up PR?

Comment thread src/renderers/common/XRManager.js Outdated
viewDescriptors: [],
viewports: []
viewports: [],
depthTextures: [],

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.

The spec says that the same texture needs to be returned for left and right eye. Treat the depth texture like colorTexture

Comment thread src/renderers/common/XRManager.js Outdated

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.

Should we pass depthStencilFormat here? The current WebXR WebGPU spec says omitting it means no depth texture is allocated. That leaves ignoreDepthValues true, so the new supplied-depth path depends on the browser implicitly allocating depth.

Alternatively, we can update the spec to leave it up to the user agent

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd rather follow the spec at this point, so i'll fix it :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

if we put it here will Quest then supply a depth buffer? I think it will ignore this and it won't, right?

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.

if you put it here, quest will create a depth buffer. Use usesDepthValues on the gpubinding. If it's true, ask for depth textures

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

thank you!

Comment thread src/renderers/common/XRManager.js Outdated
if ( supplyDepth === true && gpuSubImage.depthStencilTexture ) {

viewData.depthTextures.push( gpuSubImage.depthStencilTexture );
viewData.depthViewDescriptors.push( gpuSubImage.getViewDescriptor() );

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.

getViewDescriptor() returns a descriptor usable for both color and depth. Can we reuse viewDescriptors and remove depthViewDescriptors and the second call?

xrRenderTarget.dispose();

// The external texture can be registered before the render target is initialized.
for ( const texture of xrRenderTarget.textures ) {

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.

Can we also clear the external depth wrapper’s backend record here?
Its registration bypasses Textures.updateTexture(), so normal texture cleanup does not remove it, and this loop only handles color textures. We should release the cached depth references without destroying the browser-owned GPU texture

Comment thread src/renderers/common/XRManager.js Outdated
}

// The pass viewport comes from the render target viewport. Use the logical sub-image
// viewport: on visionOS it is larger than the foveated texture and the runtime maps it.

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.

Could we describe this as “using runtime-provided single-sample depth for reprojection”? The logical viewport explanation is useful, but please clarify that foveation is handled by the runtime and this PR consumes its viewport and depth texture.

@AdaRoseCannon

Copy link
Copy Markdown
Contributor Author

@AdaRoseCannon , your change disables MSAA in WebXR. Are you planning on bringing it back in a follow up PR?

The issue is because the same sample count is needed for all the attachments in the render pass and the WebXR provided depth and colour textures have sampleCount: 1. So without a WebXR supplied depth attachment the colour can be resolved down to write correctly, but that can't be performed for depthStencilTextures as far as I can tell, so when there is a WebXR supplied depth texture then they all need to render single-sample. This is only when there is a WebXR supplied depthTexture though, so Quest should be unaffected.

I wasn't planning a follow up PR. if you have an idea for a solution it would be very welcome

@cabanier

Copy link
Copy Markdown
Contributor

I wasn't planning a follow up PR. if you have an idea for a solution it would be very welcome

I think there's a way to do it but we'll have to teach three.js on how to resolve depth. We can do that after this lands.

…, request depth unless usesDepthValues is false, release the depth record on session end
@AdaRoseCannon

Copy link
Copy Markdown
Contributor Author

I wasn't planning a follow up PR. if you have an idea for a solution it would be very welcome

I think there's a way to do it but we'll have to teach three.js on how to resolve depth. We can do that after this lands.

Oh nice, thanks!

@cabanier

cabanier commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Your change looks good! There could be instances (ie color mapping) where depth is lost.
I think we can detect when this happens and then print a warning to the console. Alternatively, we can also exit the session.
If we want to add that, it can be in a follow up PR.

@AdaRoseCannon

Copy link
Copy Markdown
Contributor Author

Your change looks good! There could be instances (ie color mapping) where depth is lost. I think we can detect when this happens and then print a warning to the console. Alternatively, we can also exit the session. If we want to add that, it can be in a follow up PR.

I tested to see what happens if depth isn't provided, it doesn't break everything but the quality of the reprojection drops significantly. This usually looks like warping around objects close to the viewer.

@cabanier

Copy link
Copy Markdown
Contributor

ok, so a warning should suffice.

@Mugen87

Mugen87 commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Just for my understanding: DirectRenderPipeline should be the only way WebGPURenderer can be used with VisionOS, right?

Without DirectRenderPipeline, a plain renderer.render() with the default sRGB output (or any tone mapping) needs two render passes (beauty + output). AFAICS, the depth texture the PR requests from VisionOS never gets written in this case. Is that correct?

if ( xrRenderTarget.depthTexture !== null ) {

xrRenderTarget.depthTexture.image.width = layerWidth;
xrRenderTarget.depthTexture.image.height = layerHeight;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this resize is redundant. Textures.updateRenderTarget() does that for you.

const depthFormat = this._renderer.stencil ? DepthStencilFormat : DepthFormat;
const depthType = this._renderer.stencil ? UnsignedInt248Type : UnsignedIntType;
depthTexture = new DepthTexture( glProjLayer.textureWidth, glProjLayer.textureHeight, depthType, undefined, undefined, undefined, undefined, undefined, undefined, depthFormat, 2 );
depthTexture.isArrayTexture = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This line is redundant as well. Textures.updateRenderTarget() should set that flag automatically.

@AdaRoseCannon

Copy link
Copy Markdown
Contributor Author

Just for my understanding: DirectRenderPipeline should be the only way WebGPURenderer can be used with VisionOS, right?

That's correct.

Without DirectRenderPipeline, a plain renderer.render() with the default sRGB output (or any tone mapping) needs two render passes (beauty + output). AFAICS, the depth texture the PR requests from VisionOS never gets written in this case. Is that correct?

Yes that's correct. A single write without the depth buffer causes some distortion but doesn't make an experience unusable.

Unfortunately the two writes cause foveation to get applied twice which results in pinched appearance which is unusable. We've had some discussions recently in the Immersive Web WG about ways to support multiple render passes with foveation but it still needs further discussion with the WebGPU WG before we can decide on a solution.

@cabanier

Copy link
Copy Markdown
Contributor

Unfortunately the two writes cause foveation to get applied twice which results in pinched appearance which is unusable. We've had some discussions recently in the Immersive Web WG about ways to support multiple render passes with foveation but it still needs further discussion with the WebGPU WG before we can decide on a solution.

FYI @mwyrzykowski and I are working on extending the WebGPU for WebXR spec to clean this up: immersive-web/webxr-webgpu-binding#41

This branch has not been deployed

No deployments
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