v0.6.0: WebAssembly build with a WebGL backend (playable in the browser)

- ra3.webgl: a Web peer of Vulkan/Direct3D (SDL3 canvas + GLES3/WebGL2), with
  2D blit and the GPU terrain raymarch; GLSL ES shaders; null fallback off
  Emscripten. backend::webgl + default_backend() (webgl under Emscripten).
- Emscripten toolchain + Dockerfile.wasm + scripts/build-wasm.sh; import std,
  global -fexceptions, and Asyncify so the blocking frame loop yields to the
  browser (display::sleep_frame calls emscripten_sleep).
- apps/web/shell.html + apps/web/serve.py (Range-capable dev server).
- Assets: browsers forbid synchronous on-demand reads on the main thread (and
  FS.createLazyFile / the WasmFS fetch backend are worker-only; SDL3's Emscripten
  backend is main-thread DOM only, so the engine cannot run in a worker). The
  wasm build therefore preloads a compact per-map set via OPENRA3_WEB_ASSETS.
- New openra3 textures --map ID lists the loose terrain TGAs a map resolves to
  (terrain::resolve_texture_files, shared with load_textures_from_dir).
- Fixes: webgl heightmap used GL_R16 = 0x8229 (that is R8) -> upload rejected,
  terrain flattened to water; now 0x822A. Logger uses a stdout console sink on
  the web (stderr maps to console.error); a GL error check logs bad uploads.
- CI: wasm image + build jobs.
This commit is contained in:
EnderTheCoder
2026-09-28 22:57:19 +08:00
parent d0ae4c76ba
commit 70d4beca4f
22 changed files with 1341 additions and 72 deletions
+101 -11
View File
@@ -7,8 +7,9 @@ set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444")
# Path to the standard-library module description. A cross toolchain overrides
# this before the compiler is probed; this default targets the Linux LLVM
# package.
if(NOT CMAKE_CXX_STDLIB_MODULES_JSON)
# package. Skipped for Emscripten, whose toolchain supplies its own sysroot
# modules.json.
if(NOT CMAKE_CXX_STDLIB_MODULES_JSON AND NOT DEFINED ENV{EMSDK})
set(CMAKE_CXX_STDLIB_MODULES_JSON "/usr/lib/llvm-21/lib/libc++.modules.json")
endif()
@@ -25,7 +26,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
project(OpenRA3 VERSION 0.5.0 LANGUAGES C CXX)
project(OpenRA3 VERSION 0.6.0 LANGUAGES C CXX)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
@@ -55,7 +56,13 @@ set(OPENRA3_HAS_SDL3 OFF)
# The triple-named subdirectory inside a MinGW SDL3 development package
# (`<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(OPENRA3_SDL3_ROOT)
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)
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")
set(OPENRA3_HAS_SDL3 ON)
@@ -78,9 +85,15 @@ endif()
# --- Vulkan: vendored volk + headers, so no Vulkan SDK is needed ---------------
# The loader is resolved at runtime (volk dlopen/LoadLibrary), which lets the
# same source build the Vulkan backend on Linux and on the MinGW Windows target.
option(OPENRA3_VULKAN "Build the Vulkan viewer" ON)
# WebAssembly has no Vulkan, so the viewer is off there (the null backend).
if(EMSCRIPTEN)
set(OPENRA3_VULKAN_DEFAULT OFF)
else()
set(OPENRA3_VULKAN_DEFAULT ON)
endif()
option(OPENRA3_VULKAN "Build the Vulkan viewer" ${OPENRA3_VULKAN_DEFAULT})
set(OPENRA3_HAS_VULKAN OFF)
if(OPENRA3_VULKAN AND OPENRA3_HAS_SDL3)
if(OPENRA3_VULKAN AND OPENRA3_HAS_SDL3 AND NOT EMSCRIPTEN)
set(OPENRA3_HAS_VULKAN ON)
endif()
if(OPENRA3_HAS_VULKAN)
@@ -89,9 +102,11 @@ else()
message(STATUS "OpenRA3: Vulkan viewer disabled - offscreen image only")
endif()
add_library(ra3_volk STATIC third_party/volk/volk.c)
target_include_directories(ra3_volk PUBLIC third_party/volk third_party/vulkan/include)
target_compile_definitions(ra3_volk PUBLIC VK_NO_PROTOTYPES)
if(NOT EMSCRIPTEN)
add_library(ra3_volk STATIC third_party/volk/volk.c)
target_include_directories(ra3_volk PUBLIC third_party/volk third_party/vulkan/include)
target_compile_definitions(ra3_volk PUBLIC VK_NO_PROTOTYPES)
endif()
# Embed the committed SPIR-V blobs into a generated header at configure time.
# Regenerate with shaders/compile.sh after editing a .vert/.frag.
@@ -135,6 +150,25 @@ function(openra3_embed_hlsl out_header)
file(WRITE "${out_header}" "${content}")
endfunction()
# Embed the GLSL ES sources for the WebGL backend as string literals (compiled
# at runtime by the browser's GL, like the HLSL for Direct3D).
function(openra3_embed_glsl out_header)
set(content "// Generated by CMake from shaders/*.glsl.\n")
string(APPEND content "#pragma once\n\nnamespace ra3_shaders {\n")
foreach(src IN LISTS ARGN)
if(NOT EXISTS "${src}")
message(FATAL_ERROR "Missing GLSL shader ${src}")
endif()
get_filename_component(base "${src}" NAME_WE)
string(MAKE_C_IDENTIFIER "${base}" ident)
string(APPEND ident "_glsl")
file(READ "${src}" text)
string(APPEND content "inline constexpr const char ${ident}[] = R\"RA3GLSL(${text})RA3GLSL\";\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
@@ -275,17 +309,55 @@ 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)
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)
openra3_embed_glsl(
"${CMAKE_CURRENT_BINARY_DIR}/generated/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"
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/webgl_terrain_frag.glsl"
)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${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"
"${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 WebGL2 so GLES3-only entry points (glTexImage3D, VAOs, ...) link.
target_link_options(ra3_webgl 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)
endif()
openra3_target_defaults(ra3_webgl)
# --- 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)
target_link_libraries(ra3_display PUBLIC ra3_core ra3_render ra3_terrain ra3_client ra3_ui ra3_vulkan ra3_dx ra3_webgl)
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_enderlog)
ra3_ui ra3_vulkan ra3_dx ra3_webgl ra3_enderlog)
openra3_target_defaults(ra3)
# --- executable ---------------------------------------------------------------
@@ -300,6 +372,24 @@ if(WIN32 AND OPENRA3_SDL3_ROOT)
"$<TARGET_FILE_DIR:openra3>")
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")
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}")
target_link_options(openra3 PRIVATE "--preload-file=${OPENRA3_WEB_ASSETS}@/assets")
endif()
endif()
# --- packaging (.deb / .msi / portable archives) -----------------------------
# CPack produces the release artifacts in one shot:
# Linux -> DEB (dependencies derived from the binary with dpkg-shlibdeps)