Files
libakgl/include/akgl/renderer.h
Andrew Kesterson 9924d74dcc Namespace every exported symbol, and bump to 0.5.0
Closes internal-consistency items 1 through 6, 12 and 13. Every include guard
is _AKGL_<FILE>_H_, every in-project header include is angled, and every
exported function, type and global carries the akgl_ prefix. This is an ABI
break; the soname goes to libakgl.so.0.5. TODO.md carries the full rename
table.

The renames were driven by renaming each declaration and letting the compiler
find the uses, not by pattern substitution: renderer, physics and camera are
also parameter and struct-member names, and a sed would have rewritten
map->physics and every akgl_RenderBackend *renderer parameter without a word.

Item 4 turned out not to be cosmetic. The library exported a global called
renderer and tests/character.c defined an SDL_Renderer *renderer of its own;
the executable's definition preempted the library's, akgl_sprite_load_json
read a SDL_Renderer * through an akgl_RenderBackend *, and every texture load
in that suite failed. The suite reported success anyway, because libakerror's
unhandled-error handler ends in exit(errctx->status), exit keeps only the low
byte, and AKGL_ERR_SDL is exactly 256. So character had been green while
running one of its four tests, and every suite in the tree was unable to fail
on the most common status in a library built on SDL.

Both are fixed. tests/testutil.h gains TEST_TRAP_UNHANDLED_ERRORS(), which
collapses any status a byte cannot carry onto 1, and every suite installs it.
character binds a real backend with akgl_render_2d_bind. Its fourth test then
runs for the first time and fails on a defect it has asserted all along, so
akgl_heap_release_character now walks state_sprites with
AKGL_ITERATOR_OP_RELEASE and destroys the property set before zeroing the slot
-- TODO.md Defects item 21 and half of Carried over item 1.

AKGL_TIME_ONESEC_MS said "one second in milliseconds" and held 1000000, so
akgl_game_state_lock waited roughly sixteen minutes rather than one second. It
is AKGL_TIME_ONEMS_NS now, the budget is its own named constant, and
tests/game.c holds the mutex from a second thread to assert the wait -- the
contended path had no coverage at all.

Headers are self-contained and it is enforced: AKGL_PUBLIC_HEADERS drives both
install() and a generated translation unit per header, so a header that ships
is a header that is checked. Writing that found registry.h, which used
SDL_PropertiesID in eight declarations and included no SDL header.

23/23 suites pass, memcheck is clean, reindent --check is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:35 -04:00

201 lines
11 KiB
C

