├── .github └── workflows │ └── deploy.yml ├── .pr-preview.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── explainer.md ├── favicon-32x32.png ├── favicon-96x96.png ├── index.bs ├── package.json └── w3c.json /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. 12 | with: 13 | persist-credentials: false 14 | 15 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 16 | run: make 17 | - name: Prepare Deploy folder 18 | run: mkdir deploy && rsync -av --exclude=.git --exclude=.gitignore --exclude=deploy . deploy/ 19 | - name: Deploy 🚀 20 | uses: JamesIves/github-pages-deploy-action@4.0.0 21 | with: 22 | BRANCH: gh-pages # The branch the action should deploy to. 23 | FOLDER: deploy # The folder the action should deploy. 24 | -------------------------------------------------------------------------------- /.pr-preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_file": "index.bs", 3 | "type": "bikeshed", 4 | "params": { 5 | "force": 1 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Immersive Web Working Group 2 | 3 | Contributions to this repository are intended to become part of Recommendation-track documents governed by the 4 | [W3C Patent Policy](https://www.w3.org/Consortium/Patent-Policy-20040205/) and 5 | [Document License](https://www.w3.org/Consortium/Legal/copyright-documents). To make substantive contributions to specifications, you must either participate 6 | in the relevant W3C Working Group or make a non-member patent licensing commitment. 7 | 8 | If you are not the sole contributor to a contribution (pull request), please identify all 9 | contributors in the pull request comment. 10 | 11 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows: 12 | 13 | ``` 14 | +@github_username 15 | ``` 16 | 17 | If you added a contributor by mistake, you can remove them in a comment with: 18 | 19 | ``` 20 | -@github_username 21 | ``` 22 | 23 | If you are making a pull request on behalf of someone else but you had no part in designing the 24 | feature, you can remove yourself with the above syntax. 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All documents in this Repository are licensed by contributors 2 | under the 3 | [W3C Document License](https://www.w3.org/Consortium/Legal/copyright-documents). 4 | 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all index.html 2 | 3 | all: index.html 4 | 5 | index.html: index.bs 6 | curl https://api.csswg.org/bikeshed/ -F file=@index.bs -F output=err 7 | curl https://api.csswg.org/bikeshed/ -F file=@index.bs -F force=1 > index.html | tee 8 | 9 | local: index.bs 10 | bikeshed --die-on=everything spec index.bs 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebXR-WebGPU-Binding 2 | 3 | Allow the [WebXR Layers module](https://immersive-web.github.io/layers/) to interface with [WebGPU](https://gpuweb.github.io/gpuweb/) by providing WebGPU swap chains for each layer type. 4 | 5 | See the [Explainer](./explainer.md) for additional details. -------------------------------------------------------------------------------- /explainer.md: -------------------------------------------------------------------------------- 1 | # WebXR/WebGPU binding 2 | 3 | WebXR is well understood to be a demanding API in terms of graphics rendering performance, a task that has previously fallen entirely to WebGL. The [WebGL API](https://www.khronos.org/registry/webgl/specs/latest/1.0/), while capable, is based on the relatively outdated native APIs which have recently been overtaken by more modern equivalents. As a result, it can sometimes be a struggle to implement various recommended XR rendering techniques in a performant way. 4 | 5 | The [WebGPU API](https://gpuweb.github.io/gpuweb/) is an upcoming API for utilizing the graphics and compute capabilities of a device's GPU more efficiently than WebGL allows, with an API that better matches both GPU hardware architecture and the modern native APIs that interface with them, such as Vulkan, Direct3D 12, and Metal. As it offers the potential to enable developers to get significantly better performance in their WebXR applications. 6 | 7 | This module aims to allow the existing [WebXR Layers module](https://immersive-web.github.io/layers/) to interface with WebGPU by providing WebGPU swap chains for each layer type. 8 | 9 | ## WebGPU-compatible XRSessions 10 | 11 | Mixing content rendered by different APIs in a single session is not something that most native VR/AR APIs support. As such, the decision of which graphics API to use needs to be specified at `XRSession` creation time. WebGL is the default, but a WebGPU-compatible session can be created by requesting the `'webgpu'` feature descriptor as either a optional or required feature. 12 | 13 | ```js 14 | const xrSession = await navigator.xr.requestSession('immersive-vr', {requiredFeatures: ['webgpu']}); 15 | ``` 16 | 17 | A WebGPU-compatible `XRSession` has the following differences from a WebGL-compatible session: 18 | 19 | - `XRWebGLBinding` and `XRWebGLLayer` instances cannot be created. 20 | - `XRGPUBinding` instances can be created (see below). 21 | - `baseLayer` cannot be be set in `updateRenderState()`. `layers` must be used instead. 22 | - The `projectionMatrix` attribute of `XRView`s will return a matrix appropriate for a clip-space depth range of [0, 1] instead of [-1, 1]. 23 | 24 | ## WebGPU binding 25 | 26 | As with the existing WebGL path described in the Layers module, all WebGPU resources required by WebXR would be supplied by an `XRGPUBinding` instance, created with a WebGPU-compatible `XRSession` and [`GPUDevice`](https://gpuweb.github.io/gpuweb/#gpu-device) like so: 27 | 28 | ```js 29 | const gpuAdapter = await navigator.gpu.requestAdapter({xrCompatible: true}); 30 | const gpuDevice = await gpuAdapter.requestDevice(); 31 | const xrGpuBinding = new XRGPUBinding(xrSession, gpuDevice); 32 | ``` 33 | 34 | Note that the [`GPUAdapter`](https://gpuweb.github.io/gpuweb/#gpu-adapter) must be requested with the `xrCompatible` option set to `true`. This mirrors the WebGL context creation arg by the same name, and ensures that the returned adapter will be one that is compatible with the UAs selected XR Device. 35 | 36 | If the `XRGPUBinding` constructor is called with a WebGL-compatible `XRSession` or a `GPUDevice` created by a non-`xrCompatible` adapter an InvalidState exception is thrown. 37 | 38 | Once the `XRGPUBinding` instance has been created, it can be used to create the various `XRCompositorLayer`s, just like `XRWebGLBinding`. 39 | 40 | ```js 41 | const gpuAdapter = await navigator.gpu.requestAdapter({xrCompatible: true}); 42 | const gpuDevice = await gpuAdapter.requestDevice(); 43 | const xrGpuBinding = new XRGPUBinding(xrSession, gpuDevice); 44 | const projectionLayer = xrGpuBinding.createProjectionLayer(); 45 | ``` 46 | 47 | This allocates a layer that supplies a [`GPUTexture`](https://gpuweb.github.io/gpuweb/#gputexture) to use for color attachments. The color format of the layer must be specified, and if depth/stencil is required it can be requested as well by specifying an appropriate depth/stencil format. The preferred color format for the `XRSession` is given by `XRGPUBinding.getPreferredColorFormat()` method. 48 | 49 | ```js 50 | const gpuAdapter = await navigator.gpu.requestAdapter({xrCompatible: true}); 51 | const gpuDevice = await gpuAdapter.requestDevice(); 52 | const xrGpuBinding = new XRGPUBinding(xrSession, gpuDevice); 53 | const projectionLayer = xrGpuBinding.createProjectionLayer({ 54 | colorFormat: xrGpuBinding.getPreferredColorFormat(), 55 | depthStencilFormat: 'depth24plus', 56 | }); 57 | ``` 58 | 59 | This allocates a layer that supplies a [`GPUTexture`](https://gpuweb.github.io/gpuweb/#gputexture) to use for both color attachments and depth/stencil attachements. Note that if a `depthStencilFormat` is provided it is implied that the application will populate it will a reasonable representation of the scene's depth and that the UAs XR compositor may use that information when rendering. If you cannot guarantee that the the depth information output by your application is representative of the scene rendered into the color attachment your application should allocate it's own depth/stencil textures instead. 60 | 61 | As with the base XR Layers module, `XRGPUBinding` is only required to support `XRProjectionLayer`s unless the `layers` feature descriptor is supplied at session creation and supported by the UA/device. If the `layers` feature descriptor is requested and supported, however, all other `XRCompositionLayer` types must be supported. Layers are still set via `XRSession`'s `updateRenderState` method, as usual: 62 | 63 | ```js 64 | const quadLayer = xrGpuBinding.createQuadLayer({ 65 | space: xrReferenceSpace, 66 | viewPixelWidth: 1024, 67 | viewPixelHeight: 768, 68 | layout: 'stereo' 69 | }); 70 | 71 | xrSession.updateRenderState({ layers: [projectionLayer, quadLayer] }); 72 | ``` 73 | 74 | ## Rendering 75 | 76 | During `XRFrame` processing each layer can be updated with new imagery. Calling `getViewSubImage()` with a view from the `XRFrame` will return an `XRGPUSubImage` indicating the textures to use as the render target and what portion of the texture will be presented to the `XRView`'s associated physical display. 77 | 78 | WebGPU projection layers will provide the same `colorTexture` and `depthStencilTexture` for each `GPUSubImage` queried, while the `GPUSubImage` queried for each `XRView` will contian a different `GPUTextureViewDescriptor` that should be used when creating the texture views of both the color and depth textures to use as render pass attachments. The `GPUSubImage`'s `viewport` must also be set to ensure only the expected portion of the texture is written to. 79 | 80 | ```js 81 | // Render Loop for a projection layer with a WebGPU texture source. 82 | const xrGpuBinding = new XRGPUBinding(xrSession, gpuDevice); 83 | const layer = xrGpuBinding.createProjectionLayer({ 84 | colorFormat: xrGpuBinding.getPreferredColorFormat(), 85 | depthStencilFormat: 'depth24plus', 86 | }); 87 | 88 | xrSession.updateRenderState({ layers: [layer] }); 89 | xrSession.requestAnimationFrame(onXRFrame); 90 | 91 | function onXRFrame(time, xrFrame) { 92 | xrSession.requestAnimationFrame(onXRFrame); 93 | 94 | const commandEncoder = device.createCommandEncoder(); 95 | 96 | for (const view in xrViewerPose.views) { 97 | const subImage = xrGpuBinding.getViewSubImage(layer, view); 98 | 99 | // Render to the subImage's color and depth textures 100 | const passEncoder = commandEncoder.beginRenderPass({ 101 | colorAttachments: [{ 102 | view: subImage.colorTexture.createView(subImage.getViewDescriptor()), 103 | loadOp: 'clear', 104 | clearValue: [0,0,0,1], 105 | }], 106 | depthStencilAttachment: { 107 | view: subImage.depthStencilTexture.createView(subImage.getViewDescriptor()), 108 | depthLoadOp: 'clear', 109 | depthClearValue: 1.0, 110 | depthStoreOp: 'store', 111 | } 112 | }); 113 | 114 | let vp = subImage.viewport; 115 | passEncoder.setViewport(vp.x, vp.y, vp.width, vp.height, 0.0, 1.0); 116 | 117 | // Render from the viewpoint of xrView 118 | 119 | passEncoder.end(); 120 | } 121 | 122 | device.queue.submit([commandEncoder.finish()]); 123 | } 124 | ``` 125 | 126 | Non-projection layers, such as `XRQuadLayer`, may only have 1 sub image for `'mono'` layers and 2 sub images for `'stereo'` layers, which may not align exactly with the number of `XRView`s reported by the device. To avoid rendering the same view multiple times in these scenarios Non-projection layers must use the `XRGPUBinding`'s `getSubImage()` method to get the `XRSubImage` to render to. 127 | 128 | For mono textures the `XRSubImage` can be queried using just the layer and `XRFrame`: 129 | 130 | ```js 131 | // Render Loop for a projection layer with a WebGPU texture source. 132 | const xrGpuBinding = new XRGPUBinding(xrSession, gpuDevice); 133 | const quadLayer = xrGpuBinding.createQuadLayer({ 134 | colorFormat: xrGpuBinding.getPreferredColorFormat(), 135 | space: xrReferenceSpace, 136 | viewPixelWidth: 512, 137 | viewPixelHeight: 512, 138 | layout: 'mono' 139 | }); 140 | 141 | // Position 2 meters away from the origin with a width and height of 1.5 meters 142 | quadLayer.transform = new XRRigidTransform({z: -2}); 143 | quadLayer.width = 1.5; 144 | quadLayer.height = 1.5; 145 | 146 | xrSession.updateRenderState({ layers: [quadLayer] }); 147 | xrSession.requestAnimationFrame(onXRFrame); 148 | 149 | function onXRFrame(time, xrFrame) { 150 | xrSession.requestAnimationFrame(onXRFrame); 151 | 152 | const commandEncoder = device.createCommandEncoder(); 153 | 154 | const subImage = xrGpuBinding.getSubImage(quadLayer, xrFrame); 155 | 156 | // Render to the subImage's color texture. 157 | const passEncoder = commandEncoder.beginRenderPass({ 158 | colorAttachments: [{ 159 | view: subImage.colorTexture.createView(subImage.getViewDescriptor()), 160 | loadOp: 'clear', 161 | clearValue: [0,0,0,0], 162 | }] 163 | // Many times simple quad layers won't require a depth attachment, as they're often just 164 | // displaying a pre-rendered 2D image. 165 | }); 166 | 167 | let vp = subImage.viewport; 168 | passEncoder.setViewport(vp.x, vp.y, vp.width, vp.height, 0.0, 1.0); 169 | 170 | // Render the mono content. 171 | 172 | passEncoder.end(); 173 | 174 | device.queue.submit([commandEncoder.finish()]); 175 | } 176 | ``` 177 | 178 | For stereo textures the target `XREye` must be given to `getSubImage()` as well: 179 | 180 | ```js 181 | // Render Loop for a projection layer with a WebGPU texture source. 182 | const xrGpuBinding = new XRGPUBinding(xrSession, gpuDevice); 183 | const quadLayer = xrGpuBinding.createQuadLayer({ 184 | colorFormat: xrGpuBinding.getPreferredColorFormat(), 185 | space: xrReferenceSpace, 186 | viewPixelWidth: 512, 187 | viewPixelHeight: 512, 188 | layout: 'stereo' 189 | }); 190 | 191 | // Position 2 meters away from the origin with a width and height of 1.5 meters 192 | quadLayer.transform = new XRRigidTransform({z: -2}); 193 | quadLayer.width = 1.5; 194 | quadLayer.height = 1.5; 195 | 196 | xrSession.updateRenderState({ layers: [quadLayer] }); 197 | xrSession.requestAnimationFrame(onXRFrame); 198 | 199 | function onXRFrame(time, xrFrame) { 200 | xrSession.requestAnimationFrame(onXRFrame); 201 | 202 | const commandEncoder = device.createCommandEncoder(); 203 | 204 | for (const eye of ['left', 'right']) { 205 | const subImage = xrGpuBinding.getSubImage(quadLayer, xrFrame, eye); 206 | 207 | // Render to the subImage's color texture. 208 | const passEncoder = commandEncoder.beginRenderPass({ 209 | colorAttachments: [{ 210 | view: subImage.colorTexture.createView(subImage.getViewDescriptor()), 211 | loadOp: 'clear', 212 | clearValue: [0,0,0,0], 213 | }] 214 | // Many times simple quad layers won't require a depth attachment, as they're often just 215 | // displaying a pre-rendered 2D image. 216 | }); 217 | 218 | let vp = subImage.viewport; 219 | passEncoder.setViewport(vp.x, vp.y, vp.width, vp.height, 0.0, 1.0); 220 | 221 | // Render content for the given eye. 222 | 223 | passEncoder.end(); 224 | } 225 | 226 | device.queue.submit([commandEncoder.finish()]); 227 | } 228 | ``` 229 | 230 | ## Proposed IDL 231 | 232 | ```webidl 233 | // New feature descriptor: "webgpu" 234 | 235 | partial dictionary GPURequestAdapterOptions { 236 | boolean xrCompatible = false; 237 | }; 238 | 239 | [Exposed=Window] interface XRGPUSubImage : XRSubImage { 240 | [SameObject] readonly attribute GPUTexture colorTexture; 241 | [SameObject] readonly attribute GPUTexture? depthStencilTexture; 242 | [SameObject] readonly attribute GPUTexture? motionVectorTexture; 243 | GPUTextureViewDescriptor getViewDescriptor(); 244 | }; 245 | 246 | dictionary XRGPUProjectionLayerInit { 247 | required GPUTextureFormat colorFormat; 248 | GPUTextureFormat? depthStencilFormat; 249 | GPUTextureUsageFlags textureUsage = 0x10; // GPUTextureUsage.RENDER_ATTACHMENT 250 | double scaleFactor = 1.0; 251 | }; 252 | 253 | dictionary XRGPULayerInit { 254 | required GPUTextureFormat colorFormat; 255 | GPUTextureFormat? depthStencilFormat; 256 | GPUTextureUsageFlags textureUsage = 0x10; // GPUTextureUsage.RENDER_ATTACHMENT 257 | required XRSpace space; 258 | unsigned long mipLevels = 1; 259 | required unsigned long viewPixelWidth; 260 | required unsigned long viewPixelHeight; 261 | XRLayerLayout layout = "mono"; 262 | boolean isStatic = false; 263 | }; 264 | 265 | dictionary XRGPUQuadLayerInit : XRGPULayerInit { 266 | XRRigidTransform? transform; 267 | float width = 1.0; 268 | float height = 1.0; 269 | }; 270 | 271 | dictionary XRGPUCylinderLayerInit : XRGPULayerInit { 272 | XRRigidTransform? transform; 273 | float radius = 2.0; 274 | float centralAngle = 0.78539; 275 | float aspectRatio = 2.0; 276 | }; 277 | 278 | dictionary XRGPUEquirectLayerInit : XRGPULayerInit { 279 | XRRigidTransform? transform; 280 | float radius = 0; 281 | float centralHorizontalAngle = 6.28318; 282 | float upperVerticalAngle = 1.570795; 283 | float lowerVerticalAngle = -1.570795; 284 | }; 285 | 286 | dictionary XRGPUCubeLayerInit : XRGPULayerInit { 287 | DOMPointReadOnly? orientation; 288 | }; 289 | 290 | [Exposed=Window] interface XRGPUBinding { 291 | constructor(XRSession session, GPUDevice device); 292 | 293 | readonly attribute double nativeProjectionScaleFactor; 294 | 295 | XRProjectionLayer createProjectionLayer(optional XRGPUProjectionLayerInit init); 296 | XRQuadLayer createQuadLayer(optional XRGPUQuadLayerInit init); 297 | XRCylinderLayer createCylinderLayer(optional XRGPUCylinderLayerInit init); 298 | XREquirectLayer createEquirectLayer(optional XRGPUEquirectLayerInit init); 299 | XRCubeLayer createCubeLayer(optional XRGPUCubeLayerInit init); 300 | 301 | XRGPUSubImage getSubImage(XRCompositionLayer layer, XRFrame frame, optional XREye eye = "none"); 302 | XRGPUSubImage getViewSubImage(XRProjectionLayer layer, XRView view); 303 | 304 | GPUTextureFormat getPreferredColorFormat(); 305 | }; 306 | ``` 307 | -------------------------------------------------------------------------------- /favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immersive-web/WebXR-WebGPU-Binding/001e4148b4902b4c33fbefcbfa044bb1bcbfb3c6/favicon-32x32.png -------------------------------------------------------------------------------- /favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immersive-web/WebXR-WebGPU-Binding/001e4148b4902b4c33fbefcbfa044bb1bcbfb3c6/favicon-96x96.png -------------------------------------------------------------------------------- /index.bs: -------------------------------------------------------------------------------- 1 |
2 | Shortname: webxr-webgpu-binding 3 | Title: WebXR/WebGPU Binding Module - Level 1 4 | Group: immersivewebwg 5 | Status: w3c/ED 6 | TR: https://www.w3.org/TR/webxr-webgpu-binding-1/ 7 | ED: https://immersive-web.github.io/webxr-webgpu-binding/ 8 | Previous Version: 9 | Repository: immersive-web/webxr-webgpu-binding 10 | Level: 1 11 | Mailing List Archives: https://lists.w3.org/Archives/Public/public-immersive-web/ 12 | 13 | Editor: Brandon Jones, Google https://www.google.com, bajones@google.com, w3cid 87824 14 | 15 | Abstract: This specification describes support for rendering content for a WebXR session with WebGPU. 16 | 17 | Markup Shorthands: markdown yes 18 | Markup Shorthands: dfn yes 19 | Markup Shorthands: idl yes 20 | Markup Shorthands: css no 21 | Assume Explicit For: yes 22 | 23 | Warning: custom 24 | Custom Warning Title: Unstable API 25 | Custom Warning Text: 26 | The API represented in this document is under development and may change at any time. 27 |29 | 30 |For additional context on the use of this API please reference the WebXR/WebGPU Binding Module Explainer.
28 |
31 | spec: webxr; 32 | type: dfn; 33 | text: feature descriptor 34 |35 | 36 | 37 | 38 | 39 | 93 | 94 | # Introduction # {#intro} 95 | 96 |
142 | navigator.xr.requestSession('immersive-vr', { 143 | requiredFeatures: ['webgpu'] 144 | } 145 |146 |