webgl - 在不超载的情况下最大化 WebGL2 的使用
问题描述
我的 Web 应用程序进行了很长时间的计算,然后显示结果。我正在使用 WebGL2 进行计算 - 绘制到屏幕外的 2D 纹理中。我不能简单地在单个 WegGL 调用中完成 - 计算将花费太长时间并导致“丢失上下文”错误。所以我将计算分成矩形部分,每个部分都可以在短时间内绘制出来。
问题在于安排这些 WebGL 调用。如果我经常这样做,浏览器可能会变得无响应或带走我的 WebGL 上下文。如果我不经常这样做,计算将花费比必要时间更长的时间。我知道偶尔丢失上下文是正常的,我害怕系统地丢失它,因为我使用 GPU 太多了。
我能想到的最好的办法是有一些工作与睡眠的比例,并且睡眠时间只是我用于计算的时间的一小部分。我想我可以使用 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 工作完成和我的计时器回调之间浪费时间。不能依赖 gl.clientWaitSync 因为它的超时参数在许多浏览器中被限制为零,即使在 Web Worker 线程中也是如此。
- 不管我设置 workSleepRatio 有多大,我仍然不能确定浏览器不会认为我做的太多并带走 WebGL 上下文。也许 requestAnimationFrame 在受到限制时可以以某种方式用于减慢速度,但是用户在等待计算完成时无法切换选项卡。
- setTimeout 可能会被浏览器限制并且睡眠时间比请求的时间长很多。
所以,简而言之,我有这些问题:
- 如何在不超载但又不浪费时间的情况下使用 WebGL?这甚至可能吗?
- 如果不可能,那么有没有更好的方法来处理这个问题?
解决方案
您也许可以使用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>
在我的 2014 Macbook Pro Dual GPU (Intel/Nvidia) 上,首先,即使我要求高性能 Chrome 为我提供低功耗意味着它使用的是 Intel 集成 GPU。
1x1 像素的第一次计时通常是间歇性的 ~17ms,但并非总是如此。我不知道如何解决这个问题。我可以一直计时直到 1x1 像素是一些更合理的数字,比如时间 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
在 2018 年末的配备英特尔集成 GPU 的 Macbook Air 上进行的测试显示了类似的问题,除了第一个时间在 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
此外,时间安排有点虚假。请注意我的 2014 MBP,32x32 是 0 毫秒,而 64x32 突然是 13 毫秒。我希望 32x32 为 6.5 毫秒。上面的MBA也是一样,一切都是0然后突然51ms!??!??
在带有 Nvidia RTX 2070 的 Windows 10 桌面上运行它似乎更合理。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
类似的功能。
更新:英特尔 GPU 上的问题可能与模糊时序以避免安全问题有关?英特尔 GPU 使用统一内存(意味着它们与 CPU 共享内存)。我不知道。chrome 安全文章提到降低具有统一内存的设备的精度。
我想即使没有时间扩展,您也可以尝试通过检查 requestAnimationFrame 时间来查看是否可以在 60hz 以下进行渲染。不幸的是,我的经验也表明它可能是片状的。任何事情都可能导致 rAF 超过 60fps。也许用户正在运行其他应用程序。也许他们在一个 30 赫兹的显示器上。等等......也许是在一定数量的帧上平均时间或获取多个时间的最低读数。
推荐阅读
- html - 如何在按钮内正确集中跨度?
- nativescript - 无法验证 BarcodeScannerFramework.framework/BarcodeScannerFramework 中的位码
- kotlin - Kotlin - 在保持精度的同时将浮点数转换为双精度数
- swift - 我应该如何从 Firebase 将数据检索到 UICollectionViewCell?
- scala - 拆分rdd并访问元素的子组
- javascript - 如何删除chartjs甜甜圈底部的图例
- javascript - 为股票图表中的预配置日期范围添加工具提示 (Highstock)
- tsql - 如何反转表格的内容?
- image - 动态路径在需要反应本机时不起作用
- azure-cognitive-search - 如何使用 azure search REST API 一次发出多个搜索请求?