From 52bd94c66fc5476a57ebd94984ae7dfbf6ef30af Mon Sep 17 00:00:00 2001
From: Gregg Tavares <github@greggman.com>
Date: Tue, 18 Jun 2024 15:27:33 -0700
Subject: [PATCH] Consider making optimal WebGL versions

1. Add more textures to all examples

2. Optimize WebGL examples
---
 3rdparty/twgl-full.module.js                  |  72 +-
 webgpu/lessons/webgpu-from-webgl.md           |   6 +-
 webgpu/lessons/webgpu-optimization.md         |  12 +-
 ...l-material-per-object-uniform-buffers.html | 537 +++++++++++++++
 ...bgl-optimization-none-uniform-buffers.html |  84 ++-
 webgpu/webgl-optimization-none.html           |  42 +-
 ...ptimization-uniform-buffers-one-large.html | 548 ++++++++++++++++
 webgpu/webgpu-optimization-all.html           | 603 -----------------
 webgpu/webgpu-optimization-none.html          |   2 +-
 ...n-step3-global-vs-per-object-uniforms.html |   2 +-
 ...-optimization-step4-material-uniforms.html |   2 +-
 ...ly-updated-uniform-buffers-pre-submit.html | 615 -----------------
 ...er-frequently-updated-uniform-buffers.html | 615 -----------------
 ...optimization-step5-use-buffer-offsets.html |   4 +-
 ...-use-mapped-buffers-2-command-buffers.html | 619 ------------------
 ...p6-use-mapped-buffers-dyanmic-offsets.html |   2 +-
 ...ep6-use-mapped-buffers-math-w-offsets.html |   2 +-
 ...optimization-step6-use-mapped-buffers.html |   4 +-
 ...mization-step7-double-buffer-2-submit.html |   2 +-
 ...on-step7-double-buffer-typedarray-set.html |   2 +-
 ...bgpu-optimization-step7-double-buffer.html |   2 +-
 21 files changed, 1251 insertions(+), 2526 deletions(-)
 create mode 100644 webgpu/webgl-optimization-global-material-per-object-uniform-buffers.html
 create mode 100644 webgpu/webgl-optimization-uniform-buffers-one-large.html
 delete mode 100644 webgpu/webgpu-optimization-all.html
 delete mode 100644 webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers-pre-submit.html
 delete mode 100644 webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers.html
 delete mode 100644 webgpu/webgpu-optimization-step6-use-mapped-buffers-2-command-buffers.html

diff --git a/3rdparty/twgl-full.module.js b/3rdparty/twgl-full.module.js
index 9702f31a..d7338867 100644
--- a/3rdparty/twgl-full.module.js
+++ b/3rdparty/twgl-full.module.js
@@ -1,4 +1,4 @@
-/* @license twgl.js 5.3.1 Copyright (c) 2015, Gregg Tavares All Rights Reserved.
+/* @license twgl.js 5.6.0 Copyright (c) 2015, Gregg Tavares All Rights Reserved.
 Available via the MIT license.
 see: http://github.com/greggman/twgl.js for details */
 /*
@@ -5104,7 +5104,8 @@ const TEXTURE_MIN_LOD                = 0x813a;
 const TEXTURE_MAX_LOD                = 0x813b;
 const TEXTURE_BASE_LEVEL             = 0x813c;
 const TEXTURE_MAX_LEVEL              = 0x813d;
-
+const TEXTURE_COMPARE_MODE           = 0x884C;
+const TEXTURE_COMPARE_FUNC           = 0x884D;
 
 /* Pixel store */
 const UNPACK_ALIGNMENT                   = 0x0cf5;
