Compare commits

..

No commits in common. "6d073c136d48b37db16466fbe4590eacadee781c" and "acb3925bb4fd6b0805ed9f38b5dbfbafe5e2815b" have entirely different histories.

36 changed files with 260 additions and 2079 deletions

View File

@ -1,54 +0,0 @@
# Kobo Deluxe DOD Refactor — Benchmark Scoreboard
Each row is one phase of the refactor (see
`~/.claude/plans/can-you-explain-the-noble-wilkes.md`). Numbers are median
ns/frame from `KOBO_PROF=1 KOBO_STRESS=1500 ./build/kobodl`, ~1000 frames,
release build (`-O2 -g`).
## How to capture a baseline
```sh
cd build
cmake --build . -j
KOBO_PROF=1 KOBO_STRESS=1500 ./kobodl
# play through a stage; profile dumps every 600 frames to stderr.
# Redirect stderr to a file if you want to keep it:
KOBO_PROF=1 KOBO_STRESS=1500 ./kobodl 2>prof.log
```
The profiler dumps a table per zone with `calls`, `total_ms`, `ns/call`.
Record the `ns/call` for these zones (they are the moving parts the refactor
targets):
| Zone | What it measures |
|-----------------------|-----------------------------------------------------|
| `run_game` | Full game tick |
| `myship.move` | Player + bolt update |
| `enemies.move` | Realize pass + move pass combined |
| `enemies.realize` | Just the realize-reserved pass |
| `enemies.move_pass` | Just the per-enemy move pass (the hot loop) |
| `hit_structure` | Player vs map + bolt vs map |
| `hit_bolt` | Bolt-vs-enemy scan (called from inside enemy move) |
## Scoreboard
| Phase | run_game | myship.move | enemies.move_pass | hit_bolt | Notes |
|------------------------------|---------:|------------:|------------------:|---------:|-------|
| 0 — baseline | ? | ? | ? | ? | TBD — user to capture |
| 1 — hot/cold split | | | | | |
| 2 — SoA pool | | | | | |
| 3 — bucket dispatch | | | | | |
| 4 — flipped bolt collision | | | | | |
| 5 — render decouple | | | | | |
| 6 — SIMD integrator (opt.) | | | | | |
## Notes
- `KOBO_STRESS=N` keeps the pool topped up with rocks; N=1500 sits well
below ENEMY_MAX=2048 so the spawner doesn't thrash on full-pool checks.
- The stress spawner uses `pubrand`, which is acceptable for benchmarking
but will *not* produce identical demos across builds — that's fine, we
only care about steady-state cost.
- For a stable run, start the game, get into a stage, then let it sit for
~1000 frames before reading numbers. The first dump (frame 600) will
include startup costs and should be discarded.

View File

@ -14,13 +14,6 @@ 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
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -43,22 +36,8 @@ 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 find_package(SDL3 CONFIG REQUIRED)
# libpng + a small shim (graphics/img_compat.c) for IMG_Load. The find_package(SDL3_image CONFIG REQUIRED)
# `--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)
find_package(CURL REQUIRED)
endif()
if(KOBO_ENABLE_OPENGL) if(KOBO_ENABLE_OPENGL)
find_package(OpenGL) find_package(OpenGL)
@ -86,19 +65,7 @@ endif()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Platform-specific install layout (matches the old autotools behavior) # Platform-specific install layout (matches the old autotools behavior)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if(EMSCRIPTEN) if(APPLE)
# 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")
# index.html so the game can be served from a clean subpath URL
# (e.g. https://lindholm.dev/kobo/ no explicit filename).
set(KOBO_EXEFILE "index.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")
@ -172,13 +139,6 @@ 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
@ -200,52 +160,14 @@ set(KOBO_SOURCES
myship.cpp radar.cpp random.cpp scenes.cpp score.cpp screen.cpp myship.cpp radar.cpp random.cpp scenes.cpp score.cpp screen.cpp
filemap.cpp prefs.cpp cfgform.cpp options.cpp gamestate.cpp filemap.cpp prefs.cpp cfgform.cpp options.cpp gamestate.cpp
states.cpp form.cpp cfgparse.cpp game.cpp kobo.cpp logger.c states.cpp form.cpp cfgparse.cpp game.cpp kobo.cpp logger.c
dashboard.cpp sound.cpp prof.cpp bench.cpp leaderboard.cpp dashboard.cpp sound.cpp
) )
if(EMSCRIPTEN)
list(APPEND KOBO_SOURCES httpclient_web.cpp)
else()
list(APPEND KOBO_SOURCES httpclient_native.cpp)
endif()
add_executable(kobodl ${KOBO_SOURCES}) add_executable(kobodl ${KOBO_SOURCES})
target_include_directories(kobodl PRIVATE ${CMAKE_SOURCE_DIR}/data/sfx) 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)
target_link_options(kobodl PRIVATE -sFETCH=1)
else()
target_link_libraries(kobodl PRIVATE CURL::libcurl)
endif()
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
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -267,14 +189,6 @@ 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 index.html / .js / .wasm / .data install all four.
install(TARGETS kobodl RUNTIME DESTINATION web)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/index.js
${CMAKE_CURRENT_BINARY_DIR}/index.wasm
${CMAKE_CURRENT_BINARY_DIR}/index.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})

View File

@ -1,35 +0,0 @@
#include "bench.h"
#include <stdio.h>
#include <stdlib.h>
int kobo_bench_enabled = 0;
int kobo_bench_frames = 1800;
Uint32 kobo_bench_seed = 12345;
extern "C" void kobo_bench_init_env(void)
{
const char *e = getenv("KOBO_BENCH");
kobo_bench_enabled = (e && *e && *e != '0') ? 1 : 0;
const char *f = getenv("KOBO_BENCH_FRAMES");
if (f && *f) kobo_bench_frames = atoi(f);
const char *s = getenv("KOBO_BENCH_SEED");
if (s && *s) kobo_bench_seed = (Uint32)strtoul(s, NULL, 10);
if (!kobo_bench_enabled) return;
// Default a steady stress workload so the move pass has predictable cost.
if (!getenv("KOBO_STRESS"))
setenv("KOBO_STRESS", "1500", 1);
// Profiler is the whole point of running in bench mode.
if (!getenv("KOBO_PROF"))
setenv("KOBO_PROF", "1", 1);
fprintf(stderr,
"[bench] enabled: frames=%d seed=%u stress=%s prof=%s\n",
kobo_bench_frames,
(unsigned)kobo_bench_seed,
getenv("KOBO_STRESS"),
getenv("KOBO_PROF"));
}

36
bench.h
View File

@ -1,36 +0,0 @@
// Deterministic, hands-off benchmark mode.
//
// Activated by KOBO_BENCH=1 in the environment. When on, the game:
// * skips the intro/menu sequence and drops straight into stage 0
// * seeds gamerand and pubrand from KOBO_BENCH_SEED (default 12345)
// * enables cmd_indicator (collisions register but neither side takes
// damage — true invulnerability for deterministic workload),
// cmd_cheat (skip score recording), and always_fire (auto-fire)
// * defaults KOBO_STRESS=1500 unless overridden, for a steady workload
// * dumps the profiler table and exits after KOBO_BENCH_FRAMES
// run_game frames (default 1800 = ~60s at the game's tick rate)
//
// Together this makes a single command (`KOBO_BENCH=1 ./kobodl`) produce a
// reproducible profiler dump across phases — no menu navigation, no keyboard
// input, identical RNG and spawn pattern run-to-run.
#ifndef KOBO_BENCH_H
#define KOBO_BENCH_H
#include "sdl_compat.h"
#ifdef __cplusplus
extern "C" {
#endif
extern int kobo_bench_enabled;
extern int kobo_bench_frames;
extern Uint32 kobo_bench_seed;
void kobo_bench_init_env(void);
#ifdef __cplusplus
}
#endif
#endif // KOBO_BENCH_H

View File

@ -1,48 +0,0 @@
#!/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/index.html"
echo "Serve: ./build-web.sh serve (then open http://localhost:8000/)"
;;
esac

View File

@ -309,8 +309,8 @@ void dashboard_window_t::nibble(int tool)
} }
gengine->invalidate(); gengine->invalidate();
gengine->flip(); gengine->flip();
// SDL 3: one logical back buffer; previously a second pass was if(gengine->doublebuffer())
// only needed when actual hardware double-buffering was in use. {
for(i = 0; i < dt * 4; ++i) for(i = 0; i < dt * 4; ++i)
{ {
if(last_index >= NIBBLE_TILES) if(last_index >= NIBBLE_TILES)
@ -337,6 +337,7 @@ void dashboard_window_t::nibble(int tool)
++last_index; ++last_index;
} }
} }
}
mode(DASHBOARD_BLACK); mode(DASHBOARD_BLACK);
gengine->flip(); gengine->flip();
} }

View File

@ -24,7 +24,6 @@
#include "enemies.h" #include "enemies.h"
#include "random.h" #include "random.h"
#include "radar.h" #include "radar.h"
#include "prof.h"
_enemy _enemies::enemy[ENEMY_MAX]; _enemy _enemies::enemy[ENEMY_MAX];
_enemy *_enemies::enemy_max; _enemy *_enemies::enemy_max;
@ -100,20 +99,14 @@ int _enemies::init()
void _enemies::move() void _enemies::move()
{ {
_enemy *enemyp; _enemy *enemyp;
{
PROF_ZONE("enemies.realize");
/* realize reserved enemies */ /* realize reserved enemies */
for(enemyp = enemy; enemyp < enemy + ENEMY_MAX; enemyp++) for(enemyp = enemy; enemyp < enemy + ENEMY_MAX; enemyp++)
{ {
if(enemyp->realize()) if(enemyp->realize())
enemy_max = enemyp; enemy_max = enemyp;
} }
}
{
PROF_ZONE("enemies.move_pass");
for(enemyp = enemy; enemyp <= enemy_max; enemyp++) for(enemyp = enemy; enemyp <= enemy_max; enemyp++)
enemyp->move(); enemyp->move();
}
} }
void _enemies::move_intro() void _enemies::move_intro()

View File

@ -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 pipe2k; extern const enemy_kind pipe2;
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 == &pipe2k))); return ((_state != notuse) && ((ek == &pipe1) || (ek == &pipe2)));
} }

View File

