在不超载的情况下最大限度地使用 WebGL2

Maximize WebGL2 usage without overloading it

我的网络应用程序进行了很长时间的计算,然后显示了结果。我正在使用 WebGL2 进行计算 - 绘制到屏幕外 二维纹理。我不能简单地在单个 WegGL 调用中完成它——计算会花费太长时间并导致“丢失上下文”错误。 所以我将计算分成矩形部分,每个部分都可以在短时间内绘制。

问题是安排这些 WebGL 调用。如果我经常这样做,浏览器可能会变得无响应或带走我的 WebGL 上下文。 如果我不经常这样做,计算将花费比必要的更长的时间。 我知道偶尔丢失上下文是正常的,我害怕系统地丢失它,因为我使用 GPU 太多了。

我能想到的最好的办法是有一些工作与睡眠的比例,并且睡眠时间是我用于计算的时间的一小部分。我认为 我可以使用 WebGL2 同步对象等待发出的调用完成并粗略估计它们花费了多少时间。像这样:

var workSleepRatio = 0.5; // some value
var waitPeriod = 5;
var sync;
var startTime;

function makeSomeWebglCalls() {
    startTime = performance.now();
    sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
    for (<estimate how many rectangles we can do so as not to waste too much time on waiting>) {
        gl.drawArrays(); // draw next small rectangle
    }
    setTimeout(timerCb, waitPeriod);
}

function timerCb() {
    var status = gl.getSyncParameter(sync, gl.SYNC_STATUS);
    if (status != gl.SIGNALED) {
        setTimeout(timerCb, waitPeriod);
    } else {
        gl.deleteSync(sync);
        
        var workTime = performance.now() - startTime;
        setTimeout(makeSomeWebglCalls, Math.min(1000, workTime * workSleepRatio));
    }
}

makeSomeWebglCalls();

这种方法不是很好,存在以下问题:

所以,简而言之,我有这些问题:

您或许可以使用 EXT_disjoint_timer_query_webgl2?

function main() {
  const gl = document.createElement('canvas').getContext('webgl2', {
    powerPreference: 'high-performance',
  });
  log(`powerPreference: ${gl.getContextAttributes().powerPreference}\n\n`);
  if (!gl) {
    log('need WebGL2');
    return;
  }
  const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');
  if (!ext) {
    log('need EXT_disjoint_timer_query_webgl2');
    return;
  }

  const vs = `#version 300 es
  in vec4 position;
  void main() {
    gl_Position = position;
  }
  `;

  const fs = `#version 300 es
  precision highp float;
  uniform sampler2D tex;
  out vec4 fragColor;
  void main() {
    const int across = 100;
    const int up = 100;
    vec2 size = vec2(textureSize(tex, 0));
    vec4 sum = vec4(0);
    for (int y = 0; y < up; ++y) {
      for (int x = 0; x < across; ++x) {
        vec2 start = gl_FragCoord.xy + vec2(x, y);
        vec2 uv = (mod(start, size) + 0.5) / size;
        uv = texture(tex, uv).xy;
        uv = texture(tex, uv).xy;
        uv = texture(tex, uv).xy;
        uv = texture(tex, uv).xy;
        uv = texture(tex, uv).xy;
        uv = texture(tex, uv).xy;
        uv = texture(tex, uv).xy;
        sum += texture(tex, uv);
      }
    }  
    fragColor = sum / float(across * up);
  }
  `;

  const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
  const bufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);

  const pixels = new Uint8Array(1024 * 1024 * 4);
  for (let i = 0; i < pixels.length; ++i) {
    pixels[i] = Math.random() * 256;
  }
  // creates a 1024x1024 RGBA texture.
  const tex = twgl.createTexture(gl, {src: pixels});

  gl.useProgram(programInfo.program);
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);

  const waitFrame = _ => new Promise(resolve => requestAnimationFrame(resolve));

  const widthHeightFromIndex = i => {
    const height = 2 ** (i / 2 | 0);
    const width = height * (i % 2 + 1);
    return { width, height };
  };

  async function getSizeThatRunsUnderLimit(gl, limitMs) {
    log('size        time in milliseconds');
    log('--------------------------------');
    for (let i = 0; i < 32; ++i) {
      const {width, height} = widthHeightFromIndex(i);
      const timeElapsedMs = await getTimeMsForSize(gl, width, height);
      const dims = `${width}x${height}`;
      log(`${dims.padEnd(11)} ${timeElapsedMs.toFixed(1).padStart(6)}`);
      if (timeElapsedMs > limitMs) {
        return widthHeightFromIndex(i - 1);
      }
    }
  }

  (async () => {
    const limit = 1000 / 20;
    const {width, height} = await getSizeThatRunsUnderLimit(gl, limit);
    log('--------------------------------');
    log(`use ${width}x${height}`);
  })();

  async function getTimeMsForSize(gl, width, height) {
    gl.canvas.width = width;
    gl.canvas.height = height;
    gl.viewport(0, 0, width, height);

    // prime the GPU/driver
    // this is voodoo but if I don't do this
    // all the numbers come out bad. Even with
    // this the first test seems to fail with
    // a large number intermittently
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

    for (;;) {
      const query = gl.createQuery();
      gl.beginQuery(ext.TIME_ELAPSED_EXT, query);

      gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

      gl.endQuery(ext.TIME_ELAPSED_EXT);
      gl.flush();

      for (;;) {
        await waitFrame();

        const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);
        if (available) {
          break;
        }
      }

      const disjoint = gl.getParameter(ext.GPU_DISJOINT_EXT);    
      if (!disjoint) {
        const timeElapsed = gl.getQueryParameter(query, gl.QUERY_RESULT); 
        gl.deleteQuery(query);
        return timeElapsed / (10 ** 6);  // return milliseconds
      }

      gl.deleteQuery(query);
    }
  }
}

