【问题标题】:Updating many megabytes of vertex data per frame每帧更新数兆字节的顶点数据
【发布时间】:2021-01-04 20:25:00
【问题描述】:

我正在研究可以使用bufferDataSTATIC_DRAW 上传到WebGL 1 应用程序的顶点数据量的实际上限。我准备了一个 sn-p,可让您将每帧推送的三角形数量从 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,这对于我的应用程序来说已经绰绰有余了。在我的智能手机(摩托罗拉 G7 Power)上,每帧上传 4 MB 仍然会产生 60 FPS。

问题

  1. 我是否正确设计了这个性能实验?
  2. 我可以信任/将其结果扩展到现实世界吗?
  3. 您认为 2020 年中端移动设备和笔记本电脑每帧推送多少数据是合理的?

【问题讨论】:

  • snippet 比 codepen 更有用。站点规则实质上是说,如果您要询问的代码不在问题本身中,则问题不在主题范围内。您是在询问您的性能测试是否正确,但性能测试的代码不在问题本身中。
  • 谢谢@gman,很棒的建议。我将代码转换为sn-p。我在想也许 1) 本身更像是一个 CodeReview 问题,但我认为 1) 2) 3) 的精神作为一个整体更适合 StackOverflow。

标签: gpu webgl


【解决方案1】:
  1. 我是否正确设计了这个性能实验

这取决于你对正确的定义。 setInterval 不能保证远程准确,但代码假定您要求 1000 毫秒并且您得到 1000 毫秒,而不是 1100 毫秒或 900 毫秒等。如果是我,我不会使用 setInterval 并使用传递给 @987654323 的时间@ 并在考虑这些帧的实际时间的情况下获得最后 1 秒帧的结果

  1. 我可以信任/将其结果扩展到现实世界吗?

我不知道这个问题是什么意思。每个 gpu/cpu/browser/driver 可能会给出不同的结果。您可以尝试将结果与 gpu/cpu/浏览器的某些指纹相匹配。指纹可能与用户代理 + WEBGL_debug_renderer_info 信息一样简单,但并非所有浏览器都支持 WEBGL_debug_renderer_info 或用户代理。

  1. 您认为 2020 年中端移动设备和笔记本电脑每帧推送多少数据是合理的?

我不知道,我必须测试一堆不同的设备并在不同的浏览器上测试。

你的测试中还有几个 cmets

  • 它使用gl.STATIC_DRAW,但可以说它应该使用gl.DYNAMIC_DRAWgl.STREAM_DRAW,因为这告诉浏览器/驱动程序您将经常更改数据(不知道哪些驱动程序使用此信息)

  • 这取决于驱动程序,但对于许多驱动程序来说,分配一次缓冲区并使用bufferSubData 上传新数据可能更快。从语义上讲,bufferData 分配内存,而bufferSubData 不分配内存,虽然我听说过相反的情况,但通常内存分配比不分配要慢。

  • 如果您只关心顶点上传速度,那么您可能希望绘制尽可能小的三角形或根本不绘制(零尺寸三角形),这样您就可以消除绘制开销。换句话说,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);

【讨论】:

  • 谢谢。对于 STATIC vs DYNAMIC/STREAM 以及 Data vs SubData,这是故意的;我也尝试了 DYNAMIC 和 Data 并没有注意到我的机器上有任何区别。也许 DYNAMIC 和 SubData 会产生明显的变化。
  • 对于三角形大小,我没有将其设为零,因为我不希望一些奇异的驱动程序优化/诡计开始启动而不是在每一帧上上传缓冲区。但现在我意识到我很偏执,司机不可能这么聪明,也没有理由为这样的边缘情况编写这样的优化代码。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-31
  • 1970-01-01
相关资源
最近更新 更多