5560edf41020115826b582d4ba68365c55d3fa76
184 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
5560edf410
|
Exclude the two shell-script tests from the memcheck run
The memory job's first-ever complete valgrind pass on the runner reported every real suite and all three example games clean -- and 414 findings in docs_examples and docs_screenshots. Those two tests are bash scripts that drive gcc and run the compiled snippets as children; valgrind wraps the test command and does not trace children, so all 414 were /bin/bash's own by-design leaks and not one byte of libakgl. Memchecking them proves nothing the suites and example games do not already prove as real binaries under the same valgrind run. memcheck.sh now excludes exactly those two by name, with the reasoning in a comment beside the exclusion -- per the suppressions file's own rule that nothing gets quieted without a stated reason. Verified locally: full run over the RelWithDebInfo tree, every suite and example under valgrind, zero findings, exit 0. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
be3c8ae850
|
Pin upload-artifact to v3: v4 refuses to run against Gitea
The env-list fix got every test green on the runner for the first time --
37 of 37, the JUnit publish included -- and the job then failed in the
artifact upload: upload-artifact@v4 speaks GitHub's v2 artifact service and
hard-refuses any host that is not github.com ("not currently supported on
GHES"). Gitea implements the v3 artifact API, so the three upload steps --
coverage, the benchmark tables, the valgrind logs -- pin to v3.
Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
|
|||
|
d8458d1e68
|
Stop set_tests_properties from eating the test environment lists
The example games failed on the CI runner with "ALSA: Couldn't open audio device" despite SDL_AUDIODRIVER=dummy sitting right there in their CMake -- because it never reached the process. set_tests_properties parses its PROPERTIES arguments as name/value pairs, so a semicolon-separated value list is split and every element after the first is consumed as a bogus property name: ENVIRONMENT kept only SDL_VIDEODRIVER=dummy, and the LD_LIBRARY_PATH prepend list kept only its first directory. ctest --show-only=json-v1 shows the truncation plainly. Nobody could see it. Video survived because SDL3 falls through its driver list to dummy on a headless machine with no hint at all; audio survived on every developer machine because the real device works there; the library paths survived because RPATH covered them. A runner with no sound card was the first environment where any of it mattered. Every list-valued test property now goes through set_property(TEST ...), which takes real list arguments -- the examples' ENVIRONMENT triplets and vendored-path modifications, the suites' LD_LIBRARY_PATH prepends, and the docs harness's driver variables. Verified by property dump (3 environment entries and all 7 prepends present) and by running the example tests with ALSA_CONFIG_PATH=/dev/null, which now pass where a real ALSA open would fail: the dummy driver is finally the one being asked. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
219d7182f1
|
Install graphviz where doxygen runs
The submodule fix got the cmake_build job through checkout, dependencies, configure and the whole compile for the first time in the run history -- and into the next latent failure: `doxygen Doxyfile` generates dot graphs and the Doxyfile fails on warnings, but the runner image has no `dot`, so every graph reported "Problems running dot: exit code=127" and the step failed after an otherwise clean build. Locally graphviz was installed, which is why the pre-flight missed it. One package in the one job that builds documentation. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
2242cfac9f
|
Unbreak CI: https submodule URLs, and three suites that assumed a display
CI has never gone green in the retained run history, and every failure is the same 20-second death during checkout: six submodules -- SDL, SDL_image, SDL_mixer, SDL_ttf, jansson, semver -- were registered with git@github.com: SSH URLs, and the runner has no GitHub key. The https submodules (libccd, tg, clay, and the two starfort ones) clone fine in the same run. All six now use https, which is what every stanza added since already did; these are public upstream repositories nothing here pushes to, so SSH bought nothing. Second problem, waiting right behind the first: the character, sprite and tilemap suites were the only three of seventeen that never set the dummy video/audio/software-render hints, so on a headless runner they die in SDL_Init with "No available video device" before testing anything. They now set the same three hints the other fourteen suites set. (How this survived: every local run happens on a machine with a display, and CI never got far enough to run a test.) Pre-flighted locally against every job the workflow defines, headless with no driver variables in the environment, exactly as the runner would: Debug with -Werror and coverage builds clean and all 37 tests pass including the coverage fixtures; RelWithDebInfo with -Werror builds clean and the perf suites pass their budgets at full scale; doxygen Doxyfile exits 0 under FAIL_ON_WARNINGS; and the new ui suite runs under valgrind with zero errors and no leaks. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
f2efbdf1b7
|
Fill the gaps the chapter 22 weaker-model reader found
The AGENTS.md "Testing a Tutorial" pass: chapter 22 was handed to a reader on a weaker model with the library tree stripped of examples/ and docs/, and they had to build a menu/HUD/dialog program from the chapter alone. They built one that compiled and ran -- and reported success their own screenshots disproved, which is why the protocol says to verify: their synthesized key events set only the scancode, akgl_ui_menu_handle_event matches keycodes, and their program never actually left the title screen. With that one line fixed, everything they had built from the chapter worked. Three findings survived verification against eight reported: - The chapter never stated its prerequisites, so the reader burned most of the session on library bring-up it does not cover and invented goto error handling for main(). A new "What this chapter assumes" paragraph points at chapters 3, 7 and 17, and at the demo's startup() and main() as the complete reference. - Nothing named the menu's exact keys, or that they are keycodes rather than scancodes -- invisible with a real keyboard, fatal for synthesized events, which the demo's own --demo script teaches. The menus section now names SDLK_UP/DOWN/RETURN, the gamepad buttons, and the key.key trap. - Verifying their dialog screenshot showed a message wrapping past the fixed AKGL_UI_DIALOG_HEIGHT runs visibly out of the panel. Deliberate -- same as the hand-rolled textbox, and visible overflow beats silent truncation -- but undocumented; akgl_ui_dialog's header note now says so. Rejected for the record, per protocol: akgl_game_init already initializes the property registry (src/game.c:219), and the font they "found" in a different directory was the build tree's file(COPY) of tests/assets. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
f772da7296
|
Record the UI's paper trail and bump to 0.9.0
Two TODO.md notes the UI work created. The text texture ring cache (plan item 2) now has its second consumer, and the bigger one: the UI executor draws every TEXT render command through akgl_text_rendertextat, one wrapped line per command per frame, so a five-row menu re-rasterizes five lines at 60 Hz against the original case of one HUD line changing once a second. And akgl_ControlMap's mouseid/penid fields stay dead deliberately -- the mouse path lives in the UI subsystem because control maps and pointer state are different consumers, and the note says where the work lands when a game wants mouse-driven actors. 0.8.0 to 0.9.0: a new public header, a new status code, three new drawing primitives and a new subsystem is new API, and while the major version is 0 the soname carries major.minor -- libakgl.so.0.9 is what makes 0.8.0 and 0.9.0 unmixable at load time, which is the honesty the bump is for. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
8f848d80be
|
Document the UI subsystem: chapter 22, with the appendix moving to 23
docs/22-ui.md covers the clay-backed UI: what clay owns against what libakgl owns, bring-up and the arena refusal contract, the frame bracket and where it sits in a real frame, the consumed-event rules (with the one-frame-stale hit test stated plainly), the three widgets, menus across keyboard, gamepad and mouse, writing raw CLAY() with an application-owned press edge, images and styles, and how layout errors surface from frame_end with a captured trace. Every listing is a checked block: one linked-and-run example with pinned output, eight excerpts quoting examples/uidemo and examples/jrpg/textbox.c, and the chapter figure is a frame out of uidemo via docs_game_figures. The chapter owes and pays the comparison the tutorials earn: the dialog widget's default style reproduces the JRPG textbox palette, the two listings sit side by side, and the trade is stated -- 125 lines you own completely against one call that costs you the subsystem. Neither is deprecated; chapter 21 keeps teaching the hand-rolled panel on purpose. The appendix moves to 23 (links in chapters 4, 5, 6, 7, 13 and the TOC updated) and gains the ui.h status cross-reference, the AKGL_UI_* limits table, and the new draw primitives' rows. Its status-band figures were stale at COUNT 5 / LIMIT 261 while the band already held six codes; now correct at 7 / 263. Chapter 4 gains AKGL_ERR_UI (262) in the excerpt, the table and the name list. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
4ddd8d1ad3
|
Add the UI demo: title menu, raw-CLAY options screen, HUD and dialog
examples/uidemo is the program the UI chapter quotes. Three screens, three ways of building an interface: the title screen is one akgl_UiMenu plus a heading label; the options screen is written in raw CLAY() declarations -- hover highlighting inside the declaration, clicks paired with the application's own press edge -- because the widgets are a convenience, not a boundary; and the play screen is two HUD labels and the one-call dialog over a checkerboard standing in for a game world. No tilemap, no actors, no physics: everything left on screen is the subject. Its route_event() is the documented call-order contract in the flesh -- UI first, consumed events stop there, then the current screen's keys -- and the screen routing is the focus model, stated rather than invented. The headless smoke run (example_uidemo, --frames 240 --demo) tours every path through the real event chain: a mouse click on the centred menu (the consumed path, landing on the middle row by symmetry), keyboard into and out of the options screen, the dialog opened and dismissed, and Quit confirmed from the menu. The run prints its score and settings so a pass can be read, not just counted. docs_game_figures gains a uidemo frame -- the play screen with the dialog up -- for the chapter to come. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
badaf570ab
|
Stop a stationary pointer from pinning the menu selection
A pointer parked over a menu row re-claimed the selection on every frame's declaration, so keyboard navigation fought the mouse sixty times a second and lost -- press Up, and the hover put the selection straight back. Found by the UI demo's scripted tour: its mouse click leaves the pointer resting on the menu, and the Up keystroke that followed did nothing. Hover now moves the selection only on frames the pointer actually moved (or clicked), through a frame-latched motion edge beside the existing press edge. Parking the mouse claims nothing; moving it onto a row still selects, clicking still selects and activates. Regression test drives the exact sequence: click a row, move the selection away by keyboard, redeclare, and assert the stationary pointer stole nothing back. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
0679d40ee4
|
Add the widget helpers: dialog, HUD label, and menu
The other half of the hybrid decision: an application that wants a dialog box, a score counter or a main menu gets each in one call, without touching a CLAY() macro -- and an application that outgrows them still has clay's whole DSL, because the widgets are nothing but functions that declare the same elements between the same frame bracket. akgl_ui_dialog is the JRPG textbox in one line: bottom panel spanning the layout, AKGL_UI_DIALOG_HEIGHT tall, one-pixel border, wrapped text. The NULL style *is* the textbox palette -- near-black fill, parchment edge and ink, 8px padding -- deliberately, so the widget and the 125-line hand-rolled panel it stands beside in the comparison chapter produce the same picture. Showing and hiding is the frame's business: call it while somebody is talking, don't while nobody is; a declarative frame is the visible flag. akgl_ui_label pins a fit-sized text chip to a corner or the centre, inset by the style's padding. akgl_ui_menu declares a centred vertical menu from a caller-owned struct (no heap layer: it owns no texture, no font, no registry entry), with the selected row drawn inverted, hover moving the selection and the frame-latched press edge activating it. akgl_ui_menu_handle_event drives the same struct from Up/Down/Return and D-pad/South with wrapping, slotting into the event chain after akgl_ui_handle_event; routing events to a menu is the application's focus model, stated rather than invented. Tests cover the dialog's geometry and palette by pixel, label anchoring by quadrant, menu validation, the inverted highlight, keyboard and gamepad wrap/activate/pass-through, and the full two-frame mouse protocol -- click aimed at the second row's real box via Clay_GetElementData, seen by the next frame's declaration as selection plus activation. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
566b870a5c
|
Feed the mouse to the UI and tell the host which events it consumed
akgl_ui_handle_event is the mouse half of the input story -- keyboard and gamepad stay with the control maps, because keyboard focus is something an application declares rather than something a pointer position implies. It handles motion, left-button presses and releases, the wheel and window resizes; everything else, and everything while the subsystem is inert, reports consumed=false and succeeds, the same pass-everything contract akgl_controller_handle_event already has. The documented call order is UI first, control maps second, skipping on consumed. A press, release or wheel is consumed when the pointer is over any UI element. The hit test asks clay against the layout the previous frame retained -- this frame's does not exist while events are polled, and one frame of staleness is the standard model's accepted cost -- with clay's internal full-screen Clay__RootContainer excluded, because being inside the window is not being over the interface (unexcluded, every click anywhere was consumed; the test that pins this caught it). Pointer state is fed to clay per event, as clay documents; the press edge is latched once per frame_begin for the widgets to come, rather than trusting Clay_PointerData's per-SetPointerState edge, which advances per mouse event. frame_begin also drives Clay_UpdateScrollContainers from the accumulated wheel (32 pixels per notch) and a wall-clock dt. Tests pin the whole contract: pass-through before init, no consumption before any layout exists, press/release inside vs beside a panel, right button ignored, motion never consumed, wheel consumed only over the UI, resize reaching the layout engine, and key events left alone. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
3b3bfcbe10
|
Render clay layouts: fonts, text measurement, executor and the frame bracket
The UI subsystem now draws. akgl_ui_frame_begin/frame_end bracket one frame's CLAY() declarations: begin clears the error stash and starts the clay layout, end computes it and walks the render commands through a backend -- RECTANGLE as (rounded) fills, BORDER as radius-shortened edge fills plus corner arcs, TEXT through akgl_text_rendertextat one wrapped line per command, IMAGE as an akgl_Sprite's first frame stretched to the bounding box, and the SCISSOR pair through akgl_draw_set_clip, cleared again on any exit so a failed frame cannot leave the world clipped. akgl_ui_font_register maps registry font names onto clay's uint16_t fontIds. The table keeps the *name* and resolves it per use -- fonts are not reference counted, and a cached TTF_Font* would dangle where a name reports "gone" honestly. Clay_TextElementConfig.fontSize is deliberately ignored: libakgl bakes the size into the handle at load, so one face at two sizes is two ids. The measure bridge passes clay's non-NUL-terminated slices straight to SDL_ttf's explicit-length TTF_GetStringSize -- no copy, no scratch. Its signature has no error channel, so failures report zero-by-zero and stash a message that frame_end raises, the same route clay's own void error handler uses; layout errors take precedence over drawing and one bad frame leaves the next one clean. Tests drive real CLAY() layouts through the software renderer and read pixels back: fill placement, border edges with an empty middle, a child clipped by its container, text landing in its colour, slice-vs-whole measurement agreement, fontId table dedupe/refusal/exhaustion, and the bracket's begin/begin, end-without-begin and failure-recovery contracts. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
064e6569e8
|
Add rounded-rect fill, arc stroke and clip-rect drawing primitives
The UI subsystem's render executor needs all three -- clay emits rectangles with corner radii, per-side borders, and scissor commands -- but they earn their place in draw.h on the same terms as everything else there: a rounded panel, a ring gauge and a clipped viewport are things a game wants with or without a layout engine. akgl_draw_filled_rounded_rect decomposes into three band fills plus four quarter-circle triangle fans through SDL_RenderGeometry, with the radius clamped to half the shorter side. akgl_draw_arc strokes between an outer and inner radius as one triangle strip, stepping in proportion to the swept angle under a fixed vertex cap -- no VLAs, unlike clay's own SDL3 reference renderer. akgl_draw_set_clip wraps SDL_SetRenderClipRect and is the one draw entry point where a NULL rectangle is legal, because that is how a clip is cleared. Pixel-readback tests cover the corner geometry (inside the arc filled, outside it untouched), the radius clamp, the zero-radius fall-through, ring and quarter-arc coverage at mid-stroke, sweep bounds, and clip set/clear round trips. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
b7ff54b09f
|
Vendor clay and bring the UI subsystem up: arena, lifecycle, status band
clay v0.14 (deps/clay, zlib licence) supplies the layout engine for the new akgl_ui subsystem. Sources-listed rather than add_subdirectory()'d, on the semver/libccd precedent: clay's CMakeLists declares no library target, builds its examples by default, and requires CMake 3.27 against this project's 3.10. src/ui_clay.c is the one CLAY_IMPLEMENTATION translation unit, compiled -w on the vendored-code terms but with clay's symbols deliberately exported -- consumers' CLAY() macros resolve against libakgl.so, and akgl/ui.h warns them never to link a second clay. The subsystem is runtime-optional the way collision is: always compiled in, inert until akgl_ui_init(), which bounds clay to the overridable AKGL_UI_* ceilings, checks Clay_MinMemorySize() against a static BSS arena (no malloc), and refuses with both numbers in the message when it does not fit. Measured: 812544 bytes at the default 1024 elements / 4096 measured words, against a 1 MiB arena. clay's void-callback layout errors are logged as they happen and the first is stashed for the error protocol to raise later. AKGL_ERR_UI joins the status band before AKGL_ERR_LIMIT, named in akgl_error_init. New `ui` test suite covers validation, the init/shutdown lifecycle, and arena exhaustion via the akgl_ui_arena_limit test hook (same contract as akgl_ccd_arena_set_limit). clay.h is installed beside semver.h, its licence beside libccd's, and the headers suite proves akgl/ui.h self-contained -- clay.h compiles clean under -Wall. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
75da766724
|
Record how to find out whether a tutorial teaches
docs_examples proves every listing in docs/ compiles and still matches the file it was quoted from. It cannot prove a chapter teaches anything -- a document can be made entirely of verified excerpts and still be unfollowable, because what a reader needs is the glue between them. So AGENTS.md gains "Testing a Tutorial": hand the chapter to a subagent on a weaker model with the library, its headers and the art but not the finished program, and make them build and run the game. The weaker model is the point; it will not paper over a gap with knowledge the document did not give it. The section carries the rules that make the result mean something -- copy the tree with examples/ and docs/ removed rather than asking them not to look, let them read the public headers because a real reader has them, do not make them hand-author a .tmj, give them a screenshot helper, and verify their binary yourself -- plus the sandbox recipe, and the warning to prove the build path works before handing it over. It also records how to read the report, because a weaker model states its own mistakes as documentation defects with total confidence. Reproduce before believing; and a rejected finding usually still leaves something behind. The evidence for doing any of this is in the table at the end: four read-only reviews of chapters 20 and 21 found real gaps and missed all three of the defects the first build-and-run found, including a published example output the chapter could not produce. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
cf0142ea85
|
Say how close an NPC has to be placed to be talked to
A reader drew their own town, put the elder 115 pixels from anywhere the player could stand, and spent the rest of their time looking for the bug in jrpg_cmhf_talk. There is no bug: the handler finds nobody within TALK_RANGE and returns success, which is indistinguishable from the game not being wired up. The chapter stated the constant and not what it means in the editor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
6e97d27e49
|
Fix what showed up when the tutorials were actually built and run
Two readers were given the library, the art, the chapter, and no reference
program, and told to compile and run. Both got a working game. These are the
defects that only came out because a compiler and a frame loop were involved --
four earlier read-only reviews had missed every one.
libakstdlib's header was never named. The chapters used aksl_strncpy,
aksl_snprintf and aksl_atoi throughout and said only "libakstdlib is documented
in deps/libakstdlib"; the obvious guess from the function prefix is <aksl.h> and
it is wrong. That was the only compile error the sidescroller reader hit, and it
was the chapter's fault. Both chapters now carry a per-file include table and say
which header it is.
The sidescroller's asset directory defaulted to ".". The chapter said the code
"falls back to `.`" without showing the #ifndef or that assetdir is initialised
from the macro, so a reader gets a game that only finds its art when launched
from exactly the right directory.
The sidescroller's summary line reported 0.0,0.0 airborne while the screenshot
showed the player standing on the ground. Teardown runs in main's CLEANUP block
and releases the actor pool before the summary prints. The chapter published an
example output a reader could not reproduce; the capture that makes it true is
now shown beside it.
The JRPG's CMake block interpolated ${JRPG_REPO_ROOT}, which is this repository's
own variable and undefined for anybody else -- so the font path came out wrong
and akgl_text_loadfont failed long after everything else had loaded. It is
${CMAKE_CURRENT_SOURCE_DIR} now, and the chapter says why both definitions are
needed rather than one.
Chapter 21 described --demo and a summary line in its build section that no step
wrote. Both are now a section: the script table, playback through
akgl_controller_handle_event rather than the handlers, the fixed clock step, and
printing the position before teardown. It also now says libakgl keeps no frame
counter, which a reader assumed it did.
Both map sections now say to draw it in Tiled and point at chapter 13, naming the
fields the loader needs that Tiled fills in automatically -- both readers
hand-wrote a .tmj before being given one and hit exactly those.
Excerpts across docs/ go from 190 to 198.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
|
|||
|
149bee0c99
|
Fill the gaps two readers found in the tutorials
Two agents were given nothing but the chapter text and asked to build the game from it. Between them they named every place the chapters showed a fragment and called it an explanation. This closes those. Chapter 20 now shows what it previously only named: the whole ss_Game struct rather than a truncated one, SS_COIN_COUNT and SS_HAZARD_COUNT, ss_ActorData, the player and blob body rectangles, asset_path, hitbox, find_actor, respawn, both jump handlers, the control-map function head, the gamepad buttons, the blob's probe arithmetic, and a complete standalone CMakeLists rather than three lines out of one. It also names the library globals a reader is expected to use without declaring, lists all seven actor hooks rather than the two this game replaces, and says in order what each of the two hook functions ends up doing. Chapter 21 gains the same treatment: the header's includes and declarations, the textbox colours and their SDL_Color type, TTF_Font, the jrpg_Townsfolk row type, character_load in full, the player lookup, the frame loop, and per-file include tables for both games. Two claims were wrong and are corrected. "Each step is complete on its own" was not true of a cumulative tutorial. And the first argument to akgl_controller_handle_event is not a game-state word the dispatcher reads -- the header says nothing reads it and it is required only as a non-NULL token. Every step that produces code now ends with what the reader should see when they run it. Excerpts across docs/ go from 137 to 190. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
6553da812c
|
Rewrite both tutorials as tutorials
They read as design commentary: why a choice was made, what the previous state of the project was, which library defect a line works around. That is useful to somebody who already knows the library and useless to somebody trying to build a game, which is who a tutorial is for. Both chapters now open with a screenshot of what the reader is building and a numbered list of the steps to get there, and each step is its own section: what you are trying to achieve, the code that achieves it, and the two or three things about that code that are easy to get wrong. Chapter 20 starts from first principles -- what a sprite, a character, an actor and a tilemap are, and the four error macros -- and chapter 21 assumes it and covers what a top-down game adds. Where a line exists because of a defect, the line comes first and the defect comes after it, with a pointer to the TODO entry that will remove the need. A reader who is copying code should not have to read an apology before they can use it. The library's own history is gone from both. That belongs in the design chapters and in git. Two new docs_examples setups stage the tutorials' own art, so the sprite and character files a reader is shown are loaded exactly as printed rather than through a generic fixture. `json excerpt=` is new for the same reason: a Tiled object is now quoted out of the real map instead of being retyped. Excerpts across docs/ go from 91 to 137. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
0972472cfa
|
Take the tutorial figures out of the games themselves
Both examples gain --screenshot PATH and --screenshot-frame N. The capture sits between the world being drawn and the frame being presented, because SDL_RenderPresent is where the target stops being readable, and it works under the dummy video driver and the software renderer like the rest of the headless path does. `cmake --build build --target docs_game_figures` regenerates docs/images/sidescroller.png and docs/images/jrpg.png by running each game to a chosen frame. Same contract as docs_screenshots: deliberate, never part of a build, because the PNGs are tracked. There is no --check counterpart, and the target says why -- the sidescroller drives its physics from the wall clock, so a byte comparison would fail for reasons that have nothing to do with the documentation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
4e32328681
|
Remove the corner helpers akgl_collide_rectangles no longer uses
akgl_rectangle_points, akgl_collide_point_rectangle, akgl_Point and akgl_RectanglePoints go. They were the intermediate form of an implementation that changed: akgl_collide_rectangles was eight corner-containment tests built on them, and it has been four span comparisons since the cross-case fix. Nothing outside tests/ called either function, and a point-in-rectangle test is four comparisons a caller can write without a struct conversion in front of them. akgl_collide_rectangles stays. It has two correct callers in the sidescroller asking a game-level overlap question -- a coin, a hazard, from an updatefunc -- where a bool is the whole answer and a proxy plus a narrowphase call would be computing a normal nothing reads. TODO.md records the split rather than leaving it to be rediscovered. Public API removal, so 194 exported akgl_ symbols against 196, and the manual's counts move with them. The perf suite loses its rectangle_points row; the all-pairs sweep stays as the control it is now labelled, and PERFORMANCE.md says what 0.8.0 measured against it -- 188.5 us for 32,640 pairs at 256 actors, where a whole step with collision attached is 54.1 us doing strictly more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
3a6569e384
|
Build the shape before spawning a proxy from it
test_reset_and_empty called spawn() with an uninitialized stack akgl_CollisionShape and built it on the next line. akgl_collision_proxy_initialize copies the shape, so grid_insert read uninitialized half-extents to work out which cells to file the proxy under -- eighteen findings inside grid_cellrect, and the test still passed because the proxy_sync two lines later overwrote all of it. Found by scripts/memcheck.sh, which is the only thing that could have found it: the assertion it makes is about the query afterwards, and the query answers correctly either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
71e18184c0
|
Ship libccd's notice, and keep the rectangle helpers on purpose
libccd is compiled into libakgl.so and is BSD-3-Clause, so the notice has to travel with the binary form. cmake --install now puts BSD-LICENSE at share/doc/akgl/BSD-LICENSE.libccd, and the README says which dependency is in that position and which are not -- everything else in deps/ is either a separate shared object carrying its own notice, a header, or compiled into nothing. The plan's last step was to delete akgl_rectangle_points, akgl_collide_point_rectangle and akgl_collide_rectangles. That step is not taken, and TODO.md carries the reasoning rather than leaving it to be rediscovered: akgl_collide_rectangles has two correct callers in the sidescroller asking a game-level overlap question that wants a bool, and it was fixed two commits into this same series -- deleting it now would be a strange thing to do to a caller. The other two are the weak case and are named as the candidates if a 0.9.0 wants to trim. akgl_RectanglePoints' comment still claimed akgl_collide_rectangles works corner by corner, which stopped being true when the cross case was fixed. Corrected in place, with the cross case named so the note explains why the shape changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
986c80d0ec
|
Bump to 0.8.0, close Target 12, and record what is still missing
Collision changed the collide slot's signature and grew four public structs, so this is an ABI break and the soname moves to libakgl.so.0.8. The manual's counts move with it: 195 functions across 23 headers, 183 of which return an error context. Target 12 -- "collision for 256 actors under 2 ms without the caller writing a broad phase" -- is met. A new benchmark times the whole step with a world attached, which is the number the target is actually about rather than a narrowphase call in isolation: 15.4 us at 64 actors, and 54.1 us at 256 in a purpose-built -DAKGL_MAX_HEAP_ACTOR=256 configuration. Over that 4x range the step grew 3.5x while the all-pairs control grew 15.6x, which is the n-squared the index exists to avoid. Plan item 7 is rewritten to record what shipped and why both partitioners exist; the Construct and Phaser citations stay. Three new TODO entries. Actor rotation, scoped to the five places it touches and the one place it does not -- collision_support is written so rotating `dir` in and the answer back is the whole narrowphase change, and rotational *response* is explicitly a different piece of work. The swept narrowphase FLAG_BULLET reserves, with the tunnelling arithmetic written out so a game can check its own numbers against 1280 px/s. And the ccd arena being single-threaded and sized by measurement at 7,264 bytes per GJK/EPA box pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
cbf7c1b6c2
|
Delete the JRPG's collision too, and wall the map with proxies
cell_solid and feet_blocked go, and so does the prediction in the player's movementlogicfunc. That prediction was only ever exact because the town has zero gravity and zero drag -- v is t, so `x + tx * dt` is where the step lands. A map with gravity would have had to fold ey in as well, which is the game re-implementing the integrator. Resolution runs after the move now, so there is nothing to predict. The map's decoration layer carries `collidable`, which retires JRPG_LAYER_SOLID: akgl_TilemapLayer has no name member, so the game and the map had to agree on an index out of band and inserting a layer in Tiled broke it. The edge of the world is not on any layer, so it is four static proxies covering the outer ring plus a tile of overhang -- one pool slot per side instead of a hundred solid tiles, and the first use of the static-proxy path in either example. They carry LAYER_STATIC explicitly: shape_box defaults a shape to LAYER_ACTOR, and a wall left on that layer is a wall everything walks through. Only the player gets a shape. NPCs stand still and are spoken to; the follower is a child snapped to its parent every step. Verified against the old implementation with the demo script rewritten to hold one direction: holding left into a building stops the player at x=122 with both, and holding up into the map's edge stops them at y=-4 with both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
490e62dbbf
|
Delete the sidescroller's collision, and let the library do it
collision.c was 383 lines that re-implemented akgl_physics_simulate's own arithmetic, because movementlogicfunc runs before gravity, drag and the move and so had to predict where the actor would end up. The whole file goes. What replaces it is three lines in main.c, a shape on the player and the blob, and a `collidable` property on the map's terrain layer -- which also retires SS_TERRAIN_LAYER_ID, a compiled-in layer id that broke whenever somebody reordered layers in Tiled. grounded stays a probe rather than becoming a contact test. A contact says something pushed back this step; grounded asks whether there is a floor to push off, and that stays true through the frame after landing. The blob keeps its own wall and ledge probes for the same reason: the resolver does not say which side of the blob it was pushed from, and says nothing at all about a floor that is missing. The summary line now carries the player's resting position, because exiting 0 was not evidence that collision ran. It reports 136.0,160.0 grounded after 240 autoplay frames -- feet at y=192, flush on the surface of terrain row 12. The tutorial's collision section is rewritten around the library's, keeping the quarter-of-a-pixel story as what a predicting resolver costs. The docs harness learns `json excerpt=`, so the map property the chapter quotes is checked against the map. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
35a58670d0
|
Document collision, and correct what it made false elsewhere
The new chapter 15 covers the opt-in setup, where resolution runs in the step and why it runs after the move, shapes and the z-extrusion trap, the asymmetric masks and the defaults that matter more than the mechanism, tiles as a source rather than proxies, the response hook and the three ways it is easy to write wrongly, sensors, the four queries, both partitioners, and what is still not implemented. Collision falsified claims in eight other chapters. The physics chapter said "There is no collision detection, at all" and that collide always raises AKERR_API; actors had six behaviour hooks; the heap had five pools and four non-claiming acquires; the status band held five codes; and the design philosophy had exactly two pluggable subsystems. The appendix gains a collision.h status section, the three new pool ceilings and a table of the collision constants that are deliberately not in a public header. Also fixes chapter labels the renumbering commit left pointing at their old numbers -- "18. Utilities" linked to 19-utilities.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 |
|||
|
bace818998
|
Renumber chapters 15 to 21 up by one, to open a slot for collision
A pure rename plus link rewrite and nothing else. Collision is a pluggable subsystem with two implementations, shapes, a response hook and a tile binding; that is a chapter, and burying it inside the physics chapter is the shape this manual otherwise avoids -- rendering and drawing are 8 and 9, spritesheets and characters and actors are 10, 11 and 12. Kept separate from writing the chapter because **nothing validates a cross-reference target.** The example harness reads fenced blocks; it does not follow links. A rename mixed into five hundred lines of new prose is not reviewable, and a broken link would land silently. As its own commit it is reviewable as a rename, and a grep for dangling `](NN-` targets is clean. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
fcad2822a9
|
Measure collision, and say which numbers moved for what reason
Seven new benchmark rows, budgets set from a measured full-scale run rather than guessed, and the three existing rows the work moved re-recorded from that same run. The all-pairs sweep stays and is relabelled `control:` -- it is the cost a caller paid before the library had a broad phase, measured on the same machine in the same run, which is the only honest way to read a reduction. What the numbers say: - The box fast path is two to seven times cheaper than the general solver, and the disjoint case is cheaper still because the proxies' bounds reject it before any shape arithmetic runs. 9 to 20 ns is what a tile game actually pays. - The grid beats the tree by 2.6x on the same population, which is the argument for the default measured here rather than cited from another engine. The tree also rebuilds on every move and that is not in its row, so a moving scene is worse than 2.6x. - A grid `move` that changes nothing is 11.5 ns. That is the incremental claim in one number. Two rows moved on a backend with **no collision world attached**, so no collision code runs in either, and the commit says so rather than letting the feature take credit: - akgl_Actor grew from 415 to 464 bytes -- a 40-byte shape, an override flag and a proxy pointer. The step sweeps the whole pool, so that is about 3 KB more working set per frame. The empty-pool row is unchanged at 58.7 ns, which is what identifies the cost as per live actor rather than per slot. - Reaching the collision check through CATCH cost more than the check. PASS and CATCH call akerr_valid_error_address, which walks AKERR_ARRAY_ERROR, so an early-returning function is not free when it is reached through one. The no-collision path now routes around the machinery entirely, which took the 64-actor sweep from 2,018.7 ns back to 1,588.5. AGENTS.md records that lesson for writing benchmarks; this is the same thing in production code. The remainder is the struct, and it is paid whether or not a game uses collision. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
cf930f68bb
|
Add a second partitioner, so the vtable is a seam and not a decoration
A binary space partition on libakstdlib's tree links and lists, registered in the factory as "bsp". It runs the same assertions the grid does, because tests/partition.c is a table over partitioners and adding a row is all it takes to be held to the contract. **Use the grid.** This is here to prove the vtable works and to have something to measure the grid against, and the file says so at the top. It rebuilds whenever the proxy set changes, which is the shape PERFORMANCE.md records Phaser using and capping out around five thousand bodies. It would earn its place in a world with wildly non-uniform object sizes, or one larger than the grid's fixed cell array covers. The difference is visible in the code rather than buried in a benchmark: the grid's `move` compares four integers and returns when a proxy has not left its cells, and this one marks the whole partition stale. The incremental-move assertion in the suite is therefore grid-only, and says why. aksl_tree_iterate is not used, and the file carries the three reasons so the next reader does not rediscover them: it is a complete traversal whose only control signal stops the entire walk, so there is no way to prune a subtree -- which is the only operation a spatial query is made of; it carries no per-node context, and a pruning descent needs each node's bounds and plane; and its breadth-first modes allocate, while only the depth-first ones are malloc-free and those are the ones without pruning. aksl_tree_insert is unusable for a different reason again: it is a comparator-ordered BST, and a spatial insert has to compare a leaf against a plane, which aksl_TreeCompareFunc cannot express. What is used is the link structure and the lists, neither of which allocates, which is why they fit here at all. The descent is an explicit stack rather than recursion, so the depth bound is an array bound the compiler can see -- this library already has one documented way to blow the C stack and does not need a second. Split planes are the median of the item centres on the longer axis, not the spatial midpoint: actors in a tile game cluster on the floor, and a midpoint split gives one empty child and one full one for several levels running. A split that separates nothing degrades the node to a leaf rather than recursing forever on the same set. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
5e213061af
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the old shape had nowhere to put a normal, a depth, or a piece of map geometry, and no answer to which of the two actors it was supposed to be resolving. **Resolution runs after `move`.** That is the decision everything else follows from. A resolver called from movementlogicfunc runs before gravity, drag and the move, so it has to predict where the actor will end up -- which means re-implementing the integrator's arithmetic in the game, and being wrong whenever the integrator changes. examples/sidescroller/collision.c carries forty lines of exactly that, and says so. Only `move` is subdivided. Gravity, drag, the thrust integration and the speed ellipse all still run once against the whole dt, and sum(subdt) is dt, so an actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f` is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That is what lets the integrator stay untouched and every recorded physics number stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no world attached. Collision is opt-in. A backend with no collision world costs one comparison per actor per step and does nothing else. Also here: - `self->gravity` was never null-checked and is dereferenced unconditionally, so a hand-built backend with only `move` filled in crashed rather than reporting. - Children are re-snapped in a post-pass, gated on collision being on. The in-loop snap uses whatever position the parent had when the child's slot came up in pool order, which is already sometimes a frame stale; that was harmless while a parent only moved by v*dt and is not once a parent can be pushed out of a wall mid-step. - tests/physics.c's deliberate tripwire has been visited. It asserted that arcade collide always raised AKERR_API, with a comment saying it existed so that implementing collision would have to come back here. What replaces it asserts the opt-in contract instead. - physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal goes stale the first time somebody adds a simulation, which is this commit. Three new whole-motion simulations, and the third took two attempts to make honest: - Landing and resting: 0.0000 px of drift over 120 steps after settling. - Walking after landing: 107 px in a second, which is the symptom a player sees when a resting actor is caught on the ground it is standing on. - A fast fall not tunnelling. The first version used gravity, so the step size grew every frame and the actor happened to land *inside* the floor rather than stepping over it -- it passed with sub-stepping disabled and proved nothing. It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a 32-pixel window in which an overlap exists, and the samples miss it cleanly. With sub-stepping the actor stops at y=288; without it, y=1300. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
a8526ad55e
|
Give an actor a say in how it answers a collision
A seventh behaviour hook, collidefunc, defaulting to akgl_actor_collide_block. Contacts are delivered one actor at a time with the normal already pointing the way that actor has to move, which removes the "which one is a1" ambiguity that made the old physics `collide` slot unimplementable -- and means a game overriding one actor's response never has to reason about the other's. The default moves the actor out along the normal by the penetration depth and removes the component of its motion that was going into the surface. Three things about that are easy to write wrongly, and each has a test that names the symptom rather than the assertion: - **It writes `e` and `t`, never `v`.** akgl_physics_simulate recomputes velocity as `e + t` at the top of every step, so a write to `v` is discarded before anything reads it. Worse than useless: an actor holding a direction into a wall would keep accumulating thrust while standing still and leave at speed the instant the wall ended. Breaking this leaves the fall running at 300 px/s through the floor. - **It removes the component, not the axis.** Zeroing the whole thrust vector costs the actor its motion *along* the surface as well, which is a character scraping a ceiling stopping dead and a character landing on a floor losing its run. Sliding along a wall is the difference between a wall and glue. - **A separating contact is left alone.** An actor already moving out of a surface that gets its velocity zeroed is an actor stuck to a wall it is walking away from, and it reads to a player as the collision grabbing them. `e` and `t` are treated separately rather than as their sum, so an actor pressing into a floor loses its downward gravity without losing the sideways run it is also doing. A sensor is reported and never pushed -- walking through a coin is not being blocked by it. The default deliberately does not pre-load the environmental term with a step of gravity. A resolver running *before* the step has to, or the step it is correcting for re-adds gravity and commits a quarter pixel of penetration, which is invisible and still fatal because every horizontal move then reads as blocked. This library resolves after the move, so the case does not arise; the sidescroller example carries that workaround because it had no choice. Also asserted: the other six hooks survive. A seventh that displaced one of them would be a silent regression somewhere unrelated. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
e632abf720
|
Let a map say which layers are solid, and answer questions about them
A tile layer with a `collidable` boolean custom property is collision geometry. Before this a game had to hard-code a layer index -- both examples do, because akgl_TilemapLayer does not retain the name Tiled wrote -- and that index changes the moment somebody reorders layers in the editor. Solid tiles are **not** given proxies. At the maximum map size that is a quarter of a million per layer, tens of megabytes of index to describe data that is already a dense grid sitting in the tilemap. The world keeps a borrowed pointer and reads layers[i].data[] over whatever cell range a query covers: nine array reads for a 32-pixel actor on 16-pixel tiles, nothing to build at level load, and nothing to maintain per frame. Static geometry that is not tile-aligned is still an ordinary proxy with AKGL_COLLISION_FLAG_STATIC; both mechanisms exist and tiles use the free one because there are a hundred thousand of them. Four public queries come with it, all of which answer without resolving: solid_at, box_blocked, query_box and settle. They are what a game reaches for when it wants to know rather than to be pushed -- a ledge probe ahead of a walking enemy, a check that a doorway is clear -- and the sidescroller cannot drop its hand-rolled collision without them. akgl_collision_settle is the one worth naming. Resolution stops a shape entering geometry and has nothing to say about one that began inside it: what it does instead is refuse every move, so an actor spawned in a wall is simply stuck. Level authors produce that constantly, so settling walks a shape up a tile at a time and refuses loudly rather than searching forever. Two defects found by writing the tests: - The fixture put an akgl_Tilemap on the stack and segfaulted before the first assertion. It is about 26 MB -- the layer and tileset arrays are sized for the worst case the format allows -- and tilemap.h says so. It is static now, with a comment saying why, because the next person to write a map fixture will reach for a local first as well. - The far-edge nudge used AKGL_COLLISION_EPSILON, which is 1e-6. That is a sensible tolerance on a unit vector and a meaningless one on a map coordinate: `float` carries about seven significant digits, so at a coordinate of 144 the smallest representable step is around 1.5e-5 and `144.0f - 1e-6f` is exactly 144.0f. The nudge did nothing, a box resting flush on the floor read as inside it, and every move it tried looked blocked -- an actor standing on the ground unable to walk. Two different quantities were sharing one constant; the tile one is now its own, at a thousandth of a pixel, which is what the sidescroller example independently arrived at. Both breaks verified: removing the nudge and ignoring the `collidable` bit each turn the suite red with the symptom named. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
68e042bd47
|
Add a pluggable broad phase, and the incremental uniform grid behind it
akgl_Partitioner is a record of function pointers and an initializer, the same shape as the render and physics backends. `move` is its own slot rather than remove-then-insert, and that is the whole design: a uniform grid's move is a comparison and a return when the proxy has not left the cells it was in, which is the steady state for a walking actor and the permanent state for a static one. Spelling it as remove-and-insert would turn the incremental grid into the rebuild-every-frame tree that PERFORMANCE.md already measured and rejected. The grid is a dense 128x128 array of cell heads over the world, cells keyed on tile size, with two intrusive chains per entry -- one through the cell so a query can walk it, one through the proxy so removal unlinks a whole span without searching. Everything is an int16_t index rather than a pointer, so the structure is relocatable and clearing it is a memset. A proxy covering more cells than AKGL_COLLISION_GRID_MAX_SPAN spills onto one chain every query walks, which is what makes the cell pool's ceiling provable rather than hopeful. tests/partition.c compares every query against a linear scan over the same proxies and asserts containment in one direction, not equality. That asymmetry is the contract: over-reporting costs a narrowphase call, under-reporting is a wall an actor walks through. The suite is a table over partitioners so the same assertions run against every implementation, which is what the second one landing later will be held to. Three deliberate breaks, and two of them were not caught the first time: - Removing the min-cell rule so a shared-cell pair reports repeatedly: caught. - Dropping the unlink in `move`: **not caught**, initially. Stale entries do not produce wrong query answers, because the bounds re-check filters them out -- they leak the cell pool until the grid stops working, on a timescale no unit test reaches by accident. Counting live pool entries across 200 moves is the only thing that sees it, and now does: 505 entries against an expected 5. - Swapping floorf for a truncating cast: **not caught, and correctly so.** Everything below zero is clamped to cell 0 either way, so today the two are indistinguishable. The comment claiming truncation is a defect was overstating it and now says what is actually true: the clamp is the only thing hiding it, and a negative world origin or a relaxed clamp would make it real with no test standing in front of it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
a3cb6ca79a
|
Answer whether two shapes overlap, and how to undo it
akgl_collision_test takes two proxies and fills in a contact: a unit normal, a depth, and a point. Three paths, cheapest first. The proxies' bounds reject most pairs in four comparisons and no arithmetic, which is free because the bounds were computed when the proxy was synced. A box against a box is answered in closed form. That is not only cheaper than the general solver, it is *exact*, and the difference shows up where it matters most: an actor resting on a floor wants a normal of precisely (0, -1, 0), and an iterative solver converges to something like (0.0001, -0.99999, 0), which accumulates into a slow sideways creep. A tile game is almost entirely boxes, so this is the path that runs. Everything else goes to ccdMPRPenetration. MPR rather than GJK+EPA because MPR allocates nothing at all, converges in fewer iterations, and its one weakness -- a coarser contact *point* -- is on a field the blocking resolver never reads. EPA stays compiled and available behind the arena for a caller who one day wants an accurate manifold. The header says the point is approximate so nobody builds a damage falloff on it. The normal points out of the second shape and toward the first, so a caller moving `a` along it by `depth` separates the pair. libccd hands back the opposite convention; converting once here saves every resolver from remembering the sign, and getting it backwards would compile, would pass any "do these collide" test, and would drag actors into walls. There is a test that asserts the direction, and inverting the conversion turns it red. AKGL_COLLISION_TEST_PLANAR flattens the normal into the xy plane. The extrusion the setters apply already makes z the most expensive axis, so this is a net for a caller who set a depth by hand -- and the failure it catches is silent: the narrowphase reports a contact, the resolver pushes the actor into the screen, and the actor does not move on screen while remaining inside the floor. It also rescues the degenerate case, two shapes at exactly the same centre, where there is no planar direction at all and a zero-length normal would be a wall that stops nothing. Level authors stack things constantly. Three things the tests found rather than assumed: - The guard has two copies, one per path, and the box one is where it earns its place. Removing both is caught; removing only the iterative one is not, because the iterative solver is seeded from the line between the two centres and so converges to a planar answer on its own for a planar offset. That is measured and written down in the test rather than papered over with a fixture contrived to force it. - A concentric pair has no preferred sign on the degenerate axis, so the test asserts the magnitude of the normal rather than its direction. The first version asserted -1 and failed against an equally correct +1. - The box fast path and the iterative solver are two implementations of one answer, so they are driven over the same arrangements and required to agree on the decision and the axis. A shortcut nothing cross-checks is a shortcut waiting to diverge. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
d0b057f47a
|
Pool collision proxies, and tie one to the life of its actor
A proxy is what the broad phase will index: an actor's shape, where it is, and the bounds that follow from those. It is a sixth heap layer, two per actor -- one for the actor itself and headroom for static geometry a game registers that is not tile-aligned. Solid *tiles* deliberately get none: the broad phase will read those out of the tilemap's own array, because one proxy per tile is tens of megabytes to index data that is already a grid. The pool follows the existing convention rather than improving on it. akgl_heap_next_collision_proxy finds a free slot and does not claim it; akgl_collision_proxy_initialize claims it. That is TODO.md item 8's asymmetry, and it is kept on purpose: an acquire abandoned before initialization leaks nothing, because the slot still reads as free, and a sixth pool with its own rule would be worse than one defect with five instances. Fixing item 8 means fixing all five together with every call site audited, and that is its own change. The proxy holds a *copy* of the shape rather than a pointer. An actor's own logic may rewrite its shape mid-step -- a character crouching, a projectile arming -- and a broad phase whose bounds came from a shape that has since changed is a bug that only appears when two things happen in the same frame. akgl_heap_release_actor now releases the actor's proxy. Without it the proxy holds a borrowed `owner` into a slot that has just been zeroed, so the broad phase keeps a registration whose owner reads as a free actor, and the next contact against it is a collision with nothing. Verified by removing the release and watching test_proxy_dies_with_its_actor go red. The tests pin the convention as well as the behaviour: that two acquires with no initialize between them return the same slot is asserted, not merely tolerated, so that changing the convention has to change a test that says why it exists. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
9056ff06d6
|
Add collision shapes, on the character and overridable per actor
A shape is a convex volume positioned relative to an actor's origin. It lives on akgl_Character, so every goblin sharing a character shares one shape definition the way they already share speeds and the state-to-sprite map, and an actor may override it with akgl_Actor::shape_override. The flag is not redundant with an empty shape. "This actor deliberately has no collider" and "this actor has not been given one yet" are different states, and without somewhere to record the difference akgl_actor_set_character cannot tell whether it is allowed to overwrite what it finds. include/akgl/collision.h is a leaf: it names actors and tilemaps as incomplete struct pointers rather than including their headers, because both of those need akgl_CollisionShape by value and a cycle forms otherwise. The `headers` suite compiles it as the first include of a translation unit, so that property is enforced rather than merely intended -- which is why the tilemap types got struct tags two commits ago. The masks are asymmetric and the defaults are the load-bearing part: layermask ACTOR, collidemask STATIC. An actor given a shape and nothing else collides with map geometry and with no other actor. "Everything with a hitbox shoves everything else" would be a surprising default for a town full of scenery and a painful one to discover after the fact; opting in to actor-versus-actor is one bit, opting out of it would have been a hunt through every NPC a game spawns. Every shape gets a z depth, because the narrowphase behind this is three dimensional and answers with the *minimum* separating translation. A thin extrusion makes z the cheapest axis, and an actor pushed along z goes nowhere a player can see while remaining inside the floor -- a collision system reporting success while doing nothing. The ratio of 2 is the smallest for which the z overlap of any two shapes the setters build provably exceeds any planar penetration they can reach. The test for that asserts the inequality across a spread of sizes rather than asserting the constant is 2, so a change to the constant fails here rather than in a game. Two things about that test are worth recording, because the first version had both: - It could not fail. TEST_ASSERT expands to FAIL_BREAK, which reports by `break`ing, and the assertion was inside a nested `for` -- so the break left the loop rather than the ATTEMPT block and the failure evaporated. This is the hazard AGENTS.md documents for CATCH; it applies to the whole family. Verified by setting the ratio to 0.5 and watching the suite still pass, then fixing it and watching it report the 512x512 case by name. - tests/collision_arena.c had the same bug in a loop added in the previous commit, and it is fixed here too. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
46232a5ad1
|
Give libccd a static arena so libakgl still allocates nothing
libakgl does not call malloc at runtime. That is stated in seven places in the manual and is AGENTS.md's second standing rule, and every object comes from a fixed array in akgl/heap.h. libccd's EPA path does not know that: it builds its expanding polytope out of realloc and free, and ccdGJKPenetration documents a -2 return for when that fails. The alternative was to compile only the three files MPR needs and leave EPA out of the build. That works and it puts a copy of somebody else's source list in this repository, where it rots silently on the next submodule bump. This instead compiles all of libccd and points its allocator at a bump allocator over static BSS, which keeps the promise literally true -- and keeps EPA available rather than amputated, for the day a precise contact manifold is worth having. The arena is reset at the top of each query rather than freed block by block, so the lifetime is one narrowphase call, `free` is a no-op, and allocation is a pointer bump. Exhaustion returns NULL, which libccd already handles by unwinding to -2, which becomes AKGL_ERR_COLLISION naming the high-water mark -- a loud failure with a number in it rather than a silently missed collision, which would be a floor an actor falls through reported as success. One GJK/EPA box pair costs 7,264 bytes, measured and printed by the suite. The 64 KB ceiling is that with about nine times headroom, and the high-water mark is reported so the next reader can re-derive it rather than trust this line. The redirect lives in src/ccd_arena_shim.h, injected with -include, and not in CMake's COMPILE_DEFINITIONS. It was in COMPILE_DEFINITIONS first, and that is a mistake worth recording: CMake cannot carry a function-like macro through a -D, so it dropped __CCD_ALLOC_MEMORY without a diagnostic. libccd went on calling the C library's realloc while the shim's `free` quietly discarded the results -- strictly worse than doing nothing, and invisible, because it leaks rather than crashes. What caught it was insisting the test prove the wiring rather than the outcome. The first version asserted the arena balanced back to zero after a query, which turned out to be the wrong assertion for a different reason -- free is a no-op by design, so it cannot balance -- but a test that had merely checked "two boxes collide" would have passed throughout, against an allocator nothing was using. The suite now asserts what is actually provable: that a query allocates from the arena at all, and that the process survives, since glibc aborts when the real free(3) is handed a pointer it never issued. Also here: AKGL_ERR_COLLISION, with the name registered -- tests/error.c asserts the band and the names agree, and it caught the missing one immediately. -fvisibility=hidden and CCD_STATIC_DEFINE keep every ccd* symbol out of libakgl.so's dynamic table, which `nm -D` confirms is empty; AGENTS.md records a shipped defect where an exported `renderer` was preempted by a same-named symbol elsewhere, and a game linking a system libccd would hit exactly that. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
6bb241cfb9
|
Register libccd and tg as submodules instead of stray directories
Both were sitting in deps/ untracked, with dangling .git pointer files into
.git/modules/{libccd,tg} and no .gitmodules entry at all. A clean clone of this
repository got neither, and `git submodule update` had nothing to update. tg's
worktree had also been emptied -- all 95 files staged as deletions inside its
own index -- so the directory held nothing but the pointer.
Both are now real submodules, pinned by SHA and checked against the tag:
deps/libccd 7931e764a19ef6b21b443376c699bbc9c6d4fba8 = v2.1
deps/tg 823f78db7e104ad5074288b4f362f0d3ef0fb0b6 = v0.7.9
Named "deps/libccd" and "deps/tg" to match the eight entries already there,
which meant re-registering rather than adopting the existing .git/modules
directories -- those were named "libccd" and "tg" while every other module lives
under .git/modules/deps/. Both checkouts were verified pristine at their tags
first, so nothing was lost by re-cloning.
URLs are https://, which is what both remotes already pointed at. Five of the
existing eight entries use git@github.com:, which a CI checkout with no SSH key
cannot use; that is not this commit's problem and is left alone.
tg is wired into nothing, deliberately, and TODO.md now says so with the reason
it is kept anyway -- indexed point-in-polygon against concave polygons is the
one job GJK and MPR structurally cannot do. That entry is a deadline: either the
consumer appears or the submodule comes out.
Nothing consumes either dependency yet, so the build and the full suite are
unchanged.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
|
06f8e03a03
|
Give the tilemap types struct tags so they can be forward-declared
`typedef struct { ... } akgl_Tilemap;` declares no struct tag, so
`struct akgl_Tilemap` does not exist and no header can forward-declare it. That
is fine while every consumer includes tilemap.h, and it is a blocker the moment
a leaf header needs to name the type without pulling it in -- which the coming
collision header does, since tilemap.h reaches physics.h and a cycle forms
otherwise.
All four types in the file get a tag rather than just akgl_Tilemap: they are
siblings declared the same way in the same header, and tagging one of four is
the kind of inconsistency that makes the next reader wonder what is special
about it.
Layout-identical and no behaviour: a tag names a type, it does not change one.
Full suite, reindent and doxygen unchanged.
sprite.h and iterator.h declare their types the same way and are deliberately
left alone; nothing needs to forward-declare them yet.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
|
842ef75ddf
|
Report the overlap akgl_collide_rectangles could not see
It asked whether either rectangle enclosed one of the other's four corners -- eight akgl_collide_point_rectangle calls, stopping at the first hit. That is a different question from "do these overlap", and it has the wrong answer for one arrangement: a tall thin rectangle crossing a short wide one overlaps in a plus sign with no corner of either inside the other, and all eight tests said no. A long thin platform crossing a tall thin character is exactly that shape, so this is a shape a 2D game produces, not a curiosity. util.h carried an @note describing it and docs/18-utilities.md had a diagram of it, both under the heading of a limitation rather than a defect, and there was no test for it at all -- nor for full containment, nor for a shared edge. It is four comparisons now, on both axes. `<=` rather than `<` because akgl_collide_point_rectangle is inclusive on all four edges and these two have always agreed that touching counts; a span test written with `<` would have silently changed a contract both the header and the manual state. The test was written first and failed on the cross before the fix went in. Two answers change for a caller upgrading, and both are in the header note and the chapter: - The cross reports `true`, which is the point. - The comparison is in float rather than through akgl_Point's int members, so a sub-pixel overlap is no longer truncated away. A pickup test that was accidentally forgiving by up to a pixel is no longer forgiving. Both tutorials use this for coins and hazards; both still pass. Faster as a side effect rather than a goal, and worth recording because the numbers move a documented budget: 24.9 ns -> 6.1 overlapping, 57.9 -> 6.1 disjoint, and the all-pairs sweep over 64 actors 115 us -> 12.2. The disjoint case gained most because it was the one that ran all eight tests before answering. The three moved rows are re-recorded in PERFORMANCE.md and nothing else is. akgl_rectangle_points is untouched by this change and reads 6.1 in the same run against the 4.0 recorded, so 6 ns is this run's floor and the new figure means "too cheap to measure" rather than "exactly 6.1" -- said in the prose so the next reader does not re-baseline the table around it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
943bf4324e
|
Let a control map listen to any keyboard, and fix the dead arrow keys
The sidescroller's controls did nothing. `akgl_controller_handle_event` matched `event->key.which == curmap->kbid` exactly, with no way to say "whatever keyboard the player is typing on", so the game did the obvious thing and bound `SDL_GetKeyboards()[0]`. That cannot work. The id a key event *carries* is chosen by the video backend and is not required to be an id `SDL_GetKeyboards()` reports. On X11 without XInput2 every key event carries `SDL_GLOBAL_KEYBOARD_ID`, which is 0, while `SDL_AddKeyboard` registers the attached keyboard as `SDL_DEFAULT_KEYBOARD_ID`, which is 1; with XInput2 the events carry the physical slave device's `sourceid` while `SDL_GetKeyboards()[0]` may be the master. Either way the map matched nothing and every key press was dropped. A `kbid` or `jsid` of 0 now matches any device of that kind. 0 is safe to spend: SDL documents `which` as 0 when the source is unknown or virtual, and joystick ids start at 1, so no real device is 0. A non-zero id still matches only that device, which is what keeps two local players on two keyboards apart, and there is a test for that half too. `util/charviewer.c` already passed 0 and depended on the old behaviour by accident; it works under XInput2 now as well. Two reasons this shipped, both closed: - Every test in tests/controller.c dispatched an event whose id equalled the id the map was bound with, so none of them could see it. - The sidescroller's smoke test called the control handlers directly instead of dispatching events, so it passed against a control map that matched nothing. It now pushes synthetic events through akgl_controller_handle_event from a deliberately non-zero device id and fails if the press does not arrive. Verified by breaking the binding and watching the run exit non-zero. `test_controller_wildcard_device_ids` was written first and failed against the unfixed library with "a map bound to keyboard 0 ignored a key press from device 11", which is the whole reason to trust it now that it passes. The controls were documented, in one sentence. Chapter 19 has a proper table of them, and chapter 15 explains why not to reach for SDL_GetKeyboards(). The header said the match was exact and now says what it does. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
15ac5d5dd0
|
Record what writing the manual found: 18 defects and 27 false claims
Twenty-one chapters and two games were written against src/ rather than against the header comments, and the exercise turned up two classes of problem. Both are recorded here because publishing a problem you cannot fix yet is a contribution, and because the second class is the more dangerous one: every item in it was documented, in a header, incorrectly. The one to fix first is defect 1. akgl_game_update calls akgl_physics->simulate() with no NULL check and akgl_default_physics is zeroed BSS, so a program that never calls an initializer segfaults on its first frame -- measured, exit 139, not an AKERR_NULLPOINTER. physics.h tells the reader akgl_game_init selects a backend from a `physics.engine` property, which is false in both halves, so a caller who believes the header writes exactly the program that crashes. That is the worst first-contact experience in the library and the fix is a NULL check. Defect 2 is the subtlest. A child actor's offset is counted twice: physics.c writes x as parent->x + vx, an absolute coordinate, and actor.c then draws at parent->x + obj->x while actor_visible three lines above treats obj->x as absolute. Two readings of one field inside one function, and actor.h documents both of them without noticing. Confirmed by rendering the frame with and without the guard and hashing the readback. The rest run from silent invisibility (an actor on layer >= 16 is updated and simulated and never drawn; a map-spawned actor has no facing bit and so no sprite) through dead API (speedtime is loaded, mis-cast, and read by nothing; there is no way to play a sound effect from a file at all) to asset provenance: tests/assets/World_A1.png and util/assets/Actor1.png carry RPG Maker's default filenames with no licence file, and util/assets/littleguy.json -- the sample data for the one demo the library ships -- does not load. The second section lists the 27 header comments that describe code that has since changed. Nothing catches these: WARN_IF_UNDOCUMENTED proves a symbol has a comment, not that the comment is true, and check_api_surface.sh strips comments precisely because prose is not a declaration. Doxygen publishes them. A third section marks entries in this file that are themselves stale, including item 15, which describes a leak that src/util.c already fixes with exactly the technique the item proposes. AGENTS.md warns that this file "carried eleven entries describing code that had already changed"; that is still accumulating, and a premise nobody has re-checked is where the next defect is hiding. plan.md is the plan the work was executed from, kept because it records the decisions and, in three places, corrections to itself -- including getting the error-code count wrong in the document whose purpose was fixing wrong documentation. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
88aaa184e4
|
Add two tutorial games that build, run in CI, and cannot drift
examples/sidescroller and examples/jrpg are complete programs, built with the library and exercised headless by ctest. The chapters quote them with `c excerpt=examples/...` blocks rather than restating the code, so a chapter cannot drift from a program that compiles -- the excerpt check fails the moment the source moves. 34 excerpts in one chapter, 21 in the other. The two are complementary. The sidescroller is the physics tutorial: gravity, a jump, coins, hazards. The JRPG is the content-pipeline tutorial: a town map, NPCs spawned from map objects, four-way per-facing animation, a text box, a follower. Both smoke tests drive real SDL_Events through akgl_controller_handle_event and step the physics clock at a fixed 1/60s rather than sleeping, so a scripted run is deterministic and finishes in under five seconds. Writing them is what turned up most of the defects recorded in the next commit, because a game exercises paths a unit test does not. Each workaround says in the chapter which library gap forced it: - collision is written in a custom movementlogicfunc, because akgl_physics_arcade_collide raises AKERR_API and akgl_physics_simulate never calls collide at all; - the sidescroller cancels the step's own gravity when it blocks downward, because otherwise a quarter-pixel of penetration makes the *horizontal* sweep report blocked and the character walks backwards a tile at a time; - both clear movement_controls_face on every map-spawned actor, because the default facefunc leaves a stopped actor with no facing bit, no sprite, and no draw; - the JRPG's follower gets a renderfunc that nulls obj->parent for the duration of the draw, because a child's offset is counted twice. Assets are CC0 from three Kenney packs, vendored with per-pack licence text, per-file provenance, and the geometry contract in docs/tutorials/assets/README.md. CC0 specifically rather than merely free: a reader who copies a tutorial into their own game inherits no obligation. scripts/fetch_tutorial_assets.sh refreshes them in the shape mkcontrollermappings.sh was fixed into for 0.5.0 -- it checks curl's status, refuses a pack page that does not say CC0, verifies the archive and the staged dimensions, and leaves the tracked bytes untouched on any failure. Both failure paths were tested, and a no-op refresh is byte-identical. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
b938460127
|
Add the manual: nineteen chapters and a corrected README
docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
8ac291d2dd
|
Compile, link and run every example in the documentation
libakgl exports 157 functions and the only user-facing documentation was a FAQ in README.md whose examples did not compile: two unbalanced `PASS()` calls, a `sprite->frameids = [0, 1, 2, 3];` that is not C in any dialect, a stray `9` inside a bitmask expression, an `int screenwidth = NULL`, and -- in the first snippet a reader ever saw -- the exact `strncpy` call AGENTS.md forbids. That is not a reader routing around typos. It is what happens to samples that nothing executes. This is akbasic's documentation harness with the interpreter taken out and the ability to link and run put in. The contract is a set of fence info strings, so an example is ordinary markdown that still highlights on the forge: ```c compiled with -fsyntax-only -Wall -Werror ```c wrap=NAME the same, wrapped in tests/docs_preludes/NAME.pre/.post ```c run=NAME linked against akgl and run headless ```c excerpt=PATH must still appear verbatim in PATH ```c screenshot=NAME also the source of docs/images/NAME.png ```json kind=KIND loaded through the real akgl_*_load_json ```output the exact stdout of the runnable block above it `run=` is why this links at all: -fsyntax-only proves a call typechecks, not that the startup order works or that an ATTEMPT block gives back what it took. `json kind=` exists because the asset formats are documented in prose and read by four loaders with nothing tying the two together -- util/assets/littleguy.json is already invalid against the loader it ships with. A fence with no info string is a hard error, and so is an unknown one. The failure mode this exists to prevent is passing because it quietly ran nothing, so an unannotated block is a missing decision rather than a default. Exit status is the number of failed examples; 2 for a usage error, which is a different thing and has to be distinguishable. Proven to fail on all ten of its failure modes -- untagged fence, non-compiling snippet, stale excerpt, orphan output block, unknown info string, run= output mismatch, invalid JSON, missing figure, a -Werror warning, and a dangling preload= -- because a check that has never failed has not been tested. `-Werror` here although AKGL_WERROR stays off for the library: that option is off so a new compiler's diagnostic cannot break a consumer's build, and a doc snippet is not a consumer. A sample that warns is a sample that teaches the warning. Registered as `docs_examples` and `docs_screenshots`. No docs-path filter in CI, deliberately: documentation goes stale because the code moved, not because somebody edited a chapter. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
2766372f37
|
Reindent tools/ and examples/ too, in both copies of the scope list
`scripts/reindent.sh` and `scripts/hooks/pre-commit` each carried their own copy of the directory list, and neither included `tools` or `examples`. Both hold ordinary hand-written C, so `--check` reported a clean tree without having looked at them and the hook let them through unformatted -- which defeats the one thing that keeps this tree a fixed point of `indent-region`. The comment in `reindent.sh` already said the scope is "directories whose C sources are hand-maintained", so this is the list catching up with the stated intent rather than a new policy. The two copies are now marked as being two copies of one decision, because they have already disagreed once and nothing detects it when they do. `scripts/reindent.sh --check` is clean with the wider scope. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
cc757ab578
|
Fix the two Doxygen errors that were failing the docs build
`Doxyfile` sets `WARN_AS_ERROR = FAIL_ON_WARNINGS` and CI builds the docs as a step of `cmake_build`, so both of these failed the job: - `akgl_character_initialize` documented `@param basechar` for a parameter the declaration spells `obj`. This is exactly the mismatch AGENTS.md warns about under Naming Conventions -- "parameter names must match between the declaration and the definition, Doxygen publishes the header spelling" -- and it cost two errors, one for the argument that does not exist and one for the argument left undocumented. - `require_at_eof` asked for an explicit link to `#AKERR_EOF`. That symbol is libakerror's and `Doxyfile`'s INPUT is `include/akgl src`, so the link can never resolve. Backticks say the same thing to a reader without asking Doxygen to find a page it does not have. `doxygen Doxyfile` now exits 0 with no diagnostics. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
|
7cf213235d
|
Plan the performance work the baseline points at
New "The plan" subsection under Performance in TODO.md: nine entries in blast-radius order. Six are next (string-pool free list, text texture ring cache, non-raising sprite lookup, hot-path log demotion, the tileset scan fix with its two latent draw bugs, the draw_world layer loop); three are on record but unscheduled (uniform-grid broad phase behind target 12, the realistic map fixture for target 13, the footprint refactor for 14 and 15). Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> |