@ -755,10 +755,10 @@ void _enemy::move_core()
void _enemy::kill_core() void _enemy::kill_core()
{ {
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3); enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3);
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7); enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7);
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1); enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1);
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5); enemies.make(&pipe2, 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(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1); enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1);
if(p & R_MASK) if(p & R_MASK)
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3); enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3);
if(p & D_MASK) if(p & D_MASK)
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5); enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5);
if(p & L_MASK) if(p & L_MASK)
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7); enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7);
manage.add_score(10); manage.add_score(10);
release(); release();
} }
const enemy_kind pipe2k = { const enemy_kind pipe2 = {
0, 0,
&_enemy::make_pipe2, &_enemy::make_pipe2,
&_enemy::move_pipe2, &_enemy::move_pipe2,

View File

@ -9,7 +9,6 @@ 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

View File

@ -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 "img_compat.h" #include <SDL3_image/SDL_image.h>
#include "sprite.h" #include "sprite.h"
#include "filters.h" #include "filters.h"

View File

@ -28,7 +28,7 @@
#include "gfxengine.h" #include "gfxengine.h"
#include "filters.h" #include "filters.h"
#include "img_compat.h" #include <SDL3_image/SDL_image.h>
#include "sdl_compat.h" #include "sdl_compat.h"
#include "sofont.h" #include "sofont.h"
#include "window.h" #include "window.h"
@ -54,10 +54,15 @@ gfxengine_t::gfxengine_t()
sf1 = df = dsf = acf = NULL; sf1 = df = dsf = acf = NULL;
gfx = NULL; gfx = NULL;
csengine = NULL; csengine = NULL;
_driver = GFX_DRIVER_SDL2D;
_shadow = 1;
_doublebuf = 1;
_pages = -1;
_vsync = 1; _vsync = 1;
_fullscreen = 0; _fullscreen = 0;
_centered = 0; _centered = 0;
use_interpolation = 1; use_interpolation = 1;
_depth = 0;
_title = "GfxEngine v0.4"; _title = "GfxEngine v0.4";
_icontitle = "GfxEngine"; _icontitle = "GfxEngine";
_cursor = 1; _cursor = 1;
@ -66,9 +71,11 @@ gfxengine_t::gfxengine_t()
_autoinvalidate = 1; _autoinvalidate = 1;
_scalemode = GFX_SCALE_NEAREST; _scalemode = GFX_SCALE_NEAREST;
_clamping = 0; _clamping = 0;
xflags = 0;
_dither = 0; _dither = 0;
_dither_type = 0; _dither_type = 0;
broken_rgba8 = 0;
alpha_threshold = 0; alpha_threshold = 0;
_brightness = 1.0; _brightness = 1.0;
@ -87,7 +94,10 @@ gfxengine_t::gfxengine_t()
for(int i = 0; i < CS_LAYERS ; ++i) for(int i = 0; i < CS_LAYERS ; ++i)
xratio[i] = yratio[i] = 0.0; xratio[i] = yratio[i] = 0.0;
dirtyrects = 0; dirtyrects[0] = 0;
dirtyrects[1] = 0;
frontpage = 0;
backpage = 1;
screenshot_count = 0; screenshot_count = 0;
} }
@ -150,23 +160,75 @@ void gfxengine_t::scale(float x, float y)
void gfxengine_t::mode(int bits, int fullscreen) void gfxengine_t::mode(int bits, int fullscreen)
{ {
// SDL 3 uses the renderer's pixel format directly; the legacy depth
// argument is accepted for API compatibility but ignored.
(void)bits;
int was_showing = is_showing; int was_showing = is_showing;
hide(); hide();
int olddepth = _depth;
_depth = bits;
if(_depth != olddepth)
reload();
_fullscreen = fullscreen; _fullscreen = fullscreen;
if(was_showing) if(was_showing)
show(); show();
} }
void gfxengine_t::driver(gfx_drivers_t drv)
{
int was_showing = is_showing;
hide();
_driver = drv;
if(was_showing)
show();
}
void gfxengine_t::doublebuffer(int use)
{
if(_doublebuf == use)
return;
int was_showing = is_showing;
hide();
_doublebuf = use;
if(was_showing)
show();
}
void gfxengine_t::pages(int np)
{
_pages = np;
}
void gfxengine_t::vsync(int use) void gfxengine_t::vsync(int use)
{ {
if(_vsync == use) if(_vsync == use)
return; return;
int was_showing = is_showing;
hide();
_vsync = use; _vsync = use;
if(sdl_renderer)
SDL_SetRenderVSync(sdl_renderer, use ? 1 : 0); if(was_showing)
show();
}
void gfxengine_t::shadow(int use)
{
if(_shadow == use)
return;
int was_showing = is_showing;
hide();
_shadow = use;
if(was_showing)
show();
} }
void gfxengine_t::autoinvalidate(int use) void gfxengine_t::autoinvalidate(int use)
@ -373,9 +435,9 @@ void gfxengine_t::clampcolor(Uint8 r, Uint8 g, Uint8 b, Uint8 a)
void gfxengine_t::dither(int type, int _broken_rgba8) void gfxengine_t::dither(int type, int _broken_rgba8)
{ {
(void)_broken_rgba8; // legacy OpenGL workaround; no-op in SDL 3
_dither = type >= 0; _dither = type >= 0;
_dither_type = type; _dither_type = type;
broken_rgba8 = _broken_rgba8;
if(!df) if(!df)
return; return;
@ -616,8 +678,10 @@ void gfxengine_t::on_frame(cs_engine_t *e)
} }
int gfxengine_t::open(int objects) int gfxengine_t::open(int objects, int extraflags)
{ {
xflags = extraflags;
if(is_open) if(is_open)
return show(); return show();
@ -853,23 +917,42 @@ void gfxengine_t::hide(void)
void gfxengine_t::invalidate(SDL_Rect *rect, window_t *window) void gfxengine_t::invalidate(SDL_Rect *rect, window_t *window)
{ {
__invalidate(rect, window); switch(_pages)
{
case -1:
if(_doublebuf)
__invalidate(1, rect, window);
__invalidate(0, rect, window);
break;
case 0:
__invalidate(0, NULL, NULL);
break;
case 3:
__invalidate(2, rect, window);
// Fallthrough!
case 2:
__invalidate(1, rect, window);
// Fallthrough!
case 1:
__invalidate(0, rect, window);
break;
}
} }
void gfxengine_t::__invalidate(SDL_Rect *rect, window_t *window) void gfxengine_t::__invalidate(int page, SDL_Rect *rect, window_t *window)
{ {
if(!screen_surface) if(!screen_surface)
return; return;
if(!rect) if(!rect || (_pages == 0))
{ {
dirtyrects = 1; dirtyrects[page] = 1;
dirtytable[0].x = 0; dirtytable[page][0].x = 0;
dirtytable[0].y = 0; dirtytable[page][0].y = 0;
dirtytable[0].w = screen_surface->w; dirtytable[page][0].w = screen_surface->w;
dirtytable[0].h = screen_surface->h; dirtytable[page][0].h = screen_surface->h;
dirtywtable[0] = NULL; dirtywtable[page][0] = NULL;
return; return;
} }
@ -906,18 +989,19 @@ void gfxengine_t::__invalidate(SDL_Rect *rect, window_t *window)
if(!dr.w || !dr.h) if(!dr.w || !dr.h)
return; return;
for(int i = 0; i < dirtyrects; ++i) for(int i = 0; i < dirtyrects[page]; ++i)
if(memcmp(&dirtytable[i], &dr, sizeof(dr)) == 0) if(memcmp(&dirtytable[page][i], &dr, sizeof(dr)) == 0)
return; return;
if(dirtyrects < MAX_DIRTYRECTS - 1) if(dirtyrects[page] < MAX_DIRTYRECTS - 1)
{ {
dirtytable[dirtyrects] = dr; dirtytable[page][dirtyrects[page]] = dr;
dirtywtable[dirtyrects] = window; dirtywtable[page][dirtyrects[page]] = window;
++dirtyrects; ++dirtyrects[page];
} }
else else
log_printf(ELOG, "gfxengine: Out of dirtyrects!\n"); log_printf(ELOG, "gfxengine: Page %d out of dirtyrects!\n",
page);
} }
@ -1184,19 +1268,20 @@ void gfxengine_t::flip()
// buffer per frame. Keep the dirtyrect iteration only to drive the // buffer per frame. Keep the dirtyrect iteration only to drive the
// phys_refresh() callbacks that window widgets rely on; the upload // phys_refresh() callbacks that window widgets rely on; the upload
// is a single full-surface copy regardless. // is a single full-surface copy regardless.
for(int i = 0; i < dirtyrects; ++i) frontpage = backpage = 0;
for(int i = 0; i < dirtyrects[backpage]; ++i)
{ {
if(dirtywtable[i]) if(dirtywtable[backpage][i])
{ {
if(!dirtywtable[i]->visible()) if(!dirtywtable[backpage][i]->visible())
continue; continue;
SDL_Rect dr = dirtytable[i]; SDL_Rect dr = dirtytable[backpage][i];
dirtywtable[i]->phys_refresh(&dr); dirtywtable[backpage][i]->phys_refresh(&dr);
} }
else else
refresh_rect(&dirtytable[i]); refresh_rect(&dirtytable[backpage][i]);
} }
dirtyrects = 0; dirtyrects[backpage] = 0;
SDL_UpdateTexture(fb_texture, NULL, softbuf->pixels, softbuf->pitch); SDL_UpdateTexture(fb_texture, NULL, softbuf->pixels, softbuf->pitch);
SDL_RenderClear(sdl_renderer); SDL_RenderClear(sdl_renderer);

View File

@ -24,6 +24,7 @@
#define GFX_BANKS 256 #define GFX_BANKS 256
#define MAX_DIRTYRECTS 1024 #define MAX_DIRTYRECTS 1024
#define MAX_PAGES 3
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -32,6 +33,11 @@
#include "sprite.h" #include "sprite.h"
#include "cs.h" #include "cs.h"
enum gfx_drivers_t
{
GFX_DRIVER_SDL2D = 0
};
enum gfx_scalemodes_t enum gfx_scalemodes_t
{ {
GFX_SCALE_NEAREST = 0, GFX_SCALE_NEAREST = 0,
@ -63,11 +69,25 @@ class gfxengine_t
void size(int w, int h); void size(int w, int h);
void centered(int c); void centered(int c);
void scale(float x, float y); void scale(float x, float y);
void driver(gfx_drivers_t drv);
void mode(int bits, int fullscreen); void mode(int bits, int fullscreen);
// 1: Use double buffering if possible
void doublebuffer(int use);
// -1: Use default for shadow() and doublebuffer() settings
// 0: None; assume flipping gives you a garbage buffer
// 1: Assume flipping leaves the back buffer intact
// 2: Assume two buffers that are swapped when flipping
// 3: Assume three buffers cycled when flipping
void pages(int np);
// 1: Enable vsync, if available // 1: Enable vsync, if available
void vsync(int use); void vsync(int use);
// 1: Use a software shadow back buffer, if possible
void shadow(int use);
void autoinvalidate(int use); void autoinvalidate(int use);
void interpolation(int inter); void interpolation(int inter);
@ -82,10 +102,12 @@ class gfxengine_t
void wrap(int x, int y); void wrap(int x, int y);
/* Info */ /* Info */
int doublebuffer() { return _doublebuf; }
int shadow() { return _shadow; }
int autoinvalidate() { return _autoinvalidate; } int autoinvalidate() { return _autoinvalidate; }
/* Engine open/close */ /* Engine open/close */
int open(int objects = 1024); int open(int objects = 1024, int extraflags = 0);
void close(); void close();
/* Data management (use while engine is open) */ /* Data management (use while engine is open) */
@ -184,6 +206,7 @@ class gfxengine_t
float yscale() { return ys * (1.f/256.f); } float yscale() { return ys * (1.f/256.f); }
protected: protected:
gfx_drivers_t _driver;
gfx_scalemodes_t _scalemode; gfx_scalemodes_t _scalemode;
int _clamping; int _clamping;
SDL_Window *sdl_window; SDL_Window *sdl_window;
@ -191,9 +214,11 @@ class gfxengine_t
SDL_Texture *fb_texture; SDL_Texture *fb_texture;
SDL_Surface *screen_surface; // alias for softbuf in SDL 3 SDL_Surface *screen_surface; // alias for softbuf in SDL 3
SDL_Surface *softbuf; SDL_Surface *softbuf;
int dirtyrects; int backpage;
SDL_Rect dirtytable[MAX_DIRTYRECTS]; int frontpage;
window_t *dirtywtable[MAX_DIRTYRECTS]; int dirtyrects[MAX_PAGES];
SDL_Rect dirtytable[MAX_PAGES][MAX_DIRTYRECTS];
window_t *dirtywtable[MAX_PAGES][MAX_DIRTYRECTS];
window_t *fullwin; window_t *fullwin;
window_t *window; window_t *window;
window_t *windows; // Linked list window_t *windows; // Linked list
@ -211,17 +236,23 @@ class gfxengine_t
s_container_t *gfx; s_container_t *gfx;
SoFont *fonts[GFX_BANKS]; // Kludge. SoFont *fonts[GFX_BANKS]; // Kludge.
cs_engine_t *csengine; cs_engine_t *csengine;
int xflags;
int _doublebuf;
int _pages;
int _vsync; int _vsync;
int _shadow;
int _fullscreen; int _fullscreen;
int _centered; int _centered;
int _autoinvalidate; int _autoinvalidate;
int use_interpolation; int use_interpolation;
int _width, _height; int _width, _height;
int _depth;
const char *_title; const char *_title;
const char *_icontitle; const char *_icontitle;
int _cursor; int _cursor;
int _dither; int _dither;
int _dither_type; int _dither_type;
int broken_rgba8; //Klugde for OpenGL (if RGBA8 ==> RGBA4)
int alpha_threshold; //For noalpha() int alpha_threshold; //For noalpha()
float _brightness; float _brightness;
float _contrast; float _contrast;
@ -236,7 +267,8 @@ class gfxengine_t
int screenshot_count; int screenshot_count;
void __invalidate(SDL_Rect *rect = NULL, window_t *window = NULL); void __invalidate(int page, SDL_Rect *rect = NULL,
window_t *window = NULL);
void refresh_rect(SDL_Rect *r); void refresh_rect(SDL_Rect *r);
static void on_frame(cs_engine_t *e); static void on_frame(cs_engine_t *e);

View File

@ -1,92 +0,0 @@
/*
* 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__ */

View File

@ -1,32 +0,0 @@
/*
* 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 */

View File

@ -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 "img_compat.h" #include <SDL3_image/SDL_image.h>
#include "sprite.h" #include "sprite.h"
#include "filters.h" #include "filters.h"

View File

@ -23,7 +23,6 @@
#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"

View File

@ -1,44 +0,0 @@
/*
* httpclient.h thin async HTTP client.
*
* Two impls share this header:
* httpclient_native.cpp libcurl-multi, polled from http_pump().
* httpclient_web.cpp emscripten_fetch; http_pump() is a no-op.
*
* Callbacks fire on the main thread: native impls dispatch from http_pump(),
* web from the Emscripten runtime (which is already main-thread).
*
* The `body` pointer in http_response_t is only valid for the duration of
* the callback copy what you need.
*/
#ifndef KOBO_HTTPCLIENT_H
#define KOBO_HTTPCLIENT_H
#include <stddef.h>
typedef struct http_response_t {
int status; // HTTP status code; 0 = network/transport failure
const char *body; // NUL-terminated; valid only during the callback
size_t len;
} http_response_t;
typedef void (*http_callback_t)(const http_response_t *resp, void *user);
#ifdef __cplusplus
extern "C" {
#endif
void http_init(void);
void http_shutdown(void);
void http_post_json(const char *url, const char *body,
http_callback_t cb, void *user);
void http_get(const char *url, http_callback_t cb, void *user);
void http_pump(void);
#ifdef __cplusplus
}
#endif
#endif /* KOBO_HTTPCLIENT_H */

View File

@ -1,175 +0,0 @@
/*
* httpclient_native.cpp libcurl-multi backend.
*
* Single CURLM handle owned by this module. http_post_json / http_get queue
* an easy handle; http_pump() runs curl_multi_perform and drains finished
* transfers, firing user callbacks exactly once.
*
* Failure modes (DNS, connect refused, timeout, etc.) are surfaced as
* status=0 in the response caller doesn't have to distinguish curl error
* codes from HTTP errors.
*/
#include "httpclient.h"
#include "kobolog.h"
#include <curl/curl.h>
#include <string>
#include <unordered_map>
namespace {
CURLM *g_multi = nullptr;
struct pending_t {
http_callback_t cb;
void *user;
std::string body;
curl_slist *headers = nullptr;
};
// Keyed by CURL* easy handle so curl_multi_info_read results can be routed.
std::unordered_map<CURL *, pending_t *> g_pending;
size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
{
pending_t *p = static_cast<pending_t *>(userdata);
p->body.append(ptr, size * nmemb);
return size * nmemb;
}
void start_request(CURL *easy, pending_t *pending)
{
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(easy, CURLOPT_WRITEDATA, pending);
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 10L);
curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT, 5L);
curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(easy, CURLOPT_USERAGENT, "kobodl/0.5.1");
g_pending[easy] = pending;
CURLMcode rc = curl_multi_add_handle(g_multi, easy);
if(rc != CURLM_OK)
{
log_printf(ELOG, "http: curl_multi_add_handle failed: %s\n",
curl_multi_strerror(rc));
// Synthesize a transport failure callback.
http_response_t resp{0, "", 0};
pending->cb(&resp, pending->user);
g_pending.erase(easy);
if(pending->headers) curl_slist_free_all(pending->headers);
curl_easy_cleanup(easy);
delete pending;
}
}
} // namespace
extern "C" void http_init(void)
{
if(g_multi) return;
curl_global_init(CURL_GLOBAL_DEFAULT);
g_multi = curl_multi_init();
}
extern "C" void http_shutdown(void)
{
if(!g_multi) return;
// Abandon any in-flight handles — we're shutting down, callbacks are
// moot. Free the resources to avoid leak reports.
for(auto &kv : g_pending)
{
curl_multi_remove_handle(g_multi, kv.first);
if(kv.second->headers) curl_slist_free_all(kv.second->headers);
curl_easy_cleanup(kv.first);
delete kv.second;
}
g_pending.clear();
curl_multi_cleanup(g_multi);
g_multi = nullptr;
curl_global_cleanup();
}
extern "C" void http_post_json(const char *url, const char *body,
http_callback_t cb, void *user)
{
if(!g_multi || !cb) return;
CURL *easy = curl_easy_init();
if(!easy)
{
http_response_t resp{0, "", 0};
cb(&resp, user);
return;
}
auto *pending = new pending_t;
pending->cb = cb;
pending->user = user;
pending->headers = curl_slist_append(nullptr, "Content-Type: application/json");
curl_easy_setopt(easy, CURLOPT_URL, url);
curl_easy_setopt(easy, CURLOPT_POST, 1L);
curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, body ? body : "");
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, pending->headers);
start_request(easy, pending);
}
extern "C" void http_get(const char *url, http_callback_t cb, void *user)
{
if(!g_multi || !cb) return;
CURL *easy = curl_easy_init();
if(!easy)
{
http_response_t resp{0, "", 0};
cb(&resp, user);
return;
}
auto *pending = new pending_t;
pending->cb = cb;
pending->user = user;
curl_easy_setopt(easy, CURLOPT_URL, url);
curl_easy_setopt(easy, CURLOPT_HTTPGET, 1L);
start_request(easy, pending);
}
extern "C" void http_pump(void)
{
if(!g_multi) return;
int running = 0;
curl_multi_perform(g_multi, &running);
CURLMsg *msg;
int msgs_left = 0;
while((msg = curl_multi_info_read(g_multi, &msgs_left)) != nullptr)
{
if(msg->msg != CURLMSG_DONE) continue;
CURL *easy = msg->easy_handle;
auto it = g_pending.find(easy);
if(it == g_pending.end())
{
curl_multi_remove_handle(g_multi, easy);
curl_easy_cleanup(easy);
continue;
}
pending_t *p = it->second;
long status = 0;
if(msg->data.result == CURLE_OK)
curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &status);
else
log_printf(DLOG, "http: transport error: %s\n",
curl_easy_strerror(msg->data.result));
http_response_t resp;
resp.status = (int)status;
resp.body = p->body.c_str();
resp.len = p->body.size();
p->cb(&resp, p->user);
curl_multi_remove_handle(g_multi, easy);
if(p->headers) curl_slist_free_all(p->headers);
curl_easy_cleanup(easy);
g_pending.erase(it);
delete p;
}
}

