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

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

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

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

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

622 lines
13 KiB
C

/*(LGPL)
----------------------------------------------------------------------
sprite.c - Sprite engine for use with cs.h
----------------------------------------------------------------------
* Copyright (C) 2001, 2003, 2007 David Olofson
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/*
TODO: Automatic extension of the bank and sprite
TODO: tables as needed when loading banks.
*/
#define DBG(x) x
#include <stdlib.h>
#include <string.h>
#include "logger.h"
#include "sdl_compat.h"
#include "img_compat.h"
#include "sprite.h"
#include "filters.h"
s_blitmodes_t s_blitmode = S_BLITMODE_AUTO;
pix_t s_colorkey = {0, 0, 0, 0};
pix_t s_clampcolor = {0, 0, 0, 0};
unsigned char s_alpha = SDL_ALPHA_OPAQUE;
int s_filter_flags = 0;
s_filter_t *filters = NULL;
/*
----------------------------------------------------------------------
Filter Plugin Interface
----------------------------------------------------------------------
*/
s_filter_t *s_insert_filter(s_filter_cb_t callback)
{
s_filter_t *nf = calloc(1, sizeof(s_filter_t));
if(!nf)
return NULL;
nf->callback = callback;
nf->next = filters;
filters = nf;
return nf;
}
s_filter_t *s_add_filter(s_filter_cb_t callback)
{
s_filter_t *nf = calloc(1, sizeof(s_filter_t));
if(!nf)
return NULL;
nf->callback = callback;
if(!filters)
filters = nf;
else
{
s_filter_t *f = filters;
while(f->next)
f = f->next;
f->next = nf;
}
return nf;
}
/* filter == NULL means "remove all plugins" */
void s_remove_filter(s_filter_t *filter)
{
s_filter_t *f;
if(!filters)
return;
f = filters;
while(f->next)
{
if(f->next == filter || !filter)
{
s_filter_t *df = f->next;
f->next = f->next->next;
free(df);
}
else
f = f->next;
}
if(filters == filter || !filter)
{
s_filter_t *df = filters;
filters = filters->next;
free(df);
}
}
static void __run_plugins(s_bank_t *b, unsigned first, unsigned frames)
{
s_filter_t *f = filters;
while(f)
{
int oflags = f->args.flags;
f->args.flags |= s_filter_flags;
f->callback(b, first, frames, &f->args);
f->args.flags = oflags;
f = f->next;
}
}
/*
----------------------------------------------------------------------
Basic Container Management
----------------------------------------------------------------------
*/
static int __alloc_bank_table(s_container_t *c, unsigned int count)
{
c->max = count - 1;
c->banks = (s_bank_t **)calloc((unsigned)count, sizeof(s_bank_t *));
return -(c->banks == NULL);
}
s_container_t *s_new_container(unsigned banks)
{
s_container_t *ret = (s_container_t *)calloc(1, sizeof(s_container_t));
if(!ret)
return NULL;
if(__alloc_bank_table(ret, banks) < 0)
return NULL;
return ret;
}
void s_delete_container(s_container_t *c)
{
s_delete_all_banks(c);
free(c->banks);
free(c);
}
/*
* Allocates a new sprite.
* If the sprite exists already, it's surface will be removed, so
* that one can safely expect to get an *empty* sprite.
*/
s_sprite_t *s_new_sprite_b(s_bank_t *b, unsigned frame)
{
s_sprite_t *s = NULL;
if(frame > b->max)
return NULL;
if(!b->sprites[frame])
{
s = calloc(1, sizeof(s_sprite_t));
if(!s)
return NULL;
b->sprites[frame] = s;
}
else
{
if(s->surface)
SDL_DestroySurface(s->surface);
s->surface = NULL;
}
return s;
}
/*
* Allocates a new sprite.
* If the sprite exists already, it's surface will be removed, so
* that one can safely expect to get an *empty* sprite.
*
* If the bank does not exist, or if the operation failed for
* other reasons, this call returns NULL.
*/
s_sprite_t *s_new_sprite(s_container_t *c, unsigned bank, unsigned frame)
{
if(bank > c->max)
return NULL;
if(!c->banks[bank])
return NULL;
return s_new_sprite_b(c->banks[bank], frame);
}
void s_delete_sprite_b(s_bank_t *b, unsigned frame)
{
if(!b->sprites) /* This can happen when failing to create a new bank */
return;
if(frame > b->max)
return;
if(!b->sprites[frame])
return;
if(b->sprites[frame]->surface)
SDL_DestroySurface(b->sprites[frame]->surface);
b->sprites[frame]->surface = NULL;
free(b->sprites[frame]);
b->sprites[frame] = NULL;
}
void s_delete_sprite(s_container_t *c, unsigned bank, unsigned frame)
{
s_bank_t *b = s_get_bank(c, bank);
if(b)
s_delete_sprite_b(b, frame);
}
static int __alloc_sprite_table(s_bank_t *b, unsigned frames)
{
b->max = frames - 1;
b->sprites = (s_sprite_t **)calloc(frames, sizeof(s_sprite_t *));
if(!b->sprites)
return -1;
return 0;
}
s_bank_t *s_new_bank(s_container_t *c, unsigned bank, unsigned frames,
unsigned w, unsigned h)
{
s_bank_t *b;
DBG(log_printf(DLOG, "s_new_bank(%p, %d, %d, %d, %d)\n",
c, bank, frames, w, h);)
if(bank > c->max)
return NULL;
if(c->banks[bank])
s_delete_bank(c, bank);
c->banks[bank] = (s_bank_t *)calloc(1, sizeof(s_bank_t));
if(!c->banks[bank])
return NULL;
if(__alloc_sprite_table(c->banks[bank], frames) < 0)
{
s_delete_bank(c, bank);
return NULL;
}
b = c->banks[bank];
b->max = frames - 1;
b->w = w;
b->h = h;
return b;
}
void s_delete_bank(s_container_t *c, unsigned bank)
{
unsigned i;
s_bank_t *b;
if(!c->banks)
return;
if(bank > c->max)
return;
b = c->banks[bank];
if(b)
{
for(i = 0; i <= b->max; ++i)
s_delete_sprite_b(b, i);
free(b->sprites);
free(b);
c->banks[bank] = NULL;
}
}
void s_delete_all_banks(s_container_t *c)
{
unsigned i;
for(i = 0; i <= c->max; ++i)
s_delete_bank(c, i);
}
/*
----------------------------------------------------------------------
Getting data
----------------------------------------------------------------------
*/
s_bank_t *s_get_bank(s_container_t *c, unsigned bank)
{
if(bank > c->max)
return NULL;
if(!c->banks[bank])
return NULL;
return c->banks[bank];
}
s_sprite_t *s_get_sprite(s_container_t *c, unsigned bank, unsigned frame)
{
s_bank_t *b = s_get_bank(c, bank);
if(!b)
return NULL;
if(frame > b->max)
return NULL;
return b->sprites[frame];
}
s_sprite_t *s_get_sprite_b(s_bank_t *b, unsigned frame)
{
if(!b)
return NULL;
if(frame > b->max)
return NULL;
return b->sprites[frame];
}
void s_detach_sprite(s_container_t *c, unsigned bank, unsigned frame)
{
s_sprite_t *s = s_get_sprite(c, bank, frame);
if(!s)
return;
s->surface = NULL;
}
/*
----------------------------------------------------------------------
File tools
----------------------------------------------------------------------
*/
static int extract_sprite(s_bank_t *bank, unsigned frame,
SDL_Surface *src, SDL_Rect *from)
{
int y;
SDL_Surface *tmp;
if(frame > bank->max)
{
log_printf(ELOG, "sprite: Too many frames!\n");
return -1;
}
if(!s_new_sprite_b(bank, frame))
return -2;
const SDL_PixelFormatDetails *src_fmt =
SDL_GetPixelFormatDetails(src->format);
if(!src_fmt)
return -3;
tmp = SDL_CreateSurface(from->w, from->h, src->format);
if(!tmp)
return -3;
/* Copy the pixel data */
if(!SDL_LockSurface(src))
{
log_printf(ELOG, "sprite: extract_sprite() failed to lock surface!\n");
SDL_DestroySurface(tmp);
return -4;
}
for(y = 0; y < tmp->h; ++y)
{
char *s = (char *)src->pixels + src->pitch * (y + from->y) +
from->x * src_fmt->bytes_per_pixel;
char *d = (char *)tmp->pixels + tmp->pitch * y;
memcpy(d, s, src_fmt->bytes_per_pixel * tmp->w);
}
SDL_UnlockSurface(src);
/* Copy palette, if any */
SDL_Palette *src_palette = SDL_GetSurfacePalette(src);
if(src_palette)
SDL_SetSurfacePalette(tmp, src_palette);
/* Copy alpha modulation and blend mode */
Uint8 src_alpha = SDL_ALPHA_OPAQUE;
SDL_GetSurfaceAlphaMod(src, &src_alpha);
SDL_SetSurfaceAlphaMod(tmp, src_alpha);
SDL_BlendMode src_blend = SDL_BLENDMODE_NONE;
SDL_GetSurfaceBlendMode(src, &src_blend);
SDL_SetSurfaceBlendMode(tmp, src_blend);
/* Copy colorkey */
if(SDL_SurfaceHasColorKey(src))
{
Uint32 src_key = 0;
SDL_GetSurfaceColorKey(src, &src_key);
SDL_SetSurfaceColorKey(tmp, true, src_key);
}
bank->sprites[frame]->surface = tmp;
DBG(log_printf(DLOG, "image %d: (%d,%d)/%dx%d @ %p\n", frame,
from->x, from->y, from->w, from->h,
bank->sprites[frame]->surface);)
return 0;
}
int s_copy_rect(s_container_t *c, unsigned bank,
unsigned frombank, unsigned fromframe, SDL_Rect *from)
{
#if 0
s_filter_args_t args;
#endif
SDL_Surface *src;
s_sprite_t *s;
s_bank_t *b;
/* Get source image */
s = s_get_sprite(c, frombank, fromframe);
if(!s)
{
log_printf(ELOG, "sprite: Couldn't get source sprite %d:%d!\n",
frombank, fromframe);
return -1;
}
src = s->surface;
if(!src)
{
log_printf(ELOG, "sprite: Sprite %d:%d"
" does not have a surface!\n",
frombank, fromframe);
return -2;
}
/* Create destination bank */
b = s_new_bank(c, bank, 1, from->w, from->h);
if(!b)
{
log_printf(ELOG, "sprite: Failed to allocate bank %d!\n", bank);
return -3;
}
if(extract_sprite(b, 0, src, from) < 0)
{
log_printf(ELOG, "sprite: Something went wrong while "
"copying from sprite %d:%d.\n",
frombank, fromframe);
return -4;
}
#if 0
memset(&args, 0, sizeof(args));
args.x = 0;
s_filter_displayformat(b, 0, 1, &args);
#endif
return 0;
}
int s_load_image(s_container_t *c, unsigned bank, const char *name)
{
SDL_Surface *src;
s_bank_t *b;
src = IMG_Load(name);
if(!src)
{
log_printf(ELOG, "sprite: Failed to load sprite \"%s\"!\n", name);
return -1;
}
SDL_Rect src_clip;
SDL_GetSurfaceClipRect(src, &src_clip);
b = s_new_bank(c, bank, 1, src_clip.w, src_clip.h);
if(!b)
{
log_printf(ELOG, "sprite: Failed to allocate bank for \"%s\"!\n", name);
return -2;
}
if(extract_sprite(b, 0, src, &src_clip) < 0)
{
log_printf(ELOG, "sprite: Something went wrong while"
" extracting sprite \"%s\".\n", name);
return -3;
}
SDL_DestroySurface(src);
__run_plugins(b, 0, 1);
return 0;
}
int s_load_sprite(s_container_t *c, unsigned bank, unsigned frame,
const char *name)
{
SDL_Surface *src;
SDL_Rect from;
s_bank_t *b = s_get_bank(c, bank);
if(!b)
{
log_printf(ELOG, "sprite: While loading \"%s\":"
" Bank %d does not exist!\n", name, bank);
return -2;
}
src = IMG_Load(name);
if(!src)
{
log_printf(ELOG, "sprite: Failed to load sprite \"%s\"!\n", name);
return -1;
}
SDL_GetSurfaceClipRect(src, &from);
if( (from.w != b->w) || (from.h != b->h) )
{
log_printf(ELOG, "sprite: Warning: Sprite \"%s\" cropped"
" to fit in bank %d.\n", name, bank);
from.w = b->w;
from.h = b->h;
}
if(extract_sprite(b, frame, src, &from) < 0)
{
log_printf(ELOG, "sprite: Something went wrong while"
" extracting sprite \"%s\".\n", name);
return -3;
}
SDL_DestroySurface(src);
__run_plugins(b, frame, 1);
return 0;
}
int s_load_bank(s_container_t *c, unsigned bank, unsigned w, unsigned h,
const char *name)
{
SDL_Surface *src;
s_bank_t *b;
int x, y;
unsigned frame = 0;
unsigned frames;
DBG(log_printf(DLOG, "s_load_bank(%p, %d, %d, %d, %s)\n",
c, bank, w, h, name);)
src = IMG_Load(name);
if(!src)
{
log_printf(ELOG, "sprite: Failed to load sprite palette %s!\n", name);
return -1;
}
if(w > src->w)
{
log_printf(ELOG, "sprite: Source image %s not wide enough!\n", name);
return -4;
}
if(h > src->h)
{
log_printf(ELOG, "sprite: Source image %s not high enough!\n", name);
return -5;
}
frames = (src->w / w) * (src->h / h);
b = s_new_bank(c, bank, frames, w, h);
if(!b)
{
log_printf(ELOG, "sprite: Failed to allocate bank for \"%s\"!\n", name);
return -2;
}
for(y = 0; y <= src->h - h; y += h)
for(x = 0; x <= src->w - w; x += w)
{
SDL_Rect r;
r.x = x;
r.y = y;
r.w = w;
r.h = h;
if(extract_sprite(b, frame, src, &r) < 0)
{
log_printf(ELOG, "sprite: Something went wrong while"
" extracting sprites.\n");
return -3;
}
++frame;
}
SDL_DestroySurface(src);
__run_plugins(b, 0, frames);
return 0;
}
int s_set_hotspot(s_container_t *c, unsigned bank, int frame, int x, int y)
{
if(frame < 0)
{
frame = 0;
while(1)
{
s_sprite_t *s = s_get_sprite(c, bank, frame++);
if(!s)
break;
s->x = x;
s->y = y;
}
}
else
{
s_sprite_t *s = s_get_sprite(c, bank, frame);
s->x = x;
s->y = y;
}
return 0;
}