WebAssembly 用于 Web 上的原生游戏

今年 Web 性能的最大提升是引入了 WebAssembly。WebAssembly 现已在 Firefox 和 Chrome 中可用,并即将在 Edge 和 WebKit 中推出,它使浏览器能够在低级的汇编级别执行代码。

Mozilla 几年来一直在与游戏行业密切合作,才取得了这一进展:包括 2013 年发布的 使用 Emscripten 构建的游戏、2014 年在 Firefox 中预览 虚幻引擎 4、2014 年在 Firefox 中预览 将 Unity 游戏引擎引入 WebGL、2016 年 将一款独立 Unity 游戏导出到 WebVR,以及最近发布的 带有 WebAssembly 的 Firefox 52

WebAssembly 基于 Mozilla 最初的 asm.js 规范,该规范旨在作为 Web 上应用程序和游戏的无插件编译目标方法。这项工作在 Mozilla 积累了大量关于移植游戏和图形技术的知识。如果您是从事游戏开发的工程师,并且对这方面感兴趣,请继续阅读以了解有关在 WebAssembly 中开发游戏的更多信息。

WebAssembly 的作用是什么?

到目前为止,Web 开发者可能已经听说了 WebAssembly 的性能承诺,但对于那些没有实际使用过它的开发者来说,让我们先了解一下它与现有技术是如何协作的以及它的可行性。Lin Clark 撰写了一篇关于 WebAssembly 的优秀入门指南。重点是,与通常由人工编写的 JavaScript 不同,WebAssembly 是一个编译目标,就像本机汇编一样。除了可能的小代码片段外,WebAssembly 不是为人类编写而设计的。通常,您将在源语言(例如 C/C++)中开发应用程序,然后使用编译器(例如 Emscripten),在编译步骤中将源代码转换为 WebAssembly。

这意味着现有 JavaScript 代码不在此模型的范围之内。如果您的应用程序是用 JavaScript 编写的,那么它已经在 Web 浏览器中原生运行,并且无法将其直接转换为 WebAssembly。但是,在这些类型的应用程序中,可以使用 WebAssembly 模块替换 JavaScript 中某些计算密集型部分。例如,Web 应用程序可能会用执行相同任务但性能更好的 WebAssembly 模块替换其用 JavaScript 实现的文件解压缩例程或字符串正则表达式例程。再举一个例子,用 JavaScript 编写的网页可以使用 编译到 WebAssembly 的 Bullet 物理引擎 来提供物理模拟。

另一个重要属性:单个 WebAssembly 指令不会与现有 JavaScript 代码行无缝地交织在一起;WebAssembly 应用程序以模块形式存在。这些模块处理低级内存,而 JavaScript 则操作高级对象表示。这种结构上的差异意味着数据需要经过转换步骤(有时称为“编组”)才能在两种语言表示之间转换。对于整数和浮点数等基本类型,此步骤非常快,但对于字典或图像等更复杂的数据类型,此步骤可能很耗时。因此,用 WebAssembly 模块替换 JavaScript 应用程序的部分代码在应用于具有足够粒度的子例程时效果最佳,这些子例程值得用完整的 WebAssembly 模块替换,从而避免频繁地在语言边界之间切换。

例如,在用 three.js 编写的 3D 游戏中,你不希望单独在 WebAssembly 中实现一个小的矩阵*矩阵乘法算法。将矩阵数据类型编组到 WebAssembly 模块中,然后再返回的成本会抵消在 WebAssembly 中执行该操作所获得的速度性能。相反,要获得性能提升,应该考虑在 WebAssembly 中实现更大规模的计算集合,例如图像或文件解压缩。

另一方面,是尽可能完全用 WebAssembly 实现的应用程序。这最大程度地减少了在语言边界之间编组大量数据的需要,并且应用程序的大部分能够在 WebAssembly 模块内运行。Unity 和虚幻引擎等原生 3D 游戏引擎实现了这种方法,您可以在其中部署整个游戏以在浏览器中的 WebAssembly 中运行。这将产生最佳的性能提升。但是,WebAssembly 并不是 JavaScript 的完全替代品。即使尽可能多地将应用程序用 WebAssembly 实现,仍然有一些部分是用 JavaScript 实现的。WebAssembly 代码不会直接与 Web 开发者熟悉的现有浏览器 API 交互,您的程序将从 WebAssembly 调用 JavaScript 以与浏览器交互。这种行为可能会随着 WebAssembly 的发展而改变。

生成 WebAssembly