View File

@ -1,95 +0,0 @@
/*
* httpclient_web.cpp emscripten_fetch backend.
*
* emscripten_fetch is async and fires onsuccess/onerror on the main thread.
* We attach a small heap-owned context to userData; the trampolines invoke
* the user callback and free the context.
*
* http_pump() is a no-op because the runtime drives completion delivery.
*/
#ifdef __EMSCRIPTEN__
#include "httpclient.h"
#include <emscripten/fetch.h>
#include <stdlib.h>
#include <string.h>
namespace {
struct ctx_t {
http_callback_t cb;
void *user;
};
void deliver(emscripten_fetch_t *fetch, int status)
{
ctx_t *c = static_cast<ctx_t *>(fetch->userData);
http_response_t resp;
resp.status = status;
resp.body = fetch->data ? fetch->data : "";
resp.len = (size_t)fetch->numBytes;
c->cb(&resp, c->user);
emscripten_fetch_close(fetch);
delete c;
}
void on_success(emscripten_fetch_t *fetch)
{
deliver(fetch, (int)fetch->status);
}
void on_error(emscripten_fetch_t *fetch)
{
// Distinguish: a non-2xx with body (e.g. 500 + JSON) reports its status;
// a true transport failure (no connection, CORS reject) reports 0.
int status = (fetch->status >= 100 && fetch->status < 600)
? (int)fetch->status : 0;
deliver(fetch, status);
}
void issue(const char *method, const char *url, const char *body,
http_callback_t cb, void *user)
{
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strncpy(attr.requestMethod, method, sizeof(attr.requestMethod) - 1);
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
attr.onsuccess = on_success;
attr.onerror = on_error;
static const char *headers[] = {
"Content-Type", "application/json", nullptr
};
if(body)
{
attr.requestHeaders = headers;
attr.requestData = body;
attr.requestDataSize = strlen(body);
}
auto *c = new ctx_t{cb, user};
attr.userData = c;
emscripten_fetch(&attr, url);
}
} // namespace
extern "C" void http_init(void) {}
extern "C" void http_shutdown(void) {}
extern "C" void http_pump(void) {}
extern "C" void http_post_json(const char *url, const char *body,
http_callback_t cb, void *user)
{
if(!cb) return;
issue("POST", url, body ? body : "", cb, user);
}
extern "C" void http_get(const char *url, http_callback_t cb, void *user)
{
if(!cb) return;
issue("GET", url, nullptr, cb, user);
}
#endif /* __EMSCRIPTEN__ */

