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>
This commit is contained in:
parent
0263be65d4
commit
0677b2cbca
@ -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_ALSA "Build ALSA audio backend (Linux)" OFF)
|
||||||
option(KOBO_ENABLE_EPM "Extreme Pickyness Mode (-Wall -Werror)" 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
|
# Feature detection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -36,8 +43,21 @@ check_symbol_exists(_vsnprintf stdio.h HAVE__VSNPRINTF)
|
|||||||
# Third-party libraries
|
# Third-party libraries
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Migrating to SDL 3. Homebrew: `brew install sdl3 sdl3_image`.
|
# Migrating to SDL 3. Homebrew: `brew install sdl3 sdl3_image`.
|
||||||
|
# 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 CONFIG REQUIRED)
|
||||||
find_package(SDL3_image CONFIG REQUIRED)
|
find_package(SDL3_image CONFIG REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(KOBO_ENABLE_OPENGL)
|
if(KOBO_ENABLE_OPENGL)
|
||||||
find_package(OpenGL)
|
find_package(OpenGL)
|
||||||
@ -65,7 +85,17 @@ endif()
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Platform-specific install layout (matches the old autotools behavior)
|
# 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_BUNDLE_STYLE "macosx")
|
||||||
set(KOBO_EXEFILE "kobodl")
|
set(KOBO_EXEFILE "kobodl")
|
||||||
set(KOBO_DATA_DIR "EXE>>../Resources")
|
set(KOBO_DATA_DIR "EXE>>../Resources")
|
||||||
@ -139,6 +169,13 @@ if(UNIX AND NOT APPLE)
|
|||||||
target_link_libraries(kobo_deps INTERFACE m)
|
target_link_libraries(kobo_deps INTERFACE m)
|
||||||
endif()
|
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)
|
if(KOBO_ENABLE_EPM)
|
||||||
target_compile_options(kobo_deps INTERFACE
|
target_compile_options(kobo_deps INTERFACE
|
||||||
-Wall -Werror -Wwrite-strings -fno-builtin
|
-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)
|
target_link_libraries(kobodl PRIVATE graphics sound eel kobo_deps)
|
||||||
set_target_properties(kobodl PROPERTIES OUTPUT_NAME ${KOBO_EXEFILE})
|
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
|
# Install rules
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -189,6 +252,14 @@ elseif(KOBO_BUNDLE_STYLE STREQUAL "simple")
|
|||||||
install(TARGETS kobodl RUNTIME DESTINATION KoboDeluxe)
|
install(TARGETS kobodl RUNTIME DESTINATION KoboDeluxe)
|
||||||
install(DIRECTORY data/gfx data/sfx DESTINATION KoboDeluxe)
|
install(DIRECTORY data/gfx data/sfx DESTINATION KoboDeluxe)
|
||||||
install(FILES COPYING COPYING.LIB README ChangeLog TODO 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()
|
else()
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
install(TARGETS kobodl RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
install(TARGETS kobodl RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||||
|
|||||||
48
build-web.sh
Executable file
48
build-web.sh
Executable file
@ -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
|
||||||
@ -72,7 +72,7 @@ extern const enemy_kind bombdeto;
|
|||||||
extern const enemy_kind cannon;
|
extern const enemy_kind cannon;
|
||||||
extern const enemy_kind pipe1;
|
extern const enemy_kind pipe1;
|
||||||
extern const enemy_kind core;
|
extern const enemy_kind core;
|
||||||
extern const enemy_kind pipe2;
|
extern const enemy_kind pipe2k;
|
||||||
extern const enemy_kind rock;
|
extern const enemy_kind rock;
|
||||||
extern const enemy_kind ring;
|
extern const enemy_kind ring;
|
||||||
extern const enemy_kind enemy_m1;
|
extern const enemy_kind enemy_m1;
|
||||||
@ -434,7 +434,7 @@ inline int _enemy::realize()
|
|||||||
|
|
||||||
inline int _enemy::is_pipe()
|
inline int _enemy::is_pipe()
|
||||||
{
|
{
|
||||||
return ((_state != notuse) && ((ek == &pipe1) || (ek == &pipe2)));
|
return ((_state != notuse) && ((ek == &pipe1) || (ek == &pipe2k)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
18
enemy.cpp
18
enemy.cpp
@ -755,10 +755,10 @@ void _enemy::move_core()
|
|||||||
|
|
||||||
void _enemy::kill_core()
|
void _enemy::kill_core()
|
||||||
{
|
{
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3);
|
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3);
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7);
|
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7);
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1);
|
enemies.make(&pipe2k, 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, 5);
|
||||||
enemies.make(&explosion4, CS2PIXEL(x), CS2PIXEL(y));
|
enemies.make(&explosion4, CS2PIXEL(x), CS2PIXEL(y));
|
||||||
sound.g_base_core_explo(x, y);
|
sound.g_base_core_explo(x, y);
|
||||||
release();
|
release();
|
||||||
@ -978,19 +978,19 @@ void _enemy::move_pipe2()
|
|||||||
}
|
}
|
||||||
p ^= a;
|
p ^= a;
|
||||||
if(p & U_MASK)
|
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)
|
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)
|
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)
|
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);
|
manage.add_score(10);
|
||||||
release();
|
release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const enemy_kind pipe2 = {
|
const enemy_kind pipe2k = {
|
||||||
0,
|
0,
|
||||||
&_enemy::make_pipe2,
|
&_enemy::make_pipe2,
|
||||||
&_enemy::move_pipe2,
|
&_enemy::move_pipe2,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ add_library(graphics STATIC
|
|||||||
toolkit.cpp
|
toolkit.cpp
|
||||||
vidmodes.c
|
vidmodes.c
|
||||||
region.c
|
region.c
|
||||||
|
img_compat.c
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(graphics
|
target_include_directories(graphics
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "sdl_compat.h"
|
#include "sdl_compat.h"
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include "img_compat.h"
|
||||||
#include "sprite.h"
|
#include "sprite.h"
|
||||||
#include "filters.h"
|
#include "filters.h"
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
#include "gfxengine.h"
|
#include "gfxengine.h"
|
||||||
#include "filters.h"
|
#include "filters.h"
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include "img_compat.h"
|
||||||
#include "sdl_compat.h"
|
#include "sdl_compat.h"
|
||||||
#include "sofont.h"
|
#include "sofont.h"
|
||||||
#include "window.h"
|
#include "window.h"
|
||||||
|
|||||||
92
graphics/img_compat.c
Normal file
92
graphics/img_compat.c
Normal file
@ -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 <png.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
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__ */
|
||||||
32
graphics/img_compat.h
Normal file
32
graphics/img_compat.h
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* img_compat.h — SDL3_image shim.
|
||||||
|
*
|
||||||
|
* On native builds, this is just a passthrough to <SDL3_image/SDL_image.h>.
|
||||||
|
* 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 <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SDL_Surface *IMG_Load(const char *file);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#else /* native */
|
||||||
|
|
||||||
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* KOBO_IMG_COMPAT_H */
|
||||||
@ -30,7 +30,7 @@ TODO: tables as needed when loading banks.
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "sdl_compat.h"
|
#include "sdl_compat.h"
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include "img_compat.h"
|
||||||
#include "sprite.h"
|
#include "sprite.h"
|
||||||
#include "filters.h"
|
#include "filters.h"
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
#include "window.h"
|
#include "window.h"
|
||||||
#include "toolkit.h"
|
#include "toolkit.h"
|
||||||
|
|||||||
@ -1286,7 +1286,11 @@ void main_menu_t::build()
|
|||||||
button("Return to Intro", 0);
|
button("Return to Intro", 0);
|
||||||
else
|
else
|
||||||
button("Abort Current Game", 101);
|
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);
|
button("Quit Kobo Deluxe", MENU_TAG_CANCEL);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void main_menu_t::rebuild()
|
void main_menu_t::rebuild()
|
||||||
|
|||||||
62
web/shell.html
Normal file
62
web/shell.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!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>
|
||||||
Loading…
x
Reference in New Issue
Block a user