diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96773ffbd..b50d231a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1313,3 +1313,117 @@ jobs: if-no-files-found: error name: "tic80-html-players" path: build/players/* + + # === HTML, WebGPU backend === + # A job of its own so a WebGPU-only break cannot keep the GLES3 players off dev. + html-webgpu: + runs-on: ubuntu-latest + + steps: + - uses: emscripten-core/setup-emsdk@v15 + + - uses: actions/checkout@v6 + with: + submodules: recursive + fetch-depth: 0 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6 + + - name: Install 32-bit build tools + run: sudo apt-get update && sudo apt-get install -y gcc-multilib + + # The editorful build, as the site's players are: the loader keeps the name the site asks for. + - name: Build per-language WebGPU players + shell: bash + run: | + set -euo pipefail + langs=(lua ruby js moon yue fennel scheme squirrel wren wasm janet python miniscript forth gravity) + stage="$GITHUB_WORKSPACE/build/players-webgpu" + rm -rf "$stage" + for lang in "${langs[@]}"; do + flags=(-DBUILD_WITH_ALL=OFF) + for l in "${langs[@]}"; do + up=$(echo "$l" | tr '[:lower:]' '[:upper:]') + if [ "$l" = "$lang" ]; then flags+=("-DBUILD_WITH_$up=ON"); else flags+=("-DBUILD_WITH_$up=OFF"); fi + done + cd "$GITHUB_WORKSPACE/build" + emcmake cmake -G Ninja -DBUILD_TOUCH_INPUT=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_EDITORS=ON -DBUILD_SOKOL_WGPU=ON "${flags[@]}" -DTIC80_TARGET="tic80$lang" .. + cmake --build . --parallel + for f in "bin/tic80$lang.js" "bin/tic80$lang.wasm"; do + [ -f "$f" ] || { echo "missing player: build/$f" >&2; exit 1; } + done + mkdir -p "$stage/$lang/webgpu" + cp "bin/tic80$lang.js" "$stage/$lang/webgpu/tic80.js" + cp "bin/tic80$lang.wasm" "$stage/$lang/webgpu/tic80$lang.wasm" + rm -f "bin/tic80$lang.js" "bin/tic80$lang.wasm" + done + + # The target names the wasm its loader asks for; plain "tic80" would ask for the wrong one. + - name: Build universal WebGPU player + shell: bash + run: | + set -euo pipefail + stage="$GITHUB_WORKSPACE/build/players-webgpu/webgpu" + cd "$GITHUB_WORKSPACE/build" + emcmake cmake -G Ninja -DBUILD_TOUCH_INPUT=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_WITH_ALL=ON -DBUILD_SOKOL_WGPU=ON -DTIC80_TARGET=tic80webgpu .. --fresh + cmake --build . --parallel + mkdir -p "$stage" + cp bin/tic80webgpu.js "$stage/tic80.js" + cp bin/tic80webgpu.wasm "$stage/tic80webgpu.wasm" + # the page an exported game gets, as the html job's "Build all" copies it + cp html/index.html "$stage/index.html" + + # The backend is read from the loader: the wasm carries no such string. + - name: Check the builds are what they claim + shell: bash + run: | + set -euo pipefail + stage="$GITHUB_WORKSPACE/build/players-webgpu" + langs=(lua ruby js moon yue fennel scheme squirrel wren wasm janet python miniscript forth gravity) + files=() + for lang in "${langs[@]}"; do + js="$stage/$lang/webgpu/tic80.js" + wasm="$stage/$lang/webgpu/tic80$lang.wasm" + for f in "$js" "$wasm"; do + [ -f "$f" ] || { echo "missing player file: $f" >&2; exit 1; } + done + grep -qa "SPRITE EDITOR" "$wasm" || + { echo "player without editors: $wasm — BUILD_EDITORS=ON did not take" >&2; exit 1; } + grep -qa "Unable to initialize SDL Video" "$wasm" && + { echo "player on the SDL layer: $wasm — the sokol default did not take" >&2; exit 1; } + grep -qa "navigator.gpu" "$js" || + { echo "player without the WebGPU shim: $js — BUILD_SOKOL_WGPU=ON did not take" >&2; exit 1; } + grep -qa "tic80$lang.wasm" "$js" || + { echo "player asks for another wasm: $js does not name tic80$lang.wasm — TIC80_TARGET did not take" >&2; exit 1; } + files+=("$wasm") + done + for f in "$stage/webgpu/tic80.js" "$stage/webgpu/tic80webgpu.wasm" "$stage/webgpu/index.html"; do + [ -f "$f" ] || { echo "missing universal file: $f" >&2; exit 1; } + done + grep -qa "SPRITE EDITOR" "$stage/webgpu/tic80webgpu.wasm" || + { echo "the universal build carries no editors" >&2; exit 1; } + grep -qa "Unable to initialize SDL Video" "$stage/webgpu/tic80webgpu.wasm" && + { echo "the universal build is on the SDL layer — the sokol default did not take" >&2; exit 1; } + grep -qa "navigator.gpu" "$stage/webgpu/tic80.js" || + { echo "the universal build is not WebGPU" >&2; exit 1; } + grep -qa "tic80webgpu.wasm" "$stage/webgpu/tic80.js" || + { echo "the universal player asks for another wasm: it does not name tic80webgpu.wasm — TIC80_TARGET did not take" >&2; exit 1; } + # a language flag that did not take leaves two builds alike + for ((i = 0; i < ${#files[@]}; i++)); do + for ((j = i + 1; j < ${#files[@]}; j++)); do + if cmp -s "${files[i]}" "${files[j]}"; then + echo "duplicate player: $(basename "${files[i]}") == $(basename "${files[j]}") — a language flag did not take" >&2 + exit 1 + fi + done + done + echo "checked ${#files[@]} WebGPU players, all distinct, all with editors, all on the WebGPU shim" + + - name: Deploy + uses: actions/upload-artifact@v6 + with: + if-no-files-found: error + name: "tic80-html-webgpu" + path: build/players-webgpu/* diff --git a/CMakeLists.txt b/CMakeLists.txt index 823e514a5..53af704f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,7 @@ endif() option(BUILD_SDL "SDL Enabled" ${BUILD_SDL_DEFAULT}) option(BUILD_SOKOL "Sokol Enabled" ${BUILD_SOKOL_DEFAULT}) +option(BUILD_SOKOL_WGPU "Sokol WebGPU backend (browser builds)" OFF) if(BUILD_APPLE) set(BUILD_SDL OFF) set(BUILD_SOKOL OFF) @@ -62,6 +63,10 @@ endif() if(BUILD_SDL AND BUILD_SOKOL) message(FATAL_ERROR "BUILD_SDL and BUILD_SOKOL are two system layers: enable exactly one") endif() +# WebGPU is the Emscripten sokol backend; elsewhere it would be silently ignored. +if(BUILD_SOKOL_WGPU AND NOT (EMSCRIPTEN AND BUILD_SOKOL)) + message(FATAL_ERROR "BUILD_SOKOL_WGPU is the Emscripten sokol backend: enable it with BUILD_SOKOL, for the browser") +endif() option(BUILD_SDLGPU "SDL GPU Enabled" OFF) option(BUILD_LIBRETRO "libretro Enabled" ${BUILD_LIBRETRO_DEFAULT}) option(BUILD_TOOLS "bin2txt prj2cart" OFF) @@ -112,6 +117,7 @@ target_compile_definitions(runtime INTERFACE BUILD_DEPRECATED) message("PREFER_SYSTEM_LIBRARIES: ${PREFER_SYSTEM_LIBRARIES}") message("BUILD_STATIC: ${BUILD_STATIC}") message("BUILD_SDLGPU: ${BUILD_SDLGPU}") +message("BUILD_SOKOL_WGPU: ${BUILD_SOKOL_WGPU}") message("BUILD_TOUCH_INPUT: ${BUILD_TOUCH_INPUT}") message("BUILD_WITH_ALL: ${BUILD_WITH_ALL}") diff --git a/cmake/sokol.cmake b/cmake/sokol.cmake index bdb50a1b5..068678f35 100644 --- a/cmake/sokol.cmake +++ b/cmake/sokol.cmake @@ -28,6 +28,9 @@ if(BUILD_SOKOL) target_compile_definitions(sokol PUBLIC SOKOL_METAL) elseif(WIN32) target_compile_definitions(sokol PUBLIC SOKOL_D3D11 UNICODE) + elseif(EMSCRIPTEN AND BUILD_SOKOL_WGPU) + # The browser's other backend: sokol_app owns the WebGPU device and swapchain. + target_compile_definitions(sokol PUBLIC SOKOL_WGPU) elseif(EMSCRIPTEN) target_compile_definitions(sokol PUBLIC SOKOL_GLES3) elseif(USE_GLES2) @@ -124,7 +127,22 @@ if(BUILD_SOKOL) # The same runtime shape the SDL web target asks for — memory growth, # fetch, the 4M stack a deep cart needs, the page's file hooks — with # WebGL2 in place of SDL's GL. - set_target_properties(${TIC80_TARGET} PROPERTIES LINK_FLAGS "-s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s FETCH=1 -s STACK_SIZE=4194304 -s EXPORTED_FUNCTIONS=_main,_malloc,_free -s EXPORTED_RUNTIME_METHODS=UTF8ToString -s MIN_WEBGL_VERSION=2 -s MAX_WEBGL_VERSION=2 --pre-js ${CMAKE_SOURCE_DIR}/build/html/prejs.js -lidbfs.js") + set(TIC80_LINK_FLAGS "-s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s FETCH=1 -s STACK_SIZE=4194304 -s EXPORTED_FUNCTIONS=_main,_malloc,_free -s EXPORTED_RUNTIME_METHODS=UTF8ToString --pre-js ${CMAKE_SOURCE_DIR}/build/html/prejs.js -lidbfs.js") + + if(BUILD_SOKOL_WGPU) + + # WebGPU in place of WebGL2; the port carries the header, so it is on compile and link. + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --use-port=emdawnwebgpu") + # Named because the port's C++ drops them from the default set; unnamed they die in callMain. + set(TIC80_LINK_FLAGS "${TIC80_LINK_FLAGS} --use-port=emdawnwebgpu -sDEFAULT_TO_CXX -sEXPORTED_FUNCTIONS=_main,_malloc,_free,__emscripten_stack_alloc,_emscripten_stack_get_current,__emscripten_stack_restore") + + else() + + set(TIC80_LINK_FLAGS "${TIC80_LINK_FLAGS} -s MIN_WEBGL_VERSION=2 -s MAX_WEBGL_VERSION=2") + + endif() + + set_target_properties(${TIC80_TARGET} PROPERTIES LINK_FLAGS "${TIC80_LINK_FLAGS}") endif()