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>
This commit is contained in:
437
docs/18-utilities.md
Normal file
437
docs/18-utilities.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# 18. Utilities
|
||||
|
||||
`util.h`, `json_helpers.h` and `staticstring.h` hold the pieces that belong to no subsystem:
|
||||
rectangle overlap, asset path resolution, typed JSON accessors, and the pooled string type
|
||||
everything else passes around. The JSON accessors are the valuable part of this chapter —
|
||||
their status semantics are what every asset loader in the library is written against, and
|
||||
they are documented nowhere else.
|
||||
|
||||
## Collision helpers
|
||||
|
||||
Three functions, all axis-aligned, all in `util.h`:
|
||||
|
||||
| Function | Question it answers |
|
||||
|---|---|
|
||||
| `akgl_rectangle_points(dest, rect)` | What are the four corners of this `SDL_FRect`? |
|
||||
| `akgl_collide_point_rectangle(p, r, collide)` | Is this point inside those corners? |
|
||||
| `akgl_collide_rectangles(r1, r2, collide)` | Do these two rectangles overlap? |
|
||||
|
||||
```c
|
||||
#include <akerror.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akgl/util.h>
|
||||
|
||||
/* Do two actor boxes touch? Edges count as touching. */
|
||||
akerr_ErrorContext *boxes_touch(SDL_FRect *a, SDL_FRect *b, bool *hit)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, akgl_collide_rectangles(a, b, hit));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* Is the mouse over a button? Corners first, then the point test. */
|
||||
akerr_ErrorContext *point_in_box(int x, int y, SDL_FRect *box, bool *hit)
|
||||
{
|
||||
akgl_RectanglePoints corners;
|
||||
akgl_Point p;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
p.x = x;
|
||||
p.y = y;
|
||||
p.z = 0;
|
||||
PASS(errctx, akgl_rectangle_points(&corners, box));
|
||||
PASS(errctx, akgl_collide_point_rectangle(&p, &corners, hit));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
Four properties, all deliberate:
|
||||
|
||||
**Axis-aligned only.** There is no rotation support and no separating-axis test. An actor
|
||||
drawn with a non-zero `angle` still collides as its unrotated box.
|
||||
|
||||
**Edges count as touching.** A point exactly on a boundary is inside; two rectangles sharing
|
||||
an edge and no area overlap. Both comparisons are `>=` and `<=`.
|
||||
|
||||
**Coordinates are truncated from `float` to `int` on the way in.** A rectangle at
|
||||
`x = 10.9` has its corners at 10. That is right for tile-grid work and wrong for sub-pixel
|
||||
work; if you need the latter, do not round-trip through `akgl_rectangle_points`.
|
||||
|
||||
**`z` is carried and never used.** `akgl_Point` has one, `akgl_rectangle_points` never
|
||||
writes it, and neither collision test reads it. These are 2D tests.
|
||||
|
||||
### The arrangement it misses
|
||||
|
||||
`akgl_collide_rectangles` works corner by corner: it tests each rectangle's four corners
|
||||
against the other, eight tests, stopping at the first hit. Testing both directions is what
|
||||
catches one rectangle wholly inside the other, which has no corner in its neighbour.
|
||||
|
||||
**It still misses the cross.** A tall thin rectangle crossing a short wide one overlaps
|
||||
without either enclosing a corner of the other, and both are reported as not colliding:
|
||||
|
||||
```text
|
||||
+---+ +---+
|
||||
| | | |
|
||||
+---|---|---+ +---+---+---+ <- overlaps, but
|
||||
| | | | is | | | | no corner of
|
||||
+---|---|---+ reported +---+---+---+ either is inside
|
||||
| | as | | the other
|
||||
+---+ "no hit" +---+
|
||||
```
|
||||
|
||||
The fix is an edge-span comparison instead of a corner-containment one — `r1.x < r2.x + r2.w
|
||||
&& r2.x < r1.x + r1.w` on both axes — and it is not made here. If your game can produce that
|
||||
shape, and a long thin platform crossing a tall thin character is exactly that shape, test
|
||||
for it yourself. Recorded as an `@note` on the function.
|
||||
|
||||
This is separate from the fact that **nothing in the library calls these during a frame.**
|
||||
`akgl_physics_simulate` never calls `collide`, and `akgl_physics_arcade_collide` raises
|
||||
`AKERR_API` — see [Chapter 14](14-physics.md). Collision detection is yours to run, and these
|
||||
are the helpers for running it.
|
||||
|
||||
## Path resolution
|
||||
|
||||
Asset files name their neighbours relatively: a sprite definition names its spritesheet, a
|
||||
tilemap names its tilesets. "Relative" has to mean *relative to the file doing the naming*,
|
||||
not to wherever the game was launched from — and `akgl_path_relative` is what makes that
|
||||
true.
|
||||
|
||||
```c
|
||||
#include <akerror.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/util.h>
|
||||
|
||||
/* Resolve one asset's reference to a neighbour. dest must already be a
|
||||
claimed pool string -- this writes into it, it does not claim one. */
|
||||
akerr_ErrorContext *resolve_neighbour(char *mapdir, char *reference)
|
||||
{
|
||||
akgl_String *resolved = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, akgl_heap_next_string(&resolved));
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_path_relative(mapdir, reference, resolved));
|
||||
SDL_Log("%s -> %s", reference, resolved->data);
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(resolved));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
The order it tries things in:
|
||||
|
||||
```text
|
||||
akgl_path_relative(root, path, dest)
|
||||
|
|
||||
1. realpath(path) <- relative to the process CWD
|
||||
| |
|
||||
| +-- succeeds -> absolute result into dest, done
|
||||
| |
|
||||
| +-- ENOENT ---+
|
||||
| |
|
||||
2. +-> realpath(root + "/" + path)
|
||||
|
|
||||
+-- succeeds -> absolute result, done
|
||||
|
|
||||
+-- fails -> the errno propagates
|
||||
```
|
||||
|
||||
Either way the result is **absolute**, with symlinks and `..` folded out, because both
|
||||
branches end in `realpath(3)`.
|
||||
|
||||
Three things to know:
|
||||
|
||||
- **`dest` must already be a claimed pool string.** This writes into it; it does not claim
|
||||
one for you. `root` is required even on the path where it goes unused.
|
||||
- **The fallback pastes `root` in front of `path` unconditionally**, so an absolute `path`
|
||||
that does not exist gets `root` prepended and fails on a nonsense spelling. Do not send
|
||||
absolute paths you expect to fall through.
|
||||
- **`AKERR_OUTOFBOUNDS`** if `root + "/" + path` would not fit `AKGL_MAX_STRING_LENGTH`
|
||||
(`PATH_MAX`); **`ENOENT`**, or whatever else `realpath(3)` reports — `EACCES`, `ELOOP`,
|
||||
`ENOTDIR` — if neither spelling names an existing file; **`AKGL_ERR_HEAP`** if the string
|
||||
pool cannot supply the scratch buffers.
|
||||
|
||||
> **Stale defect note.** `util.h:124-128` warns that the fallback path "returns straight out
|
||||
> of the ENOENT handler and so never releases the error context it was handling", costing one
|
||||
> of libakerror's 128 context slots per call for the life of the process, and `TODO.md` item
|
||||
> 15 records the same thing as open. **Both are out of date.** `src/util.c:115-129` sets a
|
||||
> flag in the `HANDLE(errctx, ENOENT)` block and calls `path_relative_root` *after* `FINISH`,
|
||||
> which is exactly the fix that entry proposes, and the code carries a comment explaining
|
||||
> why. The leak is gone; the two notes describing it are not.
|
||||
|
||||
## The JSON accessors
|
||||
|
||||
Every asset format in this library is JSON, parsed by
|
||||
[jansson](https://jansson.readthedocs.io/), and jansson's own accessors answer "missing
|
||||
key", "wrong type" and "index past the end" all with the same `NULL`. `json_helpers.h`
|
||||
splits them apart so a malformed asset file produces a message naming the key and what was
|
||||
wrong with it, instead of a `NULL` dereference three frames later.
|
||||
|
||||
**jansson owns `json_t`, `json_decref`, the parser and the reference-counting rules.** What
|
||||
this section documents is which status means what, and who owns the results.
|
||||
|
||||
### Five conventions that run through the whole set
|
||||
|
||||
**Absence is an error.** A missing key is `AKERR_KEY` — not a quiet `NULL`, not a zero. That
|
||||
is the opposite of the search functions in libakstdlib, and it is deliberate: an optional key
|
||||
is expressed by handing the resulting error to `akgl_get_json_with_default`, not by the
|
||||
accessor staying quiet. So `AKERR_KEY` in a loader is frequently a `HANDLE` block rather than
|
||||
a failure.
|
||||
|
||||
**Results come back through `dest`,** because the return value is the error context. Every
|
||||
argument is `NULL`-checked — the document or array, the key where there is one, and `dest` —
|
||||
and each raises `AKERR_NULLPOINTER` with a message saying which. `dest` is not written on any
|
||||
failure path.
|
||||
|
||||
**`json_t *` results are borrowed, not owned.** `akgl_get_json_object_value`,
|
||||
`akgl_get_json_array_value` and `akgl_get_json_array_index_object` hand back pointers *into*
|
||||
the document. They are valid until the document is freed and **must never be
|
||||
`json_decref`'d.** Doing so drops a reference the accessor never took and takes the sub-object
|
||||
out from under the document that owns it.
|
||||
|
||||
**Strings are the exception: they are copied.** `akgl_get_json_string_value` and
|
||||
`akgl_get_json_array_index_string` write into a pooled `akgl_String` you release yourself.
|
||||
`*dest` doubles as an input — `NULL` means "claim one for me", non-`NULL` means "write into
|
||||
this one" — and it must be *initialized* either way, because an indeterminate `*dest` is
|
||||
dereferenced. A value longer than `AKGL_MAX_STRING_LENGTH` is truncated silently.
|
||||
|
||||
**The document is never modified.** No accessor writes to it, adds to it, or reorders it.
|
||||
|
||||
### The status table
|
||||
|
||||
These are the four statuses this family raises. [Chapter 4](04-errors.md) carries the full
|
||||
tables for the library; this one says what each means *here*.
|
||||
|
||||
| Status | Means | Raised by |
|
||||
|---|---|---|
|
||||
| `AKERR_NULLPOINTER` | Any argument was `NULL` — the document, the key, or `dest` | All |
|
||||
| `AKERR_KEY` | **The key is absent.** The idiomatic "optional thing was not there" | The object accessors |
|
||||
| `AKERR_TYPE` | The key is present but is the wrong JSON type | All |
|
||||
| `AKERR_OUTOFBOUNDS` | The index is outside the array. A negative index reports the same way as one past the end | The `akgl_get_json_array_index_*` family |
|
||||
| `AKGL_ERR_HEAP` | `*dest` was `NULL` and the string pool is exhausted | The two string accessors |
|
||||
|
||||
**`AKERR_INDEX` is not in that list, and libakgl never raises it anywhere.** It appears in
|
||||
this subsystem only as a `HANDLE_GROUP` arm inside `akgl_get_json_with_default`, defending
|
||||
against a status no accessor produces. Harmless, and worth knowing before you write a
|
||||
`HANDLE` block for it.
|
||||
|
||||
> **Stale header prose.** The conventions block at the top of `json_helpers.h` says "**@p
|
||||
> dest is not `NULL`-checked** except where noted, so a `NULL` there is a crash rather than an
|
||||
> error context", and `akgl_get_json_string_value`'s `@param key` says it is "checked here,
|
||||
> unlike the other accessors in this file". **Neither is true.** All eleven accessors check
|
||||
> `dest`, and all seven that take a `key` check that too — `src/json_helpers.c` raises
|
||||
> `AKERR_NULLPOINTER` with "NULL destination pointer" or "NULL key string" at every one. The
|
||||
> per-function claim that a `NULL` key "is reported as a missing key rather than as a `NULL`
|
||||
> pointer" is wrong the same way.
|
||||
|
||||
### Integer is strict; number is not
|
||||
|
||||
This is the distinction that catches people:
|
||||
|
||||
| Accessor | `"w": 3` | `"w": 3.0` | `"w": "3"` |
|
||||
|---|---|---|---|
|
||||
| `akgl_get_json_integer_value` | 3 | **`AKERR_TYPE`** | `AKERR_TYPE` |
|
||||
| `akgl_get_json_number_value` | 3.0f | 3.0f | `AKERR_TYPE` |
|
||||
| `akgl_get_json_double_value` | 3.0 | 3.0 | `AKERR_TYPE` |
|
||||
|
||||
`3.0` is a *real* in JSON, and `akgl_get_json_integer_value` refuses it rather than
|
||||
truncating it. Use one of the number accessors where either spelling should be accepted;
|
||||
`_double_value` exists because the physics constants are `double`.
|
||||
|
||||
`akgl_get_json_boolean_value` is strict in the same direction: `0` and `1` are numbers in
|
||||
JSON and are refused. It wants `true` or `false`.
|
||||
|
||||
### Reading a document
|
||||
|
||||
```c
|
||||
#include <akerror.h>
|
||||
#include <jansson.h>
|
||||
#include <akgl/json_helpers.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
/* A required key, an optional one with a default, and a borrowed sub-object. */
|
||||
akerr_ErrorContext *read_sprite_header(json_t *doc)
|
||||
{
|
||||
json_t *frames = NULL; /* BORROWED. Never json_decref this. */
|
||||
akgl_String *name = NULL; /* Copied, and pooled. Release it. */
|
||||
int width = 0;
|
||||
int width_default = 32;
|
||||
float32_t scale = 0.0f;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
/* Required: an absent "name" is AKERR_KEY and propagates. */
|
||||
CATCH(errctx, akgl_get_json_string_value(doc, "name", &name));
|
||||
|
||||
/* Optional: AKERR_KEY becomes width_default and a NULL return. */
|
||||
CATCH(errctx, akgl_get_json_with_default(
|
||||
akgl_get_json_integer_value(doc, "width", &width),
|
||||
&width_default,
|
||||
&width,
|
||||
sizeof(int)));
|
||||
|
||||
/* Strict: "scale": 2 and "scale": 2.0 are both accepted here.
|
||||
akgl_get_json_integer_value would refuse the second. */
|
||||
CATCH(errctx, akgl_get_json_number_value(doc, "scale", &scale));
|
||||
|
||||
/* Borrowed: valid until the document is freed. */
|
||||
CATCH(errctx, akgl_get_json_array_value(doc, "frames", &frames));
|
||||
SDL_Log("%s: %d wide, %.1f scale, %d frames",
|
||||
name->data, width, scale, (int)json_array_size(frames));
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(name));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
### `akgl_get_json_with_default` — how optional is spelled
|
||||
|
||||
Run the accessor, hand its error context here along with the fallback. A status it handles
|
||||
becomes `dest` holding the default and a `NULL` return; anything else propagates untouched.
|
||||
When the error *is* consumed it is also released, so you must not release it again. A `NULL`
|
||||
`e` — the accessor succeeded — returns at once without touching `dest`.
|
||||
|
||||
**It defaults on three statuses**, and this is the current behaviour, checked against
|
||||
`src/json_helpers.c:207-211`:
|
||||
|
||||
| Status | Defaulted? | Why |
|
||||
|---|---|---|
|
||||
| `AKERR_KEY` | Yes | The key is absent. That is what "optional" means for an object member |
|
||||
| `AKERR_OUTOFBOUNDS` | **Yes** | The index is past the end. That is what "optional" means for an array element |
|
||||
| `AKERR_INDEX` | Yes | Defensive only — nothing in libakgl raises it |
|
||||
| `AKERR_TYPE` | **No** | Propagates, and that is the correct line to draw |
|
||||
|
||||
**The `AKERR_TYPE` exclusion is the interesting one.** "The key is missing" and "the key is
|
||||
present but holds a string where a number belongs" are not the same situation. The first is
|
||||
an omitted setting and a default is exactly right for it. The second is a malformed document,
|
||||
and quietly substituting a default there would hide the defect and hand the game a value the
|
||||
author never wrote. So it propagates.
|
||||
|
||||
`AKERR_OUTOFBOUNDS` is the one that changed, because it is what the
|
||||
`akgl_get_json_array_index_*` family actually raises for a short array. **An array element
|
||||
does get a default now**, and "this element is optional" works for an array index as well as
|
||||
for an object member.
|
||||
|
||||
> **Stale header prose.** `include/akgl/json_helpers.h:230-234` still says the opposite —
|
||||
> that it defaults on `AKERR_KEY` and `AKERR_INDEX` "but *not* on `AKERR_OUTOFBOUNDS` … so
|
||||
> this pairs with the object accessors and does not currently give an array index a default".
|
||||
> That was true before 0.5.0. `src/json_helpers.c` now carries a third
|
||||
> `HANDLE_GROUP(e, AKERR_OUTOFBOUNDS)` arm, placed *above* the arm holding the `memcpy`
|
||||
> because `HANDLE_GROUP` emits no `break` and every arm falls into that body, and
|
||||
> `tests/json_helpers.c` covers it through both the integer and object index accessors.
|
||||
> `TODO.md` item 18 records it as fixed. The header comment wants correcting in its own
|
||||
> commit.
|
||||
|
||||
`defsize` is trusted, not derived: it is a `memcpy` through `void *` with no type
|
||||
information, and it must match the type both sides actually are.
|
||||
|
||||
**`defsize` is not the trap, though. Ownership is.**
|
||||
|
||||
```c
|
||||
#include <akerror.h>
|
||||
#include <jansson.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/json_helpers.h>
|
||||
|
||||
/* akgl_get_json_with_default hands BACK the context it was given when it
|
||||
does not handle the status. Take the result into a local, drop your own
|
||||
pointer, and release it yourself -- a CLEANUP block that also releases it
|
||||
double-releases, and a double-released context corrupts the failure
|
||||
instead of reporting it. */
|
||||
akerr_ErrorContext *optional_int(json_t *doc, char *key, int *dest, int fallback)
|
||||
{
|
||||
akerr_ErrorContext *keyerr = NULL;
|
||||
akerr_ErrorContext *defaulted = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
keyerr = akgl_get_json_integer_value(doc, key, dest);
|
||||
defaulted = akgl_get_json_with_default(keyerr, &fallback, dest, sizeof(int));
|
||||
keyerr = NULL; /* ownership passed either way */
|
||||
if ( defaulted != NULL ) {
|
||||
defaulted->handled = true;
|
||||
defaulted = akerr_release_error(defaulted);
|
||||
FAIL_RETURN(errctx, AKGL_ERR_BEHAVIOR, "%s was present but not an integer", key);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
The inline form in the previous example — passing the accessor's return straight into
|
||||
`akgl_get_json_with_default` inside a `CATCH` — is safe, because you never hold a second
|
||||
pointer to the context. It is when you *keep* one that this matters, and a test written the
|
||||
naive way passes against broken code: the first draft of the `AKERR_OUTOFBOUNDS` test in
|
||||
`tests/json_helpers.c` did exactly that. See [Chapter 4](04-errors.md) and `AGENTS.md` under
|
||||
"Testing Guidelines".
|
||||
|
||||
### The Tiled property helpers are elsewhere
|
||||
|
||||
`akgl_get_json_properties_integer`, `_number`, `_float`, `_double`, `_string` and
|
||||
`akgl_get_json_tilemap_property` are declared in `tilemap.h`, not here. They read Tiled's
|
||||
custom-property array — a list of `{"name": …, "type": …, "value": …}` objects — rather than
|
||||
a plain JSON object, which is a different shape and belongs to a different chapter. See
|
||||
[Chapter 13](13-tilemaps.md).
|
||||
|
||||
## `akgl_String`
|
||||
|
||||
The library allocates nothing at runtime, so a string here is a fixed-capacity buffer claimed
|
||||
from the pool:
|
||||
|
||||
```c excerpt=include/akgl/staticstring.h
|
||||
#define AKGL_MAX_STRING_LENGTH PATH_MAX
|
||||
|
||||
/** @brief Provides a fixed-capacity, heap-managed string buffer. */
|
||||
typedef struct
|
||||
{
|
||||
int refcount; /**< Pool bookkeeping; 0 means the slot is free. Owned by the heap layer. */
|
||||
char data[AKGL_MAX_STRING_LENGTH]; /**< The characters. Not guaranteed NUL-terminated when filled to capacity. */
|
||||
} akgl_String;
|
||||
```
|
||||
|
||||
`data` is a plain `char[]`, so `str->data` goes anywhere a `char *` does. `refcount` belongs
|
||||
to the heap layer — [Chapter 5](05-the-heap.md) — and **`akgl_heap_next_string` is the one
|
||||
acquire function that takes the reference for you**, unlike the other four pools.
|
||||
|
||||
Two functions operate on the contents:
|
||||
|
||||
| Function | What it does |
|
||||
|---|---|
|
||||
| `akgl_string_initialize(obj, init)` | Copies `init` in, or zero-fills when `init` is `NULL`, and sets `refcount` to 1. Always NUL-terminates; truncates silently |
|
||||
| `akgl_string_copy(src, dest, count)` | Bounded copy between two already-claimed slots. `count` of 0 selects the whole buffer. Always NUL-terminates. Leaves `refcount` alone |
|
||||
|
||||
**Capacity is `PATH_MAX`**, which is what makes the string pool by far the largest of the
|
||||
five — 256 slots is about a megabyte — and why claiming the *last* free slot from it costs
|
||||
250 ns against 4 ns from the actor pool. Same algorithm, different cache behaviour; the scan
|
||||
touches one reference count every 4 KiB. `PERFORMANCE.md` has the measurement.
|
||||
|
||||
`akgl_string_copy` refuses a negative `count` or one above `AKGL_MAX_STRING_LENGTH` with
|
||||
`AKERR_OUTOFBOUNDS`, because both buffers are exactly that long and a larger count walked off
|
||||
the end of two pool slots at once.
|
||||
|
||||
## The two test-only helpers
|
||||
|
||||
`akgl_compare_sdl_surfaces` and `akgl_render_and_compare` are in `util.h` and are labelled in
|
||||
the header as "REALLY slow routines that are only useful in testing harnesses". They are not
|
||||
frame-path functions and this manual does not otherwise cover them:
|
||||
|
||||
- `akgl_compare_sdl_surfaces(s1, s2)` — dimensions, pitch and format first, then a `memcmp`
|
||||
over the raw pixels. A mismatch is reported as `AKERR_VALUE`, an error rather than an
|
||||
out-parameter.
|
||||
- `akgl_render_and_compare(t1, t2, x, y, w, h, writeout)` — draws each texture into the same
|
||||
rectangle, reads the framebuffer back after each, and compares. Answers "do these render
|
||||
the same", which is not the same question as "are these identical", because the renderer's
|
||||
scaling and blending sit in between. `writeout` names a PNG of the first render, under
|
||||
`SDL_GetBasePath()`, which is how you see what was actually drawn when an assertion fails.
|
||||
|
||||
Both have a scar worth repeating. Until 0.5.0 `akgl_render_and_compare` drew `t1` on **both**
|
||||
passes, so it always reported a match and every image assertion built on it asserted nothing.
|
||||
Reference in New Issue
Block a user