Move outstanding work from TODO.md into the issue tracker
Some checks failed
libakgl CI Build / cmake_build (push) Successful in 9m7s
libakgl CI Build / performance (push) Successful in 9m44s
libakgl CI Build / mutation_test (push) Has been cancelled
libakgl CI Build / memory_check (push) Has been cancelled

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>
This commit is contained in:
2026-08-02 18:47:34 -04:00
parent bbb7b8f494
commit eabb9ad376
43 changed files with 850 additions and 2520 deletions

View File

@@ -68,7 +68,7 @@ library's.
## What is not implemented
Named here rather than discovered later. Each gets a callout in the chapter where you would
hit it, and an entry in `TODO.md`.
hit it, and an issue in the tracker.
| Gap | Behaviour today | Chapter |
|---|---|---|

View File

@@ -117,7 +117,7 @@ pkg-config --cflags --libs akgl akerror akstdlib sdl3 sdl3-ttf sdl3-mixer jansso
`sdl3-image` is needed at link time but appears in no public header, so it belongs in the
private half.
Recorded in `TODO.md` under "akgl.pc names no dependencies", along with why the fix wants
Recorded as issue #17, along with why the fix wants
its own commit: adding a `Requires:` line changes what `pkg-config --libs akgl` emits for
every existing consumer.

View File

@@ -173,7 +173,7 @@ So the context has exactly one owner at a time, and it is never both yours and i
returned releases the same slot twice, and a double-released context corrupts the failure
instead of reporting it. That is not hypothetical: it is how the first draft of the
`AKERR_OUTOFBOUNDS` test in `tests/json_helpers.c` passed against the unfixed library.
`AGENTS.md` documents the shape under "Testing Guidelines"; `TODO.md` item 18 records the
`AGENTS.md` documents the shape under "Testing Guidelines"; `TODO.md` records the
history.
The correct shape: take the result into a local, `NULL` your own pointer immediately, and

View File

@@ -105,7 +105,7 @@ 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
rather than a design (issue #14; also the `@warning` on
`heap.h` itself).
| Acquire | Takes the reference? | Who takes it |

View File

