Files
libakgl/include/akgl/renderer.h
Andrew Kesterson ede3452c49 Split akgl_render_bind2d out of akgl_render_init2d
A host that owns its own window -- an embedded interpreter, which must not
create one -- wants the six 2D function pointers without the window, the
camera and the property registry that came with them. Until now the only
options were to let libakgl own the window or to assign the vtable by hand.

akgl_render_bind2d() installs the methods and nothing else, deliberately
leaving self->sdl_renderer alone so a caller's own renderer survives it and
a caller without one gets a backend that reports AKERR_NULLPOINTER rather
than crashing. akgl_render_init2d() calls it once its window exists.

tests/renderer.c is new and needs no display: the binding, frame start and
end, both draw_texture paths and the draw_mesh stub against a software
renderer under the dummy video driver. src/renderer.c goes from 10% line
coverage to 56%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:51:58 -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_init2d` 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_bind2d() is that half on its own; akgl_render_init2d()
* 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 `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 _RENDERER_H_
#define _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_init2d.
*/
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_init2d.
*/
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 `gamemap` through `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 `gamemap`, `camera`, and `HEAP_ACTOR` 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 `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_init2d(), 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_init2d() 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_bind2d(akgl_RenderBackend *self);
/**
* @brief Create the window and SDL renderer, and bind the 2D backend's methods.
*
* 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 with `game.uri` as the title, points `camera`
* at the full screen rectangle, and then calls akgl_render_bind2d() 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 `game.screenwidth` or `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_init2d(akgl_RenderBackend *self);
#endif // _RENDERER_H_