View File

@ -61,9 +61,6 @@ extern "C" {
#include "options.h" #include "options.h"
#include "myship.h" #include "myship.h"
#include "enemies.h" #include "enemies.h"
#include "prof.h"
#include "bench.h"
#include "httpclient.h"
#define MAX_FPS_RESULTS 64 #define MAX_FPS_RESULTS 64
@ -638,6 +635,7 @@ int KOBO_main::init_display(prefs_t *p)
int dw, dh; // Display size int dw, dh; // Display size
int gw, gh; // Game "window" size int gw, gh; // Game "window" size
gengine->title("Kobo Deluxe " VERSION, "kobodl"); gengine->title("Kobo Deluxe " VERSION, "kobodl");
gengine->driver((gfx_drivers_t)p->videodriver);
dw = p->width; dw = p->width;
dh = p->height; dh = p->height;
@ -691,7 +689,10 @@ int KOBO_main::init_display(prefs_t *p)
gengine->size(dw, dh); gengine->size(dw, dh);
gengine->mode(0, p->fullscreen); gengine->mode(0, p->fullscreen);
gengine->doublebuffer(p->doublebuf);
gengine->pages(p->pages);
gengine->vsync(p->vsync); gengine->vsync(p->vsync);
gengine->shadow(p->shadow);
gengine->cursor(0); gengine->cursor(0);
gengine->period(game.speed); gengine->period(game.speed);
@ -1085,7 +1086,7 @@ int KOBO_main::load_graphics(prefs_t *p)
if(gd->flags & KOBO_NODITHER || !p->use_dither) if(gd->flags & KOBO_NODITHER || !p->use_dither)
gengine->dither(-1); gengine->dither(-1);
else else
gengine->dither(p->dither_type, 0); gengine->dither(p->dither_type, p->broken_rgba8);
// Alpha channels // Alpha channels
if(gd->flags & KOBO_NOALPHA || !p->alpha) if(gd->flags & KOBO_NOALPHA || !p->alpha)
@ -1390,40 +1391,12 @@ int KOBO_main::open()
ct_engine.render_highlight = kobo_render_highlight; ct_engine.render_highlight = kobo_render_highlight;
wdash->mode(DASHBOARD_GAME); wdash->mode(DASHBOARD_GAME);
wradar->mode(RM_NOISE); wradar->mode(RM_NOISE);
// Bench mode overrides flags that downstream init reads (always_fire is
// captured by gamecontrol.init at call-time, so it has to be set before).
if(kobo_bench_enabled)
{
// cmd_indicator is "collision testing mode" — collisions still
// register sounds but neither side takes damage, so the ship
// can't die and the bench workload doesn't drift across runs.
// (cmd_cheat is *unlimited lives*, not invulnerability.)
prefs->cmd_indicator = 1;
prefs->cmd_cheat = 1; // skip score recording on exit
prefs->always_fire = 1; // constant auto-fire
prefs->countdown = 0; // auto-pop the "GET READY" wait
}
if(kobo_bench_enabled)
pubrand.init(kobo_bench_seed);
else
pubrand.init(); pubrand.init();
init_js(prefs); init_js(prefs);
gamecontrol.init(prefs->always_fire); gamecontrol.init(prefs->always_fire);
manage.init(); manage.init();
if(kobo_bench_enabled)
{
// Hands-off bench: jump straight into stage 0, no menu navigation.
scorefile.profile()->skill = SKILL_CLASSIC;
manage.set_scene_num(0);
gsm.push(&st_game);
}
else
{
gsm.push(&st_intro_title); gsm.push(&st_intro_title);
}
return 0; return 0;
} }
@ -1685,9 +1658,14 @@ void kobo_gfxengine_t::frame()
{ {
km.pause_game(); km.pause_game();
prefs->fullscreen = 0; prefs->fullscreen = 0;
prefs->videodriver = (int)GFX_DRIVER_SDL2D;
prefs->width = 640; prefs->width = 640;
prefs->height = 480; prefs->height = 480;
prefs->aspect = 1000; prefs->aspect = 1000;
prefs->depth = 0;
prefs->doublebuf = 0;
prefs->pages = -1;
prefs->shadow = 1;
prefs->scalemode = (int)GFX_SCALE_NEAREST; prefs->scalemode = (int)GFX_SCALE_NEAREST;
prefs->brightness = 100; prefs->brightness = 100;
prefs->contrast = 100; prefs->contrast = 100;
@ -1861,7 +1839,6 @@ void kobo_gfxengine_t::frame()
/* /*
* Run the current gamestate for one frame * Run the current gamestate for one frame
*/ */
http_pump();
gsm.frame(); gsm.frame();
if(prefs->cmd_autoshot && !manage.game_stopped()) if(prefs->cmd_autoshot && !manage.game_stopped())
@ -2086,11 +2063,7 @@ int main(int argc, char *argv[])
signal(SIGTERM, breakhandler); signal(SIGTERM, breakhandler);
signal(SIGINT, breakhandler); signal(SIGINT, breakhandler);
kobo_bench_init_env();
prof_init();
SDL_Init(0); SDL_Init(0);
http_init();
if(main_init()) if(main_init())
{ {
@ -2114,31 +2087,6 @@ int main(int argc, char *argv[])
return 1; return 1;
} }
// First-run client_id generation. Stable per-install identifier sent
// with leaderboard submissions for rate-limiting/dedup (not used as
// the public identity — that's name + board_name).
if(prefs->client_id[0] == '\0')
{
// Not cryptographically random — this is a rate-limit dedup
// token, not a secret. SDL_rand_bits is sufficient.
Uint8 b[16];
for(int i = 0; i < 16; i += 4)
{
Uint32 r = SDL_rand_bits();
b[i] = (Uint8)(r >> 24);
b[i+1] = (Uint8)(r >> 16);
b[i+2] = (Uint8)(r >> 8);
b[i+3] = (Uint8)r;
}
b[6] = (b[6] & 0x0F) | 0x40; // RFC 4122 v4
b[8] = (b[8] & 0x3F) | 0x80;
snprintf(prefs->client_id, sizeof(prefs->client_id),
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
b[0],b[1],b[2],b[3], b[4],b[5], b[6],b[7],
b[8],b[9], b[10],b[11],b[12],b[13],b[14],b[15]);
prefs->changed = 1;
}
if(prefs->cmd_options_man) if(prefs->cmd_options_man)
{ {
put_options_man(); put_options_man();
@ -2222,7 +2170,6 @@ int main(int argc, char *argv[])
km.print_fps_results(); km.print_fps_results();
km.close_logging(); km.close_logging();
http_shutdown();
main_cleanup(); main_cleanup();
return 0; return 0;
} }

View File

@ -1,260 +0,0 @@
/*
* leaderboard.cpp submission + browse over HTTP.
*
* Hand-rolled JSON build/parse. The wire schema is tiny and fixed-shape
* (see plan); no point in pulling in a JSON dep just to handle two
* response types.
*
* Threading: callbacks fire from http_pump() (native) or the Emscripten
* runtime (web) both single-threaded relative to the game loop, so
* mutating the global state structs from the callbacks is safe.
*/
// pfile.h (pulled in via score.h via leaderboard.h) uses FILE without
// including stdio.h itself; pull it in first.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "leaderboard.h"
#include "httpclient.h"
#include "prefs.h"
#include "kobo.h"
#include "kobolog.h"
extern prefs_t *prefs;
namespace {
leaderboard_submission_t g_sub = { LB_IDLE, 0, 0 };
leaderboard_browse_t g_brw = { LB_IDLE, 0, 0, {}, "" };
bool enabled()
{
return prefs && prefs->global_leaderboard
&& prefs->leaderboard_url[0] != '\0'
&& prefs->leaderboard_board[0] != '\0';
}
// ---- JSON helpers ----------------------------------------------------------
//
// Build: snprintf with manual escaping for string fields.
// Parse: substring-search the known keys. Tolerates whitespace but assumes
// the server emits one occurrence of each key (which our server contract does).
void json_escape(char *out, size_t cap, const char *in)
{
size_t o = 0;
for(size_t i = 0; in[i] && o + 2 < cap; ++i)
{
char c = in[i];
if(c == '"' || c == '\\')
{
if(o + 3 >= cap) break;
out[o++] = '\\'; out[o++] = c;
}
else if((unsigned char)c < 0x20)
{
// Skip control bytes; the in-game name editor only emits ASCII
// letters anyway. Logging this would be noise.
continue;
}
else out[o++] = c;
}
out[o] = '\0';
}
// Find `"key":` and return pointer to first char after the colon, or NULL.
const char *find_key(const char *body, const char *key)
{
size_t klen = strlen(key);
for(const char *p = body; (p = strchr(p, '"')) != NULL; ++p)
{
++p;
if(strncmp(p, key, klen) != 0) continue;
if(p[klen] != '"') continue;
const char *q = p + klen + 1;
while(*q == ' ' || *q == '\t') ++q;
if(*q != ':') continue;
++q;
while(*q == ' ' || *q == '\t') ++q;
return q;
}
return NULL;
}
bool parse_int(const char *p, int *out)
{
if(!p) return false;
char *end = NULL;
long v = strtol(p, &end, 10);
if(end == p) return false;
*out = (int)v;
return true;
}
// Copies the next "string" into out, returns pointer past the closing quote.
const char *parse_string(const char *p, char *out, size_t cap)
{
if(!p || *p != '"') { if(cap) out[0] = '\0'; return NULL; }
++p;
size_t o = 0;
while(*p && *p != '"')
{
if(*p == '\\' && p[1])
{
if(o + 1 < cap) out[o++] = p[1];
p += 2;
}
else
{
if(o + 1 < cap) out[o++] = *p;
++p;
}
}
if(cap) out[o] = '\0';
return *p == '"' ? p + 1 : NULL;
}
// ---- callbacks --------------------------------------------------------------
void on_submit(const http_response_t *resp, void * /*user*/)
{
if(resp->status == 0)
{
g_sub.status = LB_OFFLINE;
log_printf(WLOG, "leaderboard: submission failed (network)\n");
return;
}
if(resp->status / 100 != 2)
{
g_sub.status = LB_FAILED;
log_printf(WLOG, "leaderboard: submission rejected (HTTP %d)\n",
resp->status);
return;
}
int rank = -1, total = -1;
parse_int(find_key(resp->body, "rank"), &rank);
parse_int(find_key(resp->body, "total"), &total);
g_sub.rank = rank;
g_sub.total = total;
g_sub.status = LB_DONE;
}
void on_browse(const http_response_t *resp, void * /*user*/)
{
if(resp->status == 0) { g_brw.status = LB_OFFLINE; return; }
if(resp->status / 100 != 2) { g_brw.status = LB_FAILED; return; }
g_brw.count = 0;
int total = -1;
parse_int(find_key(resp->body, "total"), &total);
g_brw.total = total;
// "scores": [ {name, score, end_scene}, ... ]
const char *p = find_key(resp->body, "scores");
if(!p || *p != '[') { g_brw.status = LB_FAILED; return; }
++p;
while(g_brw.count < LEADERBOARD_BROWSE_MAX)
{
while(*p && *p != '{' && *p != ']') ++p;
if(*p != '{') break;
// Each entry is a small object — scan it locally.
const char *end = strchr(p, '}');
if(!end) break;
leaderboard_entry_t &e = g_brw.entries[g_brw.count];
e.name[0] = '\0';
e.score = 0;
e.end_scene = 0;
const char *kp = find_key(p, "name");
if(kp && kp < end) parse_string(kp, e.name, sizeof(e.name));
kp = find_key(p, "score");
int sv = 0;
if(kp && kp < end && parse_int(kp, &sv) && sv >= 0)
e.score = (unsigned int)sv;
kp = find_key(p, "end_scene");
if(kp && kp < end) parse_int(kp, &e.end_scene);
++g_brw.count;
p = end + 1;
}
g_brw.status = LB_DONE;
}
} // namespace
namespace leaderboard {
void submit_score(const s_hiscore_t *hi)
{
g_sub.status = LB_IDLE;
g_sub.rank = -1;
g_sub.total = -1;
if(!enabled() || !hi) return;
char name_esc[LEADERBOARD_NAME_MAX * 2 + 1];
char board_esc[LEADERBOARD_NAME_MAX * 2 + 1];
char id_esc[128];
const char *player_name = (hi->profile && hi->profile->name[0])
? hi->profile->name : "ANON";
json_escape(name_esc, sizeof(name_esc), player_name);
json_escape(board_esc, sizeof(board_esc), prefs->leaderboard_board);
json_escape(id_esc, sizeof(id_esc), prefs->client_id);
char body[1024];
int n = snprintf(body, sizeof(body),
"{\"board\":\"%s\",\"name\":\"%s\",\"client_id\":\"%s\","
"\"client\":\"kobodl/0.5.1\","
"\"score\":%u,\"skill\":%d,\"gametype\":%d,"
"\"end_scene\":%d,\"end_lives\":%d,\"end_health\":%d,"
"\"playtime\":%u,\"start_date\":%u,\"end_date\":%u}",
board_esc, name_esc, id_esc,
hi->score, hi->skill, hi->gametype,
hi->end_scene, hi->end_lives, hi->end_health,
hi->playtime, hi->start_date, hi->end_date);
if(n <= 0 || n >= (int)sizeof(body)) return;
char url[512];
snprintf(url, sizeof(url), "%s/api/scores", prefs->leaderboard_url);
g_sub.status = LB_INFLIGHT;
http_post_json(url, body, on_submit, NULL);
}
void fetch_board()
{
g_brw.status = LB_IDLE;
g_brw.count = 0;
g_brw.total = 0;
if(!enabled()) return;
strncpy(g_brw.board, prefs->leaderboard_board, sizeof(g_brw.board) - 1);
g_brw.board[sizeof(g_brw.board) - 1] = '\0';
char board_esc[LEADERBOARD_NAME_MAX * 2 + 1];
json_escape(board_esc, sizeof(board_esc), prefs->leaderboard_board);
char url[768];
snprintf(url, sizeof(url),
"%s/api/scores?board=%s&limit=%d",
prefs->leaderboard_url, board_esc, LEADERBOARD_BROWSE_MAX);
g_brw.status = LB_INFLIGHT;
http_get(url, on_browse, NULL);
}
const leaderboard_submission_t *submission() { return &g_sub; }
const leaderboard_browse_t *browse() { return &g_brw; }
void reset_submission()
{
g_sub.status = LB_IDLE;
g_sub.rank = -1;
g_sub.total = -1;
}
} // namespace leaderboard

View File

@ -1,69 +0,0 @@
/*
* leaderboard.h global leaderboard client.
*
* Two flows:
*
* 1. submit_score() is called after each completed game (manage.cpp). It
* POSTs the run's s_hiscore_t to leaderboard_url + "/api/scores". The
* game-over screen polls submission() to render the in-flight UI.
*
* 2. fetch_board() is called when the in-game leaderboard browser opens.
* It GETs the top N for the current board. The browser state polls
* browse() to render the list.
*
* Both flows are no-ops when prefs->global_leaderboard is 0, or when the
* board name is empty, or when the URL is empty.
*/
#ifndef KOBO_LEADERBOARD_H
#define KOBO_LEADERBOARD_H
#include "score.h"
#define LEADERBOARD_BROWSE_MAX 50
#define LEADERBOARD_NAME_MAX 32
enum leaderboard_status_t {
LB_IDLE = 0,
LB_INFLIGHT,
LB_DONE,
LB_FAILED, // server error, network error, or feature disabled
LB_OFFLINE // transport failure (status 0)
};
struct leaderboard_submission_t {
leaderboard_status_t status;
int rank; // valid when status == LB_DONE
int total; // valid when status == LB_DONE
};
struct leaderboard_entry_t {
char name[LEADERBOARD_NAME_MAX];
unsigned int score;
int end_scene;
};
struct leaderboard_browse_t {
leaderboard_status_t status;
int count;
int total;
leaderboard_entry_t entries[LEADERBOARD_BROWSE_MAX];
char board[LEADERBOARD_NAME_MAX];
};
namespace leaderboard {
// Called from manage.cpp after scorefile.record(). Safe to call always;
// it self-checks prefs and silently no-ops if disabled.
void submit_score(const s_hiscore_t *hi);
// Called from the browser state on enter. Replaces previous results.
void fetch_board();
// State accessors — game UI reads these every frame.
const leaderboard_submission_t *submission();
const leaderboard_browse_t *browse();
// Reset submission state to LB_IDLE (e.g. when starting a new game).
void reset_submission();
}
#endif // KOBO_LEADERBOARD_H

View File

@ -50,27 +50,9 @@
#include "states.h" #include "states.h"
#include "audio.h" #include "audio.h"
#include "random.h" #include "random.h"
#include "prof.h"
#include "bench.h"
#include "leaderboard.h"
#include <time.h>
#define GIGA 1000000000 #define GIGA 1000000000
// Stress-test spawner: keeps the enemy pool topped up with rocks so frame
// timing is stable. Enabled by KOBO_STRESS=N (target population, 0 = off).
static int kobo_stress_target = -1;
static int kobo_stress_init(void)
{
if (kobo_stress_target >= 0) return kobo_stress_target;
const char *e = getenv("KOBO_STRESS");
kobo_stress_target = (e && *e) ? atoi(e) : 0;
if (kobo_stress_target > 0)
fprintf(stderr, "[stress] target population=%d\n", kobo_stress_target);
return kobo_stress_target;
}
static void kobo_stress_step(void);
int _manage::blank = 0; int _manage::blank = 0;
int _manage::next_state_out; int _manage::next_state_out;
int _manage::next_state_next; int _manage::next_state_next;
@ -215,8 +197,6 @@ void _manage::game_start()
{ {
_game_over = 0; _game_over = 0;
hi.clear(); hi.clear();
hi.start_date = (unsigned int)time(NULL);
leaderboard::reset_submission();
game.set(GAME_SINGLE, (skill_levels_t)scorefile.profile()->skill); game.set(GAME_SINGLE, (skill_levels_t)scorefile.profile()->skill);
@ -271,20 +251,8 @@ void _manage::game_stop()
hi.score = score; hi.score = score;
hi.end_scene = scene_num; hi.end_scene = scene_num;
hi.end_health = myship.health(); hi.end_health = myship.health();
hi.end_date = (unsigned int)time(NULL);
// Profile name isn't part of s_hiscore_t's serialized form, but
// leaderboard::submit_score wants it. Sync it here so the
// payload picks it up.
s_profile_t *p = scorefile.profile();
if(p)
{
hi.profile = p;
strncpy(hi.name, p->name, sizeof(hi.name) - 1);
hi.name[sizeof(hi.name) - 1] = '\0';
}
scorefile.record(&hi); scorefile.record(&hi);
leaderboard::submit_score(&hi);
} }
ships = 0; ships = 0;
ships_changed = 1; ships_changed = 1;
@ -367,9 +335,6 @@ void _manage::init_resources_to_play(int newship)
next_state_out = 0; next_state_out = 0;
next_state_next = 0; next_state_next = 0;
if(kobo_bench_enabled)
gamerand.init(kobo_bench_seed); // deterministic map + spawn pattern
else
gamerand.init(); gamerand.init();
game_seed = gamerand.get_seed(); game_seed = gamerand.get_seed();
enemies.init(); enemies.init();
@ -591,7 +556,6 @@ void _manage::run_pause()
void _manage::run_game() void _manage::run_game()
{ {
PROF_ZONE("run_game");
put_health(); put_health();
put_temp(); put_temp();
@ -619,56 +583,11 @@ void _manage::run_game()
} }
} }
if(kobo_stress_init() > 0) myship.move();
kobo_stress_step(); enemies.move();
myship.hit_structure();
{ PROF_ZONE("myship.move"); myship.move(); }
{ PROF_ZONE("enemies.move"); enemies.move(); }
{ PROF_ZONE("hit_structure"); myship.hit_structure(); }
update(); update();
++hi.playtime; ++hi.playtime;
prof_frame_tick();
// Bench harness: stop the program after a fixed number of frames so a
// single command produces a comparable profiler dump across phases.
// Push SDL_EVENT_QUIT instead of calling exit() — exit() runs the
// atexit handler which tries to shut down SDL audio synchronously and
// hangs on macOS. Going through the engine's own quit path tears down
// in the right order.
if(kobo_bench_enabled && (int)hi.playtime >= kobo_bench_frames)
{
static int triggered = 0;
if(!triggered)
{
triggered = 1;
fprintf(stderr,
"[bench] reached %d frames; dumping profile and quitting\n",
kobo_bench_frames);
prof_dump(stderr);
SDL_Event ev;
SDL_zero(ev);
ev.type = SDL_EVENT_QUIT;
SDL_PushEvent(&ev);
}
}
}
// Top up the enemy pool with rocks until we hit the target population.
// Rocks are inert enough to give a stable per-frame integration cost.
static void kobo_stress_step(void)
{
extern const enemy_kind rock;
int target = kobo_stress_target;
if (target <= 0) return;
// Spawn at most a few per frame to avoid burst allocation spikes.
for (int i = 0; i < 8; ++i) {
int px = (pubrand.get() & (WORLD_SIZEX - 1));
int py = (pubrand.get() & (WORLD_SIZEY - 1));
int h = (int)(pubrand.get() & 0x7f) - 64;
int v = (int)(pubrand.get() & 0x7f) - 64;
// enemies.make returns 0 on success, non-zero when pool is full.
if (enemies.make(&rock, px, py, h, v) != 0) break;
}
} }