@@ -35,8 +35,8 @@ same `SDL_Texture`, and the texture is loaded once.
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` items
2426). The individual name registrations are not checked, either.
`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.
@@ -51,8 +51,7 @@ around.
Read against `src/registry.c` and `src/game.c`, this is what is true:
- `akgl_registry_init` creates **eight**, including `AKGL_REGISTRY_PROPERTIES`. The
properties call was added in 0.5.0 (`TODO.md`, "Known and still open" item 3, marked
fixed). Its order is spritesheet, sprite, character, actor, actor-state-strings, font,
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_init` never calls `akgl_registry_init`.** It calls the eight individual
initializers itself, in a different order: actor, sprite, spritesheet, character, font,
@@ -113,12 +112,11 @@ document. The fields:
**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 recorded in `TODO.md`, "Truncated registry keys can
collide", and it is not being fixed because the fix is a contract change — refusing an
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 `TODO.md` entry understates. It says the truncated name *is*
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:

View File

@@ -347,7 +347,7 @@ Three further things to know:
- **A save containing any registered spritesheet cannot be read back.** The writer uses
`AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH` (512) for that table and the reader used
`AKGL_ACTOR_MAX_NAME_LENGTH` (128) for all four. That is fixed as of 0.5.0 (`TODO.md`,
"Known and still open" item 7) — and what catches it is the `require_at_eof` check, which
"Defects") — and what catches it is the `require_at_eof` check, which
turns a field-width disagreement into `AKERR_IO` instead of a silent load with wrong maps.
A reader whose widths disagree finds a run of zeros mid-entry, stops early, and would
otherwise report success.

View File

@@ -187,7 +187,7 @@ land.
The first failure aborts the frame and propagates unchanged; there is no
draw-what-you-can behaviour.
> **Known defect (`TODO.md`, "Performance", item 6).** The layer loop always runs
> **Known defect (issue #26).** The layer loop always runs
> all 16 iterations and rescans all 64 actor slots on each — 1024 refcount checks
> per frame for a one-layer map. Invisible at 60 fps under the software renderer,
> measurable on a 2 ms GPU frame. Bounding the walk by `numlayers` and building

View File

@@ -295,14 +295,14 @@ game that wants one binds its own `changeframefunc`.
- **Truncated names are registry keys.** `akgl_Sprite::name` is 128 bytes and
`akgl_SpriteSheet::name` is 512. A longer name truncates *silently*, and two names
that truncate the same collide — the second registration replaces the first with no
error. Recorded in `TODO.md`, "Truncated registry keys can collide"; the fix is a
error. Recorded as issue #54; the fix is a
contract change, since the headers currently promise truncation.
- **The heap acquire functions are asymmetric.** `akgl_heap_next_string` increments
`refcount`; `next_sprite` and `next_spritesheet` do not. `TODO.md` item 8. See
`refcount`; `next_sprite` and `next_spritesheet` do not. Issue #14. See
[Chapter 05](05-the-heap.md).
- **No test asserts a clean sprite or spritesheet load/release cycle.** The tilemap
cycle is asserted over 64 iterations; the sprite, spritesheet and character ones
are not. `TODO.md`, "Targets", row 16.
are not. `TODO.md`, "Targets", row 16; issue #13.
- **`akgl_spritesheet_coords_for_frame` bounds-checks nothing.** See above.
## Where to look next

View File

@@ -310,7 +310,7 @@ character out from under a live actor leaves a dangling `basechar`.
pointer, while `akgl_heap_release_character` clears it under the character's own
truncated 128-byte copy. For a name over 127 bytes those differ, and the registry
entry survives the release — pointing at a zeroed slot. Latent for ordinary names;
related to `TODO.md`, "Truncated registry keys can collide", which covers the
related to issue #54, which covers the
collision half.
- **`speedtime` is written through an `int *` cast of a `uint64_t` field.**
`src/character.c` passes `(int *)&obj->speedtime` to

View File

@@ -331,7 +331,7 @@ backend accumulates gravity, so tapping down mid-jump stopped the character in t
air. Velocity was never theirs to clear either — `simulate` recomputes `v` as
`e + t` every step. Those notes predate the fix.
> **Known defect (`TODO.md`, "Arcade physics feel").** There is no friction and no
> **Known defect (issue #30).** There is no friction and no
> deceleration: zeroing `tx` on release **stops the actor dead**. Correct for Zelda,
> wrong for Mario. Bind your own `_off` handler that decays `tx` over time if you
> want momentum.
@@ -431,12 +431,12 @@ for `akgl_Actor`.
`akgl_actor_initialize` writes the registry entry under the *caller's* `name`
pointer while `akgl_heap_release_actor` clears it under the actor's truncated
128-byte copy. Identical for ordinary names; divergent past 127 bytes, and then
the registry keeps an entry pointing at a zeroed slot. Related to `TODO.md`,
"Truncated registry keys can collide".
the registry keeps an entry pointing at a zeroed slot. That is issue #38;
issue #54 covers the related truncation collision.
- **`akgl_actor_initialize` silently replaces a same-named actor**, and the one it
displaced becomes unreachable rather than being released.
- **No friction on release**, and **no terminal velocity** under gravity —
`TODO.md`, "Arcade physics feel". [Chapter 14](14-physics.md) covers both.
issues #29 through #32. [Chapter 14](14-physics.md) covers both.
## Where to look next

View File

@@ -362,7 +362,7 @@ pass per layer; see [Chapter 8](08-rendering.md).
## Known defects
Documented here because this is where a reader hits them. Each is cross-referenced to
`TODO.md`.
its issue.
**A failed load releases nothing.** `akgl_tilemap_load` zeroes the destination up front and
then loads physics, geometry, layers and tilesets in order. If any of that fails, the
@@ -375,8 +375,8 @@ leak them.
**`akgl_tilemap_release` does not release the actors an object layer created.** It destroys
tileset and image-layer textures and clears each pointer as it goes — that half is correct
as of 0.5.0, and a second release is safe rather than a use-after-free (`TODO.md`, "Known
and still open" item 2). The actors, and their references on characters and sprites, are
as of 0.5.0, and a second release is safe rather than a use-after-free (`TODO.md`,
"Defects"). The actors, and their references on characters and sprites, are
yours to release. The header's `@warning` describing a tileset double-free is stale; the
loop it describes was fixed and `tests/tilemap.c` releases the fixture three times and
asserts every texture pointer is `NULL`.

View File

@@ -177,7 +177,7 @@ cannot get in the way of that. The first version of that helper set `gravity_tim
`now - dt` and let the real clock supply the step; a machine busy enough to deschedule the
process between that store and the `SDL_GetTicksNS()` inside `simulate` produced a longer
step and a red suite, exactly once, under a parallel `ctest`. **That the engine cannot be
stepped exactly is itself a finding**, recorded in `TODO.md` under "Arcade physics feel".
stepped exactly is itself a finding**, recorded as issue #31.
Two more rules from that suite, both learned the hard way. `main` must call `SDL_Init` —
without it SDL sets its clock epoch on first use, `SDL_GetTicksNS()` returns something
@@ -332,7 +332,7 @@ this section used to carry.
Found by `tests/physics_sim.c`, which runs the arcade backend the way a game does — a
Mario-esque jump and fall, Zelda-style top-down walking, and a run reversed at full speed —
and prints what the actor actually did. Three other defects that suite found *are* fixed in
0.6.0. These four are not. All are in `TODO.md`, "Arcade physics feel".
0.6.0. These four are not. All four are issues #29 through #32.
| Gap | What you see | The workaround today |
|---|---|---|

