# 08. Rendering SDL3 already owns the renderer. `SDL_Renderer`, `SDL_Texture`, `SDL_FRect`, blend modes and the batching behaviour behind `SDL_RenderPresent` are documented in the [SDL3 wiki](https://wiki.libsdl.org/SDL3/CategoryRender), and this chapter does not restate any of it. What libakgl adds is three things, and only three: 1. **A backend struct** — a record of function pointers, so a renderer can be replaced without a branch anywhere in the library. 2. **A frame contract** — `frame_start`, then any number of `draw_*` calls, then `frame_end`. 3. **A scene walk** — `draw_world`, which interleaves tilemap layers with the actors standing on them. Per-function reference is generated from the headers by Doxygen and is CI-gated; this chapter teaches the shape and links there for signatures. ## The backend is a record of function pointers There is no renderer base class, no vtable pointer, and no dynamic dispatch. There is a struct with an `SDL_Renderer *` and six function pointers on it: ```c excerpt=include/akgl/renderer.h typedef struct akgl_RenderBackend { SDL_Renderer *sdl_renderer; akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self); akerr_ErrorContext AKERR_NOIGNORE *(*frame_start)(struct akgl_RenderBackend *self); akerr_ErrorContext AKERR_NOIGNORE *(*frame_end)(struct akgl_RenderBackend *self); akerr_ErrorContext AKERR_NOIGNORE *(*draw_texture)(struct akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip); akerr_ErrorContext AKERR_NOIGNORE *(*draw_mesh)(struct akgl_RenderBackend *self); akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags); } akgl_RenderBackend; ``` A *backend* is therefore an initializer that fills those six slots in. One is shipped — the 2D SDL one, whose entry points are the `akgl_render_2d_*` functions — and a second renderer would be a second initializer pointing the same six slots at different functions. Nothing in the library tests "which renderer is this". **Call through the pointers, not the names.** `akgl_render_2d_frame_start(b)` and `b->frame_start(b)` do the same thing today and stop doing the same thing the moment anybody swaps a backend in. Every call site inside libakgl goes through the pointer, and so should yours: ```c #include #include akerr_ErrorContext *draw_one_frame(void) { PREPARE_ERROR(errctx); PASS(errctx, akgl_renderer->frame_start(akgl_renderer)); PASS(errctx, akgl_renderer->draw_world(akgl_renderer, NULL)); PASS(errctx, akgl_renderer->frame_end(akgl_renderer)); SUCCEED_RETURN(errctx); } ``` `akgl_renderer` is the global the rest of the library draws through. It is a pointer into `akgl_default_renderer`, set by `akgl_game_init`; see [Chapter 7](07-the-game-and-the-frame.md). Reassigning `akgl_renderer` is how a program installs its own backend — there is exactly one world, so there is exactly one of these. Every entry point returns `akerr_ErrorContext AKERR_NOIGNORE *`. Statuses and what they mean are [Chapter 4](04-errors.md); the protocol itself is libakerror's and is documented in `deps/libakerror`. ## The frame contract ```text frame_start(self) clear the target to opaque black | +--> draw_world(self, opflags) the tilemap and its actors +--> draw_texture(self, ...) one blit +--> akgl_draw_* (chapter 09) lines, boxes, circles, fills | frame_end(self) present ``` `frame_start` sets the draw colour to opaque black and calls `SDL_RenderClear` itself. **You do not need to clear again.** The FAQ half of the old `README.md` showed this: ```c norun PASS(e, akgl_renderer->frame_start(akgl_renderer)); SDL_RenderClear(akgl_renderer->sdl_renderer); ``` The second line is redundant — it clears an already-cleared target, unchecked, and at whatever draw colour was left behind. Drop it. (`norun` because it is quoted as an example of what *not* to write; it compiles, and that is the problem.) Both check `self` and then `self->sdl_renderer` before touching either, so a `NULL` backend and a backend that was bound but never given a renderer both report `AKERR_NULLPOINTER` rather than crashing. `renderer.h` says the opposite — its `@param self` for `frame_start`, `frame_end` and `draw_texture` reads "dereferenced *before* it is checked -- a `NULL` @p self is a crash, not an error". Read `src/renderer.c`: the first statement in each is a `FAIL_ZERO_RETURN` on `self`. ## The camera `akgl_camera` is an `SDL_FRect *` in **map coordinates**: `x`/`y` are the top-left corner of the view into the world, `w`/`h` its size. It is a plain pointer into `akgl_default_camera`, and everything that draws world content subtracts it. `akgl_render_2d_init` points it at the full screen rectangle at startup. Scrolling is just writing to it: ```c #include #include /* Centre the view on an actor, and do not scroll past the top-left of the map. */ akerr_ErrorContext *camera_follow(akgl_Actor *obj) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); akgl_camera->x = obj->x - (akgl_camera->w / 2.0f); akgl_camera->y = obj->y - (akgl_camera->h / 2.0f); if ( akgl_camera->x < 0.0f ) { akgl_camera->x = 0.0f; } if ( akgl_camera->y < 0.0f ) { akgl_camera->y = 0.0f; } SUCCEED_RETURN(errctx); } ``` There is no camera object and no camera API — a rectangle is the whole model. A game that wants two views draws twice with the pointer moved between them. ## draw_world walks sixteen layers `draw_world` is the one call that draws a scene rather than a thing. For each layer index `i` from 0 to `AKGL_TILEMAP_MAX_LAYERS` (16), it: 1. draws layer `i` of the global `akgl_gamemap` through `akgl_camera`, **if the map has that many layers**; then 2. sweeps all `AKGL_MAX_HEAP_ACTOR` (64) actor pool slots and calls `renderfunc` on every live actor whose `layer` equals `i`. ```text layer 0 tilemap layer 0 ....... actors with layer == 0 layer 1 tilemap layer 1 ....... actors with layer == 1 layer 2 (map has only 2) actors with layer == 2 ... ... layer 15 -- actors with layer == 15 ``` That interleaving is the point: it is what puts an actor in front of the ground it stands on and behind the archway it walks under. An actor's `layer` field is set from the object layer it was placed in — see [Chapter 12](12-actors.md) and [Chapter 13](13-tilemaps.md). The map layer is skipped once `i` reaches `akgl_gamemap->numlayers`, but the actor sweep is not: **an actor on layer 9 of a two-layer map still draws.** It just draws over bare background. `draw_world` reads `akgl_gamemap`, `akgl_camera` and `akgl_heap_actors` directly rather than taking them as arguments. There is one world. ### opflags is accepted and ignored The `akgl_Iterator *opflags` argument is part of the backend signature and is currently **ignored entirely**. `NULL` substitutes a zeroed iterator, and neither that nor a set of flags you built yourself reaches anything: `draw_world` sweeps the actor pool itself instead of going through `akgl_registry_iterate_actor`, so no `AKGL_ITERATOR_OP_*` bit — including `AKGL_ITERATOR_OP_LAYERMASK` — has any effect here. Pass `NULL`. When layer masking is wired up, the parameter is where it will land. ### Two things draw_world does not check - **`akgl_gamemap` is dereferenced unguarded.** Calling `draw_world` before `akgl_tilemap_load` is a crash, not an error context. - **A live actor's `renderfunc` is called unguarded.** A hand-built actor that was never run through `akgl_actor_initialize` has a `NULL` there. Always initialize actors through the library. The first failure aborts the frame and propagates unchanged; there is no draw-what-you-can behaviour. > **Known defect (`TODO.md`, "Performance", item 6).** The layer loop always runs > all 16 iterations and rescans all 64 actor slots on each — 1024 refcount checks > per frame for a one-layer map. Invisible at 60 fps under the software renderer, > measurable on a 2 ms GPU frame. Bounding the walk by `numlayers` and building > per-layer actor lists in one pool pass is the recorded fix. ## The embedding seam: akgl_render_2d_bind vs akgl_render_2d_init This is a first-class use case, not a footnote. Creating a window and populating a vtable are two separable jobs, and libakgl separates them. | | `akgl_render_2d_init` | `akgl_render_2d_bind` | |---|---|---| | Reads configuration properties | yes — `game.screenwidth`, `game.screenheight` | no | | Creates a window and `SDL_Renderer` | yes | **no** | | Touches `self->sdl_renderer` | overwrites it | **never touches it** | | Points `akgl_camera` at the screen | yes | no | | Installs the six function pointers | yes, by calling `_bind` | yes | | Needs the registry initialized first | yes | no | `akgl_render_2d_init` is the ordinary path for a program that owns its whole process. It reads `game.screenwidth` and `game.screenheight` from the property registry — both defaulting to the string `"0"`, which asks SDL for a zero-sized window — creates the window and renderer, sets `akgl_camera` to the full screen rectangle, and then calls `akgl_render_2d_bind` to fill in the vtable. Because it reads properties, `akgl_registry_init_properties` and your property writes have to happen first; see [Chapter 6](06-the-registry.md). **The window title is `akgl_game.uri`.** Not `akgl_game.name` — the reverse-DNS identifier is what `SDL_CreateWindowAndRenderer` is handed. If your title bar reads `tech.starfort.mygame`, that is why. `akgl_render_2d_bind` is the other half on its own. It writes six pointers and returns. It deliberately does not touch `sdl_renderer`, which is exactly what makes it usable by a host that already has one — an embedded interpreter, a level editor, an application with its own window that wants libakgl to draw inside it. This is what the sibling `akbasic` consumer uses. ```c #include #include #include /* * A host that already owns its window and SDL_Renderer. It never calls * akgl_render_2d_init, so libakgl never creates a second window. */ akerr_ErrorContext *host_bind_renderer(SDL_Renderer *mine, int w, int h) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, mine, AKERR_NULLPOINTER, "mine"); /* _bind never writes sdl_renderer, so put yours there first. */ akgl_default_renderer.sdl_renderer = mine; PASS(errctx, akgl_render_2d_bind(&akgl_default_renderer)); /* Everything else in the library draws through the global. */ akgl_renderer = &akgl_default_renderer; /* _bind does not set the camera; _init would have. */ akgl_camera = &akgl_default_camera; akgl_camera->x = 0.0f; akgl_camera->y = 0.0f; akgl_camera->w = (float)w; akgl_camera->h = (float)h; SUCCEED_RETURN(errctx); } ``` ### Bind the global, not a backend of your own This is the trap in the seam, and it is worth stating flatly: **`draw_world` hands `self` to the tilemap and to each actor's `renderfunc`, but neither of them uses it.** `akgl_actor_render` and `akgl_tilemap_draw` both reach straight for the global `akgl_renderer`. So does `akgl_spritesheet_initialize`, and so does every texture load in the tilemap loader. A host that binds a *private* `akgl_RenderBackend` and calls `mine.draw_world(&mine, NULL)` gets its tilemap and its actors drawn through whatever `akgl_renderer` points at — which, if `akgl_game_init` ran, is `akgl_default_renderer` with a `NULL` `sdl_renderer`. The failure is an `AKERR_NULLPOINTER` from deep inside a texture load, and it reads like a missing asset. Set `akgl_renderer` to the backend you bound. The example above does. A backend that has been bound but never given an `SDL_Renderer` refuses cleanly rather than crashing: every entry point checks `self->sdl_renderer` and reports `AKERR_NULLPOINTER`. That is the designed-for state and is what makes `_bind` safe to call before you have a renderer. > **A header claim that is no longer true.** `renderer.h` ends > `akgl_render_2d_init`'s documentation with a `@note` saying the two pooled > strings holding the dimensions "are only released on the success path, so each > failed initialization leaks two string slots". They are released in a `CLEANUP` > block that runs on both paths, and `src/renderer.c` carries a comment saying > that is exactly why it was moved there. **There is no leak on the current > code.** Correcting the header comment is a separate commit; this chapter > documents what the code does. ## What the 2D backend refuses **`draw_mesh` always fails.** Every call raises `AKERR_API` with the message "Not implemented". The hook exists so a 3D backend has a slot to fill. It is not a stub that quietly does nothing — a caller that reaches it finds out on the first call, which is the intent: ```c #include #include akerr_ErrorContext *try_the_mesh_hook(void) { PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_renderer->draw_mesh(akgl_renderer)); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_API) { /* The 2D backend has no geometry path. Draw it as textures instead. */ } FINISH(errctx, true); SUCCEED_RETURN(errctx); } ``` **`akgl_render_2d_shutdown` is a documented no-op.** It validates `self` and succeeds. The window and `SDL_Renderer` are still SDL's and are reclaimed by `SDL_Quit`, so there is nothing here to release yet. Call it anyway — a backend that acquires anything will grow a failure path, and the hook is where it will go. ## Where to look next - [Chapter 09](09-drawing.md) — the immediate-mode primitives that draw through the same backend. - [Chapter 12](12-actors.md) — `renderfunc`, `layer`, and what `draw_world` actually calls. - [Chapter 13](13-tilemaps.md) — layers, and what `akgl_tilemap_draw` does with the camera. - [Chapter 07](07-the-game-and-the-frame.md) — where `frame_start` and `frame_end` sit inside `akgl_game_update`.