Last active
June 20, 2024 05:44
-
-
Save greggman/772c2e2122ceac9ba2390ed0c767b4bb to your computer and use it in GitHub Desktop.
WebGL: uniform buffer chrome/safari perf issue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<canvas></canvas> | |
<pre></pre> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{"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