diff --git a/CMakeLists.txt b/CMakeLists.txt index 19424e0..26b48c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,13 @@ option(KOBO_ENABLE_OSS "Build OSS audio backend (Linux)" OFF) option(KOBO_ENABLE_ALSA "Build ALSA audio backend (Linux)" OFF) option(KOBO_ENABLE_EPM "Extreme Pickyness Mode (-Wall -Werror)" OFF) +# Emscripten target: SDL renderer maps to WebGL, no desktop GL needed. +if(EMSCRIPTEN) + set(KOBO_ENABLE_OPENGL OFF CACHE BOOL "Use OpenGL rendering layer" FORCE) + set(KOBO_ENABLE_OSS OFF CACHE BOOL "Build OSS audio backend" FORCE) + set(KOBO_ENABLE_ALSA OFF CACHE BOOL "Build ALSA audio backend" FORCE) +endif() + # --------------------------------------------------------------------------- # Feature detection # --------------------------------------------------------------------------- @@ -36,8 +43,21 @@ check_symbol_exists(_vsnprintf stdio.h HAVE__VSNPRINTF) # Third-party libraries # --------------------------------------------------------------------------- # Migrating to SDL 3. Homebrew: `brew install sdl3 sdl3_image`. -find_package(SDL3 CONFIG REQUIRED) -find_package(SDL3_image CONFIG REQUIRED) +# Emscripten 5.x ships an sdl3 port but no sdl3_image yet — we substitute +# libpng + a small shim (graphics/img_compat.c) for IMG_Load. The +# `--use-port` flags must appear in both compile and link phases. +if(EMSCRIPTEN) + add_library(SDL3::SDL3 INTERFACE IMPORTED) + target_compile_options(SDL3::SDL3 INTERFACE --use-port=sdl3) + target_link_options(SDL3::SDL3 INTERFACE --use-port=sdl3) + + add_library(SDL3_image::SDL3_image INTERFACE IMPORTED) + target_compile_options(SDL3_image::SDL3_image INTERFACE --use-port=libpng) + target_link_options(SDL3_image::SDL3_image INTERFACE --use-port=libpng) +else() + find_package(SDL3 CONFIG REQUIRED) + find_package(SDL3_image CONFIG REQUIRED) +endif() if(KOBO_ENABLE_OPENGL) find_package(OpenGL) @@ -65,7 +85,17 @@ endif() # --------------------------------------------------------------------------- # Platform-specific install layout (matches the old autotools behavior) # --------------------------------------------------------------------------- -if(APPLE) +if(EMSCRIPTEN) + # In the browser, "data" is preloaded into Emscripten's virtual FS at /. + # Config/scores live under /home which we mount as IDBFS for persistence + # (see Module.preRun in the HTML shell / build-web.sh). + set(KOBO_BUNDLE_STYLE "web") + set(KOBO_EXEFILE "kobodl.html") + set(KOBO_DATA_DIR "/data") + set(KOBO_SCORE_DIR "/home/scores") + set(KOBO_CONFIG_DIR "/home") + set(KOBO_CONFIG_FILE ".kobodlrc") +elseif(APPLE) set(KOBO_BUNDLE_STYLE "macosx") set(KOBO_EXEFILE "kobodl") set(KOBO_DATA_DIR "EXE>>../Resources") @@ -139,6 +169,13 @@ if(UNIX AND NOT APPLE) target_link_libraries(kobo_deps INTERFACE m) endif() +if(EMSCRIPTEN) + # -pthread enables wasm atomics+bulk-memory; every TU must agree on it or + # wasm-ld rejects --shared-memory. + target_compile_options(kobo_deps INTERFACE -pthread) + target_link_options(kobo_deps INTERFACE -pthread) +endif() + if(KOBO_ENABLE_EPM) target_compile_options(kobo_deps INTERFACE -Wall -Werror -Wwrite-strings -fno-builtin @@ -168,6 +205,32 @@ target_include_directories(kobodl PRIVATE ${CMAKE_SOURCE_DIR}/data/sfx) target_link_libraries(kobodl PRIVATE graphics sound eel kobo_deps) set_target_properties(kobodl PROPERTIES OUTPUT_NAME ${KOBO_EXEFILE}) +if(EMSCRIPTEN) + # ASYNCIFY lets the existing blocking main loop (gengine->run(), SDL_Delay) + # yield to the browser without rewriting it around emscripten_set_main_loop. + # pthread (atomics+bulk-memory) is applied via kobo_deps for every TU. + # The data directory is baked into the .data sidecar via --preload-file. + target_link_options(kobodl PRIVATE + -sASYNCIFY=1 + -sALLOW_MEMORY_GROWTH=1 + -sINITIAL_MEMORY=128MB + -sMAXIMUM_MEMORY=1gb + -sPTHREAD_POOL_SIZE=4 + -sEXIT_RUNTIME=1 + -sFORCE_FILESYSTEM=1 + -sEXPORTED_RUNTIME_METHODS=FS,callMain + -lidbfs.js + --preload-file ${CMAKE_SOURCE_DIR}/data@/data + ) + # Generate index.html alongside kobodl.html so it can be served as the + # default document. The shell file mounts /home as IDBFS for persistence. + if(EXISTS ${CMAKE_SOURCE_DIR}/web/shell.html) + target_link_options(kobodl PRIVATE + --shell-file ${CMAKE_SOURCE_DIR}/web/shell.html) + endif() + set_target_properties(kobodl PROPERTIES SUFFIX "") +endif() + # --------------------------------------------------------------------------- # Install rules # --------------------------------------------------------------------------- @@ -189,6 +252,14 @@ elseif(KOBO_BUNDLE_STYLE STREQUAL "simple") install(TARGETS kobodl RUNTIME DESTINATION KoboDeluxe) install(DIRECTORY data/gfx data/sfx DESTINATION KoboDeluxe) install(FILES COPYING COPYING.LIB README ChangeLog TODO DESTINATION KoboDeluxe) +elseif(KOBO_BUNDLE_STYLE STREQUAL "web") + # Emscripten emits kobodl.html / .js / .wasm / .data — install all four. + install(TARGETS kobodl RUNTIME DESTINATION web) + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/kobodl.js + ${CMAKE_CURRENT_BINARY_DIR}/kobodl.wasm + ${CMAKE_CURRENT_BINARY_DIR}/kobodl.data + DESTINATION web OPTIONAL) else() include(GNUInstallDirs) install(TARGETS kobodl RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/build-web.sh b/build-web.sh new file mode 100755 index 0000000..c6e4167 --- /dev/null +++ b/build-web.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# Build Kobo Deluxe as a WebAssembly app via Emscripten. +# +# Prereq: install and activate emsdk so emcmake / emcc are on PATH. +# git clone https://github.com/emscripten-core/emsdk ~/emsdk +# ~/emsdk/emsdk install latest && ~/emsdk/emsdk activate latest +# . ~/emsdk/emsdk_env.sh +# +# Output: build-web/kobodl.{html,js,wasm,data} +# Serve from build-web/ with any static server that sets the COOP/COEP headers +# required for SharedArrayBuffer (pthreads). Example: +# ./build-web.sh && ./build-web.sh serve + +set -e + +HERE="$(cd "$(dirname "$0")" && pwd)" +BUILD="$HERE/build-web" + +case "${1:-build}" in + serve) + cd "$BUILD" + exec python3 -c " +import http.server, socketserver +class H(http.server.SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header('Cross-Origin-Opener-Policy', 'same-origin') + self.send_header('Cross-Origin-Embedder-Policy', 'require-corp') + super().end_headers() +socketserver.TCPServer(('', 8000), H).serve_forever() +" + ;; + clean) + rm -rf "$BUILD" + ;; + build|*) + command -v emcmake >/dev/null 2>&1 || { + echo "emcmake not found. Source emsdk_env.sh first." >&2 + exit 1 + } + mkdir -p "$BUILD" + cd "$BUILD" + emcmake cmake -DCMAKE_BUILD_TYPE=Release "$HERE" + cmake --build . -j + echo + echo "Built: $BUILD/kobodl.html" + echo "Serve: ./build-web.sh serve (then open http://localhost:8000/kobodl.html)" + ;; +esac diff --git a/enemies.h b/enemies.h index 92bc01e..aaa240f 100644 --- a/enemies.h +++ b/enemies.h @@ -72,7 +72,7 @@ extern const enemy_kind bombdeto; extern const enemy_kind cannon; extern const enemy_kind pipe1; extern const enemy_kind core; -extern const enemy_kind pipe2; +extern const enemy_kind pipe2k; extern const enemy_kind rock; extern const enemy_kind ring; extern const enemy_kind enemy_m1; @@ -434,7 +434,7 @@ inline int _enemy::realize() inline int _enemy::is_pipe() { - return ((_state != notuse) && ((ek == &pipe1) || (ek == &pipe2))); + return ((_state != notuse) && ((ek == &pipe1) || (ek == &pipe2k))); } diff --git a/enemy.cpp b/enemy.cpp index 136eefb..3b295eb 100644 --- a/enemy.cpp +++ b/enemy.cpp @@ -755,10 +755,10 @@ void _enemy::move_core() void _enemy::kill_core() { - enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3); - enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7); - enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1); - enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5); + enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3); + enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7); + enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1); + enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5); enemies.make(&explosion4, CS2PIXEL(x), CS2PIXEL(y)); sound.g_base_core_explo(x, y); release(); @@ -978,19 +978,19 @@ void _enemy::move_pipe2() } p ^= a; if(p & U_MASK) - enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1); + enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1); if(p & R_MASK) - enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3); + enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3); if(p & D_MASK) - enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5); + enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5); if(p & L_MASK) - enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7); + enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7); manage.add_score(10); release(); } -const enemy_kind pipe2 = { +const enemy_kind pipe2k = { 0, &_enemy::make_pipe2, &_enemy::move_pipe2, diff --git a/graphics/CMakeLists.txt b/graphics/CMakeLists.txt index f149d72..f48627d 100644 --- a/graphics/CMakeLists.txt +++ b/graphics/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(graphics STATIC toolkit.cpp vidmodes.c region.c + img_compat.c ) target_include_directories(graphics diff --git a/graphics/filters.c b/graphics/filters.c index 9b744de..0fb1392 100644 --- a/graphics/filters.c +++ b/graphics/filters.c @@ -23,7 +23,7 @@ #include #include "logger.h" #include "sdl_compat.h" -#include +#include "img_compat.h" #include "sprite.h" #include "filters.h" diff --git a/graphics/gfxengine.cpp b/graphics/gfxengine.cpp index b618dd3..d5dcc32 100644 --- a/graphics/gfxengine.cpp +++ b/graphics/gfxengine.cpp @@ -28,7 +28,7 @@ #include "gfxengine.h" #include "filters.h" -#include +#include "img_compat.h" #include "sdl_compat.h" #include "sofont.h" #include "window.h" diff --git a/graphics/img_compat.c b/graphics/img_compat.c new file mode 100644 index 0000000..e1c603e --- /dev/null +++ b/graphics/img_compat.c @@ -0,0 +1,92 @@ +/* + * img_compat.c — libpng-backed IMG_Load for the Emscripten build. + * + * Returns an RGBA8 SDL_Surface. Handles palette, grayscale, tRNS, and 16-bit + * inputs by normalizing to 8-bit RGBA in the read pipeline. + */ +#ifdef __EMSCRIPTEN__ + +#include "img_compat.h" + +#include +#include +#include + +SDL_Surface *IMG_Load(const char *file) +{ + FILE *fp = fopen(file, "rb"); + if(!fp) + return NULL; + + unsigned char hdr[8]; + if(fread(hdr, 1, 8, fp) != 8 || png_sig_cmp(hdr, 0, 8)) + { + fclose(fp); + return NULL; + } + + png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, + NULL, NULL, NULL); + if(!png) { fclose(fp); return NULL; } + png_infop info = png_create_info_struct(png); + if(!info) + { + png_destroy_read_struct(&png, NULL, NULL); + fclose(fp); + return NULL; + } + + SDL_Surface *surf = NULL; + png_bytep *rows = NULL; + + if(setjmp(png_jmpbuf(png))) + { + if(rows) free(rows); + if(surf) SDL_DestroySurface(surf); + png_destroy_read_struct(&png, &info, NULL); + fclose(fp); + return NULL; + } + + png_init_io(png, fp); + png_set_sig_bytes(png, 8); + png_read_info(png, info); + + png_uint_32 w, h; + int depth, ctype; + png_get_IHDR(png, info, &w, &h, &depth, &ctype, NULL, NULL, NULL); + + if(depth == 16) png_set_strip_16(png); + if(ctype == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png); + if(ctype == PNG_COLOR_TYPE_GRAY && depth < 8) + png_set_expand_gray_1_2_4_to_8(png); + if(png_get_valid(png, info, PNG_INFO_tRNS)) + png_set_tRNS_to_alpha(png); + if(ctype == PNG_COLOR_TYPE_GRAY || ctype == PNG_COLOR_TYPE_GRAY_ALPHA) + png_set_gray_to_rgb(png); + if(ctype == PNG_COLOR_TYPE_RGB || ctype == PNG_COLOR_TYPE_GRAY + || ctype == PNG_COLOR_TYPE_PALETTE) + png_set_filler(png, 0xFF, PNG_FILLER_AFTER); + + png_read_update_info(png, info); + + surf = SDL_CreateSurface((int)w, (int)h, SDL_PIXELFORMAT_RGBA32); + if(!surf) + png_error(png, "SDL_CreateSurface failed"); + + rows = (png_bytep *)malloc(sizeof(png_bytep) * h); + if(!rows) + png_error(png, "row table alloc failed"); + for(png_uint_32 y = 0; y < h; ++y) + rows[y] = (png_bytep)((unsigned char *)surf->pixels + y * surf->pitch); + + png_read_image(png, rows); + png_read_end(png, NULL); + + free(rows); + png_destroy_read_struct(&png, &info, NULL); + fclose(fp); + return surf; +} + +#endif /* __EMSCRIPTEN__ */ diff --git a/graphics/img_compat.h b/graphics/img_compat.h new file mode 100644 index 0000000..7860f25 --- /dev/null +++ b/graphics/img_compat.h @@ -0,0 +1,32 @@ +/* + * img_compat.h — SDL3_image shim. + * + * On native builds, this is just a passthrough to . + * On Emscripten there is no SDL3_image port yet (only sdl3 and sdl3_ttf), so + * we provide an IMG_Load implementation backed by the libpng port. Only PNG + * decoding is needed — Kobo Deluxe's assets are all .png. + */ +#ifndef KOBO_IMG_COMPAT_H +#define KOBO_IMG_COMPAT_H + +#ifdef __EMSCRIPTEN__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +SDL_Surface *IMG_Load(const char *file); + +#ifdef __cplusplus +} +#endif + +#else /* native */ + +#include + +#endif + +#endif /* KOBO_IMG_COMPAT_H */ diff --git a/graphics/sprite.c b/graphics/sprite.c index a4d0722..8a213f5 100644 --- a/graphics/sprite.c +++ b/graphics/sprite.c @@ -30,7 +30,7 @@ TODO: tables as needed when loading banks. #include #include "logger.h" #include "sdl_compat.h" -#include +#include "img_compat.h" #include "sprite.h" #include "filters.h" diff --git a/graphics/toolkit.cpp b/graphics/toolkit.cpp index d1a1f06..3b54db5 100644 --- a/graphics/toolkit.cpp +++ b/graphics/toolkit.cpp @@ -23,6 +23,7 @@ #include #include +#include #include "window.h" #include "toolkit.h" diff --git a/states.cpp b/states.cpp index 116fda5..7910bd1 100644 --- a/states.cpp +++ b/states.cpp @@ -1286,7 +1286,11 @@ void main_menu_t::build() button("Return to Intro", 0); else button("Abort Current Game", 101); +#ifndef __EMSCRIPTEN__ + // In the browser, exit() leaves the page on a dead canvas. Drop the + // option entirely; reloading the tab is the user's exit. button("Quit Kobo Deluxe", MENU_TAG_CANCEL); +#endif } void main_menu_t::rebuild() diff --git a/web/shell.html b/web/shell.html new file mode 100644 index 0000000..d8ff95c --- /dev/null +++ b/web/shell.html @@ -0,0 +1,62 @@ + + + + +Kobo Deluxe + + + +
+ +
Loading…
+
+ +{{{ SCRIPT }}} + +