Skip to content

Emscripten (WebAssembly) Build

GSSquared can be compiled to WebAssembly with Emscripten and run in a browser. The same top-level CMakeLists.txt is used for native and web builds; the web build is selected entirely by invoking CMake through emcmake, which injects Emscripten's toolchain (defining EMSCRIPTEN, setting CMAKE_SYSTEM_NAME to Emscripten, and leaving APPLE unset). Use a separate build directory so the native and web builds coexist.

Prerequisites

Install Emscripten. On macOS via Homebrew:

brew install emscripten

Homebrew's package puts its cache directory under a non-writable location, so set EM_CACHE to a writable path before building (any directory works):

export EM_CACHE="$HOME/.emscripten_cache"   # or e.g. "$PWD/.emcache"
mkdir -p "$EM_CACHE"

(If you use the official emsdk instead, source /path/to/emsdk/emsdk_env.sh and you can skip the EM_CACHE step.)

Configure and build

emcmake cmake -S . -B build-web -DCMAKE_BUILD_TYPE=Release
cmake --build build-web --target GSSquared -j

This produces, in build-web/:

  • GSSquared.html – the page (generated from assets/web/shell.html)
  • GSSquared.js – loader/runtime glue (also the pthread worker entry point)
  • GSSquared.wasm – the compiled emulator
  • GSSquared.data – the preloaded resource bundle (ROMs, fonts, images)

Run it

The build uses pthreads, which requires SharedArrayBuffer. Browsers only expose that when the page is served with cross-origin isolation headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

A helper dev server that sets these is included:

python3 assets/web/serve.py 8000 build-web
# then open http://localhost:8000/GSSquared.html

Opening GSSquared.html directly from file:// will not work (no headers, and WASM fetch is blocked). Always serve over HTTP with the headers above.

Deploying to gssquared.net/live

scripts/deploy-web.sh              # builds, then rsyncs to the server
scripts/deploy-web.sh --dry-run    # show what would be uploaded
scripts/deploy-web.sh --skip-build # upload the current build-web as-is

The script stages the four build products and uploads them to bazyar@gssquared.net:/var/www/gssquared.net/public/live/ (override with GS2_WEB_HOST / GS2_WEB_PATH / GS2_WEB_BUILD_DIR). On the server the directory looks like this:

  • index.php – the generated page, i.e. GSSquared.html renamed. It is plain HTML today; the .php extension only leaves room to require the site's layout helpers later.
  • GSSquared.js / .wasm / .data
  • .htaccess – copied from assets/web/live.htaccess; this is what sets the COOP/COEP headers, the application/wasm type, and Cache-Control: public, max-age=86400 (a day). The artifacts keep their names across builds, so a browser may keep the previous build until that cache expires. The local helper assets/web/serve.py sends Cache-Control: no-store, no-cache, must-revalidate, max-age=0 instead, so a rebuild shows up on the next reload.

/live is generated, not tracked in the gssquared-web repo: 11 MB of wasm per build has no place in a marketing site's git history, and public/live/ is gitignored there. A git pull on the server will therefore never restore it — after a fresh clone, run this script again.

The site nav bar at the top of the page is hardcoded in assets/web/shell.html. If the nav in gssquared-web's includes/layout.php changes, update the shell to match.

Using disk images

Two ways to mount media in the browser:

  • Drag and drop a disk image (.dsk, .do, .po, .woz, .hdv, .2mg, .img, .hda) onto the canvas, dropping it on a drive button in the drives panel. The file is read into the in-memory filesystem and mounted.
  • File menu -> Drives, or clicking a drive button, opens a browser file picker (replacing the native open dialog, which has no web backend).

Mounted images live in the in-memory filesystem and are not persisted across page reloads.

Playing an arQyv pack

A Play link opens this page with the pack URL in the fragment:

https://gssquared.net/live/#pack=<urlencoded https URL ending in .gs2pack>

The fragment is removed before analytics runs, so the launch token is not sent to gssquared.net. The page asks to download, naming the host and the file, then fetches that URL (CORS, no cookies) into MEMFS at /packs/<filename>.gs2pack. Only https://arqyv.net is accepted. The emulator extracts the archive under /tmp and boots it, skipping System Select. When the response includes X-GS2-Save-Token, File → Save to Collection uploads the rewritten pack. Closing the tab while that copy is dirty asks whether to save.

The downloaded archive and the extracted work tree are both in the WASM heap. INITIAL_MEMORY is 256 MB and MAXIMUM_MEMORY is 1 GB, so a pack near the 200 MB cap can fail to start. The page shows the error and boots System Select instead of a machine with missing disks.

To try this without arQyv, serve a directory of .gs2pack files on a second port. That server sends the CORS headers, a fake X-GS2-Save-Token (dev-save-token), and accepts PUT:

python3 assets/web/serve.py 8000 build-web --pack-port 8001 --pack-dir /tmp/packs
# http://localhost:8000/GSSquared.html#pack=http%3A%2F%2Flocalhost%3A8001%2Fdemo.gs2pack

http://localhost and http://127.0.0.1 are allowed only for this. A file named reject-put in the pack directory makes every PUT return 401.

Notes / limitations

  • Audio starts only after the first user gesture (browser autoplay policy). The shell shows a "Click to start" overlay for this.
  • Networking / modem: SDL_net has no Emscripten backend (its interface-enumeration code is a hard #error for unknown platforms) and the browser sandbox blocks raw TCP anyway. The web build therefore does not build or link SDL_net, and the SCC channel-B modem (ModemDevice, the only SDL_net user) is compiled out under __EMSCRIPTEN__ in src/devices/scc8530/scc8530.cpp (channel B falls back to a file device). No vendored library source is modified.
  • Drag-and-drop temp dir: SDL's Emscripten backend runs an unguarded FS.mkdir("/tmp/filedrop") during SDL_CreateWindow; if that directory already exists (or /tmp is missing) it throws and aborts init. Rather than patch vendored SDL, video_system_t ensures /tmp exists and clears any stale /tmp/filedrop before creating each window (see gs2_web_prepare_filedrop_dir() in src/videosystem.cpp).
  • Keyboard focus: SDL only treats the window as keyboard-focused when the canvas is document.activeElement, so input dies after Alt-Tab. The shell (assets/web/shell.html) re-focuses the canvas on window focus/click. When there is no keyboard focus, SDL still delivers key events but with windowID == 0. Emscripten supports only one window, so the debugger's second SDL_CreateWindow() returns NULL and its window_id is also 0 — debug_window_t::handle_event() therefore guards on a valid/nonzero window_id so the (nonexistent) debug window doesn't swallow every keystroke meant for the emulator.
  • Memory: INITIAL_MEMORY is set to 256 MB with ALLOW_MEMORY_GROWTH. If a large machine (e.g. IIgs) fails to start with an out-of-memory error, raise INITIAL_MEMORY in the if(EMSCRIPTEN) block of CMakeLists.txt.
  • The auxiliary apps/ developer tools are not built for the web target.