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