TODO.md carried two records in one file: what had been done, with the measurements behind it, and what was left. The second half is what a tracker is for, and keeping it here has already cost something -- AGENTS.md records a round where eleven entries described code that had already changed, and this file admitted to three more. Every open item is now an issue on source.starfort.tech/andrew/libakgl, labelled by kind and blast radius and milestoned by what it can land in: 0.9.x for anything that breaks no ABI, 0.10.0 for new or changed public symbols, 1.0.0 for the design work. Four are epics: the performance plan (#60), coverage (#61), actor rotation (#62), and the false header comments (#63). Verified against the tree before filing rather than transcribed. Three entries were already fixed and were not filed: the akgl_path_relative context leak, the akgl_draw_background test extension, and the SDL enumeration audit -- keyboards, gamepads and mappings are all freed in CLEANUP today. Two were reworded because the code had moved: the fonts item is a missing teardown entry point rather than a missing API, since akgl_text_unloadallfonts exists, and draw_world's tilemap call is already bounded by numlayers, so only the per-layer actor rescan remains. TODO.md keeps the part a tracker has no place for: why a decision went the way it did, what the measurement was, and which arguments turned out to be wrong. TODO.txt is deleted. Four of its eight entries had shipped -- actor-to-actor collision, actor-to-world collision, automatic facing, image layers -- and the four that had not are #74 through #77, with the GPU renderer's research links kept because that is the part that took the time. Every reference that named an item number or a moved section is repointed, in the manual, the headers, the tests and the examples. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
13 KiB
06. The registry
Nothing in this library is passed around by pointer where a name will do. An actor names the character it instantiates, a character names the sprites it draws, a sprite names the sheet it cuts frames from. All of it is resolved at load time through eight registries, and that is what lets the whole asset graph be described in JSON files that reference each other by name.
The registries are SDL property sets, not akgl types. They are SDL_PropertiesID
values with external linkage, so you can enumerate one with SDL_EnumerateProperties,
which is exactly what akgl_registry_iterate_actor and
akgl_character_state_sprites_iterate do.
The eight registries
| Registry | Key | Value | Written by | Cleared by |
|---|---|---|---|---|
AKGL_REGISTRY_ACTOR |
actor name | akgl_Actor * |
akgl_actor_initialize |
akgl_heap_release_actor at refcount 0 |
AKGL_REGISTRY_SPRITE |
sprite name | akgl_Sprite * |
akgl_sprite_initialize |
akgl_heap_release_sprite at refcount 0 |
AKGL_REGISTRY_SPRITESHEET |
resolved image path | akgl_SpriteSheet * |
akgl_spritesheet_initialize |
akgl_heap_release_spritesheet at refcount 0 |
AKGL_REGISTRY_CHARACTER |
character name | akgl_Character * |
akgl_character_initialize |
akgl_heap_release_character at refcount 0 |
AKGL_REGISTRY_ACTOR_STATE_STRINGS |
state name | its bit value, as a number | akgl_registry_init_actor_state_strings |
never |
AKGL_REGISTRY_FONT |
font name (yours) | TTF_Font * |
akgl_text_loadfont |
akgl_text_unloadfont |
AKGL_REGISTRY_MUSIC |
music name | audio handle | nothing — created but never populated | — |
AKGL_REGISTRY_PROPERTIES |
configuration key | string value | akgl_set_property, akgl_registry_load_properties |
never |
Three of those rows are worth expanding.
The spritesheet registry is keyed by path, not by name. That is the sharing mechanism:
two sprites cutting frames out of the same image find the same akgl_SpriteSheet and the
same SDL_Texture, and the texture is loaded once.
The actor-state-string registry exists so JSON can name a bit.
akgl_registry_init_actor_state_strings walks AKGL_ACTOR_STATE_STRING_NAMES and maps
entry i to 1 << i, which is what lets a character definition write
"AKGL_ACTOR_STATE_FACE_LEFT" instead of 2. Two entries in that name table disagree with
actor.h: bits 11 and 12 are UNDEFINED_11 and UNDEFINED_12 rather than MOVING_IN and
MOVING_OUT, so those two states cannot be named from JSON at all (TODO.md,
"AKGL_ACTOR_STATE_STRING_NAMES"). The individual name registrations are not checked, either.
The music registry is empty. akgl_registry_init_music creates it and nothing in the
library ever writes to it. It is there for you.
What actually runs at startup
registry.h documents akgl_registry_init as creating "the seven asset registries", with
a @warning that it does not create AKGL_REGISTRY_PROPERTIES. Both halves of that are
now wrong, and the correction matters because the warning is what a reader would design
around.
Read against src/registry.c and src/game.c, this is what is true:
akgl_registry_initcreates eight, includingAKGL_REGISTRY_PROPERTIES. The properties call was added in 0.5.0 (TODO.md, "Formatting and hygiene"). Its order is spritesheet, sprite, character, actor, actor-state-strings, font, music, properties.akgl_game_initnever callsakgl_registry_init. It calls the eight individual initializers itself, in a different order: actor, sprite, spritesheet, character, font, music, properties, actor-state-strings.
So there are two supported startup paths and they do not share code. If you call
akgl_game_init, you have all eight and need do nothing. If you are building your own
startup — an embedder binding an existing renderer, say — call akgl_registry_init and you
also have all eight. What you must not do is call some subset by hand and assume the rest
followed.
akgl_registry_init_actor is the one initializer that behaves differently in a way you
would notice: since 0.5.0 every initializer destroys the previous property set before
creating the replacement, so none of them leaks on a second call, but the actor one is
the one meant to be called again — between levels, paired with akgl_heap_init_actor. Note
that it destroys the registry, not the actors; releasing those is
akgl_heap_release_actor's job.
The id-0 trap
Every SDL_PropertiesID in src/registry.c starts at 0, and SDL treats 0 as "no such
property set". Reads return the default and writes are refused. An uninitialized
registry therefore does not fail the way you would want it to.
How loud that failure is depends entirely on whether libakgl checks SDL's return value, and it does not check everywhere:
| Call | Behaviour against an id of 0 |
|---|---|
akgl_actor_initialize, akgl_sprite_initialize, akgl_spritesheet_initialize, akgl_character_initialize |
Loud. The SDL_SetPointerProperty result is checked; you get AKERR_KEY, "Unable to add … to registry" |
akgl_actor_set_character |
Loud. SDL_GetPointerProperty returns NULL and the result is checked; you get AKERR_NULLPOINTER |
akgl_set_property |
Silent. The SDL_SetStringProperty result is not checked. The value is discarded and the call returns success |
akgl_registry_load_properties |
Silent, and worse. Same unchecked write, and it logs Set property x = y for every entry it dropped, then logs Properties loaded |
akgl_get_property |
Silent. You get the default you passed, every time |
akgl_registry_init_actor_state_strings |
Silent. The SDL_SetNumberProperty results are not checked |
The consequence is the one registry.h warns about, and it is real even though the
attribution has moved: a startup path that leaves AKGL_REGISTRY_PROPERTIES at 0 gives
you a no-op akgl_set_property and an akgl_get_property that always hands back your
default — which in turn means akgl_render_2d_init builds a window from its fallback
size and akgl_physics_init_arcade runs with no gravity, both reporting success.
If you are debugging configuration that appears not to apply, check
AKGL_REGISTRY_PROPERTIES != 0 before you check anything else.
Truncated keys
Every name is copied into a fixed-width field with aksl_strncpy bounded to
sizeof(field) - 1, which is the "truncated, not rejected" contract those headers
document. The fields:
| Object | Field | Bytes |
|---|---|---|
akgl_Actor::name |
AKGL_ACTOR_MAX_NAME_LENGTH |
128 |
akgl_Sprite::name |
AKGL_SPRITE_MAX_NAME_LENGTH |
128 |
akgl_Character::name |
AKGL_CHARACTER_MAX_NAME_LENGTH |
128 |
akgl_SpriteSheet::name |
AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH |
512 |
Two distinct names that agree on their first 127 bytes truncate to the same key, and
the second SDL_SetPointerProperty silently replaces the first. The objects are different;
the registry cannot tell. That is issue #54, and it is not fixed yet because the fix is a contract change — refusing an
over-long name with AKERR_OUTOFBOUNDS — and every one of those headers currently promises
the opposite.
There is a second half that the issue understates. It says the truncated name is the registry key. That is true for sprites and spritesheets, which register under their own copied field. It is not true for actors and characters:
akgl_actor_initializeandakgl_character_initializeregister under the caller's string, untruncated.akgl_heap_release_actorandakgl_heap_release_characterclear under the object's truncated field.
For any name of 127 bytes or fewer those are the same string and nothing happens. Over that, the entry is written under one key and cleared under another, so releasing the object leaves a live registry entry pointing at a zeroed pool slot — and the next lookup of that name hands out a pointer to freed storage. Keep asset names short; 127 bytes is generous for something a human types into a JSON file.
Configuration properties
AKGL_REGISTRY_PROPERTIES is a string-to-string store. Everything is a string, including
numbers — game.screenwidth is "800", not 800 — and whoever reads a property parses
it.
These are every property the library itself reads, with the default it falls back to and where it is read:
| Property | Default | Read by | Meaning |
|---|---|---|---|
game.screenwidth |
"0" |
akgl_render_2d_init |
Window and camera width, in pixels |
game.screenheight |
"0" |
akgl_render_2d_init |
Window and camera height, in pixels |
physics.gravity.x |
"0.0" |
akgl_physics_init_arcade |
Constant acceleration on x, px/s² |
physics.gravity.y |
"0.0" |
akgl_physics_init_arcade |
Constant acceleration on y, px/s². Positive is down |
physics.gravity.z |
"0.0" |
akgl_physics_init_arcade |
Constant acceleration on z, px/s² |
physics.drag.x |
"0.0" |
akgl_physics_init_arcade |
Fraction of environmental velocity shed per second on x |
physics.drag.y |
"0.0" |
akgl_physics_init_arcade |
Same for y. This is the only brake on falling — there is no terminal velocity |
physics.drag.z |
"0.0" |
akgl_physics_init_arcade |
Same for z |
physics.max_timestep |
"0.05" |
akgl_physics_init_arcade |
Upper bound on a single simulation dt, in seconds |
That is the whole list. Two things follow from it:
The screen size defaults to 0×0. akgl_render_2d_init passes whatever it parsed
straight to SDL_CreateWindowAndRenderer, so a program that never sets those two asks SDL
for a zero-sized window. Set them before calling it.
physics.engine is not a property. physics.h says in two places that akgl_game_init
passes a physics.engine property to akgl_physics_factory. It does not — akgl_game_init
never calls the factory at all, and the string physics.engine does not appear anywhere in
src/. Choosing a backend is an explicit call you make yourself, with a name of "null" or
"arcade"; see Chapter 14. The header comment is a known-false claim left
alone deliberately, since correcting it is a source change rather than a documentation one.
Setting properties
Two ways in, and both must happen after akgl_game_init (which creates the registry)
and before akgl_render_2d_init or akgl_physics_init_arcade (which read it).
akgl_set_property(name, value) sets one. SDL copies the value, so your buffer can go away
afterwards.
akgl_registry_load_properties(fname) loads a JSON document. It expects a top-level
properties object whose members are all strings:
{
"properties": {
"game.screenwidth": "800",
"game.screenheight": "600",
"physics.gravity.y": "400.0",
"physics.drag.y": "0.9",
"physics.max_timestep": "0.05"
}
}
A member that is not a string is AKERR_TYPE; a document with no properties member is
AKERR_KEY; a file that will not open or will not parse is AKERR_NULLPOINTER with
jansson's line number and text in the message. Note that a failure part-way through leaks
one string-pool slot, because the per-property CLEANUP block is empty — a malformed
configuration file is worth failing on rather than retrying in a loop.
Reading properties
akgl_get_property(name, dest, def) reads one into a pooled string. Absence is not an
error: an unset property yields def, which is how every caller inside the library gets a
working default without checking first.
Three details, all of which have caused a defect here before:
*destdecides who allocates.NULLclaims a slot from the string pool for you; non-NULLis written in place. Either way you release it withakgl_heap_release_string. Initialize your local toNULLon the first call.defis effectively required. ANULLdefault on an unset property isAKERR_NULLPOINTER, not an empty result.- Only the value and its terminator are copied. The rest of the destination keeps
whatever the previous holder left, so read the result as a C string and never as
AKGL_MAX_STRING_LENGTHbytes. A value ofAKGL_MAX_STRING_LENGTHbytes or more isAKERR_OUTOFBOUNDS; it will not fit anakgl_Stringwith its terminator.
The full worked example — claim, read, parse, release in CLEANUP — is in
Chapter 5, because the ownership rule is the string pool's rather than the
registry's.
Where to go next
- Chapter 5 — the pools the registries point into, and who releases what.
- Chapter 7 — where in startup the registries are created, and where configuration has to be in place by.
- Chapter 14 — choosing a backend, and what the
physics.*properties do. - Chapter 23 — the configuration table again, alongside every
AKGL_MAX_*.