WebGL 不支持 binary shader,因此单个 shader 在编译、链接和检查链接状态等过程中,GL 函数的执行时间较长,可能需要几十到上百毫秒。这导致在调用 ShaderVariantCollection.WarmUp()
方法时,主线程可能会被阻塞数秒。
然而,在支持 KHR_parallel_shader_compile
扩展的浏览器中,我们可以异步执行 ShaderWarmup
。其工作原理是,在调用 glGetProgramiv
以检查链接状态时,使用 GL_COMPLETION_STATUS_KHR
替代 GL_LINK_STATUS
。GL_COMPLETION_STATUS_KHR
可以立即返回状态结果( TRUE
或 FALSE
),而不必等待链接过程完成。
基于此原理,我们引入了异步接口 ShaderVariantCollection.WarmUpAsync()
。此接口会预先编译和链接着色器变体集合中配置的变体。通过返回的 AsyncOperation
,我们可以获取进度并注册完成事件处理程序。之后,每帧使用 GL_COMPLETION_STATUS_KHR
进行检查,直到所有 program 链接完成,然后触发 completed 事件。与同步接口相比,异步接口的总耗时大致相同,但其优势在于允许主线程在 warmup 期间继续执行其他任务。
示例如下:
public class WarmUpAsyncTest : MonoBehaviour
{
private bool warmupInProcess = false;
private AsyncOperation warmupOp;
private ShaderVariantCollection svc;
void Start()
{
svc = Resources.Load<ShaderVariantCollection>("ShaderVariants");
if (svc)
{
warmupOp = svc.WarmUpAsync();
warmupOp.completed += WarmupOp_completed;
warmupInProcess = true;
}
}
private void WarmupOp_completed(AsyncOperation obj)
{
warmupInProcess = false;
// Enter game scene or do rendering works.
}
void Update()
{
if (warmupInProcess)
{
Debug.Log("----progress: " + warmupOp.progress);
}
}
}
推荐在执行网络请求或数据 IO 等耗时工作之前调用 WarmUpAsync()
,收到 completed 事件后再使用相关 shader 进行渲染工作,例如进入游戏场景。如果在 warmup 完成之前就进行渲染工作,用到的 shader 会退化到同步方式进行编译链接,可能出现卡顿现象。
测试环境: iPad mini 6,A15,iOS 17.5.1,变体集合中包含79个 shader 变体
NoDevelopment 模式下: WarmUpAsync 总耗时:3599ms,主线程阻塞时间:125ms,WarmUpSync 总耗时:4173ms,主线程阻塞时间4173ms。
Development 模式下: 异步方式时,CreateGpuProgram 函数很快返回,主线程只阻塞76ms,之后就可以处理其他工作。
同步方式时,有些 CreateGpuProgram 函数单次调用就需要181ms,主线程阻塞4633ms之后,才能处理其他工作。