# 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 #include #include #include #include #include #include #include #include #include /* 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 eight 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 #include #include #include #include /* 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 22](22-appendix-limits.md) — `AKGL_GAME_*` and the rest of the constants.