WebGL 能否跟不上 requestAnimationFrame?
Can WebGL fail to keep up with requestAnimationFrame?
假设我有一个这样的 WebGL 循环:
function draw(timestamp)
{
// ...
gl.drawArrays(...);
window.requestAnimationFrame(draw);
}
我怕有可能出现以下问题:
- 浏览器每秒调用我的 requestAnimationFrame 回调“draw”大约 60 次。
- gl.drawArrays returns 立即执行,但底层作业需要 GPU 超过 1/60 秒的时间来执行。
- 我最终使 gl.drawArrays() 调用比 GPU 执行它们的速度更快。
问题:
- 这个问题理论上真的能出现吗?如果是这样,正确的处理方法是什么?如果不是,那为什么?
- “requestAnimationFrame 回调”是否保证在我之前对页面上的所有 canvas 元素完成所有 WebGL 调用之前不会被调用?如果是这样,那么它是否记录在规格中?我到处都找不到它。
提前致谢!
不,这个问题不可能发生。
可能应该将其作为重复项关闭,但要知道为什么您应该继续阅读 the browser's event loop
基本上浏览器运行是这样的
const tasks = [];
// loop forever
for (;;) {
// execute all tasks
while (tasks.length) {
const task = tasks.shift();
task();
}
goToSleepUntilThereAreNewTasks();
}
它永远不会 运行s JavaScript 并行(工作人员除外),因此您的 requestAnimationFrame
回调不会在另一个 requestAnimationFrame
回调的中间被调用。
requestAnimationFrame
所做的就是说“下次浏览器将重新绘制屏幕时,将此回调添加到 运行 的任务列表”。
浏览器中的所有其他事情都是一样的。当您加载页面时,会添加一个任务来执行您的脚本。当您添加 setTimeout
时,浏览器将在您设置计时器的多少毫秒内将您的回调添加到任务列表中。当您为 mousemove
或 keydown
或 img.onload
添加事件侦听器时,浏览器只会在鼠标移动或按下某个键或图像完成加载时将任务添加到任务列表。
重要的是 JavaScript 的 POV 没有并行工作。每个任务 运行s 完成,浏览器在当前任务完成 运行ning 之前不会处理任何其他任务。
至于 the specs
中的文档
请注意,如果不以某种方式等待您的 WebGL 绘图调用填充 canvas 的绘图缓冲区,浏览器将无法合成页面。它可以 运行 并行处理,而不是 JavaScript 本身,但是例如执行先前提交的 GPU 命令,而 JavaScript 正在添加新命令或做其他事情,但最终它会阻塞。
从 JavaScript 的 POV 来看,它是 运行 连续的 requestAnimationFrames。发生一个,它请求下一个。从合成器的 POV 来看,这是将所有 DOM 元素绘制到浏览器 window 中的代码,它 运行 与 JavaScript 并行获取最后一个状态DOM 并在浏览器中绘制下一帧。其中一部分是等待 WebGL 中的调用。它不需要像 gl.finish
那样苦苦等待。它只需要以这样一种方式组织 GPU 命令,以便合成 GPU 命令发生在最后一个 WebGL 命令之后。合成器本身在完成当前帧之前不会开始渲染新帧,合成器有效地决定新帧的时间,决定新帧的时间是执行 requestAnimationFrame 事件的触发器。
浏览器充其量会加倍或三倍缓冲,这样当浏览器合成这一帧时,它可以让 JavaScript 开始为下一帧排队命令,这样会有更多的并行性,但同样,它不会继续发出无限的 requestAnimationFrame 回调,而无需等待它生成的帧以一种或另一种方式完成。
所以不,你指出的问题不可能发生。
注意:上面两个分隔符之间的部分最初并不存在。 Kaiido 在评论中指出他们认为我没有回答问题。我以为我含蓄地做了。事件循环的含义是
- 在浏览器决定是时候绘制页面之前,rAF 事件不会执行
- 如果当前正在为当前框架绘制页面,则不会决定再次绘制页面
- 显然,在您通过 WebGL 向 canvas 发出的命令执行完成之前,它无法完成页面绘制。
第 3 步似乎很明显,无需说明。如果浏览器不等待 canvas 将您绘制的图像放入其中,它如何绘制页面?从那以后,其余的就脱离了事件循环的含义。如果当前框架没有完成,为什么浏览器会启动一个新框架?它不会。如果它还没有开始一个新的框架,那么它就不会调用你的 rAF 回调。
我还想澄清一下,这是一个合理的问题。就像我上面提到的,浏览器可能会在忙于合成当前帧时让 JavaScript 开始制作下一帧。如果浏览器确实让 JavaScript 并行启动下一帧并且没有采取任何措施来限制您的问题可能会发生。浏览器将以一种或另一种方式与 GPU 同步,并确保它提交给 GPU 的帧在它获得太多帧(1 或 2 帧)之前已经完成执行。在 OpenGL 术语中,它可以很容易地用 sync objects 做到这一点。我不是说浏览器使用同步对象。只是浏览器可以做类似的事情来确保它不会提前获得太多帧。
我不知道这是否会让事情或多或少变得混乱,但 Kaiido 在聊天讨论中指出,如果你使用 setTimeout
而不是 requestAnimationFrame
那么在某种意义上你会得到你担心的问题。
假设您确实这样做了
function draw(timestamp)
{
// ...
gl.drawArrays(...);
setTimeout(draw);
}
在这种情况下,setTimeout 为 0(或一帧下的任何数字),复合材料之间发出多少 GPU 命令是未知的。也许它在复合材料之间调用 draw
5 次或 500 次。它是未定义的,由浏览器决定。
我要指出的是,在复合材料之间多次通过 setTimeout
调用 draw
并不是“帧”,它只是发出更多 GPU 工作的一种非常模糊的方式。 draw
通过 N setTimeouts 或仅通过简单的 for (let i = 0; i < N; ++i) { draw(); }
循环在复合材料之间被调用 N 次之间没有功能差异。在这两种情况下,在复合材料之间向 GPU 添加了大量工作。 setTimeout
方法的唯一区别是浏览器会为您选择 N
。
这实际上是使用requestAnimationFrame
的要点之一,因为setTimeout
与帧没有任何关系。
假设我有一个这样的 WebGL 循环:
function draw(timestamp)
{
// ...
gl.drawArrays(...);
window.requestAnimationFrame(draw);
}
我怕有可能出现以下问题:
- 浏览器每秒调用我的 requestAnimationFrame 回调“draw”大约 60 次。
- gl.drawArrays returns 立即执行,但底层作业需要 GPU 超过 1/60 秒的时间来执行。
- 我最终使 gl.drawArrays() 调用比 GPU 执行它们的速度更快。
问题:
- 这个问题理论上真的能出现吗?如果是这样,正确的处理方法是什么?如果不是,那为什么?
- “requestAnimationFrame 回调”是否保证在我之前对页面上的所有 canvas 元素完成所有 WebGL 调用之前不会被调用?如果是这样,那么它是否记录在规格中?我到处都找不到它。
提前致谢!
不,这个问题不可能发生。
可能应该将其作为重复项关闭,但要知道为什么您应该继续阅读 the browser's event loop
基本上浏览器运行是这样的
const tasks = [];
// loop forever
for (;;) {
// execute all tasks
while (tasks.length) {
const task = tasks.shift();
task();
}
goToSleepUntilThereAreNewTasks();
}
它永远不会 运行s JavaScript 并行(工作人员除外),因此您的 requestAnimationFrame
回调不会在另一个 requestAnimationFrame
回调的中间被调用。
requestAnimationFrame
所做的就是说“下次浏览器将重新绘制屏幕时,将此回调添加到 运行 的任务列表”。
浏览器中的所有其他事情都是一样的。当您加载页面时,会添加一个任务来执行您的脚本。当您添加 setTimeout
时,浏览器将在您设置计时器的多少毫秒内将您的回调添加到任务列表中。当您为 mousemove
或 keydown
或 img.onload
添加事件侦听器时,浏览器只会在鼠标移动或按下某个键或图像完成加载时将任务添加到任务列表。
重要的是 JavaScript 的 POV 没有并行工作。每个任务 运行s 完成,浏览器在当前任务完成 运行ning 之前不会处理任何其他任务。
至于 the specs
中的文档请注意,如果不以某种方式等待您的 WebGL 绘图调用填充 canvas 的绘图缓冲区,浏览器将无法合成页面。它可以 运行 并行处理,而不是 JavaScript 本身,但是例如执行先前提交的 GPU 命令,而 JavaScript 正在添加新命令或做其他事情,但最终它会阻塞。
从 JavaScript 的 POV 来看,它是 运行 连续的 requestAnimationFrames。发生一个,它请求下一个。从合成器的 POV 来看,这是将所有 DOM 元素绘制到浏览器 window 中的代码,它 运行 与 JavaScript 并行获取最后一个状态DOM 并在浏览器中绘制下一帧。其中一部分是等待 WebGL 中的调用。它不需要像 gl.finish
那样苦苦等待。它只需要以这样一种方式组织 GPU 命令,以便合成 GPU 命令发生在最后一个 WebGL 命令之后。合成器本身在完成当前帧之前不会开始渲染新帧,合成器有效地决定新帧的时间,决定新帧的时间是执行 requestAnimationFrame 事件的触发器。
浏览器充其量会加倍或三倍缓冲,这样当浏览器合成这一帧时,它可以让 JavaScript 开始为下一帧排队命令,这样会有更多的并行性,但同样,它不会继续发出无限的 requestAnimationFrame 回调,而无需等待它生成的帧以一种或另一种方式完成。
所以不,你指出的问题不可能发生。
注意:上面两个分隔符之间的部分最初并不存在。 Kaiido 在评论中指出他们认为我没有回答问题。我以为我含蓄地做了。事件循环的含义是
- 在浏览器决定是时候绘制页面之前,rAF 事件不会执行
- 如果当前正在为当前框架绘制页面,则不会决定再次绘制页面
- 显然,在您通过 WebGL 向 canvas 发出的命令执行完成之前,它无法完成页面绘制。
第 3 步似乎很明显,无需说明。如果浏览器不等待 canvas 将您绘制的图像放入其中,它如何绘制页面?从那以后,其余的就脱离了事件循环的含义。如果当前框架没有完成,为什么浏览器会启动一个新框架?它不会。如果它还没有开始一个新的框架,那么它就不会调用你的 rAF 回调。
我还想澄清一下,这是一个合理的问题。就像我上面提到的,浏览器可能会在忙于合成当前帧时让 JavaScript 开始制作下一帧。如果浏览器确实让 JavaScript 并行启动下一帧并且没有采取任何措施来限制您的问题可能会发生。浏览器将以一种或另一种方式与 GPU 同步,并确保它提交给 GPU 的帧在它获得太多帧(1 或 2 帧)之前已经完成执行。在 OpenGL 术语中,它可以很容易地用 sync objects 做到这一点。我不是说浏览器使用同步对象。只是浏览器可以做类似的事情来确保它不会提前获得太多帧。
我不知道这是否会让事情或多或少变得混乱,但 Kaiido 在聊天讨论中指出,如果你使用 setTimeout
而不是 requestAnimationFrame
那么在某种意义上你会得到你担心的问题。
假设您确实这样做了
function draw(timestamp)
{
// ...
gl.drawArrays(...);
setTimeout(draw);
}
在这种情况下,setTimeout 为 0(或一帧下的任何数字),复合材料之间发出多少 GPU 命令是未知的。也许它在复合材料之间调用 draw
5 次或 500 次。它是未定义的,由浏览器决定。
我要指出的是,在复合材料之间多次通过 setTimeout
调用 draw
并不是“帧”,它只是发出更多 GPU 工作的一种非常模糊的方式。 draw
通过 N setTimeouts 或仅通过简单的 for (let i = 0; i < N; ++i) { draw(); }
循环在复合材料之间被调用 N 次之间没有功能差异。在这两种情况下,在复合材料之间向 GPU 添加了大量工作。 setTimeout
方法的唯一区别是浏览器会为您选择 N
。
这实际上是使用requestAnimationFrame
的要点之一,因为setTimeout
与帧没有任何关系。