Files
libakgl/docs/22-appendix-limits.md

430 lines
22 KiB
Markdown
Raw Normal View History

# 22. Appendix: limits, statuses and properties
Add the manual: nineteen chapters and a corrected README 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>
2026-08-01 20:58:37 -04:00
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](04-errors.md).
### `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`, `_collision_proxy`, `_collision_cell`, `_bspnode` | `AKGL_ERR_HEAP` |
| `akgl_heap_release_actor`, `_sprite`, `_spritesheet`, `_character`, `_string`, `_collision_proxy`, `_collision_cell`, `_bspnode` | `AKERR_NULLPOINTER` |
Add the manual: nineteen chapters and a corrected README 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>
2026-08-01 20:58:37 -04:00
### `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`, plus `akgl_collision_resolve`'s. A no-op when `self->collision` is `NULL` |
### `collision.h`
| Function | Raises |
|---|---|
| `akgl_collision_shape_box`, `_shape_circle`, `_shape_capsule`, `_shape_bounds`, `_shape_interacts` | `AKERR_NULLPOINTER` |
| `akgl_collision_world_init` | `AKERR_NULLPOINTER`, **`AKERR_VALUE` for a non-positive cell size**, plus `akgl_partitioner_factory`'s |
| `akgl_collision_bind_tilemap` | `AKERR_NULLPOINTER`, plus `akgl_get_json_properties_boolean`'s; handles `AKERR_KEY` (H) for a layer with no `collidable` property |
| `akgl_collision_test` | `AKERR_NULLPOINTER`, **`AKGL_ERR_COLLISION` when the ccd arena is exhausted** — the message carries the high-water mark |
| `akgl_collision_resolve` | `AKERR_NULLPOINTER`, plus `akgl_collision_test`'s and the actor's own `collidefunc`'s |
| `akgl_collision_settle` | `AKERR_NULLPOINTER`, **`AKERR_VALUE` when the shape is not clear within `maxsteps`** |
| `akgl_collision_solid_at`, `_box_blocked`, `_query_box`, `_substeps`, `_sync_actors` | `AKERR_NULLPOINTER` |
| `akgl_collision_proxy_initialize`, `_proxy_sync`, `_cell_initialize` | `AKERR_NULLPOINTER` |
| `akgl_partitioner_factory` | `AKERR_NULLPOINTER`, **`AKERR_KEY` for an unknown partitioner name** |
| `akgl_partitioner_init_grid`, `_init_bsp` | `AKERR_NULLPOINTER`. The BSP's `insert` raises `AKGL_ERR_HEAP` past `AKGL_MAX_HEAP_COLLISION_PROXY` proxies |
| `akgl_actor_collide_block` | `AKERR_NULLPOINTER` |
Add the manual: nineteen chapters and a corrected README 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>
2026-08-01 20:58:37 -04:00
### `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_*` eight
and `AKGL_CCD_ARENA_BYTES` are overridable**, and even those have to be overridden for the whole build — see
Add the manual: nineteen chapters and a corrected README 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>
2026-08-01 20:58:37 -04:00
[Chapter 5](05-the-heap.md), "The ceilings are a compile-time ABI constraint". The rest are
plain `#define`s with no `#ifndef` guard: changing one means editing the header and
rebuilding libakgl and everything linking it.
### The object pools
```c excerpt=include/akgl/heap.h
#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
```
```c excerpt=include/akgl/heap.h
#ifndef AKGL_MAX_HEAP_COLLISION_PROXY
#define AKGL_MAX_HEAP_COLLISION_PROXY (AKGL_MAX_HEAP_ACTOR * 2)
#endif
```
```c excerpt=include/akgl/heap.h
#ifndef AKGL_MAX_HEAP_BSPNODE
#define AKGL_MAX_HEAP_BSPNODE (AKGL_MAX_HEAP_COLLISION_PROXY)
#endif
#ifndef AKGL_MAX_HEAP_COLLISION_CELL
#define AKGL_MAX_HEAP_COLLISION_CELL (AKGL_MAX_HEAP_COLLISION_PROXY * 16)
#endif
```
Add the manual: nineteen chapters and a corrected README 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>
2026-08-01 20:58:37 -04:00
Exceeding any of them is `AKGL_ERR_HEAP`.
### Collision
Three of these are not in a public header, because nothing outside the broad phase may size
anything against them:
| Constant | Value | Where | Bounds |
|---|---:|---|---|
| `AKGL_CCD_ARENA_BYTES` | 65536 | `collision_arena.h` | The narrowphase's whole allocation budget. Overridable. One GJK/EPA box pair measures 7,264 bytes |
| `AKGL_COLLISION_GRID_COLS` / `_ROWS` | 128 | `src/collision_grid.c` | The dense cell-head array, 32 KB. A map wider than 128 cells wraps rather than growing |
| `AKGL_COLLISION_GRID_MAX_SPAN` | 16 | `src/collision_grid.c` | Cells one proxy may occupy. Anything larger spills to one oversized chain, which is what makes `AKGL_MAX_HEAP_COLLISION_CELL` provable rather than hopeful |
| `AKGL_COLLISION_MAX_SUBSTEPS` | 8 | `src/collision.c` | Sub-steps per actor per step, and therefore the speed above which tunnelling returns |
| `AKGL_COLLISION_MAX_ITERATIONS` | 100 | `src/collision.c` | `ccd_t.max_iterations`. libccd's own default is `(unsigned long)-1` — an unbounded loop inside a frame |
| `AKGL_COLLISION_DEPTH_RATIO` | 2.0 | `collision.h` | The z extrusion, as a multiple of the largest planar half-extent. See [Chapter 15](15-collision.md) before changing it |
Add the manual: nineteen chapters and a corrected README 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>
2026-08-01 20:58:37 -04:00
### 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
```c excerpt=include/akgl/actor.h
/** @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
```c excerpt=include/akgl/sprite.h
#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
```c excerpt=include/akgl/tilemap.h
/** @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
```c excerpt=include/akgl/controller.h
/** @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
```c excerpt=include/akgl/audio.h
/** @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](06-the-registry.md), and the startup order in
[Chapter 7](07-the-game-and-the-frame.md).
**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](04-errors.md) — what each status means.
- [Chapter 5](05-the-heap.md) — the pools, and the ABI constraint on their ceilings.
- [Chapter 6](06-the-registry.md) — the property store these live in.
- The generated Doxygen reference — every function's own `@throws` list and full signature.