Files
libakgl/src/draw.c

622 lines
20 KiB
C
Raw Normal View History

/**
* @file draw.c
* @brief Implements the draw subsystem.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <akerror.h>
#include <akgl/draw.h>
#include <akgl/error.h>
#include <akgl/game.h>
/** @brief One horizontal run of pixels the flood fill has still to examine. */
typedef struct {
Document what the functions actually do instead of that they can fail The Doxygen comments were generated from the declarations, so 217 @throws lines across 21 headers read "When the corresponding validation or operation fails" and told a caller nothing beyond the status name. The @param lines were the same shape: every output was "Output destination populated by the function", every instance "Object to initialize, inspect, or modify". Rewritten against the implementations, following the pattern libakstdlib already uses: - @throws names the condition. akgl_sprite_load_json separated AKERR_KEY (absent) from AKERR_TYPE (present, wrong type) from AKERR_OUTOFBOUNDS (filename too long, or array indexed past its end), and gained AKGL_ERR_SDL and AKGL_ERR_HEAP, which it raises and never declared. - Parameters say whether they are required, what a NULL means, and what is written on a failure path. Where an argument is not checked, the doc says so: akgl_heap_next_actor's dest is a crash on NULL, not an error, and akgl_render_2d_frame_start dereferences self before testing it. - The conventions move up to the file blocks so the per-function docs stay short. json_helpers.h states once that absence is an error here and that json_t * results are borrowed; heap.h explains the pool model and the acquire asymmetry; physics.h carries the thrust/environmental/velocity table. - Struct fields, enum values, macros and exported globals are documented, including the dead ones - sprite_w/sprite_h, movetimer, p_scale and timer_gravity are read by nothing, and say so. Also fixes ten comments in error.h and audio.h that opened with /** rather than /**<, so Doxygen attached them to the following entity and rendered the text as part of the macro's value. Verified against the generated HTML. Comments only - no declaration changed. Doxygen builds clean under WARN_AS_ERROR, scripts/reindent.sh --check passes, 19/19 suites pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:02:20 -04:00
int x1; /**< Leftmost column of the run, inclusive. */
int x2; /**< Rightmost column of the run, inclusive. */
int y; /**< The row the run is on. */
} FloodSpan;
/*
* The flood fill's working stack. File scope and fixed size rather than a local
* array because AKGL_DRAW_MAX_FLOOD_SPANS spans is 48 KB, which does not belong
* on the stack of a function a game may call every frame. The consequence is
* that akgl_draw_flood_fill is not reentrant -- it is a single-threaded
* immediate-mode operation on a single render target, and so is everything else
* that touches an SDL_Renderer.
*/
static FloodSpan floodspans[AKGL_DRAW_MAX_FLOOD_SPANS];
/**
* @brief Remember the renderer's draw color and replace it with @p color.
*
* @p previous is written before anything that can fail, so a caller may restore
* it unconditionally from a CLEANUP block.
Document what the functions actually do instead of that they can fail The Doxygen comments were generated from the declarations, so 217 @throws lines across 21 headers read "When the corresponding validation or operation fails" and told a caller nothing beyond the status name. The @param lines were the same shape: every output was "Output destination populated by the function", every instance "Object to initialize, inspect, or modify". Rewritten against the implementations, following the pattern libakstdlib already uses: - @throws names the condition. akgl_sprite_load_json separated AKERR_KEY (absent) from AKERR_TYPE (present, wrong type) from AKERR_OUTOFBOUNDS (filename too long, or array indexed past its end), and gained AKGL_ERR_SDL and AKGL_ERR_HEAP, which it raises and never declared. - Parameters say whether they are required, what a NULL means, and what is written on a failure path. Where an argument is not checked, the doc says so: akgl_heap_next_actor's dest is a crash on NULL, not an error, and akgl_render_2d_frame_start dereferences self before testing it. - The conventions move up to the file blocks so the per-function docs stay short. json_helpers.h states once that absence is an error here and that json_t * results are borrowed; heap.h explains the pool model and the acquire asymmetry; physics.h carries the thrust/environmental/velocity table. - Struct fields, enum values, macros and exported globals are documented, including the dead ones - sprite_w/sprite_h, movetimer, p_scale and timer_gravity are read by nothing, and say so. Also fixes ten comments in error.h and audio.h that opened with /** rather than /**<, so Doxygen attached them to the following entity and rendered the text as part of the macro's value. Verified against the generated HTML. Comments only - no declaration changed. Doxygen builds clean under WARN_AS_ERROR, scripts/reindent.sh --check passes, 19/19 suites pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:02:20 -04:00
*
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`;
* every caller has already checked both.
* @param color The colour to install.
* @param previous Receives the colour that was in place. Assumed non-`NULL`.
* Pre-filled with opaque black, so it is safe to restore from
* even if the query below fails.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If the current draw colour cannot be read or the new one
* cannot be set. The message carries `SDL_GetError()`.
*/
static akerr_ErrorContext *push_draw_color(akgl_RenderBackend *self, SDL_Color color, SDL_Color *previous)
{
PREPARE_ERROR(errctx);
previous->r = 0x00;
previous->g = 0x00;
previous->b = 0x00;
previous->a = SDL_ALPHA_OPAQUE;
FAIL_ZERO_RETURN(
errctx,
SDL_GetRenderDrawColor(self->sdl_renderer, &previous->r, &previous->g, &previous->b, &previous->a),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
FAIL_ZERO_RETURN(
errctx,
SDL_SetRenderDrawColor(self->sdl_renderer, color.r, color.g, color.b, color.a),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}
Document what the functions actually do instead of that they can fail The Doxygen comments were generated from the declarations, so 217 @throws lines across 21 headers read "When the corresponding validation or operation fails" and told a caller nothing beyond the status name. The @param lines were the same shape: every output was "Output destination populated by the function", every instance "Object to initialize, inspect, or modify". Rewritten against the implementations, following the pattern libakstdlib already uses: - @throws names the condition. akgl_sprite_load_json separated AKERR_KEY (absent) from AKERR_TYPE (present, wrong type) from AKERR_OUTOFBOUNDS (filename too long, or array indexed past its end), and gained AKGL_ERR_SDL and AKGL_ERR_HEAP, which it raises and never declared. - Parameters say whether they are required, what a NULL means, and what is written on a failure path. Where an argument is not checked, the doc says so: akgl_heap_next_actor's dest is a crash on NULL, not an error, and akgl_render_2d_frame_start dereferences self before testing it. - The conventions move up to the file blocks so the per-function docs stay short. json_helpers.h states once that absence is an error here and that json_t * results are borrowed; heap.h explains the pool model and the acquire asymmetry; physics.h carries the thrust/environmental/velocity table. - Struct fields, enum values, macros and exported globals are documented, including the dead ones - sprite_w/sprite_h, movetimer, p_scale and timer_gravity are read by nothing, and say so. Also fixes ten comments in error.h and audio.h that opened with /** rather than /**<, so Doxygen attached them to the following entity and rendered the text as part of the macro's value. Verified against the generated HTML. Comments only - no declaration changed. Doxygen builds clean under WARN_AS_ERROR, scripts/reindent.sh --check passes, 19/19 suites pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:02:20 -04:00
/**
* @brief Put back the draw color push_draw_color() recorded.
*
* Called from `CLEANUP` blocks under `IGNORE()`, so its error is logged rather
* than propagated -- failing to restore a colour must not mask the failure the
* cleanup is unwinding from.
*
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`.
* @param previous The colour push_draw_color() recorded. Assumed non-`NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If the draw colour cannot be set.
*/
static akerr_ErrorContext *pop_draw_color(akgl_RenderBackend *self, SDL_Color *previous)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(
errctx,
SDL_SetRenderDrawColor(self->sdl_renderer, previous->r, previous->g, previous->b, previous->a),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}
Fix the type, macro and state-table defects, and the leftover debris Closes internal-consistency items 19 through 36, 38 and 41. Item 37, the ~180 redundant casts, is deliberately left open with its reasoning in TODO.md: the benefit only arrives once the build turns on the warnings those casts suppress, and doing it before that is churn across the two files with the most outstanding functional defects. The two that were real bugs are in the actor state table. AKGL_ACTOR_STATE_STRING_NAMES was declared [AKGL_ACTOR_MAX_STATES+1] and defined [32], so a consumer trusting the declared bound read past the object; and indices 11 and 12 were named UNDEFINED_11 and UNDEFINED_12 where actor.h has MOVING_IN and MOVING_OUT, so no character JSON could bind a sprite to either state. tests/registry.c now walks the whole table -- every entry non-NULL, every entry resolving to its own bit, no two entries sharing a name. The bitmask macros are parenthesized and AKGL_BITMASK_CLEAR has lost the semicolon inside its body. Writing tests/bitmasks.c for that turned up something worth knowing: the obvious test does not catch it. For a bit that is set, the misparse `!(mask & bit) == bit` gives the same answer as the correct one. It only diverges for an unset bit whose value is not 1, and that is the shape the suite uses now. akgl_draw_background was the last public function outside the error protocol. It takes a backend like everything else in draw.h, restores the draw colour it found, and is tested -- TODO.md had it filed under "needs the offscreen renderer harness", which was never true; what it needed was to stop reading the global. All eight registry initializers go through one helper, so the seven that leaked an SDL_PropertiesID on every call after the first no longer do. Fixed in the same place because it is the same function: akgl_registry_init never called akgl_registry_init_properties, which made akgl_set_property a silent no-op for anyone not going through akgl_game_init -- Defects, Known and still open item 3. Also: AKGL_COLLIDE_RECTANGLES (three open parens, two closes) and akgl_Frame deleted, float32_t/float64_t used consistently, the developer-specific debug logging removed from the controller inner loop, the abandoned SDL_GetBasePath comments removed, nine unused locals removed, and dst renamed to dest. akgl_game_update's default flags no longer OR the same bit twice. That changes nothing today, and the reason is Performance item 32: the loop never reads either bit, which is why every actor is updated sixteen times a frame. Still open. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 00:15:36 -04:00
/* Draw a Gimpish background pattern to show transparency in the image */
akerr_ErrorContext *akgl_draw_background(akgl_RenderBackend *self, int w, int h)
{
PREPARE_ERROR(errctx);
SDL_Color col[2] = {
{ 0x66, 0x66, 0x66, 0xff },
{ 0x99, 0x99, 0x99, 0xff },
};
SDL_Color previous;
SDL_FRect rect;
const int dx = 8, dy = 8;
bool pushed = false;
bool drawfailed = false;
int i = 0;
int x = 0;
int y = 0;
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
rect.w = (float)dx;
rect.h = (float)dy;
ATTEMPT {
CATCH(errctx, push_draw_color(self, col[0], &previous));
pushed = true;
// The two SDL calls are checked through a flag rather than with
// FAIL_ZERO_BREAK. Inside these loops a `break` would leave the loop,
// not the ATTEMPT block, and the fill would carry on with the failure
// unnoticed -- the hazard AGENTS.md describes for CATCH inside a loop.
for ( y = 0; (y < h) && (drawfailed == false); y += dy ) {
for ( x = 0; x < w; x += dx ) {
/* use an 8x8 checkerboard pattern */
i = (((x ^ y) >> 3) & 1);
if ( SDL_SetRenderDrawColor(self->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a) == false ) {
drawfailed = true;
break;
}
rect.x = (float)x;
rect.y = (float)y;
if ( SDL_RenderFillRect(self->sdl_renderer, &rect) == false ) {
drawfailed = true;
break;
}
}
}
FAIL_NONZERO_BREAK(errctx, drawfailed, AKGL_ERR_SDL, "%s", SDL_GetError());
} CLEANUP {
if ( pushed == true ) {
IGNORE(pop_draw_color(self, &previous));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Fill the four-connected region of @p oldpixel around a seed pixel.
*
* A scanline fill: each entry on the stack is a run of pixels on one row that
* still has to be examined. Finding a matching pixel expands it to the whole
* run it belongs to, fills that run, and pushes the rows above and below.
* Filled pixels no longer match @p oldpixel, which is what terminates it.
*
* @p surface must be SDL_PIXELFORMAT_RGBA32; the fill compares and writes whole
* 32-bit words rather than going through SDL_ReadSurfacePixel per pixel.
*
* @p dirty is set to the bounding box of everything written, so the caller can
* put back only the pixels that changed.
*
* Running out of stack leaves the region partially filled and reports
* AKERR_OUTOFBOUNDS. There is no way to unwind a partial fill short of keeping
* a copy of the whole surface, and the caller asked for a bounded operation.
Document what the functions actually do instead of that they can fail The Doxygen comments were generated from the declarations, so 217 @throws lines across 21 headers read "When the corresponding validation or operation fails" and told a caller nothing beyond the status name. The @param lines were the same shape: every output was "Output destination populated by the function", every instance "Object to initialize, inspect, or modify". Rewritten against the implementations, following the pattern libakstdlib already uses: - @throws names the condition. akgl_sprite_load_json separated AKERR_KEY (absent) from AKERR_TYPE (present, wrong type) from AKERR_OUTOFBOUNDS (filename too long, or array indexed past its end), and gained AKGL_ERR_SDL and AKGL_ERR_HEAP, which it raises and never declared. - Parameters say whether they are required, what a NULL means, and what is written on a failure path. Where an argument is not checked, the doc says so: akgl_heap_next_actor's dest is a crash on NULL, not an error, and akgl_render_2d_frame_start dereferences self before testing it. - The conventions move up to the file blocks so the per-function docs stay short. json_helpers.h states once that absence is an error here and that json_t * results are borrowed; heap.h explains the pool model and the acquire asymmetry; physics.h carries the thrust/environmental/velocity table. - Struct fields, enum values, macros and exported globals are documented, including the dead ones - sprite_w/sprite_h, movetimer, p_scale and timer_gravity are read by nothing, and say so. Also fixes ten comments in error.h and audio.h that opened with /** rather than /**<, so Doxygen attached them to the following entity and rendered the text as part of the macro's value. Verified against the generated HTML. Comments only - no declaration changed. Doxygen builds clean under WARN_AS_ERROR, scripts/reindent.sh --check passes, 19/19 suites pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:02:20 -04:00
*
* @param surface The pixels to fill, in SDL_PIXELFORMAT_RGBA32. Assumed
* non-`NULL` and locked-or-lockless; the caller converts.
* @param x Seed column. Assumed inside the surface -- akgl_draw_flood_fill
* has already range-checked it.
* @param y Seed row, likewise.
* @param oldpixel The packed pixel value the region is made of. Everything else
* is a boundary.
* @param newpixel The packed pixel value to write. Must differ from @p oldpixel:
* if they are equal the fill never terminates making progress,
* which is why the caller checks that case first.
* @param dirty Receives the bounding box of everything written, so the caller
* can put back only the pixels that changed. Assumed non-`NULL`.
* Left describing an empty box if nothing matched.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If the region needs more than
* #AKGL_DRAW_MAX_FLOOD_SPANS pending spans. @p dirty is then not written
* and the surface is partially filled.
*/
static akerr_ErrorContext *flood_region(SDL_Surface *surface, int x, int y, uint32_t oldpixel, uint32_t newpixel, SDL_Rect *dirty)
{
uint32_t *pixels = (uint32_t *)surface->pixels;
int pitch = surface->pitch / (int)sizeof(uint32_t);
int count = 0;
int col = 0;
int left = 0;
int right = 0;
int i = 0;
int minx = surface->w;
int miny = surface->h;
int maxx = -1;
int maxy = -1;
FloodSpan span;
PREPARE_ERROR(errctx);
floodspans[0].x1 = x;
floodspans[0].x2 = x;
floodspans[0].y = y;
count = 1;
while ( count > 0 ) {
count -= 1;
span = floodspans[count];
col = span.x1;
while ( col <= span.x2 ) {
if ( pixels[(span.y * pitch) + col] != oldpixel ) {
col += 1;
continue;
}
left = col;
while ( left > 0 && pixels[(span.y * pitch) + (left - 1)] == oldpixel ) {
left -= 1;
}
right = col;
while ( right < (surface->w - 1) && pixels[(span.y * pitch) + (right + 1)] == oldpixel ) {
right += 1;
}
for ( i = left; i <= right; i++ ) {
pixels[(span.y * pitch) + i] = newpixel;
}
if ( left < minx ) {
minx = left;
}
if ( right > maxx ) {
maxx = right;
}
if ( span.y < miny ) {
miny = span.y;
}
if ( span.y > maxy ) {
maxy = span.y;
}
// Two pushes per run, so the check is for room for both.
FAIL_NONZERO_RETURN(
errctx,
((count + 2) > AKGL_DRAW_MAX_FLOOD_SPANS),
AKERR_OUTOFBOUNDS,
"Region needs more than %d pending spans; it is partially filled",
AKGL_DRAW_MAX_FLOOD_SPANS);
if ( span.y > 0 ) {
floodspans[count].x1 = left;
floodspans[count].x2 = right;
floodspans[count].y = span.y - 1;
count += 1;
}
if ( span.y < (surface->h - 1) ) {
floodspans[count].x1 = left;
floodspans[count].x2 = right;
floodspans[count].y = span.y + 1;
count += 1;
}
col = right + 1;
}
}
dirty->x = minx;
dirty->y = miny;
dirty->w = (maxx - minx) + 1;
dirty->h = (maxy - miny) + 1;
SUCCEED_RETURN(errctx);
}
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
akerr_ErrorContext *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color)
{
SDL_Color previous;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
FAIL_ZERO_BREAK(
errctx,
SDL_RenderPoint(self->sdl_renderer, x, y),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
IGNORE(pop_draw_color(self, &previous));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
akerr_ErrorContext *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color)
{
SDL_Color previous;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
FAIL_ZERO_BREAK(
errctx,
SDL_RenderLine(self->sdl_renderer, x1, y1, x2, y2),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
IGNORE(pop_draw_color(self, &previous));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
akerr_ErrorContext *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
{
SDL_Color previous;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
FAIL_ZERO_BREAK(
errctx,
SDL_RenderRect(self->sdl_renderer, rect),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
IGNORE(pop_draw_color(self, &previous));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
akerr_ErrorContext *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
{
SDL_Color previous;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
FAIL_ZERO_BREAK(
errctx,
SDL_RenderFillRect(self->sdl_renderer, rect),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
IGNORE(pop_draw_color(self, &previous));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
akerr_ErrorContext *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color)
{
SDL_Color previous;
SDL_FPoint octants[8];
int centerx = 0;
int centery = 0;
int r = 0;
int offsetx = 0;
int offsety = 0;
int decision = 0;
bool plotted = true;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_NONZERO_RETURN(errctx, (radius < 0), AKERR_OUTOFBOUNDS, "Negative radius %f", radius);
centerx = (int)SDL_lroundf(x);
centery = (int)SDL_lroundf(y);
r = (int)SDL_lroundf(radius);
offsety = r;
// The midpoint decision variable, started so the first step chooses between
// (0, r) and (1, r-1) correctly.
decision = 1 - r;
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
while ( offsety >= offsetx ) {
// Eight-way symmetry: one computed point in the second octant gives
// the seven others by reflection.
octants[0].x = (float)(centerx + offsetx);
octants[0].y = (float)(centery + offsety);
octants[1].x = (float)(centerx - offsetx);
octants[1].y = (float)(centery + offsety);
octants[2].x = (float)(centerx + offsetx);
octants[2].y = (float)(centery - offsety);
octants[3].x = (float)(centerx - offsetx);
octants[3].y = (float)(centery - offsety);
octants[4].x = (float)(centerx + offsety);
octants[4].y = (float)(centery + offsetx);
octants[5].x = (float)(centerx - offsety);
octants[5].y = (float)(centery + offsetx);
octants[6].x = (float)(centerx + offsety);
octants[6].y = (float)(centery - offsetx);
octants[7].x = (float)(centerx - offsety);
octants[7].y = (float)(centery - offsetx);
// A CATCH here would break this loop rather than leave the function,
// so failure is recorded and reported once the loop is done.
if ( !SDL_RenderPoints(self->sdl_renderer, octants, 8) ) {
plotted = false;
}
offsetx += 1;
if ( decision < 0 ) {
decision += (2 * offsetx) + 1;
} else {
offsety -= 1;
decision += 2 * (offsetx - offsety) + 1;
}
}
FAIL_ZERO_BREAK(errctx, plotted, AKGL_ERR_SDL, "%s", SDL_GetError());
} CLEANUP {
IGNORE(pop_draw_color(self, &previous));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
akerr_ErrorContext *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color)
{
SDL_Surface *target = NULL;
SDL_Surface *rgba = NULL;
SDL_Texture *patch = NULL;
// Only written by a successful flood_region(), and only read after one, but
// the paths in between are far enough apart that the compiler cannot see it.
SDL_Rect dirty = { 0, 0, 0, 0 };
SDL_FRect src;
SDL_FRect dest;
uint32_t *pixels = NULL;
uint32_t oldpixel = 0;
uint32_t newpixel = 0;
int width = 0;
int height = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
ATTEMPT {
FAIL_ZERO_BREAK(
errctx,
SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
FAIL_NONZERO_BREAK(
errctx,
((x < 0) || (y < 0) || (x >= width) || (y >= height)),
AKERR_OUTOFBOUNDS,
"Seed pixel %d,%d is outside the %dx%d render target",
x, y, width, height);
target = SDL_RenderReadPixels(self->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, target, AKGL_ERR_SDL, "%s", SDL_GetError());
// The fill works on 32-bit words, so the layout has to be known rather
// than whatever the render target happens to use.
rgba = SDL_ConvertSurface(target, SDL_PIXELFORMAT_RGBA32);
FAIL_ZERO_BREAK(errctx, rgba, AKGL_ERR_SDL, "%s", SDL_GetError());
pixels = (uint32_t *)rgba->pixels;
oldpixel = pixels[(y * (rgba->pitch / (int)sizeof(uint32_t))) + x];
newpixel = SDL_MapSurfaceRGBA(rgba, color.r, color.g, color.b, color.a);
if ( oldpixel == newpixel ) {
// Already the requested color. Walking it would compare filled
// pixels against themselves and find nothing, so say so up front.
SUCCEED_BREAK(errctx);
}
CATCH(errctx, flood_region(rgba, x, y, oldpixel, newpixel, &dirty));
patch = SDL_CreateTextureFromSurface(self->sdl_renderer, rgba);
FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
// Replace rather than blend: this is a framebuffer operation, and the
// pixels being put back are the ones that were just read out of it.
FAIL_ZERO_BREAK(
errctx,
SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
// Only the bounding box of what changed goes back to the target.
src.x = (float)dirty.x;
src.y = (float)dirty.y;
src.w = (float)dirty.w;
src.h = (float)dirty.h;
dest = src;
FAIL_ZERO_BREAK(
errctx,
SDL_RenderTexture(self->sdl_renderer, patch, &src, &dest),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
if ( patch != NULL ) {
SDL_DestroyTexture(patch);
}
if ( rgba != NULL ) {
SDL_DestroySurface(rgba);
}
if ( target != NULL ) {
SDL_DestroySurface(target);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
akerr_ErrorContext *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest)
{
SDL_Surface *saved = NULL;
int width = 0;
int height = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
ATTEMPT {
FAIL_NONZERO_BREAK(
errctx,
((src->w <= 0) || (src->h <= 0)),
AKERR_OUTOFBOUNDS,
"Region %dx%d has no area",
src->w, src->h);
FAIL_ZERO_BREAK(
errctx,
SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
// SDL clips a read to the target and hands back a smaller surface than
// was asked for, which a caller pasting it back would not notice.
FAIL_NONZERO_BREAK(
errctx,
((src->x < 0) || (src->y < 0) ||
((src->x + src->w) > width) || ((src->y + src->h) > height)),
AKERR_OUTOFBOUNDS,
"Region %d,%d %dx%d does not fit inside the %dx%d render target",
src->x, src->y, src->w, src->h, width, height);
saved = SDL_RenderReadPixels(self->sdl_renderer, src);
FAIL_ZERO_BREAK(errctx, saved, AKGL_ERR_SDL, "%s", SDL_GetError());
if ( *dest == NULL ) {
*dest = saved;
// Ownership has moved to the caller; CLEANUP must not free it.
saved = NULL;
} else {
FAIL_NONZERO_BREAK(
errctx,
(((*dest)->w != src->w) || ((*dest)->h != src->h)),
AKERR_OUTOFBOUNDS,
"Destination surface is %dx%d, region is %dx%d",
(*dest)->w, (*dest)->h, src->w, src->h);
FAIL_ZERO_BREAK(
errctx,
SDL_SetSurfaceBlendMode(saved, SDL_BLENDMODE_NONE),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
FAIL_ZERO_BREAK(
errctx,
SDL_BlitSurface(saved, NULL, *dest, NULL),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
}
} CLEANUP {
if ( saved != NULL ) {
SDL_DestroySurface(saved);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
akerr_ErrorContext *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y)
{
SDL_Texture *patch = NULL;
SDL_FRect dest;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
ATTEMPT {
patch = SDL_CreateTextureFromSurface(self->sdl_renderer, src);
FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
// Replace what is on the target, the way GSHAPE does by default.
FAIL_ZERO_BREAK(
errctx,
SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
dest.x = x;
dest.y = y;
dest.w = (float)src->w;
dest.h = (float)src->h;
FAIL_ZERO_BREAK(
errctx,
SDL_RenderTexture(self->sdl_renderer, patch, NULL, &dest),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
if ( patch != NULL ) {
SDL_DestroyTexture(patch);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}