Максимизируйте использование WebGL2, не перегружая его

Мое веб-приложение выполняет очень долгие вычисления, а затем представляет результаты. Я использую WebGL2 для вычислений - рисования в закадровой 2D-текстуре. Я не могу просто сделать это за один вызов WegGL - вычисление займет слишком много времени и приведет к ошибке потери контекста. Поэтому я разделил вычисления на прямоугольные части, каждую из которых можно нарисовать за короткое время.

Проблема заключается в планировании этих вызовов WebGL. Если я буду делать это слишком часто, браузер может перестать отвечать или забрать мой контекст WebGL. Если я не буду делать их достаточно часто, вычисления займут больше времени, чем необходимо. Я понимаю, что время от времени терять контекст - это нормально, я боюсь систематически его терять, потому что слишком много использую графический процессор.

Лучшее, что я мог придумать, - это иметь какое-то соотношение работы и сна и спать в течение части времени, которое я использовал для вычислений. Я думаю, что могу использовать объекты синхронизации 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.
  • Потраченное время между завершением работы графического процессора и обратным вызовом таймера. Нельзя полагаться на gl.clientWaitSync, потому что его параметр тайм-аута ограничен нулем во многих браузерах, даже в потоке Web Worker.
  • Каким бы большим я ни установил workSleepRatio, я все еще не могу быть уверен, что браузер не подумает, что я делаю слишком много, и не уберет контекст WebGL. Возможно, requestAnimationFrame можно каким-то образом использовать для замедления при его регулировании, но тогда пользователь не может переключать вкладки, ожидая завершения вычисления.
  • setTimeout может регулироваться браузером и спать намного дольше, чем было запрошено.

Итак, вкратце, у меня есть такие вопросы:

  • как можно использовать WebGL, не перегружая его, но при этом не теряя времени? Это вообще возможно?
  • Если это невозможно, то есть ли более эффективные способы решения проблемы?

person egor    schedule 23.08.2020    source источник
comment
Полезно ли ?   -  person gman    schedule 24.08.2020


Ответы (1)


Возможно, вы сможете использовать 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>

На моем Macbook Pro 2014 года с двумя графическими процессорами (Intel / Nvidia), во-первых, даже если я прошу высокопроизводительный Chrome, он дает мне низкое энергопотребление, что означает использование встроенного графического процессора Intel.

Первый тайминг на пикселях 1x1 часто составляет ~ 17 мс с перерывами и часто, но не всегда. Я не знаю, как это исправить. Я мог бы продолжать отсчет времени до тех пор, пока 1х1 пиксель не станет более разумным числом, например, 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

Тестирование на Macbook Air конца 2018 года со встроенным графическим процессором Intel показало аналогичную проблему, за исключением того, что первое время оказалось еще хуже - 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

Кроме того, тайминги являются фиктивными. Обратите внимание на мой MBP 2014 года, 32x32 - это 0 мс, а 64x32 - внезапно 13 мс. Я ожидал, что 32x32 будет 6.5 мс. То же самое с MBA выше, все 0, а затем внезапно 51 мс! ??! ??

Запускать его на рабочем столе Windows 10 с Nvidia RTX 2070 все кажется более разумным. Тайминги 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 могут быть связаны с нечеткой синхронизацией, чтобы избежать проблем с безопасностью? Графические процессоры Intel используют единую память (то есть они разделяют память с ЦП). Я не знаю. В статье о безопасности Chrome упоминается снижение точности на устройствах с объединенной памятью.

Я полагаю, что даже без расширений времени вы могли бы попробовать проверить, сможете ли вы рендерить менее 60 Гц, проверив время requestAnimationFrame. К сожалению, мой опыт показывает, что это может быть нестабильно. Что угодно может привести к тому, что rAF будет принимать более 60 кадров в секунду. Возможно, пользователь запускает другие приложения. Может, они на мониторе 30 Гц. и т.д ... Возможно, усреднение таймингов по определенному количеству кадров или получение наименьшего значения из нескольких таймингов.

person gman    schedule 24.08.2020
comment
Спасибо, gman. Думаю, я воспользуюсь временем этого расширения, если оно доступно. У меня также есть встроенный графический процессор Intel, все мои результаты превышают 10 мс. Также они подозрительно близки к кратным 1/60 (16, 33, 50). Возможно, дело в скорости прорисовки браузеров. Но в любом случае они точнее моих измерений preformance.now () - person egor; 28.08.2020
comment
Я также получаю низкое энергопотребление во фрагменте, но высокое при запуске из локального файла html - person egor; 28.08.2020