diff --git a/webgpu/lessons/webgpu-from-webgl.md b/webgpu/lessons/webgpu-from-webgl.md
index f2727fa8..55340f55 100644
--- a/webgpu/lessons/webgpu-from-webgl.md
+++ b/webgpu/lessons/webgpu-from-webgl.md
@@ -1413,7 +1413,7 @@ This forces you to implement a portable point solution.
If you take a WebGL app and directly convert it to WebGPU you might find
it runs slower. To get the benefits of WebGPU you'll need to change the
-way you organize and optimize how you draw.
+way you organize data and optimize how you draw.
See [this article on WebGPU optimization](webgpu-optimization.html) for
ideas.
diff --git a/webgpu/lessons/webgpu-lighting-point.md b/webgpu/lessons/webgpu-lighting-point.md
index 2684dfc0..3d2fe004 100644
--- a/webgpu/lessons/webgpu-lighting-point.md
+++ b/webgpu/lessons/webgpu-lighting-point.md
@@ -211,6 +211,8 @@ And here it is
{{{example url="../webgpu-lighting-point.html" }}}
+# Specular Highlighting
+
Now that we have a point we can add something called specular highlighting.
If you look at on object in the real world, if it's remotely shiny, then if it happens
diff --git a/webgpu/lessons/webgpu-optimization.md b/webgpu/lessons/webgpu-optimization.md
index 773eab43..86304695 100644
--- a/webgpu/lessons/webgpu-optimization.md
+++ b/webgpu/lessons/webgpu-optimization.md
@@ -11,13 +11,12 @@ optimizations.
In this article will cover some of the most basic optimizations and
discuss a few others.
-The basics: The less work you do, and the less work you ask WebGPU to do
-the faster things will go.
+The basics: **The less work you do, and the less work you ask WebGPU to do
+the faster things will go.**
In pretty much all of the examples to date, if we draw multiple shapes
we've done the following steps
-```
* At Init time:
* for each thing we want to draw
* create a uniform buffer
@@ -29,20 +28,870 @@ we've done the following steps
* copy the typed array to the uniform buffer for this object
* bind the bindGroup for this object
* draw
+
+Let's make an example we can optimize that follows the steps above so
+we can then optimize it.
+
+Note, this a fake example.
+We are only going to draw a bunch of cubes and as such we could
+certainly optimize things by using *instancing* which we covered
+in the articles on [storage buffers](webgpu-storage-buffers.html#a-instancing)
+and [vertex buffers](webgpu-vertex-buffers.html#a-instancing).
+I didn't want to clutter the code by handling tons of different kinds of
+objects. Instancing is certainly a great way to optimize if your
+project uses lots of the same model. Plants, trees, rocks, trash, etc
+are often optimized by using instancing. Other models, it's arguably
+less common. For example a table might have 4, 6 or 8 chairs around
+it and it would probably be faster to use instancing to draw those
+chairs, except in a list of 500+ things to draw, if the chairs are the
+only exceptions, then it's probably not worth the effort to figure out
+some optimal data organization that some how organizes the chairs
+to use instancing but finds no other situations to use instancing.
+
+The point of the paragraph above is, use instancing when it's
+appropriate.
+
+In any case, here's our code. We've got the initialization code
+we've been using in general.
+
+```js
+async function main() {
+ const adapter = await navigator.gpu?.requestAdapter();
+ const device = await adapter?.requestDevice();
+ if (!device) {
+ fail('need a browser that supports WebGPU');
+ return;
+ }
+
+ // 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,
+ });
+```
+
+Then let's make a shader module.
+
+```js
+ const module = device.createShaderModule({
+ code: `
+ struct Uniforms {
+ normalMatrix: mat3x3f,
+ viewProjection: mat4x4f,
+ world: mat4x4f,
+ color: vec4f,
+ lightWorldPosition: vec3f,
+ viewWorldPosition: vec3f,
+ 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;
+ @group(0) @binding(1) var diffuseSampler: sampler;
+ @group(0) @binding(2) var uni: Uniforms;
+
+ @vertex fn vs(vert: Vertex) -> VSOutput {
+ var vsOut: VSOutput;
+ vsOut.position = uni.viewProjection * uni.world * vert.position;
+
+ // Orient the normals and pass to the fragment shader
+ vsOut.normal = uni.normalMatrix * vert.normal;
+
+ // Compute the world position of the surface
+ let surfaceWorldPosition = (uni.world * vert.position).xyz;
+
+ // Compute the vector of the surface to the light
+ // and pass it to the fragment shader
+ vsOut.surfaceToLight = uni.lightWorldPosition - surfaceWorldPosition;
+
+ // Compute the vector of the surface to the light
+ // and pass it to the fragment shader
+ vsOut.surfaceToView = uni.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, uni.shininess), // value if condition is true
+ specular > 0.0); // condition
+
+ let diffuse = uni.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);
+ }
+ `,
+ });
+```
+
+This shader module is uses lighting similar to
+[the point light with specular highlights cover else where](webgpu-lighting-point.html#a-specular).
+It uses a texture because most 3d models use textures so I thought it best to include one.
+It multiplies the texture by a color so we can adjust the colors of each cube.
+And it has all of the uniform we need to do the lighting and project the cube in 3d.
+
+We need data for a cube and to put that data in buffers.
+
+```js
+ function createBufferWithData(device, data, usage) {
+ const buffer = device.createBuffer({
+ size: data.byteLength,
+ usage: usage | GPUBufferUsage.COPY_DST,
+ });
+ device.queue.writeBuffer(buffer, 0, data);
+ return buffer;
+ }
+
+ const positions = 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]);
+ const normals = 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]);
+ const texcoords = 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]);
+ 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 positionBuffer = createBufferWithData(device, positions, GPUBufferUsage.VERTEX);
+ const normalBuffer = createBufferWithData(device, normals, GPUBufferUsage.VERTEX);
+ const texcoordBuffer = createBufferWithData(device, texcoords, GPUBufferUsage.VERTEX);
+ const indicesBuffer = createBufferWithData(device, indices, GPUBufferUsage.INDEX);
+ const numVertices = indices.length;
+```
+
+We need a render pipeline
+
+```js
+ const pipeline = device.createRenderPipeline({
+ label: 'textured model with point light w/specular highlight',
+ layout: 'auto',
+ vertex: {
+ module,
+ buffers: [
+ // position
+ {
+ arrayStride: 3 * 4, // 3 floats
+ attributes: [
+ {shaderLocation: 0, offset: 0, format: 'float32x3'},
+ ],
+ },
+ // normal
+ {
+ arrayStride: 3 * 4, // 3 floats
+ attributes: [
+ {shaderLocation: 1, offset: 0, format: 'float32x3'},
+ ],
+ },
+ // uvs
+ {
+ arrayStride: 2 * 4, // 2 floats
+ attributes: [
+ {shaderLocation: 2, offset: 0, format: 'float32x2'},
+ ],
+ },
+ ],
+ },
+ fragment: {
+ module,
+ targets: [{ format: presentationFormat }],
+ },
+ primitive: {
+ cullMode: 'back',
+ },
+ depthStencil: {
+ depthWriteEnabled: true,
+ depthCompare: 'less',
+ format: 'depth24plus',
+ },
+ });
+```
+
+The pipeline above uses 1 buffer per attribute. One for position data,
+one for normal data, and one for texture coordinates (UVs). It culls
+back facing triangles, and it expects a depth texture to depth testing.
+All things we've covered in other articles.
+
+Let's insert a few utilities for making colors and random numbers.
+
+```js
+/** 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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];
+```
+
+Hopefully they are all pretty straight forward.
+
+Now let's make a texture and a sampler. The texture will
+just be a 2x2 texel texture with 4 shades of gray.
+
+```js
+ const texture = device.createTexture({
+ size: [2, 2],
+ format: 'rgba8unorm',
+ usage:
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.COPY_DST,
+ });
+ device.queue.writeTexture(
+ { texture },
+ new Uint8Array([
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
+ ]),
+ { bytesPerRow: 8, rowsPerImage: 2 },
+ { width: 2, height: 2 },
+ );
+
+ const sampler = device.createSampler({
+ magFilter: 'nearest',
+ minFilter: 'nearest',
+ });
+```
+
+Now let's setup a set of material info. We haven't done this anywhere else
+but it's a common setup. Unity, Unreal, Blender, Three.js, Babylon,js all
+have a concept of a *material*. Generally, a material holds things like
+the color of the material, how shiny it is, as well as which texture to
+use, etc...
+
+We'll make 20 "materials" and then pick one at random for each cube.
+
+```js
+ 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,
+ sampler,
+ });
+ }
+```
+
+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 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 an animate each object.
+
+```js
+ const maxObjects = 20000;
+ const objectInfos = [];
+
+ for (let i = 0; i < maxObjects; ++i) {
+ const uniformBufferSize = (12 + 16 + 16 + 4 + 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 kViewProjectionOffset = 12;
+ const kWorldOffset = 28;
+ const kColorOffset = 44;
+ const kLightWorldPositionOffset = 48;
+ const kViewWorldPositionOffset = 52;
+ const kShininessOffset = 55;
+
+ const normalMatrixValue = uniformValues.subarray(
+ kNormalMatrixOffset, kNormalMatrixOffset + 12);
+ const viewProjectionValue = uniformValues.subarray(
+ kViewProjectionOffset, kViewProjectionOffset + 16);
+ const worldValue = uniformValues.subarray(
+ kWorldOffset, kWorldOffset + 16);
+ const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4);
+ const lightWorldPositionValue = uniformValues.subarray(
+ kLightWorldPositionOffset, kLightWorldPositionOffset + 3);
+ const viewWorldPositionValue = uniformValues.subarray(
+ kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
+ 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 }},
+ ],
+ });
+
+ 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,
+
+ uniformBuffer,
+ uniformValues,
+
+ normalMatrixValue,
+ worldValue,
+ viewProjectionValue,
+ colorValue,
+ lightWorldPositionValue,
+ viewWorldPositionValue,
+ shininessValue,
+
+ axis,
+ material,
+ radius,
+ speed,
+ rotationSpeed,
+ scale,
+ });
+ }
+```
+
+We pre-create a render pass descriptor which we'll update to begin a render pass at render time.
+
+```js
+ 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',
+ },
+ };
+```
+
+We need a simple UI so we can adjust how many things we're drawing.
+
+
+```js
+ const settings = {
+ numObjects: 1000,
+ render: true,
+ };
+
+ const gui = new GUI();
+ gui.add(settings, 'numObjects', { min: 0, max: maxObjects, step: 1});
+ gui.add(settings, 'render');
+```
+
+Now we can write our render loop.
+
+```js
+ let depthTexture;
+ let then = 0;
+
+ function render(time) {
+ time *= 0.001; // convert to seconds
+ const deltaTime = time - then;
+ then = time;
+
+
+ requestAnimationFrame(render);
+ }
+ requestAnimationFrame(render);
+```
+
+Inside the render loop we'll update our render pass descriptor. we'll also
+create a depth texture if one doesn't exist or if the one
+we have has a different size then our canvas texture. We did this in
+[the article on 3d](webgpu-orthographic-projection.html#a-depth-textures).
+
+```js
+ // 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();
+```
+
+We'll start a command buffer and a render pass and set our vertex and index buffers.
+
+```js
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.setPipeline(pipeline);
+ pass.setVertexBuffer(0, positionBuffer);
+ pass.setVertexBuffer(1, normalBuffer);
+ pass.setVertexBuffer(2, texcoordBuffer);
+ pass.setIndexBuffer(indicesBuffer, 'uint16');
+```
+
+when we'll compute a viewProjection matrix like we covered in
+[the article on perspective projection](webgpu-perspective-projection.html).
+
+```js
++ const degToRad = d => d * Math.PI / 180;
+
+ function render(time) {
+ ...
+
++ 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
++ const viewProjectionMatrix = mat4.multiply(projection, viewMatrix);
+```
+
+Now we can loop over all the objects and draw them, for each one we need
+to update all of is uniform values, copy the uniform values to its uniform buffer,
+bind the bind group for this object, and draw.
+
+```js
+ for (let i = 0; i < settings.numObjects; ++i) {
+ const {
+ bindGroup,
+ uniformBuffer,
+ uniformValues,
+ normalMatrixValue,
+ worldValue,
+ viewProjectionValue,
+ colorValue,
+ lightWorldPositionValue,
+ viewWorldPositionValue,
+ shininessValue,
+
+ axis,
+ material,
+ radius,
+ speed,
+ rotationSpeed,
+ scale,
+ } = objectInfos[i];
+
+ // Copy the viewProjectionMatrix into the uniform values for this object
+ viewProjectionValue.set(viewProjectionMatrix);
+
+ // 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;
+
+ // copy the materials values.
+ colorValue.set(color);
+ lightWorldPositionValue.set([-10, 30, 300]);
+ viewWorldPositionValue.set(eye);
+ shininessValue[0] = shininess;
+
+ // upload the uniform values to the uniform buffer
+ device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
+
+ pass.setBindGroup(0, bindGroup);
+ pass.drawIndexed(numVertices);
+ }
+```
+
+Then we can end the pass and submit it.
+
+```js
++ pass.end();
++
++ const commandBuffer = encoder.finish();
++ device.queue.submit([commandBuffer]);
+
+ requestAnimationFrame(render);
+ }
+ requestAnimationFrame(render);
+```
+
+A few more things left to do. Let's add in resizing
+
+```js
++ const canvasToSizeMap = new WeakMap();
+
+ function render(time) {
+ time *= 0.001; // convert to seconds
+ const deltaTime = time - then;
+ then = time;
+
++ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
++ // 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();
+
+ ...
+
+ 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);
+```
+
+Let's also add in some timing. We'll use the `RollingAverage` and `TimingHelper` classes
+we made in [the article on timing](webgpu-timing.html).
+
+```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();
+```
+
+Then we'll time our JavaScript from the beginning to the end of our rendering code
+
+```js
+ function render(time) {
+ ...
+
++ const startTimeMs = performance.now();
+
+ ...
+
++ const elapsedTimeMs = performance.now() - startTimeMs;
++ jsAverage.addSample(elapsedTimeMs);
+
+ requestAnimationFrame(render);
+ }
+ requestAnimationFrame(render);
+```
+
+We'll time the part of the JavaScript that does the 3D math
+
+```js
+ function render(time) {
+ ...
+
++ let mathElapsedTimeMs = 0;
+
+ for (let i = 0; i < settings.numObjects; ++i) {
+ const {
+ bindGroup,
+ uniformBuffer,
+ uniformValues,
+ normalMatrixValue,
+ worldValue,
+ viewProjectionValue,
+ colorValue,
+ lightWorldPositionValue,
+ viewWorldPositionValue,
+ shininessValue,
+
+ axis,
+ material,
+ radius,
+ speed,
+ rotationSpeed,
+ scale,
+ } = objectInfos[i];
++ const mathTimeStartMs = performance.now();
+
+ // Copy the viewProjectionMatrix into the uniform values for this object
+ viewProjectionValue.set(viewProjectionMatrix);
+
+ // 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);
+ lightWorldPositionValue.set([-10, 30, 300]);
+ viewWorldPositionValue.set(eye);
+ 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);
+ }
+
+ ...
+
+ const elapsedTimeMs = performance.now() - startTimeMs;
+ jsAverage.addSample(elapsedTimeMs);
++ mathAverage.addSample(mathElapsedTimeMs);
+
+
+ requestAnimationFrame(render);
+ }
+ requestAnimationFrame(render);
+```
+
+We'll time the time between `requestAnimationFrame` callbacks.
+
+```js
+ let depthTexture;
+ let then = 0;
+
+ function render(time) {
+ time *= 0.001; // convert to seconds
+ const deltaTime = time - then;
+ then = time;
+
+ ...
+
+ const elapsedTimeMs = performance.now() - startTimeMs;
++ fpsAverage.addSample(1 / deltaTime);
+ jsAverage.addSample(elapsedTimeMs);
+ mathAverage.addSample(mathElapsedTimeMs);
+
+
+ requestAnimationFrame(render);
+ }
+ requestAnimationFrame(render);
+```
+
+And we'll time our render pass
+
+```js
+async function main() {
+ const adapter = await navigator.gpu?.requestAdapter();
+- const device = await adapter?.requestDevice();
++ 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);
+
+ ...
+
+ function render(time) {
+ ...
+
+- const pass = encoder.beginRenderPass(renderPassEncoder);
++ const pass = timingHelper.beginRenderPass(encoder, renderPassDescriptor);
+
+ ...
+
+ pass.end();
+
+ const commandBuffer = encoder.finish();
+ device.queue.submit([commandBuffer]);
+
++ timingHelper.getResult().then(gpuTime => {
++ gpuAverage.addSample(gpuTime / 1000);
++ });
+
+ ...
+
+ requestAnimationFrame(render);
+ }
+ requestAnimationFrame(render);
```
+And finally we need to show the timing
+```js
+async function main() {
+ ...
+
+ const timingHelper = new TimingHelper(device);
++ const infoElem = document.querySelector('#info');
-Let's make an example we can optimize
+ ...
-## Use mappedOnCreation for initial data
+ function render(time) {
+ ...
+
+ 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);
+```
+
+And with that, we have our first "un-optimized" example. It's following the
+steps listed near the top of the article, and it works.
+
+{{{example url="../webgpu-optimization-none.html"}}}
+
+Increase the number of objects and see when the framerate drops for you.
+For me, on my 75hz monitor on an M1 Mac I got ~9000 cubes before the
+framerate dropped.
+
+# Optimization: Mapped On Creation
In the example above, and in most of the examples on this site we've
used `writeBuffer` to copy data into a vertex or index buffer. As a very
minor optimization, for this particular case, when you create a buffer
you can pass in `mappedAtCreation: true`. This has 2 benefits.
-1. It's slightly faster to put the data into the new buffer (2)
+1. It's slightly faster to put the data into the new buffer
2. You don't have to add `GPUBufferUsage.COPY_DST` to the buffer's usage.
@@ -67,15 +916,32 @@ you can pass in `mappedAtCreation: true`. This has 2 benefits.
Note that this optimization only helps at creation time so it will not
affect our performance at render time.
-## Pack and interleave your vertices
+# Optimization: Pack and interleave your vertices
+
+In the example above we have 3 attributes, one for position, one for normals,
+and one for texture coordinates. It's common to have 4 to 6 attributes where
+we'd have [tangents for normal mapping](webgpu-normal-mapping.html) and, if
+we had [a skinned model](webgpu-skinning.html), we'd add in weights and joints.
+
+In the example above each attribute is using it's own buffer.
+This is slower both on the CPU and GPU. It's slower on the CPU in JavaScript
+because we need to call `setVertexBuffer` once for each
+buffer for each model we want to draw.
-In the example above we have 3 buffers, one for position, one for normals,
-and one for texture coordinates. This is slower both on the CPU and GPU.
-One the CPU in JavaScript we need to call `setVertexBuffer` once for each
-buffer for each model we want to draw. On the GPU there are cache issues.
-So, if we interleave the vertex data into a single buffer we'll only need
-one call to `setVertexBuffer` and we'll help the GPU as well as all the
-data needed for a single vertex will be located together in memory.
+Imagine instead of just a cube we had 100s or models. Each time we switched
+which model to draw we'd have to call `setVertexBuffer` up to 6 times.
+100 * 6 calls per model = 600 calls.
+
+Following the rule "less work = go faster", if we merged the data for the
+attributes into a single buffer then we'd only need one call to `setVertexBuffer`
+pre model. 100 calls. That's like 600% faster!
+
+On the GPU, loading things that are together in memory is usually faster
+than loading from different places in memory so on top of just putting
+the vertex data for a single model into a single buffer, it's better
+to interleave the data.
+
+Let's make that change.
```js
- const positions = 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]);
@@ -175,9 +1041,17 @@ data needed for a single vertex will be located together in memory.
+ pass.setVertexBuffer(0, vertexBuffer);
```
-* Split uniform buffers (shared, material, per model)
+Above we put the data for all 3 attributes into a single buffer and then
+changed our render pass so it expects the data interleaved into a single
+buffer.
+
+Note: if you're loading gLTF files it's arguably good to either
+pre-process them so their vertex data is interleaved into a single buffer (best)
+or else interleave the data at load them (ok).
-Our example right now has one uniform buffer object.
+# Optimization: Split uniform buffers (shared, material, per model)
+
+Our example right now has one uniform buffer per object.
```wgsl
struct Uniforms {
@@ -194,11 +1068,11 @@ struct Uniforms {
Some of those uniform values like `viewProjection`, `lightWorldPosition`
and `viewWorldPosition` can be shared.
-We can split these into at least 2 uniform buffers. One for the shared
+We can split these in the shader to use 2 uniform buffers. One for the shared
values and one for *per object values*.
```wgsl
-struct SharedUniforms {
+struct GlobalUniforms {
viewProjection: mat4x4f,
lightWorldPosition: vec3f,
viewWorldPosition: vec3f,
@@ -211,41 +1085,744 @@ struct PerObjectUniforms {
};
```
-With this change, we'll save having to copy the `viewProjection`, `lightWorldPosition` and `viewWorldPosition` to every uniform buffer.
-We'll also copy less data with `device.queue.writeBuffer`
+With this change, we'll save having to copy the
+`viewProjection`, `lightWorldPosition` and `viewWorldPosition`
+to every uniform buffer. We'll also copy less data per object
+with `device.queue.writeBuffer`
+
+Here's the new shader
+
+```js
+ const module = device.createShaderModule({
+ code: `
+- struct Uniforms {
+- normalMatrix: mat3x3f,
+- viewProjection: mat4x4f,
+- world: mat4x4f,
+- color: vec4f,
+- lightWorldPosition: vec3f,
+- viewWorldPosition: vec3f,
+- shininess: f32,
+- };
+
++ 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;
+ @group(0) @binding(1) var diffuseSampler: sampler;
+- @group(0) @binding(2) var uni: Uniforms;
++ @group(0) @binding(2) var obj: PerObjectUniforms;
++ @group(0) @binding(3) var glb: GlobalUniforms;
+
+ @vertex fn vs(vert: Vertex) -> VSOutput {
+ var vsOut: VSOutput;
+- vsOut.position = uni.viewProjection * uni.world * vert.position;
++ vsOut.position = glb.viewProjection * obj.world * vert.position;
+
+ // Orient the normals and pass to the fragment shader
+- vsOut.normal = uni.normalMatrix * vert.normal;
++ vsOut.normal = obj.normalMatrix * vert.normal;
+
+ // Compute the world position of the surface
+- let surfaceWorldPosition = (uni.world * vert.position).xyz;
++ 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 = uni.lightWorldPosition - surfaceWorldPosition;
++ vsOut.surfaceToLight = glb.lightWorldPosition - surfaceWorldPosition;
+
+ // Compute the vector of the surface to the light
+ // and pass it to the fragment shader
+- vsOut.surfaceToView = uni.viewWorldPosition - surfaceWorldPosition;
++ 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, uni.shininess), // value if condition is true
++ pow(specular, obj.shininess), // value if condition is true
+ specular > 0.0); // condition
+
+- let diffuse = uni.color * textureSample(diffuseTexture, diffuseSampler, vsOut.texcoord);
++ 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);
+ }
+ `,
+ });
+```
+
+We need to create one global uniform buffer for the global uniforms.
+
+```js
+ 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);
+```
+
+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 objectInfos = [];
+
+ for (let i = 0; i < maxObjects; ++i) {
+- const uniformBufferSize = (12 + 16 + 16 + 4 + 4 + 4) * 4;
++ 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 kViewProjectionOffset = 12;
+- const kWorldOffset = 28;
+- const kColorOffset = 44;
+- const kLightWorldPositionOffset = 48;
+- const kViewWorldPositionOffset = 52;
+- const kShininessOffset = 55;
++ const kWorldOffset = 12;
++ const kColorOffset = 28;
++ const kShininessOffset = 32;
+
+ const normalMatrixValue = uniformValues.subarray(
+ kNormalMatrixOffset, kNormalMatrixOffset + 12);
+- const viewProjectionValue = uniformValues.subarray(
+- kViewProjectionOffset, kViewProjectionOffset + 16);
+ const worldValue = uniformValues.subarray(
+ kWorldOffset, kWorldOffset + 16);
+ const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4);
+- const lightWorldPositionValue = uniformValues.subarray(
+- kLightWorldPositionOffset, kLightWorldPositionOffset + 3);
+- const viewWorldPositionValue = uniformValues.subarray(
+- kViewWorldPositionOffset, kViewWorldPositionOffset + 3);
+ 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: globalUniformBuffer }},
+ ],
+ });
+
+ 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,
+
+ uniformBuffer,
+ uniformValues,
+
+ normalMatrixValue,
+ worldValue,
+- viewProjectionValue,
+ colorValue,
+- lightWorldPositionValue,
+- viewWorldPositionValue,
+ shininessValue,
+ material,
+
+ axis,
+ radius,
+ speed,
+ rotationSpeed,
+ scale,
+ });
+ }
+```
+
+Then, at render time, we update the global uniform buffer just once,
+outside the loop of rendering our objects.
+
+```js
+ 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
+- const viewProjectionMatrix = mat4.multiply(projection, viewMatrix);
++ 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 {
+ bindGroup,
+ uniformBuffer,
+ uniformValues,
+ normalMatrixValue,
+ worldValue,
+- viewProjectionValue,
+ colorValue,
+- lightWorldPositionValue,
+- viewWorldPositionValue,
+ shininessValue,
+
+ axis,
+ material,
+ radius,
+ speed,
+ rotationSpeed,
+ scale,
+ } = objectInfos[i];
+ const mathTimeStartMs = performance.now();
+
+- // Copy the viewProjectionMatrix into the uniform values for this object
+- viewProjectionValue.set(viewProjectionMatrix);
+
+ // 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);
+- lightWorldPositionValue.set([-10, 30, 300]);
+- viewWorldPositionValue.set(eye);
+ shininessValue[0] = shininess;
-With that change our math portion dropped ~30%
+ 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();
+```
+
+That didn't change the number of calls into WebGPU, in fact it added 1. But, it
+reduced a bunch of the work we were doing per model.
+
+{{{example url="../webgpu-optimization-step3-global-vs-per-object-uniforms.html"}}}
+
+On my machine, with that change, our math portion dropped ~16%
+
+# Optimization: Separate more uniforms
A common organization in a 3D library is to have "models" (the vertex data),
-"materials" (the colors, shininess, and texture), "lights" (which lights to use),
+"materials" (the colors, shininess, and textures), "lights" (which lights to use),
"viewInfo" (the view and projection matrix). In particular, in our example,
`color` and `shininess` never change so it's a waste to keep copying them
to the uniform buffer every frame.
-## Double buffer uniform buffers that are updated every frame
+Let's make a uniform buffer per material. Well copy the material settings
+into them at init time and then just add them to our bind group.
+
+First let's change the shaders to use another uniform buffer.
-WebGPU is required to make accessing a buffer to be safe. That means
-when submit a command buffer, WebGPU has to effectively check, "is this buffer
-being updated? If so wait until the update is finished". Or, going the other way,
-let's say you call `device.queue.writeBuffer`. WebGPU has to check "is this buffer currently being read by shaders? If so wait until that finishes".
+```js
+ const module = device.createShaderModule({
+ code: `
+ struct GlobalUniforms {
+ viewProjection: mat4x4f,
+ lightWorldPosition: vec3f,
+ viewWorldPosition: vec3f,
+ };
-Double buffering in this case means, instead of one uniform buffer for
-the "per object uniforms", the ones we're updating with thee world and
-normal matrices, we'd have two. We'd ping-pong which one we're updating.
-This why, while WebGPU is drawing using one of those 2 buffers, we'r updating
-the other. So, WebGPU never has to wait.
++ struct MaterialUniforms {
++ color: vec4f,
++ shininess: f32,
++ };
-{{{example url="../webgpu-optimization-none.html"}}}
+ 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;
+ @group(0) @binding(1) var diffuseSampler: sampler;
+ @group(0) @binding(2) var obj: PerObjectUniforms;
+ @group(0) @binding(3) var glb: GlobalUniforms;
++ @group(0) @binding(4) var 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, obj.shininess), // value if condition is true
++ pow(specular, material.shininess), // value if condition is true
+ specular > 0.0); // condition
+
+- let diffuse = obj.color * textureSample(diffuseTexture, diffuseSampler, vsOut.texcoord);
++ 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);
+ }
+ `,
+ });
+```
+
+Then we'll make a uniform buffer for each material.
+
+```js
+ 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({
+- color,
+- shininess,
++ materialUniformBuffer,
+ texture,
+ sampler,
+ });
+ }
+```
+
+When we setup the per object info we no longer need to pass
+on the material settings. Instead we just need to add the
+material's uniform buffer to the object's bind group.
+
+```js
+ const maxObjects = 20000;
+ const objectInfos = [];
+
+ for (let i = 0; i < maxObjects; ++i) {
+- const uniformBufferSize = (12 + 16 + 4 + 4) * 4;
++ const uniformBufferSize = (12 + 16) * 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: 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,
+
+ uniformBuffer,
+ uniformValues,
+
+ normalMatrixValue,
+ worldValue,
+- colorValue,
+- shininessValue,
+
+ axis,
+- material,
+ radius,
+ speed,
+ rotationSpeed,
+ scale,
+ });
+ }
+```
+
+We also no longer need to deal with this stuff at render time.
+
+```js
+ 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);
+ }
+```
+
+# Optimization: Use One large Uniform Buffer with buffer offsets
+
+Right now, each object has it's own uniform buffer. At render time,
+for each object, we update a typed array with the uniform values for
+that object and then call `device.queue.writeBuffer` to update that
+single uniform buffer's values. If we're rendering 8400 objects
+that's 8400 calls to `device.queue.writeBuffer`.
+
+Instead, we could make one larger uniform buffer. We can then setup
+the bind group for each object to use it's own portion of the larger
+buffer. At render time, we can update all the values for all of
+the objects in one large typed array and make just one call to
+`device.queue.writeBuffer` which should be faster.
+
+First let's allocate a large uniform buffer and large typed array.
+Uniform buffer offsets have a minimum alignment which defaults to
+256 bytes so we'll round up the size we need per object to 256 bytes.
+
+```js
++ 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 uniformValues = new Float32Array(uniformBuffer.size / 4);
+```
+
+Now we can change the per object views to view into that large
+typedarray. We can also set the bind group to use the correct
+portion of the large uniform buffer.
+
+```js
+ for (let i = 0; i < maxObjects; ++i) {
++ const uniformBufferOffset = i * uniformBufferSpace;
++ const f32Offset = uniformBufferOffset / 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 normalMatrixValue = uniformValues.subarray(
++ f32Offset + kNormalMatrixOffset, f32Offset + kNormalMatrixOffset + 12);
++ const worldValue = uniformValues.subarray(
++ f32Offset + kWorldOffset, f32Offset + kWorldOffset + 16);
+
+ 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: 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,
+
+- uniformBuffer,
+- uniformValues,
-{{{example url="../webgl-optimization-none-uniform-buffers.html"}}}
+ normalMatrixValue,
+ worldValue,
+
+ axis,
+ radius,
+ speed,
+ rotationSpeed,
+ scale,
+ });
+ }
+```
+
+At render time we update all the objects values and then make
+just one call to `device.queue.writeBuffer`.
+
+```js
+ for (let i = 0; i < settings.numObjects; ++i) {
+ const {
+ bindGroup,
+- uniformBuffer,
+- 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;
+- // upload the uniform values to the uniform buffer
+- device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
+
+ pass.setBindGroup(0, bindGroup);
+ pass.drawIndexed(numVertices);
+ }
+
++ // upload all uniform values to the uniform buffer
++ if (settings.numObjects) {
++ const size = (settings.numObjects - 1) * uniformBufferSpace + uniformBufferSize;
++ device.queue.writeBuffer( uniformBuffer, 0, uniformValues, 0, size / uniformValues.BYTES_PER_ELEMENT);
++ }
+
+ pass.end();
+```
+
+{{{example url="../webgpu-optimization-step5-use-buffer-offsets.html"}}}
+
+On my machine that shaved off 40% of the JavaScript time!
+
+
+
+
+
+* Use dynamic offsets
* Texture Atlas or 2D-array
* GPU Occlusion culling
* GPU Scene graph matrix calculation
* GPU Frustum culling
* Indirect Drawing
* Render Bundles
+
+
diff --git a/webgpu/webgl-optimization-none-uniform-buffers-4kplus.html b/webgpu/webgl-optimization-none-uniform-buffers-4kplus.html
index 0b5e3e60..a7903e28 100644
--- a/webgpu/webgl-optimization-none-uniform-buffers-4kplus.html
+++ b/webgpu/webgl-optimization-none-uniform-buffers-4kplus.html
@@ -122,6 +122,7 @@
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});
@@ -133,13 +134,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -151,7 +170,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -406,7 +425,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrix);
const {color, shininess} = material;
diff --git a/webgpu/webgl-optimization-none-uniform-buffers-8k.html b/webgpu/webgl-optimization-none-uniform-buffers-8k.html
index 103a9406..0649eaa7 100644
--- a/webgpu/webgl-optimization-none-uniform-buffers-8k.html
+++ b/webgpu/webgl-optimization-none-uniform-buffers-8k.html
@@ -122,6 +122,7 @@
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});
@@ -133,13 +134,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -151,7 +170,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -406,7 +425,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrix);
const {color, shininess} = material;
diff --git a/webgpu/webgl-optimization-none-uniform-buffers.html b/webgpu/webgl-optimization-none-uniform-buffers.html
index d4830096..981efba1 100644
--- a/webgpu/webgl-optimization-none-uniform-buffers.html
+++ b/webgpu/webgl-optimization-none-uniform-buffers.html
@@ -122,6 +122,7 @@
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});
@@ -133,13 +134,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -151,7 +170,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -405,7 +424,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrix);
const {color, shininess} = material;
diff --git a/webgpu/webgl-optimization-none.html b/webgpu/webgl-optimization-none.html
index 7fbac3c2..8638020a 100644
--- a/webgpu/webgl-optimization-none.html
+++ b/webgpu/webgl-optimization-none.html
@@ -122,6 +122,7 @@
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});
@@ -133,13 +134,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -151,7 +170,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -405,7 +424,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), uniforms.normalMatrix);
const {color, shininess} = material;
diff --git a/webgpu/webgpu-optimization-all.html b/webgpu/webgpu-optimization-all.html
index da489a19..ea29f48e 100644
--- a/webgpu/webgpu-optimization-all.html
+++ b/webgpu/webgpu-optimization-all.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,7 +103,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -284,10 +303,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -441,15 +460,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -534,7 +546,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
const {color, shininess} = material;
@@ -576,7 +588,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-none-4kplus.html b/webgpu/webgpu-optimization-none-4kplus.html
index 195cbc66..bc656c81 100644
--- a/webgpu/webgpu-optimization-none-4kplus.html
+++ b/webgpu/webgpu-optimization-none-4kplus.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,7 +103,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -270,10 +289,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -414,15 +433,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -510,7 +522,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
const {color, shininess} = material;
@@ -555,7 +567,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-none.html b/webgpu/webgpu-optimization-none.html
index 6651de08..2989ac35 100644
--- a/webgpu/webgpu-optimization-none.html
+++ b/webgpu/webgpu-optimization-none.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,7 +103,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -269,10 +288,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -413,15 +432,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -509,7 +521,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
const {color, shininess} = material;
@@ -554,7 +566,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
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 873c5ab6..2c7d919e 100644
--- a/webgpu/webgpu-optimization-step3-global-vs-per-object-uniforms.html
+++ b/webgpu/webgpu-optimization-step3-global-vs-per-object-uniforms.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,7 +103,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -284,10 +303,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -371,13 +390,11 @@
],
});
- 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,
@@ -389,15 +406,13 @@
worldValue,
colorValue,
shininessValue,
- material,
axis,
- color,
+ material,
radius,
speed,
rotationSpeed,
scale,
- shininess,
});
}
@@ -441,15 +456,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -534,7 +542,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
const {color, shininess} = material;
@@ -576,7 +584,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-step4-material-uniforms.html b/webgpu/webgpu-optimization-step4-material-uniforms.html
index ccb86479..27c1b894 100644
--- a/webgpu/webgpu-optimization-step4-material-uniforms.html
+++ b/webgpu/webgpu-optimization-step4-material-uniforms.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,7 +103,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -289,10 +308,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -446,15 +465,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -536,7 +548,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
mathElapsedTimeMs += performance.now() - mathTimeStartMs;
@@ -574,7 +586,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-step5-double-buffer-frequenly-updated-uniform-buffers-pre-submit.html b/webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers-pre-submit.html
similarity index 92%
rename from webgpu/webgpu-optimization-step5-double-buffer-frequenly-updated-uniform-buffers-pre-submit.html
rename to webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers-pre-submit.html
index 553e96de..5e299854 100644
--- a/webgpu/webgpu-optimization-step5-double-buffer-frequenly-updated-uniform-buffers-pre-submit.html
+++ b/webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers-pre-submit.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,7 +103,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -289,10 +308,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -458,15 +477,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -547,7 +559,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
mathElapsedTimeMs += performance.now() - mathTimeStartMs;
@@ -588,7 +600,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-step5-double-buffer-frequenly-updated-uniform-buffers.html b/webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers.html
similarity index 92%
rename from webgpu/webgpu-optimization-step5-double-buffer-frequenly-updated-uniform-buffers.html
rename to webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers.html
index 553e96de..5e299854 100644
--- a/webgpu/webgpu-optimization-step5-double-buffer-frequenly-updated-uniform-buffers.html
+++ b/webgpu/webgpu-optimization-step5-double-buffer-frequently-updated-uniform-buffers.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,7 +103,7 @@
return Math.random() * (max - min) + min;
}
-// Selects a random array element
+/** Selects a random array element */
const randomArrayElement = arr => arr[Math.random() * arr.length | 0];
async function main() {
@@ -289,10 +308,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -458,15 +477,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -547,7 +559,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
mathElapsedTimeMs += performance.now() - mathTimeStartMs;
@@ -588,7 +600,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-step5-use-buffer-offsets.html b/webgpu/webgpu-optimization-step5-use-buffer-offsets.html
index c5fcc6df..3efac7ac 100644
--- a/webgpu/webgpu-optimization-step5-use-buffer-offsets.html
+++ b/webgpu/webgpu-optimization-step5-use-buffer-offsets.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,12 +103,12 @@
return Math.random() * (max - min) + min;
}
-// Rounds up v to a multiple of alignment
-const roundUp = (v, alignment) => Math.ceil(v / alignment) * alignment;
-
-// Selects a random array element
+/** 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');
@@ -292,10 +311,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -449,15 +468,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -537,7 +549,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
mathElapsedTimeMs += performance.now() - mathTimeStartMs;
@@ -578,7 +590,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-step6-use-mapped-buffers.html b/webgpu/webgpu-optimization-step6-use-mapped-buffers.html
index 390188ce..8b533a92 100644
--- a/webgpu/webgpu-optimization-step6-use-mapped-buffers.html
+++ b/webgpu/webgpu-optimization-step6-use-mapped-buffers.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,12 +103,12 @@
return Math.random() * (max - min) + min;
}
-// Rounds up v to a multiple of alignment
-const roundUp = (v, alignment) => Math.ceil(v / alignment) * alignment;
-
-// Selects a random array element
+/** 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');
@@ -292,10 +311,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -448,15 +467,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -515,7 +527,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
mathElapsedTimeMs += performance.now() - mathTimeStartMs;
@@ -592,7 +604,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-step7-double-buffer-2-submit.html b/webgpu/webgpu-optimization-step7-double-buffer-2-submit.html
index 8d833fda..8c64cedb 100644
--- a/webgpu/webgpu-optimization-step7-double-buffer-2-submit.html
+++ b/webgpu/webgpu-optimization-step7-double-buffer-2-submit.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,12 +103,12 @@
return Math.random() * (max - min) + min;
}
-// Rounds up v to a multiple of alignment
-const roundUp = (v, alignment) => Math.ceil(v / alignment) * alignment;
-
-// Selects a random array element
+/** 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');
@@ -292,10 +311,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -450,15 +469,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -515,7 +527,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
mathElapsedTimeMs += performance.now() - mathTimeStartMs;
@@ -598,7 +610,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set-count-100.html b/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set-count-100.html
index 0a57f2b9..cd6d367d 100644
--- a/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set-count-100.html
+++ b/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set-count-100.html
@@ -84,7 +84,7 @@
return Math.random() * (max - min) + min;
}
-// Rounds up v to a multiple of alignment
+/** Rounds up v to a multiple of alignment */
const roundUp = (v, alignment) => Math.ceil(v / alignment) * alignment;
// Selects a random array element
@@ -292,10 +292,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -453,15 +453,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -516,7 +509,7 @@
mat4.rotateX(worldTemp, time * rotationSpeed + i, worldTemp);
mat4.scale(worldTemp, [scale, scale, scale], worldTemp);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldTemp)), normalMatrixTemp);
uniformValues.set(worldTemp, f32Offset + kWorldOffset);
@@ -599,7 +592,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set.html b/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set.html
index 6b76b3ec..bbade6c2 100644
--- a/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set.html
+++ b/webgpu/webgpu-optimization-step7-double-buffer-typedarray-set.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,12 +103,12 @@
return Math.random() * (max - min) + min;
}
-// Rounds up v to a multiple of alignment
-const roundUp = (v, alignment) => Math.ceil(v / alignment) * alignment;
-
-// Selects a random array element
+/** 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');
@@ -292,10 +311,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -453,15 +472,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -516,7 +528,7 @@
mat4.rotateX(worldTemp, time * rotationSpeed + i, worldTemp);
mat4.scale(worldTemp, [scale, scale, scale], worldTemp);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldTemp)), normalMatrixTemp);
uniformValues.set(worldTemp, f32Offset + kWorldOffset);
@@ -599,7 +611,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}
diff --git a/webgpu/webgpu-optimization-step7-double-buffer.html b/webgpu/webgpu-optimization-step7-double-buffer.html
index cf3a2908..90cbee2e 100644
--- a/webgpu/webgpu-optimization-step7-double-buffer.html
+++ b/webgpu/webgpu-optimization-step7-double-buffer.html
@@ -55,6 +55,7 @@
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});
@@ -66,13 +67,31 @@
};
})();
-const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
+/** Given a css color string, return an array of 4 values from 0 to 1 */
const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255);
-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.
+/**
+ * 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 values from 0 to 1
+ */
+const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l));
+/**
+ * Given hue, saturation, and luminance values in the range of 0 to 1
+ * returns an array of values from 0 to 255
+ */
+const hslToRGBA8 = (h, s, l) => cssColorToRGBA8(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;
@@ -84,12 +103,12 @@
return Math.random() * (max - min) + min;
}
-// Rounds up v to a multiple of alignment
-const roundUp = (v, alignment) => Math.ceil(v / alignment) * alignment;
-
-// Selects a random array element
+/** 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');
@@ -292,10 +311,10 @@
device.queue.writeTexture(
{ texture },
new Uint8Array([
- 255, 255, 255, 255,
- 128, 128, 128, 255,
- 192, 192, 192, 255,
- 64, 64, 64, 255,
+ ...hslToRGBA8(0, 0, 1),
+ ...hslToRGBA8(0, 0, 0.5),
+ ...hslToRGBA8(0, 0, 0.75),
+ ...hslToRGBA8(0, 0, 0.25),
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
@@ -450,15 +469,8 @@
const startTimeMs = performance.now();
- let width = 1;
- let height = 1;
- if (settings.render) {
- const entry = canvasToSizeMap.get(canvas);
- if (entry) {
- 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));
- }
- }
+ const {width, height} = canvasToSizeMap.get(canvas) ?? canvas;
+ // 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;
@@ -517,7 +529,7 @@
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
mat4.scale(worldValue, [scale, scale, scale], worldValue);
- // Inverse and transpose it into the worldInverseTranspose value
+ // Inverse and transpose it into the normalMatrix value
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);
mathElapsedTimeMs += performance.now() - mathTimeStartMs;
@@ -597,7 +609,12 @@
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
- entries.forEach(e => canvasToSizeMap.set(e.target, e));
+ 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);
}