WebGL:异步操作?

4

我想知道WebGL是否有任何异步调用可以利用?

我已经查看了v1和v2规范,它们都没有提到任何内容。在V2中,有一个WebGL查询机制,我认为那不是我想要的。

在网上搜索并没有得出明确的结论。有这个例子,但同步版本和异步版本的区别不太清楚。http://toji.github.io/shader-perf/

最终,我希望能够异步执行以下操作之一或全部操作:

  • readPixels
  • texSubImage2D和texImage2D
  • Shader编译
  • 程序链接
  • draw???

有一个glFinish操作,其文档说明为:“直到所有先前调用的GL命令的效果完成后,才会返回。”对我来说,这意味着存在可以通过调用Finish()等待的异步操作?

网上的某些帖子表明,调用getError()也会强制发生某些同步操作,并且每次调用后也不是很理想。


发现了一些关于异步着色器编译的信息,也许会有帮助:http://toji.github.io/shader-perf/ - HankMoody
除此之外,我认为你所要求的其他操作都是同步的。 - HankMoody
谢谢Hank。我已经看过了,但会再次审查。 - Jeff Saremi
1个回答

8
这取决于您对异步的定义。
在Chrome中(Firefox现在可能也这样做?不确定),所有GPU代码都在与JavaScript分开的单独进程中运行。这意味着您的命令正在异步运行。即使是OpenGL本身也被设计为异步的。函数(WebGL / OpenGL)将命令插入命令缓冲区。其他线程/进程会执行它们。通过调用gl.flush,您会告诉OpenGL“嘿,我有新的命令要执行!” 它将异步执行这些命令。如果您不调用gl.flush,当发出太多命令时,定期会为您调用它。如果您调用了任何渲染命令到画布(gl.drawXXX,gl.clear),当前JavaScript事件退出时,它也会被调用。
从这个意义上讲,WebGL的一切都是异步的。如果您不查询某些内容(gl.getXXX,gl.readXXX),那么与您的JavaScript不同步处理(绘制)的内容正在处理中。毕竟,WebGL提供给您一个GPU,它在CPU之外运行。
知道这一点,优化Chrome的一种方法是通过提交着色器来异步编译着色器。
for each shader
  s = gl.createShader()
  gl.shaderSource(...);
  gl.compileShader(...);
  gl.attachShader(...);
gl.linkProgram(...)
gl.flush()

GPU进程现在将编译你的着色器。所以,假设250毫秒后你才开始询问是否成功并查询位置,那么如果编译和链接着色器所需的时间少于250毫秒,那么所有这些都是异步发生的。
在WebGL2中,至少还有一个明显的异步操作——遮挡查询,在其中WebGL2可以告诉你绘制了多少像素用于一组绘制调用。如果没有绘制,则你的绘制被遮挡了。要获得答案,你需要定期轮询以查看答案是否准备就绪。通常你下一帧检查,事实上WebGL规范要求答案在下一帧之前不可用。
否则,在目前(2018年8月),没有明确的异步API。
更新
HankMoody在评论中提到,texImagesync。同样地,它取决于你对异步的定义。添加指令及其数据需要时间。像gl.enable(gl.DEPTH_TEST)这样的命令只需要添加2-8字节。像gl.texImage2D(..., width = 1024, height = 1024, RGBA, UNSIGNED_BYTE)这样的命令必须添加4兆!上传这4兆后的其余部分是异步的,但上传需要时间。这两个命令的情况相同,只是添加2-8字节需要的时间比添加4兆要少得多。
更明确地说,上传了那4兆后,许多其他事情发生在异步方式下。驱动程序被调用,并复制那4兆。驱动程序安排在稍后的某个时间使用该4兆,因为如果纹理已经在使用中,则无法立即上传数据。或者它确实立即上传到一个新区域,然后在实际使用该新数据的绘制调用之前交换纹理指向的内容。其他驱动程序只复制数据并存储它,并等待直到纹理在绘制调用中使用才实际更新纹理。这是因为texImage2D具有疯狂的语义,可以以任何顺序上传不同大小的mip,因此驱动程序无法知道GPU内存中实际需要的内容,直到绘制时,因为它不知道您将调用texIamge2D的顺序。本段中提到的所有这些事情都是异步发生的。
但这也带来了一些更多的信息。

