Files
libakgl/plan.md
Andrew Kesterson 15ac5d5dd0
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 19s
libakgl CI Build / performance (push) Failing after 19s
libakgl CI Build / memory_check (push) Failing after 15s
libakgl CI Build / mutation_test (push) Failing after 17s
Record what writing the manual found: 18 defects and 27 false claims
Twenty-one chapters and two games were written against src/ rather than against
the header comments, and the exercise turned up two classes of problem. Both are
recorded here because publishing a problem you cannot fix yet is a contribution,
and because the second class is the more dangerous one: every item in it was
documented, in a header, incorrectly.

The one to fix first is defect 1. akgl_game_update calls
akgl_physics->simulate() with no NULL check and akgl_default_physics is zeroed
BSS, so a program that never calls an initializer segfaults on its first frame
-- measured, exit 139, not an AKERR_NULLPOINTER. physics.h tells the reader
akgl_game_init selects a backend from a `physics.engine` property, which is
false in both halves, so a caller who believes the header writes exactly the
program that crashes. That is the worst first-contact experience in the library
and the fix is a NULL check.

Defect 2 is the subtlest. A child actor's offset is counted twice: physics.c
writes x as parent->x + vx, an absolute coordinate, and actor.c then draws at
parent->x + obj->x while actor_visible three lines above treats obj->x as
absolute. Two readings of one field inside one function, and actor.h documents
both of them without noticing. Confirmed by rendering the frame with and
without the guard and hashing the readback.

The rest run from silent invisibility (an actor on layer >= 16 is updated and
simulated and never drawn; a map-spawned actor has no facing bit and so no
sprite) through dead API (speedtime is loaded, mis-cast, and read by nothing;
there is no way to play a sound effect from a file at all) to asset provenance:
tests/assets/World_A1.png and util/assets/Actor1.png carry RPG Maker's default
filenames with no licence file, and util/assets/littleguy.json -- the sample
data for the one demo the library ships -- does not load.

The second section lists the 27 header comments that describe code that has
since changed. Nothing catches these: WARN_IF_UNDOCUMENTED proves a symbol has
a comment, not that the comment is true, and check_api_surface.sh strips
comments precisely because prose is not a declaration. Doxygen publishes them.

A third section marks entries in this file that are themselves stale, including
item 15, which describes a leak that src/util.c already fixes with exactly the
technique the item proposes. AGENTS.md warns that this file "carried eleven
entries describing code that had already changed"; that is still accumulating,
and a premise nobody has re-checked is where the next defect is hiding.

plan.md is the plan the work was executed from, kept because it records the
decisions and, in three places, corrections to itself -- including getting the
error-code count wrong in the document whose purpose was fixing wrong
documentation.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:59:20 -04:00

33 KiB
Raw Blame History

Write the libakgl manual

Context

libakgl 0.7.0 exports 156 akgl_* functions across 20 public headers, and the only user-facing documentation is a 404-line README.md written as a FAQ. Half of it is developer process (git hooks, mutation testing, perf suites, memcheck) rather than anything a person building a game needs, and the half that is user-facing does not compile:

  • PASS(e, akgl_heap_next_spritesheet(&sheet); — unbalanced parentheses, twice.
  • sprite->frameids = [0, 1, 2, 3]; — not C in any dialect.
  • myactor->state = 9AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT); — a stray 9.
  • strncpy((char *)&game.name, "sdl3-gametest", 256); — the exact call AGENTS.md forbids under Copying Into Fixed-Width Fields, in the first snippet a reader sees.
  • int screenwidth = NULL;

The prose has drifted the same way, and two claims were confirmed false against src/:

  • physics.h:195 says akgl_game_init passes the physics.engine property to akgl_physics_factory. akgl_game_init never calls the factory, and the string physics.engine appears nowhere in src/.
  • registry.h:54 says akgl_registry_init creates the registries. akgl_game_init calls the eight individual initializers and never calls akgl_registry_init at all.