View File

@ -95,7 +95,6 @@ class _manage
static void regenerate(); static void regenerate();
static void select_scene(int scene, int redraw_map = 1); static void select_scene(int scene, int redraw_map = 1);
static int scene() { return scene_num; } static int scene() { return scene_num; }
static void set_scene_num(int n) { scene_num = n; } // for bench harness
static void abort(); static void abort();
static void freeze_abort(); static void freeze_abort();
static int aborted() { return exit_manage; } static int aborted() { return exit_manage; }

View File

@ -28,7 +28,6 @@
#include "manage.h" #include "manage.h"
#include "random.h" #include "random.h"
#include "sound.h" #include "sound.h"
#include "prof.h"
#define WING_GUN_OFFSET 4 #define WING_GUN_OFFSET 4
@ -385,7 +384,6 @@ int _myship::hit_structure()
int _myship::hit_bolt(int ex, int ey, int hitsize, int health) int _myship::hit_bolt(int ex, int ey, int hitsize, int health)
{ {
PROF_ZONE("hit_bolt");
int dmg = 0; int dmg = 0;
int i; int i;
for(i = 0; i < MAX_BOLTS; i++) for(i = 0; i < MAX_BOLTS; i++)

View File

@ -177,7 +177,29 @@ void video_options_t::build()
} }
space(); space();
yesno("Fullscreen Display", &prf->fullscreen, OS_RESTART_VIDEO); yesno("Fullscreen Display", &prf->fullscreen, OS_RESTART_VIDEO);
list("Display Depth", &prf->depth, OS_RESTART_VIDEO);
item("Default", 0);
item("8 bits", 8);
item("15 bits", 15);
item("16 bits", 16);
item("24 bits", 24);
item("32 bits", 32);
prf->videodriver = GFX_DRIVER_SDL2D;
space();
xoffs = 0.55;
// Only the SDL 2D driver remains since the SDL 3 migration; the
// OpenGL driver is now handled by the SDL renderer itself.
list("Display Buffering Mode", &prf->doublebuf, OS_RESTART_VIDEO);
item("Single", 0);
item("Double", 1);
yesno("Software Shadow Buffer", &prf->shadow, OS_RESTART_VIDEO);
yesno("Vertical Retrace Sync", &prf->vsync, OS_RESTART_VIDEO); yesno("Vertical Retrace Sync", &prf->vsync, OS_RESTART_VIDEO);
list("Display Buffer Pages", &prf->pages, OS_UPDATE_ENGINE);
item("Assume Standard", -1);
item("Always Repaint", 0);
item("Single", 1);
item("Double", 2);
item("Triple", 3);
big(); big();
space(); space();
xoffs = 0.5; xoffs = 0.5;
@ -458,14 +480,6 @@ void game_options_t::build()
item("Off", 200); item("Off", 200);
item("Low", 100); item("Low", 100);
item("High", 50); item("High", 50);
// Leaderboard toggle. Board name and URL live in the config file
// (the form toolkit has no free-text widget). When this is on but
// the board name is empty, submission no-ops silently.
space();
xoffs = 0.6;
onoff("Global Leaderboard", &prf->global_leaderboard, 0);
big(); big();
space(); space();
xoffs = 0.5; xoffs = 0.5;

