There was no audio API at all: SDL3_mixer was vendored and AKGL_REGISTRY_MUSIC was declared, but nothing opened a mixer, and a mixer plays recordings anyway. BASIC 7.0's SOUND, PLAY, ENVELOPE and VOL describe notes, so this adds a tone generator -- three voices, five waveforms, an ADSR envelope each, mixed to one float stream and fed to an SDL_AudioStream. The voice table works whether or not a device is open. akgl_audio_init connects it to one; a host that owns its own audio pipeline can call akgl_audio_mix instead. That is also what makes the tests deterministic: a device pulls samples on SDL's audio thread whenever it likes, so tests/audio.c mixes by hand and only opens a device in its last test. Phase is computed from the frame counter rather than accumulated, because a float increment of hz/44100 added 44100 times a second walks a held note off pitch. A voice that has never been configured defaults to a square wave at full level, since a zeroed voice would have a sustain of 0.0 and make no sound with no error to explain why. Voices summing past full scale are clamped rather than scaled, so one voice plays at the level it was asked for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
46 KiB
TODO
Internal consistency
Findings from a sweep of src/ and include/. These are consistency and
convention problems, not new functional defects — where one has a functional
consequence it is called out. Ordered roughly by blast radius. Items that
overlap the existing Defects list are cross-referenced rather than repeated.
1. Public naming conventions
-
Include guards use three different schemes.
_AKGL_ACTOR_H_,_AKGL_CHARACTER_H_,_AKGL_GAME_H_,_AKGL_HEAP_H_,_AKGL_ITERATOR_H_,_AKGL_SPRITE_H_, and_AKGL_TYPES_H_carry the project prefix;_ASSETS_H_,_CONTROLLER_H_,_DRAW_H_,_ERROR_H_,_JSON_HELPERS_H_,_PHYSICS_H_,_REGISTRY_H_,_RENDERER_H_,_TEXT_H_,_TILEMAP_H_, and_UTIL_H_do not. Pick_AKGL_<FILE>_H_everywhere.The worst case is
include/akgl/staticstring.h:6, which guards with_STRING_H_— a name several libc implementations use for their own<string.h>. The same file then does#include "string.h"(line 9) with quotes, which is a relative-first lookup that only reaches the system header by accident. Rename the guard to_AKGL_STATICSTRING_H_and use#include <string.h>. -
Function names contradict the stated convention.
AGENTS.mdsays public symbols useakgl_and types useakgl_TypeName. Exceptions:akgl_Actor_cmhf_left_onand its seven siblings (include/akgl/actor.h:196-252) embed the type name in a function name, unlike every other actor entry point (akgl_actor_*). Rename toakgl_actor_cmhf_*.akgl_game_updateFPS(include/akgl/game.h:104) is camelCase; every other function is snake_case.akgl_render_init2d(include/akgl/renderer.h:83) puts the2dat the end while the six functions it installs areakgl_render_2d_*. Make itakgl_render_2d_init.akgl_sprite_sheet_coords_for_frame(include/akgl/sprite.h:86) spells itsprite_sheet;akgl_spritesheet_initializeandakgl_heap_next_spritesheetspell itspritesheet.
-
Two public types have no prefix at all.
pointandRectanglePoints(include/akgl/util.h:13-25) are unprefixed, andRectanglePointsis PascalCase with no namespace. Both are dumped into every translation unit that includesutil.h. Rename toakgl_Point/akgl_RectanglePoints. -
Global variables use four different conventions.
window,bgm,game,gamemap,renderer,physics, andcamera(src/game.c:26-43) are unprefixed single common words exported from a shared library —rendererandcamerain particular are very likely to collide with a consuming game. Alongside them the same file exportsakgl_mixerandakgl_tracks(prefixed),_akgl_renderer/_akgl_camera/_akgl_physics/_akgl_gamemap(underscore-prefixed, which is reserved at file scope), and elsewhereHEAP_ACTOR…HEAP_STRING(src/heap.c:17-21) andGAME_ControlMaps(src/controller.c:13) are SCREAMING_SNAKE, which the convention reserves for constants and macros. Settle onakgl_for all exported objects. -
AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH(include/akgl/character.h:13) is a character constant carrying theSPRITEprefix, and lives incharacter.h. Rename toAKGL_CHARACTER_MAX_NAME_LENGTH. -
AKGL_TIME_ONESEC_MSis misnamed and the error is live.include/akgl/game.h:22defines it as1000000. One second in milliseconds is1000;1000000is the number of nanoseconds in a millisecond. The name says "one second" but the value means "one millisecond", so:src/character.c:209andsrc/sprite.c:141use it correctly as a milliseconds-to-nanoseconds scale factor.src/game.c:136uses it as documented — a one-second budget for the state lock — in a loop that advancestotaltime += 100alongsideSDL_Delay(100). The loop therefore runs 10,000 iterations of 100 ms, soakgl_game_state_lockblocks for roughly 16 minutes rather than 1 second before reporting failure.
Rename to
AKGL_TIME_ONEMS_NSto matchAKGL_TIME_ONESEC_NS, and giveakgl_game_state_locka real one-second budget.
2. Header/implementation surface drift
-
Nineteen non-static functions are defined in
src/but declared in no header. They have external linkage and public-looking names, so they are part of the ABI whether intended or not, and no consumer can call them:akgl_game_load_objectnamemap,akgl_game_load_versioncmp,akgl_game_save_actors,akgl_game_save_actorname_iterator,akgl_game_save_charactername_iterator,akgl_game_save_spritename_iterator,akgl_game_save_spritesheetname_iterator(src/game.c);akgl_get_json_properties_double,akgl_get_json_properties_float,akgl_get_json_properties_number,akgl_tilemap_load_layer_image,akgl_tilemap_load_layer_object_actor,akgl_tilemap_load_physics(src/tilemap.c);akgl_path_relative_from,akgl_path_relative_root(src/util.c);gamepad_handle_added,gamepad_handle_button_down,gamepad_handle_button_up,gamepad_handle_removed(src/controller.c).Each should be either declared in its header or made
static. Note thattilemap.halready has a "part of the internal API, exposed here for unit testing" block — the tilemap entries belong there. -
akgl_game_init_screenis declared but never defined (include/akgl/game.h:100). Same failure mode as Defects → Known and still open #10, which covers the fourakgl_controller_handle_*declarations; fold this one into that item. -
Static helpers use three different naming styles.
actor_visible(src/actor.c:185) is bare;akgl_character_load_json_innerandakgl_character_load_json_state_int_from_strings(src/character.c:101,132) andakgl_sprite_load_json_spritesheet(src/sprite.c:51) carry the full public prefix;gamepad_handle_*(src/controller.c:121+) uses a third subsystem word that appears nowhere else. Adopt one rule — the clearest is thatstatichelpers drop theakgl_prefix, since it exists to avoid external collisions. -
Parameter names disagree between declaration and definition. Doxygen documents the header spelling, so the generated docs describe names the implementation does not use:
Header Implementation character.h:41basecharcharacter.c:21objcharacter.h:69propscharacter.c:75registryheap.h:121ptrheap.c:143basecharregistry.h:97valueregistry.c:163srcjson_helpers.h:134ejson_helpers.c:149errtilemap.h:134,143desttilemap.c:646,767mapThe
tilemap.hpair is the most misleading: the parameter is the map being read and drawn, but it is nameddestand documented as "Output destination populated by the function". -
Object-pool size macros are defined twice, and the override hook is dead.
heap.h:15-29wrapsAKGL_MAX_HEAP_ACTOR,_SPRITE,_SPRITESHEET,_CHARACTER, and_STRINGin#ifndefguards so a consumer can override them, butactor.h:65,sprite.h:19-20, andcharacter.h:14define the same four unconditionally and are included fromheap.h:9-11. Whichever header lands first wins and the#ifndefnever fires, so the override mechanism cannot work. Define each pool size once —heap.his the natural home. -
Headers rely on their includers for types.
iterator.husesuint32_twithout<stdint.h>;json_helpers.husesjson_twithout<jansson.h>;util.husesSDL_FRectandboolwithout any SDL include;text.husesSDL_Colorandakerr_ErrorContextwithout including SDL orakerror.h;controller.husesakgl_Actorwithout includingactor.h. Each compiles only because of.c-file include ordering. Headers should be self-contained. -
Include spelling is split between quoted and angled forms for the same directory.
actor.h:10-11,character.h:10-11,controller.h:11,game.h:11-14,json_helpers.h:10, andstaticstring.h:9use#include "sibling.h";physics.h:11-13,registry.h:9-10,renderer.h:13,tilemap.h:10-12, andutil.h:10use#include <akgl/sibling.h>. The angled form is correct for an installed library. -
Empty parameter lists.
akgl_heap_init(),akgl_heap_init_actor(),akgl_registry_init*(),akgl_game_init(),akgl_game_init_screen(), andakgl_game_updateFPS()declare()rather than(void), whileakgl_controller_list_keyboards(void),akgl_controller_open_gamepads(void),akgl_game_lowfps(void), andakgl_game_state_lock(void)use(void). Before C23 the two are not equivalent —()suppresses argument checking.akgl_heap_init_actoris even declared()inheap.h:57and defined(void)inheap.c:48. -
AKERR_NOIGNOREis applied inconsistently at definition sites. Headers use it uniformly (exceptakgl_sprite_sheet_coords_for_frame,sprite.h:86, which omits it). Definitions are split even within one file:registry.c:55,120,163,172repeat it,registry.c:27,44,63,71,79,96,104,112do not. Since the attribute is already on the declaration, drop it from all definitions.
3. Error-handling pattern
-
*_RETURNmacros are used insideATTEMPTblocks, which skipsCLEANUP. The established pattern isFAIL_*_BREAK/CATCHinsideATTEMPTand*_RETURNoutside it. Violations:src/tilemap.c:52—SUCCEED_RETURNon the success path ofakgl_get_json_tilemap_propertyreturns directly from insideATTEMPT, bypassing theCLEANUPat line 54 that releasestmpstrandtypestr. Every successful property lookup leaks two heap strings, and the string pool is only 256 entries.src/tilemap.c:620—FAIL_RETURNinsideATTEMPT.src/controller.c:286-289,306—FAIL_ZERO_RETURN/FAIL_NONZERO_RETURNinsideATTEMPT;src/controller.c:383—SUCCEED_RETURNinsideATTEMPT, which also leavesakgl_controller_defaultwith no return statement on the path that falls out ofFINISH.src/game.c:457,462—FAIL_NONZERO_RETURNinsideATTEMPT, skipping thefcloseinCLEANUPat line 482.
-
NULL-check discipline varies by function. In
src/json_helpers.conlyakgl_get_json_string_value(line 71) andakgl_get_json_array_index_string(line 128) validatekey/dest; the other eight accessors validate only the container and then dereferencedestunconditionally. Insrc/physics.cthe arcade backends checkactorbutakgl_physics_null_gravity,_null_collide, and_null_move(lines 15-34) check onlyself. Insrc/renderer.c:65,74,akgl_render_2d_frame_startand_frame_enddereferenceself->sdl_rendererwith no check onself, whileakgl_render_2d_draw_texture(line 82) checksselffirst. -
Error-context variable naming is split between
errctxande, sometimes within one file.src/util.cusesein the path helpers (lines 32-116) anderrctxin the geometry helpers (lines 118-259);src/heap.cuseserrctxeverywhere exceptakgl_heap_init_actor(line 50).src/actor.c,character.c,json_helpers.c,registry.c,sprite.c, andtilemap.cfavorerrctx;game.c,physics.c,renderer.c, andcontroller.cfavore. Pick one.
4. Types and macros
-
float/doubleare used raw wheretypes.hdefines aliases.types.hexportsfloat32_tandfloat64_t, and the actor and character structs usefloat32_tthroughout — butakgl_get_json_number_valuetakesfloat *(json_helpers.h:55),akgl_get_json_double_valuetakesdouble *(line 66),akgl_PhysicsBackend's six drag/gravity fields aredouble(physics.h:22-27), andakgl_Tilemap's perspective fields arefloat(tilemap.h:105-108). Either use the aliases consistently or delete them. -
AKGL_COLLIDE_RECTANGLES(include/akgl/util.h:27) has unbalanced parentheses — three opens, two closes — so any use is a syntax error. It has no callers and duplicatesakgl_collide_rectangles. Delete it. -
Bitmask macros are unparenthesized.
AKGL_BITMASK_HAS(x, y)expands to(x & y) == ywith no outer parens, so!AKGL_BITMASK_HAS(a, b)parses as!(a & b) == b. No in-tree caller negates it today (AKGL_BITMASK_HASNOTexists for that), so this is latent rather than live.AKGL_BITMASK_CLEAR(x)(game.h:86) also carries a trailing semicolon inside the macro body, so normal use produces an empty statement. Parenthesize all five and drop the semicolon. -
The state and iterator bit macros mix forms.
AKGL_ITERATOR_OP_UPDATE(iterator.h:15) is written1while its 31 siblings are1 << n, and none of the1 << nvalues initerator.horactor.h:15-49are parenthesized. The hand-maintained trailing bit-pattern comments inactor.h:34-49are also wrong for the high word — they restart at0000 0000 0000 0001for bit 16 rather than showing the full 32-bit value. -
akgl_Frame(game.h:27-31) is defined and never used anywhere insrc/,include/,tests/, orutil/.
5. AKGL_ACTOR_STATE_STRING_NAMES disagrees with actor.h
-
The array bound differs between declaration and definition.
include/akgl/actor.h:57declares[AKGL_ACTOR_MAX_STATES+1](33);src/actor_state_string_names.c:6defines[32]. Any consumer that trusts the declared bound and reads index 32 reads past the object. -
Two entries name the wrong bit.
actor.hassigns bit 11 toAKGL_ACTOR_STATE_MOVING_INand bit 12 toAKGL_ACTOR_STATE_MOVING_OUT, butactor_state_string_names.c:18-19puts"AKGL_ACTOR_STATE_UNDEFINED_11"and"AKGL_ACTOR_STATE_UNDEFINED_12"at those indices — names for macros that do not exist. Sinceakgl_registry_init_actor_state_strings(src/registry.c:79-94) buildsAKGL_REGISTRY_ACTOR_STATE_STRINGSfrom this array andakgl_character_load_json_state_int_from_strings(src/character.c:101) resolves character-JSON state names through it, a character JSON can never bind a sprite toMOVING_INorMOVING_OUT. -
The generation comment is stale.
actor.h:53-55says the file "is built by a utility script and not kept in git, see the Makefile for lib_src/actor_state_string_names.c". There is no Makefile (the project is CMake), nolib_src/, no such script underscripts/orutil/, and the file is tracked in git atsrc/actor_state_string_names.c. Either restore the generator — which would fix items 24 and 25 by construction — or delete the comment and maintain the file by hand.
6. Doxygen drift
-
Three struct doc comments in
tilemap.hare rotated by one.akgl_TilemapObject(line 31) is documented as "Stores tileset metadata, texture, and frame offsets" (that isakgl_Tileset);akgl_TilemapLayer(line 46) as "Represents an object embedded in a tilemap layer" (that isakgl_TilemapObject);akgl_Tileset(line 61) as "Stores tile, image, or object data for one map layer" (that isakgl_TilemapLayer). -
pointis documented as "Represents a two-dimensional point" (util.h:12) but hasx,y, andz. -
Doc comments live on the definition for public functions. The convention is header-side documentation, but
akgl_path_relative_root(util.c:23),akgl_path_relative_from(util.c:97), the fourgamepad_handle_*(controller.c:114+), theakgl_game_save_*iterators andakgl_game_load_*helpers (game.c:173+), and the tilemap helpers (tilemap.c:90+) are documented only in the.c. This is the same set as item 7 — resolving that resolves this.
7. Formatting
-
A minority of files use a 2-column offset instead of the canonical 4-column one. The mix of tabs and spaces across most of
src/is not disorder: it is exactly what Emacscc-modeemits for thestroustrupstyle withindent-tabs-mode tandtab-width 8— depth 1 is four spaces, depth 2 is one tab, depth 3 is a tab plus four spaces, and so on. Twelve of the seventeen.cfiles follow that ladder cleanly.The genuine outliers indent at 2 columns:
src/json_helpers.c(82 lines, exceptakgl_get_json_with_defaultat line 149 which is 4),src/util.c(37 lines, fromakgl_rectangle_pointsat line 118 onward),src/assets.c(9),src/staticstring.c(7),src/actor.c(8, inakgl_actor_add_childat line 272), plusinclude/akgl/util.h(7) andinclude/akgl/staticstring.h(2).Resolved.
AGENTS.mdnow specifies the canonical style and the exactcc-modesettings;.dir-locals.elapplies them in Emacs; andscripts/reindent.shapplies them in batch. The whole tree (32 files acrosssrc/,include/,tests/, andutil/) has been reindented and is a fixed point ofscripts/reindent.sh --check. Verified whitespace-only: apart from three trailing blank lines removed at EOF insrc/heap.c,src/registry.c, andtests/tilemap.c,git diff -wover the reindent is empty, and the test results are unchanged (13/14,characterstill the intentional failure).scripts/hooks/pre-commitkeeps it that way — enable withgit config core.hooksPath scripts/hooks. -
Leftover debug code ships in the library.
src/controller.c:91-97logs four lines wheneverevent->type == 768 && event->key.which == 11 && event->key.key == 13— hardcoded decimal values for a specific keyboard ID on a specific developer's machine, inside the per-event inner loop. -
Large commented-out blocks.
src/sprite.c:115-120,157,185-198,210;src/character.c:192-199,215;src/assets.c:18-27,50;src/tilemap.c:583,596-598,640. All are the same abandonedSDL_GetBasePath()path-prefixing approach, superseded byakgl_path_relative. Delete them. -
Unused locals.
screenwidth/screenheight(game.c:53-54),curTime(game.c:499),curTimeandj(renderer.c:114,116—jis also shadowed by the inner loop at line 128),target(character.c:59),result(util.c:37,73),opflags(heap.c:146, declared and cleared but never read). -
akgl_game_update's default flags OR the same bit twice.src/game.c:496reads(AKGL_ITERATOR_OP_LAYERMASK | AKGL_ITERATOR_OP_LAYERMASK). Given the loop that follows walks layers and callsupdatefunc, the intent was almost certainlyAKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_LAYERMASK. -
akgl_draw_backgroundis the only public function outside the error protocol.include/akgl/draw.h:14returnsvoidand reports nothing; every other public entry point returnsakerr_ErrorContext *. It also callsSDL_SetRenderDrawColorandSDL_RenderFillRectwithout checkingrendererorrenderer->sdl_renderer. -
akgl_registry_init_actoris the only registry initializer that destroys an existing registry before recreating it (src/registry.c:47-49). The other seven leak the oldSDL_PropertiesIDon a second call. Either all of them should do it or none should. -
Redundant casts obscure the code.
(json_t *)jsonwherejsonis alreadyjson_t *,(akgl_Tilemap *)dest,(akgl_Actor *)actorobj,(char *)&obj->nameon an array that already decays — roughly 180 pointer casts acrosssrc/, a large majority of them no-ops, densest insrc/tilemap.c:150-624andsrc/character.c:144-172. They suppress exactly the conversion warnings that would catch a real mismatch. -
struct-qualified parameters in two definitions.akgl_physics_null_move(src/physics.c:29) andakgl_physics_arcade_move(src/physics.c:80) are defined withstruct akgl_PhysicsBackend *selfwhile the header and the other eight physics functions use the typedef. -
text.c:19validates the wrong argument.FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null filepath")checksnamea second time;filepathis never checked and is passed straight toTTF_OpenFont. Copy-paste of line 18.Resolved alongside the text measurement work;
tests/text.casserts a NULL filepath reportsAKERR_NULLPOINTER. -
akgl_path_relativeandakgl_path_relative_fromdisagree on the output parameter.akgl_path_relativeandakgl_path_relative_roottakeakgl_String *dst;akgl_path_relative_fromtakesakgl_String **dst(src/util.c:105). The**form matches the rest of the library (akgl_get_json_string_value,akgl_get_property,akgl_heap_next_string), which allocate when*destis NULL. Related: Defects → Known and still open #4, which covers the fact thatakgl_path_relative_fromnever writes*dstat all. -
dstvsdestfor output parameters.akgl_string_copyand theakgl_path_relative*family usedst; everything else usesdest.
Coverage status
Generated with:
cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build-coverage --parallel
ctest --test-dir build-coverage --output-on-failure
Reports land in build-coverage/coverage/ (index.html, coverage.xml).
Line coverage 72.2%, function coverage 78.6% (1534/2125 lines), up from a
39.6% / 44.3% baseline. character is the one intentionally failing suite;
everything else passes.
| File | Lines | Functions |
|---|---|---|
src/actor.c |
205/258 (80%) | 16/18 |
src/assets.c |
0/21 (0%) | 0/1 |
src/character.c |
104/118 (88%) | 6/7 |
src/controller.c |
220/243 (90%) | 9/9 |
src/draw.c |
0/13 (0%) | 0/1 |
src/game.c |
124/230 (54%) | 10/15 |
src/heap.c |
116/116 (100%) | 12/12 |
src/json_helpers.c |
111/111 (100%) | 11/11 |
src/physics.c |
140/140 (100%) | 10/10 |
src/registry.c |
76/102 (74%) | 11/12 |
src/renderer.c |
7/70 (10%) | 1/7 |
src/sprite.c |
93/101 (92%) | 5/5 |
src/staticstring.c |
16/17 (94%) | 2/2 |
src/text.c |
0/28 (0%) | 0/2 |
src/tilemap.c |
201/426 (47%) | 10/20 |
src/util.c |
121/131 (92%) | 7/8 |
Branch coverage reads 18.6% and should not be used as a target. The akerror
control-flow macros (ATTEMPT/CATCH/PROCESS/FINISH, FAIL_*_RETURN)
expand into large branch trees per call site, most of them unreachable in normal
operation — src/game.c reports over 1700 branches across 230 lines. Track line
and function coverage; treat branch coverage as a relative signal within a file.
Suites
Every suite is registered through the AKGL_TEST_SUITES list in
CMakeLists.txt, which drives target creation, CTest registration, the
WORKING_DIRECTORY/TIMEOUT properties, the link line, and the
FIXTURES_REQUIRED akgl_coverage list together. Adding tests/<name>.c and the
name to that list is all a new suite needs; it can no longer be accidentally
left out of the coverage fixture.
Shared assertion helpers are in tests/testutil.h: TEST_ASSERT,
TEST_ASSERT_FEQ, TEST_EXPECT_STATUS, TEST_EXPECT_OK, TEST_EXPECT_ANY_ERROR,
and TEST_ASSERT_FLAG. All except the last expand to a break on failure, so
they belong directly inside an ATTEMPT block, not inside a loop nested in one.
Done:
tests/physics.c— both backends, the factory, and the full simulation loop including thrust clamping, drag, layer masking, parent/child positioning, and logic-interrupt handling. 100%.tests/heap.c— pool exhaustion for all five pools, refcount clamping, recursive child release, registry cleanup on release. 100%.tests/json_helpers.c— every typed accessor, both string accessors' allocate-vs-reuse paths, array bounds, andakgl_get_json_with_default. 100%.tests/controller.c— control map push and capacity, the default binding set, keyboard and gamepad dispatch including cross-device rejection, the dpad handlers, and device add/remove against the dummy drivers. 90%.tests/game.c— version gating, the save/load roundtrip, foreign-save rejection, truncated-table detection, the state lock, and FPS accounting. 54%.tests/actor.c— extended with the eight control-map handlers, automatic facing, movement logic, the animation frame state machine,akgl_actor_update, and character/sprite binding lookups. 80%.
Remaining work
Needs the offscreen renderer harness
src/renderer.c (63 lines), src/text.c (28), src/draw.c (13),
src/assets.c (21), akgl_actor_render/actor_visible in src/actor.c (53),
and the drawing half of src/tilemap.c all need a live renderer global.
Build tests/harness.c / tests/harness.h with akgl_test_init_headless() and
akgl_test_shutdown_headless(): set the dummy video and audio drivers,
SDL_Init(), akgl_heap_init(), akgl_registry_init(), create a software
SDL_CreateWindowAndRenderer, and point the global renderer at it. Four
existing tests hand-roll this today (tests/sprite.c:194, tests/character.c:200,
tests/tilemap.c:421, tests/charviewer.c:42); collapse them onto the shared
harness in the same change. The new suites set the driver hints inline and need
no window, so they are unaffected.
Then:
tests/renderer.c—akgl_render_init2dpopulating all six function pointers and the camera; frame start/end against a NULLsdl_renderer; the rotateddraw_texturepath includingangle != 0with a NULL center; thedraw_mesh"not implemented" stub; anddraw_worldlayer ordering. Notedefflagsatsrc/renderer.c:113is uninitialized until theifbody runs.tests/text.c,tests/draw.c,tests/assets.c— font loading intoAKGL_REGISTRY_FONT, text rendering with wrap on and off, background drawing at zero/negative/oversized dimensions, and BGM loading intoAKGL_REGISTRY_MUSICunder the dummy audio driver.tests/tilemap.cextensions —akgl_tilemap_draw,_draw_tileset, andakgl_tilemap_load_layer_image.
Does not need a renderer
src/tilemap.c—akgl_tilemap_scale_actoris pure math over three branches (src/tilemap.c:824-830);akgl_get_json_properties_number,_float, and_doubleneed only a JSON snippet;akgl_tilemap_load_physicsneeds a fixture with the physics property block present, absent, and malformed. Together roughly 100 of the 225 uncovered lines.src/game.c—akgl_game_init,akgl_game_update,akgl_game_lowfps, andakgl_game_updateFPS's frame loop need a window; revisit after the harness lands.src/registry.c—akgl_registry_load_propertiesneeds a fixture with apropertiesobject, plus the missing-file, missing-key, and wrong-value-type cases. Assert the loop atsrc/registry.c:148-158does not leak the string heap.
Defects
Fixed while building the suites
Each was found by a test written to assert correct behavior.
akgl_physics_simulatedereferencedselfbefore its NULL check.src/physics.c:132readself->gravity_timeat declaration time, three lines aboveFAIL_ZERO_RETURN(e, self, ...). A NULL backend segfaulted.akgl_game_savenever flushed or closed its stream.CLEANUPandPROCESSwere transposed, which put thefcloseinside thePROCESSswitch, where it only ran if an error context existed and reported success. An ordinary save produced an empty file.akgl_game_save_actorswrote name-table terminators from a single char.aksl_fwrite((void *)&nullval, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp)emitted 127 bytes of adjacent stack memory into the save file and produced a sentinel the loader could not recognize.akgl_game_load_objectnamemapswallowed read failures.CATCHused directly insidewhile (1)breaks the loop, not the function, so a truncated or corrupt name table loaded as a successful game.akgl_Actor_cmhf_up_onand_down_ondereferencedbasecharunguarded, unlike their left and right counterparts.akgl_actor_logic_movementcheckedactortwice instead of checkingactor->basecharbefore dereferencing it.- The gamepad handlers checked
appstatethree times each, so a NULL event or a missing player actor was never caught andplayer->statewas dereferenced regardless.
Known and still open
-
akgl_render_and_comparecompares a texture against itself.src/util.c:228andsrc/util.c:245both drawt1;t2is never rendered, so the function always passes and the image assertions intests/sprite.cassert nothing. -
akgl_tilemap_releasedouble-frees tileset textures.src/tilemap.c:849destroysdest->tilesets[i].textureinside the layers loop; it should bedest->layers[i].texture. It also never NULLs the pointers, so a second release is a use-after-free. -
akgl_registry_initnever initializes the properties registry.akgl_registry_init_properties()is not called fromakgl_registry_init()(src/registry.c:27), soAKGL_REGISTRY_PROPERTIESstays 0 unless the caller initializes it separately.akgl_set_propertyis then a silent no-op andakgl_get_propertyalways returns the caller's default, which meansakgl_physics_init_arcadeandakgl_render_init2dsilently ignore configuration.akgl_game_initdoes call it; a caller that does not useakgl_game_initdoes not get it. -
akgl_path_relative_fromis a stub that leaks.src/util.c:105claims a heap string, never writes*dst, and never releases it. 256 calls exhaust the string pool. -
akgl_compare_sdl_surfacesmemcmps without checking geometry.src/util.c:208comparess1->pitch * s1->hbytes ofs2without verifying the surfaces share dimensions, pitch, or format. -
akgl_string_initializeoverflows by four bytes wheninitis NULL.src/staticstring.c:17doesmemset(&obj->data, 0x00, sizeof(akgl_String)), butdatastarts four bytes into the struct (afterrefcount), so the memset runs four bytes past the end. -
Savegame name lengths disagree between writer and reader.
akgl_game_save_actorswrites spritesheet names atAKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH(512) and character names atAKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH(128), butakgl_game_loadreads every table atAKGL_ACTOR_MAX_NAME_LENGTH(128). A save with any registered spritesheet cannot be read back. The current roundtrip test passes only because empty registries write nothing but zeroed sentinels. -
Heap acquire functions are asymmetric.
akgl_heap_next_stringincrementsrefcount;next_actor,next_sprite,next_spritesheet, andnext_characterdo not.tests/heap.cpins the current behavior and says so; decide whether to make them symmetric or document the split. -
tests/util.cdefinestest_akgl_collide_point_rectangle_logicbutmain()never calls it. -
controller.hdeclares functions that do not exist. It declaresakgl_controller_handle_button_down,_button_up,_added, and_removed, butsrc/controller.cdefines them asgamepad_handle_*. Anything compiled against the header alone fails to link. -
akgl_controller_pushmapandakgl_controller_defaultaccept negative map ids. Both checkcontrolmapid >= AKGL_MAX_CONTROL_MAPSbut notcontrolmapid < 0, so a negative id indexes beforeGAME_ControlMaps. -
A failed controller-DB fetch silently destroys the tracked fallback.
include/akgl/SDL_GameControllerDB.his committed deliberately so the library still builds if upstream disappears. Butmkcontrollermappings.shhas noset -eand never checkscurl's exit status: on a failed fetch it writesmappings.txtempty, then overwrites the good tracked header withAKGL_SDL_GAMECONTROLLER_DB_LEN 0and an empty initializer — and exits 0. Verified by pointing the script at an unresolvable host.Two compounding problems:
add_custom_commandatCMakeLists.txt:89-93declaresOUTPUT include/akgl/SDL_GameControllerDB.has a relative path, which CMake resolves against the binary directory, while the script writes to the source directory. The declared output never appears, so the command is permanently out of date and re-runs on every build — making every build depend on network access and leaving the file dirty in the working tree each time.const char *SDL_GAMECONTROLLER_DB[] = {\n};is an empty initializer, which is a constraint violation in ISO C and compiles only as a GCC extension.
Fix: have the script fetch to a temporary file, check
curl's status and a plausible minimum line count, and leave the existing header untouched on failure (exiting non-zero). Separately, make regeneration explicit — a dedicatedcontrollerdbtarget, or anOUTPUTthat matches where the script actually writes — so an ordinary build neither needs the network nor dirties the tree. -
A stale build tree in the source directory breaks the coverage run.
CMakeLists.txt:208andCMakeLists.txt:223pass--root "${CMAKE_CURRENT_SOURCE_DIR}"to gcovr, and gcovr searches for.gcda/.gcnofiles under the root. The--object-directoryargument on the line below each does not narrow that search: pergcovr --helpit only identifies "the path between gcda files and the directory where the compiler was originally run". So every instrumented build tree left inside the source directory is folded into the report alongside the one actually being measured.With a
build-coverage/from an earlier session still present, a freshly configured-DAKGL_COVERAGE=ONtree fails incoverage_reset, before any test runs:AssertionError: Got function akgl_game_lowfps on multiple lines: 45, 46.45 and 46 are that function's line numbers before and after an unrelated
#includewas added tosrc/game.c: gcovr found 18 stale.gcnofiles describing the old layout, merged them with the current ones, and could not reconcile the two. Movingbuild-coverage/aside makes the same tree pass 18/18. Verified with gcovr 7.0.The
build*/entry in.gitignorehides these trees fromgit status, which makes the state easier to get into and no easier to notice.Fix: pass the build tree to gcovr as an explicit search path instead of letting it default to
--root, so only the tree under measurement is considered. Touches the twoadd_testblocks atCMakeLists.txt:204-211andCMakeLists.txt:220-229; nothing outside theAKGL_COVERAGEbranch.
Build notes
The vendored SDL satellite libraries are built into per-project subdirectories
that are not on the loader's default search path, and LD_LIBRARY_PATH is
searched ahead of RPATH — so a developer who has previously run rebuild.sh had
their installed libakgl.so shadow the one under test, and a developer who had
not saw every test abort before main() with "cannot open shared object file".
CMakeLists.txt now sets BUILD_RPATH on the library, the utility, and every
test target, and prepends the build tree to LD_LIBRARY_PATH for the CTest run.
API gaps blocking akbasic
akbasic (the C port of the BASIC interpreter, source.starfort.tech/andrew/akbasic) is
being built to link into libakgl as a scripting engine for game authors. Its interpreter
core is complete and passes its whole acceptance corpus against a stdio text sink; the
libakgl-backed sink and the graphics, sound and console verbs of Commodore BASIC 7.0 are
blocked on the four gaps below.
These are filed here rather than worked around in akbasic because growing libakgl to serve
a consumer is the wanted outcome. Each entry says what the BASIC verb needs, what the
akgl_* entry point should look like, and what would cover it.
-
No way to measure rendered text.
include/akgl/text.hexposesakgl_text_loadfont()andakgl_text_rendertextat(), and nothing that reports how large a string will be in a given font. A terminal-style text surface cannot be built on that: a cursor needs the advance width of one cell, and wrapping needs to know where a string crosses the right margin. The reference interpreter got this from SDL2_ttf'sfont.SizeUTF8("A")and derived its whole character grid from it.Wants
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h);overTTF_GetStringSize, and probably a companionakgl_text_measure_wrapped(font, text, wraplength, w, h)matching thewraplengthargumentakgl_text_rendertextatalready takes. Tests: a known string in a known font at a known size, the empty string, and a wrapped string wide enough to force two lines.This is the only one of the four that blocks work already designed and waiting.
akbasiccannot render any output throughlibakgluntil it lands.Resolved.
akgl_text_measure(font, text, w, h)andakgl_text_measure_wrapped(font, text, wraplength, w, h)are ininclude/akgl/text.h, overTTF_GetStringSizeandTTF_GetStringSizeWrapped. Neither needs a renderer. A negativewraplengthis refused withAKERR_OUTOFBOUNDSrather than passed through, because SDL_ttf reads it as a very large unsigned width and silently stops wrapping.tests/text.ccovers both againsttests/assets/akgl_test_mono.ttf, a 10 KB monospaced ASCII subset added for the purpose — being monospaced, it lets the suite assertwidth("AAAA") == 4 * width("A")instead of hardcoding glyph metrics that FreeType is free to round differently. Same change fixed item 39 below:akgl_text_loadfontcheckednametwice and never checkedfilepath. -
No immediate-mode drawing.
include/akgl/draw.hdeclares exactly one function,akgl_draw_background(int w, int h), andsrc/draw.cis at 0% coverage. BASIC 7.0's graphics verbs are all immediate-mode plotting against the current screen:DRAW(line and point),BOX,CIRCLE,PAINT(flood fill),LOCATE(set the pixel cursor),COLOR, andSSHAPE/GSHAPE(save and restore a rectangle of pixels).Wants an
akgl_draw_*family taking the renderer the host already initialized --akgl_draw_line,_rect,_filled_rect,_circle,_point,_flood_fill,_copy_region-- in the shape of the existingakgl_render_2d_draw_texture. SDL3'sSDL_RenderLine/SDL_RenderRect/SDL_RenderFillRectcover most of it; the circle and the flood fill do not exist in SDL3 and need writing. Tests belong with the offscreen renderer harness described under "Remaining work": render a known shape, read the target back, and compare against a reference surface with the existingakgl_compare_sdl_surfaces.Resolved.
include/akgl/draw.hnow declaresakgl_draw_point,_line,_rect,_filled_rect,_circle,_flood_fill,_copy_regionand_paste_region, all taking theakgl_RenderBackend *the host initialized, in the shape ofakgl_render_2d_draw_texture. Decisions worth knowing:- Color is an argument, not state. There is no current-color global to
get out of step with the caller's own. Each call saves and restores the
renderer's draw color, so drawing a line does not change what the host's
next
SDL_RenderClearpaints.tests/draw.casserts that. - The circle is a midpoint circle, integer arithmetic with eight-way
symmetry, plotted eight points per step through
SDL_RenderPoints. - The flood fill reads the target back, fills on the CPU, and blits only
the bounding box of what changed. It keeps a fixed
AKGL_DRAW_MAX_FLOOD_SPANS(4096) stack of horizontal runs at file scope rather than recursing per pixel; running out reportsAKERR_OUTOFBOUNDSand leaves the region partially filled, which is stated in the header. It is therefore not reentrant — neither is anything else that draws to a singleSDL_Renderer. _copy_regionallocates when*destisNULLand otherwise copies into the caller's surface, matchingakgl_get_json_string_valueand friends. A region that would be clipped by the target edge is refused rather than silently returning a smaller surface.
tests/draw.cdraws into a 64x64 software renderer under the dummy video driver and reads pixels back, so it did not need the offscreen harness. That harness is still wanted forsrc/renderer.c,src/text.candsrc/assets.c.akgl_draw_backgroundis untouched and still outside the error protocol (item 35). - Color is an argument, not state. There is no current-color global to
get out of step with the caller's own. Each call saves and restores the
renderer's draw color, so drawing a line does not change what the host's
next
-
No audio API at all.
SDL3_mixeris a vendored dependency andregistry.hdeclaresAKGL_REGISTRY_MUSIC, but there is nosrc/audio.c, noinclude/akgl/audio.h, and noakgl_*symbol that opens a mixer, loads a chunk, or plays a note. BASIC 7.0's sound verbs areSOUND(a tone on a voice, with a duration),PLAY(a string of notes in a Commodore-specific notation),ENVELOPE(ADSR per voice),FILTER,VOLandTEMPO.PLAYandENVELOPEwant a synthesised voice rather than a sample, which SDL3_mixer does not provide directly -- the honest first step is a small tone generator feedingSDL_AudioStream, withakgl_audio_init,akgl_audio_tone(voice, hz, ms),akgl_audio_envelope(voice, a, d, s, r)andakgl_audio_volume(level). This is the largest of the four and the one most worth designing before writing. Tests can run under the dummy audio driver and assert state transitions rather than sound.Resolved.
include/akgl/audio.handsrc/audio.cadd a three-voice tone generator overSDL_AudioStream:akgl_audio_init,_shutdown,_tone,_stop,_waveform,_envelope,_volume,_voice_activeand_mix. It is deliberately separate from the SDL3_mixer side of the library, which plays audio assets; nothing inaudio.creads a file. Decisions worth knowing:- The voice table works with no device open.
akgl_audio_initconnects it to one; without that a host can still pull samples itself throughakgl_audio_mix. That is not only for embedding — it is what makes the suite deterministic. A device pulls samples on SDL's audio thread whenever it likes, so a test that opened one and then asserted on voice state would be racing the callback.tests/audio.cmixes by hand and opens a device only in its last test. - Phase is derived from the frame counter, not accumulated. A float
increment of
hz / 44100is not exact, and adding it 44100 times a second walks a held note off pitch. - A voice that was never configured is audible. A zeroed voice has a sustain of 0.0, which is silence with no error to explain it, so the table defaults to a square wave held at full level.
- Three voices summing past full scale are clamped, not scaled, so one voice plays at the level it was asked for rather than a third of it.
- Everything that touches the table takes the stream lock when a device is open, since the callback reads it on another thread.
Still missing for a complete BASIC sound vocabulary:
FILTER(SDL3 has no filter primitive; this would need writing),TEMPOand thePLAYnote-string parser, both of which belong in the interpreter rather than here. - The voice table works with no device open.
-
No non-blocking keystroke read.
include/akgl/controller.his built around SDL event handlers the host pumps (akgl_controller_handle_eventand friends), which suits a game loop and does not suitGETandGETKEY-- those ask "is there a keystroke waiting, yes or no" and must not require the interpreter to own the event loop. Goal 3 ofakbasicforbids it owning one.Wants
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available);reading a small ring buffer thatakgl_controller_handle_eventalready fills, so the host keeps pumping events and the interpreter drains characters at its own pace. Tests: push syntheticSDL_EVENT_KEY_DOWNevents through the existing handler and drain them, plus the empty-buffer and overflow cases.Resolved.
akgl_controller_poll_key(int *keycode, bool *available)andakgl_controller_flush_keys(void)are ininclude/akgl/controller.h, over a fixedAKGL_CONTROLLER_KEY_BUFFER(32) ring insrc/controller.c.akgl_controller_handle_eventrecords everySDL_EVENT_KEY_DOWNbefore it scans the control maps, so a key bound to an actor still reaches a polling caller — the scan returns as soon as a binding claims the event, and doing it afterwards would have lost exactly the keys a game also acts on. An empty buffer is success withavailablefalse, not an error. A full buffer drops the newest key rather than overwriting the oldest, matching the Commodore keyboard buffer and keeping what was typed first.tests/controller.ccovers drain order, the release-is-not-a-keystroke case, a key shared with a control map, flush, both NULL arguments, and overflow plus reuse afterwards.
Carried over
-
Make character-to-sprite state bindings release their references symmetrically.
akgl_character_sprite_add()increments the sprite refcount for every state-map binding, but no corresponding removal API exists. Replacing a state binding also leaves the previous sprite's refcount incremented. Whenakgl_heap_release_character()drops the character refcount to zero, it clears the character registry entry and zeroes the structure without enumeratingstate_sprites, decrementing the bound sprites, or destroying the SDL property map. Implement binding removal/replacement so each removed binding releases exactly one sprite reference. On final character release, enumerate every remaining binding (the existingakgl_character_state_sprites_iterate()release path may be reusable), release each reference, destroystate_sprites, and then clear the character. Add tests for removal, replacement, duplicate sprite bindings across multiple states, and final character release.akgl_character_state_sprites_iterateis still uncovered and is the natural place to start;tests/actor.cnow coversakgl_character_sprite_getand the composite-state binding behavior it depends on.