/**
* @file renderer.h
* @brief The pluggable rendering backend: a record of function pointers plus an initializer.
*
* There is one backend shipped, the 2D SDL one, and `akgl_render_2d_init` is its
* initializer -- it creates the window and the `SDL_Renderer` and then fills in
* the six `akgl_render_2d_*` entry points. A different renderer is a different
* initializer populating the same struct, not a branch inside these functions.
*
* Those are two separable jobs, and a host that already owns an `SDL_Renderer`
* -- an embedded interpreter, which must not create the window -- wants only the
* second. akgl_render_2d_bind() is that half on its own; akgl_render_2d_init()
* makes the window and then calls it.
*
* Callers do not normally name the `akgl_render_2d_*` functions directly; they
* go through the pointers on the backend (`renderer->frame_start(renderer)`),
* which is what makes the swap possible. The global `akgl_renderer` in game.h is the
* instance the rest of the library draws through.
*
* A frame is `frame_start` (clear), any number of `draw_*` calls, then
* `frame_end` (present).
*/
#ifndef _AKGL_RENDERER_H_
#define _AKGL_RENDERER_H_
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/iterator.h>
/** @brief Defines a pluggable renderer backend and drawing callbacks. */
typedef struct akgl_RenderBackend {
SDL_Renderer *sdl_renderer; /**< The SDL renderer, created by the initializer. Owned by SDL, not freed here. */
akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self); /**< Tear the backend down. */
akerr_ErrorContext AKERR_NOIGNORE *(*frame_start)(struct akgl_RenderBackend *self); /**< Begin a frame: clear the target. */
akerr_ErrorContext AKERR_NOIGNORE *(*frame_end)(struct akgl_RenderBackend *self); /**< End a frame: present it. */
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); /**< Blit one texture. */
akerr_ErrorContext AKERR_NOIGNORE *(*draw_mesh)(struct akgl_RenderBackend *self); /**< Reserved for a 3D backend; the 2D one refuses. */
akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags); /**< Draw the whole scene, layer by layer. */
} akgl_RenderBackend;
/**
* @brief Tear the 2D backend down.
*
* A no-op placeholder that always succeeds. The window and `SDL_Renderer` this
* backend created are still owned by SDL and are reclaimed by `SDL_Quit`, so
* there is nothing here to release yet; the hook exists so a backend that *does*
* own resources has somewhere to free them.
*
* @param self The backend to shut down. Ignored -- not even dereferenced, so
* `NULL` is currently harmless.
* @return `NULL` always. It has no failure path today; check it anyway, because
* a backend that acquires anything will grow one.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self);
/**
* @brief Begin a frame: clear the render target to opaque black.
* @param self The backend to draw through. Required, and dereferenced *before*
* it is checked -- a `NULL` @p self is a crash, not an error.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If `self->sdl_renderer` is `NULL`, which means the
* backend was never run through akgl_render_2d_init.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self);
/**
* @brief End a frame: present everything drawn since akgl_render_2d_frame_start.
* @param self The backend to draw through. Required, and dereferenced *before*
* it is checked -- a `NULL` @p self is a crash, not an error.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If `self->sdl_renderer` is `NULL`, which means the
* backend was never run through akgl_render_2d_init.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self);
/**
* @brief Blit a texture, rotating it only when asked to.
*
* A rotation of exactly 0 takes the plain `SDL_RenderTexture` path, which is why
* @p center and @p flip are only consulted when @p angle is non-zero.
*
* @param self The backend to draw through. Required.
* @param texture The texture to blit. Required.
* @param src The rectangle of @p texture to take, in pixels. Optional --
* `NULL` means the whole texture.
* @param dest Where to put it on the render target, in pixels. Optional --
* `NULL` means stretch to fill the whole target. Not an output
* parameter despite the name.
* @param angle Clockwise rotation in degrees. Exactly 0 skips rotation
* entirely, along with @p center and @p flip.
* @param center Pivot for the rotation, relative to @p dest. Required *when*
* @p angle is non-zero, ignored otherwise. SDL's own "NULL means
* the centre of dest" convention is not available here.
* @param flip Horizontal/vertical mirroring. Applied only on the rotated
* path; ignored when @p angle is 0.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or @p texture is `NULL`; if @p angle is
* non-zero and @p center is `NULL`; or if the SDL draw call itself
* fails, in which case the message carries `SDL_GetError()`. That last
* case is a reused status rather than a pointer problem.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip);
/**
* @brief Draw geometry. Not implemented by the 2D backend.
*
* The hook exists so a 3D backend has a slot to fill; this one refuses every
* call. It is not a stub that quietly does nothing -- it fails loudly, so a
* caller that reaches it finds out at once.
*
* @param self The backend. Ignored.
* @return Never `NULL`.
* @throws AKERR_API Always, with the message "Not implemented".
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self);
/**
* @brief Draw the whole scene: every tilemap layer, and the actors standing on it.
*
* Walks layers from 0 to #AKGL_TILEMAP_MAX_LAYERS. For each, it draws that layer
* of the global `akgl_gamemap` through `akgl_camera` (if the map has that many layers),
* then sweeps the actor pool and calls `renderfunc` on every live actor whose
* `layer` matches. Sweeping in layer order is what puts actors in front of the
* scenery they stand on and behind the scenery they walk under.
*
* It reads the globals `akgl_gamemap`, `akgl_camera`, and `akgl_heap_actors` directly rather
* than taking them as arguments, so there is exactly one world to draw.
*
* @param self The backend to draw through. Required.
* @param opflags Iterator flags. Optional -- `NULL` substitutes a zeroed set.
* Currently ignored either way: this function sweeps the actor
* pool itself instead of going through
* akgl_registry_iterate_actor, so no `AKGL_ITERATOR_OP_*` bit
* reaches anything. The parameter is here for the backend
* signature and for the layer-mask support that has yet to be
* wired up.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_* Whatever akgl_tilemap_draw or an actor's own `renderfunc`
* raises; the first failure aborts the frame and propagates unchanged.
*
* @warning Neither the global `akgl_gamemap` nor a live actor's `renderfunc` is
* checked before use, so drawing before akgl_tilemap_load, or with a
* hand-built actor that was never run through akgl_actor_initialize,
* is a crash rather than an error context.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags);
/**
* @brief Install the 2D backend's methods on a backend the caller owns.
*
* The vtable half of akgl_render_2d_init(), with no window, no renderer, and no
* property registry involved: it points @p self's six function pointers at the
* `akgl_render_2d_*` entry points and returns. `sdl_renderer` is not touched,
* so a caller who has already put its own `SDL_Renderer` there keeps it, and a
* caller who has not gets a backend whose entry points all report
* `AKERR_NULLPOINTER` rather than crash.
*
* This is the entry point for a host that owns its own window -- it can drive
* akgl_actor_render(), akgl_tilemap_draw() and the rest through a backend
* libakgl never created. akgl_render_2d_init() is the same thing with a window
* in front of it.
*
* @param self The backend to bind. Required. Only the method pointers are
* written.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_bind(akgl_RenderBackend *self);
/**
* @brief Create the window and SDL renderer, and bind the 2D backend's methods.
*
* Reads `akgl_game.screenwidth` and `akgl_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 with `akgl_game.uri` as the title, points `akgl_camera`
* at the full screen rectangle, and then calls akgl_render_2d_bind() to install
* the six `akgl_render_2d_*` function pointers on @p self.
*
* Because the dimensions come from the registry, akgl_registry_init_properties
* and the property writes have to happen first -- see the note on
* akgl_registry_init about the properties registry not being initialized for
* callers that skip akgl_game_init.
*
* @param self The backend to initialize. Required. Its `sdl_renderer` and method
* pointers are overwritten; anything it held before is not released.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_VALUE If `akgl_game.screenwidth` or `akgl_game.screenheight` is set to
* something that is not a base-10 integer.
* @throws ERANGE If either dimension does not fit in an `int`.
* @throws AKGL_ERR_SDL If the window and renderer cannot be created. The message
* carries `SDL_GetError()`.
* @throws AKGL_ERR_HEAP If the string pool is exhausted while reading the two
* properties.
*
* @note The two pooled strings holding the dimensions are only released on the
* success path, so each failed initialization leaks two string slots.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_init(akgl_RenderBackend *self);
#endif // _AKGL_RENDERER_H_