util/assets/littleguy.json — the sample data for the one shipped demo — still uses the pre-prefix state names ("ACTOR_STATE_ALIVE") and velocity_x, neither of which the current loader accepts.

This is not a reader routing around typos. It is what happens to samples and prose that nothing executes, and AGENTS.md already makes the argument in another context — "A test that has not failed has not been tested", "Do not trust a comment, a TODO entry, or a CI exclusion that states a premise." The sibling akbasic repository solved exactly this: every example in its manual is compiled or run by CTest, and a chapter that drifts from the code turns a job red.

The outcome wanted: a docs/ manual — introduction, design philosophy, a chapter per subsystem, and two tutorials building a complete 2D sidescroller and a complete top-down JRPG — with akbasic's harness ported so no sample in it can rot.

Decisions taken

Question Answer
Tutorial assets Vendor a curated Kenney.nl CC0 subset, with LICENSE, a provenance manifest, and a refresh script
Tutorial code Real compiling, runnable targets under examples/, built by default and in CI
Existing README.md Split: the FAQ half seeds docs/ (corrected); the developer-process half stays
Known defects Documented inline in the owning chapter, cross-referenced to TODO.md

The governing editorial rule: reference upstream, do not restate it

This manual documents libakgl. It does not re-document its dependencies.

libakgl sits on libakerror, libakstdlib, SDL3, SDL3_image, SDL3_mixer, SDL3_ttf, jansson and the Tiled map format. Every one of those is documented by its own project, by people who own the code. A chapter that restates the ATTEMPT/CLEANUP/PROCESS/HANDLE/ FINISH protocol is a chapter that will be wrong the day libakerror changes it, and nothing in this repository's test suite would notice — the drift this whole project exists to fix, reintroduced from a different direction.

So each chapter answers exactly two questions and links out for the rest:

  1. What does libakgl add or constrain here?
  2. What does a libakgl caller actually write? — a small, harness-verified example.
Topic Owned upstream — link, do not restate What this manual owes the reader
ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH, PASS, CATCH, IGNORE deps/libakerror The status-code tables below; akgl_error_init() ordering; libakgl's own hazards
aksl_strncpy, aksl_fclose, aksl_atoi and friends deps/libakstdlib Which ones libakgl requires you to use, and why (AGENTS.md already argues it)
SDL_Renderer, SDL_Texture, events, SDL_PropertiesID SDL3 wiki The backend vtable, the frame contract, what libakgl does to the renderer's state
Audio decoding, MIX_Audio, mixers and tracks SDL3_mixer akgl_load_start_bgm, the track table, the separate three-voice synthesizer (ours)
TTF rasterizing and metrics SDL3_ttf The font registry, the teardown ordering trap, the rasterize-per-call cost
The TMJ map format, layers, tilesets, custom properties Tiled documentation libakgl's extensions and limits — actor objects, physics.model, the perspective band, AKGL_TILEMAP_MAX_*
json_t, json_decref, the jansson API jansson manual akgl_get_json_* — which status means "absent" vs "wrong type", and the borrowed-reference rule

Chapter 04 is the sharpest case. It does not teach the error protocol; it says "the protocol is libakerror's and is documented there", then spends its length on the three things that are genuinely libakgl's: the status-code tables, the libakgl-specific traps, and one worked example of a real libakgl call sequence.

One more decision, forced by what the exploration found

docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block explaining the subsystem's design rationale, and Doxyfile sets WARN_IF_UNDOCUMENTED = YES with WARN_AS_ERROR = FAIL_ON_WARNINGS, so an undocumented symbol already fails CI. The gap is navigation and worked examples, not reference text.

