# 10. Spritesheets and sprites

Two objects, and the split between them is the whole design:

- An **`akgl_SpriteSheet`** owns one `SDL_Texture` — an image file uploaded to the
  GPU — and nothing else.
- An **`akgl_Sprite`** is one animation: a frame size, a playlist of frame numbers,
  a dwell time, two loop flags, and a **borrowed** pointer to the sheet those
  frames are cut from.

So a walk cycle, an idle pose and a death animation drawn on the same PNG are three
`akgl_Sprite` objects pointing at one `akgl_SpriteSheet`, and the image is uploaded
once. That is not an optimization you have to ask for; it is what the loader does.

Both are pool objects — `akgl_heap_next_sprite`, `akgl_heap_next_spritesheet`, see
[Chapter 05](05-the-heap.md) — and both publish themselves in a registry under
their name, which is how [Chapter 11](11-characters.md) and the tilemap loader find
them. See [Chapter 06](06-the-registry.md).

**The renderer has to exist before any of this.** Loading a sheet uploads a
texture, so `akgl_render_2d_init` (or `akgl_render_2d_bind` with a live
`SDL_Renderer` on it — see [Chapter 08](08-rendering.md)) comes first.

## One image, one texture: how sharing actually works

`akgl_sprite_load_json` does not load an image and hope. It:

1. resolves the sheet's `filename` to an **absolute, canonical path**;
2. looks that path up in `AKGL_REGISTRY_SPRITESHEET`;
3. reuses the sheet it finds, or claims one from the pool and loads it.

**The resolved path is the registry key.** That is what makes ten sprites cut from
one image cost one texture, and it is why the resolution rule below matters more
than it looks.

On the reuse path the sheet's reference count is **not** incremented — the sprite
borrows it. `akgl_sprite_initialize` does not take a reference either. So:

> **Releasing a spritesheet out from under a live sprite leaves a dangling
> pointer.** Nothing detects it. In practice, release sprites and sheets together
> at the same lifecycle boundary (a level change), not individually.

### Where a sheet filename resolves from

This is a two-step rule and the first step surprises people:

```text
    "spritesheet": { "filename": "hero.png" }   in docs/assets/hero_walk.json

    step 1   realpath("hero.png")
             -> relative to the PROCESS'S CURRENT WORKING DIRECTORY
             -> if that file exists, that is the sheet. Done.

    step 2   only if step 1 raised ENOENT:
             realpath(dirname("docs/assets/hero_walk.json") + "/" + "hero.png")
             -> relative to the SPRITE JSON'S OWN DIRECTORY
```

**A sprite definition can sit next to its image and move with it** — that is step 2,
and it is the behaviour the format is designed around. But step 1 runs first, so a
file of the same name in the working directory shadows the one beside the JSON. If
a sprite loads the wrong art, check the working directory before checking the path.

Both steps end in `realpath(3)`, so symlinks and `..` are folded out and the key is
canonical. Two different spellings of the same file through `akgl_sprite_load_json`
therefore land on **one** sheet. (Calling `akgl_spritesheet_initialize` directly
skips all of this — it uses whatever string you hand it as the key verbatim, so
there two spellings really are two sheets.)

`AKERR_KEY` from a sheet load usually means the registry is not up. `ENOENT` means
neither step found the file.

## Frames are counted left to right, wrapping down

A sheet has no grid metadata that the library uses. The grid comes from the
*sprite*: `width` and `height` are both the drawn frame size **and** the stride
used to find a frame on the sheet.

```text
    A 192x96 sheet, with a sprite whose width and height are 48:

    +----+----+----+----+
    |  0 |  1 |  2 |  3 |   frame numbers count left to right from the
    +----+----+----+----+   top-left, then wrap to the next row
    |  4 |  5 |  6 |  7 |
    +----+----+----+----+

    "frames": [ 4, 5, 6, 5 ]   an animation is a playlist of frame numbers.
       index:   0  1  2  3     A frame may repeat; the order is yours.
```

Two indices, and confusing them is the most common mistake here:

| Name | What it indexes | Where you see it |
|---|---|---|
| **frame number** | a cell on the sheet | the values inside `"frames"` |
| **frame id** | a slot in the `"frames"` playlist | `akgl_Actor::curSpriteFrameId`, and the `frameid` argument to `akgl_spritesheet_coords_for_frame` |

`akgl_spritesheet_coords_for_frame` takes a **frame id**, looks up
`frameids[frameid]`, multiplies by the sprite width, and wraps whole rows off the
right-hand edge of the texture, stepping down one sprite height per row. The result
is the source rectangle for the blit.