View File

@@ -225,7 +225,7 @@ akgl_collision_world_init(&world, "bsp", 16.0f, 16.0f);
- **No rotation.** No actor carries an angle, `akgl_actor_render` hard-codes
`SDL_FLIP_NONE`, and every shape is axis-aligned. The support functions are written so that
adding it touches one static function in the narrowphase; see `TODO.md`.
adding it touches one static function in the narrowphase; see issue #62.
- **No restitution and no friction.** The default response blocks. Anything bouncier is your
`collidefunc`.
- **No continuous collision.** Sub-stepping bounds how far an actor travels between tests, and

View File

@@ -353,7 +353,7 @@ re-derives them from the header prose:
`akgl_controller_default` check `controlmapid < 0` as well as the upper bound and raise
`AKERR_OUTOFBOUNDS`. `tests/controller.c` passes `-1` and `-4096` to each. The
`@warning` blocks in `include/akgl/controller.h` saying only the upper bound is checked
are stale; `TODO.md`, "Known and still open" item 11.
are stale; `TODO.md`, "Defects", records the fix.
- **The `akgl_controller_handle_*` functions exist.** `controller.h` once declared four
names that were defined under different spellings, so they linked nowhere. Fixed in
0.5.0, and `scripts/check_api_surface.sh` runs as the `api_surface` test to stop that

View File

@@ -127,8 +127,8 @@ Measuring is 340 times cheaper than drawing, which tells you the whole cost is t
and the upload. **Six HUD readouts is 76 µs a frame. That is fine at 60 fps on a laptop and
it is 4% of a 2 ms GPU frame** — and it is being paid every frame for a score that changes
once a second. A one-line cache keyed on (font, string, colour) would take it to nothing;
`PERFORMANCE.md` calls it the single clearest optimisation in the library, and `TODO.md`
carries it as a target.
`PERFORMANCE.md` calls it the single clearest optimisation in the library, and it is
issue #22.
So: **fine for a HUD line, wrong for a static body of text redrawn every frame.** If you are
drawing a page of dialogue that does not change, rasterize it yourself once with SDL3_ttf,
@@ -209,7 +209,7 @@ guards, so drawing nothing still refuses everything drawing something refuses.
> refused, and `text.h:120-122` still warns that a failure after rasterizing leaks the
> surface and the texture. Both describe pre-0.5.0 behaviour. `src/text.c:110-112` returns
> success for `""`, and `src/text.c:140-146` destroys both objects in a `CLEANUP` block that
> runs on every path. `TODO.md` items 27 and 23 record both as fixed. The header comments
> runs on every path. `TODO.md` records both as fixed. The header comments
> want correcting in their own commit.
## Fonts, the pools, and what is not shared