So the chapters teach a task and link to the generated Doxygen for per-function detail. They do not restate 156 signatures — a hand-copied signature table is exactly the artifact that drifts, and it would compete with a reference CI already keeps honest. Where a chapter genuinely needs a declaration or a constant table in front of the reader, it uses a ```c excerpt=include/akgl/heap.h block, so the text is the header.


The error-code tables

These are the exception to the rule above, and the reason the exception exists is concrete: libakerror documents the mechanism, but only libakgl can say which statuses its own 156 functions raise and what they mean here. A caller writing a HANDLE block needs that, and it is written down nowhere today. These tables are a required deliverable, not a nice-to-have.

Three tables, in chapter 04, with the appendix carrying the full cross-reference.

Corrected during execution. The tables below were written from header prose and were wrong in five places; the chapter as built carries the verified version. Recorded here because being wrong about the error codes, in the plan that exists to fix wrong documentation, is the joke writing itself.

  • AKGL_ERR_LIMIT is not a status code. It is the one-past-the-end sentinel used to compute AKGL_ERR_COUNT (261). There are five codes, not six: AKGL_ERR_COUNT is 5, akgl_error_init names five, and tests/error.c asserts five.
  • AKGL_ERR_REGISTRY is not raised by akgl_actor_set_character. One raise site in the library: src/controller.c:491. akgl_actor_initialize raises AKERR_KEY on a failed registry write; akgl_actor_set_character raises AKERR_NULLPOINTER.
  • AKGL_ERR_BEHAVIOR is never raised by the library at all — only by tests/.
  • akgl_get_json_with_default does default on AKERR_OUTOFBOUNDS (src/json_helpers.c:208, fixed in 0.5.0). The status it deliberately does not default on is AKERR_TYPE — a missing key can take a default, a malformed one cannot.
  • Three statuses were missing: AKERR_TYPE (11 raise sites), AKERR_VALUE (8), AKERR_RELATIONSHIP (1). And AKERR_INDEX is never raised — it appears only as a HANDLE_GROUP arm.

Table 1 — libakgl's own status codes

All six are offsets from AKERR_FIRST_CONSUMER_STATUS, declared in include/akgl/error.h under owner string AKGL_ERR_OWNER ("libakgl"). The chapter renders this as an aligned table; the values come in as an excerpt=include/akgl/error.h block so the constants cannot drift from it.

Code Value Means Typically raised by What the caller does
AKGL_ERR_SDL base + 0 An SDL call failed; the message carries SDL_GetError() Anything touching a window, texture, renderer or mixer Usually fatal at startup; check the driver and the asset path
AKGL_ERR_REGISTRY base + 1 A name lookup or registration failed akgl_actor_set_character, registry writes Check the name and that the asset was loaded before the thing referencing it
AKGL_ERR_HEAP base + 2 A pool is exhausted every akgl_heap_next_* Normally a missing release, not a small pool. Raise the AKGL_MAX_HEAP_* override only after checking
AKGL_ERR_BEHAVIOR base + 3 A call was made in a state that forbids it lifecycle and ordering violations Fix the call order; see the startup sequence in chapter 07
AKGL_ERR_LOGICINTERRUPT base + 4 Not a failure — a control signal. "Skip the rest of this tick for this actor" your own movementlogicfunc Raise it deliberately; akgl_physics_simulate swallows it. A backend's gravity/move must never raise it — there it aborts the whole step
AKGL_ERR_LIMIT base + 5 A fixed compile-time bound was exceeded loaders hitting AKGL_*_MAX_* Reduce the asset, or raise the bound and rebuild everything linking libakgl

Table 2 — libakerror statuses libakgl raises, and what they mean here

The statuses themselves are libakerror's; their libakgl meaning is not documented anywhere. This table is what lets a caller write a HANDLE block with confidence.

Status What it means when a libakgl function raises it
AKERR_NULLPOINTER A required pointer argument was NULL, or a required field (akgl_game.name/.version/.uri) was empty. Also what akgl_text_rendertextat raises for an empty string — while akgl_text_measure accepts one
AKERR_KEY A key is absent. The idiomatic "optional thing was not there" status: a missing JSON key, a character with no sprite for a state, a map with no properties. Frequently a HANDLE block rather than a failure
AKERR_INDEX An array index was out of range
AKERR_OUTOFBOUNDS A value did not fit its destination — aksl_strncpy truncation, a frame id past uint8_t, a flood fill past AKGL_DRAW_MAX_FLOOD_SPANS. Note: akgl_get_json_with_default does not default on this one, which is why it cannot currently give an array element a default
AKERR_IO A read or write failed. Distinct from AKERR_EOF — that separation is the whole reason aksl_fgetc exists
AKERR_EOF End of input. Sometimes the desired outcome, as in require_at_eof in src/game.c
AKERR_API The function is not implemented. Currently akgl_physics_arcade_collide and akgl_render_2d_draw_mesh — both reached by ordinary-looking calls, so both get a chapter callout

Table 3 — the exit-status trap

Not a status list; a table because the failure is arithmetic and silent.

You write Wait status the shell sees Why
exit(AKGL_ERR_SDL) 0 — a clean run An exit status is one byte; libakgl's band starts at 256
akerr_exit(status) 0→0, 1255→itself, else 125 AKERR_EXIT_STATUS_UNREPRESENTABLE

Every suite in tests/ once reported success on the most common failure a library built on SDL can have. This belongs in the manual because a reader writing their own main will write the first line.

libakgl-specific traps that are ours to document

Not the protocol — the places libakgl makes the protocol bite in a way libakerror's docs cannot anticipate:

  • akgl_error_init() must run before anything that can raise, or every libakgl error prints as "Unknown Error". akgl_game_init does it first; a host with its own startup path must too.
  • akgl_registry_iterate_actor is an SDL callback returning void that ends in FINISH_NORETURN, so libakerror's default unhandled-error handler exits the process. A reader meets this the first time a sprite name is wrong.
  • akgl_get_json_with_default hands back the context it was given when it does not handle the status, so a CLEANUP that also releases it double-releases and corrupts the failure instead of reporting it. AGENTS.md documents the correct shape; the chapter shows it as a verified example because a reader will hit it writing their first loader.
  • libakerror ≥ 2.0.1 is a hard floor, enforced by two #error feature tests in include/akgl/error.h.
  • akgl.pc names no dependencies at all — no Requires: — despite akerror.h being part of libakgl's public interface. Chapter 03 says so and gives the flags to add by hand. (TODO.md.)

Part 1 — The verification harness

Ported from akbasic/tests/docs_examples.sh (550 lines, bash + awk, no third-party deps). The c / excerpt / sh / output / norun / text machinery is generic and transfers directly; what drops out is everything about running an interpreter, and what gets added is the ability to link and run, because libakgl's samples are C rather than BASIC.

New file: tests/docs_examples.sh. Same contract, and these house conventions are worth copying verbatim rather than reinventing:

  • Exit status is the number of failed examples; 2 for usage/setup errors.
  • A fence with no info string is a hard error, and so is an unknown one. The failure mode the whole harness exists to prevent is passing because it quietly ran nothing.
  • Paths forced absolute, because the script cds into sandboxes.
  • An unreadable named document is exit 2 — akbasic learned this when an empty generator expression contributed an empty argument that replaced the entire document list, and the suite passed having checked nothing.
  • A closing census line every run, treated as part of the result.
  • FAIL <doc>:<line>: message on stderr; output mismatches diffed through cat -A, because the bugs it catches are trailing spaces and missing newlines.

Block kinds

Info string What happens
```c cc -fsyntax-only -std=gnu99 -Wall -Werror with the project include path
```c wrap=NAME The same, wrapped in tests/docs_preludes/NAME.pre / .post
```c run=NAME New. Compile, link against akgl, run headless; stdout compared to a following ```output block
```c excerpt=PATH Must appear verbatim in PATH (comment- and whitespace-insensitive); not compiled
```c screenshot=NAME New. Linked against the figure host; renders docs/images/NAME.png
```json kind=KIND New. Loaded through the real loader: sprite, character, tilemap, properties
```output Claimed by the preceding runnable block; an unclaimed one is a failure
```sh / sh norun Run in a sandbox and must exit 0 / shown only
```cmake , ```text , norun Never executed; counted as skipped, so the decision is explicit

