Massimizza l'utilizzo di WebGL2 senza sovraccaricarlo

Aug 23 2020

La mia applicazione web esegue un calcolo molto lungo e quindi presenta i risultati. Sto usando WebGL2 per il calcolo, disegnando in una trama 2D fuori schermo. Non posso semplicemente farlo in una singola chiamata WegGL: il calcolo richiederebbe troppo tempo e comporterebbe l'errore "contesto perso". Quindi ho diviso il calcolo in parti rettangolari che possono essere disegnate ciascuna in breve tempo.

Il problema è la pianificazione di queste chiamate WebGL. Se li eseguo troppo spesso, il browser potrebbe non rispondere o rimuovere il mio contesto WebGL. Se non li eseguo abbastanza spesso, il calcolo richiederà più tempo del necessario. Capisco che perdere contesto di tanto in tanto sia normale, ho paura di perderlo sistematicamente perché sto usando troppo la GPU.

Il meglio a cui potrei pensare è avere un rapporto lavoro-sonno e dormire per una frazione del tempo che ho usato per il calcolo. Penso di poter utilizzare WebGL2 Sync Objects per attendere il completamento delle chiamate emesse e per stimare approssimativamente quanto tempo hanno impiegato. Come questo:

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

Questo approccio non è molto buono e presenta questi problemi:

  • Non so su cosa impostare workSleepRatio.
  • Tempo sprecato tra il completamento del lavoro della gpu e la mia richiamata del timer. Non posso fare affidamento su gl.clientWaitSync perché il suo parametro di timeout è limitato da zero in molti browser, anche in un thread di Web Worker.
  • Per quanto grande imposti workSleepRatio, non posso ancora essere sicuro che il browser non penserà che sto facendo troppo e toglierà il contesto WebGL. Forse requestAnimationFrame può in qualche modo essere utilizzato per rallentare quando viene limitato, ma l'utente non può cambiare scheda mentre attende il completamento del calcolo.
  • setTimeout potrebbe essere limitato dal browser e dormire molto più a lungo di quanto richiesto.

Quindi, in breve, ho queste domande:

  • come si può utilizzare WebGL senza sovraccaricarlo ma anche senza perdere tempo? È possibile?
  • Se non è possibile, ci sono modi migliori per affrontare il problema?

Risposte

1 gman Aug 24 2020 at 22:30

Potresti essere in grado di utilizzare il 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>

Sul mio Macbook Pro Dual GPU (Intel/Nvidia) del 2014, prima di tutto, anche se richiedo Chrome ad alte prestazioni mi dà un basso consumo, il che significa che utilizza la GPU Intel integrata.

La prima temporizzazione su pixel 1x1 è spesso ~ 17 ms in modo intermittente e spesso ma non sempre. Non so come risolverlo. Potrei continuare a cronometrare fino a quando 1x1 pixel è un numero più ragionevole come il tempo 5 volte finché non è <1 ms e se mai fallisce?

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

I test su un Macbook Air di fine 2018 con GPU integrata Intel mostrano un problema simile, tranne per il fatto che il primo timing risulta ancora peggiore a 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

Inoltre, i tempi sono piuttosto fasulli. Nota sul mio MBP 2014, 32x32 è 0ms e 64x32 è improvvisamente 13ms. Mi aspetto che 32x32 sia 6,5 ​​ms. Lo stesso sull'MBA sopra, tutto è 0 e poi improvvisamente 51ms!??!??

Eseguendolo su un desktop Windows 10 con Nvidia RTX 2070 tutto sembra più ragionevole. Il tempismo 1x1 è corretto e i tempi crescono come previsto.

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

Inoltre, su tutti i sistemi, se non disegno in anticipo ogni dimensione prima del tempo, fallisce e tutti i tempi escono> 16 ms. L'aggiunta del pre-draw sembra funzionare ma è voodoo. Ho anche provato a pre-disegnare solo 1x1 pixel invece di pixel di larghezza per altezza come pre-disegno e non è riuscito!?!?!?

Inoltre, Firefox non supporta EXT_disjoint_timer_query_webgl2, credo che sia perché il tempismo di precisione rende possibile rubare informazioni da altri processi. Chrome ha risolto questo problema con l' isolamento del sito, ma immagino che Firefox debba ancora farlo.

nota: WebGL1 ha EXT_disjoint_timer_queryfunzionalità simili.

aggiornamento: i problemi sulle GPU Intel potrebbero essere correlati alla confusione dei tempi per evitare problemi di sicurezza? Le GPU Intel utilizzano la memoria unificata (nel senso che condividono la memoria con la CPU). Non lo so. L' articolo sulla sicurezza di Chrome menziona l'abbassamento della precisione sui dispositivi con memoria unificata.

Suppongo che anche senza le estensioni di temporizzazione potresti provare a vedere se riesci a eseguire il rendering in meno di 60 Hz controllando i tempi di requestAnimationFrame. Sfortunatamente la mia esperienza è anche che può essere traballante. Qualsiasi cosa potrebbe far sì che rAF impieghi più di 60 fps. Forse l'utente sta eseguendo altre app. Forse sono su un monitor a 30Hz. ecc ... Forse calcolando la media dei tempi su un certo numero di fotogrammi o prendendo la lettura più bassa di più tempi.