Skip to content

Instantly share code, notes, and snippets.

@greggman
Last active June 20, 2024 05:44
Show Gist options
  • Save greggman/772c2e2122ceac9ba2390ed0c767b4bb to your computer and use it in GitHub Desktop.
Save greggman/772c2e2122ceac9ba2390ed0c767b4bb to your computer and use it in GitHub Desktop.
WebGL: uniform buffer chrome/safari perf issue
html, body, canvas {
margin: 0;
width: 100%;
height: 100%;
display: block;
}
pre {
position: absolute;
left: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 0.5em;
}
<canvas></canvas>
<pre></pre>
import GUI from 'https://webgpufundamentals.org/3rdparty/muigui-0.x.module.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;
}
}
/**
* 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;
}
const euclideanModulo = (x, a) => x - a * Math.floor(x / a);
/** Rounds up v to a multiple of alignment */
const roundUp = (v, alignment) => Math.ceil(v / alignment) * alignment;
function createShader(gl, type, src) {
const shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(shader));
}
logIfNotEmpty(gl.getShaderInfoLog(shader));
return shader;
}
function createProgram(gl, vs, fs, tf) {
const program = gl.createProgram();
gl.attachShader(program, createShader(gl, gl.VERTEX_SHADER, vs));
gl.attachShader(program, createShader(gl, gl.FRAGMENT_SHADER, fs));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program));
}
logIfNotEmpty(gl.getProgramInfoLog(program));
return program;
}
function logIfNotEmpty(v) {
if (v !== '') {
console.log(v);
}
}
function createCircleVertices({
radius = 1,
numSubdivisions = 24,
innerRadius = 0,
startAngle = 0,
endAngle = Math.PI * 2,
} = {}) {
// 2 triangles per subdivision, 3 verts per tri, 2 values (xy) each.
const numVertices = numSubdivisions * 3 * 2;
const vertexData = new Float32Array(numSubdivisions * 2 * 3 * 2);
let offset = 0;
const addVertex = (x, y) => {
vertexData[offset++] = x;
vertexData[offset++] = y;
};
// 2 triangles per subdivision
//
// 0--1 4
// | / /|
// |/ / |
// 2 3--5
for (let i = 0; i < numSubdivisions; ++i) {
const angle1 = startAngle + (i + 0) * (endAngle - startAngle) / numSubdivisions;
const angle2 = startAngle + (i + 1) * (endAngle - startAngle) / numSubdivisions;
const c1 = Math.cos(angle1);
const s1 = Math.sin(angle1);
const c2 = Math.cos(angle2);
const s2 = Math.sin(angle2);
// first triangle
addVertex(c1 * radius, s1 * radius);
addVertex(c2 * radius, s2 * radius);
addVertex(c1 * innerRadius, s1 * innerRadius);
// second triangle
addVertex(c1 * innerRadius, s1 * innerRadius);
addVertex(c2 * radius, s2 * radius);
addVertex(c2 * innerRadius, s2 * innerRadius);
}
return {
vertexData,
numVertices,
};
}
async function main() {
const infoElem = document.querySelector('pre');
// 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 Uniforms {
mat3 normalMatrix;
mat4 viewProjection;
mat4 matrix;
vec3 lightWorldPosition;
vec3 viewWorldPosition;
vec4 color;
float shininess;
};
`;
const vs = `#version 300 es
${uniformBlock}
layout(location = 0) in vec4 position;
void main() {
gl_Position = matrix * position;
}
`;
const fs = `#version 300 es
precision highp float;
${uniformBlock}
uniform sampler2D tex;
out vec4 fragColor;
void main() {
fragColor = color * texture(tex, vec2(0));
}
`;
const prg = createProgram(gl, vs, fs);
const blockNdx = gl.getUniformBlockIndex(prg, 'Uniforms');
gl.uniformBlockBinding(prg, blockNdx, 0);
const texLoc = gl.getUniformLocation(prg, 'tex');
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([255, 255, 255, 255]));
const geo = createCircleVertices({numSubdivisions: 6});
const vBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vBuf);
gl.bufferData(gl.ARRAY_BUFFER, geo.vertexData, gl.STATIC_DRAW);
gl.enable(gl.CULL_FACE);
gl.enable(gl.DEPTH_TEST);
const maxObjects = 20000;
const objectInfos = [];
const uniformBlockSize = (12 + 16 + 16 + 4 + 4 + 4 + 1 + 3) * 4
const blockSize = roundUp(uniformBlockSize, 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 Float32Array(uniformArrayBuffer);
const matrixOffset = 28;
const colorOffset = 28 + 16 + 4 + 4;
for (let i = 0; i < maxObjects; ++i) {
const color = [rand(), rand(), rand(), 1]
const position = [rand(-1, 1), rand(-1, 1)];
const velocity = [rand(-1, 1), rand(-1, 1)];
const offset = blockSize / 4 * i;
const matrix = uniformView.subarray(offset + matrixOffset, offset + matrixOffset + 16);
uniformView.set(color, offset + colorOffset);
objectInfos.push({
color,
position,
velocity,
matrix,
});
}
gl.clearColor(0.3, 0.3, 0.3, 1);
const canvasToSizeMap = new WeakMap();
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 {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);
timingHelper.begin();
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(prg);
gl.bindBuffer(gl.ARRAY_BUFFER, vBuf);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
for (let i = 0; i < settings.numObjects; ++i) {
const {
position,
velocity,
matrix,
} = objectInfos[i];
// -1.5 to 1.5
position[0] = euclideanModulo(position[0] + velocity[0] * deltaTime + 1.5, 3) - 1.5;
position[1] = euclideanModulo(position[1] + velocity[1] * deltaTime + 1.5, 3) - 1.5;
const [x, y] = position;
const s = 0.03;
matrix.set([
s, 0, 0, 0,
0, s, 0, 0,
0, 0, 1, 0,
x, y, 0, 1,
]);
}
gl.bindBuffer(gl.UNIFORM_BUFFER, uniformBuffer);
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, uniformView, 0, settings.numObjects * blockSize / 4);
for (let i = 0; i < settings.numObjects; ++i) {
const offset = i * blockSize;
gl.bindBufferRange(gl.UNIFORM_BUFFER, 0, uniformBuffer, offset, uniformBlockSize);
gl.uniform1i(texLoc, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.bindSampler(0, null);
gl.drawArrays(gl.TRIANGLES, 0, geo.numVertices);
}
timingHelper.end();
infoElem.textContent = `\
fps: ${(1 / deltaTime).toFixed(1)}
gpu: ${(timingHelper.getResult() / 1000 / 1000).toFixed(1)};
`;
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();
{"name":"WebGL: uniform buffer chrome/safari perf issue ","settings":{},"filenames":["index.html","index.css","index.js"]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment