Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emscripten build improvements #658

Merged
merged 13 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
sudo apt-get update
sudo apt-get install libsdl2-dev
- name: Compile
run: make release
run: make release -j$(nproc)
env:
ARCHIVE: 1
- uses: actions/upload-artifact@v4
Expand All @@ -27,7 +27,7 @@ jobs:
- name: Compile
run: |
choco install zip
make release
make release -j $env:NUMBER_OF_PROCESSORS
env:
ARCHIVE: 1
- uses: actions/upload-artifact@v4
Expand All @@ -40,10 +40,33 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Compile
run: make release
run: make release -j$(sysctl -n hw.logicalcpu)
env:
ARCHIVE: 1
- uses: actions/upload-artifact@v4
with:
name: macOS
path: build/*.zip
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: emscripten-core/emsdk
path: emsdk
- name: Install Dependencies
run: |
cd emsdk
./emsdk install 3.1.58
./emsdk activate 3.1.58
- name: Compile
env:
ARCHIVE: 1
run: |
source emsdk/emsdk_env.sh
emmake make release -j$(nproc)
- uses: actions/upload-artifact@v4
with:
name: Web
path: build/*.zip
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Makefile.local
*.swp
*tags
*~
/.vscode/
/baseq3

# OS X
####################
Expand Down
108 changes: 88 additions & 20 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
#
# GNU Make required
#

# Rebuild every target if Makefile changes
.EXTRA_PREREQS:= $(abspath $(lastword $(MAKEFILE_LIST)))

COMPILE_PLATFORM=$(shell uname | sed -e 's/_.*//' | tr '[:upper:]' '[:lower:]' | sed -e 's/\//_/g')
COMPILE_ARCH=$(shell uname -m | sed -e 's/i.86/x86/' | sed -e 's/^arm.*/arm/')

Expand Down Expand Up @@ -61,6 +65,11 @@ ifeq ($(COMPILE_PLATFORM),cygwin)
PLATFORM=mingw32
endif

# detect "emmake make"
ifeq ($(findstring /emcc,$(CC)),/emcc)
PLATFORM=emscripten
endif

ifndef PLATFORM
PLATFORM=$(COMPILE_PLATFORM)
endif
Expand Down Expand Up @@ -1055,18 +1064,63 @@ ifeq ($(PLATFORM),emscripten)
# 5. Serve the build/release-emscripten-wasm32/ioquake3_opengl2.{html,js,wasm,data} from a web server.
# 6. Load ioquake3_opengl2.html in a web browser.

CC=emcc
ifneq ($(findstring /emcc,$(CC)),/emcc)
CC=emcc
endif
ARCH=wasm32

BINEXT=.js

# LDFLAGS+=-s MAIN_MODULE is needed for dlopen() in client/server but it causes compile errors
USE_RENDERER_DLOPEN=0
USE_OPENAL_DLOPEN=0
USE_CURL=0
HAVE_VM_COMPILED=false
BUILD_GAME_SO=0
BUILD_GAME_QVM=0
# Would be interesting to try to get the server working via WebRTC DataChannel.
# This would enable P2P play, hosting a server in the browser. Also,
# DataChannel is the only way to use UDP in the browser.
BUILD_SERVER=0

CLIENT_EXTRA_FILES+=code/web/ioquake3.html

CLIENT_CFLAGS+=-s USE_SDL=2

CLIENT_LDFLAGS+=-s TOTAL_MEMORY=256mb
CLIENT_LDFLAGS+=-s STACK_SIZE=5MB
# Informing Emscripten which WebGL versions we support makes the JS bundle smaller and faster to load.
CLIENT_LDFLAGS+=-s MIN_WEBGL_VERSION=2
CLIENT_LDFLAGS+=-s MAX_WEBGL_VERSION=2
CLIENT_LDFLAGS+=-s FULL_ES2=1
# The HTML file can use these functions to load extra files before the game starts.
CLIENT_LDFLAGS+=-s EXPORTED_RUNTIME_METHODS=FS,addRunDependency,removeRunDependency
CLIENT_LDFLAGS+=-s EXIT_RUNTIME=1
CLIENT_LDFLAGS+=-s EXPORT_ES6
CLIENT_LDFLAGS+=-s EXPORT_NAME=ioquake3
# Game data files can be packaged by emcc into a .data file that lives next to the wasm bundle
# and added to the virtual filesystem before the game starts. This requires the game data to be
# present at build time and it can't be changed afterward.
# For more flexibility, game data files can be loaded from a web server at runtime by listing
# them in ioq3-config.json. This way they don't have to be present at build time and can be
# changed later.
ifneq ($(wildcard $(BASEGAME)/*),)
CLIENT_LDFLAGS+=--preload-file $(BASEGAME)
EMSCRIPTEN_PRELOAD_FILE=1
CLIENT_EXTRA_FILES+=code/web/empty/ioq3-config.json
else
CLIENT_EXTRA_FILES+=code/web/$(BASEGAME)/ioq3-config.json
endif

BASE_CFLAGS=-fPIC -s USE_SDL=2
LDFLAGS=-s TOTAL_MEMORY=256mb -s MAX_WEBGL_VERSION=2 --preload-file $(BASEGAME)
OPTIMIZEVM = -O3
OPTIMIZE = $(OPTIMIZEVM)
OPTIMIZE = $(OPTIMIZEVM) -ffast-math

# These allow a warning-free build.
# Some of these warnings may actually be legit problems and should be fixed at some point.
BASE_CFLAGS+=-Wno-deprecated-non-prototype -Wno-dangling-else -Wno-implicit-const-int-float-conversion -Wno-misleading-indentation -Wno-format-overflow -Wno-logical-not-parentheses -Wno-absolute-value

FULLBINEXT=.html
DEBUG_CFLAGS=-g3 -O0 # -fsanitize=address -fsanitize=undefined
# Emscripten needs debug compiler flags to be passed to the linker as well
DEBUG_LDFLAGS=$(DEBUG_CFLAGS)

SHLIBEXT=wasm
SHLIBCFLAGS=-fPIC
Expand Down Expand Up @@ -1120,19 +1174,16 @@ ifneq ($(BUILD_SERVER),0)
TARGETS += $(B)/$(SERVERBIN)$(FULLBINEXT)

ifeq ($(PLATFORM),emscripten)
EMSCRIPTENOBJ += $(B)/$(SERVERBIN).js \
$(B)/$(SERVERBIN).wasm \
$(B)/$(SERVERBIN).data
EMSCRIPTENOBJ+=$(B)/$(SERVERBIN).wasm
ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1)
EMSCRIPTENOBJ+=$(B)/$(SERVERBIN).data
endif
endif
endif

ifneq ($(BUILD_CLIENT),0)
TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT)

ifeq ($(PLATFORM),emscripten)
EMSCRIPTENOBJ += $(B)/$(CLIENTBIN).js \
$(B)/$(CLIENTBIN).wasm \
$(B)/$(CLIENTBIN).data
ifneq ($(PLATFORM),emscripten)
TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT)
endif

ifneq ($(USE_RENDERER_DLOPEN),0)
Expand All @@ -1145,9 +1196,10 @@ ifneq ($(BUILD_CLIENT),0)
TARGETS += $(B)/$(CLIENTBIN)_opengl2$(FULLBINEXT)

ifeq ($(PLATFORM),emscripten)
EMSCRIPTENOBJ += $(B)/$(CLIENTBIN)_opengl2.js \
$(B)/$(CLIENTBIN)_opengl2.wasm \
$(B)/$(CLIENTBIN)_opengl2.data
EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN)_opengl2.wasm32.wasm
ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1)
EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN)_opengl2.wasm32.data
endif
endif
endif
endif
Expand Down Expand Up @@ -1458,7 +1510,8 @@ all: debug release
debug:
@$(MAKE) targets B=$(BD) CFLAGS="$(CFLAGS) $(BASE_CFLAGS) $(DEPEND_CFLAGS)" \
OPTIMIZE="$(DEBUG_CFLAGS)" OPTIMIZEVM="$(DEBUG_CFLAGS)" \
CLIENT_CFLAGS="$(CLIENT_CFLAGS)" SERVER_CFLAGS="$(SERVER_CFLAGS)" V=$(V)
CLIENT_CFLAGS="$(CLIENT_CFLAGS)" SERVER_CFLAGS="$(SERVER_CFLAGS)" V=$(V) \
LDFLAGS="$(LDFLAGS) $(DEBUG_LDFLAGS)"

release:
@$(MAKE) targets B=$(BR) CFLAGS="$(CFLAGS) $(BASE_CFLAGS) $(DEPEND_CFLAGS)" \
Expand Down Expand Up @@ -1495,6 +1548,7 @@ ifneq ($(BUILD_CLIENT),0)
endif

NAKED_TARGETS=$(shell echo $(TARGETS) | sed -e "s!$(B)/!!g")
NAKED_EMSCRIPTENOBJ=$(shell echo $(EMSCRIPTENOBJ) | sed -e "s!$(B)/!!g")

print_list=-@for i in $(1); \
do \
Expand Down Expand Up @@ -1550,7 +1604,20 @@ endif
@echo ""
@echo " Output:"
$(call print_list, $(NAKED_TARGETS))
$(call print_list, $(NAKED_EMSCRIPTENOBJ))
@echo ""
ifeq ($(PLATFORM),emscripten)
ifneq ($(EMSCRIPTEN_PRELOAD_FILE),1)
@echo " Warning: Game files not found in '$(BASEGAME)'."
@echo " They will not be packaged by Emscripten or preloaded."
@echo " To run this build you must serve the game files from a web server"
@echo " and list their paths in ioq3-config.json."
@echo " To make a build that automatically loads the game files, create a"
@echo " directory called '$(BASEGAME)' and copy your game files into it, then"
@echo " 'emmake make clean' and rebuild."
@echo ""
endif
endif
ifneq ($(TARGETS),)
ifndef DEBUG_MAKEFILE
@$(MAKE) $(TARGETS) $(B).zip V=$(V)
Expand All @@ -1566,9 +1633,10 @@ endif
ifneq ($(PLATFORM),darwin)
ifdef ARCHIVE
@rm -f $@
@(cd $(B) && zip -r9 ../../$@ $(NAKED_TARGETS))
@(cd $(B) && zip -r9 ../../$@ $(NAKED_TARGETS) $(NAKED_EMSCRIPTENOBJ))
endif
endif
@:

makedirs:
@$(MKDIR) $(B)/autoupdater
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Some of the major features currently implemented are:
* Multiuser support on Windows systems (user specific game data
is stored in "%APPDATA%\Quake3")
* PNG support
* Web support via Emscripten
* Many, many bug fixes

The map editor and associated compiling tools are not included. We suggest you
Expand Down Expand Up @@ -98,6 +99,23 @@ For macOS, building a Universal Binary 2 (macOS 10.9+, arm64, x86_64)
4. Copy the resulting ioquake3.app in /build/release-darwin-universal2
to your /Applications/ioquake3 folder.

For Web, building with Emscripten
1. Follow the installation instructions for the Emscripten SDK including
setting up the environment with emsdk_env.
2. Copy or symlink your baseq3 directory into this directory so Emscripten
can package your game files. (Alternatively, you can build without this,
and provide the game data files at runtime. For this, game files should
be listed in `ioq3-config.json`)
3. Run `emmake make debug` (or release, but if you do both then you will
need to pass an extra URL parameter to the HTML file to select the
build you want).
4. Start a web server serving this directory. `python3 -m http.server`
is an easy default that you may already have installed.
5. Open `code/web/ioquake3.html` in a web browser. Open the developer
console to see errors and warnings.
6. Debugging the C code is possible using a Chrome extension. For details
see https://developer.chrome.com/blog/wasm-debugging-2020

Installation, for *nix
1. Set the COPYDIR variable in the shell to be where you installed Quake 3
to. By default it will be /usr/local/games/quake3 if you haven't set it.
Expand Down
13 changes: 13 additions & 0 deletions code/web/baseq3/ioq3-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"files": [
{"src": "baseq3/pak0.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak1.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak2.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak3.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak4.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak5.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak6.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak7.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak8.pk3", "dst": "/baseq3"}
]
}
3 changes: 3 additions & 0 deletions code/web/empty/ioq3-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"files": []
}
70 changes: 70 additions & 0 deletions code/web/ioquake3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<!DOCTYPE html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>ioquake3 Emscripten demo</title>
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: rgb(0, 0, 0); display:flex; align-items: center; justify-content: center; }
canvas { max-width: 100%; max-height: 100%; min-width: 100%; min-height: 100%; object-fit: contain; }
</style>

<canvas id=canvas></canvas>

<script type=module>
if (window.location.protocol === 'file:') throw new Error('Unfortunately browser security restrictions prevent loading wasm from a file: URL. This file must be loaded from a web server. The easiest way to do this is probably to use Python\'s built-in web server by running `python3 -m http.server` in the top level ioq3 directory and then navigating to http://localhost:8000/code/web/ioquake3.html');

// First set up the command line arguments and the Emscripten filesystem.
const urlParams = new URLSearchParams(window.location.search);
const basegame = urlParams.get('basegame') || 'baseq3';
let generatedArguments = `
+set net_enabled 0
+set r_mode -2
+set fs_game ${basegame}
`;
// Note that unfortunately "+" needs to be encoded as "%2b" in URL query strings or it will be stripped by the browser.
const queryArgs = urlParams.get('args');
if (queryArgs) generatedArguments += ` ${queryArgs} `;

// If buildPath is not specified, try to find a build in one of a few default paths.
let buildPath = urlParams.get('buildPath');
if (buildPath && !buildPath.endsWith('/')) buildPath += '/';
const buildPaths = buildPath ? [buildPath] : ['../../build/debug-emscripten-wasm32/', '../../build/release-emscripten-wasm32/', './'];
const scriptPaths = buildPaths.map(buildPath => buildPath + 'ioquake3_opengl2.wasm32.js');
const scriptResponses = await Promise.all(scriptPaths.map(p => fetch(p, {method: 'HEAD'})));
const validBuilds = scriptResponses.filter(r => r.ok).length;
const goodURL = (newPath) => {
const url = new URL(window.location);
url.searchParams.set('buildPath', newPath);
return url.toString().replace(/%2f/gi, '/');
};
if (validBuilds === 0) throw new Error(`Didn't find any wasm builds. Run \`emmake make debug\` to build one, or use the buildPath query parameter to specify a directory containing ioquake3_opengl2.wasm32.[js,wasm,data], e.g. ${goodURL('../../build/debug-emscripten-wasm32/')}`);
if (validBuilds > 1) throw new Error(`Found multiple valid builds at the following paths: [${buildPaths.filter((path, i)=>scriptResponses[i].ok)}]. Please specify which one to run by adding a buildPath query parameter to the URL, e.g. ${goodURL(buildPaths.filter((path, i)=>scriptResponses[i].ok)[0])}`);
const buildIndex = scriptResponses.findIndex(r => r.ok);
const selectedScript = scriptPaths[buildIndex];
buildPath = buildPaths[buildIndex];
const buildURL = new URL(buildPath, location.origin + location.pathname);

const configPromise = fetch(buildPath + 'ioq3-config.json').then(r => r.ok ? r.json() : {files: []});

const ioquake3 = (await import(selectedScript)).default;
ioquake3({
canvas: canvas,
arguments: generatedArguments.trim().split(/\s+/),
locateFile: (file) => buildPath + file,
preRun: [async (module) => {
module.addRunDependency('setup-ioq3-filesystem');
try {
const config = await configPromise;
const fetches = config.files.map(file => fetch(new URL(file.src, buildURL)));
for (let i = 0; i < config.files.length; i++) {
const response = await fetches[i];
if (!response.ok) continue;
const data = await response.arrayBuffer();
let name = config.files[i].src.match(/[^/]+$/)[0];
let dir = config.files[i].dst;
module.FS.mkdirTree(dir);
module.FS.writeFile(`${dir}/${name}`, new Uint8Array(data));
}
} finally {
module.removeRunDependency('setup-ioq3-filesystem');
}
}],
});
</script>