WebGL Insights - Automated Testing of WebGL Applications

最近看到「WebGL Insights」中的第4篇文章,其中有一节「Automated Testing of WebGL Applications」。 仔细想想,JS 中的自动测试框架有很多,ava,Karma 等等,e2e 也有 nightwatch。 但是对于 WebGL 程序应该如何测试呢?

首先想到对于 JS 文件中使用的纯算法类代码,是完全可以通过单元测试完成的。难点在于渲染类的功能代码,尤其是 shader 中的代码运行在 GPU 中,JS 单元测试库可完全不能用。

运行环境

首先完全 Mock 一个 WebGL 环境是不现实的,我们必须让 WebGL 代码跑在浏览器环境中得到真实的结果。

electron

在持续集成自动化测试中,可以通过 electron 启动一个浏览器窗口。在 Medium 一篇文章中就介绍了这种做法。

const win = new BrowserWindow({ show: false });
server({ electronWin: win });
// 载入包含 WebGL 代码的页面
win.loadURL(`http://localhost:${port}`);

文中还提供了完整的 Docker 镜像,十分贴心。

headless-gl

相比 electron 的启动速度,headless-gl由于只实现了 WebGL 相关的功能,无需启动整个浏览器窗口,因此速度具有优势。

在 WebGL 的实现上,通过 node-gyp 实现了在 Node 环境运行。之前在「编译 Node.js 可执行文件」一文中简单介绍过一下:

var nativeGL = require('bindings')('webgl')
var gl = nativeGL.WebGLRenderingContext.prototype

接下来的问题是,如何判断渲染结果的正确性呢?

判别方法

对于页面渲染效果方面,如果是组件相关的单元测试,可以使用 Karma 启动 Chrome 然后通过 DOM API 判断某些组件是否正确渲染完成。 而如果需要 e2e 的完整验证,可以使用 nightwatch 提供的基本断言以及包含了更丰富浏览器 API 的 WebDriver 协议扩展。 但是对于使用 WebGL 渲染的内容,这些办法似乎都不好使。

对于这种场景,文章中提到了一种非常直观判断方法。那就是通过和正确结果截图进行比较,来判断本次渲染结果是否正确。 虽然这种方法存在明显的局限性,就算比对失败,也很难查找错误原因,但是目前似乎也没有更好的方法了。

对于简单场景,作者在文中提到了实践中使用另一种方法,用于简单判断渲染没有失败。 方法本身很直接,就判断一个像素点。

it('renders', function() {
    var gl = createContext();
    setupCamera(gl);
    drawPolygon(gl);

    var pixels = new Uint8Array(4);
    gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
    expect(pixels).not.toEqual([0, 0, 0, 0]);
    destroyContext(context);
});

对于 shader 中的代码,也可以借鉴之前的方式,如果符合预期就输出某个特定颜色。 然后继续通过 gl.readPixels 进行验证:

void main() {
    mat2 m = mat2(1.0, 2.0, 3.0, 4.0);
    mat2 mt = mat2(1.0, 3.0, 2.0, 4.0);
    gl_FragColor = vec4(czm_transpose(m) == mt);
}

不过这种方式局限性也很明显,一个测试用例只能判断一种情况,对于复杂情况就无能为力了,毕竟只能输出一种颜色。

文中还提到了 glsl-unit,不过并没有展开。乍一看颇有 JUnit 的架势,但是搜索一番会发现,原本 Google Code 上的代码迁移到了 GitHub 上,不过页面中介绍赫然写着:

Automatically exported from code.google.com/p/glsl-unit - DONT USE FOR DEVELOPMENT

访问项目主页也显示 Python 2.5 不可用之类的信息,看来已经年久失修了。缺乏文档和用例,实在是没法使用了。

性能测试

之前总结过一篇文章,关于 RAIL 性能评估模型。在 JS 中可以通过打点或者 performance API 的方式计算某个方法或是页面渲染某些特定阶段的执行时间。那么在 WebGL 中也能这样使用吗?

在回答这个问题之前,可以先来了解一下一个 WebGL 程序运行时依赖的各个软件组件。 「Professional WebGL Programming: Developing 3D Graphics for the Web」书中在第八章中给出了相关的示意图:

简单概括下,有点类似知乎上常见的“地址栏输入 URL 后经历了哪些步骤”这样的问题:

  1. URL 通过浏览器传入 WebKit 内核,使用 HTTP 栈创建对于目标网页的请求
  2. HTTP 请求经过 TCP/IP 封装,通过网络层发送
  3. 服务端响应请求,返回页面 HTML
  4. 经过网络层,TCP/IP 和 HTTP 解包到达 WebKit 内核,开始构建 DOM 树
  5. HTML 中包含的 JS 代码交给 V8 引擎执行,V8 编译成机器码在 CPU 上运行
  6. 如果这些 JS 代码中包含了对于 WebGL API 的调用,V8 返回给 WebKit,由 WebKit 调用 OpenGL ES 2.0 API。其中 shader 代码由 OpenGL ES 2.0 Lib 编译成二进制代码通过 kernal GPU driver 上传给 GPU
  7. texture,vertex buffer 等等一切准备就绪,GPU 开始渲染

了解了整个流程,就知道之前用于 JS 的打点方式不可行,因为对于 WebGL API 的调用不是立刻执行的:

let t0 = window.performance.now();
gl.drawElements(gl.TRIANGLE_STRIP, ...); 
let t1 = window.performance.now();

gl.finish

对于 gl.finish() MDN 中这样介绍:

The WebGLRenderingContext.finish() method of the WebGL API blocks execution until all previously called commands are finished.

这意味着这个方法会阻塞主线程,所以我们能够这样使用:

let t0 = window.performance.now();
gl.drawElements(gl.TRIANGLE_STRIP, ...); 
gl.finish()
let t1 = window.performance.now();

但是 GPU 出于性能考虑,在具体实现中并不一定会尊重这种阻塞行为。我自己试了一下,加了之后也确实和前一种并无差别。

WebGLSync

在 StackExchange 上有一个回答 提供了另一种方法,使用 WebGL2 中的 WebGLSync 对象。从 CanIUse WebGL2 可见目前只有高版本 Chrome 才支持。

首先使用 fenceSync 设置一个 WebGLSync 对象,加入 GL 命令队列中。

The WebGL2RenderingContext.fenceSync() method of the WebGL 2 API creates a new WebGLSync object and inserts it into the GL command stream.

使用 getSyncParameter 可以检查对象的状态,如果在设置后立即检查,显然是 gl.UNSIGNALED 状态。

// 想检测的执行过程
gl.drawElements(gl.TRIANGLE_STRIP, ...); 
let sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
let signaled = gl.getSyncParameter(sync, gl.SYNC_STATUS);

不必我们手动去轮询 Sync 对象的状态,clientWaitSync 可以阻塞主线程,直至 Sync 对象状态改变或者达到超时时间。

The WebGL2RenderingContext.clientWaitSync() method of the WebGL 2 API blocks and waits for a WebGLSync object to become signaled or a given timeout to be passed.

let status = gl.clientWaitSync(sync, 0, 0);

在实际测试时,如果设置了大于 0 的超时时间,Chrome 会有一个警告。这点非常奇怪,上面的回答中提问者也提到了这一点:

WebGL: INVALID_OPERATION: clientWaitSync: timeout > MAX_CLIENT_WAIT_TIMEOUT_WEBGL

完整的例子可以参考:

See the Pen measure perf by xiaop (@xiaoiver) on CodePen.

参考资料