每帧更新数兆字节的顶点数据

Updating many megabytes of vertex data per frame

我正在研究可以使用 bufferDataSTATIC_DRAW 上传到 WebGL 1 应用程序的顶点数据量的实际上限。我准备了一个片段,可以让您将每帧推送的三角形数量从 0 调整到 10'000'000。我没有使用效率更高的 bufferSubDataSTREAM_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。

问题

  1. 我是否正确设计了这个性能实验?
  2. 我可以trust/extend它的结果到现实世界吗?
  3. 您认为对于 2020 年中端移动设备和笔记本电脑,每帧推送多少数据是合理的?
  1. Did I design this performance experiment correctly

这取决于你对正确的定义。 setInterval 不能保证远程准确,但代码假定您要求 1000 毫秒,而您得到了 1000 毫秒,而不是 1100 毫秒或 900 毫秒等。如果是我,我不会使用 setInterval 并使用时间传入 requestAnimationFrame 并在最后 1 秒的帧中获得结果,同时考虑到这些帧所花费的实际时间

  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 或用户代理。

  1. 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_DRAWgl.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);