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:
+112
-41
@@ -57,11 +57,9 @@ set(OPENRA3_HAS_SDL3 OFF)
|
||||
# (`<root>/<triple>/{include,lib,bin}`); x86_64 unless a toolchain overrides it.
|
||||
set(OPENRA3_SDL3_MINGW_TRIPLE "x86_64-w64-mingw32" CACHE STRING "SDL3 MinGW triple subdirectory")
|
||||
if(EMSCRIPTEN)
|
||||
# Emscripten's SDL3 web port provides the headers and the (static) library
|
||||
# via the `-sUSE_SDL=3` flag, on every target that uses SDL.
|
||||
target_compile_options(openra3_sdl3 INTERFACE -sUSE_SDL=3)
|
||||
target_link_options(openra3_sdl3 INTERFACE -sUSE_SDL=3)
|
||||
set(OPENRA3_HAS_SDL3 ON)
|
||||
# The wasm build runs the engine on a Web Worker and renders into an
|
||||
# OffscreenCanvas (ra3.webgpu / ra3.wasmgl), so it links no SDL: SDL3's
|
||||
# Emscripten port is main-thread DOM only and cannot run in a worker.
|
||||
elseif(OPENRA3_SDL3_ROOT)
|
||||
target_include_directories(openra3_sdl3 INTERFACE "${OPENRA3_SDL3_ROOT}/${OPENRA3_SDL3_MINGW_TRIPLE}/include")
|
||||
target_link_libraries(openra3_sdl3 INTERFACE "${OPENRA3_SDL3_ROOT}/${OPENRA3_SDL3_MINGW_TRIPLE}/lib/libSDL3.dll.a")
|
||||
@@ -169,6 +167,25 @@ function(openra3_embed_glsl out_header)
|
||||
file(WRITE "${out_header}" "${content}")
|
||||
endfunction()
|
||||
|
||||
# Embed the WGSL sources for the WebGPU backend as string literals (compiled at
|
||||
# runtime by the browser's WebGPU, like the GLSL/HLSL for the other backends).
|
||||
function(openra3_embed_wgsl out_header)
|
||||
set(content "// Generated by CMake from shaders/*.wgsl.\n")
|
||||
string(APPEND content "#pragma once\n\nnamespace ra3_shaders {\n")
|
||||
foreach(src IN LISTS ARGN)
|
||||
if(NOT EXISTS "${src}")
|
||||
message(FATAL_ERROR "Missing WGSL shader ${src}")
|
||||
endif()
|
||||
get_filename_component(base "${src}" NAME_WE)
|
||||
string(MAKE_C_IDENTIFIER "${base}" ident)
|
||||
string(APPEND ident "_wgsl")
|
||||
file(READ "${src}" text)
|
||||
string(APPEND content "inline constexpr const char ${ident}[] = R\"RA3WGSL(${text})RA3WGSL\";\n\n")
|
||||
endforeach()
|
||||
string(APPEND content "} // namespace ra3_shaders\n")
|
||||
file(WRITE "${out_header}" "${content}")
|
||||
endfunction()
|
||||
|
||||
# --- logging (vendored libenderlog, MIT) -------------------------------------
|
||||
# A standalone C++26 module logger (`import ender.log;`) with a per-run
|
||||
# archiving file sink. libc++ has no <stacktrace>, so on this toolchain the
|
||||
@@ -309,23 +326,20 @@ else()
|
||||
endif()
|
||||
openra3_target_defaults(ra3_dx)
|
||||
|
||||
# --- WebGL viewer (Emscripten/WebAssembly, or null fallback) ------------------
|
||||
# A peer of the Vulkan/Direct3D backends; built only for the wasm target, where
|
||||
# it uses an SDL3 canvas + GLES 3.0 (WebGL2). Elsewhere it is the null stub.
|
||||
option(OPENRA3_WEBGL "Build the WebGL viewer (Emscripten)" ON)
|
||||
set(OPENRA3_HAS_WEBGL OFF)
|
||||
if(OPENRA3_WEBGL AND EMSCRIPTEN)
|
||||
set(OPENRA3_HAS_WEBGL ON)
|
||||
# --- wasm worker viewer (SDL-free OffscreenCanvas + WebGL2, or null fallback) --
|
||||
# The default wasm backend: the engine runs on a plain Web Worker and renders
|
||||
# into an OffscreenCanvas transferred from the page, with no SDL. Non-wasm builds
|
||||
# link the null stub (init fails, so desktop backends are unaffected).
|
||||
option(OPENRA3_WASMGL "Build the SDL-free wasm worker viewer (Emscripten)" ON)
|
||||
set(OPENRA3_HAS_WASMGL OFF)
|
||||
if(OPENRA3_WASMGL AND EMSCRIPTEN)
|
||||
set(OPENRA3_HAS_WASMGL ON)
|
||||
endif()
|
||||
if(OPENRA3_HAS_WEBGL)
|
||||
message(STATUS "OpenRA3: WebGL viewer enabled")
|
||||
else()
|
||||
message(STATUS "OpenRA3: WebGL viewer disabled - null backend")
|
||||
endif()
|
||||
add_library(ra3_webgl STATIC)
|
||||
if(OPENRA3_HAS_WEBGL)
|
||||
add_library(ra3_wasmgl STATIC)
|
||||
if(OPENRA3_HAS_WASMGL)
|
||||
message(STATUS "OpenRA3: SDL-free wasm worker viewer enabled")
|
||||
openra3_embed_glsl(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated/glsl_embedded.hpp"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated/wasmgl_glsl_embedded.hpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgl_scene_vert.glsl"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgl_scene_frag.glsl"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgl_terrain_vert.glsl"
|
||||
@@ -336,28 +350,68 @@ if(OPENRA3_HAS_WEBGL)
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgl_scene_frag.glsl"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgl_terrain_vert.glsl"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgl_terrain_frag.glsl")
|
||||
target_sources(ra3_webgl PUBLIC FILE_SET CXX_MODULES FILES src/webgl/ra3.webgl.cppm)
|
||||
target_include_directories(ra3_webgl PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||
target_link_libraries(ra3_webgl PUBLIC ra3_core ra3_render ra3_terrain ra3_client ra3_enderlog openra3_sdl3)
|
||||
target_sources(ra3_wasmgl PUBLIC FILE_SET CXX_MODULES FILES src/wasmgl/ra3.wasmgl.cppm)
|
||||
target_include_directories(ra3_wasmgl PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||
target_link_libraries(ra3_wasmgl PUBLIC ra3_core ra3_render ra3_terrain ra3_client ra3_enderlog)
|
||||
# Target WebGL2 so GLES3-only entry points (glTexImage3D, VAOs, ...) link.
|
||||
target_link_options(ra3_webgl PUBLIC -sMIN_WEBGL_VERSION=2 -sMAX_WEBGL_VERSION=2)
|
||||
target_link_options(ra3_wasmgl PUBLIC -sMIN_WEBGL_VERSION=2 -sMAX_WEBGL_VERSION=2)
|
||||
else()
|
||||
target_sources(ra3_webgl PUBLIC FILE_SET CXX_MODULES FILES src/webgl/ra3.webgl.null.cppm)
|
||||
target_link_libraries(ra3_webgl PUBLIC ra3_core ra3_render ra3_client)
|
||||
message(STATUS "OpenRA3: SDL-free wasm worker viewer disabled (non-wasm) - null backend")
|
||||
target_sources(ra3_wasmgl PUBLIC FILE_SET CXX_MODULES FILES src/wasmgl/ra3.wasmgl.null.cppm)
|
||||
target_link_libraries(ra3_wasmgl PUBLIC ra3_core ra3_render ra3_client)
|
||||
endif()
|
||||
openra3_target_defaults(ra3_webgl)
|
||||
openra3_target_defaults(ra3_wasmgl)
|
||||
|
||||
# --- wasm WebGPU viewer (Dawn emdawnwebgpu, or null fallback) ------------------
|
||||
# A peer of ra3.wasmgl in the same Web Worker; web WebGPU is WGSL-only, so it has
|
||||
# its own WGSL shaders (the Vulkan SPIR-V blobs cannot be used on the web). The
|
||||
# `--use-port=emdawnwebgpu` port is downloaded/cached by Emscripten and linked
|
||||
# into the final module.
|
||||
option(OPENRA3_WEBGPU "Build the WebGPU wasm viewer (Emscripten emdawnwebgpu)" ON)
|
||||
set(OPENRA3_HAS_WEBGPU OFF)
|
||||
if(OPENRA3_WEBGPU AND EMSCRIPTEN)
|
||||
set(OPENRA3_HAS_WEBGPU ON)
|
||||
endif()
|
||||
add_library(ra3_webgpu STATIC)
|
||||
if(OPENRA3_HAS_WEBGPU)
|
||||
message(STATUS "OpenRA3: WebGPU wasm viewer enabled (emdawnwebgpu)")
|
||||
openra3_embed_wgsl(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated/webgpu_wgsl_embedded.hpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgpu_scene.wgsl"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgpu_terrain.wgsl"
|
||||
)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgpu_scene.wgsl"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgpu_terrain.wgsl")
|
||||
target_sources(ra3_webgpu PUBLIC FILE_SET CXX_MODULES FILES src/webgpu/ra3.webgpu.cppm)
|
||||
target_include_directories(ra3_webgpu PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||
# CMake's module dependency scanner does not process `--use-port`, so point
|
||||
# the include path at the (image-baked) port package explicitly.
|
||||
set(OPENRA3_EMDAWNWEBGPU_DIR "$ENV{EMSDK}/upstream/emscripten/cache/ports/emdawnwebgpu/emdawnwebgpu_pkg")
|
||||
target_include_directories(ra3_webgpu SYSTEM PRIVATE
|
||||
"${OPENRA3_EMDAWNWEBGPU_DIR}/webgpu_cpp/include"
|
||||
"${OPENRA3_EMDAWNWEBGPU_DIR}/webgpu/include")
|
||||
target_compile_options(ra3_webgpu PUBLIC --use-port=emdawnwebgpu)
|
||||
target_link_options(ra3_webgpu PUBLIC --use-port=emdawnwebgpu)
|
||||
target_link_libraries(ra3_webgpu PUBLIC ra3_core ra3_render ra3_terrain ra3_client ra3_wasmgl ra3_enderlog)
|
||||
else()
|
||||
message(STATUS "OpenRA3: WebGPU wasm viewer disabled (non-wasm) - null backend")
|
||||
target_sources(ra3_webgpu PUBLIC FILE_SET CXX_MODULES FILES src/webgpu/ra3.webgpu.null.cppm)
|
||||
target_link_libraries(ra3_webgpu PUBLIC ra3_core ra3_render ra3_client)
|
||||
endif()
|
||||
openra3_target_defaults(ra3_webgpu)
|
||||
|
||||
# --- display backend selection ------------------------------------------------
|
||||
add_library(ra3_display STATIC)
|
||||
target_sources(ra3_display PUBLIC FILE_SET CXX_MODULES FILES src/display/ra3.display.cppm)
|
||||
target_link_libraries(ra3_display PUBLIC ra3_core ra3_render ra3_terrain ra3_client ra3_ui ra3_vulkan ra3_dx ra3_webgl)
|
||||
target_link_libraries(ra3_display PUBLIC ra3_core ra3_render ra3_terrain ra3_client ra3_ui ra3_vulkan ra3_dx ra3_wasmgl ra3_webgpu)
|
||||
openra3_target_defaults(ra3_display)
|
||||
|
||||
# --- umbrella -----------------------------------------------------------------
|
||||
add_library(ra3 STATIC)
|
||||
target_sources(ra3 PUBLIC FILE_SET CXX_MODULES FILES src/ra3.cppm)
|
||||
target_link_libraries(ra3 PUBLIC ra3_core ra3_logic ra3_client ra3_data ra3_game ra3_fs ra3_map ra3_skirmish ra3_render ra3_terrain ra3_display
|
||||
ra3_ui ra3_vulkan ra3_dx ra3_webgl ra3_enderlog)
|
||||
ra3_ui ra3_vulkan ra3_dx ra3_wasmgl ra3_webgpu ra3_enderlog)
|
||||
openra3_target_defaults(ra3)
|
||||
|
||||
# --- executable ---------------------------------------------------------------
|
||||
@@ -373,19 +427,36 @@ if(WIN32 AND OPENRA3_SDL3_ROOT)
|
||||
endif()
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
# Emit a ready-to-serve openra3.html that loads the module and drives the
|
||||
# #canvas element SDL3 renders into.
|
||||
set_target_properties(openra3 PROPERTIES SUFFIX ".html")
|
||||
# Emit openra3.js/.wasm. The engine runs inside apps/web/openra3.worker.js
|
||||
# (a plain Web Worker), which the page (apps/web/index.html) loads and hands
|
||||
# an OffscreenCanvas; so there is no Emscripten HTML shell here - both web
|
||||
# files are copied next to the module.
|
||||
set_target_properties(openra3 PROPERTIES SUFFIX ".js")
|
||||
target_link_options(openra3 PRIVATE
|
||||
"--shell-file=${CMAKE_CURRENT_SOURCE_DIR}/apps/web/shell.html"
|
||||
-sFORCE_FILESYSTEM=1)
|
||||
# Preload a compact assets directory into the virtual filesystem at /assets,
|
||||
# where the runtime looks next to the module. Prefer a per-map set (one map +
|
||||
# the tiles it uses, listed by `openra3 textures --map ID`): browsers forbid
|
||||
# synchronous on-demand file reads on the main thread, so bundling a small
|
||||
# set is the portable choice.
|
||||
set(OPENRA3_WEB_ASSETS "" CACHE PATH "Assets directory preloaded into the wasm FS at /assets")
|
||||
if(OPENRA3_WEB_ASSETS AND EXISTS "${OPENRA3_WEB_ASSETS}")
|
||||
-sFORCE_FILESYSTEM=1
|
||||
# index.html input glue calls the exported openra3_* functions via ccall.
|
||||
"-sEXPORTED_RUNTIME_METHODS=ccall,cwrap")
|
||||
add_custom_command(TARGET openra3 POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/apps/web/index.html"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/apps/web/openra3.worker.js"
|
||||
"$<TARGET_FILE_DIR:openra3>"
|
||||
COMMENT "Copying the OpenRA3 web page and worker bootstrap")
|
||||
# On-demand assets: `OPENRA3_WEB_MANIFEST` is a JSON `{"files": [...]}` listing
|
||||
# every asset relative to the assets root (generate it with
|
||||
# scripts/make_web_manifest.py). Only that small manifest is preloaded, to
|
||||
# /assets.manifest.json; at startup the worker registers every listed file as
|
||||
# an Emscripten lazy file, so the browser fetches each asset when the engine
|
||||
# first opens it (serve the tree at <root>/assets, or pass serve.py --assets).
|
||||
set(OPENRA3_WEB_MANIFEST "" CACHE FILEPATH "Assets manifest JSON, embedded as /assets.manifest.json for lazy loading")
|
||||
if(OPENRA3_WEB_MANIFEST AND EXISTS "${OPENRA3_WEB_MANIFEST}")
|
||||
target_link_options(openra3 PRIVATE "--embed-file=${OPENRA3_WEB_MANIFEST}@/assets.manifest.json")
|
||||
endif()
|
||||
# Legacy: preload a whole assets directory into the virtual FS at /assets (the
|
||||
# monolithic path the lazy worker backend replaces). Only used when no manifest
|
||||
# is given, since lazy files cannot shadow a preloaded tree.
|
||||
set(OPENRA3_WEB_ASSETS "" CACHE PATH "Assets directory preloaded into the wasm FS at /assets (legacy)")
|
||||
if(NOT (OPENRA3_WEB_MANIFEST AND EXISTS "${OPENRA3_WEB_MANIFEST}") AND OPENRA3_WEB_ASSETS AND EXISTS "${OPENRA3_WEB_ASSETS}")
|
||||
target_link_options(openra3 PRIVATE "--preload-file=${OPENRA3_WEB_ASSETS}@/assets")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
Reference in New Issue
Block a user