目前 WebAssembly 服务的最大用户群是原生 C/C++ 开发者,他们通常负责编写对性能敏感的代码。Emscripten 是一个由 Mozilla 支持的开源社区项目,它是一个与 GCC/Clang 兼容的编译器工具链,允许在 Web 上构建 WebAssembly 应用程序。Emscripten 的主要范围是支持 C/C++ 语言系列,但由于 Emscripten 由 LLVM 提供支持,因此它有可能允许其他语言也进行编译。如果您的游戏是用 C/C++ 开发的,并且它以 OpenGL ES 2 或 3 为目标,那么基于 Emscripten 的 Web 移植可能是一种可行的方法。

Mozilla 从游戏行业的反馈中获益匪浅 - 这推动了 asm.js 和 WebAssembly 的发展。由于这种合作,Unity3D虚幻引擎 4 等其他游戏引擎已经能够将内容部署到 WebAssembly。这种支持在很大程度上是在引擎的内部进行的,目的是让它对应用程序尽可能透明。

移植原生游戏的注意事项

对于游戏开发者来说,WebAssembly 代表了对已经很长的受支持目标平台列表(Windows、Mac、Android、Xbox、Playstation 等)的一个补充,而不是一个从头开始开发项目的全新的平台。正因为如此,我们在 Emscripten、asm.js 和 WebAssembly 的开发过程中非常重视与其他现有平台的功能一致性。这种一致性一直在不断改善,尽管在某些情况下,提供的功能存在明显差异,这通常是由于 Web 安全问题造成的。

本文的其余部分重点介绍了开发人员在开始使用 WebAssembly 时应该注意的最重要的事项。如果您使用的是现有的游戏引擎,那么其中一些事项被成功地隐藏在抽象层之下,但使用 Emscripten 的原生开发者应该最清楚地了解以下主题。

执行模型注意事项

最基本的是代码执行和内存模型方面的差异。

  • Asm.js 和 WebAssembly 使用类型化数组(一个连续的线性内存缓冲区)的概念来表示 应用程序的低级内存地址空间。开发者为这个堆指定一个初始大小,堆的大小可以随着应用程序需要更多内存而增长。
  • 实际上所有 Web API 都使用事件 和事件队列机制来提供通知,例如键盘和鼠标输入、文件 I/O 和网络事件。这些事件都是异步的,并传递给事件处理程序函数。没有用于同步询问“浏览器操作系统”事件的轮询类型 API,例如原生平台经常提供的那些 API。
  • Web 浏览器在浏览器的主线程上执行网页。此属性也适用于 WebAssembly 模块,它们也在主线程上执行,除非显式创建 Web Worker 并在其中运行代码。在主线程上,不允许长时间阻塞执行,因为这也会阻塞浏览器本身的处理。对于 C/C++ 代码,这意味着主线程无法同步运行自己的循环,而必须 基于事件回调向前推进模拟和动画,以便执行定期将控制权交还给浏览器。用户启动的 pthreads 不会有此限制,并且它们允许运行自己的阻塞主循环。
  • 在撰写本文时,WebAssembly 还没有多线程支持 - 此功能目前正在开发中。
  • 与其他平台相比,Web 安全模型可能更加严格。特别是,浏览器 API 限制应用程序直接访问有关系统硬件的低级信息,以减轻生成强指纹识别用户的可能性。例如,无法查询诸如 CPU 型号、本地 IP 地址、RAM 量或可用硬盘空间等信息。此外,许多 Web 功能在 Web 域边界上运行,跨域传播的信息由跨域资源共享规则配置。
  • Web 安全还阻止了一种特殊的编程技术,即动态生成和变异代码。可以在浏览器中生成 WebAssembly 模块,但在加载后,WebAssembly 模块是不可变的,并且无法再向其中添加或更改函数。
  • 在移植 C/C++ 代码时,符合标准的代码应该可以轻松编译,但原生编译器会放宽 x86 上的某些功能,例如未对齐的内存访问、浮点数到整数的溢出强制转换以及通过与函数实际类型不匹配的签名调用函数指针。x86 的普遍性使得这类非标准代码模式在原生代码中比较常见,但在编译到 asm.js 或 WebAssembly 时,这类结构可能会在运行时导致问题。有关可移植代码类型的更多信息,请参阅Emscripten 文档

另一个差异来源是,网页上的代码无法直接访问主机计算机上的原生文件系统,因此提供的文件系统解决方案与原生文件系统略有不同。Emscripten 在网页内部定义了一个虚拟文件系统空间,它以 IndexedDB API 为后盾,可在页面访问之间持久保存。浏览器还会将下载的数据存储在导航缓存中,这在某些情况下是可取的,但在其他情况下则不然。

