将我的C游戏移植到WASM,这是我遇到的所有Bug
摘要
一位开发者分享了将C游戏移植到WebAssembly的经验,详细介绍了因32位与64位差异遇到的Bug,并提供了调试技巧。
暂无内容
查看缓存全文
缓存时间: 2026/06/15 11:57
## 将 Match Morphosis 移植到 WASM | ernesernesto io
来源:http://ernesernesto.github.io/writes/portingmatchmorphosistowasm/
我用纯 C 语言和自研引擎(bgfx、SDL2、miniaudio、cimgui)写了一个游戏,最近通过 Emscripten 将其移植到了 Web 端。现在它已经在 itch.io 上线了。以下是我遇到的所有非显而易见的问题,希望能帮一些人少走弯路。
**0. 不得不回到 Visual Studio。唉。**
我日常用 RemedyBG 作为调试器,它很好用,但不支持 32 位进程。由于 WASM 是 32 位的,我需要一个 32 位的本地构建来复现本地 bug,这意味着又得打开 Visual Studio。
实际上你不需要解决方案文件。只需运行:
``
devenv build\main.exe
``
在构建之前,在你的构建过程中添加 vcvars32:
``
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars32.bat"
``
在 VS 中,直接按 F5 或 F11 就能运行 exe。不需要 sln 文件,就可以单步执行代码并捕获崩溃。虽然不太理想,但能完成任务。
**1. Web 是 32 位的。你的 64 位结构体会出问题。**
这是我大多数 bug 的根本原因。WASM 是 32 位地址空间,指针占 4 字节而不是 8 字节。我之前将包含原始指针的资源结构体直接序列化到磁盘(pak 文件):
``
typedef struct AssetSprite {
u32 width, height;
u8* dataBytes; // 64 位上 8 字节,WASM 上 4 字节
i32 dataSize;
} AssetSprite;
``
当我在 64 位 Windows 上打包资源并在 WASM 上加载时,结构体布局完全不同。`sizeof(Assets)` 在本机上是 26328,在 Web 上是 25556。第一个指针之后的每个字段都偏移错误,导致所有纹理和着色器数据变成垃圾。
事后看来,对经常做跨平台开发的人来说这可能是显而易见的,但我已经好多年没构建 32 位程序了,所以指针大小问题让我措手不及。
修复方法:将运行时数据与烘焙数据完全分离。我不再在资源结构体内部放置指针,而是在旁边用一个扁平数组:
``
AssetDataBytes assetData[TOTAL_ASSET_COUNT];
i32 assetDataId;
typedef struct AssetDataBytes {
u8* data;
i32 size;
} AssetDataBytes;
``
每次在烘焙过程中添加新资源时,只需递增 assetDataId 并将字节写入那里。序列化的资源结构体不再包含任何指针,因此在 32 位和 64 位上的布局完全相同。打包器是单线程的,整个游戏仍能在 3 秒内完成,对我的用例来说足够好,因为资源数量相对较少。
**2. 在 32 位本地环境中调试,而不是在浏览器中**
说实话,这是最大的生产力提升。由于 32 位本地环境与 WASM 具有相同的结构体大小,因此只在 Web 上出现的 bug 也会在 32 位本地环境中出现,而我可以使用真正的断点、内存监视和调用堆栈。
为了实际追查 bug,我使用了编译时的 `/fsanitize=address` 结合数据断点。触发 bug,ASan 会捕获非法访问。数据断点会精确告诉你哪个地址被写入了什么内容。这样就把原本可能需要数小时的排查变成了能快速解决的问题。不要试图仅从浏览器控制台调试 WASM 崩溃,因为那既痛苦又慢。
**3. 一个在 64 位上悄然正确但实际上有 bug 的问题**
``
typedef struct ThingHandle {
i32 id;
i32 generation;
} ThingHandle;
// 错误
game->boardPieces = swAlloc(sizeof(ThingHandle*) * row * column);
// 正确
game->boardPieces = swAlloc(sizeof(ThingHandle) * row * column);
``
在 64 位上,`sizeof(ThingHandle*)` 是 8,恰好与 `sizeof(ThingHandle)` 相同。因此错误的代码恰好分配了正确大小的内存,并在很长一段时间内正常工作。在 32 位的 WASM 上,`sizeof(ThingHandle*)` 是 4,所以它分配的内存只有所需的一半,并破坏了之后的所有内容。这是一个相当经典的低级错误,只是由于 64 位让它们意外相等而被隐藏了很久。
**4. OpenGL ES(WebGL)比 Direct3D 严格得多**
bgfx 在 Windows 上使用 Direct3D,在 Web 上使用 OpenGL ES。很多在 D3D 上没问题的事情在 WebGL 上就崩溃了:
**顶点布局渲染器类型:** 我在 `bgfx_vertex_layout_begin` 中传递了 `BGFX_RENDERER_TYPE_NOOP`。这在 D3D 上可以工作,但在 OpenGL 上则不行,因为它无法正确分配属性位置。请改用 `bgfx_get_renderer_type()`。
**组件数量不匹配:** 我在布局中将 COLOR1 声明为 2 个组件,但着色器使用了 vec4。D3D 忽略了这个不匹配,而 OpenGL ES 每帧都会报致命错误。组件数量必须与着色器声明的完全一致。
**帧缓冲 Y 轴翻转:** OpenGL 的 Y=0 在底部,D3D 的 Y=0 在顶部。我的全屏 blit 在 Web 上上下颠倒了。修复方法是在最终渲染目标纹理 blit 时翻转 UV 的 V 坐标。
**5. 着色器需要为 GLSL ES 重新编译**
bgfx 的 shaderc 为特定后端编译着色器。我的着色器是 HLSL 格式,为 DirectX 编译。在 Web 上我需要 GLSL ES,编译标志从 `-p s_5_0` 改为 `-p 300_es`。
有两件事让我困惑:
- `lerp()` 仅适用于 HLSL。GLSL 使用 `mix()`。bgfx 的 `bgfx_shader.sh` 已经将 `mix` 定义为跨平台宏,所以只要到处都用它,两个平台都能正常工作。
- GLSL ES 对整型和浮点型要求严格。将 0 或 1 传递给浮点参数是编译错误。必须写成 0.0 和 1.0。
**6. Web Audio 自动播放 + 一个奇怪的 Emscripten 导出问题**
Google 浏览器实施了一项政策,禁止在没有用户输入的情况下自动播放媒体。miniaudio 通过在内部注册 click 和 touchend 监听器来自动恢复 AudioContext 来解决这个问题。我花了很多时间尝试让 miniaudio 的 Web 构建工作正常,摆弄了许多它的标志:AUDIO_WORKLET、WASM_WORKERS、ASYNCIFY。甚至尝试在 Web 和本地之间使用不同的初始化路径,在第一次触摸后初始化 Web,但它仍然不工作,初始化 AudioContext 时 JS 控制台仍然会报错。
原来,较新版本的 Emscripten 似乎默认移除了某些运行时导出。miniaudio 需要从 JS 端访问 `HEAPF32`,但它不存在。我必须显式添加它:
``
-s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','HEAPF32']"
``
不确定这是新版本 Emscripten 的行为还是我的标志组合问题,我在 Google 上找不到相关信息,但这可能会帮某人省下一个小时的挠头时间。总的来说,miniaudio 确实能完成任务,在本地和 Web 之间不需要做不同的初始化。
**最后感想**
我对最终结果非常满意。我花了一个周末做这个移植,原本以为要更久。用纯 C 编写自研引擎,移植到 Web,游戏加载快、立即能玩,没有 Unity 或 Godot 的负担,这种感觉真的很好。
Emscripten 工具链很稳定。大部分痛苦来自于那些在 Windows 上碰巧能工作、但在 Web 上会被追究的事情。一旦你知道要检查什么,修复它们就非常直接了。
游戏已上线:https://zhongda8.itch.io/matchmorphosis
你也可以加入心愿单:https://store.steampowered.com/app/4131100/Match_Morphosis
感谢阅读!
相似文章
在 Chrome DevTools 中调试 WASM
关于使用 Chrome DevTools 调试 WebAssembly 代码的指南,包括设置断点和捕获异常。
Theseus: 将 win32 翻译为 wasm
将 Windows 可执行文件 (win32/x86) 翻译为 WebAssembly 以在浏览器中运行,讨论诸如阻塞与异步设计等挑战。
WATaBoy:将Game Boy指令即时编译为Wasm,性能超越原生解释器
本文介绍了WATaBoy,一个Game Boy模拟器,它使用即时编译到WebAssembly的方式,实现了超越原生解释器的性能,是JIT到Wasm在模拟领域的一个概念验证。
将WINE移植到新爱好操作系统
详细记录了将Wine移植到Astral爱好操作系统的过程,通过WoW64实现了32位Windows应用的运行,并解决了OpenGL/EGL依赖问题,从而能够运行Cogmind等游戏。
这个周末你打算做什么?
一位开发者描述了将《完美黑暗64》关卡移植到 noclip.website 的过程,强调了读取 N64 显示列表和重新实现渲染引擎的挑战。