View File

@ -82,18 +82,25 @@ void prefs_t::init()
comment("--- Video settings -------------------------"); comment("--- Video settings -------------------------");
yesno("fullscreen", fullscreen, 0); desc("Fullscreen Display"); yesno("fullscreen", fullscreen, 0); desc("Fullscreen Display");
key("videodriver", videodriver, GFX_DRIVER_SDL2D);
desc("Display Driver");
key("width", width, 640); desc("Horizontal Resolution"); key("width", width, 640); desc("Horizontal Resolution");
key("height", height, 480); desc("Vertical Resolution"); key("height", height, 480); desc("Vertical Resolution");
key("aspect", aspect, 1000); desc("Pixel Aspect Ratio"); key("aspect", aspect, 1000); desc("Pixel Aspect Ratio");
key("depth", depth, 0); desc("Display Depth");
key("maxfps", max_fps, 100); desc("Maximum fps"); key("maxfps", max_fps, 100); desc("Maximum fps");
key("maxfpsstrict", max_fps_strict, 0); desc("Strictly Regulated fps"); key("maxfpsstrict", max_fps_strict, 0); desc("Strictly Regulated fps");
key("buffer", doublebuf, 1); desc("Display Buffer Mode");
yesno("shadow", shadow, 1); desc("Use Software Shadow Buffer");
key("videomode", videomode, 0x04330); desc("Video Mode"); key("videomode", videomode, 0x04330); desc("Video Mode");
yesno("vsync", vsync, 1); desc("Enable Vertical Sync"); yesno("vsync", vsync, 1); desc("Enable Vertical Sync");
key("videopages", pages, -1); desc("Number of Video Pages");
comment("--- Graphics settings ----------------------"); comment("--- Graphics settings ----------------------");
key("scalemode", scalemode, 1); desc("Scaling Filter Mode"); key("scalemode", scalemode, 1); desc("Scaling Filter Mode");
yesno("dither", use_dither, 1); desc("Use Dithering"); yesno("dither", use_dither, 1); desc("Use Dithering");
key("dither_type", dither_type, 0); desc("Dither Type"); key("dither_type", dither_type, 0); desc("Dither Type");
yesno("broken_rgba8", broken_rgba8, 0); desc("Broken RGBA (OpenGL)");
yesno("alpha", alpha, 1); desc("Use Alpha Blending"); yesno("alpha", alpha, 1); desc("Use Alpha Blending");
key("brightness", brightness, 100); desc("Brightness"); key("brightness", brightness, 100); desc("Brightness");
key("contrast", contrast, 100); desc("Contrast"); key("contrast", contrast, 100); desc("Contrast");
@ -104,24 +111,6 @@ void prefs_t::init()
key("sfx", sfxdir, ""); desc("Sound Data Path"); key("sfx", sfxdir, ""); desc("Sound Data Path");
key("scores", scoredir, ""); desc("Score File Path"); key("scores", scoredir, ""); desc("Score File Path");
comment("--- Global leaderboard ---------------------");
// Web build ships with the leaderboard on by default — players land on
// lindholm.dev/kobo/ specifically to play it. Native build stays off
// until the user opts in; a desktop player may not want network calls
// from a single-player arcade game.
#ifdef __EMSCRIPTEN__
yesno("global_leaderboard", global_leaderboard, 1);
#else
yesno("global_leaderboard", global_leaderboard, 0);
#endif
desc("Submit Scores to Global Leaderboard");
key("leaderboard_board", leaderboard_board, "");
desc("Leaderboard Board Name (shared with friends)");
key("leaderboard_url", leaderboard_url,
"https://leaderboard.lindholm.dev");
desc("Leaderboard Server URL");
key("client_id", client_id, ""); desc("Anonymous Per-Install ID");
// Obsolete stuff (not written into new files) // Obsolete stuff (not written into new files)
key("size", o_size, 0, 0); desc("Screen Size (Obsolete)"); key("size", o_size, 0, 0); desc("Screen Size (Obsolete)");
key("wait", o_wait_msec, 30); desc("Game Speed (Obsolete)"); key("wait", o_wait_msec, 30); desc("Game Speed (Obsolete)");
@ -131,14 +120,6 @@ void prefs_t::init()
key("release", o_release, 50, 0); desc("Limiter Speed"); key("release", o_release, 50, 0); desc("Limiter Speed");
key("internalres", o_internalres, 1, 0); desc("Texture Resolution"); key("internalres", o_internalres, 1, 0); desc("Texture Resolution");
// SDL 1.2-era video keys, ignored under SDL 3.
key("videodriver", o_videodriver, 0, 0); desc("Display Driver");
key("depth", o_depth, 0, 0); desc("Display Depth");
key("buffer", o_doublebuf, 1, 0); desc("Display Buffer Mode");
yesno("shadow", o_shadow, 1, 0); desc("Software Shadow Buffer");
key("videopages", o_pages, -1, 0); desc("Number of Video Pages");
yesno("broken_rgba8", o_broken_rgba8, 0, 0); desc("Broken RGBA (OpenGL)");
comment("--- Temporary variables --------------------"); comment("--- Temporary variables --------------------");
key("last_profile", last_profile, 0); key("last_profile", last_profile, 0);
desc("Last used player profile"); desc("Last used player profile");
@ -189,4 +170,9 @@ void prefs_t::postload()
log_printf(ELOG, "Radar sweep mode broken and disabled!\n"); log_printf(ELOG, "Radar sweep mode broken and disabled!\n");
scrollradar = 2; scrollradar = 2;
} }
// Only the SDL 2D driver remains since the SDL 3 migration; collapse
// any legacy GLSDL setting onto it.
if(videodriver != GFX_DRIVER_SDL2D)
videodriver = GFX_DRIVER_SDL2D;
} }

21
prefs.h
View File

@ -73,18 +73,24 @@ class prefs_t : public config_parser_t
//Video settings //Video settings
int fullscreen; //Use fullscreen mode int fullscreen; //Use fullscreen mode
int videodriver; //Internal video driver
int width; //Screen/window width int width; //Screen/window width
int height; //Screen/window height int height; //Screen/window height
int aspect; //Pixel aspect ratio * 1000 int aspect; //Pixel aspect ratio * 1000
int depth; //Bits per pixel
int max_fps; //Maximum fps int max_fps; //Maximum fps
int max_fps_strict; //Strictly regulated fps limiter int max_fps_strict; //Strictly regulated fps limiter
int videomode; //Selected entry from the vidmodes table int doublebuf; //Use double buffering
int shadow; //Use software shadow buffer
int videomode; //New video mode codes
int vsync; //Vertical (retrace) sync int vsync; //Vertical (retrace) sync
int pages; //Number of physical video pages
//Graphics settings //Graphics settings
int scalemode; //Scaling filter mode int scalemode; //Scaling filter mode
int use_dither; int use_dither;
int dither_type; int dither_type;
int broken_rgba8; //For some OpenGL setups
int alpha; //Alpha blending int alpha; //Alpha blending
int brightness; //Graphics brightness int brightness; //Graphics brightness
int contrast; //Graphics contrast int contrast; //Graphics contrast
@ -95,12 +101,6 @@ class prefs_t : public config_parser_t
cfg_string_t sfxdir; //Path to sfx/ cfg_string_t sfxdir; //Path to sfx/
cfg_string_t scoredir; //Path to scores/ cfg_string_t scoredir; //Path to scores/
//Global leaderboard
int global_leaderboard; //Submit scores to the server
cfg_string_t leaderboard_board; //Shared "room code"
cfg_string_t leaderboard_url; //Server endpoint base URL
cfg_string_t client_id; //Per-install UUID (auto-gen)
//Obsolete stuff (compatibility) //Obsolete stuff (compatibility)
int o_size; //Screen scale int o_size; //Screen scale
int o_wait_msec; //ms per control system frame int o_wait_msec; //ms per control system frame
@ -108,13 +108,6 @@ class prefs_t : public config_parser_t
int o_threshold; //Limiter threshold int o_threshold; //Limiter threshold
int o_release; //Limiter release rate int o_release; //Limiter release rate
int o_internalres; //Internal resolution for OpenGL int o_internalres; //Internal resolution for OpenGL
// SDL 1.2 video settings, read for back-compat but unused under SDL 3
int o_videodriver;
int o_depth;
int o_doublebuf;
int o_shadow;
int o_pages;
int o_broken_rgba8;
//"Hidden" stuff ("to remember until next startup") //"Hidden" stuff ("to remember until next startup")
int last_profile; //Last used player profile int last_profile; //Last used player profile

