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
|
|
|
# 05. The heap
|
|
|
|
|
|
|
|
|
|
libakgl does not call `malloc`. Every runtime object comes out of a fixed, statically
|
|
|
|
|
allocated array declared in `include/akgl/heap.h`, and "allocation" is a linear scan for a
|
|
|
|
|
slot whose `refcount` is zero.
|
|
|
|
|
|
|
|
|
|
**The consequence to design around is that exhaustion is an ordinary error, not an
|
|
|
|
|
out-of-memory catastrophe.** `AKGL_ERR_HEAP` is a status you can handle. It is also,
|
|
|
|
|
almost always, a missing release rather than a pool that is genuinely too small.
|
|
|
|
|
|
Document collision, and correct what it made false elsewhere
The new chapter 15 covers the opt-in setup, where resolution runs in the step
and why it runs after the move, shapes and the z-extrusion trap, the asymmetric
masks and the defaults that matter more than the mechanism, tiles as a source
rather than proxies, the response hook and the three ways it is easy to write
wrongly, sensors, the four queries, both partitioners, and what is still not
implemented.
Collision falsified claims in eight other chapters. The physics chapter said
"There is no collision detection, at all" and that collide always raises
AKERR_API; actors had six behaviour hooks; the heap had five pools and four
non-claiming acquires; the status band held five codes; and the design
philosophy had exactly two pluggable subsystems. The appendix gains a
collision.h status section, the three new pool ceilings and a table of the
collision constants that are deliberately not in a public header.
Also fixes chapter labels the renumbering commit left pointing at their old
numbers -- "18. Utilities" linked to 19-utilities.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 07:25:52 -04:00
|
|
|
## The eight pools
|
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
|
|
|
|
|
|
|
|
A "heap layer" is one array plus its `next`/`release` pair. There are five:
|
|
|
|
|
|
|
|
|
|
| Layer | Ceiling | Default | Element | Total |
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
| Actor | `AKGL_MAX_HEAP_ACTOR` | 64 | 400 B | 25 KiB |
|
|
|
|
|
| Sprite | `AKGL_MAX_HEAP_SPRITE` | 1024 | 176 B | 176 KiB |
|
|
|
|
|
| Spritesheet | `AKGL_MAX_HEAP_SPRITESHEET` | 1024 | 536 B | 536 KiB |
|
|
|
|
|
| Character | `AKGL_MAX_HEAP_CHARACTER` | 256 | 184 B | 46 KiB |
|
|
|
|
|
| String | `AKGL_MAX_HEAP_STRING` | 256 | 4100 B | 1.0 MiB |
|
|
|
|
|
|
|
|
|
|
The whole thing is about 1.8 MiB of BSS, and **the string pool is more than half of it**
|
|
|
|
|
because every `akgl_String` is `PATH_MAX` bytes whether it holds a path or the four
|
|
|
|
|
characters `"0.0"`.
|
|
|
|
|
|
|
|
|
|
The ceilings are all `#ifndef`-guarded:
|
|
|
|
|
|
|
|
|
|
```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
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The sprite pool is sixteen per actor, on the assumption of one sprite per state
|
|
|
|
|
combination. The spritesheet pool is sized to match, though sharing by resolved path means
|
|
|
|
|
far fewer are used in practice.
|
|
|
|
|
|
|
|
|
|
The five arrays are `extern` and public — `akgl_heap_actors`, `akgl_heap_sprites`,
|
|
|
|
|
`akgl_heap_spritesheets`, `akgl_heap_characters`, `akgl_heap_strings` — so the render and
|
|
|
|
|
physics sweeps can walk them directly instead of going through a registry.
|
|
|
|
|
`akgl_game_update` does exactly that.
|
|
|
|
|
|
|
|
|
|
### The ceilings are a compile-time ABI constraint
|
|
|
|
|
|
|
|
|
|
This is the part that bites, and it does not announce itself.
|
|
|
|
|
|
|
|
|
|
The arrays are sized at compile time. Defining `AKGL_MAX_HEAP_ACTOR` to 256 before
|
|
|
|
|
including `heap.h` changes the *declared* extent of `akgl_heap_actors` in your translation
|
|
|
|
|
unit — but `src/heap.c` was compiled with whatever the library was built with, and that is
|
|
|
|
|
where the storage actually lives. Mismatch the two and your code indexes past the end of an
|
|
|
|
|
array that is still 64 entries long, in another object file, with no diagnostic anywhere.
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
your game.c libakgl.so
|
|
|
|
|
------------------------ ------------------------
|
|
|
|
|
#define AKGL_MAX_HEAP_ACTOR 256 (built with the default 64)
|
|
|
|
|
#include <akgl/heap.h>
|
|
|
|
|
akgl_Actor akgl_heap_actors[64];
|
|
|
|
|
akgl_heap_actors[200] ------------------> off the end. No warning.
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Raise a ceiling for the whole build or not at all.** With `add_subdirectory`, put it in a
|
|
|
|
|
`target_compile_definitions` that reaches libakgl's own sources as well as yours. Against
|
|
|
|
|
an installed `libakgl.so`, you cannot change it at all without rebuilding and reinstalling
|
|
|
|
|
the library, and the soname does not encode it — so a rebuilt game against an old installed
|
|
|
|
|
library is a silent overrun, not a link error.
|
|
|
|
|
|
|
|
|
|
## Claiming and releasing
|
|
|
|
|
|
|
|
|
|
Each layer has an acquire and a release:
|
|
|
|
|
|
|
|
|
|
| Layer | Acquire | Release |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| Actor | `akgl_heap_next_actor` | `akgl_heap_release_actor` |
|
|
|
|
|
| Sprite | `akgl_heap_next_sprite` | `akgl_heap_release_sprite` |
|
|
|
|
|
| Spritesheet | `akgl_heap_next_spritesheet` | `akgl_heap_release_spritesheet` |
|
|
|
|
|
| Character | `akgl_heap_next_character` | `akgl_heap_release_character` |
|
|
|
|
|
| String | `akgl_heap_next_string` | `akgl_heap_release_string` |
|
|
|
|
|
|
|
|
|
|
Every acquire raises `AKGL_ERR_HEAP` and nothing else. Every release raises
|
|
|
|
|
`AKERR_NULLPOINTER` and nothing else. Full signatures are in the Doxygen reference; the
|
|
|
|
|
behaviour worth knowing is below.
|
|
|
|
|
|
|
|
|
|
**None of the `next_*` functions checks `dest`.** A `NULL` there is a crash, not an error
|
|
|
|
|
context. That is deliberate — the check would cost a branch on the allocation path — but it
|
|
|
|
|
means these are the functions where you validate your own argument first.
|
|
|
|
|
|
|
|
|
|
**None of them zeroes the slot either.** What you get back is whatever the previous holder
|
|
|
|
|
left. The `*_initialize` functions `memset` before they do anything else, which is why the
|
|
|
|
|
normal shape is acquire-then-initialize and never acquire-then-use.
|
|
|
|
|
|
|
|
|
|
### The refcount asymmetry
|
|
|
|
|
|
|
|
|
|
This is the one thing about the heap that will surprise you, and it is a known defect
|
|
|
|
|
rather than a design (`TODO.md`, "Known and still open" item 8; also the `@warning` on
|
|
|
|
|
`heap.h` itself).
|
|
|
|
|
|
|
|
|
|
| Acquire | Takes the reference? | Who takes it |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `akgl_heap_next_string` | **Yes** — `refcount` is 1 on return | the acquire itself |
|
|
|
|
|
| `akgl_heap_next_actor` | No | `akgl_actor_initialize` |
|
|
|
|
|
| `akgl_heap_next_sprite` | No | `akgl_sprite_initialize` |
|
|
|
|
|
| `akgl_heap_next_spritesheet` | No | `akgl_spritesheet_initialize` |
|
|
|
|
|
| `akgl_heap_next_character` | No | `akgl_character_initialize` |
|
|
|
|
|
|
|
|
|
|
**Until a reference is taken, the slot is still free and the next acquire hands out the
|
Document collision, and correct what it made false elsewhere
The new chapter 15 covers the opt-in setup, where resolution runs in the step
and why it runs after the move, shapes and the z-extrusion trap, the asymmetric
masks and the defaults that matter more than the mechanism, tiles as a source
rather than proxies, the response hook and the three ways it is easy to write
wrongly, sensors, the four queries, both partitioners, and what is still not
implemented.
Collision falsified claims in eight other chapters. The physics chapter said
"There is no collision detection, at all" and that collide always raises
AKERR_API; actors had six behaviour hooks; the heap had five pools and four
non-claiming acquires; the status band held five codes; and the design
philosophy had exactly two pluggable subsystems. The appendix gains a
collision.h status section, the three new pool ceilings and a table of the
collision constants that are deliberately not in a public header.
Also fixes chapter labels the renumbering commit left pointing at their old
numbers -- "18. Utilities" linked to 19-utilities.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 07:25:52 -04:00
|
|
|
same pointer.** Seven of the eight acquires therefore leave a window in which two callers can
|
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
|
|
|
be holding the same object, and the window closes only when `*_initialize` runs. In
|
|
|
|
|
practice you never see it, because the four are always immediately followed by their
|
|
|
|
|
initializer — which is exactly why the asymmetry has survived. Write them adjacent and
|
|
|
|
|
nothing can get in between:
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
#include <akerror.h>
|
|
|
|
|
#include <akgl/error.h>
|
|
|
|
|
#include <akgl/actor.h>
|
|
|
|
|
#include <akgl/heap.h>
|
|
|
|
|
|
|
|
|
|
/* Claim an actor slot and take the reference on it. */
|
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *spawn_actor(akgl_Actor **dest, char *name)
|
|
|
|
|
{
|
|
|
|
|
akgl_Actor *obj = NULL;
|
|
|
|
|
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL name");
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akgl_heap_next_actor(&obj));
|
|
|
|
|
/* obj->refcount is still 0. The slot is not yours yet. */
|
|
|
|
|
PASS(errctx, akgl_actor_initialize(obj, name));
|
|
|
|
|
/* obj->refcount is 1 now, and the actor is in AKGL_REGISTRY_ACTOR. */
|
|
|
|
|
*dest = obj;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
You do not have to take that on trust. This program is compiled, linked against the library
|
|
|
|
|
and run by the test suite, and the output below it is what it actually prints:
|
|
|
|
|
|
|
|
|
|
```c run=akglapp
|
|
|
|
|
akgl_Actor *first = NULL;
|
|
|
|
|
akgl_Actor *second = NULL;
|
|
|
|
|
akgl_String *str = NULL;
|
|
|
|
|
|
|
|
|
|
/* A string acquire takes the reference for you. */
|
|
|
|
|
PASS(errctx, akgl_heap_next_string(&str));
|
|
|
|
|
printf("string refcount after acquire: %d\n", str->refcount);
|
|
|
|
|
|
|
|
|
|
/* An actor acquire does not. */
|
|
|
|
|
PASS(errctx, akgl_heap_next_actor(&first));
|
|
|
|
|
printf("actor refcount after acquire: %d\n", first->refcount);
|
|
|
|
|
|
|
|
|
|
/* So a second acquire, before the initializer runs, finds the same free slot. */
|
|
|
|
|
PASS(errctx, akgl_heap_next_actor(&second));
|
|
|
|
|
printf("same slot handed out twice: %s\n", (first == second) ? "yes" : "no");
|
|
|
|
|
|
|
|
|
|
/* Initializing is what takes the reference and closes the window. */
|
|
|
|
|
PASS(errctx, akgl_actor_initialize(first, "player"));
|
|
|
|
|
printf("actor refcount after init: %d\n", first->refcount);
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akgl_heap_next_actor(&second));
|
|
|
|
|
printf("now a distinct slot: %s\n", (first == second) ? "no" : "yes");
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akgl_heap_release_actor(first));
|
|
|
|
|
PASS(errctx, akgl_heap_release_string(str));
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```output
|
|
|
|
|
string refcount after acquire: 1
|
|
|
|
|
actor refcount after acquire: 0
|
|
|
|
|
same slot handed out twice: yes
|
|
|
|
|
actor refcount after init: 1
|
|
|
|
|
now a distinct slot: yes
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Note also that a failure between the two — `akgl_actor_initialize` rejecting the name, say
|
|
|
|
|
— needs no cleanup, precisely because nothing was claimed. That is the one thing the
|
|
|
|
|
asymmetry is good for.
|
|
|
|
|
|
|
|
|
|
The string layer is the opposite and is safe on its own, which is what makes the
|
|
|
|
|
scratch-buffer idiom below work.
|
|
|
|
|
|
|
|
|
|
### What each release actually does
|
|
|
|
|
|
|
|
|
|
They all decrement, and they all do the teardown when the count reaches zero. What the
|
|
|
|
|
teardown *is* differs, and two of them reach outside the pool:
|
|
|
|
|
|
|
|
|
|
- **`akgl_heap_release_actor`** releases every non-`NULL` child recursively, clears the
|
|
|
|
|
actor's entry from `AKGL_REGISTRY_ACTOR`, and zeroes the slot. The recursion has **no
|
|
|
|
|
cycle check**: an actor reachable from its own child list recurses until the stack runs
|
|
|
|
|
out.
|
|
|
|
|
- **`akgl_heap_release_sprite`** clears the `AKGL_REGISTRY_SPRITE` entry and zeroes the
|
|
|
|
|
slot. **It does not release the spritesheet** — the sprite only borrowed it.
|
|
|
|
|
- **`akgl_heap_release_spritesheet`** clears the registry entry, **destroys the
|
|
|
|
|
`SDL_Texture`**, and zeroes the slot. Any sprite still pointing at the sheet is left with
|
|
|
|
|
a dangling pointer, since nothing takes a reference on a sheet on a sprite's behalf.
|
|
|
|
|
Destroying a texture is a main-thread operation in SDL, so this must be called from the
|
|
|
|
|
thread that owns the renderer.
|
|
|
|
|
- **`akgl_heap_release_character`** walks the state-to-sprite map with
|
|
|
|
|
`akgl_character_state_sprites_iterate` and `AKGL_ITERATOR_OP_RELEASE`, giving back every
|
|
|
|
|
reference `akgl_character_sprite_add` took, then destroys the `SDL_PropertiesID` holding
|
|
|
|
|
the map, clears the registry entry, and zeroes the slot.
|
|
|
|
|
- **`akgl_heap_release_string`** zeroes the character data. Strings are not registered
|
|
|
|
|
anywhere, so that is all there is to it.
|
|
|
|
|
|
|
|
|
|
One shared hazard: **a slot whose `refcount` is already 0 is re-torn-down rather than
|
|
|
|
|
rejected.** Harmless on a zeroed slot, destructive on a live one that was never registered.
|
|
|
|
|
|
|
|
|
|
`akgl_heap_release_string` is the exception to the library's usual `NULL`-is-a-no-op
|
|
|
|
|
convention: a `NULL` here is `AKERR_NULLPOINTER`, which is why every `CLEANUP` block in the
|
|
|
|
|
library wraps it in `IGNORE()`.
|
|
|
|
|
|
|
|
|
|
## `akgl_String`
|
|
|
|
|
|
|
|
|
|
A pooled string is a `refcount` and a `PATH_MAX` buffer:
|
|
|
|
|
|
|
|
|
|
```c excerpt=include/akgl/staticstring.h
|
|
|
|
|
/** @brief Provides a fixed-capacity, heap-managed string buffer. */
|
|
|
|
|
typedef struct
|
|
|
|
|
{
|
|
|
|
|
int refcount; /**< Pool bookkeeping; 0 means the slot is free. Owned by the heap layer. */
|
|
|
|
|
char data[AKGL_MAX_STRING_LENGTH]; /**< The characters. Not guaranteed NUL-terminated when filled to capacity. */
|
|
|
|
|
} akgl_String;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Capacity is fixed. `akgl_string_initialize` and `akgl_string_copy` **truncate rather than
|
|
|
|
|
grow, and the truncation is silent** — they both always NUL-terminate, but they do not tell
|
|
|
|
|
you that something was lost. If you need to know, measure first, or go through
|
|
|
|
|
`aksl_strncpy` with the full destination size and let it raise `AKERR_OUTOFBOUNDS`.
|
|
|
|
|
|
|
|
|
|
Two details that catch people:
|
|
|
|
|
|
|
|
|
|
- **`akgl_heap_next_string` does not clean the buffer.** The contents are whatever the last
|
|
|
|
|
holder left. Call `akgl_string_initialize(str, NULL)` if you need it clean — and note
|
|
|
|
|
that `akgl_get_property` only copies the value and its terminator, leaving the rest of
|
|
|
|
|
the buffer alone, so read the result as a C string rather than as
|
|
|
|
|
`AKGL_MAX_STRING_LENGTH` bytes.
|
|
|
|
|
- **Several functions claim one for you.** `akgl_get_property`,
|
|
|
|
|
`akgl_get_json_string_value` and `akgl_get_json_array_index_string` all claim a slot when
|
|
|
|
|
`*dest` is `NULL`, and write in place when it is not. Either way **the caller releases
|
|
|
|
|
it.**
|
|
|
|
|
|
|
|
|
|
The scratch-buffer idiom is claim, use, release unconditionally in `CLEANUP`:
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
#include <akerror.h>
|
|
|
|
|
#include <akstdlib.h>
|
|
|
|
|
#include <akgl/error.h>
|
|
|
|
|
#include <akgl/heap.h>
|
|
|
|
|
#include <akgl/registry.h>
|
|
|
|
|
#include <akgl/staticstring.h>
|
|
|
|
|
|
|
|
|
|
/* The scratch-string idiom: claim, use, release unconditionally in CLEANUP. */
|
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *screen_width(int *dest)
|
|
|
|
|
{
|
|
|
|
|
akgl_String *value = NULL;
|
|
|
|
|
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination");
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, akgl_heap_next_string(&value));
|
|
|
|
|
CATCH(errctx, akgl_get_property("game.screenwidth", &value, "800"));
|
|
|
|
|
CATCH(errctx, aksl_atoi(value->data, dest));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
IGNORE(akgl_heap_release_string(value));
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The `CLEANUP` block runs on every path out of the `ATTEMPT`, which is the whole reason the
|
|
|
|
|
release goes there and not after the last `CATCH`. **Never use a `*_RETURN` macro inside
|
|
|
|
|
that `ATTEMPT`** — it returns past `CLEANUP` and leaks the slot. That mistake has shipped
|
|
|
|
|
in this library before.
|
|
|
|
|
|
|
|
|
|
## Reading `AKGL_ERR_HEAP`
|
|
|
|
|
|
|
|
|
|
When a pool is exhausted the message names the layer:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Unable to find unused string on the heap
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Before you raise the ceiling, look for the missing release. The realistic causes, in the
|
|
|
|
|
order they actually happen:
|
|
|
|
|
|
|
|
|
|
1. **A string claimed and never released.** By far the most common: the pool is 256 slots
|
|
|
|
|
and every `akgl_get_property` call can claim one. A per-frame read that forgets the
|
|
|
|
|
release exhausts it in four seconds at 60 Hz.
|
|
|
|
|
2. **A `*_RETURN` macro inside an `ATTEMPT`**, skipping the `CLEANUP` that held the
|
|
|
|
|
release.
|
|
|
|
|
3. **A `return` out of a `HANDLE` block.** That leaks a libakerror context slot rather than
|
|
|
|
|
an akgl one, and shows up as `"Unable to pull an error context from the array!"` on the
|
|
|
|
|
129th call — different pool, same shape of bug.
|
|
|
|
|
4. **Characters reloaded per level without releasing the previous ones**, which exhausts
|
|
|
|
|
the *sprite* pool rather than the character pool, since each character holds a reference
|
|
|
|
|
per bound state.
|
|
|
|
|
|
|
|
|
|
Only after all four are ruled out is the ceiling actually too small — and a game that
|
|
|
|
|
genuinely needs 256 actors should say so once, for the whole build, per the ABI note above.
|
|
|
|
|
|
|
|
|
|
## Initialization and reset
|
|
|
|
|
|
Document collision, and correct what it made false elsewhere
The new chapter 15 covers the opt-in setup, where resolution runs in the step
and why it runs after the move, shapes and the z-extrusion trap, the asymmetric
masks and the defaults that matter more than the mechanism, tiles as a source
rather than proxies, the response hook and the three ways it is easy to write
wrongly, sensors, the four queries, both partitioners, and what is still not
implemented.
Collision falsified claims in eight other chapters. The physics chapter said
"There is no collision detection, at all" and that collide always raises
AKERR_API; actors had six behaviour hooks; the heap had five pools and four
non-claiming acquires; the status band held five codes; and the design
philosophy had exactly two pluggable subsystems. The appendix gains a
collision.h status section, the three new pool ceilings and a table of the
collision constants that are deliberately not in a public header.
Also fixes chapter labels the renumbering commit left pointing at their old
numbers -- "18. Utilities" linked to 19-utilities.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 07:25:52 -04:00
|
|
|
`akgl_heap_init` zeroes all eight pools. `akgl_game_init` calls it for you, before any
|
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 exists.
|
|
|
|
|
|
|
|
|
|
**Calling it again is a reset, not a refresh.** It does not release textures, clear
|
|
|
|
|
registries, or consult reference counts, so every live object becomes a dangling pointer
|
|
|
|
|
and every registry entry points at a zeroed slot.
|
|
|
|
|
|
|
|
|
|
`akgl_heap_init_actor` zeroes the actor pool only, which is what makes it usable between
|
|
|
|
|
levels: the sprites, sheets and characters are the expensive ones because they own
|
|
|
|
|
textures. Pair it with `akgl_registry_init_actor`, the one registry initializer that
|
|
|
|
|
destroys the old property set before creating the replacement. See
|
|
|
|
|
[Chapter 6](06-the-registry.md).
|
|
|
|
|
|
|
|
|
|
Both return `NULL` and have no failure path today — they are a series of `memset`s. Check
|
|
|
|
|
them anyway; the signature exists so a layer that needs real setup has somewhere to report
|
|
|
|
|
from.
|
|
|
|
|
|
|
|
|
|
## If you need a new kind of object
|
|
|
|
|
|
|
|
|
|
Add a layer here. Do not reach for the allocator: `AGENTS.md` makes that a standing rule,
|
|
|
|
|
and a `malloc` in the middle of a frame is the failure mode the pools exist to remove.
|
|
|
|
|
A layer is an array, an `AKGL_MAX_HEAP_*` ceiling, a `next`, and a `release` — five things,
|
|
|
|
|
all visible in `src/heap.c`.
|
|
|
|
|
|
|
|
|
|
## Where to go next
|
|
|
|
|
|
|
|
|
|
- [Chapter 4](04-errors.md) — `AKGL_ERR_HEAP` in the status tables.
|
|
|
|
|
- [Chapter 6](06-the-registry.md) — the registries the releases clear entries from.
|
2026-08-02 07:15:25 -04:00
|
|
|
- [Chapter 22](22-appendix-limits.md) — every `AKGL_MAX_*` in one place.
|