← 返回信息流
AI 资讯Hacker News·2 天前

将C语言游戏移植到WASM时遇到的所有Bug

原标题:Ported my C game to WASM, here's everybug that I hit

速览

本文详细记录了将C语言开发的游戏移植到WebAssembly(WASM)环境时的完整过程。作者列举了在移植过程中遇到的每一个技术障碍和Bug,并提供了相应的解决方案。这些经验对于希望优化Web端游戏性能或探索WASM应用的开发者具有重要参考价值。

AI 深度解读

将 C 语言游戏移植到 WASM:我遇到的每一个 Bug 及解决方案

背景

作者使用纯 C 语言编写了一款名为 Match Morphosis 的游戏,并基于 bgfxSDL2miniaudiocimgui 构建了一个自定义引擎。近期,作者利用 Emscripten 工具链将该游戏移植到了 Web 平台(通过 WASM 运行),目前游戏已在 itch.io 上线。

在移植过程中,作者遇到了一系列非显而易见的技术陷阱和 Bug。这篇文章旨在记录这些坑点及其解决方案,希望能帮助其他开发者避免类似的痛苦。

核心内容

0. 被迫回归 Visual Studio 进行调试

作者日常使用 RemedyBG 作为调试器,但该工具不支持 32 位进程。由于 WASM 运行在 32 位地址空间中,为了在本地复现 Bug,作者必须构建一个 32 位的原生版本,这迫使他重新启用 Visual Studio

  • 无需解决方案文件:可以直接运行 devenv build\main.exe
  • 环境变量设置:在构建前需调用 vcvars32.bat 以设置 32 位编译环境。
  • 调试体验:在 VS 中按 F5 或 F11 可直接运行 exe 进行单步调试和捕获崩溃。虽然不够优雅,但足以解决问题。

1. Web 环境是 32 位的:64 位结构体布局失效

这是大多数 Bug 的根本原因。WASM 使用 32 位地址空间,指针大小为 4 字节而非 8 字节。

  • 问题现象:作者在 64 位 Windows 上直接将包含原始指针的资源结构体序列化到磁盘(pak 文件)。当在 WASM 上加载时,结构体布局完全错位。

    typedef struct AssetSprite {
        u32 width, height;
        u8* dataBytes; // 64位下8字节,WASM下4字节
        i32 dataSize;
    } AssetSprite;
    

    在原生环境中 sizeof(Assets) 为 26328,而在 Web 端为 25556。指针后的所有字段偏移量错误,导致纹理和着色器数据变成乱码。

  • 解决方案:将运行时数据与烘焙数据完全分离。不再在资源结构体中保留指针,而是使用侧边扁平数组:

    AssetDataBytes assetData[TOTAL_ASSET_COUNT];
    i32 assetDataId;
    typedef struct AssetDataBytes {
        u8* data;
        i32 size;
    } AssetDataBytes;
    

    在烘焙时增加新资源只需递增 assetDataId 并写入字节。序列化后的资源结构体不再包含指针,从而保证 32 位和 64 位下的布局一致。

2. 在 32 位原生环境中调试,而非浏览器

这是最大的生产力提升点。由于 32 位原生环境与 WASM 具有相同的结构体大小,仅在 Web 端出现的 Bug 也会在 32 位原生环境中复现。

  • 优势:在 32 位原生环境中,开发者可以使用真实的断点、内存监视器和调用栈。
  • 工具组合:结合编译器标志 /fsanitize=address (ASan) 和数据断点。ASan 能捕获错误的内存访问,数据断点能精准定位是谁写入了该地址。这能将数小时的排查过程缩短为快速解决。切勿仅依赖浏览器控制台调试 WASM 崩溃,那既痛苦又缓慢。

3. 64 位下“静默正确”的 Bug

存在一个在 64 位下因巧合而正确的经典错误:

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)。因此错误代码分配了正确的内存量。
  • WASM 表现:在 32 位 WASM 中,sizeof(ThingHandle*) 为 4 字节,导致分配的内存仅为所需的一半,进而破坏后续内存数据。

4. OpenGL ES (WebGL) 比 Direct3D 严格得多

bgfx 在 Windows 上使用 Direct3D,而在 Web 上使用 OpenGL ES。许多在 D3D 上能蒙混过关的操作在 WebGL 上会直接崩溃:

  • 顶点布局渲染器类型:作者曾传递 BGFX_RENDERER_TYPE_NOOPbgfx_vertex_layout_begin。这在 D3D 上有效,但在 OpenGL 上因无法分配正确的属性位置而失效。应改用 bgfx_get_renderer_type()
  • 组件数量不匹配:顶点布局中声明 COLOR1 为 2 个组件,但着色器使用 vec4。D3D 忽略此不匹配,而 OpenGL ES 每帧都会抛出致命错误。组件数量必须与着色器声明完全一致。
  • Framebuffer Y 轴翻转:OpenGL 的 Y=0 在底部,D3D 的 Y=0 在顶部。全屏 Blit 在 Web 上是倒置的。通过在最终渲染目标纹理 Blit 时翻转 UV V 坐标解决。

5. 着色器需要针对 GLSL ES 重新编译

bgfxshaderc 针对特定后端编译着色器。作者的着色器原为编译为 DirectX 的 HLSL,在 Web 端需要转换为 GLSL ES,编译标志需从 -p s_5_0 改为 -p 300_es

  • 陷阱 1lerp() 是 HLSL 特有函数,GLSL 使用 mix()bgfxbgfx_shader.sh 已定义 mix 为跨平台宏,应统一使用宏。
  • 陷阱 2GLSL ES 对整数和浮点数非常严格。向浮点参数传递 01 会导致编译错误,必须使用 0.01.0

6. Web Audio 自动播放策略与 Emscripten 导出问题

  • 自动播放策略:Google 浏览器策略禁止在没有用户交互的情况下自动输出媒体。miniaudio 内部通过注册 clicktouchend 监听器来自动恢复 AudioContext
  • Emscripten 导出缺失:作者花费大量时间调试 miniaudio 的 Web 构建,尝试了 AUDIO_WORKLETWASM_WORKERSASYNCIFY 等标志,甚至尝试区分 Web 和本地的初始化路径,但均无效。
  • 根本原因:较新版本的 Emscripten 默认移除了某些运行时导出。miniaudio 需要从 JS 侧访问 HEAPF32,但默认未导出。
  • 解决方案:显式添加导出标志:
    -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','HEAPF32']"
    
    这解决了 JS 控制台的初始化错误。

关键要点

  • 指针大小差异WASM 是 32 位的,序列化包含指针的结构体时会导致布局错位。解决方案是分离数据,使用索引或扁平数组代替直接指针。
  • 调试策略:优先在 32 位原生环境中使用 **
查看原文 →ernesernesto.github.io