Files
libakgl/docs/22-appendix-limits.md
Andrew Kesterson bace818998 Renumber chapters 15 to 21 up by one, to open a slot for collision
A pure rename plus link rewrite and nothing else. Collision is a pluggable
subsystem with two implementations, shapes, a response hook and a tile binding;
that is a chapter, and burying it inside the physics chapter is the shape this
manual otherwise avoids -- rendering and drawing are 8 and 9, spritesheets and
characters and actors are 10, 11 and 12.

Kept separate from writing the chapter because **nothing validates a
cross-reference target.** The example harness reads fenced blocks; it does not
follow links. A rename mixed into five hundred lines of new prose is not
reviewable, and a broken link would land silently. As its own commit it is
reviewable as a rename, and a grep for dangling `](NN-` targets is clean.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:15:25 -04:00

19 KiB
Raw Blame History

22. 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_NULLPOINTERincluding "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_KEYvoid; 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_NULLPOINTERvoid; 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_LAYER is enforced. The comment above says it is not. akgl_tilemap_load_layer_objects bounds j at the top of the loop body and raises AKERR_OUTOFBOUNDS; akgl_tilemap_load_tilesets does the same for AKGL_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_load checks width * 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_IMAGE is not checked anywhere. It sizes tile_offsets[65536][2] in the struct and nothing validates a tileset against it. It is also why akgl_Tileset is 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 unusedAKGL_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.

Where to go next

  • Chapter 4 — what each status means.
  • Chapter 5 — the pools, and the ABI constraint on their ceilings.
  • Chapter 6 — the property store these live in.
  • The generated Doxygen reference — every function's own @throws list and full signature.