-Werror on snippets, unlike the library: AGENTS.md keeps AKGL_WERROR off by default precisely because libakgl is consumed with add_subdirectory and a new compiler's diagnostic should not break someone else's build. A doc snippet is not a consumer, and a sample that warns is a sample that teaches the warning. gnu99 rather than c99, for akbasic's reason — akerror.h uses PATH_MAX, which <limits.h> hides under __STRICT_ANSI__.

#line directives are emitted before each body, so a diagnostic reads docs/14-physics.md:112: error: ... rather than pointing into a scratch file.

Three additions akbasic does not have

c run=NAME is the reason to do this at all for a game library. -fsyntax-only proves a call typechecks; it does not prove the startup order works, that a sprite loads, or that an ATTEMPT block releases what it acquired. A run block links a real binary and executes it under SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy SDL_RENDER_DRIVER=software — the same forcing scripts/memcheck.sh already uses. It asserts a clean exit and greps stderr for a raised context, because a libakgl program can fail and still exit 0.

json kind= exists because the asset formats are documented in prose and read by src/sprite.c, src/character.c, src/tilemap.c and src/registry.c, with nothing tying the two together. That gap is not hypothetical: util/assets/littleguy.json is already invalid against the loader it ships with. A small tools/docs_checkjson.c writes the block to a temp file and calls the matching akgl_*_load_json, so the documented format is the format the loader accepts.