main();

function log(...args) {
  const elem = document.createElement('pre');
  elem.textContent = args.join(' ');
  document.body.appendChild(elem);
}
pre { margin: 0; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

在我的 2014 Macbook Pro 双 GPU (Intel/Nvidia) 上,首先,即使我请求 high-performance Chrome 给了我 low-power 意味着它正在使用英特尔集成GPU.

1x1 像素的第一个计时通常断断续续地约为 17 毫秒,而且经常但并非总是如此。我不知道如何解决这个问题。我可以保持计时,直到 1x1 像素是一些更合理的数字,比如时间 5 次,直到它小于 1 毫秒,如果永远不会失败?

powerPreference: low-power

size        time in milliseconds
--------------------------------
1x1           16.1
2x1            0.0
2x2            0.0
4x2            0.0
4x4            0.0
8x4            0.1
8x8            0.1
16x8           0.0
16x16          0.0
32x16          0.0
32x32          0.0
64x32         13.6
64x64         35.7
128x64        62.6
--------------------------------
use 64x64

在配备 Intel 集成 GPU 的 2018 年末 Macbook Air 上进行的测试显示了类似的问题,除了第一个时间在 42 毫秒时更糟。

size        time in milliseconds
--------------------------------
1x1           42.4
2x1            0.0
2x2            0.0
4x2            0.0
4x4            0.0
8x4            0.0
8x8            0.0
16x8           0.0
16x16          0.0
32x16          0.0
32x32          0.0
64x32          0.0
64x64         51.5
--------------------------------
use 64x32

此外,时间安排有点假。注意我的 2014 MBP,32x32 是 0ms,64x32 突然是 13ms。我预计 32x32 为 6.5 毫秒。同样在上面的MBA上,一切都是0然后突然51ms !??!??

运行 它在带有 Nvidia RTX 2070 的 Windows 10 桌面上一切似乎都更合理。 1x1 时序正确并且时序按预期增长。

powerPreference: low-power

size        time in milliseconds
--------------------------------
1x1            0.0
2x1            0.0
2x2            0.0
4x2            0.0
4x4            0.0
8x4            0.0
8x8            0.0
16x8           0.0
16x16          0.0
32x16          0.1
32x32          0.1
64x32          2.4
64x64          2.9
128x64         3.1
128x128        6.0
256x128       15.4
256x256       27.8
512x256       58.6
--------------------------------
use 256x256

此外,在所有系统上,如果我在计时失败之前不 pre-draw 每个尺寸,并且所有计时都超过 16 毫秒。添加 pre-draw 似乎可行,但它是巫术。我什至尝试 pre-drawing 只是 1x1 像素而不是宽度乘高度像素作为 pre-draw 但失败了!?!?!?

此外,Firefox 不支持 EXT_disjoint_timer_query_webgl2 我认为这是因为精确的计时使得从其他进程窃取信息成为可能。 Chrome 用 site isolation 修复了这个问题,但我猜 Firefox 还没有这样做。

注意:WebGL1 具有 EXT_disjoint_timer_query 类似的功能。

更新:英特尔 GPU 上的问题可能与模糊时序有关,以避免安全问题?英特尔 GPU 使用统一内存(意味着它们与 CPU 共享内存)。我不知道。 chrome security article 提到在具有统一内存的设备上降低精度。

我想即使没有时序扩展,您也可以尝试通过检查 requestAnimationFrame 时序来查看是否可以在 60hz 以下渲染。不幸的是,我的经验是它也可能是片状的。任何事情都可能导致 rAF 超过 60fps。也许用户是 运行 其他应用程序。也许他们在 30hz 显示器上。等等...也许对一定数量的帧进行平均计时,或者取多个计时的最低读数。