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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
@@ -6,24 +6,24 @@
|
||||
* 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. `renderer`, `physics`, `camera`, and
|
||||
* `gamemap` are globals pointing at the `_akgl_*` storage below them, so a
|
||||
* There is exactly one of everything. `akgl_renderer`, `akgl_physics`, `akgl_camera`, and
|
||||
* `akgl_gamemap` are globals pointing at the `akgl_default_*` storage below them, 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 that actually works:
|
||||
*
|
||||
* 1. fill in `game.name`, `game.version`, and `game.uri` -- akgl_game_init
|
||||
* 1. fill in `akgl_game.name`, `akgl_game.version`, and `akgl_game.uri` -- akgl_game_init
|
||||
* refuses to run without them;
|
||||
* 2. akgl_game_init();
|
||||
* 3. akgl_registry_load_properties() or akgl_set_property(), to configure
|
||||
* screen size, physics constants and so on;
|
||||
* 4. akgl_render_init2d(renderer) and akgl_physics_factory(physics, ...), both
|
||||
* 4. akgl_render_2d_init(renderer) and akgl_physics_factory(physics, ...), both
|
||||
* of which read that configuration;
|
||||
* 5. load assets, then loop on akgl_game_update().
|
||||
*
|
||||
* @warning None of this is thread-safe beyond the `game.state` mutex, and that
|
||||
* @warning None of this is thread-safe beyond the `akgl_game.state` mutex, and that
|
||||
* mutex protects the state flags, not the pools or the registries.
|
||||
*/
|
||||
|
||||
@@ -32,10 +32,10 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
#include "types.h"
|
||||
#include "tilemap.h"
|
||||
#include "renderer.h"
|
||||
#include "physics.h"
|
||||
#include <akgl/types.h>
|
||||
#include <akgl/tilemap.h>
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/physics.h>
|
||||
// AKGL_VERSION used to be defined here by hand, which is how akgl.pc came to
|
||||
// ship an empty Version field: nothing tied the two together.
|
||||
#include <akgl/version.h>
|
||||
@@ -48,15 +48,29 @@
|
||||
/** @brief Nanoseconds in one second. The unit `SDL_GetTicksNS` reports in. */
|
||||
#define AKGL_TIME_ONESEC_NS 1000000000
|
||||
/**
|
||||
* @brief Misnamed: this is nanoseconds per **millisecond**, not milliseconds per second.
|
||||
* @brief Nanoseconds in one millisecond. The scale factor from JSON durations to internal ones.
|
||||
*
|
||||
* One second in milliseconds is 1000. 1000000 is one millisecond in
|
||||
* nanoseconds, which is how sprite and character load actually use it -- as a
|
||||
* milliseconds-to-nanoseconds scale factor -- and is not how
|
||||
* akgl_game_state_lock uses it. TODO.md item 6 proposes renaming it
|
||||
* `AKGL_TIME_ONEMS_NS`.
|
||||
* Sprite and character definitions state frame durations in milliseconds
|
||||
* because that is what a human writing one wants to type; everything that
|
||||
* compares them against `SDL_GetTicksNS` needs nanoseconds.
|
||||
*
|
||||
* This was called `AKGL_TIME_ONESEC_MS` until 0.5.0, which said "one second in
|
||||
* milliseconds" and held 1000000 -- a name and a value that described two
|
||||
* different quantities. Both callers that meant the scale factor were right;
|
||||
* the one that read it as a one-second budget was wrong by a factor of a
|
||||
* thousand. See akgl_game_state_lock.
|
||||
*/
|
||||
#define AKGL_TIME_ONESEC_MS 1000000
|
||||
#define AKGL_TIME_ONEMS_NS 1000000
|
||||
|
||||
/**
|
||||
* @brief How long akgl_game_state_lock keeps trying for the state mutex, in milliseconds.
|
||||
*
|
||||
* A deliberate ceiling rather than a blocking wait: a deadlock here reports an
|
||||
* error the caller can act on instead of hanging the process.
|
||||
*/
|
||||
#define AKGL_GAME_STATE_LOCK_BUDGET_MS 1000
|
||||
/** @brief How long akgl_game_state_lock sleeps between attempts, in milliseconds. */
|
||||
#define AKGL_GAME_STATE_LOCK_RETRY_MS 100
|
||||
|
||||
/* ==================== GAME STATE VARIABLES =================== */
|
||||
|
||||
@@ -82,39 +96,39 @@ typedef struct {
|
||||
SDL_Mutex *statelock; /**< Guards `state`. Created by akgl_game_init. */
|
||||
int16_t fps; /**< Frames drawn during the last completed second. Recomputed once per second, not per frame. */
|
||||
SDL_Time gameStartTime; /**< `SDL_GetTicksNS()` at akgl_game_init. */
|
||||
SDL_Time lastIterTime; /**< Timestamp of the most recent akgl_game_updateFPS call. */
|
||||
SDL_Time lastIterTime; /**< Timestamp of the most recent akgl_game_update_fps call. */
|
||||
SDL_Time lastFPSTime; /**< When `fps` was last recomputed. */
|
||||
int16_t framesSinceUpdate; /**< Frames counted so far in the current second. */
|
||||
void (*lowfpsfunc)(void); /**< Called every frame while `fps` is under 30. Defaults to akgl_game_lowfps; replace it to do something more useful than log. */
|
||||
} akgl_Game;
|
||||
|
||||
/** @brief The SDL window, created by akgl_render_init2d. `NULL` until then. */
|
||||
extern SDL_Window *window;
|
||||
/** @brief The SDL window, created by akgl_render_2d_init. `NULL` until then. */
|
||||
extern SDL_Window *akgl_window;
|
||||
/** @brief The background music, loaded by akgl_load_start_bgm. `NULL` until then. */
|
||||
extern MIX_Audio *bgm;
|
||||
extern MIX_Audio *akgl_bgm;
|
||||
/** @brief The mixer device, created by akgl_game_init. Everything audio goes through it. */
|
||||
extern MIX_Mixer *akgl_mixer;
|
||||
/** @brief Playback tracks by slot. #AKGL_GAME_AUDIO_TRACK_BGM is the music track; the rest are the application's to assign. */
|
||||
extern MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
|
||||
/** @brief Storage behind the default `camera`. Point `camera` elsewhere rather than reaching for this. */
|
||||
extern SDL_FRect _akgl_camera;
|
||||
/** @brief Storage behind the default `akgl_camera`. Point `akgl_camera` elsewhere rather than reaching for this. */
|
||||
extern SDL_FRect akgl_default_camera;
|
||||
/** @brief The one game object: metadata, timing, and FPS accounting. */
|
||||
extern akgl_Game game;
|
||||
/** @brief Storage behind the default `renderer`. */
|
||||
extern akgl_RenderBackend _akgl_renderer;
|
||||
/** @brief Storage behind the default `physics`. */
|
||||
extern akgl_PhysicsBackend _akgl_physics;
|
||||
/** @brief Storage behind the default `gamemap`. */
|
||||
extern akgl_Tilemap _akgl_gamemap;
|
||||
extern akgl_Game akgl_game;
|
||||
/** @brief Storage behind the default `akgl_renderer`. */
|
||||
extern akgl_RenderBackend akgl_default_renderer;
|
||||
/** @brief Storage behind the default `akgl_physics`. */
|
||||
extern akgl_PhysicsBackend akgl_default_physics;
|
||||
/** @brief Storage behind the default `akgl_gamemap`. */
|
||||
extern akgl_Tilemap akgl_default_gamemap;
|
||||
|
||||
/** @brief Currently active tilemap. */
|
||||
extern akgl_Tilemap *gamemap;
|
||||
extern akgl_Tilemap *akgl_gamemap;
|
||||
/** @brief Currently active renderer. */
|
||||
extern akgl_RenderBackend *renderer;
|
||||
extern akgl_RenderBackend *akgl_renderer;
|
||||
/** @brief Currently active physics backend. */
|
||||
extern akgl_PhysicsBackend *physics;
|
||||
extern akgl_PhysicsBackend *akgl_physics;
|
||||
/** @brief Currently active camera. */
|
||||
extern SDL_FRect *camera;
|
||||
extern SDL_FRect *akgl_camera;
|
||||
|
||||
/**
|
||||
* @brief True when every bit of `y` is set in `x`. Not "any of them" -- all of them.
|
||||
@@ -141,14 +155,14 @@ extern SDL_FRect *camera;
|
||||
* pools, create the registries, hand SDL the app metadata, clear the control
|
||||
* maps, `SDL_Init` video/gamepad/audio, load the bundled controller database,
|
||||
* open any attached gamepads, start SDL_mixer and SDL_ttf, and finally point
|
||||
* `renderer`, `physics`, `camera`, and `gamemap` at their default storage.
|
||||
* `akgl_renderer`, `akgl_physics`, `akgl_camera`, and `akgl_gamemap` at their default storage.
|
||||
*
|
||||
* What it does *not* do: create the window, choose a physics backend, or load
|
||||
* any configuration. Those read properties, so they come after the caller has
|
||||
* set them. See the sequence at the top of this file.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If `game.name`, `game.version`, or `game.uri` is
|
||||
* @throws AKERR_NULLPOINTER If `akgl_game.name`, `akgl_game.version`, or `akgl_game.uri` is
|
||||
* empty. All three are required and there are no defaults -- the
|
||||
* window title, SDL's app metadata, and the savegame compatibility
|
||||
* check are all built from them.
|
||||
@@ -168,7 +182,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init();
|
||||
* @brief Declared but never defined. Do not call.
|
||||
*
|
||||
* There is no definition anywhere in the library, so a translation unit that
|
||||
* calls this compiles and then fails to link. Screen setup is akgl_render_init2d.
|
||||
* calls this compiles and then fails to link. Screen setup is akgl_render_2d_init.
|
||||
* Tracked in TODO.md under header/implementation surface drift.
|
||||
*
|
||||
* @return Nothing; it cannot be called.
|
||||
@@ -178,13 +192,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen();
|
||||
* @brief Count this frame, and recompute the frame rate once a second has passed.
|
||||
*
|
||||
* Called at the top of akgl_game_update, so a caller running its own loop needs
|
||||
* to call it itself. `game.fps` is only refreshed when a full second has
|
||||
* to call it itself. `akgl_game.fps` is only refreshed when a full second has
|
||||
* elapsed, so it is a completed-second average rather than an instantaneous
|
||||
* figure -- and it reads 0 for the first second of the process, which is under
|
||||
* the low-FPS threshold and so fires `lowfpsfunc` on every frame until the first
|
||||
* second is up.
|
||||
*/
|
||||
void akgl_game_updateFPS();
|
||||
void akgl_game_update_fps();
|
||||
/**
|
||||
* @brief Write the game state and the name-to-pointer tables to a save file.
|
||||
*
|
||||
@@ -236,9 +250,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath);
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath);
|
||||
/**
|
||||
* @brief The default `game.lowfpsfunc`: log the current frame rate.
|
||||
* @brief The default `akgl_game.lowfpsfunc`: log the current frame rate.
|
||||
*
|
||||
* Called from akgl_game_updateFPS on every frame where `game.fps` is under 30.
|
||||
* Called from akgl_game_update_fps on every frame where `akgl_game.fps` is under 30.
|
||||
* It is a placeholder -- the point of the hook is that a game can replace it
|
||||
* with something that actually sheds work.
|
||||
*/
|
||||
@@ -251,15 +265,16 @@ void akgl_game_lowfps(void);
|
||||
*
|
||||
* @return `NULL` once the lock is held, otherwise an error context owned by the
|
||||
* caller.
|
||||
* @throws AKGL_ERR_SDL If the budget runs out with the lock still held
|
||||
* elsewhere. The message carries `SDL_GetError()`, which after a failed
|
||||
* `SDL_TryLockMutex` is usually stale or empty -- the status is the
|
||||
* signal, not the text.
|
||||
* @throws AKGL_ERR_SDL If #AKGL_GAME_STATE_LOCK_BUDGET_MS elapses with the lock
|
||||
* still held elsewhere. `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.
|
||||
*
|
||||
* @note The budget is meant to be one second but is not: the loop counts against
|
||||
* #AKGL_TIME_ONESEC_MS, which is nanoseconds-per-millisecond (1000000)
|
||||
* rather than milliseconds-per-second, so it retries 10,000 times at 100 ms
|
||||
* and gives up after roughly 16 minutes. TODO.md item 6.
|
||||
* @note Before 0.5.0 the loop counted against a constant named
|
||||
* `AKGL_TIME_ONESEC_MS` that held 1000000, so it retried 10,000 times at
|
||||
* 100 ms and blocked for roughly sixteen minutes rather than the one
|
||||
* second documented here.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void);
|
||||
/**
|
||||
@@ -276,8 +291,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
|
||||
*
|
||||
* Takes the state lock, counts the frame, then walks layers 0 through
|
||||
* #AKGL_TILEMAP_MAX_LAYERS calling each live actor's `updatefunc`, optionally
|
||||
* rescaling it to the tilemap first. Then it steps `physics` and draws through
|
||||
* `renderer`, and releases the lock.
|
||||
* rescaling it to the tilemap first. Then it steps `akgl_physics` and draws through
|
||||
* `akgl_renderer`, and releases the lock.
|
||||
*
|
||||
* @param opflags Iterator flags. Optional -- `NULL` selects a default set that
|
||||
* sweeps one layer at a time, in which case `layerid` is advanced
|
||||
|
||||
Reference in New Issue
Block a user