c screenshot=NAMEtools/docs_screenshot.c supplies main, brings the library up headless at a stated size, calls the snippet's docs_frame(void), then SDL_RenderReadPixels and writes the PNG via SDL3_image. Copy two of akbasic's decisions exactly: any stdout during a render is a failure (an image of a blank screen is worse than no image), and --check compares and never repairs (a test that fixes what it measures passes the second time for the wrong reason).

Regeneration is cmake --build build --target docs_screenshots, never part of all. Figures are tracked, and docs/images/README.md says they are generated — the same tracked-generated-artifact contract AGENTS.md already spells out for SDL_GameControllerDB.h.

Support files and CMake wiring

tests/docs_examples.sh          the verifier
tests/docs_preludes/*.pre/.post akglbody, akglfile, akglapp, akglframe
tests/docs_setups/*.sh          asset fixtures a chapter should not have to show
tools/docs_checkjson.c          the json kind= validator
tools/docs_screenshot.c         the figure host
tools/docs_screenshots.sh       figure generation and --check
docs/images/                    tracked, generated

Include paths come from a file(GENERATE)'d docs_cflags.txt, so the transitive path through akerror, akstdlib, SDL3 and jansson has one source of truth. Register docs_examples and docs_screenshots with add_test, WORKING_DIRECTORY at the source root — and note CMakeLists.txt:76-102 shadows add_test only when top-level, so follow the file's existing idiom rather than calling the builtin.

Follow akbasic's CI decision: no docs-path filter. Documentation goes stale because the code moved, not because somebody edited a chapter.

A prelude must never let a wrong example compile. A prelude declaring akgl_game_init itself would defeat the check.


Part 2 — Chapter structure

docs/README.md is a hand-maintained two-column TOC, matching akbasic's conventions: NN-kebab-case.md, H1 repeating the ordinal, no front matter, H2/H3 only (no H4), relative links between chapters.

01-introduction.md What libakgl is, what it refuses to be, the 0.03 ms frame budget from PERFORMANCE.md, and the dependency map — who owns which documentation
02-design-philosophy.md Pools not malloc, backends not branches, errors that carry context, bit flags, name-based registries
03-getting-started.md add_subdirectory vs akgl.pc (and the missing Requires:), the first window
04-errors.md The three status tables above. Protocol referenced, not restated; libakgl's own traps; one worked call sequence
05-the-heap.md The five pools, akgl_heap_next_*, akgl_String, the refcount asymmetry, AKGL_MAX_HEAP_* as an ABI constraint
06-the-registry.md The eight registries, configuration properties, the id-0 silent-no-op, key truncation
07-the-game-and-the-frame.md Startup order, akgl_game_update, the state lock, iterators, what savegames do and do not do
08-rendering.md akgl_RenderBackend, the frame contract, cameras, and embedding via akgl_render_2d_bind
09-drawing.md draw.h primitives, colour-as-argument, regions, flood fill's reentrancy limit
10-spritesheets-and-sprites.md Sheet sharing by resolved path, the sprite JSON format, animation
11-characters.md State→sprite mappings, speeds and accelerations, the character JSON format
12-actors.md The 32-bit state mask, the six behaviour hooks, parents and children, layers
13-tilemaps.md libakgl's Tiled extensions and limits — actor objects, physics.model, the perspective band. Format itself referenced
14-physics.md thrust/environmental/velocity, null and arcade, what is not implemented
15-input.md Control maps, push-not-poll dispatch, the keystroke ring, the gamepad DB
16-text-and-fonts.md The font registry, measuring, the teardown ordering trap
17-audio.md The three-voice synthesizer (ours, in full), and the SDL_mixer asset path (referenced)
18-utilities.md Collision helpers, path resolution, akgl_get_json_* status semantics, static strings
19-tutorial-sidescroller.md The first game, start to finish
20-tutorial-jrpg.md The second game, start to finish
21-appendix-limits.md Status cross-reference (which functions raise what), every AKGL_MAX_*, the full configuration property table

Chapter 04 precedes every subsystem because all 156 functions return akerr_ErrorContext AKERR_NOIGNORE *, and a reader who has not met the tables cannot read a single example. Chapter 08 carries the embedding seam as a first-class topic rather than a footnote — akgl_render_2d_bind is what akbasic actually consumes.

Constant tables (AKGL_MAX_HEAP_*, the actor state bits, the iterator ops, the status codes) go in as excerpt= blocks against their headers. Those headers hand-align their bit-flag tables, and scripts/reindent.el deliberately avoids tabify to preserve that alignment — an excerpt keeps the alignment and the values honest at once.

README.md keeps its developer-process half and gains a link to docs/. Its FAQ half is deleted, not copied: corrected content lives in the chapters, and one source of truth per topic is the entire point of the exercise.


Part 3 — The two tutorial games

Real targets: examples/sidescroller/ and examples/jrpg/, each a complete program, built by default and exercised in CI by a headless smoke run (run N frames, exit 0), so a tutorial cannot silently stop working.

The tutorial chapters quote these programs with ```c excerpt=examples/... blocks rather than restating the code. A chapter then cannot drift from a program that builds — the excerpt check fails the moment the source moves.

examples/sidescroller/ — gravity, a jump, platforms from a Tiled map, a coin pickup, a hazard. Exercises arcade physics, physics.gravity.y, AKGL_ACTOR_STATE_MOVING_*, tilemap layers, and a custom movementlogicfunc (which is where AKGL_ERR_LOGICINTERRUPT stops being a table row and becomes something the reader writes).

This game forces the chapter to make three honest statements, all from TODO.md:

  • akgl_physics_arcade_collide is not implemented — it raises AKERR_API, and akgl_physics_simulate never calls collide at all. arcade_move does no clamping and consults no tilemap: an actor walks through a wall and off the edge of the world. The tutorial implements its own collision in movementlogicfunc and says exactly why.
  • There is no terminal velocity. Gravity accumulates into ey unbounded — the physics sim reaches 560 px/s in 0.7 s and keeps going. physics.drag.y is the only brake (ey approaches gravity_y / drag_y) and is not documented as such anywhere today.
  • Releasing a direction stops the actor dead. akgl_actor_cmhf_*_off zeroes tx/ty and there is no friction or deceleration. Correct for Zelda, wrong for Mario; the chapter shows the workaround rather than pretending.

examples/jrpg/ — four-direction walking with per-facing animation, a Tiled town map, NPCs spawned from map objects, a text box, and a party member as a child actor. Exercises null-gravity arcade physics, the full four-way state mask, characters with per-facing sprite mappings, actors auto-created from object layers, akgl_text_*, parent/child actors (children are snapped to the parent and never simulated — a documented behaviour this game depends on).

The two are deliberately complementary: the sidescroller is the physics tutorial, the JRPG is the content-pipeline tutorial.


Part 4 — Assets

docs/tutorials/assets/ holds a curated CC0 subset from Kenney.nl, with:

  • LICENSE — the CC0 deed text.
  • PROVENANCE.md — an aligned table, one row per file: which pack, the source URL, and what it was cropped or repacked into.
  • scripts/fetch_tutorial_assets.sh — refreshes from upstream into a temp directory and moves into place only after verifying the fetch and sanity-checking the contents. This is exactly the shape mkcontrollermappings.sh was fixed into for 0.5.0: check curl's status, check a plausible minimum, never overwrite a good tracked copy from a failed run, exit non-zero. Do not reintroduce the version that silently destroyed its own fallback.

CC0 specifically, not merely "free": a reader who copies a tutorial into their own game inherits whatever obligation the assets carry, and CC0 carries none.

Asset contract, fixed up front so the art and the game code can be built in parallel: 16×16 tiles, 32×32 character frames, sheets counted left-to-right from the top-left as akgl_spritesheet_initialize expects, at most AKGL_SPRITE_MAX_FRAMES (16) frames per animation with frame ids that fit a uint8_t. Maps are Tiled TMJ with embedded tilesets, under AKGL_TILEMAP_MAX_LAYERS (16) and AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER (128). Actor objects use "type":"actor" with a character string property and a state int property; the string-array form works in character JSON and is not accepted here.

Corrected during execution. This contract originally said external tileset references, quoting README.md: "The engine ONLY supports TilED TMJ tilemaps with tileset external references." That is backwards, and it is one more stale claim of the kind this project exists to fix. "source" appears nowhere in src/tilemap.c; nothing in the library ever opens a .tsj. akgl_tilemap_load_tilesets_each (src/tilemap.c:139-159) reads columns, firstgid, tilecount and image directly off each element of the map's tilesets array, and tests/assets/testmap.tmj is embedded. An external stub fails with AKERR_KEY on the missing columns.

Four further constraints verified against the loader, all of which bind the tutorial maps:

  • Every object in an object group needs a type string, plain rectangles included. A missing type fails the whole load.
  • Perspective markers need "type": "perspective", not merely the name p_foreground/p_vanishing. The loader checks the type first and silently ignores the object otherwise.
  • A literal 512×512 map is rejected. Both bounds use >=, so 262,144 cells is one too many; 512×511 is the largest that loads.
  • Tileset images resolve through akgl_path_relative (canonicalized, absolute paths work) but image-layer images resolve through a plain "%s/%s" join (absolute paths do not). Keep every image path relative to the map file.

A defect found while planning

tests/assets/World_A1.png and util/assets/Actor1.png carry the default filenames of RPG Maker's bundled art and have no license file, while tests/assets/akgl_test_mono.ttf sits beside akgl_test_mono.LICENSE.txt. RPG Maker's bundled assets are licensed to users of that product; redistributing them inside a C library is not something that license covers.

This plan does not fix it — replacing test fixtures is a separate change with its own blast radius, and it is not blocking the docs. It does two things: the tutorial assets get a clean provenance story that does not depend on those files, and the finding goes into TODO.md with file, functional consequence and blast radius, per the house practice of documenting defects against yourself.


Part 5 — How the work is split across subagents

Wave 1 runs entirely in parallel. Nothing waits on docs/ existing, because the asset contract, the block grammar and the status tables are all pinned above — that is what lets the tutorial work start immediately rather than queueing behind the manual.

Agent Deliverable Depends on
A — harness tests/docs_examples.sh, preludes, setups, tools/docs_checkjson.c, the screenshot pair, CMake wiring, the harness spec section in README.md nothing
B — assets Kenney CC0 subset, LICENSE, PROVENANCE.md, fetch script, both Tiled maps, sprite/character JSON the asset contract
C — sidescroller examples/sidescroller/ + chapter 19 asset contract (B's bytes arrive later)
D — JRPG examples/jrpg/ + chapter 20 asset contract
E — core 04 errors (owns the three tables), 05 heap, 06 registry, 07 game/frame, 21 appendix nothing
F — presentation 08 rendering + embedding, 09 drawing, 10 sprites, 11 characters, 12 actors nothing
G — world 13 tilemaps, 14 physics, 15 input nothing
H — periphery + front matter 16 text, 17 audio, 18 utilities, 01 intro, 02 philosophy, 03 getting started nothing

Agent E's status tables are the one wave-1 artifact other agents consume, so E publishes them first, before writing prose. Every other chapter cites them rather than re-listing codes locally.

Every chapter agent gets the same five standing instructions, because the failure this project exists to fix is documentation asserting things nobody checked:

  1. Read src/, not the header prose. The physics.engine and akgl_registry_init claims are both false and have been quoted forward. "A premise nobody has re-checked is where the next defect is hiding."
  2. Reference upstream, do not restate it. If the sentence you are writing would still be true in a project that does not use libakgl, it belongs in a link. Cite the specific upstream document; do not paraphrase it.
  3. Every fence carries an info string. Prefer run=, then wrap=, then excerpt=; a norun block is a decision that has to be justified.
  4. Do not restate signatures — link the Doxygen reference, or excerpt= the header.
  5. Note the known defect where a reader would hit it, with the guard to apply, linked to TODO.md.

Wave 2 is the integration pass, and the only genuinely serial step: build the TOC, insert cross-references between chapters and both tutorials, verify no chapter re-teaches upstream material, reconcile terminology, cut the README's FAQ half, add the TODO.md entries, then run the full gate.

Verification

cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build --parallel
ctest --test-dir build --output-on-failure

Iterating on one chapter without a full run:

./tests/docs_examples.sh --root . --cflags-file build/docs_cflags.txt docs/14-physics.md

Specific things that must hold before this is done:

  • ctest -R docs_examples passes, and its census line accounts for every block. A rising norun count is the harness being talked out of its job, and is a review finding.
  • ctest -R docs_screenshots passes with no figure regenerated.
  • Every status code in include/akgl/error.h appears in Table 1, and every status the library actually raises appears in Table 2 — checked by grepping src/ for FAIL_* and AKERR_ status arguments, not by reading the headers.
  • Both example games build, and their headless smoke runs exit 0.
  • ctest -R api_surface and -R headers still pass — writing a subsystem chapter tends to turn up a symbol declared nowhere, which is precisely what api_surface is for.
  • doxygen Doxyfile is still clean; WARN_AS_ERROR = FAIL_ON_WARNINGS means a header touched while documenting must stay fully documented.
  • scripts/reindent.sh --check is clean — examples/ and tools/ are C sources in this tree and the pre-commit hook will reindent them.
  • scripts/memcheck.sh -R docs_examples — the run= blocks execute real library code and are a genuine new memory-check vehicle, which is the shape AGENTS.md asks for ("a new path worth checking belongs in a benchmark, where it gets both").
  • Every commit names the agent program, model and version as co-author, per AGENTS.md.

Out of scope, deliberately

  • Replacing the RPG-Maker-named test fixtures (recorded in TODO.md instead).
  • Fixing the false claims in physics.h and registry.h. The chapters document what the code does; correcting the header comments is a separate commit, since AGENTS.md requires style and behaviour changes to stay unbundled.
  • Implementing arcade_collide, terminal velocity, or friction. The tutorials work around them and say so.
  • Documenting libakerror, libakstdlib, SDL3, jansson or the Tiled format. Linked, not restated — see the governing editorial rule.