@@ -5526,6 +5527,8 @@ function setDefaults$1(newDefaults) {
  * @property {number} [maxLod] TEXTURE_MAX_LOD setting
  * @property {number} [baseLevel] TEXTURE_BASE_LEVEL setting
  * @property {number} [maxLevel] TEXTURE_MAX_LEVEL setting
+ * @property {number} [compareFunc] TEXTURE_COMPARE_FUNC setting
+ * @property {number} [compareMode] TEXTURE_COMPARE_MODE setting
  * @property {number} [unpackAlignment] The `gl.UNPACK_ALIGNMENT` used when uploading an array. Defaults to 1.
  * @property {number[]|ArrayBufferView} [color] Color to initialize this texture with if loading an image asynchronously.
  *     The default use a blue 1x1 pixel texture. You can set another default by calling `twgl.setDefaults`
@@ -5661,18 +5664,24 @@ function setTextureSamplerParameters(gl, target, parameteriFn, options) {
   if (options.wrapT) {
     parameteriFn.call(gl, target, TEXTURE_WRAP_T, options.wrapT);
   }
-  if (options.minLod) {
+  if (options.minLod !== undefined) {
     parameteriFn.call(gl, target, TEXTURE_MIN_LOD, options.minLod);
   }
-  if (options.maxLod) {
+  if (options.maxLod !== undefined) {
     parameteriFn.call(gl, target, TEXTURE_MAX_LOD, options.maxLod);
   }
-  if (options.baseLevel) {
+  if (options.baseLevel !== undefined) {
     parameteriFn.call(gl, target, TEXTURE_BASE_LEVEL, options.baseLevel);
   }
-  if (options.maxLevel) {
+  if (options.maxLevel !== undefined) {
     parameteriFn.call(gl, target, TEXTURE_MAX_LEVEL, options.maxLevel);
   }
+  if (options.compareFunc !== undefined) {
+    parameteriFn.call(gl, target, TEXTURE_COMPARE_FUNC, options.compareFunc);
+  }
+  if (options.compareMode !== undefined) {
+    parameteriFn.call(gl, target, TEXTURE_COMPARE_MODE, options.compareMode);
+  }
 }
 
 /**
@@ -7554,7 +7563,7 @@ function createProgramNoCheck(gl, shaders, programOptions) {
  * @param {WebGLRenderingContext} gl The WebGLRenderingContext to use.
  * @param {WebGLShader[]|string[]} shaders The shaders to attach, or element ids for their source, or strings that contain their source
  * @param {module:twgl.ProgramOptions|string[]|module:twgl.ErrorCallback} [opt_attribs] Options for the program or an array of attribs names or an error callback. Locations will be assigned by index if not passed in
- * @param {number[]} [opt_locations|module:twgl.ErrorCallback] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
+ * @param {number[]|module:twgl.ErrorCallback} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
  * @param {module:twgl.ErrorCallback} [opt_errorCallback] callback for errors. By default it just prints an error to the console
  *        on error. If you want something else pass an callback. It's passed an error message.
  * @return {WebGLProgram?} the created program or null if error of a callback was provided.
@@ -7622,7 +7631,7 @@ function wrapCallbackFnToAsyncFn(fn) {
  * @param {WebGLRenderingContext} gl The WebGLRenderingContext to use.
  * @param {WebGLShader[]|string[]} shaders The shaders to attach, or element ids for their source, or strings that contain their source
  * @param {module:twgl.ProgramOptions|string[]|module:twgl.ErrorCallback} [opt_attribs] Options for the program or an array of attribs names or an error callback. Locations will be assigned by index if not passed in
- * @param {number[]} [opt_locations|module:twgl.ErrorCallback] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
+ * @param {number[]|module:twgl.ErrorCallback} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
  * @param {module:twgl.ErrorCallback} [opt_errorCallback] callback for errors. By default it just prints an error to the console
  *        on error. If you want something else pass an callback. It's passed an error message.
  * @return {Promise<WebGLProgram>} The created program
@@ -7639,7 +7648,7 @@ const createProgramAsync = wrapCallbackFnToAsyncFn(createProgram);
  *        shaders or ids. The first is assumed to be the vertex shader,
  *        the second the fragment shader.
  * @param {module:twgl.ProgramOptions|string[]|module:twgl.ErrorCallback} [opt_attribs] Options for the program or an array of attribs names or an error callback. Locations will be assigned by index if not passed in
- * @param {number[]} [opt_locations|module:twgl.ErrorCallback] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
+ * @param {number[]|module:twgl.ErrorCallback} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
  * @param {module:twgl.ErrorCallback} [opt_errorCallback] callback for errors. By default it just prints an error to the console
  *        on error. If you want something else pass an callback. It's passed an error message.
  * @return {Promise<module:twgl.ProgramInfo>} The created ProgramInfo
@@ -7706,7 +7715,7 @@ function getProgramErrors(gl, program, errFn) {
  *        tags for the shaders. The first is assumed to be the
  *        vertex shader, the second the fragment shader.
  * @param {module:twgl.ProgramOptions|string[]|module:twgl.ErrorCallback} [opt_attribs] Options for the program or an array of attribs names or an error callback. Locations will be assigned by index if not passed in
- * @param {number[]} [opt_locations|module:twgl.ErrorCallback] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
+ * @param {number[]|module:twgl.ErrorCallback} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
  * @param {module:twgl.ErrorCallback} [opt_errorCallback] callback for errors. By default it just prints an error to the console
  *        on error. If you want something else pass an callback. It's passed an error message.
  * @return {WebGLProgram?} the created program or null if error or a callback was provided.
@@ -7742,7 +7751,7 @@ function createProgramFromScripts(
  *        shaders. The first is assumed to be the vertex shader,
  *        the second the fragment shader.
  * @param {module:twgl.ProgramOptions|string[]|module:twgl.ErrorCallback} [opt_attribs] Options for the program or an array of attribs names or an error callback. Locations will be assigned by index if not passed in
- * @param {number[]} [opt_locations|module:twgl.ErrorCallback] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
+ * @param {number[]|module:twgl.ErrorCallback} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
  * @param {module:twgl.ErrorCallback} [opt_errorCallback] callback for errors. By default it just prints an error to the console
  *        on error. If you want something else pass an callback. It's passed an error message.
  * @return {WebGLProgram?} the created program or null if error or a callback was provided.
@@ -8092,6 +8101,7 @@ function createUniformBlockUniformSetter(view, isArray, rows, cols) {
  * @property {ArrayBuffer} array The array buffer that contains the uniform values
  * @property {Float32Array} asFloat A float view on the array buffer. This is useful
  *    inspecting the contents of the buffer in the debugger.
+ * @property {Uint8Array} asUint8t A uint8 view on the array buffer.
  * @property {WebGLBuffer} buffer A WebGL buffer that will hold a copy of the uniform values for rendering.
  * @property {number} [offset] offset into buffer
  * @property {Object<string, ArrayBufferView>} uniforms A uniform name to ArrayBufferView map.
@@ -8110,6 +8120,15 @@ function createUniformBlockUniformSetter(view, isArray, rows, cols) {
  * @memberOf module:twgl
  */
 
+/**
+ * Options to allow createUniformBlockInfo to use an existing buffer and arrayBuffer at an offset
+ * @typedef {Object} UniformBlockInfoOptions
+ * @property {ArrayBuffer} [array] an existing array buffer to use for values
+ * @property {number} [offset] the offset in bytes to use in the array buffer (default = 0)
+ * @property {WebGLBuffer} [buffer] the buffer to use for this uniform block info
+ * @property {number} [bufferOffset] the offset in bytes in the buffer to use (default = use offset above)
+ */
+
 /**
  * Creates a `UniformBlockInfo` for the specified block
  *
@@ -8124,10 +8143,11 @@ function createUniformBlockUniformSetter(view, isArray, rows, cols) {
  * @param {module:twgl.UniformBlockSpec} uniformBlockSpec. A UniformBlockSpec as returned
  *     from {@link module:twgl.createUniformBlockSpecFromProgram}.
  * @param {string} blockName The name of the block.
+ * @param {module:twgl.UniformBlockInfoOptions} [options] Optional options for using existing an existing buffer and arrayBuffer
  * @return {module:twgl.UniformBlockInfo} The created UniformBlockInfo
  * @memberOf module:twgl/programs
  */
-function createUniformBlockInfoFromProgram(gl, program, uniformBlockSpec, blockName) {
+function createUniformBlockInfoFromProgram(gl, program, uniformBlockSpec, blockName, options = {}) {
   const blockSpecs = uniformBlockSpec.blockSpecs;
   const uniformData = uniformBlockSpec.uniformData;
   const blockSpec = blockSpecs[blockName];
@@ -8138,10 +8158,14 @@ function createUniformBlockInfoFromProgram(gl, program, uniformBlockSpec, blockN
       uniforms: {},
     };
   }
-  const array = new ArrayBuffer(blockSpec.size);
-  const buffer = gl.createBuffer();
+  const offset = options.offset ?? 0;
+  const array = options.array ?? new ArrayBuffer(blockSpec.size);
+  const buffer = options.buffer ?? gl.createBuffer();
   const uniformBufferIndex = blockSpec.index;
   gl.bindBuffer(UNIFORM_BUFFER, buffer);
+  if (!options.buffer) {
+    gl.bufferData(UNIFORM_BUFFER, array.byteLength, DYNAMIC_DRAW);
+  }
   gl.uniformBlockBinding(program, blockSpec.index, uniformBufferIndex);
 
   let prefix = blockName + ".";
@@ -8166,7 +8190,7 @@ function createUniformBlockInfoFromProgram(gl, program, uniformBlockSpec, blockN
     const byteLength = isArray
         ? pad(typeInfo.size, 16) * data.size
         : typeInfo.size * data.size;
-    const uniformView = new Type(array, data.offset, byteLength / Type.BYTES_PER_ELEMENT);
+    const uniformView = new Type(array, offset + data.offset, byteLength / Type.BYTES_PER_ELEMENT);
     uniforms[name] = uniformView;
     // Note: I'm not sure what to do here. The original
     // idea was to create TypedArray views into each part
@@ -8201,9 +8225,12 @@ function createUniformBlockInfoFromProgram(gl, program, uniformBlockSpec, blockN
     name: blockName,
     array,
     asFloat: new Float32Array(array),  // for debugging
+    asUint8: new Uint8Array(array),  // needed for gl.bufferSubData because it doesn't take an array buffer
     buffer,
     uniforms,
     setters,
+    offset: options.bufferOffset ?? offset,
+    size: blockSpec.size,
   };
 }
 
@@ -8220,11 +8247,12 @@ function createUniformBlockInfoFromProgram(gl, program, uniformBlockSpec, blockN
  * @param {module:twgl.ProgramInfo} programInfo a `ProgramInfo`
  *     as returned from {@link module:twgl.createProgramInfo}
  * @param {string} blockName The name of the block.
+ * @param {module:twgl.UniformBlockInfoOptions} [options] Optional options for using existing an existing buffer and arrayBuffer
  * @return {module:twgl.UniformBlockInfo} The created UniformBlockInfo
  * @memberOf module:twgl/programs
  */
-function createUniformBlockInfo(gl, programInfo, blockName) {
-  return createUniformBlockInfoFromProgram(gl, programInfo.program, programInfo.uniformBlockSpec, blockName);
+function createUniformBlockInfo(gl, programInfo, blockName, options = {}) {
+  return createUniformBlockInfoFromProgram(gl, programInfo.program, programInfo.uniformBlockSpec, blockName, options);
 }
 
 /**
@@ -8250,7 +8278,7 @@ function bindUniformBlock(gl, programInfo, uniformBlockInfo) {
   const blockSpec = uniformBlockSpec.blockSpecs[uniformBlockInfo.name];
   if (blockSpec) {
     const bufferBindIndex = blockSpec.index;
-    gl.bindBufferRange(UNIFORM_BUFFER, bufferBindIndex, uniformBlockInfo.buffer, uniformBlockInfo.offset || 0, uniformBlockInfo.array.byteLength);
+    gl.bindBufferRange(UNIFORM_BUFFER, bufferBindIndex, uniformBlockInfo.buffer, uniformBlockInfo.offset || 0, uniformBlockInfo.size ?? uniformBlockInfo.array.byteLength);
     return true;
   }
   return false;
@@ -8273,7 +8301,7 @@ function bindUniformBlock(gl, programInfo, uniformBlockInfo) {
  */
 function setUniformBlock(gl, programInfo, uniformBlockInfo) {
   if (bindUniformBlock(gl, programInfo, uniformBlockInfo)) {
-    gl.bufferData(UNIFORM_BUFFER, uniformBlockInfo.array, DYNAMIC_DRAW);
+    gl.bufferSubData(UNIFORM_BUFFER, 0, uniformBlockInfo.asUint8, uniformBlockInfo.offset || 0, uniformBlockInfo.size || 0);
   }
 }
 
@@ -8718,6 +8746,8 @@ function setBuffersAndAttributes(gl, programInfo, buffers) {
 /**
  * @typedef {Object} ProgramInfo
  * @property {WebGLProgram} program A shader program
+ * @property {Object<string, WebGLUniformLocation>} uniformLocations The uniform locations of each uniform
+ * @property {Object<string, number>} attribLocations The locations of each attribute
  * @property {Object<string, function>} uniformSetters object of setters as returned from createUniformSetters,
  * @property {Object<string, function>} attribSetters object of setters as returned from createAttribSetters,
  * @property {module:twgl.UniformBlockSpec} [uniformBlockSpec] a uniform block spec for making UniformBlockInfos with createUniformBlockInfo etc..
@@ -8749,6 +8779,8 @@ function createProgramInfoFromProgram(gl, program) {
     program,
     uniformSetters,
     attribSetters,
+    uniformLocations: Object.fromEntries(Object.entries(uniformSetters).map(([k, v]) => [k, v.location])),
+    attribLocations: Object.fromEntries(Object.entries(attribSetters).map(([k, v]) => [k, v.location])),
   };
 
   if (isWebGL2(gl)) {
@@ -8785,7 +8817,7 @@ const notIdRE = /\s|{|}|;/;
  *        shaders or ids. The first is assumed to be the vertex shader,
  *        the second the fragment shader.
  * @param {module:twgl.ProgramOptions|string[]|module:twgl.ErrorCallback} [opt_attribs] Options for the program or an array of attribs names or an error callback. Locations will be assigned by index if not passed in
- * @param {number[]} [opt_locations|module:twgl.ErrorCallback] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
+ * @param {number[]|module:twgl.ErrorCallback} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations or an error callback.
  * @param {module:twgl.ErrorCallback} [opt_errorCallback] callback for errors. By default it just prints an error to the console
  *        on error. If you want something else pass an callback. It's passed an error message.
  * @return {module:twgl.ProgramInfo?} The created ProgramInfo or null if it failed to link or compile
diff --git a/webgpu/lessons/webgpu-from-webgl.md b/webgpu/lessons/webgpu-from-webgl.md
index a9801fdb..463732cd 100644
--- a/webgpu/lessons/webgpu-from-webgl.md
+++ b/webgpu/lessons/webgpu-from-webgl.md
@@ -1420,8 +1420,10 @@ ideas.
 Note: If you are comparing WebGL to WebGPU in [the article on optimization](webgpu-optimization.html)
 here are 2 WebGL samples you can use to compare
 
-* [Drawing up to 20000 objects in WebGL using standard WebGL uniforms](../webgl-optimization-none.html)
-* [Drawing up to 20000 objects in WebGL using uniform blocks](../webgl-optimization-none-uniform-buffers.html)
+* [Drawing up to 30000 objects in WebGL using standard WebGL uniforms](../webgl-optimization-none.html)
+* [Drawing up to 30000 objects in WebGL using uniform blocks](../webgl-optimization-none-uniform-buffers.html)
+* [Drawing up to 30000 objects in WebGL using global/material/per object uniform blocks](../webgl-optimization-global-material-per-object-uniform-buffers.html)
+* [Drawing up to 30000 objects in WebGL using one large uniform buffer](../webgl-optimization-uniform-buffers-one-large.html)
 
 Another article, if you're comparing performance of WebGL vs WebGPU see
 [this article](https://toji.dev/webgpu-best-practices/webgl-performance-comparison).
diff --git a/webgpu/lessons/webgpu-optimization.md b/webgpu/lessons/webgpu-optimization.md
index 0eda2c32..b42c96e7 100644
--- a/webgpu/lessons/webgpu-optimization.md
+++ b/webgpu/lessons/webgpu-optimization.md
@@ -351,13 +351,13 @@ We'll make 20 "materials" and then pick a material at random for each cube.
 ```
 
 Now let's make data for each thing (cube) we want to draw. We'll support a
-maximum of 20000. Like we have in the past, we'll make a uniform buffer for each
+maximum of 30000. Like we have in the past, we'll make a uniform buffer for each
 object as well as a typed array we can update with uniform values. We'll also
 make a bind group for each object. And we'll pick some random values we can use
 to position and animate each object.
 
 ```js
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   for (let i = 0; i < maxObjects; ++i) {
@@ -1265,7 +1265,7 @@ Then we can removed these uniforms from our perObject uniform buffer and add the
 global uniform buffer to each object's bind group.
 
 ```js
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   for (let i = 0; i < maxObjects; ++i) {
@@ -1585,7 +1585,7 @@ settings. Instead we just need to add the material's uniform buffer to the
 object's bind group.
 
 ```js
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   for (let i = 0; i < maxObjects; ++i) {
@@ -2079,7 +2079,7 @@ Other things that *might* help
   
   This is why, in our loop where we update our per object uniform values, for
   each object we have to create 2 `Float32Array` views into our mapped buffer.
-  For 10000 objects that's creating 20000 of these temporary views.
+  For 20000 objects that's creating 40000 of these temporary views.
   
   Adding offsets to every input would make them burdensome to use in my opinion
   but, just as a test, I wrote a modified version of the math functions that
@@ -2097,7 +2097,7 @@ Other things that *might* help
 
   [It appears to be about 7% faster to use the offsets](../webgpu-optimization-step6-use-mapped-buffers-math-w-offsets.html).
 
-  It's up to you if you feel that's worthß it. For me personally, like I
+  It's up to you if you feel that's worth it. For me personally, like I
   mentioned at the top of the article, I'd prefer to keep it simple to use. I'm
   rarely trying to draw 10000 things. But, it's good to know, if I wanted to
   squeeze out more performance, this is one place I might find some. More likely
diff --git a/webgpu/webgl-optimization-global-material-per-object-uniform-buffers.html b/webgpu/webgl-optimization-global-material-per-object-uniform-buffers.html
new file mode 100644
index 00000000..35875444
--- /dev/null
+++ b/webgpu/webgl-optimization-global-material-per-object-uniform-buffers.html
@@ -0,0 +1,537 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>WebGL Optimization - Uniform Blocks (global/material/perObject)</title>
+    <style>
+      @import url(resources/webgpu-lesson.css);
+html, body {
+  margin: 0;       /* remove the default margin          */
+  height: 100%;    /* make the html,body fill the page   */
+}
+canvas {
+  display: block;  /* make the canvas act like a block   */
+  width: 100%;     /* make the canvas fill its container */
+  height: 100%;
+}
+:root {
+  --bg-color: #fff;
+}
+@media (prefers-color-scheme: dark) {
+  :root {
+    --bg-color: #000;
+  }
+}
+canvas {
+  background-color: var(--bg-color);
+}
+#info {
+  position: absolute;
+  left: 0;
+  top: 0;
+  padding: 0.5em;
+  margin: 0;
+  background-color: rgba(0, 0, 0, 0.8);
+  color: white;
+  min-width: 8em;
+}
+    </style>
+  </head>
+  <body>
+    <canvas></canvas>
+    <pre id="info"></pre>
+  </body>
+  <script type="module">
+//import 'https://greggman.github.io/webgl-lint/webgl-lint.js';
+import GUI from '../3rdparty/muigui-0.x.module.js';
+import * as twgl from '../3rdparty/twgl-full.module.js';
+import RollingAverage from './resources/js/rolling-average.js';
+
+class TimingHelper {
+  #ext;
+  #query;
+  #gl;
+  #state = 'free';
+  #duration = 0;
+
+  constructor(gl) {
+    this.#gl = gl;
+    this.#ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');
+    if (!this.#ext) {
+      return;
+    }
+    this.#query = gl.createQuery();
+  }
+
+  begin() {
+    if (!this.#ext || this.#state !== 'free') {
+      return;
+    }
+    this.#state = 'started';
+    const gl = this.#gl;
+    const ext = this.#ext;
+    const query = this.#query;
+    gl.beginQuery(ext.TIME_ELAPSED_EXT, query);
+  }
+  end() {
+    if (!this.#ext || this.#state === 'free') {
+      return;
+    }
+
+    const gl = this.#gl;
+    const ext = this.#ext;
+    const query = this.#query;
+
+    if (this.#state === 'started') {
+      gl.endQuery(ext.TIME_ELAPSED_EXT);
+      this.#state = 'waiting';
+    } else {
+      const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);
+      const disjoint = gl.getParameter(ext.GPU_DISJOINT_EXT);
+      if (available && !disjoint) {
+        this.#duration = gl.getQueryParameter(query, gl.QUERY_RESULT);
+      }
+      if (available || disjoint) {
+        this.#state = 'free';
+      }
+    }
+  }
+  getResult() {
+    return this.#duration;
+  }
+}
+
+const { m4: mat4, v3: vec3 } = twgl;
+const mat3 = {
+  identity() {
+    return new Float32Array(9);
+  },
+  fromMat4(m, dst) {
+    dst = dst || new Float32Array(9);
+
+    dst[0] =  m[ 0]; dst[1] = m[ 1]; dst[2] = m[ 2];
+    dst[3] =  m[ 4]; dst[4] = m[ 5]; dst[5] = m[ 6];
+    dst[6] =  m[ 8]; dst[7] = m[ 9]; dst[8] = m[10];
+
+    return dst;
+  },
+};
+
+const fpsAverage = new RollingAverage();
+const jsAverage = new RollingAverage();
+const gpuAverage = new RollingAverage();
+const mathAverage = new RollingAverage();
+
+/** Given a css color string, return an array of 4 values from 0 to 255 */
+const cssColorToRGBA8 = (() => {
+  const canvas = new OffscreenCanvas(1, 1);
+  const ctx = canvas.getContext('2d', {willReadFrequently: true});
+  return cssColor => {
+    ctx.clearRect(0, 0, 1, 1);
+    ctx.fillStyle = cssColor;
+    ctx.fillRect(0, 0, 1, 1);
+    return Array.from(ctx.getImageData(0, 0, 1, 1).data);
+  };
+})();
+
+/** Given a css color string, return an array of 4 values from 0 to 1 */
+const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
+
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * return the corresponding CSS hsl string
+ */
+const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of 4 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+
+/**
+ * Returns a random number between min and max.
+ * If min and max are not specified, returns 0 to 1
+ * If max is not specified, return 0 to min.
+ */
+function rand(min, max) {
+  if (min === undefined) {
+    max = 1;
+    min = 0;
+  } else if (max === undefined) {
+    max = min;
+    min = 0;
+  }
+  return Math.random() * (max - min) + min;
+}
+
+/** Selects a random array element */
+const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
+
+async function main() {
+  const infoElem = document.querySelector('#info');
+
+  // Get a WebGPU context from the canvas and configure it
+  const canvas = document.querySelector('canvas');
+  const gl = canvas.getContext('webgl2');
+  const timingHelper = new TimingHelper(gl);
+
+  const uniformBlock = `
+      uniform GlobalUniforms {
+        mat4 viewProjection;
+        vec3 lightWorldPosition;
+        vec3 viewWorldPosition;
+      };
+
+      uniform MaterialUniforms {
+        vec4 color;
+        float shininess;
+      };
+
+      uniform PerObjectUniforms {
+        mat3 normalMatrix;
+        mat4 world;
+      };
+  `;
+  const vs = `#version 300 es
+      ${uniformBlock}
+      layout(location = 0) in vec4 position;
+      layout(location = 1) in vec3 normal;
+      layout(location = 2) in vec2 texcoord;
+
+      out vec3 v_normal;
+      out vec3 v_surfaceToLight;
+      out vec3 v_surfaceToView;
+      out vec2 v_texcoord;
+
+      void main() {
+        gl_Position = viewProjection * world * position;
+
+        // Orient the normals and pass to the fragment shader
+        v_normal = normalMatrix * normal;
+
+        // Compute the world position of the surface
+        vec3 surfaceWorldPosition = (world * position).xyz;
+
+        // Compute the vector of the surface to the light
+        // and pass it to the fragment shader
+        v_surfaceToLight = lightWorldPosition - surfaceWorldPosition;
+
+        // Compute the vector of the surface to the light
+        // and pass it to the fragment shader
+        v_surfaceToView = viewWorldPosition - surfaceWorldPosition;
+
+        // Pass the texture coord on to the fragment shader
+        v_texcoord = texcoord;
+      }
+  `;
+
+  const fs = `#version 300 es
+      precision highp float;
+      ${uniformBlock}
+
+      in vec3 v_normal;
+      in vec3 v_surfaceToLight;
+      in vec3 v_surfaceToView;
+      in vec2 v_texcoord;
+
+      uniform sampler2D diffuseTexture;
+
+      out vec4 fragColor;
+
+      void main() {
+        // Because vsOut.normal is an inter-stage variable 
+        // it's interpolated so it will not be a unit vector.
+        // Normalizing it will make it a unit vector again
+        vec3 normal = normalize(v_normal);
+
+        vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
+        vec3 surfaceToViewDirection = normalize(v_surfaceToView);
+        vec3 halfVector = normalize(
+          surfaceToLightDirection + surfaceToViewDirection);
+
+        // Compute the light by taking the dot product
+        // of the normal with the direction to the light
+        float light = dot(normal, surfaceToLightDirection);
+
+        float specular = dot(normal, halfVector);
+        specular = specular > 0.0 ?
+            pow(specular, shininess) :
+            0.0;
+
+        vec4 diffuse = color * texture(diffuseTexture, v_texcoord);
+        // Lets multiply just the color portion (not the alpha)
+        // by the light
+        vec3 c = diffuse.rgb * light + specular;
+        fragColor = vec4(c, diffuse.a);
+      }
+    `;
+
+  const prgInfo = twgl.createProgramInfo(gl, [vs, fs]);
+
+  const globalUboInfo = twgl.createUniformBlockInfo(gl, prgInfo, 'GlobalUniforms');
+
+  const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
+    position:  new Float32Array([1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1]),
+    normal:    new Float32Array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1]),
+    texcoord: new Float32Array([1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1]),
+    indices:   new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23]),
+  });
+
+  gl.enable(gl.CULL_FACE);
+  gl.enable(gl.DEPTH_TEST);
+
+  const textures = [
+    'πŸ˜‚', 'πŸ‘Ύ', 'πŸ‘', 'πŸ‘€', '🌞', 'πŸ›Ÿ',
+  ].map(s => {
+    const size = 128;
+    const ctx = new OffscreenCanvas(size, size).getContext('2d');
+    ctx.fillStyle = '#fff';
+    ctx.fillRect(0, 0, size, size);
+    ctx.font = `${size * 0.9}px sans-serif`;
+    ctx.textAlign = 'center';
+    ctx.textBaseline = 'middle';
+    ctx.fillText(s, size / 2, size / 2);
+    return twgl.createTexture(gl, {
+      src: ctx.canvas,
+      wrap: gl.CLAMP_TO_EDGE,
+      min: gl.LINEAR_MIPMAP_NEAREST,
+      mag: gl.LINEAR,
+    });
+  });
+
+  const numMaterials = 20;
+  const materials = [];
+  for (let i = 0; i < numMaterials; ++i) {
+    const color = hslToRGBA(rand(), rand(0.5, 0.8), rand(0.5, 0.7));
+    const shininess = rand(10, 120);
+    const materialUboInfo = twgl.createUniformBlockInfo(gl, prgInfo, 'MaterialUniforms');
+
+    // Set the data in the typed array views
+    twgl.setBlockUniforms(materialUboInfo, {
+      color,
+      shininess,
+    });
+    // copy the typed array view data to the uniform buffer
+    twgl.setUniformBlock(gl, prgInfo, materialUboInfo);
+
+    materials.push({
+      color,
+      shininess,
+      texture: randomArrayElement(textures),
+      materialUboInfo,
+    });
+  }
+
+  const maxObjects = 30000;
+  const objectInfos = [];
+
+  for (let i = 0; i < maxObjects; ++i) {
+    const material = randomArrayElement(materials);
+    const axis = vec3.normalize([rand(-1, 1), rand(-1, 1), rand(-1, 1)]);
+    const radius = rand(10, 100);
+    const speed = rand(0.1, 0.4);
+    const rotationSpeed = rand(-1, 1);
+    const scale = rand(2, 10);
+
+    const uniforms = {
+      diffuseTexture: randomArrayElement(textures),
+    };
+
+    const uboInfo = twgl.createUniformBlockInfo(gl, prgInfo, 'PerObjectUniforms');
+
+    objectInfos.push({
+      uniforms,
+      uboInfo,
+
+      axis,
+      material,
+      radius,
+      speed,
+      rotationSpeed,
+      scale,
+    });
+  }
+
+  gl.clearColor(0.3, 0.3, 0.3, 1);
+
+  const canvasToSizeMap = new WeakMap();
+  const degToRad = d => d * Math.PI / 180;
+
+  const settings = {
+    numObjects: 1000,
+    render: true,
+  };
+
+  const gui = new GUI();
+  gui.add(settings, 'numObjects', { min: 0, max: maxObjects, step: 1});
+  gui.add(settings, 'render');
+
+  let then = 0;
+
+  function render(time) {
+    time *= 0.001;  // convert to seconds
+    const deltaTime = time - then;
+    then = time;
+
+    const startTimeMs = performance.now();
+
+    const {width, height} = settings.render
+       ? canvasToSizeMap.get(canvas) ?? canvas
+       : { width: 1, height: 1 };
+
+    // Don't set the canvas size if it's already that size as it may be slow.
+    if (canvas.width !== width || canvas.height !== height) {
+      canvas.width = width;
+      canvas.height = height;
+    }
+
+    gl.viewport(0, 0, canvas.width, canvas.height);
+
+    // Get the current texture from the canvas context and
+    // set it as the texture to render to.
+    timingHelper.begin();
+    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+    gl.useProgram(prgInfo.program);
+    twgl.setBuffersAndAttributes(gl, prgInfo, bufferInfo);
+
+    const aspect = canvas.clientWidth / canvas.clientHeight;
+    const projection = mat4.perspective(
+        degToRad(60),
+        aspect,
+        1,      // zNear
+        2000,   // zFar
+    );
+
+    const eye = [100, 150, 200];
+    const target = [0, 0, 0];
+    const up = [0, 1, 0];
+
+    // Compute a view matrix
+    const viewMatrix = mat4.inverse(mat4.lookAt(eye, target, up));
+
+    // Combine the view and projection matrixes
+    mat4.multiply(projection, viewMatrix, globalUboInfo.uniforms.viewProjection);
+    twgl.setBlockUniforms(globalUboInfo, {
+      lightWorldPosition: [-10, 30, 300],
+      viewWorldPosition: eye,
+    });
+    twgl.setUniformBlock(gl, prgInfo, globalUboInfo);
+
+    let mathElapsedTimeMs = 0;
+    let currentActiveTexture = -1;
+    let currentMaterialUbo;
+    const currentTextures = [];
+
+    for (let i = 0; i < settings.numObjects; ++i) {
+      const {
+        uniforms,
+        uboInfo,
+
+        axis,
+        material,
+        radius,
+        speed,
+        rotationSpeed,
+        scale,
+      } = objectInfos[i];
+      const {
+        world,
+        normalMatrix,
+      } = uboInfo.uniforms;
+      const mathTimeStartMs = performance.now();
+
+      // Compute a world matrix
+      mat4.identity(world);
+      mat4.axisRotate(world, axis, i + time * speed, world);
+      mat4.translate(world, [0, 0, Math.sin(i * 3.721 + time * speed) * radius], world);
+      mat4.translate(world, [0, 0, Math.sin(i * 9.721 + time * 0.1) * radius], world);
+      mat4.rotateX(world, time * rotationSpeed + i, world);
+      mat4.scale(world, [scale, scale, scale], world);
+
+      // Inverse and transpose it into the normalMatrix value
+      mat3.fromMat4(mat4.transpose(mat4.inverse(world)), normalMatrix);
+
+      //twgl.bindUniformBlock(gl, prgInfo, materialUboInfo);
+      const { materialUboInfo } = material;
+      if (currentMaterialUbo !== materialUboInfo) {
+        currentMaterialUbo = materialUboInfo;
+        const blockSpec = prgInfo.uniformBlockSpec.blockSpecs[materialUboInfo.name];
+        gl.bindBufferRange(gl.UNIFORM_BUFFER, blockSpec.index, materialUboInfo.buffer, materialUboInfo.offset, materialUboInfo.size);
+      }
+
+      // do this manually since we're doing it manually in WebGPU
+      //twgl.setBlockUniforms(uboInfo, {
+      //  world: worldValue,
+      //  normalMatrix,
+      //  viewProjection: viewProjectionMatrix,
+      //  color,
+      //  lightWorldPosition: [-10, 30, 300],
+      //  viewWorldPosition: eye,
+      //  shininess,
+      //});
+
+      mathElapsedTimeMs += performance.now() - mathTimeStartMs;
+
+      // upload the uniform values to the uniform buffer
+      //twgl.setUniformBlock(gl, prgInfo, uboInfo);
+      gl.bindBuffer(gl.UNIFORM_BUFFER, uboInfo.buffer);
+      gl.bufferSubData(gl.UNIFORM_BUFFER, 0, uboInfo.asUint8, 0, uboInfo.size);
+      {
+        const blockSpec = prgInfo.uniformBlockSpec.blockSpecs[uboInfo.name];
+        gl.bindBufferRange(gl.UNIFORM_BUFFER, blockSpec.index, uboInfo.buffer, uboInfo.offset, uboInfo.size);
+      }
+
+      //twgl.setUniforms(prgInfo, uniforms);
+
+      // Do it manually since we're doing it manually in WebGPU
+      const loc = prgInfo.uniformLocations;
+
+      if (currentTextures[0] !== uniforms.diffuseTexture) {
+        if (currentActiveTexture !== 0) {
+          currentActiveTexture = 0;
+          gl.activeTexture(gl.TEXTURE0);
+        }
+        currentTextures[0] = uniforms.diffuseTexture;
+        gl.bindTexture(gl.TEXTURE_2D, uniforms.diffuseTexture);
+        gl.uniform1i(loc.diffuseTexture, 0);
+      }
+
+      //twgl.drawBufferInfo(gl, bufferInfo);
+      gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
+    }
+    timingHelper.end();
+
+    const elapsedTimeMs = performance.now() - startTimeMs;
+    fpsAverage.addSample(1 / deltaTime);
+    jsAverage.addSample(elapsedTimeMs);
+    mathAverage.addSample(mathElapsedTimeMs);
+    gpuAverage.addSample(timingHelper.getResult());
+
+    infoElem.textContent = `\
+js  : ${jsAverage.get().toFixed(1)}ms
+math: ${mathAverage.get().toFixed(1)}ms
+fps : ${fpsAverage.get().toFixed(0)}
+gpu : ${(gpuAverage.get() / 1000 / 1000).toFixed(1)}ms
+`;
+
+    requestAnimationFrame(render);
+  }
+  requestAnimationFrame(render);
+
+  const observer = new ResizeObserver(entries => {
+    entries.forEach(entry => {
+      canvasToSizeMap.set(entry.target, {
+        width: Math.max(1, entry.contentBoxSize[0].inlineSize),
+        height: Math.max(1, entry.contentBoxSize[0].blockSize),
+      });
+    });
+  });
+  observer.observe(canvas);
+}
+
+main();
+  </script>
+</html>
diff --git a/webgpu/webgl-optimization-none-uniform-buffers.html b/webgpu/webgl-optimization-none-uniform-buffers.html
index 56cafeee..ca8aba91 100644
--- a/webgpu/webgl-optimization-none-uniform-buffers.html
+++ b/webgpu/webgl-optimization-none-uniform-buffers.html
@@ -304,7 +304,7 @@
     });
   }
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   for (let i = 0; i < maxObjects; ++i) {
@@ -338,8 +338,6 @@
 
   const canvasToSizeMap = new WeakMap();
   const degToRad = d => d * Math.PI / 180;
-  const worldValue = mat4.create();
-  const normalMatrix = mat3.identity();
 
   const settings = {
     numObjects: 1000,
@@ -398,6 +396,8 @@
     const viewProjectionMatrix = mat4.multiply(projection, viewMatrix);
 
     let mathElapsedTimeMs = 0;
+    let currentActiveTexture = -1;
+    const currentTextures = [];
 
     for (let i = 0; i < settings.numObjects; ++i) {
       const {
@@ -411,38 +411,72 @@
         rotationSpeed,
         scale,
       } = objectInfos[i];
+      const {
+        world,
+        normalMatrix,
+        viewProjection,
+        color,
+        lightWorldPosition,
+        viewWorldPosition,
+        shininess,
+      } = uboInfo.uniforms;
       const mathTimeStartMs = performance.now();
 
       // Compute a world matrix
-      mat4.identity(worldValue);
-      mat4.axisRotate(worldValue, axis, i + time * speed, worldValue);
-      mat4.translate(worldValue, [0, 0, Math.sin(i * 3.721 + time * speed) * radius], worldValue);
-      mat4.translate(worldValue, [0, 0, Math.sin(i * 9.721 + time * 0.1) * radius], worldValue);
-      mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
-      mat4.scale(worldValue, [scale, scale, scale], worldValue);
+      mat4.identity(world);
+      mat4.axisRotate(world, axis, i + time * speed, world);
+      mat4.translate(world, [0, 0, Math.sin(i * 3.721 + time * speed) * radius], world);
+      mat4.translate(world, [0, 0, Math.sin(i * 9.721 + time * 0.1) * radius], world);
+      mat4.rotateX(world, time * rotationSpeed + i, world);
+      mat4.scale(world, [scale, scale, scale], world);
 
       // Inverse and transpose it into the normalMatrix value
-      mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrix);
-
-      const {color, shininess} = material;
-
-      twgl.setBlockUniforms(uboInfo, {
-        world: worldValue,
-        normalMatrix,
-        viewProjection: viewProjectionMatrix,
-        color,
-        lightWorldPosition: [-10, 30, 300],
-        viewWorldPosition: eye,
-        shininess,
-      });
+      mat3.fromMat4(mat4.transpose(mat4.inverse(world)), normalMatrix);
+
+      // const {color, shininess} = material;
+      color.set(material.color);
+      shininess[0] = material.shininess;
+
+      // do this manually since we're doing it manually in WebGPU
+      //twgl.setBlockUniforms(uboInfo, {
+      //  world: worldValue,
+      //  normalMatrix,
+      //  viewProjection: viewProjectionMatrix,
+      //  color,
+      //  lightWorldPosition: [-10, 30, 300],
+      //  viewWorldPosition: eye,
+      //  shininess,
+      //});
+      viewProjection.set(viewProjectionMatrix);
+      lightWorldPosition.set([-10, 30, 300]);
+      viewWorldPosition.set(eye);
 
       mathElapsedTimeMs += performance.now() - mathTimeStartMs;
 
       // upload the uniform values to the uniform buffer
-      twgl.setUniformBlock(gl, prgInfo, uboInfo);
-      twgl.setUniforms(prgInfo, uniforms);
+      //twgl.setUniformBlock(gl, prgInfo, uboInfo);
+      gl.bindBuffer(gl.UNIFORM_BUFFER, uboInfo.buffer);
+      gl.bufferSubData(gl.UNIFORM_BUFFER, 0, uboInfo.asUint8, 0, uboInfo.size);
+      const blockSpec = prgInfo.uniformBlockSpec.blockSpecs[uboInfo.name];
+      gl.bindBufferRange(gl.UNIFORM_BUFFER, blockSpec.index, uboInfo.buffer, uboInfo.offset, uboInfo.size);
+
+      //twgl.setUniforms(prgInfo, uniforms);
+
+      // Do it manually since we're doing it manually in WebGPU
+      const loc = prgInfo.uniformLocations;
+
+      if (currentTextures[0] !== uniforms.diffuseTexture) {
+        if (currentActiveTexture !== 0) {
+          currentActiveTexture = 0;
+          gl.activeTexture(gl.TEXTURE0);
+        }
+        currentTextures[0] = uniforms.diffuseTexture;
+        gl.bindTexture(gl.TEXTURE_2D, uniforms.diffuseTexture);
+        gl.uniform1i(loc.diffuseTexture, 0);
+      }
 
-      twgl.drawBufferInfo(gl, bufferInfo);
+      //twgl.drawBufferInfo(gl, bufferInfo);
+      gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
     }
     timingHelper.end();
 
diff --git a/webgpu/webgl-optimization-none.html b/webgpu/webgl-optimization-none.html
index 3148b3a4..f68d4c27 100644
--- a/webgpu/webgl-optimization-none.html
+++ b/webgpu/webgl-optimization-none.html
@@ -299,7 +299,7 @@
     });
   }
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   for (let i = 0; i < maxObjects; ++i) {
@@ -395,6 +395,8 @@
     const viewProjectionMatrix = mat4.multiply(projection, viewMatrix);
 
     let mathElapsedTimeMs = 0;
+    let currentActiveTexture = -1;
+    const currentTextures = [];
 
     for (let i = 0; i < settings.numObjects; ++i) {
       const {
@@ -410,7 +412,7 @@
       const mathTimeStartMs = performance.now();
 
       // Copy the viewProjectionMatrix into the uniform values for this object
-      uniforms.viewProjection.set(viewProjectionMatrix);
+      // uniforms.viewProjection.set(viewProjectionMatrix);
 
       // Compute a world matrix
       const worldValue = uniforms.world;
@@ -426,17 +428,39 @@
 
       const {color, shininess} = material;
 
-      uniforms.color.set(color);
-      uniforms.lightWorldPosition.set([-10, 30, 300]);
-      uniforms.viewWorldPosition.set(eye);
-      uniforms.shininess = shininess;
+      // uniforms.color.set(color);
+      // uniforms.lightWorldPosition.set([-10, 30, 300]);
+      // uniforms.viewWorldPosition.set(eye);
+      // uniforms.shininess = shininess;
 
       mathElapsedTimeMs += performance.now() - mathTimeStartMs;
 
-      // upload the uniform values to the uniform buffer
-      twgl.setUniforms(prgInfo, uniforms);
+      // update the uniforms
+      // twgl.setUniforms(prgInfo, uniforms);
 
-      twgl.drawBufferInfo(gl, bufferInfo);
+      // Do it manually since we're doing it manually in WebGPU
+      const loc = prgInfo.uniformLocations;
+
+      if (currentTextures[0] !== uniforms.diffuseTexture) {
+        if (currentActiveTexture !== 0) {
+          currentActiveTexture = 0;
+          gl.activeTexture(gl.TEXTURE0);
+        }
+        currentTextures[0] = uniforms.diffuseTexture;
+        gl.bindTexture(gl.TEXTURE_2D, uniforms.diffuseTexture);
+        gl.uniform1i(loc.diffuseTexture, 0);
+      }
+
+      gl.uniformMatrix4fv(loc.world, false, worldValue);
+      gl.uniformMatrix4fv(loc.viewProjection, false, viewProjectionMatrix);
+      gl.uniformMatrix3fv(loc.normalMatrix, false, uniforms.normalMatrix);
+      gl.uniform3fv(loc.lightWorldPosition, [-10, 30, 300]);
+      gl.uniform3fv(loc.viewWorldPosition, eye);
+      gl.uniform4fv(loc.color, color);
+      gl.uniform1f(loc.shininess, shininess);
+
+      // twgl.drawBufferInfo(gl, bufferInfo);
+      gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
     }
     timingHelper.end();
 
diff --git a/webgpu/webgl-optimization-uniform-buffers-one-large.html b/webgpu/webgl-optimization-uniform-buffers-one-large.html
new file mode 100644
index 00000000..f1aefcac
--- /dev/null
+++ b/webgpu/webgl-optimization-uniform-buffers-one-large.html
@@ -0,0 +1,548 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>WebGL Optimization - Uniform Blocks, one large buffer</title>
+    <style>
+      @import url(resources/webgpu-lesson.css);
+html, body {
+  margin: 0;       /* remove the default margin          */
+  height: 100%;    /* make the html,body fill the page   */
+}
+canvas {
+  display: block;  /* make the canvas act like a block   */
+  width: 100%;     /* make the canvas fill its container */
+  height: 100%;
+}
+:root {
+  --bg-color: #fff;
+}
+@media (prefers-color-scheme: dark) {
+  :root {
+    --bg-color: #000;
+  }
+}
+canvas {
+  background-color: var(--bg-color);
+}
+#info {
+  position: absolute;
+  left: 0;
+  top: 0;
+  padding: 0.5em;
+  margin: 0;
+  background-color: rgba(0, 0, 0, 0.8);
+  color: white;
+  min-width: 8em;
+}
+    </style>
+  </head>
+  <body>
+    <canvas></canvas>
+    <pre id="info"></pre>
+  </body>
+  <script type="module">
+//import 'https://greggman.github.io/webgl-lint/webgl-lint.js';
+import GUI from '../3rdparty/muigui-0.x.module.js';
+import * as twgl from '../3rdparty/twgl-full.module.js';
+import RollingAverage from './resources/js/rolling-average.js';
+
+class TimingHelper {
+  #ext;
+  #query;
+  #gl;
+  #state = 'free';
+  #duration = 0;
+
+  constructor(gl) {
+    this.#gl = gl;
+    this.#ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');
+    if (!this.#ext) {
+      return;
+    }
+    this.#query = gl.createQuery();
+  }
+
+  begin() {
+    if (!this.#ext || this.#state !== 'free') {
+      return;
+    }
+    this.#state = 'started';
+    const gl = this.#gl;
+    const ext = this.#ext;
+    const query = this.#query;
+    gl.beginQuery(ext.TIME_ELAPSED_EXT, query);
+  }
+  end() {
+    if (!this.#ext || this.#state === 'free') {
+      return;
+    }
+
+    const gl = this.#gl;
+    const ext = this.#ext;
+    const query = this.#query;
+
+    if (this.#state === 'started') {
+      gl.endQuery(ext.TIME_ELAPSED_EXT);
+      this.#state = 'waiting';
+    } else {
+      const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);
+      const disjoint = gl.getParameter(ext.GPU_DISJOINT_EXT);
+      if (available && !disjoint) {
+        this.#duration = gl.getQueryParameter(query, gl.QUERY_RESULT);
+      }
+      if (available || disjoint) {
+        this.#state = 'free';
+      }
+    }
+  }
+  getResult() {
+    return this.#duration;
+  }
+}
+
+const { m4: mat4, v3: vec3 } = twgl;
+const mat3 = {
+  identity() {
+    return new Float32Array(9);
+  },
+  fromMat4(m, dst) {
+    dst = dst || new Float32Array(9);
+
+    dst[0] =  m[ 0]; dst[1] = m[ 1]; dst[2] = m[ 2];
+    dst[3] =  m[ 4]; dst[4] = m[ 5]; dst[5] = m[ 6];
+    dst[6] =  m[ 8]; dst[7] = m[ 9]; dst[8] = m[10];
+
+    return dst;
+  },
+};
+
+const fpsAverage = new RollingAverage();
+const jsAverage = new RollingAverage();
+const gpuAverage = new RollingAverage();
+const mathAverage = new RollingAverage();
+
+/** Given a css color string, return an array of 4 values from 0 to 255 */
+const cssColorToRGBA8 = (() => {
+  const canvas = new OffscreenCanvas(1, 1);
+  const ctx = canvas.getContext('2d', {willReadFrequently: true});
+  return cssColor => {
+    ctx.clearRect(0, 0, 1, 1);
+    ctx.fillStyle = cssColor;
+    ctx.fillRect(0, 0, 1, 1);
+    return Array.from(ctx.getImageData(0, 0, 1, 1).data);
+  };
+})();
+
+/** Given a css color string, return an array of 4 values from 0 to 1 */
+const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
+
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * return the corresponding CSS hsl string
+ */
+const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of 4 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+
+/**
+ * Returns a random number between min and max.
+ * If min and max are not specified, returns 0 to 1
+ * If max is not specified, return 0 to min.
+ */
+function rand(min, max) {
+  if (min === undefined) {
+    max = 1;
+    min = 0;
+  } else if (max === undefined) {
+    max = min;
+    min = 0;
+  }
+  return Math.random() * (max - min) + min;
+}
+
+/** Selects a random array element */
+const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
+
+/** Rounds up v to a multiple of alignment */
+const roundUp = (v, alignment) => Math.ceil(v / alignment) * alignment;
+
+async function main() {
+  const infoElem = document.querySelector('#info');
+
+  // Get a WebGPU context from the canvas and configure it
+  const canvas = document.querySelector('canvas');
+  const gl = canvas.getContext('webgl2');
+  const timingHelper = new TimingHelper(gl);
+
+  const uniformBlock = `
+      uniform GlobalUniforms {
+        mat4 viewProjection;
+        vec3 lightWorldPosition;
+        vec3 viewWorldPosition;
+      };
+
+      uniform MaterialUniforms {
+        vec4 color;
+        float shininess;
+      };
+
+      uniform PerObjectUniforms {
+        mat3 normalMatrix;
+        mat4 world;
+      };
+  `;
+  const vs = `#version 300 es
+      ${uniformBlock}
+      layout(location = 0) in vec4 position;
+      layout(location = 1) in vec3 normal;
+      layout(location = 2) in vec2 texcoord;
+
+      out vec3 v_normal;
+      out vec3 v_surfaceToLight;
+      out vec3 v_surfaceToView;
+      out vec2 v_texcoord;
+
+      void main() {
+        gl_Position = viewProjection * world * position;
+
+        // Orient the normals and pass to the fragment shader
+        v_normal = normalMatrix * normal;
+
+        // Compute the world position of the surface
+        vec3 surfaceWorldPosition = (world * position).xyz;
+
+        // Compute the vector of the surface to the light
+        // and pass it to the fragment shader
+        v_surfaceToLight = lightWorldPosition - surfaceWorldPosition;
+
+        // Compute the vector of the surface to the light
+        // and pass it to the fragment shader
+        v_surfaceToView = viewWorldPosition - surfaceWorldPosition;
+
+        // Pass the texture coord on to the fragment shader
+        v_texcoord = texcoord;
+      }
+  `;
+
+  const fs = `#version 300 es
+      precision highp float;
+      ${uniformBlock}
+
+      in vec3 v_normal;
+      in vec3 v_surfaceToLight;
+      in vec3 v_surfaceToView;
+      in vec2 v_texcoord;
+
+      uniform sampler2D diffuseTexture;
+
+      out vec4 fragColor;
+
+      void main() {
+        // Because vsOut.normal is an inter-stage variable 
+        // it's interpolated so it will not be a unit vector.
+        // Normalizing it will make it a unit vector again
+        vec3 normal = normalize(v_normal);
+
+        vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
+        vec3 surfaceToViewDirection = normalize(v_surfaceToView);
+        vec3 halfVector = normalize(
+          surfaceToLightDirection + surfaceToViewDirection);
+
+        // Compute the light by taking the dot product
+        // of the normal with the direction to the light
+        float light = dot(normal, surfaceToLightDirection);
+
+        float specular = dot(normal, halfVector);
+        specular = specular > 0.0 ?
+            pow(specular, shininess) :
+            0.0;
+
+        vec4 diffuse = color * texture(diffuseTexture, v_texcoord);
+        // Lets multiply just the color portion (not the alpha)
+        // by the light
+        vec3 c = diffuse.rgb * light + specular;
+        fragColor = vec4(c, diffuse.a);
+      }
+    `;
+
+  const prgInfo = twgl.createProgramInfo(gl, [vs, fs]);
+
+  const globalUboInfo = twgl.createUniformBlockInfo(gl, prgInfo, 'GlobalUniforms');
+
+  const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
+    position:  new Float32Array([1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1]),
+    normal:    new Float32Array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1]),
+    texcoord: new Float32Array([1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1]),
+    indices:   new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23]),
+  });
+
+  gl.enable(gl.CULL_FACE);
+  gl.enable(gl.DEPTH_TEST);
+
+  const textures = [
+    'πŸ˜‚', 'πŸ‘Ύ', 'πŸ‘', 'πŸ‘€', '🌞', 'πŸ›Ÿ',
+  ].map(s => {
+    const size = 128;
+    const ctx = new OffscreenCanvas(size, size).getContext('2d');
+    ctx.fillStyle = '#fff';
+    ctx.fillRect(0, 0, size, size);
+    ctx.font = `${size * 0.9}px sans-serif`;
+    ctx.textAlign = 'center';
+    ctx.textBaseline = 'middle';
+    ctx.fillText(s, size / 2, size / 2);
+    return twgl.createTexture(gl, {
+      src: ctx.canvas,
+      wrap: gl.CLAMP_TO_EDGE,
+      min: gl.LINEAR_MIPMAP_NEAREST,
+      mag: gl.LINEAR,
+    });
+  });
+
+  const numMaterials = 20;
+  const materials = [];
+  for (let i = 0; i < numMaterials; ++i) {
+    const color = hslToRGBA(rand(), rand(0.5, 0.8), rand(0.5, 0.7));
+    const shininess = rand(10, 120);
+    const materialUboInfo = twgl.createUniformBlockInfo(gl, prgInfo, 'MaterialUniforms');
+
+    // Set the data in the typed array views
+    twgl.setBlockUniforms(materialUboInfo, {
+      color,
+      shininess,
+    });
+    // copy the typed array view data to the uniform buffer
+    twgl.setUniformBlock(gl, prgInfo, materialUboInfo);
+
+    materials.push({
+      color,
+      shininess,
+      texture: randomArrayElement(textures),
+      materialUboInfo,
+    });
+  }
+
+  const maxObjects = 30000;
+  const objectInfos = [];
+
+  const blockSize = roundUp(prgInfo.uniformBlockSpec.blockSpecs.PerObjectUniforms.size, 256);
+  const totalSize = blockSize * maxObjects;
+  const uniformBuffer = gl.createBuffer();
+  gl.bindBuffer(gl.UNIFORM_BUFFER, uniformBuffer);
+  gl.bufferData(gl.UNIFORM_BUFFER, totalSize, gl.DYNAMIC_DRAW);
+
+  const uniformArrayBuffer = new ArrayBuffer(totalSize);
+  const uniformView = new Uint8Array(uniformArrayBuffer);
+
+  for (let i = 0; i < maxObjects; ++i) {
+    const material = randomArrayElement(materials);
+    const axis = vec3.normalize([rand(-1, 1), rand(-1, 1), rand(-1, 1)]);
+    const radius = rand(10, 100);
+    const speed = rand(0.1, 0.4);
+    const rotationSpeed = rand(-1, 1);
+    const scale = rand(2, 10);
+
+    const uniforms = {
+      diffuseTexture: randomArrayElement(textures),
+    };
+
+    const uboInfo = twgl.createUniformBlockInfo(gl, prgInfo, 'PerObjectUniforms', {
+      buffer: uniformBuffer,
+      offset: blockSize * i,
+      array: uniformArrayBuffer,
+    });
+
+    objectInfos.push({
+      uniforms,
+      uboInfo,
+
+      axis,
+      material,
+      radius,
+      speed,
+      rotationSpeed,
+      scale,
+    });
+  }
+
+  gl.clearColor(0.3, 0.3, 0.3, 1);
+
+  const canvasToSizeMap = new WeakMap();
+  const degToRad = d => d * Math.PI / 180;
+
+  const settings = {
+    numObjects: 1000,
+    render: true,
+  };
+
+  const gui = new GUI();
+  gui.add(settings, 'numObjects', { min: 0, max: maxObjects, step: 1});
+  gui.add(settings, 'render');
+
+  let then = 0;
+
+  function render(time) {
+    time *= 0.001;  // convert to seconds
+    const deltaTime = time - then;
+    then = time;
+
+    const startTimeMs = performance.now();
+
+    const {width, height} = settings.render
+       ? canvasToSizeMap.get(canvas) ?? canvas
+       : { width: 1, height: 1 };
+
+    // Don't set the canvas size if it's already that size as it may be slow.
+    if (canvas.width !== width || canvas.height !== height) {
+      canvas.width = width;
+      canvas.height = height;
+    }
+
+    gl.viewport(0, 0, canvas.width, canvas.height);
+
+    // Get the current texture from the canvas context and
+    // set it as the texture to render to.
+    timingHelper.begin();
+    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+    gl.useProgram(prgInfo.program);
+    twgl.setBuffersAndAttributes(gl, prgInfo, bufferInfo);
+
+    const aspect = canvas.clientWidth / canvas.clientHeight;
+    const projection = mat4.perspective(
+        degToRad(60),
+        aspect,
+        1,      // zNear
+        2000,   // zFar
+    );
+
+    const eye = [100, 150, 200];
+    const target = [0, 0, 0];
+    const up = [0, 1, 0];
+
+    // Compute a view matrix
+    const viewMatrix = mat4.inverse(mat4.lookAt(eye, target, up));
+
+    // Combine the view and projection matrixes
+    mat4.multiply(projection, viewMatrix, globalUboInfo.uniforms.viewProjection);
+    twgl.setBlockUniforms(globalUboInfo, {
+      lightWorldPosition: [-10, 30, 300],
+      viewWorldPosition: eye,
+    });
+    twgl.setUniformBlock(gl, prgInfo, globalUboInfo);
+
+    let mathElapsedTimeMs = 0;
+    let currentActiveTexture = -1;
+    let currentMaterialUbo;
+    const currentTextures = [];
+
+    const mathTimeStartMs = performance.now();
+    for (let i = 0; i < settings.numObjects; ++i) {
+      const {
+        uboInfo,
+        axis,
+        radius,
+        speed,
+        rotationSpeed,
+        scale,
+      } = objectInfos[i];
+      const {
+        world,
+        normalMatrix,
+      } = uboInfo.uniforms;
+
+      // Compute a world matrix
+      mat4.identity(world);
+      mat4.axisRotate(world, axis, i + time * speed, world);
+      mat4.translate(world, [0, 0, Math.sin(i * 3.721 + time * speed) * radius], world);
+      mat4.translate(world, [0, 0, Math.sin(i * 9.721 + time * 0.1) * radius], world);
+      mat4.rotateX(world, time * rotationSpeed + i, world);
+      mat4.scale(world, [scale, scale, scale], world);
+
+      // Inverse and transpose it into the normalMatrix value
+      mat3.fromMat4(mat4.transpose(mat4.inverse(world)), normalMatrix);
+    }
+    mathElapsedTimeMs = performance.now() - mathTimeStartMs;
+
+    // Copy all uniform block values for all objects
+    gl.bindBuffer(gl.UNIFORM_BUFFER, uniformBuffer);
+    gl.bufferSubData(gl.UNIFORM_BUFFER, 0, uniformView, 0, settings.numObjects * blockSize);
+
+    for (let i = 0; i < settings.numObjects; ++i) {
+      const {
+        uniforms,
+        uboInfo,
+        material,
+      } = objectInfos[i];
+
+      //twgl.bindUniformBlock(gl, prgInfo, materialUboInfo);
+      const { materialUboInfo } = material;
+      if (currentMaterialUbo !== materialUboInfo) {
+        currentMaterialUbo = materialUboInfo;
+        const blockSpec = prgInfo.uniformBlockSpec.blockSpecs[materialUboInfo.name];
+        gl.bindBufferRange(gl.UNIFORM_BUFFER, blockSpec.index, materialUboInfo.buffer, materialUboInfo.offset, materialUboInfo.size);
+      }
+
+      // upload the uniform values to the uniform buffer
+      //twgl.setUniformBlock(gl, prgInfo, uboInfo);
+      {
+        const blockSpec = prgInfo.uniformBlockSpec.blockSpecs[uboInfo.name];
+        gl.bindBufferRange(gl.UNIFORM_BUFFER, blockSpec.index, uboInfo.buffer, uboInfo.offset, uboInfo.size);
+      }
+
+      //twgl.setUniforms(prgInfo, uniforms);
+
+      // Do it manually since we're doing it manually in WebGPU
+      const loc = prgInfo.uniformLocations;
+
+      if (currentTextures[0] !== uniforms.diffuseTexture) {
+        if (currentActiveTexture !== 0) {
+          currentActiveTexture = 0;
+          gl.activeTexture(gl.TEXTURE0);
+        }
+        currentTextures[0] = uniforms.diffuseTexture;
+        gl.bindTexture(gl.TEXTURE_2D, uniforms.diffuseTexture);
+        gl.uniform1i(loc.diffuseTexture, 0);
+      }
+
+      //twgl.drawBufferInfo(gl, bufferInfo);
+      gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
+    }
+    timingHelper.end();
+
+    const elapsedTimeMs = performance.now() - startTimeMs;
+    fpsAverage.addSample(1 / deltaTime);
+    jsAverage.addSample(elapsedTimeMs);
+    mathAverage.addSample(mathElapsedTimeMs);
+    gpuAverage.addSample(timingHelper.getResult());
+
+    infoElem.textContent = `\
+js  : ${jsAverage.get().toFixed(1)}ms
+math: ${mathAverage.get().toFixed(1)}ms
+fps : ${fpsAverage.get().toFixed(0)}
+gpu : ${(gpuAverage.get() / 1000 / 1000).toFixed(1)}ms
+`;
+
+    requestAnimationFrame(render);
+  }
+  requestAnimationFrame(render);
+
+  const observer = new ResizeObserver(entries => {
+    entries.forEach(entry => {
+      canvasToSizeMap.set(entry.target, {
+        width: Math.max(1, entry.contentBoxSize[0].inlineSize),
+        height: Math.max(1, entry.contentBoxSize[0].blockSize),
+      });
+    });
+  });
+  observer.observe(canvas);
+}
+
+main();
+  </script>
+</html>
diff --git a/webgpu/webgpu-optimization-all.html b/webgpu/webgpu-optimization-all.html
deleted file mode 100644
index a30a814b..00000000
--- a/webgpu/webgpu-optimization-all.html
+++ /dev/null
@@ -1,603 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
-    <title>WebGPU Optimization - None</title>
-    <style>
-      @import url(resources/webgpu-lesson.css);
-html, body {
-  margin: 0;       /* remove the default margin          */
-  height: 100%;    /* make the html,body fill the page   */
-}
-canvas {
-  display: block;  /* make the canvas act like a block   */
-  width: 100%;     /* make the canvas fill its container */
-  height: 100%;
-}
-:root {
-  --bg-color: #fff;
-}
-@media (prefers-color-scheme: dark) {
-  :root {
-    --bg-color: #000;
-  }
-}
-canvas {
-  background-color: var(--bg-color);
-}
-#info {
-  position: absolute;
-  left: 0;
-  top: 0;
-  padding: 0.5em;
-  margin: 0;
-  background-color: rgba(0, 0, 0, 0.8);
-  color: white;
-  min-width: 8em;
-}
-    </style>
-  </head>
-  <body>
-    <canvas></canvas>
-    <pre id="info"></pre>
-  </body>
-  <script type="module">
-import GUI from '../3rdparty/muigui-0.x.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#webgpu-utils
-import {createTextureFromSource} from '../3rdparty/webgpu-utils-1.x.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-matrix-math.html
-import {mat4, mat3, vec3} from '../3rdparty/wgpu-matrix.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html
-import TimingHelper from './resources/js/timing-helper.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html
-import RollingAverage from './resources/js/rolling-average.js';
-
-const fpsAverage = new RollingAverage();
-const jsAverage = new RollingAverage();
-const gpuAverage = new RollingAverage();
-const mathAverage = new RollingAverage();
-
-/** Given a css color string, return an array of 4 values from 0 to 255 */
-const cssColorToRGBA8 = (() => {
-  const canvas = new OffscreenCanvas(1, 1);
-  const ctx = canvas.getContext('2d', {willReadFrequently: true});
-  return cssColor => {
-    ctx.clearRect(0, 0, 1, 1);
-    ctx.fillStyle = cssColor;
-    ctx.fillRect(0, 0, 1, 1);
-    return Array.from(ctx.getImageData(0, 0, 1, 1).data);
-  };
-})();
-
-/** Given a css color string, return an array of 4 values from 0 to 1 */
-const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-
-/**
- * Given hue, saturation, and luminance values in the range of 0 to 1
- * return the corresponding CSS hsl string
- */
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
-
-/**
- * Given hue, saturation, and luminance values in the range of 0 to 1
- * returns an array of 4 values from 0 to 1
- */
-const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
-
-/**
- * Returns a random number between min and max.
- * If min and max are not specified, returns 0 to 1
- * If max is not specified, return 0 to min.
- */
-function rand(min, max) {
-  if (min === undefined) {
-    max = 1;
-    min = 0;
-  } else if (max === undefined) {
-    max = min;
-    min = 0;
-  }
-  return Math.random() * (max - min) + min;
-}
-
-/** Selects a random array element */
-const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
-
-async function main() {
-  const adapter = await navigator.gpu?.requestAdapter();
-  const canTimestamp = adapter.features.has('timestamp-query');
-  const device = await adapter?.requestDevice({
-    requiredFeatures: [
-      ...(canTimestamp ? ['timestamp-query'] : []),
-     ],
-  });
-  if (!device) {
-    fail('could not init WebGPU');
-  }
-
-  const timingHelper = new TimingHelper(device);
-  const infoElem = document.querySelector('#info');
-
-  // Get a WebGPU context from the canvas and configure it
-  const canvas = document.querySelector('canvas');
-  const context = canvas.getContext('webgpu');
-  const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
-  context.configure({
-    device,
-    format: presentationFormat,
-    alphaMode: 'premultiplied',
-  });
-
-  const module = device.createShaderModule({
-    code: `
-      struct GlobalUniforms {
-        viewProjection: mat4x4f,
-        lightWorldPosition: vec3f,
-        viewWorldPosition: vec3f,
-      };
-      struct PerObjectUniforms {
-        normalMatrix: mat3x3f,
-        world: mat4x4f,
-        color: vec4f,
-        shininess: f32,
-      };
-
-      struct Vertex {
-        @location(0) position: vec4f,
-        @location(1) normal: vec3f,
-        @location(2) texcoord: vec2f,
-      };
-
-      struct VSOutput {
-        @builtin(position) position: vec4f,
-        @location(0) normal: vec3f,
-        @location(1) surfaceToLight: vec3f,
-        @location(2) surfaceToView: vec3f,
-        @location(3) texcoord: vec2f,
-      };
-
-      @group(0) @binding(0) var diffuseTexture: texture_2d<f32>;
-      @group(0) @binding(1) var diffuseSampler: sampler;
-      @group(0) @binding(2) var<uniform> obj: PerObjectUniforms;
-      @group(0) @binding(3) var<uniform> glb: GlobalUniforms;
-
-      @vertex fn vs(vert: Vertex) -> VSOutput {
-        var vsOut: VSOutput;
-        vsOut.position = glb.viewProjection * obj.world * vert.position;
-
-        // Orient the normals and pass to the fragment shader
-        vsOut.normal = obj.normalMatrix * vert.normal;
-
-        // Compute the world position of the surface
-        let surfaceWorldPosition = (obj.world * vert.position).xyz;
-
-        // Compute the vector of the surface to the light
-        // and pass it to the fragment shader
-        vsOut.surfaceToLight = glb.lightWorldPosition - surfaceWorldPosition;
-
-        // Compute the vector of the surface to the light
-        // and pass it to the fragment shader
-        vsOut.surfaceToView = glb.viewWorldPosition - surfaceWorldPosition;
-
-        // Pass the texture coord on to the fragment shader
-        vsOut.texcoord = vert.texcoord;
-
-        return vsOut;
-      }
-
-      @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
-        // Because vsOut.normal is an inter-stage variable 
-        // it's interpolated so it will not be a unit vector.
-        // Normalizing it will make it a unit vector again
-        let normal = normalize(vsOut.normal);
-
-        let surfaceToLightDirection = normalize(vsOut.surfaceToLight);
-        let surfaceToViewDirection = normalize(vsOut.surfaceToView);
-        let halfVector = normalize(
-          surfaceToLightDirection + surfaceToViewDirection);
-
-        // Compute the light by taking the dot product
-        // of the normal with the direction to the light
-        let light = dot(normal, surfaceToLightDirection);
-
-        var specular = dot(normal, halfVector);
-        specular = select(
-            0.0,                           // value if condition is false
-            pow(specular, obj.shininess),  // value if condition is true
-            specular > 0.0);               // condition
-
-        let diffuse = obj.color * textureSample(diffuseTexture, diffuseSampler, vsOut.texcoord);
-        // Lets multiply just the color portion (not the alpha)
-        // by the light
-        let color = diffuse.rgb * light + specular;
-        return vec4f(color, diffuse.a);
-      }
-    `,
-  });
-
-  function createBufferWithData(device, data, usage) {
-    const buffer = device.createBuffer({
-      size: data.byteLength,
-      usage: usage,
-      mappedAtCreation: true,
-    });
-    const dst = new Uint8Array(buffer.getMappedRange());
-    dst.set(new Uint8Array(data.buffer));
-    buffer.unmap();
-    return buffer;
-  }
-
-  const vertexData = new Float32Array([
-  // position       normal        texcoord
-     1,  1, -1,     1,  0,  0,    1, 0,
-     1,  1,  1,     1,  0,  0,    0, 0,
-     1, -1,  1,     1,  0,  0,    0, 1,
-     1, -1, -1,     1,  0,  0,    1, 1,
-    -1,  1,  1,    -1,  0,  0,    1, 0,
-    -1,  1, -1,    -1,  0,  0,    0, 0,
-    -1, -1, -1,    -1,  0,  0,    0, 1,
-    -1, -1,  1,    -1,  0,  0,    1, 1,
-    -1,  1,  1,     0,  1,  0,    1, 0,
-     1,  1,  1,     0,  1,  0,    0, 0,
-     1,  1, -1,     0,  1,  0,    0, 1,
-    -1,  1, -1,     0,  1,  0,    1, 1,
-    -1, -1, -1,     0, -1,  0,    1, 0,
-     1, -1, -1,     0, -1,  0,    0, 0,
-     1, -1,  1,     0, -1,  0,    0, 1,
-    -1, -1,  1,     0, -1,  0,    1, 1,
-     1,  1,  1,     0,  0,  1,    1, 0,
-    -1,  1,  1,     0,  0,  1,    0, 0,
-    -1, -1,  1,     0,  0,  1,    0, 1,
-     1, -1,  1,     0,  0,  1,    1, 1,
-    -1,  1, -1,     0,  0, -1,    1, 0,
-     1,  1, -1,     0,  0, -1,    0, 0,
-     1, -1, -1,     0,  0, -1,    0, 1,
-    -1, -1, -1,     0,  0, -1,    1, 1,
-  ]);
-  const indices   = new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23]);
-
-  const vertexBuffer = createBufferWithData(device, vertexData, GPUBufferUsage.VERTEX);
-  const indicesBuffer = createBufferWithData(device, indices, GPUBufferUsage.INDEX);
-  const numVertices = indices.length;
-
-  const pipeline = device.createRenderPipeline({
-    label: 'textured model with point light w/specular highlight',
-    layout: 'auto',
-    vertex: {
-      module,
-      buffers: [
-        {
-          arrayStride: (3 + 3 + 2) * 4, // 8 floats
-          attributes: [
-            {shaderLocation: 0, offset: 0 * 4, format: 'float32x3'}, // position
-            {shaderLocation: 1, offset: 3 * 4, format: 'float32x3'}, // normal
-            {shaderLocation: 2, offset: 6 * 4, format: 'float32x2'}, // texcoord
-          ],
-        },
-      ],
-    },
-    fragment: {
-      module,
-      targets: [{ format: presentationFormat }],
-    },
-    primitive: {
-      cullMode: 'back',
-    },
-    depthStencil: {
-      depthWriteEnabled: true,
-      depthCompare: 'less',
-      format: 'depth24plus',
-    },
-  });
-
-  const textures = [
-    'πŸ˜‚', 'πŸ‘Ύ', 'πŸ‘', 'πŸ‘€', '🌞', 'πŸ›Ÿ',
-  ].map(s => {
-    const size = 128;
-    const ctx = new OffscreenCanvas(size, size).getContext('2d');
-    ctx.fillStyle = '#fff';
-    ctx.fillRect(0, 0, size, size);
-    ctx.font = `${size * 0.9}px sans-serif`;
-    ctx.textAlign = 'center';
-    ctx.textBaseline = 'middle';
-    ctx.fillText(s, size / 2, size / 2);
-    return createTextureFromSource(device, ctx.canvas, {mips: true});
-  });
-
-  const sampler = device.createSampler({
-    magFilter: 'linear',
-    minFilter: 'linear',
-    mipmapFilter: 'nearest',
-  });
-
-  const numMaterials = 20;
-  const materials = [];
-  for (let i = 0; i < numMaterials; ++i) {
-    const color = hslToRGBA(rand(), rand(0.5, 0.8), rand(0.5, 0.7));
-    const shininess = rand(10, 120);
-    materials.push({
-      color,
-      shininess,
-      texture: randomArrayElement(textures),
-      sampler,
-    });
-  }
-
-  const sharedUniformBufferSize = (16 + 4 + 4) * 4;
-  const sharedUniformBuffer = device.createBuffer({
-    label: 'shared uniforms',
-    size: sharedUniformBufferSize,
-    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
-  });
-
-  const sharedUniformValues = new Float32Array(sharedUniformBufferSize / 4);
-
-  const kViewProjectionOffset = 0;
-  const kLightWorldPositionOffset = 16;
-  const kViewWorldPositionOffset = 20;
-
-  const viewProjectionValue = sharedUniformValues.subarray(
-      kViewProjectionOffset, kViewProjectionOffset + 16);
-  const lightWorldPositionValue = sharedUniformValues.subarray(
-      kLightWorldPositionOffset, kLightWorldPositionOffset + 3);
-  const viewWorldPositionValue = sharedUniformValues.subarray(
-      kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
-
-  const maxObjects = 20000;
-  const objectInfos = [];
-
-  for (let i = 0; i < maxObjects; ++i) {
-    const uniformBufferSize = (12 + 16 + 4 + 4) * 4;
-    const uniformBuffer = device.createBuffer({
-      label: 'uniforms',
-      size: uniformBufferSize,
-      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
-    });
-
-    const uniformValues = new Float32Array(uniformBufferSize / 4);
-
-    // offsets to the various uniform values in float32 indices
-    const kNormalMatrixOffset = 0;
-    const kWorldOffset = 12;
-    const kColorOffset = 28;
-    const kShininessOffset = 32;
-
-    const normalMatrixValue = uniformValues.subarray(
-        kNormalMatrixOffset, kNormalMatrixOffset + 12);
-    const worldValue = uniformValues.subarray(
-        kWorldOffset, kWorldOffset + 16);
-    const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4);
-    const shininessValue = uniformValues.subarray(
-        kShininessOffset, kShininessOffset + 1);
-
-    const material = randomArrayElement(materials);
-
-    const bindGroup = device.createBindGroup({
-      label: 'bind group for object',
-      layout: pipeline.getBindGroupLayout(0),
-      entries: [
-        { binding: 0, resource: material.texture.createView() },
-        { binding: 1, resource: material.sampler },
-        { binding: 2, resource: { buffer: uniformBuffer }},
-        { binding: 3, resource: { buffer: sharedUniformBuffer }},
-      ],
-    });
-
-    const color = hslToRGBA(rand(), rand(0.5, 0.8), rand(0.5, 0.7));
-    const axis = vec3.normalize([rand(-1, 1), rand(-1, 1), rand(-1, 1)]);
-    const radius = rand(10, 100);
-    const speed = rand(0.1, 0.4);
-    const rotationSpeed = rand(-1, 1);
-    const scale = rand(2, 10);
-    const shininess = rand(10, 120);
-
-    objectInfos.push({
-      bindGroup,
-
-      uniformBuffer,
-      uniformValues,
-
-      normalMatrixValue,
-      worldValue,
-      colorValue,
-      shininessValue,
-
-      axis,
-      color,
-      radius,
-      speed,
-      rotationSpeed,
-      scale,
-      shininess,
-    });
-  }
-
-  const renderPassDescriptor = {
-    label: 'our basic canvas renderPass',
-    colorAttachments: [
-      {
-        // view: <- to be filled out when we render
-        clearValue: [0.3, 0.3, 0.3, 1],
-        loadOp: 'clear',
-        storeOp: 'store',
-      },
-    ],
-    depthStencilAttachment: {
-      // view: <- to be filled out when we render
-      depthClearValue: 1.0,
-      depthLoadOp: 'clear',
-      depthStoreOp: 'store',
-    },
-  };
-
-  const canvasToSizeMap = new WeakMap();
-  const degToRad = d => d * Math.PI / 180;
-
-  const settings = {
-    numObjects: 1000,
-    render: true,
-  };
-
-  const gui = new GUI();
-  gui.add(settings, 'numObjects', { min: 0, max: maxObjects, step: 1});
-  gui.add(settings, 'render');
-
-  let depthTexture;
-  let then = 0;
-
-  function render(time) {
-    time *= 0.001;  // convert to seconds
-    const deltaTime = time - then;
-    then = time;
-
-    const startTimeMs = performance.now();
-
-    const {width, height} = settings.render
-       ? canvasToSizeMap.get(canvas) ?? canvas
-       : { width: 1, height: 1 };
-
-    // Don't set the canvas size if it's already that size as it may be slow.
-    if (canvas.width !== width || canvas.height !== height) {
-      canvas.width = width;
-      canvas.height = height;
-    }
-
-    // Get the current texture from the canvas context and
-    // set it as the texture to render to.
-    const canvasTexture = context.getCurrentTexture();
-    renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView();
-
-    // If we don't have a depth texture OR if its size is different
-    // from the canvasTexture when make a new depth texture
-    if (!depthTexture ||
-        depthTexture.width !== canvasTexture.width ||
-        depthTexture.height !== canvasTexture.height) {
-      if (depthTexture) {
-        depthTexture.destroy();
-      }
-      depthTexture = device.createTexture({
-        size: [canvasTexture.width, canvasTexture.height],
-        format: 'depth24plus',
-        usage: GPUTextureUsage.RENDER_ATTACHMENT,
-      });
-    }
-    renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
-
-    const encoder = device.createCommandEncoder();
-    const pass = timingHelper.beginRenderPass(encoder, renderPassDescriptor);
-    pass.setPipeline(pipeline);
-    pass.setVertexBuffer(0, vertexBuffer);
-    pass.setIndexBuffer(indicesBuffer, 'uint16');
-
-    const aspect = canvas.clientWidth / canvas.clientHeight;
-    const projection = mat4.perspective(
-        degToRad(60),
-        aspect,
-        1,      // zNear
-        2000,   // zFar
-    );
-
-    const eye = [100, 150, 200];
-    const target = [0, 0, 0];
-    const up = [0, 1, 0];
-
-    // Compute a view matrix
-    const viewMatrix = mat4.lookAt(eye, target, up);
-
-    // Combine the view and projection matrixes
-    mat4.multiply(projection, viewMatrix, viewProjectionValue);
-
-    lightWorldPositionValue.set([-10, 30, 300]);
-    viewWorldPositionValue.set(eye);
-
-    device.queue.writeBuffer(sharedUniformBuffer, 0, sharedUniformValues);
-
-    let mathElapsedTimeMs = 0;
-
-    for (let i = 0; i < settings.numObjects; ++i) {
-      const {
-        bindGroup,
-        uniformBuffer,
-        uniformValues,
-        normalMatrixValue,
-        worldValue,
-        colorValue,
-        shininessValue,
-
-        axis,
-        material,
-        radius,
-        speed,
-        rotationSpeed,
-        scale,
-      } = objectInfos[i];
-      const mathTimeStartMs = performance.now();
-
-      // Compute a world matrix
-      mat4.identity(worldValue);
-      mat4.axisRotate(worldValue, axis, i + time * speed, worldValue);
-      mat4.translate(worldValue, [0, 0, Math.sin(i * 3.721 + time * speed) * radius], worldValue);
-      mat4.translate(worldValue, [0, 0, Math.sin(i * 9.721 + time * 0.1) * radius], worldValue);
-      mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
-      mat4.scale(worldValue, [scale, scale, scale], worldValue);
-
-      // Inverse and transpose it into the normalMatrix value
-      mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
-
-      const {color, shininess} = material;
-      colorValue.set(color);
-      shininessValue[0] = shininess;
-
-      mathElapsedTimeMs += performance.now() - mathTimeStartMs;
-
-      // upload the uniform values to the uniform buffer
-      device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
-
-      pass.setBindGroup(0, bindGroup);
-      pass.drawIndexed(numVertices);
-    }
-
-    pass.end();
-
-    const commandBuffer = encoder.finish();
-    device.queue.submit([commandBuffer]);
-
-    timingHelper.getResult().then(gpuTime => {
-      gpuAverage.addSample(gpuTime / 1000);
-    });
-
-    const elapsedTimeMs = performance.now() - startTimeMs;
-    fpsAverage.addSample(1 / deltaTime);
-    jsAverage.addSample(elapsedTimeMs);
-    mathAverage.addSample(mathElapsedTimeMs);
-
-    infoElem.textContent = `\
-js  : ${jsAverage.get().toFixed(1)}ms
-math: ${mathAverage.get().toFixed(1)}ms
-fps : ${fpsAverage.get().toFixed(0)}
-gpu : ${canTimestamp ? `${(gpuAverage.get() / 1000).toFixed(1)}ms` : 'N/A'}
-`;
-
-    requestAnimationFrame(render);
-  }
-  requestAnimationFrame(render);
-
-  const observer = new ResizeObserver(entries => {
-    entries.forEach(entry => {
-      canvasToSizeMap.set(entry.target, {
-        width: Math.max(1, Math.min(entry.contentBoxSize[0].inlineSize, device.limits.maxTextureDimension2D)),
-        height: Math.max(1, Math.min(entry.contentBoxSize[0].blockSize, device.limits.maxTextureDimension2D)),
-      });
-    });
-  });
-  observer.observe(canvas);
-}
-
-function fail(msg) {
-  alert(msg);
-}
-
-main();
-  </script>
-</html>
diff --git a/webgpu/webgpu-optimization-none.html b/webgpu/webgpu-optimization-none.html
index 9bbdc3fd..cfecd80c 100644
--- a/webgpu/webgpu-optimization-none.html
+++ b/webgpu/webgpu-optimization-none.html
@@ -309,7 +309,7 @@
     });
   }
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   for (let i = 0; i < maxObjects; ++i) {
diff --git a/webgpu/webgpu-optimization-step3-global-vs-per-object-uniforms.html b/webgpu/webgpu-optimization-step3-global-vs-per-object-uniforms.html
index c7798063..6705c431 100644
--- a/webgpu/webgpu-optimization-step3-global-vs-per-object-uniforms.html
+++ b/webgpu/webgpu-optimization-step3-global-vs-per-object-uniforms.html
@@ -344,7 +344,7 @@
   const viewWorldPositionValue = globalUniformValues.subarray(
       kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   for (let i = 0; i < maxObjects; ++i) {
diff --git a/webgpu/webgpu-optimization-step4-material-uniforms.html b/webgpu/webgpu-optimization-step4-material-uniforms.html
index 4457f788..acacb750 100644
--- a/webgpu/webgpu-optimization-step4-material-uniforms.html
+++ b/webgpu/webgpu-optimization-step4-material-uniforms.html
@@ -360,7 +360,7 @@
   const viewWorldPositionValue = globalUniformValues.subarray(
       kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   for (let i = 0; i < maxObjects; ++i) {
diff --git a/webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers-pre-submit.html b/webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers-pre-submit.html
deleted file mode 100644
index 93416361..00000000
--- a/webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers-pre-submit.html
+++ /dev/null
@@ -1,615 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
-    <title>WebGPU Optimization - None</title>
-    <style>
-      @import url(resources/webgpu-lesson.css);
-html, body {
-  margin: 0;       /* remove the default margin          */
-  height: 100%;    /* make the html,body fill the page   */
-}
-canvas {
-  display: block;  /* make the canvas act like a block   */
-  width: 100%;     /* make the canvas fill its container */
-  height: 100%;
-}
-:root {
-  --bg-color: #fff;
-}
-@media (prefers-color-scheme: dark) {
-  :root {
-    --bg-color: #000;
-  }
-}
-canvas {
-  background-color: var(--bg-color);
-}
-#info {
-  position: absolute;
-  left: 0;
-  top: 0;
-  padding: 0.5em;
-  margin: 0;
-  background-color: rgba(0, 0, 0, 0.8);
-  color: white;
-  min-width: 8em;
-}
-    </style>
-  </head>
-  <body>
-    <canvas></canvas>
-    <pre id="info"></pre>
-  </body>
-  <script type="module">
-import GUI from '../3rdparty/muigui-0.x.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#webgpu-utils
-import {createTextureFromSource} from '../3rdparty/webgpu-utils-1.x.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-matrix-math.html
-import {mat4, mat3, vec3} from '../3rdparty/wgpu-matrix.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html
-import TimingHelper from './resources/js/timing-helper.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html
-import RollingAverage from './resources/js/rolling-average.js';
-
-const fpsAverage = new RollingAverage();
-const jsAverage = new RollingAverage();
-const gpuAverage = new RollingAverage();
-const mathAverage = new RollingAverage();
-
-/** Given a css color string, return an array of 4 values from 0 to 255 */
-const cssColorToRGBA8 = (() => {
-  const canvas = new OffscreenCanvas(1, 1);
-  const ctx = canvas.getContext('2d', {willReadFrequently: true});
-  return cssColor => {
-    ctx.clearRect(0, 0, 1, 1);
-    ctx.fillStyle = cssColor;
-    ctx.fillRect(0, 0, 1, 1);
-    return Array.from(ctx.getImageData(0, 0, 1, 1).data);
-  };
-})();
-
-/** Given a css color string, return an array of 4 values from 0 to 1 */
-const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-
-/**
- * Given hue, saturation, and luminance values in the range of 0 to 1
- * return the corresponding CSS hsl string
- */
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
-
-/**
- * Given hue, saturation, and luminance values in the range of 0 to 1
- * returns an array of 4 values from 0 to 1
- */
-const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
-
-/**
- * Returns a random number between min and max.
- * If min and max are not specified, returns 0 to 1
- * If max is not specified, return 0 to min.
- */
-function rand(min, max) {
-  if (min === undefined) {
-    max = 1;
-    min = 0;
-  } else if (max === undefined) {
-    max = min;
-    min = 0;
-  }
-  return Math.random() * (max - min) + min;
-}
-
-/** Selects a random array element */
-const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
-
-async function main() {
-  const adapter = await navigator.gpu?.requestAdapter();
-  const canTimestamp = adapter.features.has('timestamp-query');
-  const device = await adapter?.requestDevice({
-    requiredFeatures: [
-      ...(canTimestamp ? ['timestamp-query'] : []),
-     ],
-  });
-  if (!device) {
-    fail('could not init WebGPU');
-  }
-
-  const timingHelper = new TimingHelper(device);
-  const infoElem = document.querySelector('#info');
-
-  // Get a WebGPU context from the canvas and configure it
-  const canvas = document.querySelector('canvas');
-  const context = canvas.getContext('webgpu');
-  const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
-  context.configure({
-    device,
-    format: presentationFormat,
-    alphaMode: 'premultiplied',
-  });
-
-  const module = device.createShaderModule({
-    code: `
-      struct GlobalUniforms {
-        viewProjection: mat4x4f,
-        lightWorldPosition: vec3f,
-        viewWorldPosition: vec3f,
-      };
-
-      struct MaterialUniforms {
-        color: vec4f,
-        shininess: f32,
-      };
-
-      struct PerObjectUniforms {
-        normalMatrix: mat3x3f,
-        world: mat4x4f,
-      };
-
-      struct Vertex {
-        @location(0) position: vec4f,
-        @location(1) normal: vec3f,
-        @location(2) texcoord: vec2f,
-      };
-
-      struct VSOutput {
-        @builtin(position) position: vec4f,
-        @location(0) normal: vec3f,
-        @location(1) surfaceToLight: vec3f,
-        @location(2) surfaceToView: vec3f,
-        @location(3) texcoord: vec2f,
-      };
-
-      @group(0) @binding(0) var diffuseTexture: texture_2d<f32>;
-      @group(0) @binding(1) var diffuseSampler: sampler;
-      @group(0) @binding(2) var<uniform> obj: PerObjectUniforms;
-      @group(0) @binding(3) var<uniform> glb: GlobalUniforms;
-      @group(0) @binding(4) var<uniform> material: MaterialUniforms;
-
-      @vertex fn vs(vert: Vertex) -> VSOutput {
-        var vsOut: VSOutput;
-        vsOut.position = glb.viewProjection * obj.world * vert.position;
-
-        // Orient the normals and pass to the fragment shader
-        vsOut.normal = obj.normalMatrix * vert.normal;
-
-        // Compute the world position of the surface
-        let surfaceWorldPosition = (obj.world * vert.position).xyz;
-
-        // Compute the vector of the surface to the light
-        // and pass it to the fragment shader
-        vsOut.surfaceToLight = glb.lightWorldPosition - surfaceWorldPosition;
-
-        // Compute the vector of the surface to the light
-        // and pass it to the fragment shader
-        vsOut.surfaceToView = glb.viewWorldPosition - surfaceWorldPosition;
-
-        // Pass the texture coord on to the fragment shader
-        vsOut.texcoord = vert.texcoord;
-
-        return vsOut;
-      }
-
-      @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
-        // Because vsOut.normal is an inter-stage variable 
-        // it's interpolated so it will not be a unit vector.
-        // Normalizing it will make it a unit vector again
-        let normal = normalize(vsOut.normal);
-
-        let surfaceToLightDirection = normalize(vsOut.surfaceToLight);
-        let surfaceToViewDirection = normalize(vsOut.surfaceToView);
-        let halfVector = normalize(
-          surfaceToLightDirection + surfaceToViewDirection);
-
-        // Compute the light by taking the dot product
-        // of the normal with the direction to the light
-        let light = dot(normal, surfaceToLightDirection);
-
-        var specular = dot(normal, halfVector);
-        specular = select(
-            0.0,                           // value if condition is false
-            pow(specular, material.shininess),  // value if condition is true
-            specular > 0.0);               // condition
-
-        let diffuse = material.color * textureSample(diffuseTexture, diffuseSampler, vsOut.texcoord);
-        // Lets multiply just the color portion (not the alpha)
-        // by the light
-        let color = diffuse.rgb * light + specular;
-        return vec4f(color, diffuse.a);
-      }
-    `,
-  });
-
-  function createBufferWithData(device, data, usage) {
-    const buffer = device.createBuffer({
-      size: data.byteLength,
-      usage: usage,
-      mappedAtCreation: true,
-    });
-    const dst = new Uint8Array(buffer.getMappedRange());
-    dst.set(new Uint8Array(data.buffer));
-    buffer.unmap();
-    return buffer;
-  }
-
-  const vertexData = new Float32Array([
-  // position       normal        texcoord
-     1,  1, -1,     1,  0,  0,    1, 0,
-     1,  1,  1,     1,  0,  0,    0, 0,
-     1, -1,  1,     1,  0,  0,    0, 1,
-     1, -1, -1,     1,  0,  0,    1, 1,
-    -1,  1,  1,    -1,  0,  0,    1, 0,
-    -1,  1, -1,    -1,  0,  0,    0, 0,
-    -1, -1, -1,    -1,  0,  0,    0, 1,
-    -1, -1,  1,    -1,  0,  0,    1, 1,
-    -1,  1,  1,     0,  1,  0,    1, 0,
-     1,  1,  1,     0,  1,  0,    0, 0,
-     1,  1, -1,     0,  1,  0,    0, 1,
-    -1,  1, -1,     0,  1,  0,    1, 1,
-    -1, -1, -1,     0, -1,  0,    1, 0,
-     1, -1, -1,     0, -1,  0,    0, 0,
-     1, -1,  1,     0, -1,  0,    0, 1,
-    -1, -1,  1,     0, -1,  0,    1, 1,
-     1,  1,  1,     0,  0,  1,    1, 0,
-    -1,  1,  1,     0,  0,  1,    0, 0,
-    -1, -1,  1,     0,  0,  1,    0, 1,
-     1, -1,  1,     0,  0,  1,    1, 1,
-    -1,  1, -1,     0,  0, -1,    1, 0,
-     1,  1, -1,     0,  0, -1,    0, 0,
-     1, -1, -1,     0,  0, -1,    0, 1,
-    -1, -1, -1,     0,  0, -1,    1, 1,
-  ]);
-  const indices   = new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23]);
-
-  const vertexBuffer = createBufferWithData(device, vertexData, GPUBufferUsage.VERTEX);
-  const indicesBuffer = createBufferWithData(device, indices, GPUBufferUsage.INDEX);
-  const numVertices = indices.length;
-
-  const pipeline = device.createRenderPipeline({
-    label: 'textured model with point light w/specular highlight',
-    layout: 'auto',
-    vertex: {
-      module,
-      buffers: [
-        {
-          arrayStride: (3 + 3 + 2) * 4, // 8 floats
-          attributes: [
-            {shaderLocation: 0, offset: 0 * 4, format: 'float32x3'}, // position
-            {shaderLocation: 1, offset: 3 * 4, format: 'float32x3'}, // normal
-            {shaderLocation: 2, offset: 6 * 4, format: 'float32x2'}, // texcoord
-          ],
-        },
-      ],
-    },
-    fragment: {
-      module,
-      targets: [{ format: presentationFormat }],
-    },
-    primitive: {
-      cullMode: 'back',
-    },
-    depthStencil: {
-      depthWriteEnabled: true,
-      depthCompare: 'less',
-      format: 'depth24plus',
-    },
-  });
-
-  const textures = [
-    'πŸ˜‚', 'πŸ‘Ύ', 'πŸ‘', 'πŸ‘€', '🌞', 'πŸ›Ÿ',
-  ].map(s => {
-    const size = 128;
-    const ctx = new OffscreenCanvas(size, size).getContext('2d');
-    ctx.fillStyle = '#fff';
-    ctx.fillRect(0, 0, size, size);
-    ctx.font = `${size * 0.9}px sans-serif`;
-    ctx.textAlign = 'center';
-    ctx.textBaseline = 'middle';
-    ctx.fillText(s, size / 2, size / 2);
-    return createTextureFromSource(device, ctx.canvas, {mips: true});
-  });
-
-  const sampler = device.createSampler({
-    magFilter: 'linear',
-    minFilter: 'linear',
-    mipmapFilter: 'nearest',
-  });
-
-  const numMaterials = 20;
-  const materials = [];
-  for (let i = 0; i < numMaterials; ++i) {
-    const color = hslToRGBA(rand(), rand(0.5, 0.8), rand(0.5, 0.7));
-    const shininess = rand(10, 120);
-
-    const materialValues = new Float32Array([
-      ...color,
-      shininess,
-      0, 0, 0,  // padding
-    ]);
-    const materialUniformBuffer = createBufferWithData(
-      device,
-      materialValues,
-      GPUBufferUsage.UNIFORM,
-    );
-
-    materials.push({
-      materialUniformBuffer,
-      texture: randomArrayElement(textures),
-      sampler,
-    });
-  }
-
-  const globalUniformBufferSize = (16 + 4 + 4) * 4;
-  const globalUniformBuffer = device.createBuffer({
-    label: 'global uniforms',
-    size: globalUniformBufferSize,
-    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
-  });
-
-  const globalUniformValues = new Float32Array(globalUniformBufferSize / 4);
-
-  const kViewProjectionOffset = 0;
-  const kLightWorldPositionOffset = 16;
-  const kViewWorldPositionOffset = 20;
-
-  const viewProjectionValue = globalUniformValues.subarray(
-      kViewProjectionOffset, kViewProjectionOffset + 16);
-  const lightWorldPositionValue = globalUniformValues.subarray(
-      kLightWorldPositionOffset, kLightWorldPositionOffset + 3);
-  const viewWorldPositionValue = globalUniformValues.subarray(
-      kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
-
-  const maxObjects = 20000;
-  const objectInfos = [];
-
-  for (let i = 0; i < maxObjects; ++i) {
-    const material = randomArrayElement(materials);
-
-    const uniformBufferSize = (12 + 16 + 4 + 4) * 4;
-
-    const numBindGroupsUniformPairs = 2;
-    const bindGroupsUniformPairs = [];
-    for (let i = 0; i < numBindGroupsUniformPairs; ++i) {
-      const uniformBuffer = device.createBuffer({
-        label: 'uniforms',
-        size: uniformBufferSize,
-        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
-      });
-
-      const bindGroup = device.createBindGroup({
-        label: 'bind group for object',
-        layout: pipeline.getBindGroupLayout(0),
-        entries: [
-          { binding: 0, resource: material.texture.createView() },
-          { binding: 1, resource: material.sampler },
-          { binding: 2, resource: { buffer: uniformBuffer }},
-          { binding: 3, resource: { buffer: globalUniformBuffer }},
-          { binding: 4, resource: { buffer: material.materialUniformBuffer }},
-        ],
-      });
-
-      bindGroupsUniformPairs.push({
-        uniformBuffer,
-        bindGroup,
-      });
-    }
-
-    const uniformValues = new Float32Array(uniformBufferSize / 4);
-
-    // offsets to the various uniform values in float32 indices
-    const kNormalMatrixOffset = 0;
-    const kWorldOffset = 12;
-
-    const normalMatrixValue = uniformValues.subarray(
-        kNormalMatrixOffset, kNormalMatrixOffset + 12);
-    const worldValue = uniformValues.subarray(
-        kWorldOffset, kWorldOffset + 16);
-
-    const axis = vec3.normalize([rand(-1, 1), rand(-1, 1), rand(-1, 1)]);
-    const radius = rand(10, 100);
-    const speed = rand(0.1, 0.4);
-    const rotationSpeed = rand(-1, 1);
-    const scale = rand(2, 10);
-
-    objectInfos.push({
-      bindGroupsUniformPairs,
-
-      uniformValues,
-
-      normalMatrixValue,
-      worldValue,
-
-      axis,
-      radius,
-      speed,
-      rotationSpeed,
-      scale,
-    });
-  }
-
-  const renderPassDescriptor = {
-    label: 'our basic canvas renderPass',
-    colorAttachments: [
-      {
-        // view: <- to be filled out when we render
-        clearValue: [0.3, 0.3, 0.3, 1],
-        loadOp: 'clear',
-        storeOp: 'store',
-      },
-    ],
-    depthStencilAttachment: {
-      // view: <- to be filled out when we render
-      depthClearValue: 1.0,
-      depthLoadOp: 'clear',
-      depthStoreOp: 'store',
-    },
-  };
-
-  const canvasToSizeMap = new WeakMap();
-  const degToRad = d => d * Math.PI / 180;
-
-  const settings = {
-    numObjects: 1000,
-    render: true,
-  };
-
-  const gui = new GUI();
-  gui.add(settings, 'numObjects', { min: 0, max: maxObjects, step: 1});
-  gui.add(settings, 'render');
-
-  let depthTexture;
-  let then = 0;
-  let frameCount = 0;
-
-  function render(time) {
-    time *= 0.001;  // convert to seconds
-    const deltaTime = time - then;
-    then = time;
-    ++frameCount;
-
-    const startTimeMs = performance.now();
-
-    const {width, height} = settings.render
-       ? canvasToSizeMap.get(canvas) ?? canvas
-       : { width: 1, height: 1 };
-
-    // Don't set the canvas size if it's already that size as it may be slow.
-    if (canvas.width !== width || canvas.height !== height) {
-      canvas.width = width;
-      canvas.height = height;
-    }
-
-    // Get the current texture from the canvas context and
-    // set it as the texture to render to.
-    const canvasTexture = context.getCurrentTexture();
-    renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView();
-
-    // If we don't have a depth texture OR if its size is different
-    // from the canvasTexture when make a new depth texture
-    if (!depthTexture ||
-        depthTexture.width !== canvasTexture.width ||
-        depthTexture.height !== canvasTexture.height) {
-      if (depthTexture) {
-        depthTexture.destroy();
-      }
-      depthTexture = device.createTexture({
-        size: [canvasTexture.width, canvasTexture.height],
-        format: 'depth24plus',
-        usage: GPUTextureUsage.RENDER_ATTACHMENT,
-      });
-    }
-    renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
-
-    const encoder = device.createCommandEncoder();
-    const pass = timingHelper.beginRenderPass(encoder, renderPassDescriptor);
-    pass.setPipeline(pipeline);
-    pass.setVertexBuffer(0, vertexBuffer);
-    pass.setIndexBuffer(indicesBuffer, 'uint16');
-
-    const aspect = canvas.clientWidth / canvas.clientHeight;
-    const projection = mat4.perspective(
-        degToRad(60),
-        aspect,
-        1,      // zNear
-        2000,   // zFar
-    );
-
-    const eye = [100, 150, 200];
-    const target = [0, 0, 0];
-    const up = [0, 1, 0];
-
-    // Compute a view matrix
-    const viewMatrix = mat4.lookAt(eye, target, up);
-
-    // Combine the view and projection matrixes
-    mat4.multiply(projection, viewMatrix, viewProjectionValue);
-
-    lightWorldPositionValue.set([-10, 30, 300]);
-    viewWorldPositionValue.set(eye);
-
-    device.queue.writeBuffer(globalUniformBuffer, 0, globalUniformValues);
-
-    let mathElapsedTimeMs = 0;
-
-    for (let i = 0; i < settings.numObjects; ++i) {
-      const {
-        bindGroupsUniformPairs,
-        uniformValues,
-        normalMatrixValue,
-        worldValue,
-
-        axis,
-        radius,
-        speed,
-        rotationSpeed,
-        scale,
-      } = objectInfos[i];
-      const mathTimeStartMs = performance.now();
-
-      // Compute a world matrix
-      mat4.identity(worldValue);
-      mat4.axisRotate(worldValue, axis, i + time * speed, worldValue);
-      mat4.translate(worldValue, [0, 0, Math.sin(i * 3.721 + time * speed) * radius], worldValue);
-      mat4.translate(worldValue, [0, 0, Math.sin(i * 9.721 + time * 0.1) * radius], worldValue);
-      mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
-      mat4.scale(worldValue, [scale, scale, scale], worldValue);
-
-      // Inverse and transpose it into the normalMatrix value
-      mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
-
-      mathElapsedTimeMs += performance.now() - mathTimeStartMs;
-
-      const {uniformBuffer} = bindGroupsUniformPairs[frameCount % 2];
-      const {bindGroup} = bindGroupsUniformPairs[(frameCount + 1) % 2];
-
-      // upload the uniform values to the uniform buffer
-      device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
-
-      pass.setBindGroup(0, bindGroup);
-      pass.drawIndexed(numVertices);
-    }
-
-    pass.end();
-
-    const commandBuffer = encoder.finish();
-    device.queue.submit([commandBuffer]);
-
-    timingHelper.getResult().then(gpuTime => {
-      gpuAverage.addSample(gpuTime / 1000);
-    });
-
-    const elapsedTimeMs = performance.now() - startTimeMs;
-    fpsAverage.addSample(1 / deltaTime);
-    jsAverage.addSample(elapsedTimeMs);
-    mathAverage.addSample(mathElapsedTimeMs);
-
-    infoElem.textContent = `\
-js  : ${jsAverage.get().toFixed(1)}ms
-math: ${mathAverage.get().toFixed(1)}ms
-fps : ${fpsAverage.get().toFixed(0)}
-gpu : ${canTimestamp ? `${(gpuAverage.get() / 1000).toFixed(1)}ms` : 'N/A'}
-`;
-
-    requestAnimationFrame(render);
-  }
-  requestAnimationFrame(render);
-
-  const observer = new ResizeObserver(entries => {
-    entries.forEach(entry => {
-      canvasToSizeMap.set(entry.target, {
-        width: Math.max(1, Math.min(entry.contentBoxSize[0].inlineSize, device.limits.maxTextureDimension2D)),
-        height: Math.max(1, Math.min(entry.contentBoxSize[0].blockSize, device.limits.maxTextureDimension2D)),
-      });
-    });
-  });
-  observer.observe(canvas);
-}
-
-function fail(msg) {
-  alert(msg);
-}
-
-main();
-  </script>
-</html>
diff --git a/webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers.html b/webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers.html
deleted file mode 100644
index 93416361..00000000
--- a/webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers.html
+++ /dev/null
@@ -1,615 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
-    <title>WebGPU Optimization - None</title>
-    <style>
-      @import url(resources/webgpu-lesson.css);
-html, body {
-  margin: 0;       /* remove the default margin          */
-  height: 100%;    /* make the html,body fill the page   */
-}
-canvas {
-  display: block;  /* make the canvas act like a block   */
-  width: 100%;     /* make the canvas fill its container */
-  height: 100%;
-}
-:root {
-  --bg-color: #fff;
-}
-@media (prefers-color-scheme: dark) {
-  :root {
-    --bg-color: #000;
-  }
-}
-canvas {
-  background-color: var(--bg-color);
-}
-#info {
-  position: absolute;
-  left: 0;
-  top: 0;
-  padding: 0.5em;
-  margin: 0;
-  background-color: rgba(0, 0, 0, 0.8);
-  color: white;
-  min-width: 8em;
-}
-    </style>
-  </head>
-  <body>
-    <canvas></canvas>
-    <pre id="info"></pre>
-  </body>
-  <script type="module">
-import GUI from '../3rdparty/muigui-0.x.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#webgpu-utils
-import {createTextureFromSource} from '../3rdparty/webgpu-utils-1.x.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-matrix-math.html
-import {mat4, mat3, vec3} from '../3rdparty/wgpu-matrix.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html
-import TimingHelper from './resources/js/timing-helper.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html
-import RollingAverage from './resources/js/rolling-average.js';
-
-const fpsAverage = new RollingAverage();
-const jsAverage = new RollingAverage();
-const gpuAverage = new RollingAverage();
-const mathAverage = new RollingAverage();
-
-/** Given a css color string, return an array of 4 values from 0 to 255 */
-const cssColorToRGBA8 = (() => {
-  const canvas = new OffscreenCanvas(1, 1);
-  const ctx = canvas.getContext('2d', {willReadFrequently: true});
-  return cssColor => {
-    ctx.clearRect(0, 0, 1, 1);
-    ctx.fillStyle = cssColor;
-    ctx.fillRect(0, 0, 1, 1);
-    return Array.from(ctx.getImageData(0, 0, 1, 1).data);
-  };
-})();
-
-/** Given a css color string, return an array of 4 values from 0 to 1 */
-const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-
-/**
- * Given hue, saturation, and luminance values in the range of 0 to 1
- * return the corresponding CSS hsl string
- */
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
-
-/**
- * Given hue, saturation, and luminance values in the range of 0 to 1
- * returns an array of 4 values from 0 to 1
- */
-const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
-
-/**
- * Returns a random number between min and max.
- * If min and max are not specified, returns 0 to 1
- * If max is not specified, return 0 to min.
- */
-function rand(min, max) {
-  if (min === undefined) {
-    max = 1;
-    min = 0;
-  } else if (max === undefined) {
-    max = min;
-    min = 0;
-  }
-  return Math.random() * (max - min) + min;
-}
-
-/** Selects a random array element */
-const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
-
-async function main() {
-  const adapter = await navigator.gpu?.requestAdapter();
-  const canTimestamp = adapter.features.has('timestamp-query');
-  const device = await adapter?.requestDevice({
-    requiredFeatures: [
-      ...(canTimestamp ? ['timestamp-query'] : []),
-     ],
-  });
-  if (!device) {
-    fail('could not init WebGPU');
-  }
-
-  const timingHelper = new TimingHelper(device);
-  const infoElem = document.querySelector('#info');
-
-  // Get a WebGPU context from the canvas and configure it
-  const canvas = document.querySelector('canvas');
-  const context = canvas.getContext('webgpu');
-  const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
-  context.configure({
-    device,
-    format: presentationFormat,
-    alphaMode: 'premultiplied',
-  });
-
-  const module = device.createShaderModule({
-    code: `
-      struct GlobalUniforms {
-        viewProjection: mat4x4f,
-        lightWorldPosition: vec3f,
-        viewWorldPosition: vec3f,
-      };
-
-      struct MaterialUniforms {
-        color: vec4f,
-        shininess: f32,
-      };
-
-      struct PerObjectUniforms {
-        normalMatrix: mat3x3f,
-        world: mat4x4f,
-      };
-
-      struct Vertex {
-        @location(0) position: vec4f,
-        @location(1) normal: vec3f,
-        @location(2) texcoord: vec2f,
-      };
-
-      struct VSOutput {
-        @builtin(position) position: vec4f,
-        @location(0) normal: vec3f,
-        @location(1) surfaceToLight: vec3f,
-        @location(2) surfaceToView: vec3f,
-        @location(3) texcoord: vec2f,
-      };
-
-      @group(0) @binding(0) var diffuseTexture: texture_2d<f32>;
-      @group(0) @binding(1) var diffuseSampler: sampler;
-      @group(0) @binding(2) var<uniform> obj: PerObjectUniforms;
-      @group(0) @binding(3) var<uniform> glb: GlobalUniforms;
-      @group(0) @binding(4) var<uniform> material: MaterialUniforms;
-
-      @vertex fn vs(vert: Vertex) -> VSOutput {
-        var vsOut: VSOutput;
-        vsOut.position = glb.viewProjection * obj.world * vert.position;
-
-        // Orient the normals and pass to the fragment shader
-        vsOut.normal = obj.normalMatrix * vert.normal;
-
-        // Compute the world position of the surface
-        let surfaceWorldPosition = (obj.world * vert.position).xyz;
-
-        // Compute the vector of the surface to the light
-        // and pass it to the fragment shader
-        vsOut.surfaceToLight = glb.lightWorldPosition - surfaceWorldPosition;
-
-        // Compute the vector of the surface to the light
-        // and pass it to the fragment shader
-        vsOut.surfaceToView = glb.viewWorldPosition - surfaceWorldPosition;
-
-        // Pass the texture coord on to the fragment shader
-        vsOut.texcoord = vert.texcoord;
-
-        return vsOut;
-      }
-
-      @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
-        // Because vsOut.normal is an inter-stage variable 
-        // it's interpolated so it will not be a unit vector.
-        // Normalizing it will make it a unit vector again
-        let normal = normalize(vsOut.normal);
-
-        let surfaceToLightDirection = normalize(vsOut.surfaceToLight);
-        let surfaceToViewDirection = normalize(vsOut.surfaceToView);
-        let halfVector = normalize(
-          surfaceToLightDirection + surfaceToViewDirection);
-
-        // Compute the light by taking the dot product
-        // of the normal with the direction to the light
-        let light = dot(normal, surfaceToLightDirection);
-
-        var specular = dot(normal, halfVector);
-        specular = select(
-            0.0,                           // value if condition is false
-            pow(specular, material.shininess),  // value if condition is true
-            specular > 0.0);               // condition
-
-        let diffuse = material.color * textureSample(diffuseTexture, diffuseSampler, vsOut.texcoord);
-        // Lets multiply just the color portion (not the alpha)
-        // by the light
-        let color = diffuse.rgb * light + specular;
-        return vec4f(color, diffuse.a);
-      }
-    `,
-  });
-
-  function createBufferWithData(device, data, usage) {
-    const buffer = device.createBuffer({
-      size: data.byteLength,
-      usage: usage,
-      mappedAtCreation: true,
-    });
-    const dst = new Uint8Array(buffer.getMappedRange());
-    dst.set(new Uint8Array(data.buffer));
-    buffer.unmap();
-    return buffer;
-  }
-
-  const vertexData = new Float32Array([
-  // position       normal        texcoord
-     1,  1, -1,     1,  0,  0,    1, 0,
-     1,  1,  1,     1,  0,  0,    0, 0,
-     1, -1,  1,     1,  0,  0,    0, 1,
-     1, -1, -1,     1,  0,  0,    1, 1,
-    -1,  1,  1,    -1,  0,  0,    1, 0,
-    -1,  1, -1,    -1,  0,  0,    0, 0,
-    -1, -1, -1,    -1,  0,  0,    0, 1,
-    -1, -1,  1,    -1,  0,  0,    1, 1,
-    -1,  1,  1,     0,  1,  0,    1, 0,
-     1,  1,  1,     0,  1,  0,    0, 0,
-     1,  1, -1,     0,  1,  0,    0, 1,
-    -1,  1, -1,     0,  1,  0,    1, 1,
-    -1, -1, -1,     0, -1,  0,    1, 0,
-     1, -1, -1,     0, -1,  0,    0, 0,
-     1, -1,  1,     0, -1,  0,    0, 1,
-    -1, -1,  1,     0, -1,  0,    1, 1,
-     1,  1,  1,     0,  0,  1,    1, 0,
-    -1,  1,  1,     0,  0,  1,    0, 0,
-    -1, -1,  1,     0,  0,  1,    0, 1,
-     1, -1,  1,     0,  0,  1,    1, 1,
-    -1,  1, -1,     0,  0, -1,    1, 0,
-     1,  1, -1,     0,  0, -1,    0, 0,
-     1, -1, -1,     0,  0, -1,    0, 1,
-    -1, -1, -1,     0,  0, -1,    1, 1,
-  ]);
-  const indices   = new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23]);
-
-  const vertexBuffer = createBufferWithData(device, vertexData, GPUBufferUsage.VERTEX);
-  const indicesBuffer = createBufferWithData(device, indices, GPUBufferUsage.INDEX);
-  const numVertices = indices.length;
-
-  const pipeline = device.createRenderPipeline({
-    label: 'textured model with point light w/specular highlight',
-    layout: 'auto',
-    vertex: {
-      module,
-      buffers: [
-        {
-          arrayStride: (3 + 3 + 2) * 4, // 8 floats
-          attributes: [
-            {shaderLocation: 0, offset: 0 * 4, format: 'float32x3'}, // position
-            {shaderLocation: 1, offset: 3 * 4, format: 'float32x3'}, // normal
-            {shaderLocation: 2, offset: 6 * 4, format: 'float32x2'}, // texcoord
-          ],
-        },
-      ],
-    },
-    fragment: {
-      module,
-      targets: [{ format: presentationFormat }],
-    },
-    primitive: {
-      cullMode: 'back',
-    },
-    depthStencil: {
-      depthWriteEnabled: true,
-      depthCompare: 'less',
-      format: 'depth24plus',
-    },
-  });
-
-  const textures = [
-    'πŸ˜‚', 'πŸ‘Ύ', 'πŸ‘', 'πŸ‘€', '🌞', 'πŸ›Ÿ',
-  ].map(s => {
-    const size = 128;
-    const ctx = new OffscreenCanvas(size, size).getContext('2d');
-    ctx.fillStyle = '#fff';
-    ctx.fillRect(0, 0, size, size);
-    ctx.font = `${size * 0.9}px sans-serif`;
-    ctx.textAlign = 'center';
-    ctx.textBaseline = 'middle';
-    ctx.fillText(s, size / 2, size / 2);
-    return createTextureFromSource(device, ctx.canvas, {mips: true});
-  });
-
-  const sampler = device.createSampler({
-    magFilter: 'linear',
-    minFilter: 'linear',
-    mipmapFilter: 'nearest',
-  });
-
-  const numMaterials = 20;
-  const materials = [];
-  for (let i = 0; i < numMaterials; ++i) {
-    const color = hslToRGBA(rand(), rand(0.5, 0.8), rand(0.5, 0.7));
-    const shininess = rand(10, 120);
-
-    const materialValues = new Float32Array([
-      ...color,
-      shininess,
-      0, 0, 0,  // padding
-    ]);
-    const materialUniformBuffer = createBufferWithData(
-      device,
-      materialValues,
-      GPUBufferUsage.UNIFORM,
-    );
-
-    materials.push({
-      materialUniformBuffer,
-      texture: randomArrayElement(textures),
-      sampler,
-    });
-  }
-
-  const globalUniformBufferSize = (16 + 4 + 4) * 4;
-  const globalUniformBuffer = device.createBuffer({
-    label: 'global uniforms',
-    size: globalUniformBufferSize,
-    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
-  });
-
-  const globalUniformValues = new Float32Array(globalUniformBufferSize / 4);
-
-  const kViewProjectionOffset = 0;
-  const kLightWorldPositionOffset = 16;
-  const kViewWorldPositionOffset = 20;
-
-  const viewProjectionValue = globalUniformValues.subarray(
-      kViewProjectionOffset, kViewProjectionOffset + 16);
-  const lightWorldPositionValue = globalUniformValues.subarray(
-      kLightWorldPositionOffset, kLightWorldPositionOffset + 3);
-  const viewWorldPositionValue = globalUniformValues.subarray(
-      kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
-
-  const maxObjects = 20000;
-  const objectInfos = [];
-
-  for (let i = 0; i < maxObjects; ++i) {
-    const material = randomArrayElement(materials);
-
-    const uniformBufferSize = (12 + 16 + 4 + 4) * 4;
-
-    const numBindGroupsUniformPairs = 2;
-    const bindGroupsUniformPairs = [];
-    for (let i = 0; i < numBindGroupsUniformPairs; ++i) {
-      const uniformBuffer = device.createBuffer({
-        label: 'uniforms',
-        size: uniformBufferSize,
-        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
-      });
-
-      const bindGroup = device.createBindGroup({
-        label: 'bind group for object',
-        layout: pipeline.getBindGroupLayout(0),
-        entries: [
-          { binding: 0, resource: material.texture.createView() },
-          { binding: 1, resource: material.sampler },
-          { binding: 2, resource: { buffer: uniformBuffer }},
-          { binding: 3, resource: { buffer: globalUniformBuffer }},
-          { binding: 4, resource: { buffer: material.materialUniformBuffer }},
-        ],
-      });
-
-      bindGroupsUniformPairs.push({
-        uniformBuffer,
-        bindGroup,
-      });
-    }
-
-    const uniformValues = new Float32Array(uniformBufferSize / 4);
-
-    // offsets to the various uniform values in float32 indices
-    const kNormalMatrixOffset = 0;
-    const kWorldOffset = 12;
-
-    const normalMatrixValue = uniformValues.subarray(
-        kNormalMatrixOffset, kNormalMatrixOffset + 12);
-    const worldValue = uniformValues.subarray(
-        kWorldOffset, kWorldOffset + 16);
-
-    const axis = vec3.normalize([rand(-1, 1), rand(-1, 1), rand(-1, 1)]);
-    const radius = rand(10, 100);
-    const speed = rand(0.1, 0.4);
-    const rotationSpeed = rand(-1, 1);
-    const scale = rand(2, 10);
-
-    objectInfos.push({
-      bindGroupsUniformPairs,
-
-      uniformValues,
-
-      normalMatrixValue,
-      worldValue,
-
-      axis,
-      radius,
-      speed,
-      rotationSpeed,
-      scale,
-    });
-  }
-
-  const renderPassDescriptor = {
-    label: 'our basic canvas renderPass',
-    colorAttachments: [
-      {
-        // view: <- to be filled out when we render
-        clearValue: [0.3, 0.3, 0.3, 1],
-        loadOp: 'clear',
-        storeOp: 'store',
-      },
-    ],
-    depthStencilAttachment: {
-      // view: <- to be filled out when we render
-      depthClearValue: 1.0,
-      depthLoadOp: 'clear',
-      depthStoreOp: 'store',
-    },
-  };
-
-  const canvasToSizeMap = new WeakMap();
-  const degToRad = d => d * Math.PI / 180;
-
-  const settings = {
-    numObjects: 1000,
-    render: true,
-  };
-
-  const gui = new GUI();
-  gui.add(settings, 'numObjects', { min: 0, max: maxObjects, step: 1});
-  gui.add(settings, 'render');
-
-  let depthTexture;
-  let then = 0;
-  let frameCount = 0;
-
-  function render(time) {
-    time *= 0.001;  // convert to seconds
-    const deltaTime = time - then;
-    then = time;
-    ++frameCount;
-
-    const startTimeMs = performance.now();
-
-    const {width, height} = settings.render
-       ? canvasToSizeMap.get(canvas) ?? canvas
-       : { width: 1, height: 1 };
-
-    // Don't set the canvas size if it's already that size as it may be slow.
-    if (canvas.width !== width || canvas.height !== height) {
-      canvas.width = width;
-      canvas.height = height;
-    }
-
-    // Get the current texture from the canvas context and
-    // set it as the texture to render to.
-    const canvasTexture = context.getCurrentTexture();
-    renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView();
-
-    // If we don't have a depth texture OR if its size is different
-    // from the canvasTexture when make a new depth texture
-    if (!depthTexture ||
-        depthTexture.width !== canvasTexture.width ||
-        depthTexture.height !== canvasTexture.height) {
-      if (depthTexture) {
-        depthTexture.destroy();
-      }
-      depthTexture = device.createTexture({
-        size: [canvasTexture.width, canvasTexture.height],
-        format: 'depth24plus',
-        usage: GPUTextureUsage.RENDER_ATTACHMENT,
-      });
-    }
-    renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
-
-    const encoder = device.createCommandEncoder();
-    const pass = timingHelper.beginRenderPass(encoder, renderPassDescriptor);
-    pass.setPipeline(pipeline);
-    pass.setVertexBuffer(0, vertexBuffer);
-    pass.setIndexBuffer(indicesBuffer, 'uint16');
-
-    const aspect = canvas.clientWidth / canvas.clientHeight;
-    const projection = mat4.perspective(
-        degToRad(60),
-        aspect,
-        1,      // zNear
-        2000,   // zFar
-    );
-
-    const eye = [100, 150, 200];
-    const target = [0, 0, 0];
-    const up = [0, 1, 0];
-
-    // Compute a view matrix
-    const viewMatrix = mat4.lookAt(eye, target, up);
-
-    // Combine the view and projection matrixes
-    mat4.multiply(projection, viewMatrix, viewProjectionValue);
-
-    lightWorldPositionValue.set([-10, 30, 300]);
-    viewWorldPositionValue.set(eye);
-
-    device.queue.writeBuffer(globalUniformBuffer, 0, globalUniformValues);
-
-    let mathElapsedTimeMs = 0;
-
-    for (let i = 0; i < settings.numObjects; ++i) {
-      const {
-        bindGroupsUniformPairs,
-        uniformValues,
-        normalMatrixValue,
-        worldValue,
-
-        axis,
-        radius,
-        speed,
-        rotationSpeed,
-        scale,
-      } = objectInfos[i];
-      const mathTimeStartMs = performance.now();
-
-      // Compute a world matrix
-      mat4.identity(worldValue);
-      mat4.axisRotate(worldValue, axis, i + time * speed, worldValue);
-      mat4.translate(worldValue, [0, 0, Math.sin(i * 3.721 + time * speed) * radius], worldValue);
-      mat4.translate(worldValue, [0, 0, Math.sin(i * 9.721 + time * 0.1) * radius], worldValue);
-      mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
-      mat4.scale(worldValue, [scale, scale, scale], worldValue);
-
-      // Inverse and transpose it into the normalMatrix value
-      mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
-
-      mathElapsedTimeMs += performance.now() - mathTimeStartMs;
-
-      const {uniformBuffer} = bindGroupsUniformPairs[frameCount % 2];
-      const {bindGroup} = bindGroupsUniformPairs[(frameCount + 1) % 2];
-
-      // upload the uniform values to the uniform buffer
-      device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
-
-      pass.setBindGroup(0, bindGroup);
-      pass.drawIndexed(numVertices);
-    }
-
-    pass.end();
-
-    const commandBuffer = encoder.finish();
-    device.queue.submit([commandBuffer]);
-
-    timingHelper.getResult().then(gpuTime => {
-      gpuAverage.addSample(gpuTime / 1000);
-    });
-
-    const elapsedTimeMs = performance.now() - startTimeMs;
-    fpsAverage.addSample(1 / deltaTime);
-    jsAverage.addSample(elapsedTimeMs);
-    mathAverage.addSample(mathElapsedTimeMs);
-
-    infoElem.textContent = `\
-js  : ${jsAverage.get().toFixed(1)}ms
-math: ${mathAverage.get().toFixed(1)}ms
-fps : ${fpsAverage.get().toFixed(0)}
-gpu : ${canTimestamp ? `${(gpuAverage.get() / 1000).toFixed(1)}ms` : 'N/A'}
-`;
-
-    requestAnimationFrame(render);
-  }
-  requestAnimationFrame(render);
-
-  const observer = new ResizeObserver(entries => {
-    entries.forEach(entry => {
-      canvasToSizeMap.set(entry.target, {
-        width: Math.max(1, Math.min(entry.contentBoxSize[0].inlineSize, device.limits.maxTextureDimension2D)),
-        height: Math.max(1, Math.min(entry.contentBoxSize[0].blockSize, device.limits.maxTextureDimension2D)),
-      });
-    });
-  });
-  observer.observe(canvas);
-}
-
-function fail(msg) {
-  alert(msg);
-}
-
-main();
-  </script>
-</html>
diff --git a/webgpu/webgpu-optimization-step5-use-buffer-offsets.html b/webgpu/webgpu-optimization-step5-use-buffer-offsets.html
index 18e5b0b4..9b3e325c 100644
--- a/webgpu/webgpu-optimization-step5-use-buffer-offsets.html
+++ b/webgpu/webgpu-optimization-step5-use-buffer-offsets.html
@@ -3,7 +3,7 @@
   <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
-    <title>WebGPU Optimization - None</title>
+    <title>WebGPU Optimization - Use One Large Uniform Buffer with Offsets</title>
     <style>
       @import url(resources/webgpu-lesson.css);
 html, body {
@@ -363,7 +363,7 @@
   const viewWorldPositionValue = globalUniformValues.subarray(
       kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   const uniformBufferSize = (12 + 16) * 4;
diff --git a/webgpu/webgpu-optimization-step6-use-mapped-buffers-2-command-buffers.html b/webgpu/webgpu-optimization-step6-use-mapped-buffers-2-command-buffers.html
deleted file mode 100644
index 3a182bf9..00000000
--- a/webgpu/webgpu-optimization-step6-use-mapped-buffers-2-command-buffers.html
+++ /dev/null
@@ -1,619 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
-    <title>WebGPU Optimization - None</title>
-    <style>
-      @import url(resources/webgpu-lesson.css);
-html, body {
-  margin: 0;       /* remove the default margin          */
-  height: 100%;    /* make the html,body fill the page   */
-}
-canvas {
-  display: block;  /* make the canvas act like a block   */
-  width: 100%;     /* make the canvas fill its container */
-  height: 100%;
-}
-:root {
-  --bg-color: #fff;
-}
-@media (prefers-color-scheme: dark) {
-  :root {
-    --bg-color: #000;
-  }
-}
-canvas {
-  background-color: var(--bg-color);
-}
-#info {
-  position: absolute;
-  left: 0;
-  top: 0;
-  padding: 0.5em;
-  margin: 0;
-  background-color: rgba(0, 0, 0, 0.8);
-  color: white;
-  min-width: 8em;
-}
-    </style>
-  </head>
-  <body>
-    <canvas></canvas>
-    <pre id="info"></pre>
-  </body>
-  <script type="module">
-import GUI from '../3rdparty/muigui-0.x.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#webgpu-utils
-import {createTextureFromSource} from '../3rdparty/webgpu-utils-1.x.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-matrix-math.html
-import {mat4, mat3, vec3} from '../3rdparty/wgpu-matrix.module.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html
-import TimingHelper from './resources/js/timing-helper.js';
-// see https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html
-import RollingAverage from './resources/js/rolling-average.js';
-
-const fpsAverage = new RollingAverage();
-const jsAverage = new RollingAverage();
-const gpuAverage = new RollingAverage();
-const mathAverage = new RollingAverage();
-
-/** Given a css color string, return an array of 4 values from 0 to 255 */
-const cssColorToRGBA8 = (() => {
-  const canvas = new OffscreenCanvas(1, 1);
-  const ctx = canvas.getContext('2d', {willReadFrequently: true});
-  return cssColor => {
-    ctx.clearRect(0, 0, 1, 1);
-    ctx.fillStyle = cssColor;
-    ctx.fillRect(0, 0, 1, 1);
-    return Array.from(ctx.getImageData(0, 0, 1, 1).data);
-  };
-})();
-
-/** Given a css color string, return an array of 4 values from 0 to 1 */
-const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-
-/**
- * Given hue, saturation, and luminance values in the range of 0 to 1
- * return the corresponding CSS hsl string
- */
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
-
-/**
- * Given hue, saturation, and luminance values in the range of 0 to 1
- * returns an array of 4 values from 0 to 1
- */
-const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
-
-/**
- * Returns a random number between min and max.
- * If min and max are not specified, returns 0 to 1
- * If max is not specified, return 0 to min.
- */
-function rand(min, max) {
-  if (min === undefined) {
-    max = 1;
-    min = 0;
-  } else if (max === undefined) {
-    max = min;
-    min = 0;
-  }
-  return Math.random() * (max - min) + min;
-}
-
-/** Selects a random array element */
-const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
-
-/** Rounds up v to a multiple of alignment */
-const roundUp = (v, alignment) => Math.ceil(v / alignment) * alignment;
-
-async function main() {
-  const adapter = await navigator.gpu?.requestAdapter();
-  const canTimestamp = adapter.features.has('timestamp-query');
-  const device = await adapter?.requestDevice({
-    requiredFeatures: [
-      ...(canTimestamp ? ['timestamp-query'] : []),
-     ],
-  });
-  if (!device) {
-    fail('could not init WebGPU');
-  }
-
-  const timingHelper = new TimingHelper(device);
-  const infoElem = document.querySelector('#info');
-
-  // Get a WebGPU context from the canvas and configure it
-  const canvas = document.querySelector('canvas');
-  const context = canvas.getContext('webgpu');
-  const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
-  context.configure({
-    device,
-    format: presentationFormat,
-    alphaMode: 'premultiplied',
-  });
-
-  const module = device.createShaderModule({
-    code: `
-      struct GlobalUniforms {
-        viewProjection: mat4x4f,
-        lightWorldPosition: vec3f,
-        viewWorldPosition: vec3f,
-      };
-
-      struct MaterialUniforms {
-        color: vec4f,
-        shininess: f32,
-      };
-
-      struct PerObjectUniforms {
-        normalMatrix: mat3x3f,
-        world: mat4x4f,
-      };
-
-      struct Vertex {
-        @location(0) position: vec4f,
-        @location(1) normal: vec3f,
-        @location(2) texcoord: vec2f,
-      };
-
-      struct VSOutput {
-        @builtin(position) position: vec4f,
-        @location(0) normal: vec3f,
-        @location(1) surfaceToLight: vec3f,
-        @location(2) surfaceToView: vec3f,
-        @location(3) texcoord: vec2f,
-      };
-
-      @group(0) @binding(0) var diffuseTexture: texture_2d<f32>;
-      @group(0) @binding(1) var diffuseSampler: sampler;
-      @group(0) @binding(2) var<uniform> obj: PerObjectUniforms;
-      @group(0) @binding(3) var<uniform> glb: GlobalUniforms;
-      @group(0) @binding(4) var<uniform> material: MaterialUniforms;
-
-      @vertex fn vs(vert: Vertex) -> VSOutput {
-        var vsOut: VSOutput;
-        vsOut.position = glb.viewProjection * obj.world * vert.position;
-
-        // Orient the normals and pass to the fragment shader
-        vsOut.normal = obj.normalMatrix * vert.normal;
-
-        // Compute the world position of the surface
-        let surfaceWorldPosition = (obj.world * vert.position).xyz;
-
-        // Compute the vector of the surface to the light
-        // and pass it to the fragment shader
-        vsOut.surfaceToLight = glb.lightWorldPosition - surfaceWorldPosition;
-
-        // Compute the vector of the surface to the light
-        // and pass it to the fragment shader
-        vsOut.surfaceToView = glb.viewWorldPosition - surfaceWorldPosition;
-
-        // Pass the texture coord on to the fragment shader
-        vsOut.texcoord = vert.texcoord;
-
-        return vsOut;
-      }
-
-      @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
-        // Because vsOut.normal is an inter-stage variable 
-        // it's interpolated so it will not be a unit vector.
-        // Normalizing it will make it a unit vector again
-        let normal = normalize(vsOut.normal);
-
-        let surfaceToLightDirection = normalize(vsOut.surfaceToLight);
-        let surfaceToViewDirection = normalize(vsOut.surfaceToView);
-        let halfVector = normalize(
-          surfaceToLightDirection + surfaceToViewDirection);
-
-        // Compute the light by taking the dot product
-        // of the normal with the direction to the light
-        let light = dot(normal, surfaceToLightDirection);
-
-        var specular = dot(normal, halfVector);
-        specular = select(
-            0.0,                           // value if condition is false
-            pow(specular, material.shininess),  // value if condition is true
-            specular > 0.0);               // condition
-
-        let diffuse = material.color * textureSample(diffuseTexture, diffuseSampler, vsOut.texcoord);
-        // Lets multiply just the color portion (not the alpha)
-        // by the light
-        let color = diffuse.rgb * light + specular;
-        return vec4f(color, diffuse.a);
-      }
-    `,
-  });
-
-  function createBufferWithData(device, data, usage) {
-    const buffer = device.createBuffer({
-      size: data.byteLength,
-      usage: usage,
-      mappedAtCreation: true,
-    });
-    const dst = new Uint8Array(buffer.getMappedRange());
-    dst.set(new Uint8Array(data.buffer));
-    buffer.unmap();
-    return buffer;
-  }
-
-  const vertexData = new Float32Array([
-  // position       normal        texcoord
-     1,  1, -1,     1,  0,  0,    1, 0,
-     1,  1,  1,     1,  0,  0,    0, 0,
-     1, -1,  1,     1,  0,  0,    0, 1,
-     1, -1, -1,     1,  0,  0,    1, 1,
-    -1,  1,  1,    -1,  0,  0,    1, 0,
-    -1,  1, -1,    -1,  0,  0,    0, 0,
-    -1, -1, -1,    -1,  0,  0,    0, 1,
-    -1, -1,  1,    -1,  0,  0,    1, 1,
-    -1,  1,  1,     0,  1,  0,    1, 0,
-     1,  1,  1,     0,  1,  0,    0, 0,
-     1,  1, -1,     0,  1,  0,    0, 1,
-    -1,  1, -1,     0,  1,  0,    1, 1,
-    -1, -1, -1,     0, -1,  0,    1, 0,
-     1, -1, -1,     0, -1,  0,    0, 0,
-     1, -1,  1,     0, -1,  0,    0, 1,
-    -1, -1,  1,     0, -1,  0,    1, 1,
-     1,  1,  1,     0,  0,  1,    1, 0,
-    -1,  1,  1,     0,  0,  1,    0, 0,
-    -1, -1,  1,     0,  0,  1,    0, 1,
-     1, -1,  1,     0,  0,  1,    1, 1,
-    -1,  1, -1,     0,  0, -1,    1, 0,
-     1,  1, -1,     0,  0, -1,    0, 0,
-     1, -1, -1,     0,  0, -1,    0, 1,
-    -1, -1, -1,     0,  0, -1,    1, 1,
-  ]);
-  const indices   = new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23]);
-
-  const vertexBuffer = createBufferWithData(device, vertexData, GPUBufferUsage.VERTEX);
-  const indicesBuffer = createBufferWithData(device, indices, GPUBufferUsage.INDEX);
-  const numVertices = indices.length;
-
-  const pipeline = device.createRenderPipeline({
-    label: 'textured model with point light w/specular highlight',
-    layout: 'auto',
-    vertex: {
-      module,
-      buffers: [
-        {
-          arrayStride: (3 + 3 + 2) * 4, // 8 floats
-          attributes: [
-            {shaderLocation: 0, offset: 0 * 4, format: 'float32x3'}, // position
-            {shaderLocation: 1, offset: 3 * 4, format: 'float32x3'}, // normal
-            {shaderLocation: 2, offset: 6 * 4, format: 'float32x2'}, // texcoord
-          ],
-        },
-      ],
-    },
-    fragment: {
-      module,
-      targets: [{ format: presentationFormat }],
-    },
-    primitive: {
-      cullMode: 'back',
-    },
-    depthStencil: {
-      depthWriteEnabled: true,
-      depthCompare: 'less',
-      format: 'depth24plus',
-    },
-  });
-
-  const textures = [
-    'πŸ˜‚', 'πŸ‘Ύ', 'πŸ‘', 'πŸ‘€', '🌞', 'πŸ›Ÿ',
-  ].map(s => {
-    const size = 128;
-    const ctx = new OffscreenCanvas(size, size).getContext('2d');
-    ctx.fillStyle = '#fff';
-    ctx.fillRect(0, 0, size, size);
-    ctx.font = `${size * 0.9}px sans-serif`;
-    ctx.textAlign = 'center';
-    ctx.textBaseline = 'middle';
-    ctx.fillText(s, size / 2, size / 2);
-    return createTextureFromSource(device, ctx.canvas, {mips: true});
-  });
-
-  const sampler = device.createSampler({
-    magFilter: 'linear',
-    minFilter: 'linear',
-    mipmapFilter: 'nearest',
-  });
-
-  const numMaterials = 20;
-  const materials = [];
-  for (let i = 0; i < numMaterials; ++i) {
-    const color = hslToRGBA(rand(), rand(0.5, 0.8), rand(0.5, 0.7));
-    const shininess = rand(10, 120);
-
-    const materialValues = new Float32Array([
-      ...color,
-      shininess,
-      0, 0, 0,  // padding
-    ]);
-    const materialUniformBuffer = createBufferWithData(
-      device,
-      materialValues,
-      GPUBufferUsage.UNIFORM,
-    );
-
-    materials.push({
-      materialUniformBuffer,
-      texture: randomArrayElement(textures),
-      sampler,
-    });
-  }
-
-  const globalUniformBufferSize = (16 + 4 + 4) * 4;
-  const globalUniformBuffer = device.createBuffer({
-    label: 'global uniforms',
-    size: globalUniformBufferSize,
-    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
-  });
-
-  const globalUniformValues = new Float32Array(globalUniformBufferSize / 4);
-
-  const kViewProjectionOffset = 0;
-  const kLightWorldPositionOffset = 16;
-  const kViewWorldPositionOffset = 20;
-
-  const viewProjectionValue = globalUniformValues.subarray(
-      kViewProjectionOffset, kViewProjectionOffset + 16);
-  const lightWorldPositionValue = globalUniformValues.subarray(
-      kLightWorldPositionOffset, kLightWorldPositionOffset + 3);
-  const viewWorldPositionValue = globalUniformValues.subarray(
-      kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
-
-  const maxObjects = 20000;
-  const objectInfos = [];
-
-  const uniformBufferSize = (12 + 16) * 4;
-  const uniformBufferSpace = roundUp(uniformBufferSize, device.limits.minUniformBufferOffsetAlignment);
-  const uniformBuffer = device.createBuffer({
-    label: 'uniforms',
-    size: uniformBufferSpace * maxObjects,
-    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
-  });
-
-  const mappedTransferBuffers = [];
-  const getMappedTransferBuffer = () => {
-    return mappedTransferBuffers.pop() || device.createBuffer({
-      label: 'transfer buffer',
-      size: uniformBufferSpace * maxObjects,
-      usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
-      mappedAtCreation: true,
-    });
-  };
-  // offsets to the various uniform values in float32 indices
-  const kNormalMatrixOffset = 0;
-  const kWorldOffset = 12;
-
-  for (let i = 0; i < maxObjects; ++i) {
-    const uniformBufferOffset = i * uniformBufferSpace;
-
-    const material = randomArrayElement(materials);
-
-    const bindGroup = device.createBindGroup({
-      label: 'bind group for object',
-      layout: pipeline.getBindGroupLayout(0),
-      entries: [
-        { binding: 0, resource: material.texture.createView() },
-        { binding: 1, resource: material.sampler },
-        { binding: 2, resource: { buffer: uniformBuffer, offset: uniformBufferOffset, size: uniformBufferSize }},
-        { binding: 3, resource: { buffer: globalUniformBuffer }},
-        { binding: 4, resource: { buffer: material.materialUniformBuffer }},
-      ],
-    });
-
-    const axis = vec3.normalize([rand(-1, 1), rand(-1, 1), rand(-1, 1)]);
-    const radius = rand(10, 100);
-    const speed = rand(0.1, 0.4);
-    const rotationSpeed = rand(-1, 1);
-    const scale = rand(2, 10);
-
-    objectInfos.push({
-      bindGroup,
-
-      axis,
-      radius,
-      speed,
-      rotationSpeed,
-      scale,
-    });
-  }
-
-  const renderPassDescriptor = {
-    label: 'our basic canvas renderPass',
-    colorAttachments: [
-      {
-        // view: <- to be filled out when we render
-        clearValue: [0.3, 0.3, 0.3, 1],
-        loadOp: 'clear',
-        storeOp: 'store',
-      },
-    ],
-    depthStencilAttachment: {
-      // view: <- to be filled out when we render
-      depthClearValue: 1.0,
-      depthLoadOp: 'clear',
-      depthStoreOp: 'store',
-    },
-  };
-
-  const canvasToSizeMap = new WeakMap();
-  const degToRad = d => d * Math.PI / 180;
-
-  const settings = {
-    numObjects: 1000,
-    render: true,
-  };
-
-  const gui = new GUI();
-  gui.add(settings, 'numObjects', { min: 0, max: maxObjects, step: 1});
-  gui.add(settings, 'render');
-
-  let depthTexture;
-  let then = 0;
-
-  function render(time) {
-    time *= 0.001;  // convert to seconds
-    const deltaTime = time - then;
-    then = time;
-
-    const startTimeMs = performance.now();
-
-    const {width, height} = settings.render
-       ? canvasToSizeMap.get(canvas) ?? canvas
-       : { width: 1, height: 1 };
-
-    // Don't set the canvas size if it's already that size as it may be slow.
-    if (canvas.width !== width || canvas.height !== height) {
-      canvas.width = width;
-      canvas.height = height;
-    }
-
-    // Get the current texture from the canvas context and
-    // set it as the texture to render to.
-    const canvasTexture = context.getCurrentTexture();
-    renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView();
-
-    // If we don't have a depth texture OR if its size is different
-    // from the canvasTexture when make a new depth texture
-    if (!depthTexture ||
-        depthTexture.width !== canvasTexture.width ||
-        depthTexture.height !== canvasTexture.height) {
-      if (depthTexture) {
-        depthTexture.destroy();
-      }
-      depthTexture = device.createTexture({
-        size: [canvasTexture.width, canvasTexture.height],
-        format: 'depth24plus',
-        usage: GPUTextureUsage.RENDER_ATTACHMENT,
-      });
-    }
-    renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
-
-    const encoder = device.createCommandEncoder();
-    const pass = timingHelper.beginRenderPass(encoder, renderPassDescriptor);
-    pass.setPipeline(pipeline);
-    pass.setVertexBuffer(0, vertexBuffer);
-    pass.setIndexBuffer(indicesBuffer, 'uint16');
-
-    let mathElapsedTimeMs = 0;
-
-    const transferBuffer = getMappedTransferBuffer();
-    const uniformValues = new Float32Array(transferBuffer.getMappedRange());
-
-    for (let i = 0; i < settings.numObjects; ++i) {
-      const {
-        bindGroup,
-        axis,
-        radius,
-        speed,
-        rotationSpeed,
-        scale,
-      } = objectInfos[i];
-      const mathTimeStartMs = performance.now();
-
-      const uniformBufferOffset = i * uniformBufferSpace;
-      const f32Offset = uniformBufferOffset / 4;
-      const normalMatrixValue = uniformValues.subarray(
-          f32Offset + kNormalMatrixOffset, f32Offset + kNormalMatrixOffset + 12);
-      const worldValue = uniformValues.subarray(
-          f32Offset + kWorldOffset, f32Offset + kWorldOffset + 16);
-
-      // Compute a world matrix
-      mat4.identity(worldValue);
-      mat4.axisRotate(worldValue, axis, i + time * speed, worldValue);
-      mat4.translate(worldValue, [0, 0, Math.sin(i * 3.721 + time * speed) * radius], worldValue);
-      mat4.translate(worldValue, [0, 0, Math.sin(i * 9.721 + time * 0.1) * radius], worldValue);
-      mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
-      mat4.scale(worldValue, [scale, scale, scale], worldValue);
-
-      // Inverse and transpose it into the normalMatrix value
-      mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
-
-      mathElapsedTimeMs += performance.now() - mathTimeStartMs;
-
-      pass.setBindGroup(0, bindGroup);
-      pass.drawIndexed(numVertices);
-    }
-    transferBuffer.unmap();
-
-    // copy the uniform values from the transfer buffer to the uniform buffer
-    const encoder2 = device.createCommandEncoder();
-    if (settings.numObjects) {
-      const size = (settings.numObjects - 1) * uniformBufferSpace + uniformBufferSize;
-      encoder2.copyBufferToBuffer(transferBuffer, 0, uniformBuffer, 0, size);
-    }
-    const cb2 = encoder2.finish();
-
-    const aspect = canvas.clientWidth / canvas.clientHeight;
-    const projection = mat4.perspective(
-        degToRad(60),
-        aspect,
-        1,      // zNear
-        2000,   // zFar
-    );
-
-    const eye = [100, 150, 200];
-    const target = [0, 0, 0];
-    const up = [0, 1, 0];
-
-    // Compute a view matrix
-    const viewMatrix = mat4.lookAt(eye, target, up);
-
-    // Combine the view and projection matrixes
-    mat4.multiply(projection, viewMatrix, viewProjectionValue);
-
-    lightWorldPositionValue.set([-10, 30, 300]);
-    viewWorldPositionValue.set(eye);
-
-    device.queue.writeBuffer(globalUniformBuffer, 0, globalUniformValues);
-
-    pass.end();
-
-    const commandBuffer = encoder.finish();
-    device.queue.submit([cb2, commandBuffer]);
-
-    transferBuffer.mapAsync(GPUMapMode.WRITE).then(() => {
-      mappedTransferBuffers.push(transferBuffer);
-    });
-
-    timingHelper.getResult().then(gpuTime => {
-      gpuAverage.addSample(gpuTime / 1000);
-    });
-
-    const elapsedTimeMs = performance.now() - startTimeMs;
-    fpsAverage.addSample(1 / deltaTime);
-    jsAverage.addSample(elapsedTimeMs);
-    mathAverage.addSample(mathElapsedTimeMs);
-
-    infoElem.textContent = `\
-js  : ${jsAverage.get().toFixed(1)}ms
-math: ${mathAverage.get().toFixed(1)}ms
-fps : ${fpsAverage.get().toFixed(0)}
-gpu : ${canTimestamp ? `${(gpuAverage.get() / 1000).toFixed(1)}ms` : 'N/A'}
-`;
-
-    requestAnimationFrame(render);
-  }
-  requestAnimationFrame(render);
-
-  const observer = new ResizeObserver(entries => {
-    entries.forEach(entry => {
-      canvasToSizeMap.set(entry.target, {
-        width: Math.max(1, Math.min(entry.contentBoxSize[0].inlineSize, device.limits.maxTextureDimension2D)),
-        height: Math.max(1, Math.min(entry.contentBoxSize[0].blockSize, device.limits.maxTextureDimension2D)),
-      });
-    });
-  });
-  observer.observe(canvas);
-}
-
-function fail(msg) {
-  alert(msg);
-}
-
-main();
-  </script>
-</html>
diff --git a/webgpu/webgpu-optimization-step6-use-mapped-buffers-dyanmic-offsets.html b/webgpu/webgpu-optimization-step6-use-mapped-buffers-dyanmic-offsets.html
index c5076da0..3173092a 100644
--- a/webgpu/webgpu-optimization-step6-use-mapped-buffers-dyanmic-offsets.html
+++ b/webgpu/webgpu-optimization-step6-use-mapped-buffers-dyanmic-offsets.html
@@ -403,7 +403,7 @@
   const viewWorldPositionValue = globalUniformValues.subarray(
       kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   const uniformBufferSize = (12 + 16) * 4;
diff --git a/webgpu/webgpu-optimization-step6-use-mapped-buffers-math-w-offsets.html b/webgpu/webgpu-optimization-step6-use-mapped-buffers-math-w-offsets.html
index fecd4017..8f900b02 100644
--- a/webgpu/webgpu-optimization-step6-use-mapped-buffers-math-w-offsets.html
+++ b/webgpu/webgpu-optimization-step6-use-mapped-buffers-math-w-offsets.html
@@ -920,7 +920,7 @@
   const viewWorldPositionValue = globalUniformValues.subarray(
       kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   const uniformBufferSize = (12 + 16) * 4;
diff --git a/webgpu/webgpu-optimization-step6-use-mapped-buffers.html b/webgpu/webgpu-optimization-step6-use-mapped-buffers.html
index 63307848..42595485 100644
--- a/webgpu/webgpu-optimization-step6-use-mapped-buffers.html
+++ b/webgpu/webgpu-optimization-step6-use-mapped-buffers.html
@@ -3,7 +3,7 @@
   <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
-    <title>WebGPU Optimization - None</title>
+    <title>WebGPU Optimization - Use Mapped Buffers</title>
     <style>
       @import url(resources/webgpu-lesson.css);
 html, body {
@@ -363,7 +363,7 @@
   const viewWorldPositionValue = globalUniformValues.subarray(
       kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   const uniformBufferSize = (12 + 16) * 4;
diff --git a/webgpu/webgpu-optimization-step7-double-buffer-2-submit.html b/webgpu/webgpu-optimization-step7-double-buffer-2-submit.html
index beef2180..b0666c9e 100644
--- a/webgpu/webgpu-optimization-step7-double-buffer-2-submit.html
+++ b/webgpu/webgpu-optimization-step7-double-buffer-2-submit.html
@@ -363,7 +363,7 @@
   const viewWorldPositionValue = globalUniformValues.subarray(
       kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   const uniformBufferSize = (12 + 16) * 4;
diff --git a/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set.html b/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set.html
index 08d41df2..38bb4564 100644
--- a/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set.html
+++ b/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set.html
@@ -363,7 +363,7 @@
   const viewWorldPositionValue = globalUniformValues.subarray(
       kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   const uniformBufferSize = (12 + 16) * 4;
diff --git a/webgpu/webgpu-optimization-step7-double-buffer.html b/webgpu/webgpu-optimization-step7-double-buffer.html
index 867e3d47..c40dbb55 100644
--- a/webgpu/webgpu-optimization-step7-double-buffer.html
+++ b/webgpu/webgpu-optimization-step7-double-buffer.html
@@ -363,7 +363,7 @@
   const viewWorldPositionValue = globalUniformValues.subarray(
       kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
 
-  const maxObjects = 20000;
+  const maxObjects = 30000;
   const objectInfos = [];
 
   const uniformBufferSize = (12 + 16) * 4;