Starting some very simple, speculative WebXR+WebGPU samples
diff --git a/media/logo/webgpu-logo.svg b/media/logo/webgpu-logo.svg
new file mode 100644
index 0000000..69956b1
--- /dev/null
+++ b/media/logo/webgpu-logo.svg
@@ -0,0 +1,45 @@
+<svg id="Logo" xmlns="http://www.w3.org/2000/svg" width="932" height="315" viewBox="0 0 932 315">
+ <defs>
+ <style>
+ .cls-1, .cls-2, .cls-3, .cls-4, .cls-5 {
+ fill-rule: evenodd;
+ stroke-linejoin: round;
+ }
+
+ .cls-6 {
+ fill-rule: evenodd;
+ }
+
+ .cls-1 {
+ fill: #005a9c;
+ stroke: #005a9c;
+ }
+
+ .cls-2 {
+ fill: #0066b0;
+ stroke: #0066b0;
+ }
+
+ .cls-3 {
+ fill: #0076cc;
+ stroke: #0076cc;
+ }
+
+ .cls-4 {
+ fill: #0086e8;
+ stroke: #0086e8;
+ }
+
+ .cls-5 {
+ fill: #0093ff;
+ stroke: #0093ff;
+ }
+ </style>
+ </defs>
+ <path id="Triangle_4" data-name="Triangle 4" class="cls-4" d="m298.4 134.84-24.6-42.609h49.269z"/>
+ <path id="Triangle_5" data-name="Triangle 5" class="cls-5" d="m298.4 49.619-24.6 42.609h49.269z"/>
+ <path id="Triangle_3" data-name="Triangle 3" class="cls-3" d="m249.2 220.06-49.2-85.218 98.4-8.1e-4z"/>
+ <path id="Triangle_2" data-name="Triangle 2" class="cls-2" d="m249.2 49.619-49.2 85.218 98.4-8.1e-4z"/>
+ <path id="Triangle_1" data-name="Triangle 1" class="cls-1" d="m150.8 220.06-98.4-170.44h196.8z"/>
+ <path id="WebGPU" class="cls-6" d="M397.927,170a46.383,46.383,0,0,0-5.459-22.594A39.579,39.579,0,0,0,376.929,131.7a46.217,46.217,0,0,0-23.182-5.711,47.758,47.758,0,0,0-23.77,5.8,40.1,40.1,0,0,0-16.042,16.547q-5.714,10.752-5.712,25.029t5.88,25.114a41.6,41.6,0,0,0,16.21,16.714,46.512,46.512,0,0,0,23.434,5.88q16.461,0,27.55-8.315a39.574,39.574,0,0,0,14.782-21.586H379.617a24.465,24.465,0,0,1-9.323,12.347q-6.639,4.621-16.547,4.619a29.643,29.643,0,0,1-20.578-7.643q-8.484-7.642-9.323-21.25h73.577a89.923,89.923,0,0,0,.5-9.239h0Zm-15.791-3.192H324.014q1.173-13.1,9.323-20.494a28.334,28.334,0,0,1,19.738-7.391,32.751,32.751,0,0,1,14.7,3.275,25.147,25.147,0,0,1,10.5,9.575,28.21,28.21,0,0,1,3.863,15.035h0Zm51.235-22.174V95.246H418.084V219.554h15.287V202.588a35.4,35.4,0,0,0,13.523,13.27,40.941,40.941,0,0,0,20.914,5.208,41.875,41.875,0,0,0,37.964-23.014,52.851,52.851,0,0,0,5.712-24.862q0-14.111-5.712-24.777a41.653,41.653,0,0,0-15.622-16.547,42.965,42.965,0,0,0-22.342-5.879,40.763,40.763,0,0,0-20.662,5.207,36.146,36.146,0,0,0-13.775,13.439h0Zm62.49,28.557a38.312,38.312,0,0,1-4.2,18.4,29.535,29.535,0,0,1-11.338,11.927,30.887,30.887,0,0,1-15.707,4.115,30.558,30.558,0,0,1-15.539-4.115,30.072,30.072,0,0,1-11.422-11.927,40.557,40.557,0,0,1,0-36.285,30.092,30.092,0,0,1,11.422-11.927,30.578,30.578,0,0,1,15.539-4.115,31.4,31.4,0,0,1,15.707,4.031,29.1,29.1,0,0,1,11.338,11.759,37.449,37.449,0,0,1,4.2,18.142h0ZM638.647,136.4a52.884,52.884,0,0,0-20.914-26.038q-14.362-9.236-33.009-9.239a59.092,59.092,0,0,0-29.733,7.643,55.959,55.959,0,0,0-21.25,21.334,61.09,61.09,0,0,0-7.812,30.826,60.623,60.623,0,0,0,7.812,30.741,56.1,56.1,0,0,0,21.25,21.25,59.062,59.062,0,0,0,29.733,7.643,56.956,56.956,0,0,0,28.053-6.971A56.042,56.042,0,0,0,633.1,194.608a58.17,58.17,0,0,0,9.071-26.457V156.392H579.181v12.431h46.7q-1.849,17.472-12.935,27.717t-28.221,10.247a43.634,43.634,0,0,1-22.09-5.627,39.861,39.861,0,0,1-15.454-16.043q-5.631-10.413-5.628-24.189t5.628-24.274a39.769,39.769,0,0,1,15.454-16.127,43.654,43.654,0,0,1,22.09-5.627,40.52,40.52,0,0,1,21.5,5.627A36.445,36.445,0,0,1,620.337,136.4h18.31Zm101.967,0.336q0-15.118-10.332-24.694t-30.153-9.575h-37.8V219.554h15.286V170.671h22.51q20.493,0,30.489-9.659t10-24.274h0Zm-40.485,21.334h-22.51v-43h22.51q24.859,0,24.862,21.67,0,10.248-6.047,15.79t-18.815,5.544h0Zm59.13-55.6v73.913q0,14.614,5.795,24.61a36.391,36.391,0,0,0,15.791,14.866,54.05,54.05,0,0,0,44.852,0,36.911,36.911,0,0,0,15.874-14.866q5.878-9.993,5.88-24.61V102.469H832.164V176.55q0,15.623-7.643,23.182t-21.082,7.559q-13.606,0-21.25-7.559t-7.643-23.182V102.469H759.259Z"/>
+</svg>
diff --git a/webgpu/index.html b/webgpu/index.html
new file mode 100644
index 0000000..f0d97ce
--- /dev/null
+++ b/webgpu/index.html
@@ -0,0 +1,171 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <meta http-equiv='X-UA-Compatible' content='chrome=1'>
+ <meta name='viewport' content='width=device-width, initial-scale=1'>
+ <meta name='mobile-web-app-capable' content='yes'>
+ <meta name='apple-mobile-web-app-capable' content='yes'>
+
+ <meta name='twitter:card' content='summary'>
+ <meta name='twitter:title' content='WebXR Samples'>
+ <meta name='twitter:description' content='Sample WebXR pages for testing and reference'>
+
+ <link rel='icon' type='image/png' sizes='32x32' href='../favicon-32x32.png'>
+ <link rel='icon' type='image/png' sizes='96x96' href='../favicon-96x96.png'>
+
+ <link rel='stylesheet' href='../css/stylesheet.css'>
+ <link rel='stylesheet' href='../css/pygment_trac.css'>
+
+ <style>
+ article {
+ position: relative;
+ padding: 0.5em;
+ background-color: rgba(255, 255, 255, 0.90);
+ margin-bottom: 1em;
+ border-radius: 3px;
+ }
+
+ article h3 {
+ font-size: 1.0em;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+
+ article h3::before {
+ display: inline-block;
+ content: attr(data-index) ' - ';
+ font-weight: bold;
+ white-space: nowrap;
+ margin-right: 0.2em;
+ }
+
+ article h4 {
+ position: absolute;
+ right: 0.5em;
+ top: 0.5em;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+
+ article p {
+ margin: 0.5em;
+ }
+
+ article .links {
+ margin-left: 0.5em;
+ margin-right: 0.5em;
+ }
+
+ article a {
+ display: inline-block;
+ }
+
+ article a:not(:first-child)::before {
+ display: inline-block;
+ content: '•';
+ font-weight: bold;
+ white-space: nowrap;
+ margin-left: 0.5em;
+ margin-right: 0.5em;
+ }
+
+ .github-link {
+ font-size: 0.8em;
+ }
+
+ .wordmark img {
+ width: 70%;
+ }
+ </style>
+
+ <!--[if lt IE 9]>
+ <script src='https://html5shiv.googlecode.com/svn/trunk/html5.js'></script>
+ <![endif]-->
+ <title>WebXR - WebGPU</title>
+ </head>
+ <body>
+
+ <div class='container' id='container'>
+ <header class='header'>
+ <div id='nav'>
+ <a href='../'>Samples</a>
+ <a href='../layers-samples/'>Layers Samples</a>
+ <a href='./' class='selected'>WebGPU Samples</a>
+ <a href='../proposals/'>Proposals</a>
+ <a href='../tests/'>Test Pages</a>
+ <a href='../report/'>Report</a>
+ </div>
+
+ <h1>
+ <a href='' class='wordmark'>
+ <img src='../media/logo/webxr-logo.svg' alt='WebXR Logo'/><br/>
+ ❤️<br/>
+ <img src='../media/logo/webgpu-logo.svg' alt='WebGPU Logo'/>
+ </a>
+ </h1>
+ <h2 class='tagline'>WebGPU</h2>
+ </header>
+
+ <main class='main' id='main'>
+ <p>These pages test WebGPU integration with WebXR. They intentionally do not copy the WebGL-based samples 1:1
+ because a significant portion of the API functions identically regardless of which API is being used for
+ rendering.<br/>
+
+ <script>
+ let pages = [
+ { title: 'WebGPU VR barebones', category: 'WebGPU',
+ path: 'vr-barebones.html',
+ description: 'Extremely simple use of "immersive-vr" sessions that presents a WebGPU layer with no library dependencies. Doesn\'t render anything exciting.',
+ noPolyfill: true },
+ ];
+
+ let mainElement = document.getElementById("main");
+
+ // Append an element for every item in the pages list.
+ for (var i = 0; i < pages.length; ++i) {
+ var page = pages[i];
+
+ let article = document.createElement('article');
+
+ let title = document.createElement('h3');
+ title.setAttribute('data-index', i + 1);
+
+ let titleLink = document.createElement('a');
+ titleLink.href = page.path;
+ titleLink.textContent = page.title;
+ title.appendChild(titleLink);
+ article.appendChild(title);
+
+ let category = document.createElement('h4');
+ category.textContent = page.category;
+ article.appendChild(category);
+
+ let description = document.createElement('p');
+ description.textContent = page.description;
+ article.appendChild(description);
+
+ let links = document.createElement('div');
+ links.classList.add('links');
+
+ let sourceLink = document.createElement('a');
+ sourceLink.href = 'https://github.com/immersive-web/webxr-samples/blob/master/webgpu/' + page.path;
+ sourceLink.textContent = 'Source';
+ links.appendChild(sourceLink);
+
+ article.appendChild(links);
+
+ mainElement.appendChild(article);
+ }
+ </script>
+ </main>
+
+ <br/>
+
+ <h3><a class='github-link' href='https://github.com/immersive-web/webxr-samples'>View source on GitHub</a></h3>
+
+ <footer class='footer'>
+ </footer>
+ </div>
+ </body>
+</html>
diff --git a/webgpu/vr-barebones.html b/webgpu/vr-barebones.html
new file mode 100644
index 0000000..06c5b2f
--- /dev/null
+++ b/webgpu/vr-barebones.html
@@ -0,0 +1,228 @@
+<!doctype html>
+<!--
+Copyright 2023 The Immersive Web Community Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>
+ <meta name='mobile-web-app-capable' content='yes'>
+ <meta name='apple-mobile-web-app-capable' content='yes'>
+ <link rel='icon' type='image/png' sizes='32x32' href='../favicon-32x32.png'>
+ <link rel='icon' type='image/png' sizes='96x96' href='../favicon-96x96.png'>
+ <link rel='stylesheet' href='../css/common.css'>
+
+ <title>Barebones VR</title>
+ </head>
+ <body>
+ <header>
+ <details open>
+ <summary>WebGPU Barebones VR</summary>
+ <p>
+ This sample demonstrates extremely simple use of an "immersive-vr"
+ session which presents a WebGPU layer with no library dependencies.
+ It doesn't render anything exciting, just clears your headset's
+ display to a slowly changing color to prove it's working.
+ <a class="back" href="./">Back</a>
+ </p>
+ <button id="xr-button" class="barebones-button" disabled>XR not found</button>
+ </details>
+ </header>
+ <main style='text-align: center;'>
+ <p>Click 'Enter VR' to see content</p>
+ </main>
+ <script>
+ (function () {
+ 'use strict';
+
+ // XR globals.
+ let xrButton = document.getElementById('xr-button');
+ let xrSession = null;
+ let xrRefSpace = null;
+
+ // WebGGPU scene globals.
+ let device = null;
+ let xrGpuBinding = null;
+ let xrGpuProjLayer = null;
+
+ // Checks to see if WebXR is available and, if so, requests an XRDevice
+ // that is connected to the system and tests it to ensure it supports the
+ // desired session options.
+ function initXR() {
+ // Is WebXR available on this UA?
+ if (navigator.xr) {
+ // Is WebGPU avaialble on this UA?
+ if (!navigator.gpu) {
+ xrButton.textContent = 'WebGPU not found';
+ return;
+ }
+
+ // If the device allows creation of exclusive sessions set it as the
+ // target of the 'Enter XR' button.
+ navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
+ if (supported) {
+ // Updates the button to start an XR session when clicked.
+ xrButton.addEventListener('click', onButtonClicked);
+ xrButton.textContent = 'Enter VR';
+ xrButton.disabled = false;
+ }
+ });
+ }
+ }
+
+ // Called when the user clicks the button to enter XR. If we don't have a
+ // session we'll request one, and if we do have a session we'll end it.
+ function onButtonClicked() {
+ if (!xrSession) {
+ navigator.xr.requestSession('immersive-vr', {
+ requiredFeatures: ['layers'],
+ }).then(onSessionStarted);
+ } else {
+ xrSession.end();
+ }
+ }
+
+ // Called when we've successfully acquired a XRSession. In response we
+ // will set up the necessary session state and kick off the frame loop.
+ async function onSessionStarted(session) {
+ xrSession = session;
+ xrButton.textContent = 'Exit VR';
+
+ // Listen for the sessions 'end' event so we can respond if the user
+ // or UA ends the session for any reason.
+ session.addEventListener('end', onSessionEnded);
+
+ // Request a WebGPU adapter to get a device with, initialized to be
+ // compatible with the XRDisplay we're presenting to.
+ const adapter = await navigator.gpu.requestAdapter({
+ xrCompatible: true,
+ });
+
+ if (!adapter) {
+ console.log('Could not request a compatible WebGPU adapter.');
+ xrSession.end();
+ }
+
+ // Create a WebGPU device to render with, initialized to be compatible
+ // with the XRDisplay we're presenting to.
+ device = await adapter.requestDevice();
+
+ xrGpuBinding = new XRWebGPUBinding(session, device);
+
+ // Get a reference space, which is required for querying poses. In this
+ // case an 'local' reference space means that all poses will be relative
+ // to the location where the XRDevice was first detected.
+ session.requestReferenceSpace('local').then((refSpace) => {
+ xrRefSpace = refSpace;
+
+ xrGpuProjLayer = xrGpuBinding.createProjectionLayer({ colorFormat: xrGpuBinding.getPreferredColorFormat() });
+ session.updateRenderState({ layers: [xrGpuProjLayer] });
+
+ // Inform the session that we're ready to begin drawing.
+ session.requestAnimationFrame(onXRFrame);
+ });
+ }
+
+ // Called either when the user has explicitly ended the session by calling
+ // session.end() or when the UA has ended the session for any reason.
+ // At this point the session object is no longer usable and should be
+ // discarded.
+ function onSessionEnded(event) {
+ xrSession = null;
+ xrButton.textContent = 'Enter VR';
+
+ // In this simple case discard the WebGPU device too, since we're not
+ // rendering anything else to the screen with it.
+ device = null;
+ xrGpuBinding = null;
+ xrGpuProjLayer = null;
+ }
+
+ // Called every time the XRSession requests that a new frame be drawn.
+ function onXRFrame(time, frame) {
+ let session = frame.session;
+
+ // Inform the session that we're ready for the next frame.
+ session.requestAnimationFrame(onXRFrame);
+
+ // Get the XRDevice pose relative to the reference space we created
+ // earlier.
+ let pose = frame.getViewerPose(xrRefSpace);
+
+ // Getting the pose may fail if, for example, tracking is lost. So we
+ // have to check to make sure that we got a valid pose before attempting
+ // to render with it. If not in this case we'll just leave the
+ // framebuffer cleared, so tracking loss means the scene will simply
+ // disappear.
+ if (pose) {
+ const commandEncoder = device.createCommandEncoder({});
+
+ // If we do have a valid pose, loop through the views and get the
+ // appropriate sub image for each.
+ for (const view in xrViewerPose.views) {
+ const subImage = xrGpuBinding.getViewSubImage(xrGpuProjLayer, view);
+
+ // The sub image will contain a color texture which is where any
+ // content to be displayed on the XRDevice must be rendered. To
+ // render to the subImage's color texture, use is as a color
+ // attachement in a render pass.
+ const passEncoder = commandEncoder.beginRenderPass({
+ colorAttachments: [{
+ attachment: subImage.colorTexture.createView(subImage.viewDescriptor),
+ loadOp: 'clear',
+
+ // Update the clear color so that we can observe the color in the
+ // headset changing over time.
+ clearValue: [
+ Math.cos(time / 2000),
+ Math.cos(time / 4000),
+ Math.cos(time / 6000),
+ 1
+ ],
+ }]
+ });
+
+ // Normally you'd draw content for the view here into the given
+ // viewport, but we're keeping this sample slim so we're not
+ // bothering to draw any geometry.
+
+ /*
+ let vp = subImage.viewport;
+ passEncoder.setViewport(vp.x, vp.y, vp.width, vp.height, 0.0, 1.0);
+
+ // Draw a scene using view.projectionMatrix as the projection matrix
+ // and view.transform to position the virtual camera. If you need a
+ // view matrix, use view.transform.inverse.matrix.
+ */
+
+ passEncoder.end();
+ }
+
+ device.defaultQueue.submit([commandEncoder.finish()]);
+ }
+ }
+
+ // Start the XR application.
+ initXR();
+
+ })();
+ </script>
+ </body>
+</html>