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>
This commit is contained in:
297
docs/02-design-philosophy.md
Normal file
297
docs/02-design-philosophy.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# 02. Design philosophy
|
||||
|
||||
Six decisions shape every API in this library. They are not preferences stated up front and
|
||||
then quietly abandoned in the implementation; each one is visible in the code, and this
|
||||
chapter shows the code that proves it.
|
||||
|
||||
None of this is an argument that you should build a game this way. It is an explanation of
|
||||
what libakgl will and will not do for you, so you can decide whether the tradeoffs are ones
|
||||
you want.
|
||||
|
||||
## Bounded, pre-declared resources — not dynamic ones
|
||||
|
||||
**libakgl does not call `malloc` at runtime.** Every runtime object comes out of a fixed,
|
||||
statically allocated array declared in `heap.h`. There are five such arrays — actors,
|
||||
sprites, spritesheets, characters and strings — and the ceilings are compile-time constants:
|
||||
|
||||
```c excerpt=include/akgl/heap.h
|
||||
#ifndef AKGL_MAX_HEAP_ACTOR
|
||||
#define AKGL_MAX_HEAP_ACTOR 64
|
||||
#endif
|
||||
#ifndef AKGL_MAX_HEAP_SPRITE
|
||||
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
|
||||
#endif
|
||||
#ifndef AKGL_MAX_HEAP_SPRITESHEET
|
||||
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
|
||||
#endif
|
||||
#ifndef AKGL_MAX_HEAP_CHARACTER
|
||||
#define AKGL_MAX_HEAP_CHARACTER 256
|
||||
#endif
|
||||
#ifndef AKGL_MAX_HEAP_STRING
|
||||
#define AKGL_MAX_HEAP_STRING 256
|
||||
#endif
|
||||
```
|
||||
|
||||
Allocation is a linear scan for a slot whose reference count is zero. That is the whole
|
||||
algorithm:
|
||||
|
||||
```c excerpt=src/heap.c
|
||||
akerr_ErrorContext *akgl_heap_next_actor(akgl_Actor **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
for (int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||
if ( akgl_heap_actors[i].refcount != 0 ) {
|
||||
continue;
|
||||
}
|
||||
*dest = &akgl_heap_actors[i];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused actor on the heap");
|
||||
}
|
||||
```
|
||||
|
||||
Two consequences follow, and both matter more than the absence of `malloc`.
|
||||
|
||||
**Exhaustion is a normal error, not a catastrophe.** `AKGL_ERR_HEAP` means "this pool is
|
||||
full". It is not `ENOMEM`, the process is not in trouble, and the fix is almost never a
|
||||
bigger pool — **it is usually a missing release.** Raising `AKGL_MAX_HEAP_*` before you have
|
||||
checked for a leak converts a bug that fails on frame 300 into one that fails on frame 3000.
|
||||
|
||||
**The pools are compile-time sized, so the ceiling is an ABI constraint.** The arrays live
|
||||
in the library, so libakgl and everything linking it must agree on the numbers. Overriding
|
||||
one means rebuilding the whole tree, not just your game.
|
||||
|
||||
The idiom for a pool object is claim, use, release in `CLEANUP`, which runs on every path
|
||||
out of the `ATTEMPT` block:
|
||||
|
||||
```c
|
||||
#include <akerror.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/util.h>
|
||||
|
||||
/* Claim, use, release. The CLEANUP block runs on every path out of ATTEMPT. */
|
||||
akerr_ErrorContext *scratch_path(char *root, char *name)
|
||||
{
|
||||
akgl_String *buf = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, akgl_heap_next_string(&buf));
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_string_initialize(buf, name));
|
||||
CATCH(errctx, akgl_path_relative(root, buf->data, buf));
|
||||
SDL_Log("resolved to %s", buf->data);
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(buf));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
**If the pool has no layer for your type, add one.** A new kind of runtime object gets a new
|
||||
array, a `next`, and a `release`, in `heap.h` and `src/heap.c`. It does not get an
|
||||
allocator. [Chapter 5](05-the-heap.md) covers the layers, the reference-count asymmetry
|
||||
between `akgl_heap_next_string` and the other four, and what the ceilings cost you.
|
||||
|
||||
## Variation lives in a backend, not in a branch
|
||||
|
||||
There are exactly two pluggable subsystems, and both have the same shape: **a struct of
|
||||
function pointers, plus an initializer that populates it.**
|
||||
|
||||
```text
|
||||
akgl_RenderBackend akgl_PhysicsBackend
|
||||
+--------------------+ +--------------------+
|
||||
| sdl_renderer | | simulate |
|
||||
| shutdown | | gravity |
|
||||
| frame_start | | collide |
|
||||
| frame_end | | move |
|
||||
| draw_texture | | drag_x/y/z |
|
||||
| draw_mesh | | gravity_x/y/z |
|
||||
| draw_world | | max_timestep |
|
||||
+--------------------+ +--------------------+
|
||||
^ ^
|
||||
| populated by | populated by
|
||||
akgl_render_2d_bind akgl_physics_init_null
|
||||
akgl_render_2d_init akgl_physics_init_arcade
|
||||
(chosen by akgl_physics_factory)
|
||||
```
|
||||
|
||||
The shipped physics initializers are five assignments and nothing else:
|
||||
|
||||
```c excerpt=src/physics.c
|
||||
akerr_ErrorContext *akgl_physics_init_null(akgl_PhysicsBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
|
||||
self->gravity = akgl_physics_null_gravity;
|
||||
self->collide = akgl_physics_null_collide;
|
||||
self->move = akgl_physics_null_move;
|
||||
self->simulate = akgl_physics_simulate;
|
||||
```
|
||||
|
||||
**To vary the behaviour, write an initializer and register it — do not add a conditional.**
|
||||
Nothing in `akgl_physics_simulate` knows which backend it is stepping, and nothing in the
|
||||
render path knows whether `draw_texture` came from the 2D backend or from yours. You can
|
||||
also mix: borrow the entry points that already do what you want and replace only the one
|
||||
that does not.
|
||||
|
||||
```c
|
||||
#include <akerror.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/actor.h>
|
||||
|
||||
/* One tick of "everything floats up at a constant rate", as a backend. */
|
||||
static akerr_ErrorContext *floaty_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
||||
actor->ey = -40.0f * dt;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *floaty_init(akgl_PhysicsBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
|
||||
/* Borrow the parts that already do the right thing. */
|
||||
self->simulate = akgl_physics_simulate;
|
||||
self->move = akgl_physics_arcade_move;
|
||||
self->collide = akgl_physics_null_collide;
|
||||
self->gravity = floaty_gravity;
|
||||
|
||||
self->gravity_time = SDL_GetTicksNS();
|
||||
self->max_timestep = AKGL_PHYSICS_DEFAULT_MAX_TIMESTEP;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
There is no inheritance, no RTTI and no dispatch on a type tag anywhere in the library.
|
||||
Behaviour attaches as a function pointer on a struct — which is also how an actor's own six
|
||||
behaviour hooks work, in [Chapter 12](12-actors.md).
|
||||
|
||||
## Bit flags are the default representation for state
|
||||
|
||||
Where another library would give you an enum and a setter per property, libakgl gives you a
|
||||
32-bit mask and four macros. An actor's state is the clearest case: it is a *set*, not a
|
||||
value, because an actor is alive and facing left and moving left all at once.
|
||||
|
||||
```c excerpt=include/akgl/actor.h
|
||||
#define AKGL_ACTOR_STATE_FACE_DOWN (1 << 0) // 1 0000 0000 0000 0001
|
||||
#define AKGL_ACTOR_STATE_FACE_LEFT (1 << 1) // 2 0000 0000 0000 0010
|
||||
#define AKGL_ACTOR_STATE_FACE_RIGHT (1 << 2) // 4 0000 0000 0000 0100
|
||||
#define AKGL_ACTOR_STATE_FACE_UP (1 << 3) // 8 0000 0000 0000 1000
|
||||
#define AKGL_ACTOR_STATE_ALIVE (1 << 4) // 16 0000 0000 0001 0000
|
||||
```
|
||||
|
||||
The iterator flags in `iterator.h` are the same idea for "what should this sweep do":
|
||||
|
||||
```c excerpt=include/akgl/iterator.h
|
||||
#define AKGL_ITERATOR_OP_UPDATE (1 << 0) // 1 Call the actor's updatefunc
|
||||
#define AKGL_ITERATOR_OP_RENDER (1 << 1) // 2 Call the actor's renderfunc
|
||||
#define AKGL_ITERATOR_OP_RELEASE (1 << 2) // 4 Release the object back to its heap layer
|
||||
#define AKGL_ITERATOR_OP_LAYERMASK (1 << 3) // 8 Skip anything whose layer != layerid
|
||||
#define AKGL_ITERATOR_OP_TILEMAPSCALE (1 << 4) // 16 Scale actors to the tilemap; otherwise force scale 1.0
|
||||
```
|
||||
|
||||
Both tables are hand-aligned, with the decimal value and the bit pattern in a comment per
|
||||
row, and both are protected from the reindent script's `tabify` step precisely so that
|
||||
alignment survives. The tables in the headers are the reference; these excerpts are checked
|
||||
against them.
|
||||
|
||||
**Use the macros, not the operators.** `AKGL_BITMASK_HAS(x, y)` is "every bit of `y` is set
|
||||
in `x`", not "any of them". It is fully parenthesized so that `!AKGL_BITMASK_HAS(a, b)`
|
||||
means what it reads as — until 0.5.0 it did not, and the negation bound to the `&`. Nothing
|
||||
in the tree negated it, which is the only reason that was latent rather than live.
|
||||
|
||||
## Errors carry context, and callers cannot ignore them
|
||||
|
||||
**Every one of libakgl's 156 functions returns `akerr_ErrorContext AKERR_NOIGNORE *`.**
|
||||
Results come back through pointer parameters; the return value is always the error. The
|
||||
`AKERR_NOIGNORE` attribute makes discarding it a compiler diagnostic.
|
||||
|
||||
That is a libakerror design and it is documented by libakerror.
|
||||
[Chapter 4](04-errors.md) covers the part that is genuinely libakgl's: which statuses these
|
||||
functions raise, what each one means *here*, and the traps libakgl's own structure creates.
|
||||
Read it before any subsystem chapter.
|
||||
|
||||
The reasoning behind the choice is operational rather than aesthetic. A game that fails on
|
||||
somebody else's machine leaves you a log file and nothing else, so an error has to say what
|
||||
failed, where, and why, at the point it happened — not three frames later at a `NULL`
|
||||
dereference with no history attached.
|
||||
|
||||
## Everything is referenced by name
|
||||
|
||||
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 for each state; a sprite
|
||||
names the sheet it cuts frames from. All of it is resolved at load time through eight
|
||||
registries:
|
||||
|
||||
```c excerpt=include/akgl/registry.h
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR;
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_SPRITE;
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET;
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_CHARACTER;
|
||||
```
|
||||
|
||||
The lookup itself is unremarkable, and that is the point:
|
||||
|
||||
```c excerpt=src/actor.c
|
||||
obj->basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, basecharname, NULL);
|
||||
```
|
||||
|
||||
**This is what lets the whole asset graph be described in JSON files that reference each
|
||||
other by name**, with no build step and no code generation. It also means a typo in an asset
|
||||
file is a runtime failure rather than a compile error — which is the tradeoff, stated
|
||||
plainly. The registries are SDL property sets rather than an akgl type, so you can enumerate
|
||||
one with `SDL_EnumerateProperties`; that is exactly what `akgl_registry_iterate_actor` does.
|
||||
|
||||
Two sharp edges, both in [Chapter 6](06-the-registry.md): an uninitialized registry id is 0,
|
||||
which SDL treats as "no such property set" and silently drops writes to; and
|
||||
`AKGL_REGISTRY_SPRITESHEET` is keyed on the *resolved* image path, which is what makes two
|
||||
sprites naming the same PNG share one texture.
|
||||
|
||||
## One world at a time
|
||||
|
||||
The library keeps exactly one of everything, reachable through four globals:
|
||||
|
||||
```c excerpt=include/akgl/game.h
|
||||
extern akgl_Tilemap *akgl_gamemap;
|
||||
extern akgl_RenderBackend *akgl_renderer;
|
||||
extern akgl_PhysicsBackend *akgl_physics;
|
||||
extern SDL_FRect *akgl_camera;
|
||||
```
|
||||
|
||||
`akgl_game_init` points each of them at default storage — `akgl_default_renderer`,
|
||||
`akgl_default_physics`, `akgl_default_camera`, `akgl_default_gamemap`. **They are pointers
|
||||
so that you can substitute your own instance by reassigning one**, and the rest of the
|
||||
library goes on drawing and simulating through it without knowing.
|
||||
|
||||
That is the entire extent of the indirection. **There is no notion of two worlds at once.**
|
||||
No context handle is threaded through the API, no `akgl_World *` is passed to every call,
|
||||
and two simultaneous games in one process is not a thing this library supports. If you want
|
||||
a paused world behind a menu, you save the state you care about and swap the pointers back.
|
||||
|
||||
The globals all carry the `akgl_` prefix, and that is not cosmetic. Until 0.5.0 the library
|
||||
exported a bare `renderer`, `tests/character.c` defined an `SDL_Renderer *renderer` of its
|
||||
own, both had external linkage with the same spelling, and the executable's definition
|
||||
preempted the library's. Every texture load in that suite failed and the suite reported
|
||||
success anyway. A consuming game with a variable called `renderer` would have hit exactly
|
||||
the same thing, with no test to notice.
|
||||
|
||||
## What the six add up to
|
||||
|
||||
You get a library whose worst-case memory is knowable at compile time, whose failure modes
|
||||
arrive as messages instead of crashes, whose behaviour you extend by writing a function
|
||||
rather than by editing a switch, and whose assets are text files you can edit by hand.
|
||||
|
||||
You give up dynamic sizing, more than one world, and any ability to describe a game without
|
||||
writing C. Those are the tradeoffs. [Chapter 3](03-getting-started.md) puts a window on the
|
||||
screen.
|
||||
Reference in New Issue
Block a user