将我的C游戏移植到WASM,这是我遇到的所有Bug

Hacker News Top 工具

摘要

一位开发者分享了将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 感谢阅读!

相似文章

Theseus: 将 win32 翻译为 wasm

Lobsters Hottest

将 Windows 可执行文件 (win32/x86) 翻译为 WebAssembly 以在浏览器中运行,讨论诸如阻塞与异步设计等挑战。

将WINE移植到新爱好操作系统

Lobsters Hottest

详细记录了将Wine移植到Astral爱好操作系统的过程,通过WoW64实现了32位Windows应用的运行,并解决了OpenGL/EGL依赖问题,从而能够运行Cogmind等游戏。

这个周末你打算做什么?

Lobsters Hottest

一位开发者描述了将《完美黑暗64》关卡移植到 noclip.website 的过程,强调了读取 N64 显示列表和重新实现渲染引擎的挑战。