docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
19 KiB
21. Appendix: limits, statuses and properties
Three reference tables that no single chapter owns: which function raises which status, every compile-time bound, and every configuration property the library reads.
The constant tables come in as excerpt= blocks against their headers, so they cannot
drift from the values the library was built with — and so the hand-aligned columns those
headers maintain survive.
A. Status cross-reference
What this table is. The statuses a caller can expect to see out of each function. It is
built from the FAIL_* and HANDLE* sites in src/ — the function's own body and the
static helpers only it calls — plus, where a nested libakgl call is part of the documented
contract, that status too.
It is not exhaustive, and cannot be. Every libakgl function a given one calls can
propagate anything from its own row, and so can libakstdlib: nearly everything that touches
a file also carries whatever aksl_fopen, aksl_fread, aksl_fwrite and aksl_fclose
report, including bare errno values such as ENOENT, EACCES and ENOSPC. Use the table
to write the HANDLE arms you care about, not to prove a status cannot arrive.
(H) marks a status the function handles rather than raises.
What the statuses mean is Chapter 4.
error.h
| Function | Raises |
|---|---|
akgl_error_init |
AKERR_STATUS_RANGE_OVERLAP, AKERR_STATUS_RANGE_FULL, AKERR_STATUS_NAME_FULL (all libakerror's) |
heap.h
| Function | Raises |
|---|---|
akgl_heap_init, akgl_heap_init_actor |
nothing — no failure path today |
akgl_heap_next_actor, _sprite, _spritesheet, _character, _string |
AKGL_ERR_HEAP |
akgl_heap_release_actor, _sprite, _spritesheet, _character, _string |
AKERR_NULLPOINTER |
registry.h
| Function | Raises |
|---|---|
akgl_registry_init and the eight akgl_registry_init_* |
AKERR_NULLPOINTER |
akgl_registry_load_properties |
AKERR_NULLPOINTER, AKERR_KEY, AKERR_TYPE, AKGL_ERR_HEAP |
akgl_set_property |
AKERR_NULLPOINTER |
akgl_get_property |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS, AKERR_VALUE, AKGL_ERR_HEAP |
game.h
| Function | Raises |
|---|---|
akgl_game_init |
AKERR_NULLPOINTER, AKGL_ERR_SDL |
akgl_game_update_fps, akgl_game_lowfps |
void — cannot report |
akgl_game_state_lock |
AKGL_ERR_SDL |
akgl_game_state_unlock |
nothing |
akgl_game_update |
AKGL_ERR_SDL, plus whatever updatefunc, akgl_tilemap_scale_actor, the physics backend or the renderer raises |
akgl_game_save |
AKERR_NULLPOINTER, AKERR_IO, AKERR_OUTOFBOUNDS |
akgl_game_save_actors |
AKERR_NULLPOINTER, AKERR_IO |
akgl_game_load |
AKERR_NULLPOINTER, AKERR_API, AKERR_EOF, AKERR_IO, AKERR_VALUE |
akgl_game_load_versioncmp |
AKERR_NULLPOINTER, AKERR_VALUE, AKERR_API |
actor.h
| Function | Raises |
|---|---|
akgl_actor_initialize |
AKERR_NULLPOINTER, AKERR_KEY |
akgl_actor_set_character |
AKERR_NULLPOINTER — including "no such character" |
akgl_actor_add_child |
AKERR_NULLPOINTER, AKERR_RELATIONSHIP, AKERR_OUTOFBOUNDS |
akgl_actor_update |
AKERR_NULLPOINTER, AKERR_KEY (H) |
akgl_actor_render |
AKERR_NULLPOINTER, AKERR_KEY (H), AKERR_OUTOFBOUNDS (H) |
akgl_actor_automatic_face, _logic_movement, _logic_changeframe |
AKERR_NULLPOINTER |
akgl_actor_cmhf_*_on, akgl_actor_cmhf_*_off |
AKERR_NULLPOINTER |
akgl_registry_iterate_actor |
AKERR_NULLPOINTER, AKERR_KEY — void; unhandled means the process exits |
character.h
| Function | Raises |
|---|---|
akgl_character_initialize |
AKERR_NULLPOINTER, AKERR_KEY |
akgl_character_sprite_add |
AKERR_NULLPOINTER, AKERR_KEY |
akgl_character_sprite_get |
AKERR_NULLPOINTER, AKERR_KEY |
akgl_character_load_json |
AKERR_NULLPOINTER, AKERR_KEY for a state name that is not in AKGL_REGISTRY_ACTOR_STATE_STRINGS, and AKERR_TYPE from the JSON accessors |
akgl_character_state_sprites_iterate |
AKERR_NULLPOINTER — void; unhandled means the process exits |
sprite.h
| Function | Raises |
|---|---|
akgl_sprite_initialize |
AKERR_NULLPOINTER, AKERR_KEY |
akgl_sprite_load_json |
AKERR_NULLPOINTER, AKERR_VALUE, AKERR_OUTOFBOUNDS |
akgl_spritesheet_initialize |
AKERR_NULLPOINTER, AKERR_KEY, AKGL_ERR_SDL |
akgl_spritesheet_coords_for_frame |
AKERR_NULLPOINTER |
json_helpers.h
| Function | Raises |
|---|---|
akgl_get_json_object_value, _boolean_, _integer_, _number_, _double_, _string_, _array_value |
AKERR_NULLPOINTER, AKERR_KEY for absent, AKERR_TYPE for wrong type |
akgl_get_json_array_index_object, _integer, _string |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS for a short array, AKERR_TYPE |
akgl_get_json_with_default |
AKERR_NULLPOINTER; handles AKERR_KEY (H), AKERR_OUTOFBOUNDS (H), AKERR_INDEX (H) |
akgl_get_json_string_value and akgl_get_json_array_index_string also raise
AKGL_ERR_HEAP when they have to claim a pooled string and the pool is empty.
physics.h
| Function | Raises |
|---|---|
akgl_physics_init_null, akgl_physics_init_arcade |
AKERR_NULLPOINTER |
akgl_physics_factory |
AKERR_NULLPOINTER, AKERR_KEY for an unknown backend name |
akgl_physics_simulate |
AKERR_NULLPOINTER; handles AKGL_ERR_LOGICINTERRUPT (H) |
akgl_physics_null_gravity, _collide, _move |
AKERR_NULLPOINTER |
akgl_physics_arcade_gravity, _move |
AKERR_NULLPOINTER |
akgl_physics_arcade_collide |
AKERR_NULLPOINTER, AKERR_API — not implemented |
renderer.h
| Function | Raises |
|---|---|
akgl_render_2d_init |
AKERR_NULLPOINTER, AKGL_ERR_SDL, plus akgl_get_property's |
akgl_render_2d_bind, _shutdown, _frame_start, _frame_end, _draw_texture, _draw_world |
AKERR_NULLPOINTER |
akgl_render_2d_draw_mesh |
AKERR_API — not implemented |
draw.h
| Function | Raises |
|---|---|
akgl_draw_point, _line, _rect, _filled_rect |
AKERR_NULLPOINTER |
akgl_draw_background |
AKERR_NULLPOINTER, AKGL_ERR_SDL |
akgl_draw_circle |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS, AKGL_ERR_SDL |
akgl_draw_flood_fill |
AKERR_NULLPOINTER, AKGL_ERR_SDL, AKERR_OUTOFBOUNDS past AKGL_DRAW_MAX_FLOOD_SPANS |
akgl_draw_copy_region, _paste_region |
AKERR_NULLPOINTER, AKGL_ERR_SDL |
tilemap.h
| Function | Raises |
|---|---|
akgl_tilemap_load |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS |
akgl_tilemap_load_layers |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS |
akgl_tilemap_load_layer_tile |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS |
akgl_tilemap_load_layer_image |
AKERR_NULLPOINTER, AKGL_ERR_SDL |
akgl_tilemap_load_layer_objects |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS |
akgl_tilemap_load_layer_object_actor |
AKERR_KEY |
akgl_tilemap_load_tilesets |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS past AKGL_TILEMAP_MAX_TILESETS |
akgl_tilemap_load_tilesets_each |
AKERR_NULLPOINTER |
akgl_tilemap_load_physics |
handles AKERR_KEY (H) — every physics property is optional |
akgl_get_json_tilemap_property |
AKERR_NULLPOINTER, AKERR_KEY, AKERR_TYPE |
akgl_tilemap_draw, _draw_tileset |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS |
akgl_tilemap_scale_actor, akgl_tilemap_release |
AKERR_NULLPOINTER |
controller.h
| Function | Raises |
|---|---|
akgl_controller_open_gamepads, _list_keyboards |
AKERR_NULLPOINTER |
akgl_controller_handle_event, _button_down, _button_up, _added, _removed |
AKERR_NULLPOINTER |
akgl_controller_pushmap |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS |
akgl_controller_default |
AKERR_OUTOFBOUNDS, AKGL_ERR_REGISTRY — the library's only site |
akgl_controller_poll_key, _poll_keystroke |
AKERR_NULLPOINTER |
text.h, audio.h, assets.h, util.h, staticstring.h
| Function | Raises |
|---|---|
akgl_text_loadfont |
AKERR_NULLPOINTER, AKGL_ERR_SDL |
akgl_text_unloadfont |
AKERR_NULLPOINTER, AKERR_KEY |
akgl_text_rendertextat |
AKERR_NULLPOINTER — including a NULL akgl_renderer, a NULL sdl_renderer, and a backend never run through akgl_render_2d_bind and so missing draw_texture. An empty string is not an error: it returns success having drawn nothing |
akgl_text_measure, _measure_wrapped |
AKERR_NULLPOINTER. An empty string is legal and measures 0 wide by one line high |
akgl_audio_tone, akgl_audio_sweep |
AKERR_OUTOFBOUNDS |
akgl_audio_voice_active, akgl_audio_mix |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS |
akgl_load_start_bgm |
AKERR_NULLPOINTER, AKGL_ERR_SDL |
akgl_path_relative |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS |
akgl_rectangle_points, akgl_collide_point_rectangle, akgl_collide_rectangles |
AKERR_NULLPOINTER |
akgl_compare_sdl_surfaces |
AKERR_NULLPOINTER, AKERR_VALUE |
akgl_render_and_compare |
AKERR_NULLPOINTER, AKERR_IO, AKGL_ERR_SDL |
akgl_string_initialize |
AKERR_NULLPOINTER |
akgl_string_copy |
AKERR_NULLPOINTER, AKERR_OUTOFBOUNDS, AKERR_VALUE |
B. Compile-time limits
Everything below is fixed when the library is compiled. Only the AKGL_MAX_HEAP_* five
are overridable, and even those have to be overridden for the whole build — see
Chapter 5, "The ceilings are a compile-time ABI constraint". The rest are
plain #defines with no #ifndef guard: changing one means editing the header and
rebuilding libakgl and everything linking it.
The object pools
#ifndef AKGL_MAX_HEAP_ACTOR
#define AKGL_MAX_HEAP_ACTOR 64
#endif
#ifndef AKGL_MAX_HEAP_SPRITE
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
#endif
#ifndef AKGL_MAX_HEAP_SPRITESHEET
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
#endif
#ifndef AKGL_MAX_HEAP_CHARACTER
#define AKGL_MAX_HEAP_CHARACTER 256
#endif
#ifndef AKGL_MAX_HEAP_STRING
#define AKGL_MAX_HEAP_STRING 256
#endif
Exceeding any of them is AKGL_ERR_HEAP.
Strings
AKGL_MAX_STRING_LENGTH is PATH_MAX, which is 4096 on Linux. That is the capacity of
every akgl_String, and it is why the string pool is over a megabyte on its own.
Actors
/** @brief Longest actor name, including the terminator. Names are truncated, not rejected. */
#define AKGL_ACTOR_MAX_NAME_LENGTH 128
/** @brief Children one actor can carry. A child moves with its parent rather than simulating. */
#define AKGL_ACTOR_MAX_CHILDREN 8
AKGL_ACTOR_MAX_STATES is 32, fixed by the width of int32_t state rather than chosen.
A ninth child is AKERR_OUTOFBOUNDS; an over-long name is silently truncated.
Sprites and spritesheets
#define AKGL_SPRITE_MAX_FRAMES 16
#define AKGL_SPRITE_MAX_NAME_LENGTH 128
#define AKGL_SPRITE_MAX_REGISTRY_SIZE 1024
#define AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH 512
akgl_sprite_load_json bounds the frames array against AKGL_SPRITE_MAX_FRAMES before
writing anything, and refuses a frame number that will not fit a uint8_t rather than
truncating it into an index naming a different tile (TODO.md item 16).
AKGL_SPRITE_MAX_REGISTRY_SIZE is dead. It is defined in sprite.h and referenced
nowhere in src/, include/, tests/ or util/. It bounds nothing. Do not size anything
against it.
Tilemaps
/** @brief Widest map, in tiles. Width times height is what is actually bounded. */
#define AKGL_TILEMAP_MAX_WIDTH 512
/** @brief Tallest map, in tiles. */
#define AKGL_TILEMAP_MAX_HEIGHT 512
/** @brief Layers per map. Also the number of draw passes akgl_render_2d_draw_world makes. */
#define AKGL_TILEMAP_MAX_LAYERS 16
/** @brief Tilesets per map. */
#define AKGL_TILEMAP_MAX_TILESETS 16
/** @brief Entries in a tileset's offset table. Indexed by *local* tile id, so a tileset with a high `firstgid` still starts at 0. */
#define AKGL_TILEMAP_MAX_TILES_PER_IMAGE 65536
/** @brief Longest tileset name. */
#define AKGL_TILEMAP_MAX_TILESET_NAME_SIZE 512
/** @brief Longest resolved tileset image path. */
#define AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE PATH_MAX
/** @brief Longest object name. Note that an object naming an actor is truncated at #AKGL_ACTOR_MAX_NAME_LENGTH (128) instead. */
#define AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE 512
/** @brief Objects in one object layer. Not enforced by the loader -- a longer group writes past the array. */
#define AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER 128
Three corrections to what those comments say, all verified against src/tilemap.c:
AKGL_TILEMAP_MAX_OBJECTS_PER_LAYERis enforced. The comment above says it is not.akgl_tilemap_load_layer_objectsboundsjat the top of the loop body and raisesAKERR_OUTOFBOUNDS;akgl_tilemap_load_tilesetsdoes the same forAKGL_TILEMAP_MAX_TILESETS. Both landed in 0.5.0 (TODO.md, "Known and still open" item 17), and the header comment was not updated with them.- Width and height are not bounded individually.
akgl_tilemap_loadcheckswidth * height >= AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT, and the comparison is>=, so the true ceiling is 262143 tiles in any shape. A 1024×256 map loads; a 512×512 one does not. AKGL_TILEMAP_MAX_TILES_PER_IMAGEis not checked anywhere. It sizestile_offsets[65536][2]in the struct and nothing validates a tileset against it. It is also whyakgl_Tilesetis large: 512 KiB of offset table per tileset, whatever the image actually holds.
Controllers
/** @brief How many control maps exist -- effectively the local player limit. */
#define AKGL_MAX_CONTROL_MAPS 8
/** @brief Bindings per control map. The default map installed by akgl_controller_default uses 8 of them. */
#define AKGL_MAX_CONTROLS 32
AKGL_CONTROLLER_KEY_BUFFER is 32 — the keystroke ring, sized so a program polling once a
frame never loses a key to a fast typist. AKGL_CONTROLLER_KEYSTROKE_TEXT is 8, enough for
one UTF-8 code point and its terminator; a longer commit is truncated on a code point
boundary rather than through the middle of one.
Drawing
AKGL_DRAW_MAX_FLOOD_SPANS is 4096 — the fixed stack of horizontal runs akgl_draw_flood_fill
keeps instead of recursing per pixel. A region needing more pending runs at once reports
AKERR_OUTOFBOUNDS rather than overflowing; an ordinary convex or moderately concave shape
needs a few dozen.
Audio
/** @brief Sample rate of the generated stream, in frames per second. */
#define AKGL_AUDIO_SAMPLE_RATE 44100
/** @brief Frames the device callback generates per pass through the mixer. */
#define AKGL_AUDIO_MIX_FRAMES 512
AKGL_AUDIO_MAX_VOICES is 3. AKGL_AUDIO_SWEEP_TICK_HZ is 60, and it divides the sample
rate exactly, so a sweep step boundary always lands on a whole frame —
AKGL_AUDIO_SWEEP_TICK_FRAMES is 735.
The SDL_mixer side is separate: AKGL_GAME_AUDIO_MAX_TRACKS is 64 playback tracks, and
slot 0 is unused — AKGL_GAME_AUDIO_TRACK_BGM is slot 1, and the rest are yours.
Time and the frame
| Constant | Value | Meaning |
|---|---|---|
AKGL_TIME_ONESEC_NS |
1000000000 | Nanoseconds in a second — the unit SDL_GetTicksNS reports in |
AKGL_TIME_ONEMS_NS |
1000000 | Nanoseconds in a millisecond — the scale factor from JSON frame durations to internal ones |
AKGL_GAME_STATE_LOCK_BUDGET_MS |
1000 | How long akgl_game_state_lock keeps trying before raising |
AKGL_GAME_STATE_LOCK_RETRY_MS |
100 | How long it sleeps between attempts |
AKGL_PHYSICS_DEFAULT_MAX_TIMESTEP |
0.05 | Default physics.max_timestep, in seconds — three frames at 60 Hz |
AKGL_TIME_ONEMS_NS was called AKGL_TIME_ONESEC_MS until 0.5.0 and held 1000000 — a name
and a value describing two different quantities. The state lock read it as a one-second
budget and was wrong by a factor of a thousand, blocking for roughly sixteen minutes.
The status band
| Constant | Value |
|---|---|
AKGL_ERR_BASE |
256 (AKERR_FIRST_CONSUMER_STATUS) |
AKGL_ERR_COUNT |
5 |
AKGL_ERR_LIMIT |
261 — the one-past-the-end sentinel, not a status |
C. Configuration properties
Everything the library reads out of AKGL_REGISTRY_PROPERTIES. Every value is a string,
including the numbers, and each is parsed by whoever reads it.
| Property | Default | Read by | Read when | Meaning |
|---|---|---|---|---|
game.screenwidth |
"0" |
akgl_render_2d_init |
once, at init | Window and camera width in pixels. Passed straight to SDL_CreateWindowAndRenderer |
game.screenheight |
"0" |
akgl_render_2d_init |
once, at init | Window and camera height in pixels |
physics.gravity.x |
"0.0" |
akgl_physics_init_arcade |
once, at init | Constant acceleration on x, px/s² |
physics.gravity.y |
"0.0" |
akgl_physics_init_arcade |
once, at init | Constant acceleration on y, px/s². Positive is down |
physics.gravity.z |
"0.0" |
akgl_physics_init_arcade |
once, at init | Constant acceleration on z, px/s² |
physics.drag.x |
"0.0" |
akgl_physics_init_arcade |
once, at init | Fraction of environmental velocity shed per second on x |
physics.drag.y |
"0.0" |
akgl_physics_init_arcade |
once, at init | Same for y. The only brake on falling — there is no terminal velocity, and ey approaches gravity_y / drag_y |
physics.drag.z |
"0.0" |
akgl_physics_init_arcade |
once, at init | Same for z |
physics.max_timestep |
"0.05" |
akgl_physics_init_arcade |
once, at init | Ceiling on a single simulation dt, in seconds |
That is the complete list — nine properties, read by two functions, each exactly once.
physics.engine is not read by anything. physics.h says in two places that
akgl_game_init passes it to akgl_physics_factory; akgl_game_init never calls the
factory, and the string does not appear in src/. Pass "null" or "arcade" to
akgl_physics_factory yourself. Correcting the header is a source change and is deliberately
out of scope for this manual.
Everything is read at init. Setting any of these after akgl_render_2d_init or
akgl_physics_init_arcade has no effect at all, and reports success. See
Chapter 6, and the startup order in
Chapter 7.
A property set before AKGL_REGISTRY_PROPERTIES exists is silently discarded.
akgl_set_property does not check SDL's return value. akgl_game_init and
akgl_registry_init both create the registry; a hand-rolled startup that creates neither
gets no configuration and no error.