View File

@ -1,41 +0,0 @@
#include "prof.h"
prof_zone_t prof_zones[PROF_MAX_ZONES];
int prof_nzones = 0;
int prof_enabled = 0;
uint64_t prof_frame_count = 0;
extern "C" void prof_init(void)
{
const char *e = getenv("KOBO_PROF");
prof_enabled = (e && *e && *e != '0') ? 1 : 0;
if (prof_enabled)
fprintf(stderr, "[prof] enabled (dump every %d frames)\n",
PROF_DUMP_EVERY_FRAMES);
}
extern "C" void prof_dump(FILE *f)
{
if (!prof_enabled) return;
fprintf(f, "[prof] frame=%llu zones=%d\n",
(unsigned long long)prof_frame_count, prof_nzones);
for (int i = 0; i < prof_nzones; ++i) {
prof_zone_t *z = &prof_zones[i];
double ns_per_call = z->calls ? (double)z->total_ns / (double)z->calls : 0.0;
double total_ms = (double)z->total_ns / 1.0e6;
fprintf(f, " %-28s calls=%-10llu total_ms=%-10.3f ns/call=%.1f\n",
z->name,
(unsigned long long)z->calls,
total_ms,
ns_per_call);
}
fflush(f);
}
extern "C" void prof_frame_tick(void)
{
if (!prof_enabled) return;
++prof_frame_count;
if (prof_frame_count % PROF_DUMP_EVERY_FRAMES == 0)
prof_dump(stderr);
}

94
prof.h
View File

@ -1,94 +0,0 @@
// Lightweight zone-based profiler for measuring the DOD refactor.
// Enabled at runtime via KOBO_PROF=1 in the environment.
// See plans: .claude/plans/can-you-explain-the-noble-wilkes.md (Phase 0).
#ifndef KOBO_PROF_H
#define KOBO_PROF_H
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PROF_MAX_ZONES 32
#define PROF_DUMP_EVERY_FRAMES 600
typedef struct {
const char *name;
uint64_t calls;
uint64_t total_ns;
} prof_zone_t;
extern prof_zone_t prof_zones[PROF_MAX_ZONES];
extern int prof_nzones;
extern int prof_enabled;
extern uint64_t prof_frame_count;
static inline uint64_t prof_now_ns(void)
{
#if defined(__APPLE__) || defined(__linux__)
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
#else
return 0;
#endif
}
// Returns the index of the zone with the given name, creating it on first use.
// Names must be string literals (we compare by pointer for speed).
static inline int prof_zone_id(const char *name)
{
for (int i = 0; i < prof_nzones; ++i)
if (prof_zones[i].name == name)
return i;
if (prof_nzones >= PROF_MAX_ZONES)
return PROF_MAX_ZONES - 1;
int id = prof_nzones++;
prof_zones[id].name = name;
prof_zones[id].calls = 0;
prof_zones[id].total_ns = 0;
return id;
}
static inline void prof_accumulate(int id, uint64_t ns)
{
prof_zones[id].calls += 1;
prof_zones[id].total_ns += ns;
}
void prof_init(void);
void prof_frame_tick(void);
void prof_dump(FILE *f);
#ifdef __cplusplus
}
// RAII helper for C++ scoping.
struct ProfScope {
int id;
uint64_t t0;
inline ProfScope(int zone_id) : id(zone_id), t0(prof_enabled ? prof_now_ns() : 0) {}
inline ~ProfScope() {
if (prof_enabled) prof_accumulate(id, prof_now_ns() - t0);
}
};
// Pastes the line number to make the static unique per use-site.
#define PROF_ZONE_CONCAT2(a,b) a##b
#define PROF_ZONE_CONCAT(a,b) PROF_ZONE_CONCAT2(a,b)
#define PROF_ZONE(name) \
static int PROF_ZONE_CONCAT(_prof_zid_, __LINE__) = -1; \
if (PROF_ZONE_CONCAT(_prof_zid_, __LINE__) < 0) \
PROF_ZONE_CONCAT(_prof_zid_, __LINE__) = prof_zone_id(name); \
ProfScope PROF_ZONE_CONCAT(_prof_scope_, __LINE__)( \
PROF_ZONE_CONCAT(_prof_zid_, __LINE__))
#endif
#endif // KOBO_PROF_H

View File

