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
|
|
|
# 09. Drawing
|
|
|
|
|
|
|
|
|
|
`draw.h` is immediate-mode plotting: put this on the screen *now*, as opposed to
|
|
|
|
|
add this to the scene. It sits alongside the actor and tilemap rendering in
|
|
|
|
|
[Chapter 08](08-rendering.md) rather than replacing it — a game can draw a world
|
|
|
|
|
through `draw_world` and then scribble a debug rectangle over it in the same frame.
|
|
|
|
|
|
|
|
|
|
The shape comes from a BASIC-style graphics vocabulary. `DRAW`, `BOX`, `CIRCLE`,
|
|
|
|
|
`PAINT`, `SSHAPE` and `GSHAPE` all map onto one of these, which is why the set is
|
|
|
|
|
what it is and why there is a flood fill in a game library at all.
|
|
|
|
|
|
|
|
|
|
| Function | Draws | Notable |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `akgl_draw_point` | one pixel | clipping is SDL's, not reported |
|
|
|
|
|
| `akgl_draw_line` | a line, endpoints included | coincident endpoints draw one pixel |
|
|
|
|
|
| `akgl_draw_rect` | an outline | zero or negative w/h draws nothing, silently |
|
|
|
|
|
| `akgl_draw_filled_rect` | a solid box, outline included | same |
|
|
|
|
|
| `akgl_draw_circle` | an outline | midpoint algorithm, integer, no AA |
|
|
|
|
|
| `akgl_draw_flood_fill` | a connected region | CPU-side, bounded, **not reentrant** |
|
|
|
|
|
| `akgl_draw_copy_region` | *reads* a rectangle into a surface | allocates when you ask it to |
|
|
|
|
|
| `akgl_draw_paste_region` | a saved surface back onto the target | replaces, does not blend |
|
|
|
|
|
| `akgl_draw_background` | an 8x8 grey checkerboard | diagnostic, not a general primitive |
|
|
|
|
|
|
|
|
|
|
Signatures are in the generated Doxygen. Statuses are [Chapter 04](04-errors.md).
|
|
|
|
|
|
|
|
|
|
**These are ordinary functions, not backend methods.** Unlike `frame_start` and
|
|
|
|
|
`draw_world`, they are not slots on `akgl_RenderBackend` — they take an
|
|
|
|
|
`akgl_RenderBackend *` as their first argument and reach through its
|
|
|
|
|
`sdl_renderer`. Pass `akgl_renderer` unless you are deliberately drawing somewhere
|
|
|
|
|
else. Every one of them validates both `self` and `self->sdl_renderer` before
|
|
|
|
|
touching either.
|
|
|
|
|
|
|
|
|
|
## Colour is an argument, never a global
|
|
|
|
|
|
|
|
|
|
**Every entry point takes its colour as a parameter, and none of them reads or
|
|
|
|
|
writes a current-colour global.**
|
|
|
|
|
|
|
|
|
|
The reasoning is worth stating because it looks like extra typing. A caller that
|
|
|
|
|
has a notion of a current colour — a BASIC `COLOR` statement, a UI theme, a palette
|
|
|
|
|
index — *already owns that state*. Giving the library a second copy creates two
|
|
|
|
|
places for the answer to live and one opportunity for them to disagree. So there is
|
|
|
|
|
no `akgl_draw_set_color`, and there never will be.
|
|
|
|
|
|
|
|
|
|
The other half of the rule is the one that matters at a call site: **the renderer's
|
|
|
|
|
own draw colour is saved before each call and restored after it.** Drawing a red
|
|
|
|
|
line does not leave the renderer red. In particular, it does not change what the
|
|
|
|
|
next `SDL_RenderClear` — or the next `frame_start`, which clears — paints.
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
#include <akgl/draw.h>
|
|
|
|
|
#include <akgl/game.h>
|
|
|
|
|
|
|
|
|
|
/* Two rectangles, two colours, no state carried between them. */
|
|
|
|
|
akerr_ErrorContext *draw_health_bar(float32_t x, float32_t y, float32_t frac)
|
|
|
|
|
{
|
|
|
|
|
SDL_Color back = { 0x20, 0x20, 0x20, SDL_ALPHA_OPAQUE };
|
|
|
|
|
SDL_Color fill = { 0xd0, 0x30, 0x30, SDL_ALPHA_OPAQUE };
|
|
|
|
|
SDL_FRect rect;
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
rect.x = x;
|
|
|
|
|
rect.y = y;
|
|
|
|
|
rect.w = 100.0f;
|
|
|
|
|
rect.h = 8.0f;
|
|
|
|
|
PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &rect, back));
|
|
|
|
|
|
|
|
|
|
rect.w = 100.0f * frac;
|
|
|
|
|
PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &rect, fill));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Alpha is in the colour and blending follows the renderer's **current blend mode**,
|
|
|
|
|
which these functions do not change. If you want alpha to blend rather than
|
|
|
|
|
overwrite, set the blend mode yourself with
|
|
|
|
|
[`SDL_SetRenderDrawBlendMode`](https://wiki.libsdl.org/SDL3/SDL_SetRenderDrawBlendMode).
|
|
|
|
|
That one *is* renderer state libakgl leaves alone.
|
|
|
|
|
|
|
|
|
|
Restoring the colour happens in a `CLEANUP` block under `IGNORE()`. A failure to
|
|
|
|
|
restore is logged rather than propagated, on the grounds that it must not mask the
|
|
|
|
|
failure the cleanup is unwinding from.
|
|
|
|
|
|
|
|
|
|
## What the primitives look like
|
|
|
|
|
|
|
|
|
|
Every primitive in one frame, on a 320x240 surface. The picture below is generated by
|
|
|
|
|
running exactly this listing — see `MAINTENANCE.md` if you are editing it.
|
|
|
|
|
|
|
|
|
|
```c screenshot=primitives
|
|
|
|
|
SDL_Color red = { 0xd0, 0x30, 0x30, 0xff };
|
|
|
|
|
SDL_Color green = { 0x30, 0xc0, 0x50, 0xff };
|
|
|
|
|
SDL_Color blue = { 0x40, 0x70, 0xe0, 0xff };
|
|
|
|
|
SDL_Color yellow = { 0xe0, 0xc0, 0x40, 0xff };
|
|
|
|
|
SDL_FRect filled = { 20.0f, 20.0f, 80.0f, 50.0f };
|
|
|
|
|
SDL_FRect framed = { 120.0f, 20.0f, 80.0f, 50.0f };
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &filled, blue));
|
|
|
|
|
PASS(errctx, akgl_draw_rect(akgl_renderer, &framed, green));
|
|
|
|
|
PASS(errctx, akgl_draw_circle(akgl_renderer, 260.0f, 45.0f, 25.0f, yellow));
|
|
|
|
|
PASS(errctx, akgl_draw_line(akgl_renderer, 20.0f, 100.0f, 300.0f, 100.0f, red));
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A row of points, to show that a point is a pixel and not a dot of some
|
|
|
|
|
* nominal size. At this scale they read as a dashed line.
|
|
|
|
|
*/
|
|
|
|
|
for ( float32_t x = 20.0f; x < 300.0f; x += 8.0f ) {
|
|
|
|
|
PASS(errctx, akgl_draw_point(akgl_renderer, x, 115.0f, yellow));
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
The filled rectangle is `akgl_draw_filled_rect`; the outlined one beside it is
|
|
|
|
|
`akgl_draw_rect`. Neither left the renderer blue or green — the line drawn afterwards is
|
|
|
|
|
red because it asked to be, not because it inherited anything.
|
|
|
|
|
|
|
|
|
|
## Circles are integer midpoint circles
|
|
|
|
|
|
|
|
|
|
SDL3 has no circle primitive, so `akgl_draw_circle` plots one: the midpoint circle
|
|
|
|
|
algorithm, integer arithmetic throughout, eight-way symmetry — one computed point
|
|
|
|
|
in the second octant gives the other seven by reflection.
|
|
|
|
|
|
|
|
|
|
Three consequences you will see on screen:
|
|
|
|
|
|
|
|
|
|
- **The centre and the radius are rounded to whole pixels** (`SDL_lroundf`). A
|
|
|
|
|
circle at `x = 10.5` is a circle at `x = 11`. There is no sub-pixel placement.
|
|
|
|
|
- **The outline is one pixel wide and is not anti-aliased.** There is no
|
|
|
|
|
thickness parameter and no smoothing. If you want a thick or smooth circle, draw
|
|
|
|
|
concentric ones or blit a texture.
|
|
|
|
|
- **There is no filled circle.** Only the outline. A filled disc is a flood fill of
|
|
|
|
|
the interior, or a texture.
|
|
|
|
|
|
|
|
|
|
A radius of exactly 0 draws the centre pixel and nothing else. A negative radius is
|
|
|
|
|
`AKERR_OUTOFBOUNDS` and the message reports it.
|
|
|
|
|
|
|
|
|
|
A failure inside the plotting loop is recorded in a flag and reported once at the
|
|
|
|
|
end rather than aborting mid-circle — a `CATCH` there would `break` the loop rather
|
|
|
|
|
than leave the function, the hazard `AGENTS.md` describes. **So an
|
|
|
|
|
`AKGL_ERR_SDL` from `akgl_draw_circle` may leave a partial arc already on the
|
|
|
|
|
target.** The same is true of `akgl_draw_background`.
|
|
|
|
|
|
|
|
|
|
## Flood fill
|
|
|
|
|
|
|
|
|
|
`akgl_draw_flood_fill` is the odd one out, and it is worth understanding why before
|
|
|
|
|
using it. Every other primitive here is a command queued to the GPU. This one
|
|
|
|
|
cannot be: **the region it fills is defined by what is already on the screen**, so
|
|
|
|
|
it has to read the render target back, walk the region on the CPU, and blit the
|
|
|
|
|
result over the area it touched.
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
SDL_RenderReadPixels(target) the whole target comes back
|
|
|
|
|
|
|
|
|
|
|
SDL_ConvertSurface(RGBA32) fixed 32-bit layout, so the walk
|
|
|
|
|
| can compare and write whole words
|
|
|
|
|
flood_region() scanline fill over a bounded span
|
|
|
|
|
| stack; records a dirty bounding box
|
|
|
|
|
SDL_RenderTexture(dirty, dirty) only what changed goes back
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### What counts as the region
|
|
|
|
|
|
|
|
|
|
**Four-connected**: the fill spreads up, down, left and right, never diagonally. A
|
|
|
|
|
region joined only at a corner is two regions.
|
|
|
|
|
|
|
|
|
|
**The boundary is an exact colour comparison.** Any pixel whose packed RGBA value
|
|
|
|
|
differs from the seed's, by any amount, is a boundary. There is no tolerance
|
|
|
|
|
parameter.
|
|
|
|
|
|
|
|
|
|
That second one is the practical trap. **An anti-aliased edge stops the fill at its
|
|
|
|
|
first blended pixel and leaves a fringe** — you get the flat interior and a halo of
|
|
|
|
|
untouched blend pixels around it. Text rendered through SDL3_ttf, a scaled sprite,
|
|
|
|
|
and anything drawn with smoothing all have such edges. Flood fill wants hard-edged
|
|
|
|
|
art.
|
|
|
|
|
|
|
|
|
|
Filling a region that is already the requested colour is a **no-op that succeeds**,
|
|
|
|
|
not an error: walking it would compare filled pixels against themselves and find
|
|
|
|
|
nothing, so the function says so up front. A seed outside the render target is
|
|
|
|
|
`AKERR_OUTOFBOUNDS`, and the message reports both the seed and the target size.
|
|
|
|
|
|
|
|
|
|
### The span stack, and what it costs you
|
|
|
|
|
|
|
|
|
|
The fill keeps a fixed stack of horizontal runs still to be examined rather than
|
|
|
|
|
recursing per pixel:
|
|
|
|
|
|
|
|
|
|
```c excerpt=include/akgl/draw.h
|
|
|
|
|
#define AKGL_DRAW_MAX_FLOOD_SPANS 4096
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
At 4096 spans that array is 48 KB, which does not belong on the stack of a function
|
|
|
|
|
a game may call every frame — so it is **file scope**. Two consequences, and both
|
|
|
|
|
are hard rules rather than cautions:
|
|
|
|
|
|
|
|
|
|
- **`akgl_draw_flood_fill` is not reentrant.** Do not call it from inside a
|
|
|
|
|
callback that a flood fill can reach. There is one span stack for the process.
|
|
|
|
|
- **It is not thread-safe.** Nothing that touches an `SDL_Renderer` is, so this is
|
|
|
|
|
not a new restriction — but the span stack means it stays unsafe even against a
|
|
|
|
|
second renderer on a second thread.
|
|
|
|
|
|
|
|
|
|
An ordinary convex or moderately concave shape needs a few dozen pending spans. A
|
|
|
|
|
region complicated enough to need more than 4096 at once reports
|
|
|
|
|
`AKERR_OUTOFBOUNDS` — and **leaves the region partially filled**. There is no way
|
|
|
|
|
to unwind a partial fill short of keeping a copy of the whole surface, and you
|
|
|
|
|
asked for a bounded operation. Treat that status as "redraw the area", not as
|
|
|
|
|
"retry".
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
#include <akgl/draw.h>
|
|
|
|
|
#include <akgl/game.h>
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *paint_region(int x, int y, SDL_Color color)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, akgl_draw_flood_fill(akgl_renderer, x, y, color));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} HANDLE(errctx, AKERR_OUTOFBOUNDS) {
|
|
|
|
|
/*
|
|
|
|
|
* Either the seed was off-target, or the region needed more than
|
|
|
|
|
* AKGL_DRAW_MAX_FLOOD_SPANS pending spans -- in which case it is
|
|
|
|
|
* already PARTIALLY filled. Redraw the area; do not retry the fill.
|
|
|
|
|
*/
|
|
|
|
|
LOG_ERROR(errctx);
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The colour is **written over** the region rather than blended, since the pixels
|
|
|
|
|
going back are the ones just read out of it.
|
|
|
|
|
|
|
|
|
|
## Saving and restoring a region
|
|
|
|
|
|
|
|
|
|
`akgl_draw_copy_region` and `akgl_draw_paste_region` are `SSHAPE` and `GSHAPE`: read
|
|
|
|
|
a rectangle of the render target into an `SDL_Surface`, and draw one back.
|
|
|
|
|
|
|
|
|
|
### copy_region allocates only when you ask it to
|
|
|
|
|
|
|
|
|
|
`dest` is an `SDL_Surface **`, and **`*dest` must be initialized before the call**.
|
|
|
|
|
Its value selects between two behaviours:
|
|
|
|
|
|
|
|
|
|
| `*dest` on entry | What happens | Who owns the surface afterwards |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `NULL` | a surface is allocated and written back through `dest` | **you do** — release it with `SDL_DestroySurface` |
|
|
|
|
|
| a surface of *exactly* `src`'s dimensions | the pixels are copied into it | you already did |
|
|
|
|
|
| a surface of any other size | `AKERR_OUTOFBOUNDS`, both sets of dimensions in the message | unchanged |
|
|
|
|
|
|
|
|
|
|
The reuse path exists so a caller saving the same region every frame does not churn
|
|
|
|
|
allocations. Note that `akgl_draw_copy_region` is one of the few places in libakgl
|
|
|
|
|
that hands you memory you have to free yourself — everything else comes from the
|
|
|
|
|
pools in [Chapter 05](05-the-heap.md).
|
|
|
|
|
|
|
|
|
|
**`src` must fit entirely inside the render target.** This is checked and refused
|
|
|
|
|
with `AKERR_OUTOFBOUNDS` rather than clipped, deliberately: SDL would clip the read
|
|
|
|
|
and hand back a *smaller* surface than was asked for, and a caller pasting it back
|
|
|
|
|
would not notice until the art was visibly wrong. A rectangle with no area is
|
|
|
|
|
refused too.
|
|
|
|
|
|
|
|
|
|
### paste_region replaces, it does not blend
|
|
|
|
|
|
|
|
|
|
`akgl_draw_paste_region` sets `SDL_BLENDMODE_NONE` on the texture it uploads, so
|
|
|
|
|
the saved pixels **overwrite** what is on the target. That is `GSHAPE`'s default
|
|
|
|
|
behaviour and it is the one that makes a save/restore pair actually restore: a
|
|
|
|
|
saved region's transparent pixels come back as transparent, rather than letting
|
|
|
|
|
whatever is underneath show through them.
|
|
|
|
|
|
|
|
|
|
There is no scaling — the surface is drawn at its own size — and the surface is not
|
|
|
|
|
consumed, so it may be pasted as many times as you like. Parts falling outside the
|
|
|
|
|
target are clipped by SDL and not reported.
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
#include <akgl/draw.h>
|
|
|
|
|
#include <akgl/game.h>
|
|
|
|
|
|
|
|
|
|
/* SSHAPE then GSHAPE: save a 32x32 patch, draw over it, put it back. */
|
|
|
|
|
akerr_ErrorContext *blink_cursor(int x, int y)
|
|
|
|
|
{
|
|
|
|
|
SDL_Surface *saved = NULL;
|
|
|
|
|
SDL_Rect box;
|
|
|
|
|
SDL_FRect cursor;
|
|
|
|
|
SDL_Color white = { 0xff, 0xff, 0xff, SDL_ALPHA_OPAQUE };
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
box.x = x;
|
|
|
|
|
box.y = y;
|
|
|
|
|
box.w = 32;
|
|
|
|
|
box.h = 32;
|
|
|
|
|
cursor.x = (float32_t)x;
|
|
|
|
|
cursor.y = (float32_t)y;
|
|
|
|
|
cursor.w = 32.0f;
|
|
|
|
|
cursor.h = 32.0f;
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
/* saved is NULL, so copy_region allocates it and we own the result. */
|
|
|
|
|
CATCH(errctx, akgl_draw_copy_region(akgl_renderer, &box, &saved));
|
|
|
|
|
CATCH(errctx, akgl_draw_filled_rect(akgl_renderer, &cursor, white));
|
|
|
|
|
CATCH(errctx, akgl_draw_paste_region(akgl_renderer, saved, cursor.x, cursor.y));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
if ( saved != NULL ) {
|
|
|
|
|
SDL_DestroySurface(saved);
|
|
|
|
|
saved = NULL;
|
|
|
|
|
}
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## The checkerboard backdrop
|
|
|
|
|
|
|
|
|
|
`akgl_draw_background` paints an 8x8 grey checkerboard — the pattern an image
|
|
|
|
|
editor uses to show transparency. It is a **diagnostic backdrop rather than a
|
|
|
|
|
general primitive**: `charviewer` uses it so a sprite's transparent pixels are
|
|
|
|
|
visible instead of blending into black.
|
|
|
|
|
|
|
|
|
|
It always starts at the origin. `w` and `h` are the extent to cover, not a
|
|
|
|
|
rectangle, and both round *up* to whole 8-pixel cells — a height of 12 paints 16. A
|
|
|
|
|
zero or negative extent paints nothing and is not reported.
|
|
|
|
|
|
|
|
|
|
Until 0.5.0 this was the one function in the library outside the error protocol: it
|
|
|
|
|
returned `void`, drew through the global `akgl_renderer` without checking it, and
|
|
|
|
|
left the renderer's draw colour changed. It takes a backend now and restores the
|
|
|
|
|
colour it found, which is also what makes it testable without a world.
|
|
|
|
|
|
|
|
|
|
## Where to look next
|
|
|
|
|
|
|
|
|
|
- [Chapter 08](08-rendering.md) — the backend these all take, and the frame the
|
|
|
|
|
calls sit inside.
|
2026-08-02 07:15:25 -04:00
|
|
|
- [Chapter 17](17-text-and-fonts.md) — text, which is textures rather than
|
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
|
|
|
primitives.
|
|
|
|
|
- [Chapter 05](05-the-heap.md) — the pools, and why `copy_region` returning owned
|
|
|
|
|
memory is the exception rather than the rule.
|