每帧更新数兆字节的顶点数据
Updating many megabytes of vertex data per frame
我正在研究可以使用 bufferData
和 STATIC_DRAW
上传到 WebGL 1 应用程序的顶点数据量的实际上限。我准备了一个片段,可以让您将每帧推送的三角形数量从 0 调整到 10'000'000。我没有使用效率更高的 bufferSubData
和 STREAM_DRAW
选项,因为我真的很想体验最坏的情况。此外,我在屏幕上绘制的三角形非常小,因此填充率不会严重影响结果。
const canvas = document.createElement("canvas");
canvas.width = 320;
canvas.height = 180;
canvas.style.border = "1px solid black";
const gl = canvas.getContext("webgl");
document.body.appendChild(canvas);
const MAX_TRIANGLES = 10000000;
document.body.appendChild(document.createTextNode("Triangles to upload each frame: "));
const range = document.createElement("input");
range.type = "range";
range.min = 0;
range.max = MAX_TRIANGLES;
range.value = 100;
document.body.appendChild(range);
let nTriangles = range.value;
range.addEventListener("input", () => {
nTriangles = range.value;
});
const mbPerFrame = document.createElement("div");
document.body.appendChild(mbPerFrame);
const fps = document.createElement("div");
document.body.appendChild(fps);
gl.clearColor(0.2, 0.3, 0.5, 1.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, "attribute vec2 a_position; void main(void) { gl_Position = vec4(a_position / 100.0, 0.0, 1.0); }");
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, "precision mediump float; void main(void) { gl_FragColor = vec4(0.05, 0.0, 0.0, 0.05); }");
gl.compileShader(fs);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
gl.enableVertexAttribArray(0);
const randomVertexData = new Float32Array(3 * 2 * MAX_TRIANGLES);
for (let i = 0; i < 3 * MAX_TRIANGLES; i++) {
randomVertexData[i * 2 + 0] = Math.random() * 2 - 1;
randomVertexData[i * 2 + 1] = Math.random() * 2 - 1;
}
gl.useProgram(program);
const frameTimes = new Set();
function frame() {
frameTimes.add(performance.now());
gl.clear(gl.COLOR_BUFFER_BIT);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(randomVertexData.buffer, 0, nTriangles * 3 * 2), gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, nTriangles * 3);
requestAnimationFrame(frame);
}
setInterval(() => {
const now = performance.now();
let framesLastSecond = 0;
frameTimes.forEach((frameTime) => {
if (now - frameTime < 1000) {
framesLastSecond++;
} else {
frameTimes.delete(frameTime);
}
});
mbPerFrame.innerText = `${Math.floor((nTriangles * 3 * 2 * 4 / (1024 * 1024)) * 100) / 100} MB per frame`;
fps.innerText = `${framesLastSecond} FPS`;
}, 1000);
requestAnimationFrame(frame);
渲染循环基本上是这样做的:
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(randomVertexData.buffer, 0, nTriangles * 3 * 2), gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, nTriangles * 3);
其中randomVertexData
是事先准备好的一些数据,nTriangles
是滑块控制的
当您向右滑动滑块时,您会看到以 MB 为单位的大小增加而帧速率下降。
在我的机器(配备 i7 vPro 和 Quadro M1000 的戴尔笔记本电脑)上,每帧上传 30 MB 仍然会产生 60 FPS,这对我的应用程序来说已经足够了。在我的智能手机 (Motorola G7 Power) 上,每帧上传 4 MB 仍然会导致 60 FPS。
问题
- 我是否正确设计了这个性能实验?
- 我可以trust/extend它的结果到现实世界吗?
- 您认为对于 2020 年中端移动设备和笔记本电脑,每帧推送多少数据是合理的?
- Did I design this performance experiment correctly
这取决于你对正确的定义。 setInterval
不能保证远程准确,但代码假定您要求 1000 毫秒,而您得到了 1000 毫秒,而不是 1100 毫秒或 900 毫秒等。如果是我,我不会使用 setInterval
并使用时间传入 requestAnimationFrame
并在最后 1 秒的帧中获得结果,同时考虑到这些帧所花费的实际时间
- Can I trust/extend its results to the real world?
我不知道这个问题是什么意思。每个 gpu/cpu/browser/driver 可能会给出不同的结果。您可以尝试将结果与 gpu/cpu/browser 的某些指纹相匹配。指纹可能与用户代理 + WEBGL_debug_renderer_info 信息一样简单,但并非所有浏览器都支持 WEBGL_debug_renderer_info 或用户代理。
- What do you think it a reasonable amount of data to push per frame for middle-end 2020 mobile devices and laptops?
我不知道,我必须测试一堆不同的设备并在不同的浏览器上进行测试。
关于您的测试的更多评论
它正在使用 gl.STATIC_DRAW
但可以说它应该使用 gl.DYNAMIC_DRAW
或 gl.STREAM_DRAW
因为这告诉 browser/driver 您将更改数据经常(不知道哪些 driver 使用此信息)
这是 driver 相关的,但对于许多 driver 来说,分配一次缓冲区并使用 bufferSubData
上传新数据可能更快。从语义上讲,bufferData
分配内存而 bufferSubData
不分配内存,而且通常内存分配比 not-allocating 慢,尽管我听说过相反的情况。
如果您只关心顶点上传速度,那么您可能希望绘制尽可能小的三角形或可能根本不绘制(零尺寸三角形),这样您就可以消除绘制开销。换句话说,1000 个 512x512 三角形的绘制速度比 1000 个 2x2 三角形慢得多,因为一个绘制 4000 像素,另一个绘制 2.62 亿像素。
const canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
canvas.style.border = "1px solid black";
const gl = canvas.getContext("webgl");
document.body.appendChild(canvas);
const MAX_TRIANGLES = 10000000;
document.body.appendChild(document.createTextNode("Triangles to upload each frame: "));
const range = document.createElement("input");
range.type = "range";
range.min = 0;
range.max = MAX_TRIANGLES;
range.value = 100;
document.body.appendChild(range);
let nTriangles = range.value;
range.addEventListener("input", () => {
nTriangles = range.value;
});
const mbPerFrame = document.createElement("div");
document.body.appendChild(mbPerFrame);
const fps = document.createElement("div");
document.body.appendChild(fps);
gl.clearColor(0.2, 0.3, 0.5, 1.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, "attribute vec2 a_position; void main(void) { gl_Position = vec4(a_position / 100.0, 0.0, 1.0); }");
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, "precision mediump float; void main(void) { gl_FragColor = vec4(0.05, 0.0, 0.0, 0.05); }");
gl.compileShader(fs);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, MAX_TRIANGLES * 3 * 2 * 4, gl.STREAM_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
gl.enableVertexAttribArray(0);
const randomVertexData = new Float32Array(3 * 2 * MAX_TRIANGLES);
for (let i = 0; i < 3 * MAX_TRIANGLES; i++) {
randomVertexData[i * 2 + 0] = Math.random() * 2 - 1;
randomVertexData[i * 2 + 1] = Math.random() * 2 - 1;
}
gl.useProgram(program);
let then = 0;
let numFrames = 0;
function frame(now) {
const elapsedTime = now - then;
if (elapsedTime >= 1000) {
then = now;
mbPerFrame.innerText = `${(nTriangles * 3 * 2 * 4 / 1000 / 1000).toFixed(1)} MB per frame`;
fps.innerText = `${(numFrames / (elapsedTime * 0.001)).toFixed(1)} FPS`;
numFrames = 0;
}
++numFrames;
gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(randomVertexData.buffer, 0, nTriangles * 3 * 2));
gl.drawArrays(gl.TRIANGLES, 0, nTriangles * 3);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
我正在研究可以使用 bufferData
和 STATIC_DRAW
上传到 WebGL 1 应用程序的顶点数据量的实际上限。我准备了一个片段,可以让您将每帧推送的三角形数量从 0 调整到 10'000'000。我没有使用效率更高的 bufferSubData
和 STREAM_DRAW
选项,因为我真的很想体验最坏的情况。此外,我在屏幕上绘制的三角形非常小,因此填充率不会严重影响结果。
const canvas = document.createElement("canvas");
canvas.width = 320;
canvas.height = 180;
canvas.style.border = "1px solid black";
const gl = canvas.getContext("webgl");
document.body.appendChild(canvas);
const MAX_TRIANGLES = 10000000;
document.body.appendChild(document.createTextNode("Triangles to upload each frame: "));
const range = document.createElement("input");
range.type = "range";
range.min = 0;
range.max = MAX_TRIANGLES;
range.value = 100;
document.body.appendChild(range);
let nTriangles = range.value;
range.addEventListener("input", () => {
nTriangles = range.value;
});
const mbPerFrame = document.createElement("div");
document.body.appendChild(mbPerFrame);
const fps = document.createElement("div");
document.body.appendChild(fps);
gl.clearColor(0.2, 0.3, 0.5, 1.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, "attribute vec2 a_position; void main(void) { gl_Position = vec4(a_position / 100.0, 0.0, 1.0); }");
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, "precision mediump float; void main(void) { gl_FragColor = vec4(0.05, 0.0, 0.0, 0.05); }");
gl.compileShader(fs);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
gl.enableVertexAttribArray(0);
const randomVertexData = new Float32Array(3 * 2 * MAX_TRIANGLES);
for (let i = 0; i < 3 * MAX_TRIANGLES; i++) {
randomVertexData[i * 2 + 0] = Math.random() * 2 - 1;
randomVertexData[i * 2 + 1] = Math.random() * 2 - 1;
}
gl.useProgram(program);
const frameTimes = new Set();
function frame() {
frameTimes.add(performance.now());
gl.clear(gl.COLOR_BUFFER_BIT);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(randomVertexData.buffer, 0, nTriangles * 3 * 2), gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, nTriangles * 3);
requestAnimationFrame(frame);
}
setInterval(() => {
const now = performance.now();
let framesLastSecond = 0;
frameTimes.forEach((frameTime) => {
if (now - frameTime < 1000) {
framesLastSecond++;
} else {
frameTimes.delete(frameTime);
}
});
mbPerFrame.innerText = `${Math.floor((nTriangles * 3 * 2 * 4 / (1024 * 1024)) * 100) / 100} MB per frame`;
fps.innerText = `${framesLastSecond} FPS`;
}, 1000);
requestAnimationFrame(frame);
渲染循环基本上是这样做的:
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(randomVertexData.buffer, 0, nTriangles * 3 * 2), gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, nTriangles * 3);
其中randomVertexData
是事先准备好的一些数据,nTriangles
是滑块控制的
当您向右滑动滑块时,您会看到以 MB 为单位的大小增加而帧速率下降。
在我的机器(配备 i7 vPro 和 Quadro M1000 的戴尔笔记本电脑)上,每帧上传 30 MB 仍然会产生 60 FPS,这对我的应用程序来说已经足够了。在我的智能手机 (Motorola G7 Power) 上,每帧上传 4 MB 仍然会导致 60 FPS。
问题
- 我是否正确设计了这个性能实验?
- 我可以trust/extend它的结果到现实世界吗?
- 您认为对于 2020 年中端移动设备和笔记本电脑,每帧推送多少数据是合理的?
- Did I design this performance experiment correctly
这取决于你对正确的定义。 setInterval
不能保证远程准确,但代码假定您要求 1000 毫秒,而您得到了 1000 毫秒,而不是 1100 毫秒或 900 毫秒等。如果是我,我不会使用 setInterval
并使用时间传入 requestAnimationFrame
并在最后 1 秒的帧中获得结果,同时考虑到这些帧所花费的实际时间
- Can I trust/extend its results to the real world?
我不知道这个问题是什么意思。每个 gpu/cpu/browser/driver 可能会给出不同的结果。您可以尝试将结果与 gpu/cpu/browser 的某些指纹相匹配。指纹可能与用户代理 + WEBGL_debug_renderer_info 信息一样简单,但并非所有浏览器都支持 WEBGL_debug_renderer_info 或用户代理。
- What do you think it a reasonable amount of data to push per frame for middle-end 2020 mobile devices and laptops?
我不知道,我必须测试一堆不同的设备并在不同的浏览器上进行测试。
关于您的测试的更多评论
它正在使用
gl.STATIC_DRAW
但可以说它应该使用gl.DYNAMIC_DRAW
或gl.STREAM_DRAW
因为这告诉 browser/driver 您将更改数据经常(不知道哪些 driver 使用此信息)这是 driver 相关的,但对于许多 driver 来说,分配一次缓冲区并使用
bufferSubData
上传新数据可能更快。从语义上讲,bufferData
分配内存而bufferSubData
不分配内存,而且通常内存分配比 not-allocating 慢,尽管我听说过相反的情况。如果您只关心顶点上传速度,那么您可能希望绘制尽可能小的三角形或可能根本不绘制(零尺寸三角形),这样您就可以消除绘制开销。换句话说,1000 个 512x512 三角形的绘制速度比 1000 个 2x2 三角形慢得多,因为一个绘制 4000 像素,另一个绘制 2.62 亿像素。
const canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
canvas.style.border = "1px solid black";
const gl = canvas.getContext("webgl");
document.body.appendChild(canvas);
const MAX_TRIANGLES = 10000000;
document.body.appendChild(document.createTextNode("Triangles to upload each frame: "));
const range = document.createElement("input");
range.type = "range";
range.min = 0;
range.max = MAX_TRIANGLES;
range.value = 100;
document.body.appendChild(range);
let nTriangles = range.value;
range.addEventListener("input", () => {
nTriangles = range.value;
});
const mbPerFrame = document.createElement("div");
document.body.appendChild(mbPerFrame);
const fps = document.createElement("div");
document.body.appendChild(fps);
gl.clearColor(0.2, 0.3, 0.5, 1.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, "attribute vec2 a_position; void main(void) { gl_Position = vec4(a_position / 100.0, 0.0, 1.0); }");
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, "precision mediump float; void main(void) { gl_FragColor = vec4(0.05, 0.0, 0.0, 0.05); }");
gl.compileShader(fs);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, MAX_TRIANGLES * 3 * 2 * 4, gl.STREAM_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
gl.enableVertexAttribArray(0);
const randomVertexData = new Float32Array(3 * 2 * MAX_TRIANGLES);
for (let i = 0; i < 3 * MAX_TRIANGLES; i++) {
randomVertexData[i * 2 + 0] = Math.random() * 2 - 1;
randomVertexData[i * 2 + 1] = Math.random() * 2 - 1;
}
gl.useProgram(program);
let then = 0;
let numFrames = 0;
function frame(now) {
const elapsedTime = now - then;
if (elapsedTime >= 1000) {
then = now;
mbPerFrame.innerText = `${(nTriangles * 3 * 2 * 4 / 1000 / 1000).toFixed(1)} MB per frame`;
fps.innerText = `${(numFrames / (elapsedTime * 0.001)).toFixed(1)} FPS`;
numFrames = 0;
}
++numFrames;
gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(randomVertexData.buffer, 0, nTriangles * 3 * 2));
gl.drawArrays(gl.TRIANGLES, 0, nTriangles * 3);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);