View File

@@ -302,7 +302,7 @@ touches `akgl_mixer`, so calling only that leaves `akgl_load_start_bgm` handing
`src/assets.c:43` sets `MIX_PROP_PLAY_LOOPS_NUMBER` on it, and `src/assets.c:45` plays the
track with it. **0 is SDL's "no property set" sentinel**, not a set this function owns, so the
write is rejected — unchecked — and the play call is given no options. The music plays once
and stops, and `akgl_load_start_bgm` reports success either way. `TODO.md` item 19; the fix
and stops, and `akgl_load_start_bgm` reports success either way. Issue #16; the fix
is `SDL_CreateProperties()` into `bgmprops`, checked, and destroyed in `CLEANUP`.
**`AKGL_REGISTRY_MUSIC` exists and nothing populates it.** `akgl_registry_init_music()`

View File

@@ -161,8 +161,8 @@ Three things to know:
> **Stale defect note.** `util.h:124-128` warns that the fallback path "returns straight out
> of the ENOENT handler and so never releases the error context it was handling", costing one
> of libakerror's 128 context slots per call for the life of the process, and `TODO.md` item
> 15 records the same thing as open. **Both are out of date.** `src/util.c:115-129` sets a
> of libakerror's 128 context slots per call for the life of the process, and `TODO.md`
> recorded the same thing as open. **Both were out of date.** `src/util.c:115-129` sets a
> flag in the `HANDLE(errctx, ENOENT)` block and calls `path_relative_root` *after* `FINISH`,
> which is exactly the fix that entry proposes, and the code carries a comment explaining
> why. The leak is gone; the two notes describing it are not.
@@ -332,7 +332,7 @@ for an object member.
> `HANDLE_GROUP(e, AKERR_OUTOFBOUNDS)` arm, placed *above* the arm holding the `memcpy`
> because `HANDLE_GROUP` emits no `break` and every arm falls into that body, and
> `tests/json_helpers.c` covers it through both the integer and object index accessors.
> `TODO.md` item 18 records it as fixed. The header comment wants correcting in its own
> `TODO.md` records it as fixed. The header comment wants correcting in its own
> commit.
`defsize` is trusted, not derived: it is a `memcpy` through `void *` with no type

View File

