371 lines
18 KiB
Markdown
371 lines
18 KiB
Markdown
|
|
# 07. The game and the frame
|
||
|
|
|
||
|
|
`game.h` is the top of the library. `akgl_game_init` brings up SDL, the pools, the
|
||
|
|
registries and the audio and font engines; `akgl_game_update` is one frame — update every
|
||
|
|
actor, step the physics, draw the world.
|
||
|
|
|
||
|
|
**There is exactly one of everything.** `akgl_renderer`, `akgl_physics`, `akgl_camera` and
|
||
|
|
`akgl_gamemap` are globals pointing at `akgl_default_*` storage, so a program can swap in
|
||
|
|
its own instance by reassigning the pointer without the rest of the library knowing. That
|
||
|
|
is the whole extent of the indirection: there is no notion of two worlds at once.
|
||
|
|
|
||
|
|
## The startup order
|
||
|
|
|
||
|
|
`game.h` documents a five-step sequence, and it holds up against `src/game.c`:
|
||
|
|
|
||
|
|
```c
|
||
|
|
#include <SDL3/SDL.h>
|
||
|
|
#include <akerror.h>
|
||
|
|
#include <akstdlib.h>
|
||
|
|
#include <akgl/error.h>
|
||
|
|
#include <akgl/game.h>
|
||
|
|
#include <akgl/heap.h>
|
||
|
|
#include <akgl/physics.h>
|
||
|
|
#include <akgl/registry.h>
|
||
|
|
#include <akgl/renderer.h>
|
||
|
|
#include <akgl/staticstring.h>
|
||
|
|
|
||
|
|
/* The startup order, in the order it has to happen. */
|
||
|
|
akerr_ErrorContext AKERR_NOIGNORE *startup(void)
|
||
|
|
{
|
||
|
|
akgl_String *engine = NULL;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
/* 1. The three required fields. akgl_game_init refuses to run without them. */
|
||
|
|
PASS(errctx, aksl_strncpy(akgl_game.name, sizeof(akgl_game.name),
|
||
|
|
"manual-demo", sizeof(akgl_game.name) - 1));
|
||
|
|
PASS(errctx, aksl_strncpy(akgl_game.version, sizeof(akgl_game.version),
|
||
|
|
"1.0.0", sizeof(akgl_game.version) - 1));
|
||
|
|
PASS(errctx, aksl_strncpy(akgl_game.uri, sizeof(akgl_game.uri),
|
||
|
|
"tech.starfort.manual-demo", sizeof(akgl_game.uri) - 1));
|
||
|
|
|
||
|
|
/* 2. Bring the library up: errors, pools, registries, SDL, audio, fonts. */
|
||
|
|
PASS(errctx, akgl_game_init());
|
||
|
|
|
||
|
|
/* 3. Configure, before anything reads the configuration. */
|
||
|
|
PASS(errctx, akgl_set_property("game.screenwidth", "800"));
|
||
|
|
PASS(errctx, akgl_set_property("game.screenheight", "600"));
|
||
|
|
PASS(errctx, akgl_set_property("physics.gravity.y", "400.0"));
|
||
|
|
|
||
|
|
/* 4. The two subsystems that read it. This is where the window appears. */
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_heap_next_string(&engine));
|
||
|
|
CATCH(errctx, akgl_string_initialize(engine, "arcade"));
|
||
|
|
CATCH(errctx, akgl_render_2d_init(akgl_renderer));
|
||
|
|
CATCH(errctx, akgl_physics_factory(akgl_physics, engine));
|
||
|
|
} CLEANUP {
|
||
|
|
IGNORE(akgl_heap_release_string(engine));
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
|
||
|
|
/* 5. Load assets, then loop on akgl_game_update. */
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**The three required fields have no defaults.** `akgl_game.name`, `.version` and `.uri` are
|
||
|
|
each checked for a non-zero length and each raises `AKERR_NULLPOINTER` if empty. All three
|
||
|
|
are load-bearing: `.uri` becomes the window title, all three become SDL's app metadata, and
|
||
|
|
`.name`, `.uri` and `.version` are compared on savegame load. `.version` must be a valid
|
||
|
|
semver string, or `akgl_game_load` fails later rather than here.
|
||
|
|
|
||
|
|
**Steps 3 and 4 cannot be swapped.** `akgl_render_2d_init` and `akgl_physics_init_arcade`
|
||
|
|
each read their configuration once, at init, and never look again. Setting
|
||
|
|
`physics.gravity.y` after the factory call changes nothing. See
|
||
|
|
[Chapter 6](06-the-registry.md).
|
||
|
|
|
||
|
|
### What `akgl_game_init` does, in order
|
||
|
|
|
||
|
|
Verified against `src/game.c`. The order matters in three places, marked:
|
||
|
|
|
||
|
|
1. **`akgl_error_init()` — first, before anything that can raise.** Everything below reports
|
||
|
|
through `AKGL_ERR_*` codes, and a code raised before its name is registered prints as
|
||
|
|
"Unknown Error". See [Chapter 4](04-errors.md).
|
||
|
|
2. Stamp `akgl_game.libversion` from `AKGL_VERSION`.
|
||
|
|
3. Seed the frame clock: `gameStartTime`, `lastIterTime` and `lastFPSTime` all get
|
||
|
|
`SDL_GetTicksNS()`.
|
||
|
|
4. Install `akgl_game_lowfps` as `lowfpsfunc`.
|
||
|
|
5. Create the state mutex. Failure is `AKGL_ERR_SDL`.
|
||
|
|
6. **Take the state lock.** Everything from here to the end runs holding it.
|
||
|
|
7. Check `name`, `version` and `uri` are non-empty.
|
||
|
|
8. `akgl_heap_init()` — zero all five pools.
|
||
|
|
9. The eight registry initializers: actor, sprite, spritesheet, character, font, music,
|
||
|
|
properties, actor-state-strings. **Note that `akgl_registry_init` is not called** —
|
||
|
|
`akgl_game_init` calls the eight individually. See [Chapter 6](06-the-registry.md).
|
||
|
|
10. `SDL_SetAppMetadata` from the three fields.
|
||
|
|
11. Zero all `AKGL_MAX_CONTROL_MAPS` control maps.
|
||
|
|
12. `SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO)`.
|
||
|
|
13. Feed the bundled controller database to `SDL_AddGamepadMapping`, one entry at a time.
|
||
|
|
A rejected entry is `AKGL_ERR_SDL` and aborts startup.
|
||
|
|
14. `akgl_controller_open_gamepads()`.
|
||
|
|
15. `MIX_Init()` and `MIX_CreateMixerDevice()` on the default playback device.
|
||
|
|
16. `TTF_Init()`.
|
||
|
|
17. Point `akgl_camera`, `akgl_renderer`, `akgl_physics` and `akgl_gamemap` at their default
|
||
|
|
storage.
|
||
|
|
18. Release the state lock.
|
||
|
|
|
||
|
|
**What it does not do: create the window, choose a physics backend, or load any
|
||
|
|
configuration.** Those three read properties, so they belong to the caller, after step 3
|
||
|
|
above.
|
||
|
|
|
||
|
|
**A failure anywhere from step 6 onward returns with the state mutex held.** `game.h` says
|
||
|
|
so. On a single-threaded program that is survivable — SDL mutexes are recursive, so the
|
||
|
|
same thread can take it again — but the mutex is never made available to another thread, so
|
||
|
|
anything else that calls `akgl_game_state_lock` will spend a second retrying and then fail.
|
||
|
|
A failed `akgl_game_init` is not something to retry; report it and exit.
|
||
|
|
|
||
|
|
## One frame
|
||
|
|
|
||
|
|
`akgl_game_update(opflags)` is the whole frame, and it is short enough to state exactly:
|
||
|
|
|
||
|
|
```text
|
||
|
|
akgl_game_state_lock() <- retries, does not block
|
||
|
|
akgl_game_update_fps() <- counts the frame, maybe fires lowfpsfunc
|
||
|
|
for each slot in akgl_heap_actors:
|
||
|
|
skip if refcount == 0
|
||
|
|
skip if OP_LAYERMASK and actor->layer != opflags->layerid
|
||
|
|
OP_TILEMAPSCALE ? akgl_tilemap_scale_actor(...) : actor->scale = 1.0
|
||
|
|
actor->updatefunc(actor) <- exactly once per actor
|
||
|
|
akgl_physics->simulate(akgl_physics, NULL)
|
||
|
|
akgl_renderer->draw_world(akgl_renderer, NULL)
|
||
|
|
akgl_game_state_unlock()
|
||
|
|
```
|
||
|
|
|
||
|
|
Five things follow from that, and four of them are not obvious.
|
||
|
|
|
||
|
|
**The sweep walks the pool, not the registry.** It reads `akgl_heap_actors` directly and
|
||
|
|
skips slots whose `refcount` is 0. That is why an actor released mid-frame simply stops
|
||
|
|
being visited, and why the actor registry's contents do not affect the update pass at all.
|
||
|
|
|
||
|
|
**Each actor's `updatefunc` runs exactly once.** Until 0.5.0 this loop sat inside a walk
|
||
|
|
over `AKGL_TILEMAP_MAX_LAYERS` that never compared an actor's `layer` to the layer being
|
||
|
|
walked, so every actor updated sixteen times a frame — 70 µs of work to do 4.4 µs of it.
|
||
|
|
Updating an actor is not a per-layer operation. **Drawing is**, and
|
||
|
|
`akgl_render_2d_draw_world` walks the layers itself.
|
||
|
|
|
||
|
|
**`opflags` is not forwarded.** `simulate` and `draw_world` are both passed `NULL`, so they
|
||
|
|
use their own defaults regardless of what you asked for. Setting
|
||
|
|
`AKGL_ITERATOR_OP_LAYERMASK` restricts the *update* sweep and nothing else; the whole world
|
||
|
|
still draws.
|
||
|
|
|
||
|
|
**A `NULL` `opflags` means `AKGL_ITERATOR_OP_UPDATE` alone** — every live actor once, no
|
||
|
|
layer mask, and every actor's `scale` forced to 1.0 because `AKGL_ITERATOR_OP_TILEMAPSCALE`
|
||
|
|
is not set. If your sprites are rendering at the wrong size, that flag is the first thing to
|
||
|
|
check.
|
||
|
|
|
||
|
|
**A live actor's `updatefunc` is called without a `NULL` check.** A hand-built `akgl_Actor`
|
||
|
|
that never went through `akgl_actor_initialize` crashes here rather than raising.
|
||
|
|
|
||
|
|
### There is no frame pacing
|
||
|
|
|
||
|
|
`akgl_game_update` does not sleep, does not wait for vertical blank, and does not ask SDL
|
||
|
|
to. `akgl_render_2d_init` calls `SDL_CreateWindowAndRenderer` with no flags and never calls
|
||
|
|
`SDL_SetRenderVSync`, so **the loop runs as fast as the machine will let it** and burns a
|
||
|
|
core doing it.
|
||
|
|
|
||
|
|
That is a deliberate omission rather than an oversight — pacing policy belongs to the game —
|
||
|
|
but it means the pacing is yours to add. Either call `SDL_SetRenderVSync(renderer, 1)` on
|
||
|
|
`akgl_renderer->sdl_renderer` after `akgl_render_2d_init`, or measure the frame and
|
||
|
|
`SDL_DelayNS` the remainder yourself. Physics is unaffected either way:
|
||
|
|
`akgl_physics_simulate` computes its own `dt` from `SDL_GetTicksNS` and bounds it at
|
||
|
|
`physics.max_timestep`.
|
||
|
|
|
||
|
|
### `akgl_game.fps` reads 0 for the first second
|
||
|
|
|
||
|
|
`akgl_game_update_fps` recomputes `fps` only when a **full second** has elapsed since the
|
||
|
|
last recomputation, so it is a completed-second average and not an instantaneous figure. For
|
||
|
|
the first second of the process it is 0.
|
||
|
|
|
||
|
|
0 is below the low-FPS threshold of 30. **So `lowfpsfunc` fires on every single frame until
|
||
|
|
the first second is up** — several hundred `Low FPS! 0` lines from the default handler
|
||
|
|
before the game has drawn anything worth measuring. That is expected, not a symptom.
|
||
|
|
|
||
|
|
If you replace `lowfpsfunc` with something that sheds work, guard it on `fps > 0` or it will
|
||
|
|
shed work during startup, which is the worst moment for it. The hook is also what an
|
||
|
|
embedder has to know about: `akgl_game_update_fps` installs the default if it finds the
|
||
|
|
pointer `NULL`, because a host binding its own renderer with `akgl_render_2d_bind` never ran
|
||
|
|
`akgl_game_init` and used to crash here on frame one.
|
||
|
|
|
||
|
|
A caller running its own loop rather than `akgl_game_update` has to call
|
||
|
|
`akgl_game_update_fps` itself; nothing else does.
|
||
|
|
|
||
|
|
## The state lock
|
||
|
|
|
||
|
|
`akgl_game.state` is a single `int32_t` of application-defined flags. **The library never
|
||
|
|
reads it.** The mutex guarding it, `akgl_game.statelock`, is the only thread-safety
|
||
|
|
libakgl offers, and it protects the state flags — not the pools, not the registries, not
|
||
|
|
the renderer.
|
||
|
|
|
||
|
|
`akgl_game_state_lock` **polls; it does not block.** It calls `SDL_TryLockMutex` every
|
||
|
|
`AKGL_GAME_STATE_LOCK_RETRY_MS` (100 ms) until `AKGL_GAME_STATE_LOCK_BUDGET_MS` (1000 ms) is
|
||
|
|
spent, then raises `AKGL_ERR_SDL`. The point is that a deadlock reports an error you can act
|
||
|
|
on instead of hanging the process. `SDL_GetError()` is appended for whatever it is worth,
|
||
|
|
but after a failed `SDL_TryLockMutex` it is usually stale or empty — contention is reported
|
||
|
|
by the return value, not by an error string. **The status is the signal, not the text.**
|
||
|
|
|
||
|
|
Two behaviours to plan around:
|
||
|
|
|
||
|
|
**Every failure path in `akgl_game_update` returns holding the lock.** Every step from
|
||
|
|
`akgl_game_state_lock` onward propagates with `PASS`, which returns immediately, so the
|
||
|
|
`akgl_game_state_unlock` at the bottom is never reached. Because SDL mutexes are recursive
|
||
|
|
the next frame on the same thread still gets the lock, so a single-threaded game does not
|
||
|
|
deadlock — it accumulates one unmatched lock per failed frame, and the mutex is never
|
||
|
|
released to any other thread again. **Treat a failed `akgl_game_update` as terminal.** If
|
||
|
|
you genuinely must continue, unlock once yourself before the next frame — but only when you
|
||
|
|
know the failure came from *inside* the frame rather than from the lock acquisition itself,
|
||
|
|
because unlocking a mutex this thread does not hold is undefined behaviour in SDL.
|
||
|
|
|
||
|
|
**`akgl_game_state_lock` succeeds trivially before `akgl_game_init`.** `SDL_TryLockMutex`
|
||
|
|
returns true when passed `NULL`, and `akgl_game.statelock` is `NULL` until step 5 of
|
||
|
|
startup. It locks nothing and reports success.
|
||
|
|
|
||
|
|
`akgl_game_state_unlock` has no failure path and returns `NULL`. That includes the case
|
||
|
|
that matters — unlocking a mutex this thread does not hold — which is undefined behaviour in
|
||
|
|
SDL rather than an error here.
|
||
|
|
|
||
|
|
## Iterators are a work order
|
||
|
|
|
||
|
|
There is no iterator object, and **there is no `next()`**. Traversal is SDL's —
|
||
|
|
`SDL_EnumerateProperties` over a registry — and `akgl_Iterator` is the `userdata` carried
|
||
|
|
into each callback, saying *which* entries to touch and *what* to do to each one.
|
||
|
|
|
||
|
|
```c excerpt=include/akgl/iterator.h
|
||
|
|
/** @brief Selects operations and an optional layer for actor traversal. */
|
||
|
|
typedef struct {
|
||
|
|
uint32_t flags; /**< Bitwise OR of the `AKGL_ITERATOR_OP_*` values below. */
|
||
|
|
uint8_t layerid; /**< Layer to restrict the sweep to. Read only when #AKGL_ITERATOR_OP_LAYERMASK is set. */
|
||
|
|
} akgl_Iterator;
|
||
|
|
|
||
|
|
#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
|
||
|
|
```
|
||
|
|
|
||
|
|
The operations are **independent bits, not an enum**: one sweep can update, scale and
|
||
|
|
render, and they run in that fixed order regardless of the order the bits were set in. Bits
|
||
|
|
5 through 31 are declared `AKGL_ITERATOR_OP_UNDEFINED_*` and are unused.
|
||
|
|
|
||
|
|
Passing `NULL` where an `akgl_Iterator *` is expected is fine at the top-level entry points
|
||
|
|
— `akgl_game_update`, `akgl_physics_simulate` and `akgl_render_2d_draw_world` each
|
||
|
|
substitute their own defaults — but it is `AKERR_NULLPOINTER` once inside a callback.
|
||
|
|
|
||
|
|
**`akgl_game_update` reads only two of the five bits**: `AKGL_ITERATOR_OP_LAYERMASK` and
|
||
|
|
`AKGL_ITERATOR_OP_TILEMAPSCALE`. It calls `updatefunc` unconditionally rather than checking
|
||
|
|
`AKGL_ITERATOR_OP_UPDATE`, because updating every live actor once is the whole job. The
|
||
|
|
callback `akgl_registry_iterate_actor` does check all of them, and that is the one the
|
||
|
|
render sweep goes through.
|
||
|
|
|
||
|
|
## The main loop
|
||
|
|
|
||
|
|
```c
|
||
|
|
#include <SDL3/SDL.h>
|
||
|
|
#include <akerror.h>
|
||
|
|
#include <akgl/error.h>
|
||
|
|
#include <akgl/game.h>
|
||
|
|
#include <akgl/iterator.h>
|
||
|
|
|
||
|
|
/* A frame loop. A failed akgl_game_update is terminal: it returns holding the
|
||
|
|
* state lock, and FINISH_NORETURN is what turns that into a stack trace and a
|
||
|
|
* truthful exit status instead of a second frame on a poisoned mutex. */
|
||
|
|
int main(void)
|
||
|
|
{
|
||
|
|
akgl_Iterator work = {
|
||
|
|
.flags = (AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_TILEMAPSCALE),
|
||
|
|
.layerid = 0
|
||
|
|
};
|
||
|
|
SDL_Event event;
|
||
|
|
bool running = true;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
ATTEMPT {
|
||
|
|
while ( running == true ) {
|
||
|
|
while ( SDL_PollEvent(&event) == true ) {
|
||
|
|
if ( event.type == SDL_EVENT_QUIT ) {
|
||
|
|
running = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
CATCH(errctx, akgl_game_update(&work));
|
||
|
|
}
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH_NORETURN(errctx);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The `CATCH` inside the `while` is safe **only because the loop is the entire body of the
|
||
|
|
`ATTEMPT`**. `CATCH` reports failure by `break`ing, and a `break` binds to the innermost
|
||
|
|
enclosing loop, so it leaves the `while` and falls into `CLEANUP`/`PROCESS`/`FINISH` — which
|
||
|
|
is what you want. Put anything after the loop and you have to account for reaching it after
|
||
|
|
a failure. `AGENTS.md` spells the rule out, and `scripts/check_error_protocol.py` cannot see
|
||
|
|
this one.
|
||
|
|
|
||
|
|
## There is no `akgl_game_shutdown`
|
||
|
|
|
||
|
|
**Teardown is entirely the caller's.** No function in this library undoes `akgl_game_init`.
|
||
|
|
What there is, in the order it has to run:
|
||
|
|
|
||
|
|
| Step | Call | Why the order |
|
||
|
|
|---|---|---|
|
||
|
|
| 1 | `akgl_text_unloadfont` for each font you loaded | Must precede `TTF_Quit`. `text.h` documents the trap: `SDL_Quit` destroying the engine out from under a live `TTF_Font` is not a clean unload |
|
||
|
|
| 2 | `akgl_heap_release_*` for anything you still hold | `akgl_heap_release_spritesheet` destroys `SDL_Texture`s and must run on the renderer's thread, before the renderer goes |
|
||
|
|
| 3 | `akgl_audio_shutdown()` | Puts the synthesizer voices back |
|
||
|
|
| 4 | `akgl_renderer->shutdown(akgl_renderer)` | `akgl_render_2d_shutdown` |
|
||
|
|
| 5 | `TTF_Quit()`, `MIX_Quit()`, `SDL_Quit()` | SDL's own |
|
||
|
|
|
||
|
|
A process that is exiting anyway can skip most of it — the OS reclaims the pools, and SDL
|
||
|
|
reclaims what it owns — but a program that tears a *level* down and builds another one has
|
||
|
|
to get steps 1 and 2 right or it exhausts the pools. `akgl_heap_init_actor` plus
|
||
|
|
`akgl_registry_init_actor` is the cheap version of that for actors only; see
|
||
|
|
[Chapter 5](05-the-heap.md).
|
||
|
|
|
||
|
|
## Savegames are partial
|
||
|
|
|
||
|
|
`akgl_game_save` and `akgl_game_load` exist and work, and **they do not yet save your game.**
|
||
|
|
Read this before building anything on them.
|
||
|
|
|
||
|
|
What `akgl_game_save` writes:
|
||
|
|
|
||
|
|
1. The `akgl_Game` struct verbatim — metadata, timing, FPS accounting and the state flags.
|
||
|
|
2. Four name tables — actors, sprites, spritesheets, characters — each mapping a registered
|
||
|
|
name to **the address the object had at save time**, each terminated by a zeroed name
|
||
|
|
field and a zeroed pointer.
|
||
|
|
|
||
|
|
What it does not write: **the objects themselves.** Not one actor, sprite, sheet or
|
||
|
|
character. The name tables are the mechanism that would let a loader reconnect pointers
|
||
|
|
between objects that will sit at different addresses next run; the objects those pointers
|
||
|
|
would point at are not in the file. `akgl_game_load` correspondingly rebuilds the four
|
||
|
|
old-address-to-current-object maps and stops there. `game.h` says so on both functions.
|
||
|
|
|
||
|
|
Three further things to know:
|
||
|
|
|
||
|
|
- **`akgl_game_load` refuses a save that does not match this build**, comparing the library
|
||
|
|
version, the game version, the game name and the game URI. Versions are compared by
|
||
|
|
**exact** semver equality, not compatibility, because the file is a raw memory image and
|
||
|
|
semver has no way to say "the layout did not move". A mismatch is `AKERR_API`.
|
||
|
|
- **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
|
||
|
|
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.
|
||
|
|
- **`akgl_game_load` leaks four `SDL_PropertiesID` per call.** The maps it builds are never
|
||
|
|
destroyed.
|
||
|
|
|
||
|
|
A write error part-way through the name tables **terminates the process** rather than
|
||
|
|
returning. The entries go through `SDL_EnumerateProperties`, whose callbacks cannot report
|
||
|
|
failure upward and end in `FINISH_NORETURN`; only the terminators are written through the
|
||
|
|
error-reporting path. See [Chapter 4](04-errors.md).
|
||
|
|
|
||
|
|
## Where to go next
|
||
|
|
|
||
|
|
- [Chapter 4](04-errors.md) — the statuses everything here raises.
|
||
|
|
- [Chapter 6](06-the-registry.md) — the configuration step 3 above writes into.
|
||
|
|
- [Chapter 8](08-rendering.md) — `akgl_render_2d_init` versus `akgl_render_2d_bind`, and
|
||
|
|
what `draw_world` does with the layers.
|
||
|
|
- [Chapter 14](14-physics.md) — what `simulate` does with the `dt` this chapter does not
|
||
|
|
pace.
|
||
|
|
- [Chapter 21](21-appendix-limits.md) — `AKGL_GAME_*` and the rest of the constants.
|