v0.7.0: SDL-free wasm worker backend (WebGPU + WebGL2) with on-demand assets

The engine now runs on a plain Web Worker (no SDL, no PROXY_TO_PTHREAD: in a pthread Emscripten proxies every filesystem syscall to the main browser thread, where synchronous XHR - and thus FS.createLazyFile - is forbidden). A page/worker pair transfers an OffscreenCanvas and forwards DOM input; assets load lazily from an embedded manifest, so entering a map fetches only that map and its tiles.

Presentation: ra3.webgpu (WebGPU via Emscripten's emdawnwebgpu port, WGSL shaders, the default on wasm) and ra3.wasmgl (WebGL2, GLSL ES). The legacy SDL ra3.webgl backend is removed.
This commit is contained in:
EnderTheCoder
2026-09-29 12:08:07 +08:00
parent 70d4beca4f
commit 889ce945f2
27 changed files with 2161 additions and 639 deletions
+27 -11
View File
@@ -121,7 +121,7 @@ namespace {
std::puts(" [--3d] [--cam-pitch DEG] [--cam-yaw DEG] [--cam-x U] [--cam-y U]");
std::puts(" [--cam-height U] [--fov DEG] [--width W] [--height H]");
std::puts(" [--fullscreen] [--fps N] (0 = vsync)");
std::puts(" [--vulkan] [--dx11] [--dx12] [--webgl] [--sdl] (preferred display backend)");
std::puts(" [--vulkan] [--dx11] [--dx12] [--webgpu] [--wasmgl] [--sdl] (preferred display backend)");
std::puts("assets: read from <exe_dir>/assets (extracted at build time by the openra3_assets target)");
}
@@ -140,7 +140,8 @@ namespace {
[[nodiscard]] auto display_backend_from_args(const std::vector<std::string> &args) -> ra3::display::backend {
if (has_flag(args, "--dx11") || has_flag(args, "--d3d11")) return ra3::display::backend::d3d11;
if (has_flag(args, "--dx12") || has_flag(args, "--d3d12")) return ra3::display::backend::d3d12;
if (has_flag(args, "--webgl")) return ra3::display::backend::webgl;
if (has_flag(args, "--webgpu")) return ra3::display::backend::webgpu;
if (has_flag(args, "--wasmgl")) return ra3::display::backend::wasmgl;
if (has_flag(args, "--sdl")) return ra3::display::backend::sdl;
return ra3::display::backend::vulkan;
}
@@ -149,7 +150,9 @@ namespace {
auto executable_dir(const char *argv0) -> std::filesystem::path {
std::error_code ec;
const auto path = std::filesystem::absolute(argv0, ec);
return ec ? std::filesystem::current_path() : path.parent_path();
// lexically_normal drops the "." element Emscripten leaves in argv[0]
// ("/./openra3.js" -> "/"), which the wasm FS does not collapse itself.
return ec ? std::filesystem::current_path() : path.parent_path().lexically_normal();
}
auto read_file(const std::filesystem::path &path) -> std::vector<ra3::core::uint8> {
@@ -610,10 +613,12 @@ namespace {
* then the others in turn, so an unavailable backend degrades gracefully.
* Direct3D is Windows-only; on other hosts those entries fall through.
*/
inline constexpr std::array<ra3::display::backend, 5> renderer_backends{ra3::display::backend::vulkan, ra3::display::backend::d3d11,
ra3::display::backend::d3d12, ra3::display::backend::webgl,
ra3::display::backend::sdl};
inline constexpr std::array<std::string_view, 5> renderer_names{"Vulkan", "Direct3D 11", "Direct3D 12", "WebGL", "SDL (software)"};
inline constexpr std::array<ra3::display::backend, 6> renderer_backends{ra3::display::backend::vulkan, ra3::display::backend::d3d11,
ra3::display::backend::d3d12, ra3::display::backend::webgpu,
ra3::display::backend::wasmgl, ra3::display::backend::sdl};
inline constexpr std::array<std::string_view, 6> renderer_names{"Vulkan", "Direct3D 11", "Direct3D 12", "WebGPU", "WebGL (worker)",
"SDL (software)"};
inline constexpr int renderer_count = static_cast<int>(renderer_backends.size());
[[nodiscard]] inline auto renderer_backend(int index) -> ra3::display::backend {
return renderer_backends[static_cast<std::size_t>(std::clamp(index, 0, static_cast<int>(renderer_backends.size()) - 1))];
@@ -698,7 +703,7 @@ namespace {
case 15: return s.fullscreen ? "yes" : "no";
case 16: return std::string{fps_names[static_cast<std::size_t>(std::clamp(s.fps_index, 0, 4))]} + " fps";
case 17: return "start";
case 18: return std::string{renderer_names[static_cast<std::size_t>(std::clamp(s.renderer, 0, 4))]};
case 18: return std::string{renderer_names[static_cast<std::size_t>(std::clamp(s.renderer, 0, renderer_count - 1))]};
default: return "exit";
}
}
@@ -721,7 +726,7 @@ namespace {
case 13: s.thumbnail = !s.thumbnail; break;
case 15: s.fullscreen = !s.fullscreen; break;
case 16: s.fps_index = (s.fps_index + delta + 5) % 5; break;
case 18: s.renderer = (s.renderer + delta + 5) % 5; break;
case 18: s.renderer = (s.renderer + delta + renderer_count) % renderer_count; break;
default: break;
}
}
@@ -1121,8 +1126,9 @@ namespace {
if (has_flag(args, "--vulkan")) st.settings.renderer = 0;
if (has_flag(args, "--dx11") || has_flag(args, "--d3d11")) st.settings.renderer = 1;
if (has_flag(args, "--dx12") || has_flag(args, "--d3d12")) st.settings.renderer = 2;
if (has_flag(args, "--webgl")) st.settings.renderer = 3;
if (has_flag(args, "--sdl")) st.settings.renderer = 4;
if (has_flag(args, "--webgpu")) st.settings.renderer = 3;
if (has_flag(args, "--wasmgl")) st.settings.renderer = 4;
if (has_flag(args, "--sdl")) st.settings.renderer = 5;
if (const auto value = option_value(args, "--width")) st.settings.width = std::stoi(*value);
if (const auto value = option_value(args, "--height")) st.settings.height = std::stoi(*value);
if (const auto value = option_value(args, "--cam-pitch")) st.settings.cam_pitch = std::stof(*value);
@@ -1259,6 +1265,16 @@ auto main(int argc, char **argv) -> int {
const auto exe_dir = executable_dir(argc > 0 ? argv[0] : ".");
const auto assets = exe_dir / "assets";
setup_logging(exe_dir);
#if defined(__EMSCRIPTEN__)
// The wasm build runs on a Web Worker (its runtime's main thread lives
// there), where synchronous XHR is legal, so the asset tree is mounted
// lazily from a manifest instead of being preloaded: the browser fetches
// only the files the engine actually opens. On wasm the executable dir is
// "/", so the manifest sits at /assets.manifest.json and the lazy file URLs
// are "assets/<rel>" relative to the page.
const int mounted = ra3::wasmgl::mount_assets((exe_dir / "assets.manifest.json").string(), assets.string(), "assets/");
ender::log::info(std::format("wasm assets: {} files mounted lazily under {}", mounted, assets.string()));
#endif
if (args.empty()) return command_menu({}, assets);