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
+29 -22
View File
@@ -25,7 +25,8 @@ extracting ore and fighting until one side is wiped out. The balance is the
**retail Red Alert 3 balance**, pinned in `ra3.data` from EA's open RA3 XML:
damage types, `ArmorTemplate` percentages, weapon target masks, build costs and
the ore economy. Presentation offers **Vulkan** (`ra3.vulkan`), **Direct3D 11 / 12**
(`ra3.dx`) and **WebGL 2** (`ra3.webgl`, the wasm build) GPU backends with an SDL
(`ra3.dx`), **WebGPU** (`ra3.webgpu`, the wasm worker backend) and **WebGL 2**
(`ra3.wasmgl`, the SDL-free wasm worker fallback) GPU backends with an SDL
software blit and null fallbacks; the backend is selectable from the in-game menu
and the software renderer still produces headless images. Terrain tiles
cross-fade the way the retail `Terrain.fx` does (a per-cell blend ramp plus a
@@ -96,7 +97,8 @@ built-in test map so the project still builds and runs in CI.
| `src/ui/ra3.ui.*.cppm` | SDL3 window viewer and menu (null backend when SDL3 is absent) |
| `src/vulkan/ra3.vulkan.*.cppm` | Vulkan presentation backend and menu (null fallback without a loader) |
| `src/dx/ra3.dx.*.cppm` | Direct3D 11/12 presentation backend (runtime HLSL; null fallback off Windows) |
| `src/webgl/ra3.webgl.*.cppm` | WebGL2 presentation backend for the wasm build (GLSL ES; null fallback off Emscripten) |
| `src/wasmgl/ra3.wasmgl.*.cppm` | SDL-free wasm backend: engine on a Web Worker, WebGL2 on an OffscreenCanvas, lazy assets (null fallback off Emscripten) |
| `src/webgpu/ra3.webgpu.*.cppm` | WebGPU wasm backend (Dawn `emdawnwebgpu`, WGSL; the default on wasm, falls back to `ra3.wasmgl`) |
| `third_party/libenderlog/` | vendored C++26 module logger (`import ender.log;`, MIT) with a native stack fallback for libc++ |
| `src/ra3.cppm` | umbrella module re-exporting the SDK |
| `apps/openra3/main.cpp` | `menu` / `maps` / `skirmish` / `render` CLI |
@@ -141,7 +143,7 @@ scripts/build-windows.sh
# Windows (arm64) -> build/windows-arm64/bin/openra3.exe + SDL3.dll, .zip (cross)
scripts/build-windows-arm64.sh
# WebAssembly -> build/wasm/bin/openra3.html (+ .js/.wasm)
# WebAssembly -> build/wasm/bin/index.html (+ openra3.js/.wasm, openra3.worker.js)
scripts/build-wasm.sh
```
@@ -183,31 +185,36 @@ runtime, so neither image needs a Vulkan SDK.
## WebAssembly
OpenRA3 also builds to **WebAssembly** with Emscripten (`scripts/build-wasm.sh`),
producing `openra3.html` + `.js` + `.wasm`. The browser gets the same GPU renderer
as the desktop builds: a **`ra3.webgl`** backend peers with Vulkan/Direct3D — SDL3
provides the canvas and input, and GLES 3.0 / WebGL2 runs the 2D blit and the
terrain raymarcher (GLSL ES ports of the desktop shaders).
producing `openra3.js` + `.wasm` plus the `index.html` page and
`openra3.worker.js` bootstrap. The browser gets the same GPU renderer as the
desktop builds: the engine runs on a **Web Worker** (no SDL, no blocking of the
page) and draws into an **OffscreenCanvas** handed over by the page. The default
wasm backend is **`ra3.webgpu`** (WebGPU, WGSL shaders via Emscripten's
`emdawnwebgpu` port); it falls back to **`ra3.wasmgl`** (WebGL2, the GLSL ES
ports of the desktop shaders) when WebGPU is unavailable. The page keeps the DOM
(input, resize) and forwards events to the worker, so the main thread stays
responsive.
Browsers forbid synchronous on-demand file reads on the main thread, so the
assets are **preloaded into the module's filesystem** at build time. Bundle a
compact per-map set (one map plus the tiles it uses) rather than the whole dump:
Because the runtime's main thread lives in the worker, its filesystem is local
to it and assets load **on demand**: point `OPENRA3_WEB_ASSETS` at the extracted
asset tree and the build embeds an `assets.manifest.json`; at startup the worker
registers every listed file as a lazy file, so the browser fetches a file only
when the engine first opens it (entering a map pulls just that map and the tiles
it uses, not the whole tree).
```bash
# 1. which loose TGAs does the map resolve to?
openra3 textures --map map_mp_2_feasel1 # prints paths under assets/terrain
# build (generates the manifest from the extracted assets, embeds it)
OPENRA3_WEB_ASSETS=/path/to/assets scripts/build-wasm.sh
# 2. stage one map + those tiles (+ maps/map_names.tsv) into a directory, then
OPENRA3_WEB_ASSETS=/path/to/that/set scripts/build-wasm.sh
# 3. serve the build output (a Range-capable server is included)
python3 apps/web/serve.py --root build/wasm/bin --port 8199
# open http://localhost:8199/openra3.html
# serve the build output (Range-capable, COOP/COEP; --assets is the lazy tree)
python3 apps/web/serve.py --root build/wasm/bin --assets /path/to/assets
# open http://localhost:8199/index.html
```
The engine's frame loops are blocking; the wasm build yields to the browser via
Asyncify (`display::sleep_frame` calls `emscripten_sleep`), so the page repaints
and handles input. Log records go to `console.log` at their real level (the
console sink uses stdout on the web instead of stderr). Without
The engine's frame loops are blocking; the wasm build yields to the worker's
event loop via Asyncify (`display::sleep_frame` calls `emscripten_sleep`), so
the page repaints and handles input. Log records go to `console.log` at their
real level (the console sink uses stdout on the web instead of stderr). Without
`OPENRA3_WEB_ASSETS` the module still loads and starts, but exits at the menu
because no maps are found.