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>
446 lines
21 KiB
Markdown
446 lines
21 KiB
Markdown
# 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 `<=`.
|
|
|
|
**`akgl_rectangle_points` truncates from `float` to `int`.** 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 it. **`akgl_collide_rectangles` no longer does**
|
|
— it compares the spans in `float` directly, so a sub-pixel overlap registers.
|
|
|
|
**`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 used to miss
|
|
|
|
`akgl_collide_rectangles` compares the two rectangles' spans on both axes. Until 0.8.0 it
|
|
asked a different question — whether either rectangle enclosed one of the other's four
|
|
corners, eight tests, stopping at the first hit — and that question has the wrong answer for
|
|
one arrangement:
|
|
|
|
```text
|
|
+---+ +---+
|
|
| | | |
|
|
+---|---|---+ +---+---+---+ <- overlaps, but
|
|
| | | | was | | | | no corner of
|
|
+---|---|---+ reported +---+---+---+ either is inside
|
|
| | as | | the other
|
|
+---+ "no hit" +---+
|
|
```
|
|
|
|
A tall thin rectangle crossing a short wide one overlaps in a plus sign with no corner of
|
|
either inside the other, so all eight tests said no — and a long thin platform crossing a
|
|
tall thin character is exactly that shape. The header carried a `@note` describing it rather
|
|
than a fix.
|
|
|
|
It is four comparisons now, `r1.x <= r2.x + r2.w && r2.x <= r1.x + r1.w` on both axes.
|
|
`<=` rather than `<` because [`akgl_collide_point_rectangle`](#collision-helpers) is
|
|
inclusive on all four edges and these two have always agreed: touching counts.
|
|
|
|
**If you are upgrading from 0.7.x, two answers change.** The cross now reports `true`, which
|
|
is the point. And because the comparison no longer round-trips through `akgl_Point`'s `int`
|
|
members, overlaps and gaps smaller than a pixel are now seen rather than truncated away — so
|
|
a pickup test that was accidentally forgiving by up to a pixel is no longer forgiving.
|
|
|
|
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.
|