> **It does not bounds-check `frameid`.** An index past `frames` — or past
> `AKGL_SPRITE_MAX_FRAMES` — reads a neighbouring struct member and yields a
> nonsense rectangle rather than an error. It also dereferences `sheet->texture`
> without checking it, so a registered-but-unloaded sheet is a crash.

`akgl_SpriteSheet` carries `sprite_w` and `sprite_h` fields. **They are vestigial.**
Nothing in the library writes or reads them, and the two arguments
`akgl_spritesheet_initialize` takes for them are accepted and discarded. The grid
that is actually used is the sprite's `width`/`height`. They are still in the
struct and in the signature because removing them is an ABI change.

## The sprite JSON format

```json kind=sprite setup=spritesheet
{
    "spritesheet": {
	"filename": "spritesheet.png",
	"frame_width": 48,
	"frame_height": 48
    },
    "name": "hero walking left",
    "width": 48,
    "height": 48,
    "speed": 100,
    "loop": true,
    "loopReverse": true,
    "frames": [
	12,
	13,
	14
    ]
}
```

| Key | Type | Meaning |
|---|---|---|
| `spritesheet.filename` | string | Image path. Resolved as above. Required. |
| `spritesheet.frame_width` | integer | Read **only when the sheet is not already loaded**, then discarded. |
| `spritesheet.frame_height` | integer | Same. |
| `name` | string | Registry key in `AKGL_REGISTRY_SPRITE`. Required. |
| `width` | integer | Frame width in pixels, and the horizontal stride. Must be >= 1. |
| `height` | integer | Frame height in pixels, and the vertical stride. Must be >= 1. |
| `speed` | integer | **Milliseconds** one frame is held. 0 to 4294. |
| `loop` | boolean | Restart at the end rather than holding the last frame. |
| `loopReverse` | boolean | With `loop`, walk back down instead of jumping to 0 — a ping-pong. |
| `frames` | array of integers | Frame numbers, in playback order. At most 16, each 0..255. |

**Every one of those keys is required.** These are not optional-with-a-default
lookups: an absent key is `AKERR_KEY` naming it, and a key of the wrong JSON type is
`AKERR_TYPE`. `speed` as a string fails; `frames` as an object fails.

### speed is milliseconds in the file and nanoseconds in the struct

`akgl_Sprite::speed` is compared against `SDL_GetCurrentTime`, which counts
nanoseconds. Nobody wants to type nanoseconds into an asset file, so the loader
reads milliseconds and multiplies by `AKGL_TIME_ONEMS_NS` (1000000).

**If you set `speed` by hand rather than through the loader, scale it yourself.**
Writing `spr->speed = 100` means 100 nanoseconds, and the animation advances every
frame.

The field is `uint32_t`, so the multiply is what bounds the range: values above
`UINT32_MAX / AKGL_TIME_ONEMS_NS` — **4294 ms** — would overflow, and are refused
with `AKERR_OUTOFBOUNDS` rather than wrapping. A negative `speed` is refused too.

### What the loader actually validates

Read against `src/sprite.c`, not against the header prose:

| Check | Status | Message says |
|---|---|---|
| `strlen(filename) >= AKGL_MAX_STRING_LENGTH` | `AKERR_OUTOFBOUNDS` | the JSON path is too long for a pooled string |
| `width <= 0` | `AKERR_VALUE` | "a sprite must be at least one pixel wide" |
| `height <= 0` | `AKERR_VALUE` | "a sprite must be at least one pixel high" |
| `speed < 0` or `speed > 4294` | `AKERR_OUTOFBOUNDS` | the value and the permitted range |
| `frames` longer than `AKGL_SPRITE_MAX_FRAMES` (16) | `AKERR_OUTOFBOUNDS` | the declared count and the maximum |
| a frame number outside 0..255 | `AKERR_OUTOFBOUNDS` | which index, its value, and the range |

The last two are recent and are worth knowing about because `sprite.h` still says
otherwise. Its `@note` on `akgl_sprite_load_json` reads *"The `frames` array is not
bounded against `AKGL_SPRITE_MAX_FRAMES`. A definition with more than 16 frames
writes past `frameids` into the rest of the struct."* **That is no longer true** —
the count is bounded before anything is written, and `tests/assets/` carries
fixtures for both the boundary and the overflow. The same `@brief` describes
`speed` as *"seconds, scaled to milliseconds"*, which is wrong in both units.
Correcting those comments is a separate commit; this chapter documents the code.

On any failure the pooled sprite **and any sheet loaded for it** are released
again, so a bad definition does not strand pool slots.

## Loading, and building one by hand

The ordinary path is one call per definition file:

```c
#include <akgl/sprite.h>

akerr_ErrorContext *load_hero_sprites(void)
{
    PREPARE_ERROR(errctx);

    /* Both cut from one image: the second call reuses the first's texture. */
    PASS(errctx, akgl_sprite_load_json("assets/hero_walk_left.json"));
    PASS(errctx, akgl_sprite_load_json("assets/hero_walk_right.json"));
    SUCCEED_RETURN(errctx);
}
```

You can also assemble one from the pools directly, which is what the loader does
underneath. `akgl_sprite_initialize` sets the name and the sheet and takes the
first reference; **everything else is left at zero for you to fill in**:

```c
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/sprite.h>

/*
 * Two animations cut from one image, built without a JSON file. One sheet,
 * one texture, two sprites borrowing it.
 */
akerr_ErrorContext *build_hero_sprites(char *imagepath)
{
    akgl_SpriteSheet *sheet = NULL;
    akgl_Sprite *walk = NULL;
    akgl_Sprite *idle = NULL;
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, imagepath, AKERR_NULLPOINTER, "imagepath");

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_spritesheet(&sheet));
	/* The two size arguments are discarded; the sprite's width/height win. */
	CATCH(errctx, akgl_spritesheet_initialize(sheet, 48, 48, imagepath));

	CATCH(errctx, akgl_heap_next_sprite(&walk));
	CATCH(errctx, akgl_sprite_initialize(walk, "hero walking left", sheet));
	walk->width = 48;
	walk->height = 48;
	walk->frames = 3;
	walk->frameids[0] = 12;
	walk->frameids[1] = 13;
	walk->frameids[2] = 14;
	walk->speed = 100 * AKGL_TIME_ONEMS_NS;     /* 100 ms, in nanoseconds */
	walk->loop = true;
	walk->loopReverse = true;

	CATCH(errctx, akgl_heap_next_sprite(&idle));
	CATCH(errctx, akgl_sprite_initialize(idle, "hero standing left", sheet));
	idle->width = 48;
	idle->height = 48;
	idle->frames = 1;
	idle->frameids[0] = 13;
	idle->speed = 1000 * AKGL_TIME_ONEMS_NS;
	idle->loop = false;
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}
```

A string literal is a safe `name` argument. `sprite.h` warns that the name is
*"copied at a fixed `AKGL_SPRITE_MAX_NAME_LENGTH` bytes, so a shorter string reads
past its end"* — that was a `memcpy` of the full field width and it is gone. The
copy is `aksl_strncpy` now, which reads only what is there and always terminates.

## Animation is the actor's job, not the sprite's

An `akgl_Sprite` is a description. Nothing in it moves. The advancing is done by
the actor holding it — `curSpriteFrameId`, `curSpriteFrameTimer` and
`curSpriteReversing` are actor fields, and `changeframefunc` is an actor hook. See
[Chapter 12](12-actors.md).

That is why the same sprite can be shared by a hundred actors that are all on
different frames.

The three flag combinations, as the default `changeframefunc` reads them:

| `loop` | `loopReverse` | At the last frame |
|---|---|---|
| true | false | wrap to frame id 0 |
| true | true | turn round and walk back down, turning again at 0 |
| false | either | **wrap to frame id 0 anyway** |

The last row is not a typo. A sprite with `loop` clear still wraps rather than
holding the final frame; there is no play-once behaviour in the default hook. A
game that wants one binds its own `changeframefunc`.

## Known defects worth knowing here

- **Truncated names are registry keys.** `akgl_Sprite::name` is 128 bytes and
  `akgl_SpriteSheet::name` is 512. A longer name truncates *silently*, and two names
  that truncate the same collide — the second registration replaces the first with no
  error. Recorded in `TODO.md`, "Truncated registry keys can collide"; the fix is a
  contract change, since the headers currently promise truncation.
- **The heap acquire functions are asymmetric.** `akgl_heap_next_string` increments
  `refcount`; `next_sprite` and `next_spritesheet` do not. `TODO.md` item 8. See
  [Chapter 05](05-the-heap.md).
- **No test asserts a clean sprite or spritesheet load/release cycle.** The tilemap
  cycle is asserted over 64 iterations; the sprite, spritesheet and character ones
  are not. `TODO.md`, "Targets", row 16.
- **`akgl_spritesheet_coords_for_frame` bounds-checks nothing.** See above.

## Where to look next

- [Chapter 11](11-characters.md) — binding sprites to actor states.
- [Chapter 12](12-actors.md) — what actually advances a frame.
- [Chapter 05](05-the-heap.md) — the pools these come out of.
- [Chapter 06](06-the-registry.md) — how a name becomes a lookup.