@@ -412,7 +412,7 @@ window:
```
Without this line the first frame calls through a null function pointer. Making the library
pick a default is tracked in `TODO.md` under "Known and still open"; until it does, this call
pick a default is issue #36; until it does, this call
belongs in every libakgl program.
### The frame loop
@@ -524,7 +524,7 @@ Assign it right after `akgl_game_init()`:
```
The hook exists so a game can shed work when it is running slowly. This one has nothing to
shed. The first-second false positive is recorded in `TODO.md`.
shed. The first-second false positive is recorded in `TODO.md`, "Performance".
---
@@ -763,8 +763,7 @@ Add three custom properties to the **map itself** (Map → Map Properties):
| `physics.drag.y` | float | `1.5` | Air resistance on the vertical axis |
Gravity of 900 px/s² with a drag of 1.5 gives a terminal fall speed of 600 px/s. Drag is what
bounds a fall, so do not set it to zero. (A `terminal_velocity` setting is in `TODO.md` under
"Arcade physics feel".)
bounds a fall, so do not set it to zero. (A `terminal_velocity` setting is issue #29.)
Putting physics in the map rather than in code is what lets a swimming level and a walking
level differ by data.
@@ -816,7 +815,7 @@ Then the map:
**Load into `akgl_gamemap`, which already points at storage the library owns.** Do not
declare an `akgl_Tilemap` on the stack: it is about 26 MB, several times a default thread
stack, and you get a segfault before the loader writes a byte. Shrinking it is `TODO.md`
stack, and you get a segfault before the loader writes a byte. Shrinking it is issue #28
targets 14 and 15.
That one call creates an actor for every `actor` object in the object layer, binds each to
@@ -1200,7 +1199,7 @@ Snap to zero below a pixel per second, because an exponential decay never actual
The library's own `akgl_actor_cmhf_left_off` zeroes `tx` outright, which stops the actor dead
in one frame — right for a top-down game, wrong for a sidescroller. Friction and deceleration
in the backend are tracked in `TODO.md` under "Arcade physics feel"; when they land, this
in the backend are issues #29 through #32; when they land, this
whole section becomes a setting.
### The whole movement function, in order
@@ -1569,7 +1568,7 @@ else.
underneath the registry that still points at them.
Do not call `akgl_tilemap_release` unless you are loading a second level — it has a
double-free, recorded in `TODO.md` under "Known and still open" item 2. A process that is
double-free, recorded in `TODO.md` under "Defects". A process that is
exiting can leave the textures to `SDL_Quit`.
### Reporting a failure from `main`

View File

@@ -536,7 +536,7 @@ state is skipped rather than reported, so every NPC in the town disappears on fr
silently, and so does the player as soon as they stop walking.
Clearing the field leaves the facing bits wherever they were last set, which is what a
top-down game wants. `TODO.md` tracks making the default behave; until then this loop belongs
top-down game wants. Issue #39 tracks making the default behave; until then this loop belongs
in every game with a standing NPC in it.
The loop covers the player as well as the NPCs, which is what you want — a player who stops
@@ -748,7 +748,7 @@ The physics step writes a child's `x` and `y` as an absolute world position, and
`akgl_actor_render` adds the parent's position to it a second time. Detaching takes the
branch that does not add it, and `CLEANUP` puts the parent back on every path including the
failing one — an actor left with a `NULL` parent would be simulated as a free agent on the
next step. This is `TODO.md`, "Known and still open"; when the two agree on one reading, this
next step. This is issue #37; when the two agree on one reading, this
hook becomes `akgl_actor_render` again.
Run now. A second character walks a fixed distance behind and below the player.
@@ -892,7 +892,7 @@ Guard against the empty string:
```
`akgl_text_rendertextat` refuses a zero-width string while `akgl_text_measure` accepts one.
The disagreement is recorded in `TODO.md` under "Known and still open"; the check above is
The disagreement is issue #37; the check above is
the guard until it is settled.
### Drawing it over the world
@@ -1132,8 +1132,8 @@ than the `NULL` chapter 20 passes. `AKGL_ITERATOR_OP_UPDATE` asks for the update
registry and `SDL_Quit` destroys it, taking the last reference to every font with no way left
to close them.
`akgl_tilemap_release` is not called. It double-frees tileset textures, which is `TODO.md`
"Known and still open" item 2; `SDL_Quit` reclaims them correctly. A game that loads a second
`akgl_tilemap_release` is not called. It double-freed tileset textures, which is `TODO.md`
"Defects"; `SDL_Quit` reclaims them correctly. A game that loads a second
level has to unwind properly, and that is what the fix will make possible.
The pools are static storage and the process is exiting, so there is nothing else to free.

View File

@@ -301,7 +301,8 @@ A ninth child is `AKERR_OUTOFBOUNDS`; an over-long name is silently truncated.
`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).
truncating it into an index naming a different tile (`TODO.md`, "Found while rewriting
the Doxygen comments").
**`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
@@ -335,8 +336,8 @@ Three corrections to what those comments say, all verified against `src/tilemap.
- **`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.
`AKGL_TILEMAP_MAX_TILESETS`. Both landed in 0.5.0 (`TODO.md`, "Found while rewriting
the Doxygen comments"), and the header comment was not updated with them -- issue #63.
- **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

View File

@@ -58,7 +58,7 @@ that is not C in any dialect, and, in the first snippet a reader ever saw, the e
chapters turned up **twenty-seven** header claims that were false against `src/`.
Where a chapter documents behaviour that is a known defect rather than a design decision,
it says so and points at `TODO.md`. See `MAINTENANCE.md` if you are editing an example.
it says so and points at its issue. See `MAINTENANCE.md` if you are editing an example.
## Assets