@ -28,15 +28,7 @@
extern "C" { extern "C" {
#endif #endif
/* #define MAX_COMMANDS 128
* SDL 3 migration: the audio command FIFO was sized 128 for the SDL 1.2
* push-callback model where the driver fired predictably-spaced
* callbacks that drained the FIFO promptly. SDL 3's pull-stream model
* can call the audio callback less often / with larger buffers, so
* commands accumulate between drains. 1024 gives a comfortable
* headroom even under sustained heavy fire.
*/
#define MAX_COMMANDS 1024
/*---------------------------------------------------------- /*----------------------------------------------------------
Asynchronous command interface stuff Asynchronous command interface stuff

View File

@ -38,9 +38,6 @@
#include "sound.h" #include "sound.h"
#include "radar.h" #include "radar.h"
#include "random.h" #include "random.h"
#include "leaderboard.h"
#include "prefs.h"
#include <stdio.h>
gamestatemanager_t gsm; gamestatemanager_t gsm;
@ -667,41 +664,6 @@ void st_game_over_t::post_render()
int y = PIXEL2CS(100) + (int)floor(PIXEL2CS(15)*sin(ft * 6)); int y = PIXEL2CS(100) + (int)floor(PIXEL2CS(15)*sin(ft * 6));
wmain->center_fxp(y, "GAME OVER"); wmain->center_fxp(y, "GAME OVER");
const leaderboard_submission_t *sub = leaderboard::submission();
if(sub->status != LB_IDLE)
{
char buf[64];
const char *msg = NULL;
switch(sub->status)
{
case LB_INFLIGHT:
msg = "Submitting score…";
break;
case LB_DONE:
if(sub->rank > 0 && sub->total > 0)
{
snprintf(buf, sizeof(buf), "Rank #%d of %d",
sub->rank, sub->total);
msg = buf;
}
else
msg = "Score submitted";
break;
case LB_OFFLINE:
msg = "(offline)";
break;
case LB_FAILED:
msg = "(submission failed)";
break;
default: break;
}
if(msg)
{
wmain->font(B_NORMAL_FONT);
wmain->center_fxp(PIXEL2CS(140), msg);
}
}
wradar->frame(); wradar->frame();
} }
@ -1319,18 +1281,12 @@ void main_menu_t::build()
} }
space(); space();
button("Options", 2); button("Options", 2);
if(prefs->global_leaderboard && prefs->leaderboard_board[0])
button("Global Leaderboard", 200);
space(); space();
if(manage.game_stopped()) if(manage.game_stopped())
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()
@ -1357,18 +1313,6 @@ kobo_form_t *st_main_menu_t::open()
menu = new main_menu_t; menu = new main_menu_t;
menu->open(); menu->open();
// First time we reach the main menu with the leaderboard on but no
// board name configured, prompt for one. Only once per session — if
// the user cancels, they can edit it from Options later.
static bool prompted_for_board = false;
if(!prompted_for_board && prefs->global_leaderboard
&& prefs->leaderboard_board[0] == '\0')
{
prompted_for_board = true;
gsm.push(&st_edit_board);
}
return menu; return menu;
} }
@ -1419,9 +1363,6 @@ void st_main_menu_t::select(int tag)
case 3: case 3:
gsm.push(&st_new_player); gsm.push(&st_new_player);
break; break;
case 200:
gsm.push(&st_global_leaderboard);
break;
case 4: // Player: Inc/Dec case 4: // Player: Inc/Dec
sound.ui_tick(); sound.ui_tick();
prefs->changed = 1; prefs->changed = 1;
@ -1573,8 +1514,6 @@ void options_main_t::build()
button("Graphics", 6); button("Graphics", 6);
button("Audio", 2); button("Audio", 2);
button("System", 5); button("System", 5);
if(prefs->global_leaderboard)
button("Leaderboard Board Name", 7);
space(); space();
button("DONE!", 0); button("DONE!", 0);
@ -1609,9 +1548,6 @@ void st_options_main_t::select(int tag)
case 6: case 6:
gsm.push(&st_options_graphics); gsm.push(&st_options_graphics);
break; break;
case 7:
gsm.push(&st_edit_board);
break;
} }
} }
@ -1664,6 +1600,7 @@ void st_options_base_t::select(int tag)
gengine->timefilter(prefs->timefilter * 0.01f); gengine->timefilter(prefs->timefilter * 0.01f);
gengine->interpolation(prefs->filter); gengine->interpolation(prefs->filter);
gengine->vsync(prefs->vsync); gengine->vsync(prefs->vsync);
gengine->pages(prefs->pages);
} }
cfg_form->clearstatus(OS_UPDATE); cfg_form->clearstatus(OS_UPDATE);
@ -2057,340 +1994,3 @@ st_profile_audio_t st_profile_audio;
#endif /*PROFILE_AUDIO*/ #endif /*PROFILE_AUDIO*/
/*----------------------------------------------------------
Global Leaderboard browser
----------------------------------------------------------*/
st_global_leaderboard_t::st_global_leaderboard_t()
{
name = "global_leaderboard";
}
void st_global_leaderboard_t::enter()
{
start_time = (int)SDL_GetTicks();
leaderboard::fetch_board();
}
void st_global_leaderboard_t::press(int button)
{
switch(button)
{
case BTN_EXIT:
case BTN_NO:
case BTN_FIRE:
case BTN_START:
case BTN_SELECT:
case BTN_YES:
sound.ui_ok();
pop();
break;
}
}
void st_global_leaderboard_t::frame()
{
manage.run_intro();
}
void st_global_leaderboard_t::post_render()
{
kobo_basestate_t::post_render();
const leaderboard_browse_t *b = leaderboard::browse();
wmain->font(B_BIG_FONT);
wmain->center(40, "GLOBAL LEADERBOARD");
char buf[96];
wmain->font(B_NORMAL_FONT);
if(b->board[0])
{
snprintf(buf, sizeof(buf), "Board: %s", b->board);
wmain->center(70, buf);
}
switch(b->status)
{
case LB_INFLIGHT:
wmain->center(120, "Loading…");
break;
case LB_OFFLINE:
wmain->center(120, "(offline)");
break;
case LB_FAILED:
wmain->center(120, "(server error)");
break;
case LB_IDLE:
wmain->center(120, "Leaderboard disabled.");
wmain->center(140, "Set leaderboard_board in config.");
break;
case LB_DONE:
{
if(b->count == 0)
{
wmain->center(120, "No scores yet.");
break;
}
// Two-column layout: rank + name on the left, score + stage
// on the right. 18px line height matches the existing intro
// high-score screen feel.
int y = 100;
int rows = b->count > 16 ? 16 : b->count;
for(int i = 0; i < rows; ++i)
{
const leaderboard_entry_t &e = b->entries[i];
snprintf(buf, sizeof(buf), "%2d. %-10s",
i + 1, e.name);
wmain->string(40, y, buf);
snprintf(buf, sizeof(buf), "%9u st.%2d",
e.score, e.end_scene + 1);
wmain->string(160, y, buf);
y += 12;
}
break;
}
default: break;
}
wmain->center(WSIZE - 30, "(Press any key to return)");
}
st_global_leaderboard_t st_global_leaderboard;
/*----------------------------------------------------------
Edit leaderboard board name
----------------------------------------------------------*/
//
// Cloned from new_player_t. Differences: bound to prefs->leaderboard_board,
// allows alphanumerics + '-' / '_' (room-code shape), no profile creation —
// OK writes the value back into prefs and pops.
void edit_board_t::open()
{
init(gengine);
place(wmain->x(), wmain->y(), wmain->width(), wmain->height());
font(B_NORMAL_FONT);
foreground(wmain->map_rgb(255, 255, 255));
background(wmain->map_rgb(0, 0, 0));
memset(buf, 0, sizeof(buf));
strncpy(buf, prefs->leaderboard_board, sizeof(buf) - 1);
currentIndex = strlen(buf);
if(currentIndex == 0)
{
buf[0] = 'A';
currentIndex = 0;
}
editing = 1;
build_all();
if(gengine->window_handle())
SDL_StartTextInput(gengine->window_handle());
}
void edit_board_t::close()
{
if(gengine->window_handle())
SDL_StopTextInput(gengine->window_handle());
clean();
}
void edit_board_t::change(int delta)
{
kobo_form_t::change(delta);
if(!selected()) return;
selection = selected()->tag;
}
void edit_board_t::build()
{
medium();
space(2);
label("Global Leaderboard");
label("Use arrows or keyboard to enter board name");
label("(share it with friends to compete)");
big();
space();
button(buf, 1);
space();
button("Ok", MENU_TAG_OK);
button("Cancel", MENU_TAG_CANCEL);
}
void edit_board_t::rebuild()
{
int sel = selected_index();
build_all();
select(sel);
}
static bool board_char_valid(int c)
{
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9') || c == '-' || c == '_';
}
st_edit_board_t::st_edit_board_t()
{
name = "edit_board";
}
void st_edit_board_t::frame() { manage.run_intro(); }
void st_edit_board_t::enter() { menu.open(); run_intro = 0; sound.ui_ok(); }
void st_edit_board_t::leave() { menu.close(); }
void st_edit_board_t::post_render() { kobo_basestate_t::post_render(); menu.render(); }
void st_edit_board_t::press(int button)
{
if(menu.editing)
{
switch(button)
{
case BTN_EXIT:
sound.ui_ok();
menu.editing = 0;
menu.next(); // Select the CANCEL option.
menu.next();
break;
case BTN_FIRE:
if(!prefs->use_joystick) break;
// fallthrough
case BTN_START:
case BTN_SELECT:
sound.ui_ok();
menu.editing = 0;
menu.next(); // Select the OK option.
break;
case BTN_UP:
case BTN_INC:
{
char &c = menu.buf[menu.currentIndex];
if(!c) c = 'A';
else if(c == 'Z') c = 'a';
else if(c == 'z') c = '0';
else if(c == '9') c = '-';
else if(c == '-') c = '_';
else if(c == '_') c = 'A';
else c++;
sound.ui_tick();
break;
}
case BTN_DOWN:
case BTN_DEC:
{
char &c = menu.buf[menu.currentIndex];
if(!c) c = 'A';
else if(c == 'A') c = '_';
else if(c == '_') c = '-';
else if(c == '-') c = '9';
else if(c == '0') c = 'z';
else if(c == 'a') c = 'Z';
else c--;
sound.ui_tick();
break;
}
case BTN_RIGHT:
if(menu.currentIndex < sizeof(menu.buf) - 2)
{
menu.currentIndex++;
sound.ui_tick();
}
else
{
sound.ui_error();
break;
}
if(menu.buf[menu.currentIndex] == '\0')
menu.buf[menu.currentIndex] = 'A';
break;
case BTN_LEFT:
case BTN_BACK:
if(menu.currentIndex > 0)
{
menu.buf[menu.currentIndex] = '\0';
menu.currentIndex--;
sound.ui_tick();
}
else
sound.ui_error();
break;
default:
if(board_char_valid(unicode))
{
menu.buf[menu.currentIndex] = (char)unicode;
if(menu.currentIndex < sizeof(menu.buf) - 2)
{
menu.currentIndex++;
sound.ui_tick();
}
else
sound.ui_error();
}
else
sound.ui_error();
break;
}
menu.rebuild();
}
else
{
menu.selection = -1;
switch(button)
{
case BTN_EXIT: menu.selection = MENU_TAG_CANCEL; break;
case BTN_CLOSE: menu.selection = MENU_TAG_OK; break;
case BTN_FIRE:
case BTN_START:
case BTN_SELECT: menu.change(0); break;
case BTN_INC:
case BTN_UP: menu.prev(); break;
case BTN_DEC:
case BTN_DOWN: menu.next(); break;
}
switch(menu.selection)
{
case 1:
if(button == BTN_START || button == BTN_SELECT
|| button == BTN_FIRE)
{
sound.ui_ok();
menu.editing = 1;
}
break;
case MENU_TAG_OK:
{
// Trim trailing garbage from arrow-key wandering.
size_t n = strlen(menu.buf);
while(n > 0 && !board_char_valid(menu.buf[n - 1]))
menu.buf[--n] = '\0';
strncpy(prefs->leaderboard_board, menu.buf,
sizeof(prefs->leaderboard_board) - 1);
prefs->leaderboard_board[
sizeof(prefs->leaderboard_board) - 1] = '\0';
prefs->changed = 1;
sound.ui_ok();
pop();
break;
}
case MENU_TAG_CANCEL:
sound.ui_cancel();
pop();
break;
}
}
}
st_edit_board_t st_edit_board;

View File

@ -473,53 +473,6 @@ class st_ask_abort_game_t : public st_yesno_base_t
}; };
/*----------------------------------------------------------
Edit leaderboard board name
----------------------------------------------------------*/
class edit_board_t : public kobo_form_t
{
public:
int editing;
char buf[32];
unsigned currentIndex;
int selection;
virtual void change(int delta);
virtual void build();
void open();
void close();
void rebuild();
};
class st_edit_board_t : public kobo_basestate_t
{
public:
edit_board_t menu;
st_edit_board_t();
void enter();
void leave();
void press(int button);
void frame();
void post_render();
};
/*----------------------------------------------------------
Global Leaderboard browser
----------------------------------------------------------*/
class st_global_leaderboard_t : public kobo_basestate_t
{
int start_time;
public:
st_global_leaderboard_t();
void enter();
void press(int button);
void frame();
void post_render();
};
/*---------------------------------------------------------- /*----------------------------------------------------------
Debug: Audio Engine Profiling Debug: Audio Engine Profiling
----------------------------------------------------------*/ ----------------------------------------------------------*/
@ -581,8 +534,6 @@ extern st_options_graphics_t st_options_graphics;
extern st_options_audio_t st_options_audio; extern st_options_audio_t st_options_audio;
extern st_options_control_t st_options_control; extern st_options_control_t st_options_control;
extern st_options_game_t st_options_game; extern st_options_game_t st_options_game;
extern st_edit_board_t st_edit_board;
extern st_global_leaderboard_t st_global_leaderboard;
extern st_error_t st_error; extern st_error_t st_error;
#ifdef PROFILE_AUDIO #ifdef PROFILE_AUDIO
extern st_profile_audio_t st_profile_audio; extern st_profile_audio_t st_profile_audio;

View File

@ -1,98 +0,0 @@
#!/usr/bin/env python3
"""Mock leaderboard server for local testing.
Stores submitted scores in memory, returns rank/total on POST, and serves
the top-N on GET. Sends permissive CORS so the web build can talk to it
from a different port.
Usage:
./tools/mock-leaderboard.py # listens on :8765
./tools/mock-leaderboard.py 8080 # custom port
"""
import json
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
# In-memory store: {board_name: [score_record, ...]}
SCORES = {}
def _cors(handler):
handler.send_header("Access-Control-Allow-Origin", "*")
handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
handler.send_header("Access-Control-Allow-Headers", "Content-Type")
class Handler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(204)
_cors(self)
self.end_headers()
def do_GET(self):
url = urlparse(self.path)
if url.path != "/api/scores":
self.send_response(404); _cors(self); self.end_headers(); return
q = parse_qs(url.query)
board = (q.get("board") or [""])[0]
limit = int((q.get("limit") or ["50"])[0])
rows = sorted(SCORES.get(board, []),
key=lambda r: r["score"], reverse=True)[:limit]
body = json.dumps({
"board": board,
"total": len(SCORES.get(board, [])),
"scores": [
{"name": r["name"], "score": r["score"],
"end_scene": r["end_scene"]}
for r in rows
],
}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
_cors(self)
self.end_headers()
self.wfile.write(body)
def do_POST(self):
if urlparse(self.path).path != "/api/scores":
self.send_response(404); _cors(self); self.end_headers(); return
length = int(self.headers.get("Content-Length", "0"))
try:
payload = json.loads(self.rfile.read(length))
except Exception as e:
self.send_response(400); _cors(self); self.end_headers()
self.wfile.write(f"bad json: {e}".encode())
return
board = payload.get("board", "")
SCORES.setdefault(board, []).append(payload)
# Rank: 1 = best. Sort descending, find position.
ranked = sorted(SCORES[board], key=lambda r: r["score"], reverse=True)
# Identify *this* submission by reference equality.
rank = next((i + 1 for i, r in enumerate(ranked) if r is payload), -1)
body = json.dumps({"rank": rank, "total": len(SCORES[board])}).encode()
print(f"[POST] board={board} name={payload.get('name')} "
f"score={payload.get('score')} -> #{rank}/{len(SCORES[board])}")
self.send_response(200)
self.send_header("Content-Type", "application/json")
_cors(self)
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
# Keep the noise down; we print our own line on POST.
return
def main():
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8765
srv = HTTPServer(("127.0.0.1", port), Handler)
print(f"mock-leaderboard listening on http://127.0.0.1:{port}")
try:
srv.serve_forever()
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()

View File

@ -1,68 +0,0 @@
<!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; }
#back { position: fixed; top: 10px; left: 12px; z-index: 10;
color: #888; text-decoration: none; font-size: 13px;
padding: 4px 8px; border: 1px solid #333; border-radius: 3px;
background: rgba(0,0,0,0.6); }
#back:hover { color: #ddd; border-color: #666; }
</style>
</head>
<body>
<a id="back" href="https://lindholm.dev">← lindholm.dev</a>
<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>