TODO.md item 37 said the cast sweep buys nothing until the build turns on the warnings those casts suppress. This is that precondition, and the argument turned out to be right with evidence: -Wall found three genuine signedness mismatches in sprite.c, where uint32_t width, height and speed were passed straight to akgl_get_json_integer_value(..., int *). A cast would have silenced all three. Fixing them properly also turned up that speed is scaled by a million into a 32-bit field, so anything past 4294 ms overflowed rather than being held. The other real finding was ten -Wstringop-truncation warnings, every one a fixed-width strncpy. Those leave a name field unterminated when the input fills it -- and those fields are registry keys, handed to strcmp and SDL property calls that do not stop at the field. Same overread class fixed twice already this release. All ten now use aksl_strncpy from akstdlib, which always terminates and never NUL-pads, bounded to size - 1 so an over-long name still truncates rather than being refused. staticstring.h documented the old strncpy semantics explicitly and has been rewritten to match. Two sites in game.c are deliberately left on strncpy: both stage into a pre-zeroed buffer for the savegame's fixed-width fields, where the NUL padding is part of the format. Neither is flagged. sprite.c also had a memcpy of a full 128-byte field from a NUL-terminated string, which reads past the end of any shorter name. Not flagged by anything; found while converting its neighbours. -Werror is an option, default OFF, on only in the cmake_build and performance CI jobs. libakgl is consumed with add_subdirectory -- akbasic does exactly that -- so a target-level -Werror turns every diagnostic a newer compiler invents into a broken build for somebody else. The warning set is not even stable across build types here: an -O2 build reports ten warnings that -O0 and -fsyntax-only do not, because they need the middle end. The two jobs that set it are one of each. -Wextra is deliberately not adopted. It adds 22 findings, 17 inherent to the design: 13 -Wunused-parameter from backend vtables and SDL callbacks that must match a signature, and 4 -Wimplicit-fallthrough from libakerror's own PROCESS/HANDLE/HANDLE_GROUP, which fall through by design. Filed upstream as deps/libakerror TODO item 8; the submodule bump carries it. deps/semver/semver.c is listed directly in add_library, so the target's PRIVATE options reach it. Exempted with -w so a future semver update cannot fail this build -- verified by forcing -Wextra -Werror and watching our files fail while semver compiled clean. Verified: RelWithDebInfo, Debug and coverage builds all clean under -Werror; -Werror actually fails on a planted unused variable; an embedded consumer with AKGL_WERROR unset still configures and builds. 25/25 pass, memcheck clean, reindent --check, check_api_surface and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
310 lines
11 KiB
C
310 lines
11 KiB
C
/**
|
|
* @file sprite.c
|
|
* @brief Implements the sprite subsystem.
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_image/SDL_image.h>
|
|
#include <string.h>
|
|
#include <jansson.h>
|
|
#include <akerror.h>
|
|
#include <akstdlib.h>
|
|
|
|
#include <libgen.h>
|
|
#include <akgl/game.h>
|
|
#include <akgl/sprite.h>
|
|
#include <akgl/json_helpers.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/registry.h>
|
|
#include <akgl/staticstring.h>
|
|
#include <akgl/iterator.h>
|
|
#include <akgl/util.h>
|
|
|
|
akerr_ErrorContext *akgl_spritesheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL sprite");
|
|
FAIL_ZERO_RETURN(errctx, srccoords, AKERR_NULLPOINTER, "NULL SDL_Rect");
|
|
FAIL_ZERO_RETURN(errctx, self->sheet, AKERR_NULLPOINTER, "NULL spritesheet");
|
|
|
|
srccoords->x = self->width * self->frameids[frameid];
|
|
if ( srccoords->x >= self->sheet->texture->w ) {
|
|
srccoords->y = ((int)srccoords->x / self->sheet->texture->w) * self->height;
|
|
srccoords->x = ((int)srccoords->x % self->sheet->texture->w);
|
|
} else {
|
|
srccoords->y = 0;
|
|
}
|
|
srccoords->w = self->width;
|
|
srccoords->h = self->height;
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Find or load the spritesheet a sprite definition names.
|
|
*
|
|
* Resolves the `spritesheet.filename` path relative to the sprite definition's
|
|
* own directory, then looks that resolved path up in #AKGL_REGISTRY_SPRITESHEET.
|
|
* A hit is reused as is -- which is what keeps ten sprites cut from one image
|
|
* down to one texture -- and a miss claims a sheet from the pool and loads it,
|
|
* reading `frame_width` and `frame_height` only in that case.
|
|
*
|
|
* @param json The parsed sprite document, whose `spritesheet` object
|
|
* this reads. Required in practice; not checked.
|
|
* @param sheet Receives the sheet, whether found or freshly loaded.
|
|
* Required; not checked. On the reuse path the sheet's
|
|
* reference count is *not* incremented, so the sprite
|
|
* borrows it.
|
|
* @param relative_path Directory to resolve the image path against -- the
|
|
* directory holding the sprite JSON. Required.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_KEY If the document has no `spritesheet` object, or that object
|
|
* has no `filename` -- or, on the load path, no `frame_width` or
|
|
* `frame_height`.
|
|
* @throws AKERR_TYPE If one of those has the wrong JSON type.
|
|
* @throws AKERR_OUTOFBOUNDS If the joined path is too long for a pooled string.
|
|
* @throws ENOENT If the image path does not exist.
|
|
* @throws AKGL_ERR_SDL If the image cannot be decoded.
|
|
* @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted.
|
|
*/
|
|
static akerr_ErrorContext *sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
json_t *spritesheet_json = NULL;
|
|
int ss_frame_width = 0;
|
|
int ss_frame_height = 0;
|
|
akgl_String *ss_filename = NULL;
|
|
akgl_String *tmpstr = NULL;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
|
CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
|
CATCH(errctx, akgl_get_json_object_value((json_t *)json, "spritesheet", &spritesheet_json));
|
|
CATCH(errctx, akgl_get_json_string_value((json_t *)spritesheet_json, "filename", &ss_filename));
|
|
CATCH(errctx, akgl_path_relative(relative_path, ss_filename->data, tmpstr));
|
|
*sheet = SDL_GetPointerProperty(
|
|
AKGL_REGISTRY_SPRITESHEET,
|
|
(char *)&tmpstr->data,
|
|
NULL
|
|
);
|
|
if ( *sheet == NULL ) {
|
|
CATCH(errctx, akgl_heap_next_spritesheet(sheet));
|
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_width", &ss_frame_width));
|
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_height));
|
|
CATCH(errctx,
|
|
akgl_spritesheet_initialize(
|
|
(akgl_SpriteSheet *)*sheet,
|
|
ss_frame_width,
|
|
ss_frame_height,
|
|
(char *)&tmpstr->data)
|
|
);
|
|
}
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_string(ss_filename));
|
|
IGNORE(akgl_heap_release_string(tmpstr));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
json_t *json = NULL;
|
|
json_t *frames = NULL;
|
|
json_error_t error;
|
|
akgl_Sprite *obj = NULL;
|
|
akgl_SpriteSheet *sheet = NULL;
|
|
akgl_String *spritename = NULL;
|
|
akgl_String *filename_copy = NULL;
|
|
int i = 0;
|
|
int framecount = 0;
|
|
int frameid = 0;
|
|
int jsonint = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_sprite(&obj));
|
|
CATCH(errctx, akgl_heap_next_string(&filename_copy));
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
(strlen(filename) >= AKGL_MAX_STRING_LENGTH),
|
|
AKERR_OUTOFBOUNDS,
|
|
"Sprite filename exceeds temporary string capacity"
|
|
);
|
|
SDL_strlcpy(filename_copy->data, filename, AKGL_MAX_STRING_LENGTH);
|
|
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
|
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
|
CATCH(errctx, akgl_heap_next_string(&spritename));
|
|
CATCH(errctx, akgl_string_initialize(spritename, NULL));
|
|
|
|
json = (json_t *)json_load_file(filename, 0, &error);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
json,
|
|
AKERR_NULLPOINTER,
|
|
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
|
|
);
|
|
|
|
CATCH(errctx, sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
|
|
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
|
|
CATCH(errctx,
|
|
akgl_sprite_initialize(
|
|
(akgl_Sprite *)obj,
|
|
spritename->data,
|
|
(akgl_SpriteSheet *)sheet)
|
|
);
|
|
|
|
// Read into an int and narrow deliberately. These three fields are
|
|
// uint32_t and the accessor takes an int *, so passing them directly was
|
|
// a signedness mismatch -- the kind -Wpointer-sign exists to catch, and
|
|
// the kind a cast would have hidden.
|
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &jsonint));
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
(jsonint <= 0),
|
|
AKERR_VALUE,
|
|
"Sprite %s has width %d; a sprite must be at least one pixel wide",
|
|
(char *)&obj->name,
|
|
jsonint);
|
|
obj->width = (uint32_t)jsonint;
|
|
|
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &jsonint));
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
(jsonint <= 0),
|
|
AKERR_VALUE,
|
|
"Sprite %s has height %d; a sprite must be at least one pixel high",
|
|
(char *)&obj->name,
|
|
jsonint);
|
|
obj->height = (uint32_t)jsonint;
|
|
|
|
// speed is milliseconds in the file and nanoseconds in the struct, and
|
|
// the struct field is 32 bits, so anything past this overflows on the
|
|
// multiply below rather than being held.
|
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &jsonint));
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
((jsonint < 0) || ((uint32_t)jsonint > (UINT32_MAX / AKGL_TIME_ONEMS_NS))),
|
|
AKERR_OUTOFBOUNDS,
|
|
"Sprite %s has a frame speed of %d ms; the range is 0 to %u",
|
|
(char *)&obj->name,
|
|
jsonint,
|
|
(UINT32_MAX / AKGL_TIME_ONEMS_NS));
|
|
obj->speed = ((uint32_t)jsonint) * AKGL_TIME_ONEMS_NS;
|
|
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop));
|
|
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
|
|
|
|
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
|
|
// Bounded before anything is written. frameids is
|
|
// AKGL_SPRITE_MAX_FRAMES bytes, and this loop used to take its count
|
|
// straight from the document -- so a definition with seventeen frames
|
|
// wrote past the array, past the rest of akgl_Sprite, and into the
|
|
// neighbouring pool slot.
|
|
framecount = (int)json_array_size((json_t *)frames);
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
(framecount > AKGL_SPRITE_MAX_FRAMES),
|
|
AKERR_OUTOFBOUNDS,
|
|
"Sprite %s declares %d frames; the maximum is %d",
|
|
(char *)&obj->name,
|
|
framecount,
|
|
AKGL_SPRITE_MAX_FRAMES
|
|
);
|
|
obj->frames = framecount;
|
|
for ( i = 0 ; i < framecount; i++ ) {
|
|
// Read into an int and narrow deliberately. The old form wrote
|
|
// through a uint32_t * cast of a uint8_t *, so every element write
|
|
// touched four bytes; it only appeared to work because the next
|
|
// iteration overwrote the spill and the last one landed in the
|
|
// struct's alignment padding.
|
|
CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, &frameid));
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
((frameid < 0) || (frameid > UINT8_MAX)),
|
|
AKERR_OUTOFBOUNDS,
|
|
"Sprite %s frame %d is %d; frame numbers are 0..%d",
|
|
(char *)&obj->name,
|
|
i,
|
|
frameid,
|
|
UINT8_MAX
|
|
);
|
|
obj->frameids[i] = (uint8_t)frameid;
|
|
}
|
|
} CLEANUP {
|
|
// The sprite copies every field it wants out of the document and the
|
|
// sheet is found by resolved path, so nothing here points into the
|
|
// parsed tree once this block runs. Released on both paths.
|
|
if ( json != NULL ) {
|
|
json_decref(json);
|
|
json = NULL;
|
|
}
|
|
if ( errctx != NULL && errctx->status != 0 ) {
|
|
IGNORE(akgl_heap_release_sprite(obj));
|
|
IGNORE(akgl_heap_release_spritesheet(sheet));
|
|
}
|
|
IGNORE(akgl_heap_release_string(spritename));
|
|
IGNORE(akgl_heap_release_string(filename_copy));
|
|
//IGNORE(akgl_heap_release_string(tmpstr));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, spr, AKERR_NULLPOINTER, "Null sprite reference");
|
|
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Empty sprite name");
|
|
FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference");
|
|
|
|
memset(spr, 0x00, sizeof(akgl_Sprite));
|
|
// Was a memcpy of the full field width from a NUL-terminated string, which
|
|
// reads past the end of any name shorter than the field. Copies only what
|
|
// is there now, and terminates.
|
|
PASS(errctx, aksl_strncpy(spr->name, sizeof(spr->name), name, sizeof(spr->name) - 1));
|
|
spr->sheet = sheet;
|
|
FAIL_ZERO_RETURN(
|
|
errctx,
|
|
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITE, (char *)&spr->name, (void *)spr),
|
|
AKERR_KEY,
|
|
"Unable to add sprite to registry");
|
|
spr->refcount += 1;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
//akgl_String *tmpstr = NULL;
|
|
|
|
ATTEMPT {
|
|
FAIL_ZERO_BREAK(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet pointer");
|
|
FAIL_ZERO_BREAK(errctx, filename, AKERR_NULLPOINTER, "Null filename pointer");
|
|
|
|
memset(sheet, 0x00, sizeof(akgl_SpriteSheet));
|
|
|
|
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
|
|
|
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
|
CATCH(errctx, aksl_strncpy((char *)&sheet->name, sizeof(sheet->name), filename, sizeof(sheet->name) - 1));
|
|
|
|
sheet->texture = IMG_LoadTexture(akgl_renderer->sdl_renderer, filename);
|
|
FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
|
|
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet),
|
|
AKERR_KEY,
|
|
"Unable to add spritesheet to registry: %s",
|
|
SDL_GetError());
|
|
sheet->refcount += 1;
|
|
} CLEANUP {
|
|
//IGNORE(akgl_heap_release_string(tmpstr));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|