83.9% line and 91.2% function, from 79.6% / 87.2%. src/tilemap.c moved the
most, 47% to 62%, because bounding the loaders and proving the pool accounting
needed them driven rather than merely called.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes Defects items 20, 22, 23 and 29, and the residual of item 38.
The tilemap load leak was five pooled strings per load; the property-lookup
fix in an earlier commit took it to two, and the last two were each a claim
with no matching release -- the string every layer's `type` was read into, and
the dirname the map's relative paths resolve against. tests/tilemap.c asserts
the pool is exactly where it started after one load/release cycle and after 64.
Finding those two was a matter of dumping the contents of every still-claimed
slot after a cycle rather than reading the code again; 'tilelayer' and an
assets directory named themselves immediately.
Same file, same class, fixed with it: akgl_tilemap_load_layer_objects released
its scratch string after reading each object's name and then kept using the
slot, because akgl_get_json_string_value reuses a non-NULL destination without
taking another reference. The slot was free while still live, so any other
claim could have been handed it.
akgl_character_sprite_add wrote over an existing binding without releasing the
sprite it displaced, so a character that rebinds a state while alive leaked a
sprite slot per rebind -- teardown only gives back what the map holds at the
end. The new reference is taken before the write and given back if the write
fails, so there is no window where a sprite is bound with nothing behind it.
The write was unchecked too.
Three failure-path leaks moved into CLEANUP blocks: akgl_render_2d_init's two
pooled strings, akgl_controller_open_gamepads' enumeration array, and
akgl_text_rendertextat's surface and texture -- the last being a leak per frame
on a HUD line.
akgl_text_unloadallfonts() closes every font in the registry and destroys it,
which is what item 38 left open. Deliberately not a whole akgl_game_shutdown:
tearing down the mixer, SDL_ttf and SDL in the right order is a design
question, and this is the part that was simply missing. It is a new public
symbol, which 0.5.0 already covers -- this release has not shipped.
Every fix has a test that fails against the old code.
25/25 pass, memcheck clean, reindent --check, check_api_surface and
check_error_protocol all clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
akgl_game_update_fps installs the default itself now, so the benchmark no
longer has to. Leaving the hook NULL there is what keeps that true.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes Defects items 30 and 31 and Known-and-still-open items 1, 2, 5, 9 and 11.
Both string accessors in json_helpers.c ended their ATTEMPT block with
FINISH(errctx, false), which swallows the failure, and then strncpy'd through
the pointer akgl_heap_next_string never set. So the one condition the pool
exists to report -- it is full, which in practice means something is not
releasing -- arrived as a segfault somewhere else entirely. It is
FINISH(errctx, true) now, and tests/json_helpers.c claims every slot and
asserts AKGL_ERR_HEAP comes back out of both. That test segfaults against the
old code, which is also how the tilemap leak test in the previous commit
confirmed this one.
akgl_tilemap_release tested layers[i].texture and destroyed
tilesets[i].texture, so every tileset texture was freed twice on one release
and no image layer's texture was freed at all. Pointers are cleared as they go,
so a second release is safe instead of a use-after-free.
akgl_game_update_fps called game.lowfpsfunc() unguarded, on a path taken on
frame one because fps is 0 for the first second. Only akgl_game_init installs
it, and renderer.h documents the other path deliberately -- a host that owns
its window binds a backend instead. It installs the default when it finds NULL.
akgl_controller_pushmap and akgl_controller_default checked only the upper
bound, so a negative id indexed before akgl_controlmaps.
The two test-harness helpers were quietly worthless. akgl_render_and_compare
drew t1 on both passes, so it always reported a match and every image assertion
built on it asserted nothing; and akgl_compare_sdl_surfaces memcmp'd
s1->pitch * s1->h bytes out of both surfaces without checking that the second
was the same size, so a smaller one was read past its end. Both fixed, both
tested. tests/util.c also now calls the collide-point test it has defined and
never run.
25/25 pass, memcheck clean, reindent --check and check_error_protocol clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes Defects items 16 and 17 and Known-and-still-open item 6. All three let
an asset file, or a caller's argument, write past a fixed array.
akgl_sprite_load_json took its frame count straight from the document and wrote
that many entries into a 16-byte frameids -- through a uint32_t * cast of a
uint8_t *, so each write touched four bytes and the overrun reached four bytes
past the array, into the rest of akgl_Sprite and then the next pool slot. The
count is checked first now, each id is read into an int and narrowed
deliberately, and a frame number too large for a uint8_t is refused rather than
truncated into an index for a different tile.
The tilemap loader had the same shape twice: objects[j] with no check against
AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER and tilesets[i] with none against
AKGL_TILEMAP_MAX_TILESETS. akgl_tilemap_load_layers already bounded its own
loop, so the pattern was in the same file. The object one is the reachable
half -- 128 objects is not a large object layer.
akgl_string_initialize zeroed sizeof(akgl_String) starting at `data`, which
begins after the refcount in front of it, so it ran four bytes past the end of
the object and onto the *next* slot's refcount -- the field the allocator reads
to decide whether a slot is free. Same file, same class, fixed with it:
akgl_string_copy accepted a count larger than the buffers, reading past one
pool slot and writing past another, which the header documented as behaviour.
Every case has a test that fails against the old code, with five new fixtures.
Exactly-the-maximum is asserted alongside one-past in each, so the bound cannot
be fixed by making the limit off by one.
25/25 pass, memcheck clean, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes internal-consistency items 19 through 36, 38 and 41. Item 37, the ~180
redundant casts, is deliberately left open with its reasoning in TODO.md: the
benefit only arrives once the build turns on the warnings those casts suppress,
and doing it before that is churn across the two files with the most
outstanding functional defects.
The two that were real bugs are in the actor state table.
AKGL_ACTOR_STATE_STRING_NAMES was declared [AKGL_ACTOR_MAX_STATES+1] and
defined [32], so a consumer trusting the declared bound read past the object;
and indices 11 and 12 were named UNDEFINED_11 and UNDEFINED_12 where actor.h
has MOVING_IN and MOVING_OUT, so no character JSON could bind a sprite to
either state. tests/registry.c now walks the whole table -- every entry
non-NULL, every entry resolving to its own bit, no two entries sharing a name.
The bitmask macros are parenthesized and AKGL_BITMASK_CLEAR has lost the
semicolon inside its body. Writing tests/bitmasks.c for that turned up
something worth knowing: the obvious test does not catch it. For a bit that is
set, the misparse `!(mask & bit) == bit` gives the same answer as the correct
one. It only diverges for an unset bit whose value is not 1, and that is the
shape the suite uses now.
akgl_draw_background was the last public function outside the error protocol.
It takes a backend like everything else in draw.h, restores the draw colour it
found, and is tested -- TODO.md had it filed under "needs the offscreen
renderer harness", which was never true; what it needed was to stop reading the
global.
All eight registry initializers go through one helper, so the seven that leaked
an SDL_PropertiesID on every call after the first no longer do. Fixed in the
same place because it is the same function: akgl_registry_init never called
akgl_registry_init_properties, which made akgl_set_property a silent no-op for
anyone not going through akgl_game_init -- Defects, Known and still open item 3.
Also: AKGL_COLLIDE_RECTANGLES (three open parens, two closes) and akgl_Frame
deleted, float32_t/float64_t used consistently, the developer-specific debug
logging removed from the controller inner loop, the abandoned SDL_GetBasePath
comments removed, nine unused locals removed, and dst renamed to dest.
akgl_game_update's default flags no longer OR the same bit twice. That changes
nothing today, and the reason is Performance item 32: the loop never reads
either bit, which is why every actor is updated sixteen times a frame. Still
open.
25/25 pass, memcheck clean, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes internal-consistency item 18. The convention was settled -- AGENTS.md
records errctx winning 92 sites to 45 -- but the other 45 were still there, and
four files favoured `e` throughout while six favoured errctx, sometimes within
one file.
Its own commit because it is a rename and nothing else. All 333 changed lines
are a single identifier substitution: applying \be\b -> errctx to each removed
line reproduces the added line exactly, and the counts match at 333 either way.
`e` keeps its meaning where the convention actually wants it -- an incoming
error context being inspected, as in akgl_get_json_with_default(e, ...) -- which
is why that function was left alone.
25/25 pass, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes internal-consistency items 16 and 17.
Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and
skip every release in it. The one that mattered was the success path of
akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries
on every lookup that *found* what it was asked for, and a map load does that
several times per layer. tests/tilemap.c now runs each of its three paths --
found, absent, wrong type -- twice the pool size and asserts the pool is where
it started. Against the old code that test does not merely fail, it segfaults,
which is Defects item 30 seen from the outside: pool exhaustion arriving as a
NULL strncpy rather than as AKGL_ERR_HEAP.
Two of the conversions needed more than swapping the macro. In
akgl_get_json_tilemap_property a plain break would have fallen through to the
"property not found" FAIL_RETURN after FINISH, reporting a miss for something
found, so the success path sets a flag. In akgl_collide_rectangles the eight
early exits are followed by `*collide = false;`, which would have overwritten
the hit that broke out; each corner test writes the flag itself, so that line
is gone rather than moved. The same function also released its scratch string
once per loop iteration while continuing to use it, so the slot was free while
still live -- one claim now covers the whole scan.
akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was
the last statement in the ATTEMPT block, so the path that falls out of FINISH
reached the closing brace of a non-void function.
scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside
ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test.
Neither produces a compiler diagnostic and neither fails a test run until the
pool it drains is empty, which is why both have already shipped once.
For item 17: the eight typed JSON accessors that validated their container and
then wrote through dest unconditionally now check key and dest as the two
string accessors always did; the null physics backend checks its actors like
the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check
self, which the first two read straight through. tests/renderer.c calls all
three with NULL, which segfaulted before.
25/25 pass, memcheck clean, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.
The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.
The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.
scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.
The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.
Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.
24/24 pass, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
akgl_Actor::scale is one float32_t applied to both dest.w and dest.h, so
"twice as wide, the same height" cannot be expressed. The VIC-II has had
separate x- and y-expand bits since 1982 and Commodore BASIC 7.0's SPRITE
verb exposes both, so a BASIC interpreter drawing through this library
cannot implement its own documented verb.
Reached from akbasic, whose sprites are 24x21. It works around this and
defect 26 together by installing its own renderfunc on each actor rather
than patching around the library.
Two other defects were filed alongside this one before the rebase and
both are gone: the drawn-height-from-width bug is already defect 26, and
the akgl_path_relative context leak was fixed in c622754.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CMake chains command overrides one level deep: a second override rebinds
_add_test to the first and the builtin becomes unreachable to everyone. So two
projects in one tree cannot both shadow add_test, and akbasic has to shadow it
first for libakerror and libakstdlib -- which left its own registrations
recursing to CMake's depth limit.
The unconditional shadow arrived with the vendored-dependency fix in 18399f2.
That half stays; only the guard comes back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It was reporting and shrugging. A definite leak, a read past the end of an
allocation or a branch on uninitialised memory now fails the build on the
push that introduces it, which is the only point at which it is cheap to
fix.
The six findings the job had on its first run were fixed rather than
excused, so the gate closes on a clean tree instead of on a backlog.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six findings, all of them libakgl's, all closed. The memcheck run is clean.
Not one of the four json_load_file calls in src/ was ever matched by a
json_decref, so every asset load abandoned its parsed document: 1.5 KB per
sprite, 2.1 KB per character, 9 KB per load of the 2x2 fixture map, and on
the order of a megabyte for a real one. Each loader now releases it in the
CLEANUP block it already had, on the success path as well as the failure
one. akgl_registry_load_properties needed its loop moved inside the ATTEMPT
block first: props is a borrowed reference into the document and is read
after the block ends, so every exit from that loop leaked the whole tree.
akgl_get_property copied a fixed AKGL_MAX_STRING_LENGTH bytes out of what
SDL handed back, which is SDL's strdup of the value -- four bytes for
"0.0". That read up to 4 KiB past the end of somebody else's allocation on
every property read, returned whatever was there past the terminator, and
would have faulted on a value that landed at the end of a page. It copies
the value and its terminator now, and refuses a value too long for an
akgl_String rather than truncating it into an unterminated buffer. The
header note that described the overread as a quirk describes correct
behaviour instead.
The four savegame name tables wrote a fixed-width field starting at the
registry key, and SDL sizes that allocation to the name. They read past it
on every entry and put what they found into the save file: up to half a
kilobyte of this process's heap per registered object, in a file a player
might send to somebody. They stage through a zeroed buffer now, and a
negative-array-size typedef fails the build if a table's width ever outgrows
it.
akgl_controller_list_keyboards never freed the array SDL_GetKeyboards
allocated for it.
A font could be opened and published and never handed back -- there was no
way to close one, so a game that changed fonts between scenes leaked ten
kilobytes each time, and loading over a live name leaked the font it
displaced. akgl_text_unloadfont is that missing half, and akgl_text_loadfont
calls it when it replaces a name, after the new font has opened so a failed
reload leaves the caller with the font they had. A new public symbol takes
the version to 0.4.0 and the soname with it: an 0.3 consumer cannot be
handed this library and told it is the same ABI.
tests/registry.c fills a destination with a sentinel and asserts the bytes
past the terminator survive a read, which fails against the old copy.
tests/text.c covers unload, double unload, unloading a name that was never
registered, and replacement closing the displaced font. The JSON releases
have no test of their own and cannot sensibly have one -- nothing in the
public API can observe a jansson refcount -- so the memcheck run is their
test, which is an argument for gating it rather than against.
The two remaining findings are in deps/semver's own unit test, which is
vendored. They are suppressed by function name, so a rewrite of those cases
comes back as a finding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The perf suites were already running in CI, because they are ordinary CTest
tests -- but only in the coverage job, which is a Debug build with gcov
instrumentation. That is the one place their numbers cannot mean anything,
and it was spending full-scale iteration counts to prove it. They now run
there at AKGL_BENCH_SCALE=0.02, for path coverage alone, which takes the
whole coverage run to four seconds.
The `performance` job is where the timings are taken: RelWithDebInfo, full
scale, `ctest -L perf`, which is the only configuration in which
tests/benchutil.h enforces a budget. It keeps the tables as an artifact
whether it passes or fails -- a red run says which measurement moved, and a
green one is the next baseline for PERFORMANCE.md. The step uses --verbose
rather than --output-on-failure, because --output-on-failure prints nothing
at all for a passing benchmark, and sets pipefail, without which the status
reaching the runner is tee's and a blown budget reads as a pass.
The `memory_check` job runs scripts/memcheck.sh over every suite under
valgrind and keeps the logs. It carries continue-on-error, and that is a
deliberate, temporary lie: the six findings in TODO.md "Memory checking" are
real and all of them are libakgl's, so gating on it today would only teach
everyone to ignore a red build. The flag comes off with the commit that
closes the last item, and item 34 -- the missing json_decref in all four
asset loaders -- is four lines.
Both new jobs were run locally against a clean copy of the tree with the
exact commands the workflow uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cmake --build build --target memcheck` runs every registered CTest suite
under valgrind. There are no new test programs, and there should not be any:
tests/benchutil.h notices valgrind in LD_PRELOAD and drops the benchmark
scale to 0.0005, which turns the perf suites into the broadest path coverage
in the tree at a cost valgrind can survive. A benchmark walks one path a
hundred thousand times; a leak check wants every path walked once. Same
binaries, one flag apart, and the whole run is about thirty seconds.
scripts/memcheck.sh wraps `ctest -T memcheck` because that command records
defects and still exits 0, which cannot gate anything. It also forces the
headless drivers, so the vendor GPU stack is never loaded -- that removes
thousands of unfixable findings inside amdgpu_dri.so without suppressing
anything, and it is what the suites are written for anyway. Only definite
losses, invalid accesses and uninitialised reads count; "still reachable" is
what SDL and FreeType keep for the process lifetime and says nothing about
this library. The three suppressions in scripts/valgrind.supp are each a
decision that a finding belongs to somebody else.
Six defects, all filed in TODO.md under "Memory checking", the first of
which is the one that matters: not one of the four json_load_file calls in
src/ is ever matched by a json_decref, so every asset load abandons its
parsed document -- 1.5 KB per sprite, 2.1 KB per character, 9 KB per load of
the 2x2 fixture map, and on the order of a megabyte for a real one. A game
that reloads a level on death leaks a level's worth of JSON every time.
akgl_get_property also reads up to 4 KiB past the end of every property
value it copies, and the savegame name tables read past the end of every
registry key and write what they find into the file.
tests/util.c zeroes three fixtures it used to leave as stack garbage. The
library was never at fault there -- the tests handed it uninitialised floats
and then made one real call with them -- but sixteen findings of noise in a
new gate is how a gate gets ignored.
The unit suites' TIMEOUT goes from 30 to 300 because CTest applies the same
property to the checked run, where everything is twenty times slower.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The six defects the perf suites turned up were written out in full in both
files. TODO.md is where a defect belongs -- file, line, functional
consequence, blast radius, what closing it touches -- and duplicating that
in a report guarantees the two drift, with no way to tell which copy is
current.
PERFORMANCE.md keeps what is actually its argument: that stress testing
reaches failures a suite calling each function a handful of times does not,
and that two of the six kill the process rather than slowing it down. The
detail is one reference away.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tests/perf.c and tests/perf_render.c drive the hot paths hard enough to
time them -- pool acquire and release at both ends of the scan, the
registry, the state-to-sprite lookup, the per-actor update and render, the
physics sweep, an all-pairs collision sweep, the JSON accessors, path
resolution, the drawing primitives, text, asset loading, a screenful of
tiles, and a whole frame through akgl_game_update. They are registered like
any other suite but carry the `perf` label, so `ctest -L perf` runs only
them and `-LE perf` leaves them out. Every measurement is held to a budget
at roughly ten times the recorded baseline, enforced only in an optimized
build at full scale.
Three things had to be got right before the numbers meant anything, and
each was wrong first:
- No error checking inside the clock. PASS and CATCH call
akerr_valid_error_address, which walks AKERR_ARRAY_ERROR -- more work than
several of the calls being measured.
- Flush the renderer before stopping it. SDL batches, so the first version
measured queueing, reported a tilemap frame 250 times faster than it is,
and paid the real cost at teardown inside SDL_DestroyTexture.
- A drawing benchmark needs a raw-SDL control doing the same pixel work
with the same access pattern. With one, akgl_tilemap_draw turns out to
cost 0.2% of the frame it appeared to own: 16.26 ms against a control's
16.23 ms for the same 1200 blits. The rasterizer is the frame.
PERFORMANCE.md records the baseline, the frame budget it adds up to, and
what the numbers say -- including that the string pool's acquire is 64x
slower full than empty because it is a megabyte of PATH_MAX buffers, that a
handled missing-sprite condition costs nine times the update it replaces,
that text rasterizes and throws away a texture on every call, and that 28 MB
of the library's BSS is one akgl_Tilemap.
TODO.md gains a Performance section: five defects the stress tests found
that the unit suites do not reach (a tilemap load leaking five pooled
strings, two JSON accessors that turn pool exhaustion into a segfault
rather than AKGL_ERR_HEAP, akgl_game_update crashing without
akgl_game_init, and its actor update sweep running once per tilemap layer),
and eighteen targets with today's number and whether it is met.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
akgl_path_relative took its ENOENT branch by returning from inside the
HANDLE block, which skips the RELEASE_ERROR that FINISH ends with. The
handled context was never given back, so one entry of AKERR_ARRAY_ERROR
was lost per call and the 129th call hit "Unable to pull an error context
from the array!" and exited the process.
That branch is not an edge case: every asset path inside a tilemap is
resolved relative to the map's own directory, so it is taken several times
per map load. A game that loaded fifty levels died in the loader. It was
found by a benchmark that loaded the fixture map in a loop, which is the
first thing in this tree to call it more than a hundred times.
The fallback now runs after FINISH, so the context is released on the way
past. tests/util.c resolves AKERR_MAX_ARRAY_ERROR * 2 paths through the
branch and asserts the pool is where it started -- against the old code
that test does not fail, it terminates the suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All ten items under "API gaps blocking akbasic" are closed. Items 7, 9 and
10 add public symbols and item 9 adds three fields to akgl_AudioVoice, so
the version and the soname go with them -- an 0.2 consumer cannot be handed
this library and told it is compatible.
Also refreshes the coverage numbers against a fresh run (79.6% line, 87.2%
function, all 21 suites), records the mutation smoke runs over the two files
that changed most, and files one new defect the text tests turned up:
akgl_text_rendertextat refuses the empty string while akgl_text_measure
accepts it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
controller.h declares three handler pointers taking an akgl_Actor * and
included nothing that declares that type, so any translation unit reaching
for it before akgl/actor.h failed with "unknown type name 'akgl_Actor'".
src/controller.c never noticed because it includes akgl/game.h first, and
tests/controller.c carried a comment explaining the workaround.
Included rather than forward-declared: akgl_Actor is a typedef of a named
struct and repeating a typedef is C11, not C99. It closes no cycle --
actor.h reaches only types.h and character.h.
tests/headers.c is a whole suite for one #include on purpose. A second
#include after the first proves nothing about the second, because by then
the first has dragged its dependencies in; covering another header means
another file shaped like this one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ring held a keycode and dropped the rest of the event, so a line editor
built on akgl_controller_poll_key() could not reach a single shifted
character: no double quote, no colon, no parenthesis, and no lower case.
The modifier state was discarded two layers below the caller, and polling
SDL_GetModState() at read time cannot recover it -- the key is long
released by then, which is the point of a ring.
akgl_Keystroke carries the keycode, the modifiers and the UTF-8 text SDL
composed, and akgl_controller_poll_keystroke() hands back all three.
akgl_controller_poll_key() is unchanged for its callers: a game asking
whether the up arrow was pressed already gets what it wants.
The text comes from SDL_EVENT_TEXT_INPUT, which is the only correct way to
get a character out of SDL, and is attached to the press still waiting for
it -- tracked with a flag rather than by "whatever entry is newest", so a
press dropped by a full buffer cannot hand its text to an older key. Text
with no press behind it (an IME commit, a dead key) is buffered on its own
with keycode 0, and the keycode-only poller discards those on the way past.
Oversized text is cut on a code point boundary, and a sequence the string
ends in the middle of is dropped rather than read past its terminator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BASIC's SOUND ramps a tone's pitch toward a limit, and nothing here could
do it. Faking it from the caller means re-issuing tones from its own step
loop, which ties audible pitch to how often that loop runs -- a siren that
changes shape with the frame rate. The step has to be taken on the mixer's
frame counter, which only this library can see.
akgl_audio_sweep(voice, from_hz, to_hz, step_hz, ms) steps once every
1/60 second, the rate the machine this vocabulary comes from used, which
divides 44100 exactly so a step always lands on a whole frame. Direction
comes from the two frequencies rather than the sign of the step, the last
step clamps to the target, and equal frequencies are a held tone rather
than an error. akgl_audio_tone() is the same start path with a step of 0,
so a voice reused for a plain tone stops sweeping.
A swept voice accumulates its phase instead of deriving it from the frame
counter: deriving assumes a constant frequency and would jump the waveform
at every step. tests/audio.c drives the mixer by hand and asserts the exact
frame the pitch moves on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
akgl_text_rendertextat() called renderer->draw_texture() with no check on
the global renderer, its sdl_renderer or the function pointer itself, so a
backend that has an SDL_Renderer but was never bound segfaults on the first
line of text -- the state akgl_render_bind2d() exists to get a host out of.
All three are now checked, with AKERR_NULLPOINTER as the draw entry points
report, and checked before anything is rasterized rather than after.
tests/text.c grows a software renderer with a bound backend and covers all
three refusals plus the successful draw, wrapped and unwrapped: src/text.c
goes from 58% to 100% line coverage. A made-up SDL_Renderer pointer is not
enough to test this -- SDL refuses the bogus handle on its own and the call
fails for the wrong reason, which a deleted check survives.
Also documents what the tests found: SDL_ttf refuses the empty string, so
drawing an empty line is an error while measuring one is not. Filed in
TODO.md rather than pinned in the suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Embedded with add_subdirectory(), the deps/ submodules a recursive clone
just checked out were ignored and find_package(SDL3 REQUIRED) ran instead,
so configure failed on a machine with nothing installed. Each dependency is
now added by akgl_add_vendored_dependency(), which skips it when the target
is already declared or the submodule is missing; the find_package lookups
run afterwards for whatever is left. The CTest suppression moves with them
and is lifted before this project registers its own suites.
Two consequences of the move: pkg-config is only looked for when something
has to come from the system, and the build-tree RPATH keys on whether
anything was vendored rather than on being the top-level project, so an
embedded build's tests can find the satellite libraries too.
Verified by configuring and building a consumer that embeds this repository
with CMAKE_DISABLE_FIND_PACKAGE_* set for all seven dependencies.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
akbasic built a line editor on akgl_controller_poll_key() and found the ring
carries a keycode and nothing else. No shifted character is reachable, so a
BASIC string literal cannot be typed at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Writing an honest @throws list means reading the implementation against the
contract its header claimed, and the two disagreed in more places than the
status codes. Recorded as items 14-25 under Defects, ordered by blast radius,
each with file, line, consequence and what closing it would touch. They are
also noted inline as @note or @warning on the function concerned, so a reader
of the generated documentation finds them without coming here first.
The three that matter most:
- akgl_path_relative returns from inside its ENOENT handler, skipping the
RELEASE_ERROR that FINISH carries, so it leaks one of libakerror's 128
error-context slots per call. That is not a rare path - it is the ordinary
one, taken whenever an asset names a neighbour relative to its own file.
- akgl_sprite_load_json writes json_array_size() entries into a 16-element
frameids array with no bound check, and does it through a uint32_t * cast of
a uint8_t *. The same unbounded shape appears twice more in the tilemap
loader, for objects per layer and tilesets per map.
- akgl_heap_release_character zeroes the character without walking its
state-to-sprite map, so every sprite reference it took is lost and the SDL
property set holding the map is leaked. Releasing characters between levels
exhausts the sprite pool. akgl_character_state_sprites_iterate exists to do
that walk and is not called from there.
Also: the background music never loops, because MIX_PROP_PLAY_LOOPS_NUMBER is
set on property set 0 rather than one akgl_load_start_bgm owns; and
akgl_get_json_with_default defaults on AKERR_INDEX while the array accessors
report a short array as AKERR_OUTOFBOUNDS, so the optional-element form
silently does not work for arrays.
No code changes. Every src/*.c:NNN citation in the new section was checked
against the file after the preceding commit shifted the line numbers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Doxygen comments were generated from the declarations, so 217 @throws
lines across 21 headers read "When the corresponding validation or operation
fails" and told a caller nothing beyond the status name. The @param lines were
the same shape: every output was "Output destination populated by the
function", every instance "Object to initialize, inspect, or modify".
Rewritten against the implementations, following the pattern libakstdlib
already uses:
- @throws names the condition. akgl_sprite_load_json separated AKERR_KEY
(absent) from AKERR_TYPE (present, wrong type) from AKERR_OUTOFBOUNDS
(filename too long, or array indexed past its end), and gained AKGL_ERR_SDL
and AKGL_ERR_HEAP, which it raises and never declared.
- Parameters say whether they are required, what a NULL means, and what is
written on a failure path. Where an argument is not checked, the doc says so:
akgl_heap_next_actor's dest is a crash on NULL, not an error, and
akgl_render_2d_frame_start dereferences self before testing it.
- The conventions move up to the file blocks so the per-function docs stay
short. json_helpers.h states once that absence is an error here and that
json_t * results are borrowed; heap.h explains the pool model and the
acquire asymmetry; physics.h carries the thrust/environmental/velocity table.
- Struct fields, enum values, macros and exported globals are documented,
including the dead ones - sprite_w/sprite_h, movetimer, p_scale and
timer_gravity are read by nothing, and say so.
Also fixes ten comments in error.h and audio.h that opened with /** rather
than /**<, so Doxygen attached them to the following entity and rendered the
text as part of the macro's value. Verified against the generated HTML.
Comments only - no declaration changed. Doxygen builds clean under
WARN_AS_ERROR, scripts/reindent.sh --check passes, 19/19 suites pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
akbasic has now consumed all four of the API gaps this file listed as blocking
it: its text sink, graphics backend, audio backend and input backend are written
and tested, and the BASIC 7.0 graphics, sound and console verbs work against
them. Doing that turned up six things worth fixing here -- five of which still
are.
Four are workarounds akbasic had to write to build at all, and each is commented
at its site over there with the words "filed upstream":
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be installed, while vendoring all five. The dependency block is gated on being
top-level, so an add_subdirectory() consumer takes the find_package path and
fails with the submodules sitting right there in deps/.
- include/akgl/controller.h does not compile on its own: it declares handlers
taking an akgl_Actor * and includes nothing that declares the type. src/
never notices because it includes game.h first.
- akgl_render_init2d() fuses two separable jobs -- creating a window, and
installing the backend vtable -- so a caller who already owns an SDL_Renderer
has no way to get the second without the first. That caller is exactly the
embedding host this section exists to serve.
- akgl_text_rendertextat() dereferences renderer->draw_texture with no check,
which is the state item 7 leaves a host in. Same class of defect the draw
commit added a test for; the text path still has it.
The fifth is a real API gap: SOUND's frequency sweep has no equivalent, and it
cannot be faked in the interpreter without tying audible pitch to the host's
frame rate, so it has to be advanced on the mixer's own frame counter.
The sixth was that 42b60f7 shipped 22 public symbols under an unchanged VERSION
and soname. 1066ac7 fixed that independently while this was being written -- the
two crossed rather than one following the other -- so it is filed as resolved
rather than dropped. The reason is the part worth keeping: akbasic could not
feature-test for the new API and had to pin libakgl by submodule commit in its
README until that landed, which is the concrete cost of an additive release
under an unchanged soname.
All five outstanding items were re-verified against this tree after the rebase,
not just carried forward: controller.h still includes neither actor.h nor a
forward declaration, render_init2d still calls SDL_CreateWindowAndRenderer
before installing the vtable, text.c:62 still reaches through draw_texture
unchecked, the vendored-dependency block is still gated on being top-level, and
there is still no akgl_audio_sweep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Require libakstdlib 0.2 when consuming an installed dependency, matching the API adopted by the preceding commit.
Co-Authored-By: Codex GPT-5 <noreply@openai.com>
Pass fixed buffer sizes to the bounded formatting and realpath wrappers. Route save-game records through exact-transfer helpers so the required fread/fwrite count parameter stays local to the complete-record contract.
Co-Authored-By: Codex GPT-5 <noreply@openai.com>
A mutation smoke run scored src/audio.c at 50% and src/draw.c at 70% and named
three real holes:
- Nothing asserted that an unconfigured voice is audible, which is the entire
reason the voice table defaults to a square wave at full level instead of a
zeroed struct. Deleting the defaults survived.
- No envelope test used a non-zero attack and decay together, so the decay
measuring from the start of the note rather than from the end of the attack
survived.
- The circle was only checked at its four axis points, which a mis-signed
octant reflection survives. It now checks that every plotted pixel has a
mirror in the other three quadrants.
Also adds a backend that exists but was never given an SDL_Renderer, which is
the state a host is in between allocating one and initializing it; every draw
entry point has to report it rather than dereference it.
audio 50% -> 75%, draw 70% -> 90%. The survivors are recorded in TODO.md and
are honestly untestable from here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
77.2% line and 83.1% function coverage across 2637 lines, up from 72.2% / 78.6%,
with the three new suites at 91% (audio), 95% (draw) and 58% (text). All 19
suites pass: the note recording character as an intentional failure was stale,
and neither tests/character.c nor src/character.c has changed since it was
written.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GCC cannot see that it is only read on a path where flood_region() wrote it,
and warns under -Wmaybe-uninitialized.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was no audio API at all: SDL3_mixer was vendored and AKGL_REGISTRY_MUSIC
was declared, but nothing opened a mixer, and a mixer plays recordings anyway.
BASIC 7.0's SOUND, PLAY, ENVELOPE and VOL describe notes, so this adds a tone
generator -- three voices, five waveforms, an ADSR envelope each, mixed to one
float stream and fed to an SDL_AudioStream.
The voice table works whether or not a device is open. akgl_audio_init connects
it to one; a host that owns its own audio pipeline can call akgl_audio_mix
instead. That is also what makes the tests deterministic: a device pulls samples
on SDL's audio thread whenever it likes, so tests/audio.c mixes by hand and only
opens a device in its last test.
Phase is computed from the frame counter rather than accumulated, because a
float increment of hz/44100 added 44100 times a second walks a held note off
pitch. A voice that has never been configured defaults to a square wave at full
level, since a zeroed voice would have a sustain of 0.0 and make no sound with
no error to explain why. Voices summing past full scale are clamped rather than
scaled, so one voice plays at the level it was asked for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BASIC 7.0's graphics verbs -- DRAW, BOX, CIRCLE, PAINT, SSHAPE and GSHAPE -- all
plot against the current screen right now rather than adding to a scene, and
draw.h had exactly one function in it. This adds akgl_draw_point, _line, _rect,
_filled_rect, _circle, _flood_fill, _copy_region and _paste_region, each taking
the akgl_RenderBackend the host already initialized.
Color is an argument rather than a current-color global, so there is no second
copy of state to disagree with the caller's own, and each call restores the
renderer's draw color when it is done. SDL3 has no circle and no flood fill: the
circle is a midpoint circle, and the fill reads the target back, walks the
region with a fixed 4096-entry span stack, and blits back only the bounding box
of what changed. Exhausting that stack reports AKERR_OUTOFBOUNDS and says in the
header that the region is left partially filled.
tests/draw.c draws into a 64x64 software renderer under the dummy video driver
and reads the pixels back, so it needs no display and no offscreen harness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
akbasic's GET and GETKEY ask "is there a keystroke waiting, yes or no" and must
not require the interpreter to pump SDL events itself. akgl_controller_poll_key
drains one key per call from a fixed 32-entry ring that
akgl_controller_handle_event fills, so the host keeps pumping and the embedded
interpreter reads at its own pace. akgl_controller_flush_keys discards a
backlog.
The recording happens before the control-map scan, not after: that scan returns
as soon as a binding claims the event, so recording afterwards would have
dropped exactly the keys a game also acts on. A full buffer refuses the newest
keystroke rather than overwriting the oldest, so a caller reading a line gets
what was typed first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
akbasic needs the advance width of one character cell to build a terminal-style
text surface, and the wrapped size to know where a string crosses the right
margin. akgl_text_measure and akgl_text_measure_wrapped report both over
TTF_GetStringSize and TTF_GetStringSizeWrapped; neither needs a renderer, so
this half of the text subsystem is testable without the offscreen harness.
A negative wrap length is refused with AKERR_OUTOFBOUNDS: SDL_ttf reads it as a
very large unsigned width and quietly stops wrapping, which would return a
measurement that is wrong rather than an error the caller can see.
Also fixes akgl_text_loadfont, which checked name twice and passed an unchecked
filepath to TTF_OpenFont (TODO item 39).
The fixture font is a 10 KB monospaced ASCII subset of Liberation Mono, renamed
because "Liberation" is a Reserved Font Name; see
tests/assets/akgl_test_mono.LICENSE.txt. Monospaced so the suite can assert
width("AAAA") == 4 * width("A") rather than hardcode glyph metrics.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
akbasic is a C port of the BASIC interpreter being built to link into libakgl as
a scripting engine for game authors. Its interpreter core is complete and passes
its whole acceptance corpus against a stdio text sink; four gaps here block the
libakgl-backed sink and the graphics, sound and console verbs of Commodore
BASIC 7.0.
Filed rather than worked around downstream, because growing libakgl to serve a
consumer is the wanted outcome. Each entry says what the BASIC verb needs, what
the akgl_* entry point should look like, and what would cover it.
Only the first blocks work already designed and waiting. text.h can load a font
and render a string and cannot say how large that string will be, so there is no
way to build a terminal-style surface on it -- a cursor needs the advance width
of a cell and wrapping needs to know where a string crosses the margin. The
reference interpreter derived its whole character grid from SDL2_ttf's
SizeUTF8("A"). akgl_text_measure() over TTF_GetStringSize closes it.
The other three block verbs not yet started: draw.h declares one function and
src/draw.c is at 0%, so DRAW/BOX/CIRCLE/PAINT have nothing to plot with; there
is no audio API at all despite SDL3_mixer being vendored, and PLAY/ENVELOPE want
a synthesised voice SDL3_mixer does not provide; and controller.h is built
around event handlers the host pumps, which suits a game loop but not GET/GETKEY
-- those ask whether a keystroke is waiting and must not require the interpreter
to own the event loop, which goal 3 forbids anyway.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Defects #13: gcovr searches for gcov data under --root, which
CMakeLists.txt:208 and :223 set to the source directory, so any
instrumented build tree left there is merged into the report. The
--object-directory argument beside each does not narrow the search; it
only maps gcda files back to the compiler's working directory.
A leftover build-coverage/ therefore fails coverage_reset before any
test runs, with gcovr unable to reconcile a function reported on two
different lines. The build*/ entry in .gitignore keeps the offending
tree out of git status, so the state is easy to get into and hard to
spot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bump deps/libakstdlib five commits to 95e5002, which versions the library
at 0.1.0 with an soname and a ConfigVersion file, upgrades its own
libakerror to 1.0.0, namespaces its coverage target when embedded, and
raises its libc-wrapper coverage. The public header diff is purely
additive -- nothing removed, no signature changed -- so libakgl's nine
aksl_* call sites needed no edits. What broke was the build.
A default `cmake -S . -B build` stopped configuring:
add_executable cannot create target "test_version" because another
target with the same name already exists. The existing target is an
executable created in source directory ".../deps/libakstdlib"
The versioning commit added tests/test_version.c to libakstdlib.
EXCLUDE_FROM_ALL keeps that target from being built, but
add_subdirectory still creates it, so it collided with the version suite
added in the preceding commit. Build libakgl's test programs as
akgl_test_<name> instead. The CTest names are unchanged -- ctest -R
version still selects it -- and a dependency is now free to ship a test
of any name.
Ask find_package for libakstdlib 0.1 rather than any version. 0.1.0
ships akstdlibConfigVersion.cmake with SameMinorVersion compatibility,
mirroring its libakstdlib.so.0.1 soname, so this accepts any 0.1.x and
refuses 0.2 and 1.0. Unversioned, the non-embedded path would have taken
an ABI-incompatible libakstdlib without complaint.
libakerror is deliberately left unversioned in find_package. It installs
akerrorConfig.cmake and akerrorTargets.cmake but no
akerrorConfigVersion.cmake, so find_package(akerror 1.0) fails against a
correct install rather than a stale one. Its floor stays with the #error
in include/akgl/error.h. Worth fixing upstream.
Verified: clean configure, build and 16/16 ctest; a -DAKGL_COVERAGE=ON
tree configures, builds and passes 18/18, so libakstdlib's embedded
coverage-target rename does not collide here. Against a temp-prefix
install of 95e5002, find_package(akstdlib) accepts 0.1 and 0.1.0 and
refuses 0.2 and 1.0, and find_package(akerror 1.0) fails for the missing
version file as described.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first .gitignore entry was "./build/*". A leading ./ is not a valid
gitignore pattern, so it matched nothing and build/ turned up as
untracked in every git status. Use build*/, which also covers the
instrumented trees scripts/coverage.py and -DAKGL_COVERAGE=ON create.
Nothing under a build* path is tracked, so this hides no existing file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The version was written down twice and agreed with itself by luck.
AKGL_VERSION was hand-maintained in include/akgl/game.h, and nothing
connected it to the build, which had no version at all:
- project() declared no VERSION, so the @PROJECT_VERSION@ substitution
in akgl.pc.in expanded to nothing and every installed akgl.pc shipped
an empty Version field. pkg-config --modversion returned "" and
--atleast-version could not work at all.
- libakgl.so had no VERSION or SOVERSION, so there was no soname and no
symlink chain. A stale libakgl could be paired with new headers
silently, which is the failure libakerror added its own soname to
prevent.
- MY_LIBRARY_VERSION at CMakeLists.txt:288 was never set anywhere. It
built main_lib_dest as "lib/akgl-", and main_lib_dest was then never
read. Dead line, removed.
project(akgl VERSION 0.1.0) is now the only place the number appears. It
drives the generated include/akgl/version.h, the shared library VERSION
and SOVERSION, and the Version field in akgl.pc. Held at 0.1.0 to match
the constant it replaces, so akgl_game_load_versioncmp still compares
savegames against the same value.
include/akgl/version.h is generated by configure_file from version.h.in
and publishes AKGL_VERSION, AKGL_VERSION_MAJOR/MINOR/PATCH and
AKGL_VERSION_AT_LEAST(major, minor, patch). That macro is the point:
libakstdlib could not write that test against libakerror, which
publishes no version macro, and had to feature-test on
AKERR_FIRST_CONSUMER_STATUS instead. game.h now includes the generated
header rather than defining the version itself, so consumers see
AKGL_VERSION exactly where they saw it before.
While the major version is 0 the soname carries major.minor, giving
libakgl.so.0.1. A plain SOVERSION 0 would assert that 0.1.0 and 0.2.0
are ABI-interchangeable, and they are not -- the preceding commit alone
added akgl_error_init. At 1.0.0 the soname becomes the major alone,
matching libakerror.
akgl_version() in src/version.c reports the version of the libakgl that
is actually linked, where AKGL_VERSION reports what the caller compiled
against. Comparing the two is the only way to catch a stale shared
library on the loader path; the macro alone cannot see it. It returns a
string rather than an akerr_ErrorContext * because it cannot fail, for
the same reason akerr_name_for_status does.
Add tests/version.c: the string and the numeric components must describe
one version, the linked library must agree with the headers, and
AKGL_VERSION_AT_LEAST must order correctly at the major, minor and patch
boundaries. Verified out of band that bumping project() to 1.2.3 moves
version.h, akgl.pc and the soname together, and that an install produces
the libakgl.so -> .so.0.1 -> .so.0.1.0 chain with SONAME libakgl.so.0.1.
Also ignore include/akgl/version.h. include/ precedes the build tree on
the include path, so a stray copy there would shadow the generated one
and pin every consumer to whatever version it was generated at.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
libakerror 1.0.0 replaced the consumer-sized status-name array with a
private registry and made status-code ownership explicit and enforced.
AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, and the registry
entry points raise akerr_ErrorContext * instead of returning int. See
deps/libakerror/UPGRADING.md.
The break was not only source-level. libakgl's codes sat at
AKERR_LAST_ERRNO_VALUE + 18 through + 22, and 1.0.0 claimed exactly
those five offsets for its own AKERR_STATUS_* registry codes, so every
AKGL_ERR_* was aliasing a libakerror status. HANDLE(e,
AKGL_ERR_LOGICINTERRUPT) at physics.c:222 would have swallowed a
foreign-name refusal.
- Move the band to AKERR_FIRST_CONSUMER_STATUS (256) as fixed offsets,
so a libc that grows an errno cannot move the codes, and add
AKGL_ERR_OWNER, AKGL_ERR_LIMIT and AKGL_ERR_COUNT to describe it.
- Reserve the range and register the names through the owned entry
points, PASS-ing each: these are AKERR_NOIGNORE, and the old
akerr_name_for_status calls discarded failure silently.
- Drop the AKERR_MAX_ERR_VALUE=256 compile definition.
- Guard on AKERR_FIRST_CONSUMER_STATUS in include/akgl/error.h, which
now includes <akerror.h> so the guard is reliable. The embedded build
is fine, but the find_package path can pick up a stale installed
header, and 1.0.0 has an soname, so that pairing is an ABI mismatch
rather than a compile problem. Same guard libakstdlib already carries.
Registration also moves out of akgl_heap_init into a new akgl_error_init
in src/error.c. It was in the heap pool's initializer only because that
was the first thing akgl_game_init called, and the upgrade turned five
fire-and-forget name calls into a library-wide ownership claim that can
fail. That placement was hiding a defect: game.c raises AKGL_ERR_SDL
when SDL_CreateMutex fails, five lines before akgl_heap_init ran, so the
earliest error path in the library was guaranteed to print "Unknown
Error". akgl_error_init is now the first statement in akgl_game_init.
Callers that drive subsystems directly must call akgl_error_init first;
it is idempotent, so ordering it precisely is not required. The eleven
test suites that relied on akgl_heap_init to name their statuses now
call it explicitly, or their failure messages would have degraded to
"Unknown Error".
Add tests/error.c: assert every code reads back its registered name,
that the name table and AKGL_ERR_COUNT agree, that a foreign owner is
refused with AKERR_STATUS_NAME_FOREIGN and AKERR_STATUS_RANGE_OVERLAP,
and that repeating the init is a no-op. That last one is a live
constraint, not a triviality -- libakerror treats only an identical
reservation as a repeat, so a subset or superset raises.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pre-commit hook is version controlled under scripts/hooks/, but Git
configuration is not cloned, so the hook does nothing until each clone is
pointed at it. Nothing said so anywhere a new contributor would look.
Cover installation, verification, what the hook actually does, the Emacs
requirement, bypassing, and uninstalling.
Two things worth calling out explicitly:
- core.hooksPath replaces the hooks directory outright rather than adding
to it, so any pre-existing .git/hooks entries silently stop firing.
Document a symlink alternative for anyone who keeps local hooks.
- The documented --check exit codes are 0 conforming, 1 non-conforming,
2 environment failure, so a CI job failing on any non-zero status will
not mistake a missing Emacs for a clean tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--check` prefixed every path with the repository root unconditionally, so
passing an absolute path produced a bogus `$root//abs/path`, cp failed, and
the subsequent cmp reported the file as non-conforming. The exit status
happened to be 1, which looked correct while being reached for the wrong
reason.
In-place mode was unaffected -- it cds to the root and hands the list
straight to Emacs, where absolute paths resolve normally, which is why the
pre-commit hook worked.
Normalize both the copy and the compare through an abspath helper so
relative and absolute paths behave identically in either mode.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add an "Internal consistency" section to TODO.md: 41 numbered findings
from a sweep of src/ and include/, each cited by file:line and grouped by
blast radius. These are convention problems, not a re-audit of behavior,
but several have live functional consequences:
- AKGL_TIME_ONESEC_MS is misnamed. Its value is nanoseconds-per-
millisecond, but akgl_game_state_lock uses it as a one-second budget
against a 100ms delay loop, so the lock blocks ~16 minutes.
- SUCCEED_RETURN inside an ATTEMPT block at tilemap.c:52 bypasses the
CLEANUP two lines below, leaking two heap strings on every successful
tilemap property lookup, against a 256-entry pool.
- AKGL_ACTOR_STATE_STRING_NAMES is declared [33] and defined [32], and
names bits 11 and 12 as UNDEFINED where actor.h defines MOVING_IN and
MOVING_OUT, so no character JSON can bind a sprite to those states.
- 19 non-static functions are defined in src/ but declared in no header,
and akgl_game_init_screen is declared but never defined.
- AKGL_COLLIDE_RECTANGLES has unbalanced parentheses and cannot compile.
- text.c:19 checks `name` twice and never validates `filepath`.
Item 30 is marked resolved by the reindent in the preceding commit.
Also add Defects #12: a failed controller-DB fetch silently destroys the
tracked fallback. include/akgl/SDL_GameControllerDB.h is committed on
purpose so the library still builds if upstream disappears, but
mkcontrollermappings.sh has no `set -e` and never checks curl's exit
status -- on failure it overwrites the good header with
AKGL_SDL_GAMECONTROLLER_DB_LEN 0 and an empty initializer, and exits 0.
Verified against an unresolvable host. CMakeLists.txt:89-93 compounds
this: the custom command's OUTPUT is relative, so CMake resolves it
against the binary dir while the script writes to the source dir, leaving
the command permanently out of date and re-running on every build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>