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>
8.5 KiB
01. Introduction
libakgl is a C library for building 2D games on SDL3. This is version 0.8.0. It ships
193 functions declared across the twenty-three public headers in include/akgl/, plus
akgl_version() from the generated version.h — 194 exported akgl_ symbols in
libakgl.so.0.8.
0.8.0 is an ABI break. The collide slot on akgl_PhysicsBackend changed signature,
four public structs grew, akgl_physics_arcade_collide stopped raising AKERR_API, and
four symbols were removed — akgl_rectangle_points, akgl_collide_point_rectangle and the
two types they used (Chapter 19). Collision is the reason for all of it;
it is Chapter 15.
It gives you object pools instead of malloc, name-based registries instead of pointer
plumbing, a per-frame tick that updates and draws every live actor, JSON asset formats for
sprites and characters, a Tiled map loader, arcade physics, a controller/keyboard input
layer, text, and a three-voice tone synthesizer.
What it refuses to be
It is not an engine. There is no editor, no scene graph, no scripting layer, no asset
pipeline that runs before the compiler, and no runtime that owns your main. You write a C
program; libakgl is a library it calls.
The refusals are specific, and each one is a design decision documented in Chapter 2:
| Not here | Instead |
|---|---|
| Inheritance, RTTI, dynamic dispatch on a type tag | A record of function pointers you populate — akgl_RenderBackend, akgl_PhysicsBackend, akgl_Partitioner |
Runtime malloc |
Eight statically sized pools with reference counts, plus a static arena for the narrowphase; AKGL_ERR_HEAP when a pool is full |
| An editor or a project format | Tiled TMJ maps and hand-writable JSON for sprites and characters |
| A scripting layer | C. Behaviour attaches as function pointers on akgl_Actor |
| Two worlds at once | Four swappable globals: akgl_renderer, akgl_physics, akgl_camera, akgl_gamemap |
| Collision you cannot switch off | A collision pointer on the backend. NULL is byte-identical to a build before collision existed |
The library also does not own your window. akgl_render_2d_init creates one for you, and
akgl_render_2d_bind is the same job with that half removed, for a host that already has an
SDL_Renderer — see Chapter 8.
What a frame costs
PERFORMANCE.md measures a 640x480 game with a full screen of 16-pixel tiles and 64 actors,
software-rasterized. At 60 fps a frame is 16.67 ms:
| Part of the frame | Cost | Share of 16.67 ms |
|---|---|---|
| Logic: 64 actor updates + one physics sweep | 0.006 ms | 0.03% |
| Collision over 64 actors, all-pairs, as a control | 0.012 ms | 0.07% |
| Clear + present | 0.023 ms | 0.1% |
| 64 actor renders (48x48 blits) | 0.191 ms | 1.1% |
| 1200 tile blits | 16.26 ms | 97.6% |
| Six lines of HUD text | 0.076 ms | 0.5% |
libakgl's own per-tile overhead is about 0.03 ms per frame, under 0.2%. That number is
the gap between akgl_tilemap_draw at 16.26 ms and a raw SDL_RenderTexture loop issuing
the same 1200 blits from the same scattered source tiles at 16.23 ms. It covers the bounds
arithmetic, the tileset scan, the offset-table lookup, the backend indirection and the error
macros.
It does not cover the pixels, and the pixels are the frame. It does not tell you what
libakgl costs on a GPU backend, where the blits get cheap and libakgl's share of a much
shorter frame rises — PERFORMANCE.md says so explicitly and is the reason the
per-operation numbers there matter more than these totals. And it is one laptop, one build
type, one afternoon: treat the absolute numbers as that machine's and the ratios as the
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 issue in the tracker.
| Gap | Behaviour today | Chapter |
|---|---|---|
| Terminal velocity | Gravity accumulates into ey unbounded. physics.drag.y is the only brake |
14 |
| Friction / deceleration | Releasing a direction zeroes thrust and stops the actor dead. Right for Zelda, wrong for Mario | 14 |
| Savegames | akgl_game_save writes the name tables but not the objects, so a file is not yet enough to restore a session |
07 |
| Rotation | No actor carries an angle, and every collision shape is axis-aligned | 15 |
| Continuous collision | Sub-stepping is capped, so something moving faster than about 1280 px/s on 16-pixel tiles can still pass through a wall | 15 |
| Mesh drawing | akgl_render_2d_draw_mesh raises AKERR_API; the hook is reserved for a 3D backend |
08 |
| A text cache | Every akgl_text_rendertextat rasterizes, uploads, blits and destroys |
16 |
Who owns which documentation
This manual documents libakgl. It does not re-document its dependencies. Every project below is documented by the people who own its code, and a paraphrase here would be wrong the day one of them changes without anything in this repository noticing. So each chapter answers two questions — what does libakgl add or constrain here, and what does a libakgl caller actually write — and links out for the rest.
| Topic | Owned by | What this manual owes you |
|---|---|---|
ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH, PASS, CATCH, IGNORE |
libakerror (deps/libakerror) |
Which statuses libakgl raises and what they mean here — Chapter 4 |
aksl_strncpy, aksl_fclose, aksl_fgetc, aksl_snprintf |
libakstdlib (deps/libakstdlib) |
Which ones libakgl requires you to use, and why |
SDL_Renderer, SDL_Texture, events, SDL_PropertiesID |
SDL3 | The backend vtable, the frame contract, what libakgl does to the renderer's state |
| Image decoding | SDL3_image | Which formats reach a spritesheet, and when they are decoded |
Audio decoding, MIX_Audio, mixers and tracks |
SDL3_mixer | akgl_load_start_bgm and the track table — Chapter 18 |
| TTF rasterizing and metrics | SDL3_ttf | The font registry, the teardown ordering trap, the per-call cost — Chapter 17 |
json_t, json_decref, the parser |
jansson | akgl_get_json_* status semantics and the borrowed-reference rule — Chapter 19 |
| The TMJ map format, layers, tilesets, custom properties | Tiled | libakgl's extensions and limits — Chapter 13 |
The three-voice synthesizer in Chapter 18 is the one audio subsystem that is libakgl's own, and it is documented here in full. It has nothing to do with SDL3_mixer.
Where the per-function reference lives
This manual is narrative. It teaches a task and links to the generated Doxygen for
signatures, parameters and per-function @throws lists:
doxygen Doxyfile
Every header already carries a substantial @file block explaining its subsystem's design
rationale, and Doxyfile sets WARN_IF_UNDOCUMENTED = YES with
WARN_AS_ERROR = FAIL_ON_WARNINGS, so an undocumented symbol fails CI. The gap these
chapters fill is navigation and worked examples, not reference text. They deliberately do
not restate the 156 signatures — a hand-copied signature table is exactly the artifact
that drifts, and it would compete with a reference that CI already keeps honest.
Where a chapter genuinely needs a declaration or a constant table in front of you, it uses
an excerpt= block whose contents are checked against the header on every test run. The
text you read is the header.
Reading order
Chapter 4 comes before every subsystem chapter, because 181 of those 193
functions return akerr_ErrorContext AKERR_NOIGNORE * and you cannot read a single example
until you can read that return value. The dozen that do not are the void SDL enumeration
callbacks and the collision arena's accessors. After that, Chapter 3 gets a window on
the screen, and the subsystem chapters can be read in any order.
If you would rather start by building something, the two tutorials —
Chapter 20 and Chapter 21 — are
complete programs under examples/, built by default and smoke-run in CI.