Maximisez l'utilisation de WebGL2 sans le surcharger

Aug 23 2020

Mon application web fait un calcul très long et présente ensuite les résultats. J'utilise WebGL2 pour le calcul - dessin dans une texture 2D hors écran. Je ne peux pas simplement le faire en un seul appel WegGL - le calcul prendrait trop de temps et entraînerait l'erreur "contexte perdu". J'ai donc divisé le calcul en parties rectangulaires qui peuvent chacune être dessinées en peu de temps.

Le problème est de planifier ces appels WebGL. Si je les fais trop souvent, le navigateur peut ne plus répondre ou supprimer mon contexte WebGL. Si je ne les fais pas assez souvent, le calcul prendra plus de temps que nécessaire. Je comprends que perdre le contexte de temps en temps est normal, j'ai peur de le perdre systématiquement car j'utilise trop le GPU.

Le mieux que je puisse penser est d'avoir un rapport travail-sommeil et de dormir pendant une fraction du temps que j'ai utilisé pour le calcul. Je pense que je peux utiliser WebGL2 Sync Objects pour attendre la fin des appels émis et pour estimer approximativement le temps qu'ils ont pris. Comme ça:

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();

Cette approche n'est pas très bonne et elle a ces problèmes :

  • Je ne sais pas sur quoi définir workSleepRatio.
  • Temps perdu entre la fin du travail du GPU et le rappel de mon minuteur. Ne peut pas compter sur gl.clientWaitSync car son paramètre de délai d'attente est limité à zéro dans de nombreux navigateurs, même dans un thread Web Worker.
  • Quelle que soit la taille que j'ai définie pour le workSleepRatio, je ne peux toujours pas être sûr que le navigateur ne pensera pas que j'en fais trop et n'enlèvera pas le contexte WebGL. Peut-être que le requestAnimationFrame peut en quelque sorte être utilisé pour ralentir lorsqu'il est limité, mais l'utilisateur ne peut alors pas changer d'onglet en attendant la fin du calcul.
  • setTimeout peut être limité par le navigateur et dormir beaucoup plus longtemps que demandé.

Bref, j'ai ces questions :

  • comment utiliser WebGL sans le surcharger mais aussi sans perdre de temps ? Est-ce seulement possible?
  • Si ce n'est pas possible, existe-t-il de meilleurs moyens de résoudre le problème ?

Réponses

1 gman Aug 24 2020 at 22:30

Vous pourriez être en mesure d'utiliser le 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>

Sur mon Macbook Pro Dual GPU 2014 (Intel / Nvidia), tout d'abord, même si je demande des performances élevées, Chrome me donne une faible consommation d'énergie, ce qui signifie qu'il utilise le GPU intégré Intel.

Le premier timing sur 1x1 pixels est souvent d'environ 17 ms par intermittence et souvent mais pas toujours. Je ne sais pas comment résoudre ce problème. Je pourrais continuer à chronométrer jusqu'à ce que 1x1 pixels soit un nombre plus raisonnable comme le temps 5 fois jusqu'à ce qu'il soit <1 ms et si jamais échouer?

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

Les tests sur un Macbook Air fin 2018 avec GPU intégré Intel montrent un problème similaire, sauf que le premier timing est encore pire à 42 ms.

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

De plus, les horaires sont un peu faux. Remarque sur mon MBP 2014, 32x32 est de 0 ms et 64x32 est soudainement de 13 ms. Je m'attendrais à ce que 32x32 soit 6,5 ms. Idem sur le MBA au dessus, tout est à 0 et puis du coup 51ms !??!??

L'exécuter sur un bureau Windows 10 avec Nvidia RTX 2070 tout semble plus raisonnable. Le timing 1x1 est correct et les timings augmentent comme prévu.

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

De plus, sur tous les systèmes, si je ne pré-dessine pas chaque taille avant le timing, cela échoue et tous les timings sortent > 16 ms. L'ajout du pré-tirage semble fonctionner mais c'est du vaudou. J'ai même essayé de pré-dessiner juste 1x1 pixel au lieu de pixels de largeur par hauteur comme le pré-dessin et cela a échoué !?!?!?

De plus, Firefox ne prend pas en charge EXT_disjoint_timer_query_webgl2 Je pense que c'est parce que la synchronisation de précision permet de voler des informations à d'autres processus. Chrome a corrigé cela avec l' isolement du site, mais je suppose que Firefox n'a pas encore fait cela.

Remarque : WebGL1 a EXT_disjoint_timer_querydes fonctionnalités similaires.

mise à jour : les problèmes sur les GPU Intel peuvent être liés au fuzzing du timing pour éviter les problèmes de sécurité ? Les GPU Intel utilisent une mémoire unifiée (ce qui signifie qu'ils partagent la mémoire avec le CPU). Je ne sais pas. L' article sur la sécurité de Chrome mentionne une réduction de la précision sur les appareils dotés d'une mémoire unifiée.

Je suppose que même sans les extensions de synchronisation, vous pouvez essayer de voir si vous pouvez effectuer un rendu en moins de 60 Hz en vérifiant la synchronisation de requestAnimationFrame. Malheureusement, mon expérience là-bas est aussi qu'il peut être floconneux. Tout peut faire en sorte que rAF prenne plus de 60 images par seconde. Peut-être que l'utilisateur exécute d'autres applications. Peut-être qu'ils sont sur un moniteur 30hz. etc ... Peut-être en faisant la moyenne des timings sur un certain nombre d'images ou en prenant la lecture la plus basse de plusieurs timings.