過負荷にすることなくWebGL2の使用を最大化する

Aug 23 2020

私のWebアプリケーションは非常に長い計算を実行してから、結果を表示します。計算にWebGL2を使用しています-オフスクリーンの2Dテクスチャに描画します。1回のWegGL呼び出しでそれを単純に行うことはできません。計算に時間がかかりすぎて、「コンテキストが失われました」というエラーが発生します。そこで、計算を長方形の部分に分割し、それぞれを短時間で描画できるようにしました。

問題は、これらのWebGL呼び出しをスケジュールすることです。頻繁に行うと、ブラウザが応答しなくなったり、WebGLコンテキストが失われたりする可能性があります。十分な頻度で行わないと、計算に必要以上に時間がかかります。たまにコンテキストを失うのは普通のことだと理解していますが、GPUを使いすぎているため、体系的にコンテキストを失うのではないかと心配しています。

私が考えることができる最善の方法は、仕事と睡眠の比率を設定し、計算に使用した時間の何分の1かで睡眠をとることです。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();

このアプローチはあまり良くなく、次の問題があります。

  • workSleepRatioを何に設定すればよいかわからない。
  • GPU作業の完了とタイマーコールバックの間の無駄な時間。多くのブラウザでは、Web Workerスレッドであっても、タイムアウトパラメータがゼロに制限されているため、gl.clientWaitSyncに依存できません。
  • どんなに大きくworkSleepRatioを設定しても、ブラウザーが私がやりすぎだと思ってWebGLコンテキストを奪うことはないかどうかはまだわかりません。おそらく、requestAnimationFrameを使用して、スロットルされているときに速度を落とすことができますが、計算が完了するのを待っている間、ユーザーはタブを切り替えることができません。
  • setTimeoutはブラウザによって抑制され、要求されたよりもはるかに長くスリープする可能性があります。

要するに、私はこれらの質問があります:

  • WebGLを過負荷にすることなく、また時間を無駄にすることなく、どのように利用できるでしょうか。これも可能ですか?
  • それが不可能な場合、問題に対処するためのより良い方法はありますか?

回答

1 gman Aug 24 2020 at 22:30

あなたは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>

私の2014Macbook ProデュアルGPU(Intel / Nvidia)では、最初に、高性能のChromeが低電力を提供することを要求していても、Intel統合GPUを使用していることを意味します。

1x1ピクセルの最初のタイミングは、断続的に約17ミリ秒であることが多く、常にではありません。それを修正する方法がわかりません。1x1ピクセルが、1ミリ秒未満になるまで5回の時間のように、より妥当な数になるまでタイミングを維持できます。

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年後半のMacbookAirでのテストでは、最初のタイミングが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

さらに、タイミングは一種の偽物です。私の2014MBPに注意してください、32x32は0msで、64x32は突然13msです。32x32は6.5msになると思います。上記のMBAでも同じですが、すべてが0で、突然51msになります!??!??

Nvidia RTX2070を搭載したWindows10デスクトップで実行すると、すべてがより合理的に見えます。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

また、すべてのシステムで、タイミングが失敗する前に各サイズを事前に描画しないと、すべてのタイミングが16ミリ秒を超えて出力されます。プレドローを追加することはうまくいくようですが、それはブードゥーです。事前描画として幅×高さのピクセルではなく、1x1ピクセルだけを事前描画しようとしましたが、失敗しました!?!?!?

さらに、FirefoxはEXT_disjoint_timer_query_webgl2をサポートしていません。これは、正確なタイミングによって他のプロセスから情報を盗むことができるためだと思います。Chromeはサイトの分離でこれを修正しましたが、Firefoxはまだそれを行っていないと思います。

注:WebGL1にEXT_disjoint_timer_queryは同様の機能があります。

更新:Intel GPUの問題は、セキュリティの問題を回避するためのタイミングのあいまいさに関連している可能性がありますか?Intel GPUはユニファイドメモリを使用します(つまり、CPUとメモリを共有します)。知りません。クロムセキュリティの記事は、ユニファイドメモリを備えたデバイス上で精度を下げることに言及しています。

タイミング拡張がなくても、requestAnimationFrameのタイミングをチェックして、60Hz未満でレンダリングできるかどうかを確認できると思います。残念ながら、私の経験では、それが不安定になる可能性もあります。何かが原因でrAFが60fps以上かかる可能性があります。ユーザーが他のアプリを実行している可能性があります。多分彼らは30Hzのモニターにいます。など...特定のフレーム数にわたってタイミングを平均化するか、複数のタイミングの最低の読み取り値を取得する可能性があります。