Maximize o uso do WebGL2 sem sobrecarregá-lo
Meu aplicativo da web faz uma computação muito longa e apresenta os resultados. Estou usando WebGL2 para a computação - desenhando em uma textura 2D fora da tela. Não posso simplesmente fazer isso em uma única chamada WegGL - o cálculo demoraria muito e resultaria no erro "contexto perdido". Portanto, divido o cálculo em partes retangulares que podem ser desenhadas em pouco tempo.
O problema é agendar essas chamadas WebGL. Se eu fizer isso com muita frequência, o navegador pode parar de responder ou tirar meu contexto WebGL. Se eu não os fizer com frequência suficiente, o cálculo levará mais tempo do que o necessário. Entendo que perder o contexto de vez em quando é normal, tenho medo de perder sistematicamente porque estou usando muito a GPU.
O melhor que consegui pensar é ter uma relação trabalho/sono e dormir por uma fração do tempo que usei para o cálculo. Acho que posso usar WebGL2 Sync Objects para aguardar a conclusão das chamadas emitidas e estimar aproximadamente quanto tempo elas levaram. Assim:
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();
Esta abordagem não é muito boa e tem estes problemas:
- Não sei como definir workSleepRatio.
- Perdi tempo entre a conclusão do trabalho da gpu e meu retorno de chamada do timer. Não é possível confiar em gl.clientWaitSync porque seu parâmetro de tempo limite é limitado por zero em muitos navegadores, mesmo em um encadeamento do Web Worker.
- Por maior que eu defina o workSleepRatio, ainda não posso ter certeza de que o navegador não pensará que estou fazendo muito e tirará o contexto do WebGL. Talvez o requestAnimationFrame possa ser usado de alguma forma para desacelerar quando está sendo limitado, mas o usuário não pode alternar as guias enquanto aguarda a conclusão do cálculo.
- setTimeout pode ser limitado pelo navegador e dormir muito mais do que o solicitado.
Então, resumindo, eu tenho essas perguntas:
- como alguém pode utilizar o WebGL sem sobrecarregá-lo, mas também sem perder tempo? Isso é mesmo possível?
- Se não for possível, existem maneiras melhores de lidar com o problema?
Respostas
Você pode ser capaz de usar o 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>
No meu Macbook Pro Dual GPU 2014 (Intel/Nvidia), em primeiro lugar, embora eu solicite que o Chrome de alto desempenho me forneça baixo consumo de energia, o que significa que está usando a GPU integrada da Intel.
A primeira temporização em 1x1 pixels geralmente é de ~17 ms de forma intermitente e frequente, mas nem sempre. Eu não sei como consertar isso. Eu poderia manter o tempo até que 1x1 pixels seja um número mais razoável, como o tempo 5 vezes até <1 ms e, se nunca, falhar?
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
O teste em um Macbook Air do final de 2018 com GPU Intel Integrated mostra um problema semelhante, exceto que o primeiro tempo é ainda pior em 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
Além disso, os horários são meio falsos. Observe no meu MBP de 2014, 32x32 é 0ms e 64x32 é repentinamente 13ms. Eu esperaria que 32x32 fosse 6,5ms. O mesmo no MBA acima, tudo é 0 e de repente 51ms!??!??
Rodá-lo em uma área de trabalho do Windows 10 com Nvidia RTX 2070 tudo parece mais razoável. O timing 1x1 está correto e os timings crescem conforme o esperado.
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
Além disso, em todos os sistemas, se eu não pré-desenhar cada tamanho antes do tempo, ele falha e todos os tempos saem > 16 ms. Adicionar o pré-sorteio parece funcionar, mas é vodu. Eu até tentei pré-desenhar apenas 1x1 pixel em vez de pixels de largura por altura como pré-desenho e isso falhou!?!?!?
Além disso, o Firefox não oferece suporte a EXT_disjoint_timer_query_webgl2. Acredito que seja porque o tempo de precisão torna possível roubar informações de outros processos. O Chrome corrigiu isso com o isolamento do site, mas acho que o Firefox ainda não fez isso.
nota: WebGL1 tem EXT_disjoint_timer_queryfuncionalidade semelhante.
atualização: os problemas nas GPUs da Intel podem estar relacionados ao fuzzing do tempo para evitar problemas de segurança? As GPUs Intel usam memória unificada (o que significa que compartilham memória com a CPU). Não sei. O artigo de segurança do Chrome menciona a diminuição da precisão em dispositivos com memória unificada.
Suponho que, mesmo sem as extensões de tempo, você possa tentar ver se consegue renderizar em menos de 60 Hz verificando o tempo de requestAnimationFrame. Infelizmente, minha experiência também é que pode ser esquisito. Qualquer coisa pode fazer com que o rAF leve mais de 60fps. Talvez o usuário esteja executando outros aplicativos. Talvez eles estejam em um monitor de 30hz. etc ... Talvez calculando a média dos tempos em um certo número de quadros ou fazendo a leitura mais baixa de vários tempos.