gl.texImage2D和相关命令需要完成大量的工作。其中一个任务是遵守UNPACK_FLIP_Y_WEBGLUNPACK_PREMULTIPLY_ALPHA_WEBGL规范,因此可能需要复制多兆字节的数据进行翻转或预乘。其次,如果您传递给它们视频、画布或图像,它们可能需要进行大量转换,甚至重新解析源图像,特别是在UNPACK_COLORSPACE_CONVERSION_WEBGL的情况下。这是否以某种异步方式发生取决于浏览器。由于您没有直接访问图像/视频/画布,因此浏览器可以全部异步进行,但无论如何,所有这些工作都必须完成。

为了使大部分工作异步化,添加了ImageBitmap API。与大多数Web API一样,它的规范不太明确,但想法是首先进行异步fetch。然后请求创建一个ImageBitmap并提供颜色转换、翻转、预乘阿尔法选项。这也是异步发生的。然后将结果传递给gl.texImage2D,希望浏览器能在最后一步之前完成所有的重要工作。

示例:

// note: mode: 'cors' is because we are loading
// from a different domain

async function main() {
  const response = await fetch('https://i.imgur.com/TSiyiJv.jpg', {mode: 'cors'})
  if (!response.ok) {
    return console.error('response not ok?');
  }
  const blob = await response.blob();
  const bitmap = await createImageBitmap(blob, {
    premultiplyAlpha: 'none',
    colorSpaceConversion: 'none',
  });

  const gl = document.querySelector("canvas").getContext("webgl");

  const tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, tex);
  {
    const level = 0;
    const internalFormat = gl.RGBA;
    const format = gl.RGBA;
    const type = gl.UNSIGNED_BYTE;
    gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
                  format, type, bitmap);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  }

  const vs = `
  uniform mat4 u_worldViewProjection;
  attribute vec4 position;
  attribute vec2 texcoord;
  varying vec2 v_texCoord;

  void main() {
    v_texCoord = texcoord;
    gl_Position = u_worldViewProjection * position;
  }
  `;
  const fs = `
  precision mediump float;
  varying vec2 v_texCoord;
  uniform sampler2D u_tex;
  void main() {
    gl_FragColor = texture2D(u_tex, v_texCoord);
  }
  `;

  const m4 = twgl.m4;
  const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
  const bufferInfo = twgl.primitives.createCubeBufferInfo(gl, 2);
  const uniforms = {
    u_tex: tex,
  };

  function render(time) {
    time *= 0.001;
    twgl.resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.enable(gl.DEPTH_TEST);

    const fov = 30 * Math.PI / 180;
    const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
    const zNear = 0.5;
    const zFar = 10;
    const projection = m4.perspective(fov, aspect, zNear, zFar);
    const eye = [1, 4, -6];
    const target = [0, 0, 0];
    const up = [0, 1, 0];

    const camera = m4.lookAt(eye, target, up);
    const view = m4.inverse(camera);
    const viewProjection = m4.multiply(projection, view);
    const world = m4.rotationY(time);

    uniforms.u_worldViewProjection = m4.multiply(viewProjection, world);

    gl.useProgram(programInfo.program);
    twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
    twgl.setUniforms(programInfo, uniforms);
    gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>

很遗憾,截止到 2018 年 8 月,此功能仅在 Chrome 中可用。Firefox 的 bug 在这里。其他浏览器我不清楚。


非常感谢@gman提供的详细答案。这么重要的内容甚至在WebGL规范中都没有提到,也没有在我搜索的术语中出现。 - Jeff Saremi
1
默认情况下,async 的定义是不阻塞 JS 执行。无论你如何编写代码,gl.texImage2D 似乎总是会阻塞 JS 代码执行(纹理大小增加时延迟也会增加)。 - HankMoody
1
@HankMoody,所有的东西都会阻塞JS执行,只是一些比其他一些少。 var a = 1会阻塞JS的下一行代码执行。 fetch(url)也会阻塞JS的下一行代码执行。尽管仅有短暂的时间,但仍然被阻塞了。您显然选择了某种定义,在该定义中,阻塞<某个任意量是异步的,而阻塞>则是同步的。但这并不是异步的定义。异步是指操作的某些部分同时进行。调用fetch需要短暂的设置时间,其余部分是并行的。texImage2D需要较长的设置时间,然后其余部分是并行的。 - gman
WEBGL_get_buffer_sub_data_async 在 Chrome 中允许从纹理异步读取数据。 - Jeff Saremi
嗯,它在这里被列为拒绝扩展 https://www.khronos.org/registry/webgl/extensions/。 - gman
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接