kobodl/web/shell.html
Ville Lindholm 0677b2cbca
Add Emscripten/WebAssembly build target
Builds a browser-playable .html/.js/.wasm/.data via `./build-web.sh` (no
new CMake top-level target — Emscripten is detected at configure time and
takes the existing kobodl through a web-flavored pipeline).

Key pieces:
- CMakeLists.txt: route SDL3 through `--use-port=sdl3`; substitute
  `--use-port=libpng` for the missing sdl3_image port (Emscripten 5.0.7
  ships only sdl3 + sdl3_ttf). pthread (atomics+bulk-memory) lifted to
  kobo_deps so every TU agrees with wasm-ld's --shared-memory. Link with
  -sASYNCIFY (avoids rewriting kobo's blocking main loop around
  emscripten_set_main_loop), -sALLOW_MEMORY_GROWTH/-sMAXIMUM_MEMORY=1gb,
  -sPTHREAD_POOL_SIZE=4, -lidbfs.js, and --preload-file data@/data.
- graphics/img_compat.{h,c}: libpng-backed IMG_Load shim, used in
  sprite.c / gfxengine.cpp / filters.c via #include "img_compat.h".
  Native builds passthrough to <SDL3_image/SDL_image.h> unchanged.
- web/shell.html: HTML wrapper that mounts /home as IDBFS for persistent
  scores/config. Startup gated on addRunDependency until syncfs(true)
  finishes, then /home/scores is mkdir'd post-load (the game's addPlayer
  doesn't auto-create the score dir; see TODO in score.cpp).
  beforeunload + visibilitychange flushes /home back to IndexedDB.
- build-web.sh: emcmake wrapper + a static server that sets the
  COOP/COEP headers SharedArrayBuffer/pthreads require.

Source fixes needed to build under Emscripten's libc/libcxx:
- Renamed enemy_kind `pipe2` -> `pipe2k` (collided with libc pipe2(2)
  always declared in Emscripten's <unistd.h>).
- graphics/toolkit.cpp: added missing <stdio.h> for snprintf (Emscripten's
  libcxx headers don't pull it in transitively).
- states.cpp: gated the "Quit Kobo Deluxe" main-menu button on
  !__EMSCRIPTEN__; exit() on the web leaves a dead canvas.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-04 18:47:10 +03:00

63 lines
2.3 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Kobo Deluxe</title>
<style>
html, body { margin: 0; height: 100%; background: #000; color: #ccc;
font-family: monospace; }
#wrap { display: flex; flex-direction: column; align-items: center;
justify-content: center; height: 100%; }
canvas { background: #000; image-rendering: pixelated; outline: none;
max-width: 100%; max-height: 100vh; }
#status { padding: 8px; font-size: 13px; }
</style>
</head>
<body>
<div id="wrap">
<canvas id="canvas" tabindex="-1" oncontextmenu="event.preventDefault()"></canvas>
<div id="status">Loading…</div>
</div>
<script>
var statusEl = document.getElementById('status');
var Module = {
canvas: (function () {
var c = document.getElementById('canvas');
c.addEventListener('webglcontextlost', function (e) {
alert('WebGL context lost. Reload to continue.'); e.preventDefault();
}, false);
return c;
})(),
print: function (t) { console.log(t); },
printErr: function (t) { console.warn(t); },
setStatus: function (t) { statusEl.textContent = t; },
preRun: [function () {
// Persist config + high scores across reloads via IndexedDB.
// syncfs(true) is async and replaces /home with whatever's in IDBFS,
// so we (1) gate startup on it via addRunDependency, and (2) re-mkdir
// /home/scores *after* the load (the game's addPlayer doesn't auto-
// create the score dir; see the TODO in score.cpp:504).
try { FS.mkdir('/home'); } catch (e) {}
FS.mount(IDBFS, {}, '/home');
Module.addRunDependency('idbfs');
FS.syncfs(true, function (err) {
if (err) console.warn('IDBFS load failed:', err);
try { FS.mkdir('/home/scores'); } catch (e) {}
Module.removeRunDependency('idbfs');
});
}],
postRun: [function () {
// Push the in-memory /home back to IndexedDB on tab close. visibility-
// change covers mobile, where beforeunload is unreliable.
var flush = function () { FS.syncfs(false, function () {}); };
window.addEventListener('beforeunload', flush);
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') flush();
});
}],
};
</script>
{{{ SCRIPT }}}
</body>
</html>