开发人员应特别注意内容交付。在原生应用程序商店中,预先下载和安装大型应用程序的模式是预期的标准,但在 Web 上,这种单体部署模式可能会导致用户体验不佳。应用程序可以在第一次运行时下载和缓存大型资产包,但这可能会导致巨大的首次下载影响。因此,在最小下载量的情况下启动,并在需要时流式传输其他资产数据对于构建对 Web 友好的用户体验至关重要。

工具链注意事项

开发人员面临的第一个技术挑战是调整现有的构建系统以针对Emscripten 编译器。为了简化此操作,编译器 (emcc & em++) 被设计为 GCC 或 Clang 的直接替换。这简化了现有构建系统的迁移,这些构建系统已经了解 GCC 类工具链。Emscripten 支持流行的 CMake 构建系统配置生成器,并模拟对 GNU Autotools 配置脚本的支持。

有时会混淆的一个事实是,Emscripten 不是一个 x86/ARM -> WebAssembly 代码转换工具链,而是一个交叉编译器。也就是说,Emscripten 不会获取现有的原生 x86/ARM 编译代码并将其转换为在 Web 上运行,而是将 C/C++ 源代码编译为 WebAssembly。这意味着您必须拥有所有可用的源代码(或使用与 Emscripten 捆绑在一起或移植到 Emscripten 的库)。任何依赖于特定于平台(通常是闭源)的原生组件(如 Win32 和 Cocoa API)的代码都无法编译,但需要移植以利用其他解决方案。

性能注意事项

关于 asm.js/WebAssembly 最常被问到的问题之一是,它是否足够快以满足特定目的。奇怪的是,那些尚未尝试过 WebAssembly 的开发人员最常怀疑它的性能。尝试过它的开发人员很少提到性能是一个主要问题。但是,有一些性能注意事项,开发人员应该意识到。

  • 如前所述,多线程目前尚不可用,因此严重依赖线程的应用程序将无法获得相同的性能。
  • WebAssembly 中还没有但已计划的另一个功能是SIMD 指令集支持。
  • 与原生相比,某些指令在 WebAssembly 中可能相对较慢。例如,调用虚函数或函数指针由于沙箱的原因,与原生代码相比具有更高的性能开销。同样,异常处理观察到比原生平台更大的性能影响。性能环境可能看起来有些不同,因此在分析时注意这一点会有所帮助。
  • 众所周知,Web 安全验证会显着影响 WebGL。建议使用 WebGL 的应用程序谨慎优化其 WebGL API 调用,特别是通过避免冗余的 API 调用,这些调用仍然会为驱动程序安全验证支付成本。
  • 最后,应用程序内存使用量是一个特别关键的方面,尤其是在同时针对移动支持的情况下。在首次运行时预加载大型资产包并解压缩大量音频资产是两种已知的内存膨胀来源,很容易意外发生。应用程序在移植时可能需要专门对此进行优化,这也是 WebAssembly 和 Emscripten 运行时的一个活跃的优化领域。

总结

WebAssembly 支持在 Web 上以高性能执行低级代码,类似于 Web 插件以前的方式,只是 Web 安全得到强制执行。对于使用一些超级流行的游戏引擎的开发人员来说,利用 WebAssembly 将与在项目构建菜单中选择新的导出目标一样容易,并且此支持现已提供。对于原生 C/C++ 开发人员,开源 Emscripten 工具链提供了一种可以直接兼容的方式来针对 WebAssembly。围绕 Emscripten 存在一个活跃的开发人员社区,他们为其开发做出贡献,以及一个讨论邮件列表,可以帮助您入门。在 Web 上运行的游戏对每个人都是可访问的,无论他们使用哪种计算平台,都不会影响可移植性、性能或安全性,也不需要预先安装步骤。

WebAssembly 只是为基于 Web 的游戏提供支持的大量 API 集的一部分,因此请访问MDN 游戏部分以了解全局情况。赶快加入,祝您“Emscriptening”愉快!

关于 Jukka Jylänki

Jukka Jylänki 的更多文章…


2 条评论

  1. DYM

    链接到https://emscripten.webassembly.net.cn - 是错误的,没有 `httpS:` - 重定向仅适用于 `http:` atm。

    2017 年 7 月 20 日 下午 12:15

  2. Havi Hoffman [编辑]

    @DYM 感谢您发现这一点,并感谢您仔细阅读。已修复!

    2017 年 7 月 20 日 下午 3:58

本文的评论已关闭。