# 09. Drawing

`draw.h` is immediate-mode plotting: put this on the screen *now*, as opposed to
add this to the scene. It sits alongside the actor and tilemap rendering in
[Chapter 08](08-rendering.md) rather than replacing it — a game can draw a world
through `draw_world` and then scribble a debug rectangle over it in the same frame.

The shape comes from a BASIC-style graphics vocabulary. `DRAW`, `BOX`, `CIRCLE`,
`PAINT`, `SSHAPE` and `GSHAPE` all map onto one of these, which is why the set is
what it is and why there is a flood fill in a game library at all.

| Function | Draws | Notable |
|---|---|---|
| `akgl_draw_point` | one pixel | clipping is SDL's, not reported |
| `akgl_draw_line` | a line, endpoints included | coincident endpoints draw one pixel |
| `akgl_draw_rect` | an outline | zero or negative w/h draws nothing, silently |
| `akgl_draw_filled_rect` | a solid box, outline included | same |
| `akgl_draw_circle` | an outline | midpoint algorithm, integer, no AA |
| `akgl_draw_flood_fill` | a connected region | CPU-side, bounded, **not reentrant** |
| `akgl_draw_copy_region` | *reads* a rectangle into a surface | allocates when you ask it to |
| `akgl_draw_paste_region` | a saved surface back onto the target | replaces, does not blend |
| `akgl_draw_background` | an 8x8 grey checkerboard | diagnostic, not a general primitive |

Signatures are in the generated Doxygen. Statuses are [Chapter 04](04-errors.md).

**These are ordinary functions, not backend methods.** Unlike `frame_start` and
`draw_world`, they are not slots on `akgl_RenderBackend` — they take an
`akgl_RenderBackend *` as their first argument and reach through its
`sdl_renderer`. Pass `akgl_renderer` unless you are deliberately drawing somewhere
else. Every one of them validates both `self` and `self->sdl_renderer` before
touching either.

## Colour is an argument, never a global

**Every entry point takes its colour as a parameter, and none of them reads or
writes a current-colour global.**

The reasoning is worth stating because it looks like extra typing. A caller that
has a notion of a current colour — a BASIC `COLOR` statement, a UI theme, a palette
index — *already owns that state*. Giving the library a second copy creates two
places for the answer to live and one opportunity for them to disagree. So there is
no `akgl_draw_set_color`, and there never will be.

The other half of the rule is the one that matters at a call site: **the renderer's
own draw colour is saved before each call and restored after it.** Drawing a red
line does not leave the renderer red. In particular, it does not change what the
next `SDL_RenderClear` — or the next `frame_start`, which clears — paints.

```c
#include <akgl/draw.h>
#include <akgl/game.h>

/* Two rectangles, two colours, no state carried between them. */
akerr_ErrorContext *draw_health_bar(float32_t x, float32_t y, float32_t frac)
{
    SDL_Color back = { 0x20, 0x20, 0x20, SDL_ALPHA_OPAQUE };
    SDL_Color fill = { 0xd0, 0x30, 0x30, SDL_ALPHA_OPAQUE };
    SDL_FRect rect;
    PREPARE_ERROR(errctx);

    rect.x = x;
    rect.y = y;
    rect.w = 100.0f;
    rect.h = 8.0f;
    PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &rect, back));

    rect.w = 100.0f * frac;
    PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &rect, fill));
    SUCCEED_RETURN(errctx);
}
```

Alpha is in the colour and blending follows the renderer's **current blend mode**,
which these functions do not change. If you want alpha to blend rather than
overwrite, set the blend mode yourself with
[`SDL_SetRenderDrawBlendMode`](https://wiki.libsdl.org/SDL3/SDL_SetRenderDrawBlendMode).
That one *is* renderer state libakgl leaves alone.

Restoring the colour happens in a `CLEANUP` block under `IGNORE()`. A failure to
restore is logged rather than propagated, on the grounds that it must not mask the
failure the cleanup is unwinding from.

## What the primitives look like

Every primitive in one frame, on a 320x240 surface. The picture below is generated by
running exactly this listing — see `MAINTENANCE.md` if you are editing it.

```c screenshot=primitives
    SDL_Color red    = { 0xd0, 0x30, 0x30, 0xff };
    SDL_Color green  = { 0x30, 0xc0, 0x50, 0xff };
    SDL_Color blue   = { 0x40, 0x70, 0xe0, 0xff };
    SDL_Color yellow = { 0xe0, 0xc0, 0x40, 0xff };
    SDL_FRect filled = {  20.0f,  20.0f,  80.0f, 50.0f };
    SDL_FRect framed = { 120.0f,  20.0f,  80.0f, 50.0f };

    PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &filled, blue));
    PASS(errctx, akgl_draw_rect(akgl_renderer, &framed, green));
    PASS(errctx, akgl_draw_circle(akgl_renderer, 260.0f, 45.0f, 25.0f, yellow));
    PASS(errctx, akgl_draw_line(akgl_renderer, 20.0f, 100.0f, 300.0f, 100.0f, red));

    /*
     * A row of points, to show that a point is a pixel and not a dot of some
     * nominal size. At this scale they read as a dashed line.
     */
    for ( float32_t x = 20.0f; x < 300.0f; x += 8.0f ) {
	PASS(errctx, akgl_draw_point(akgl_renderer, x, 115.0f, yellow));
    }
```

![](images/primitives.png)

The filled rectangle is `akgl_draw_filled_rect`; the outlined one beside it is
`akgl_draw_rect`. Neither left the renderer blue or green — the line drawn afterwards is
red because it asked to be, not because it inherited anything.

## Circles are integer midpoint circles

SDL3 has no circle primitive, so `akgl_draw_circle` plots one: the midpoint circle
algorithm, integer arithmetic throughout, eight-way symmetry — one computed point
in the second octant gives the other seven by reflection.

Three consequences you will see on screen:

- **The centre and the radius are rounded to whole pixels** (`SDL_lroundf`). A
  circle at `x = 10.5` is a circle at `x = 11`. There is no sub-pixel placement.
- **The outline is one pixel wide and is not anti-aliased.** There is no
  thickness parameter and no smoothing. If you want a thick or smooth circle, draw
  concentric ones or blit a texture.
- **There is no filled circle.** Only the outline. A filled disc is a flood fill of
  the interior, or a texture.

A radius of exactly 0 draws the centre pixel and nothing else. A negative radius is
`AKERR_OUTOFBOUNDS` and the message reports it.

A failure inside the plotting loop is recorded in a flag and reported once at the
end rather than aborting mid-circle — a `CATCH` there would `break` the loop rather
than leave the function, the hazard `AGENTS.md` describes. **So an
`AKGL_ERR_SDL` from `akgl_draw_circle` may leave a partial arc already on the
target.** The same is true of `akgl_draw_background`.

## Flood fill

`akgl_draw_flood_fill` is the odd one out, and it is worth understanding why before
using it. Every other primitive here is a command queued to the GPU. This one
cannot be: **the region it fills is defined by what is already on the screen**, so
it has to read the render target back, walk the region on the CPU, and blit the
result over the area it touched.

```text
    SDL_RenderReadPixels(target)        the whole target comes back
              |
    SDL_ConvertSurface(RGBA32)          fixed 32-bit layout, so the walk
              |                          can compare and write whole words
    flood_region()                      scanline fill over a bounded span
              |                          stack; records a dirty bounding box
    SDL_RenderTexture(dirty, dirty)     only what changed goes back
```

### What counts as the region

**Four-connected**: the fill spreads up, down, left and right, never diagonally. A
region joined only at a corner is two regions.

**The boundary is an exact colour comparison.** Any pixel whose packed RGBA value
differs from the seed's, by any amount, is a boundary. There is no tolerance
parameter.

That second one is the practical trap. **An anti-aliased edge stops the fill at its
first blended pixel and leaves a fringe** — you get the flat interior and a halo of
untouched blend pixels around it. Text rendered through SDL3_ttf, a scaled sprite,
and anything drawn with smoothing all have such edges. Flood fill wants hard-edged
art.

Filling a region that is already the requested colour is a **no-op that succeeds**,
not an error: walking it would compare filled pixels against themselves and find
nothing, so the function says so up front. A seed outside the render target is
`AKERR_OUTOFBOUNDS`, and the message reports both the seed and the target size.

### The span stack, and what it costs you

The fill keeps a fixed stack of horizontal runs still to be examined rather than
recursing per pixel:

```c excerpt=include/akgl/draw.h
#define AKGL_DRAW_MAX_FLOOD_SPANS 4096
```

At 4096 spans that array is 48 KB, which does not belong on the stack of a function
a game may call every frame — so it is **file scope**. Two consequences, and both
are hard rules rather than cautions:

- **`akgl_draw_flood_fill` is not reentrant.** Do not call it from inside a
  callback that a flood fill can reach. There is one span stack for the process.
- **It is not thread-safe.** Nothing that touches an `SDL_Renderer` is, so this is
  not a new restriction — but the span stack means it stays unsafe even against a
  second renderer on a second thread.

An ordinary convex or moderately concave shape needs a few dozen pending spans. A
region complicated enough to need more than 4096 at once reports
`AKERR_OUTOFBOUNDS` — and **leaves the region partially filled**. There is no way
to unwind a partial fill short of keeping a copy of the whole surface, and you
asked for a bounded operation. Treat that status as "redraw the area", not as
"retry".

```c
#include <akgl/draw.h>
#include <akgl/game.h>

akerr_ErrorContext *paint_region(int x, int y, SDL_Color color)
{
    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_draw_flood_fill(akgl_renderer, x, y, color));
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_OUTOFBOUNDS) {
	/*
	 * Either the seed was off-target, or the region needed more than
	 * AKGL_DRAW_MAX_FLOOD_SPANS pending spans -- in which case it is
	 * already PARTIALLY filled. Redraw the area; do not retry the fill.
	 */
	LOG_ERROR(errctx);
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}
```

The colour is **written over** the region rather than blended, since the pixels
going back are the ones just read out of it.

## Saving and restoring a region

`akgl_draw_copy_region` and `akgl_draw_paste_region` are `SSHAPE` and `GSHAPE`: read
a rectangle of the render target into an `SDL_Surface`, and draw one back.

### copy_region allocates only when you ask it to

`dest` is an `SDL_Surface **`, and **`*dest` must be initialized before the call**.
Its value selects between two behaviours:

| `*dest` on entry | What happens | Who owns the surface afterwards |
|---|---|---|
| `NULL` | a surface is allocated and written back through `dest` | **you do** — release it with `SDL_DestroySurface` |
| a surface of *exactly* `src`'s dimensions | the pixels are copied into it | you already did |
| a surface of any other size | `AKERR_OUTOFBOUNDS`, both sets of dimensions in the message | unchanged |

The reuse path exists so a caller saving the same region every frame does not churn
allocations. Note that `akgl_draw_copy_region` is one of the few places in libakgl
that hands you memory you have to free yourself — everything else comes from the
pools in [Chapter 05](05-the-heap.md).

**`src` must fit entirely inside the render target.** This is checked and refused
with `AKERR_OUTOFBOUNDS` rather than clipped, deliberately: SDL would clip the read
and hand back a *smaller* surface than was asked for, and a caller pasting it back
would not notice until the art was visibly wrong. A rectangle with no area is
refused too.

### paste_region replaces, it does not blend

`akgl_draw_paste_region` sets `SDL_BLENDMODE_NONE` on the texture it uploads, so
the saved pixels **overwrite** what is on the target. That is `GSHAPE`'s default
behaviour and it is the one that makes a save/restore pair actually restore: a
saved region's transparent pixels come back as transparent, rather than letting
whatever is underneath show through them.

There is no scaling — the surface is drawn at its own size — and the surface is not
consumed, so it may be pasted as many times as you like. Parts falling outside the
target are clipped by SDL and not reported.

```c
#include <akgl/draw.h>
#include <akgl/game.h>

/* SSHAPE then GSHAPE: save a 32x32 patch, draw over it, put it back. */
akerr_ErrorContext *blink_cursor(int x, int y)
{
    SDL_Surface *saved = NULL;
    SDL_Rect box;
    SDL_FRect cursor;
    SDL_Color white = { 0xff, 0xff, 0xff, SDL_ALPHA_OPAQUE };
    PREPARE_ERROR(errctx);

    box.x = x;
    box.y = y;
    box.w = 32;
    box.h = 32;
    cursor.x = (float32_t)x;
    cursor.y = (float32_t)y;
    cursor.w = 32.0f;
    cursor.h = 32.0f;

    ATTEMPT {
	/* saved is NULL, so copy_region allocates it and we own the result. */
	CATCH(errctx, akgl_draw_copy_region(akgl_renderer, &box, &saved));
	CATCH(errctx, akgl_draw_filled_rect(akgl_renderer, &cursor, white));
	CATCH(errctx, akgl_draw_paste_region(akgl_renderer, saved, cursor.x, cursor.y));
    } CLEANUP {
	if ( saved != NULL ) {
	    SDL_DestroySurface(saved);
	    saved = NULL;
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}
```

## The checkerboard backdrop

`akgl_draw_background` paints an 8x8 grey checkerboard — the pattern an image
editor uses to show transparency. It is a **diagnostic backdrop rather than a
general primitive**: `charviewer` uses it so a sprite's transparent pixels are
visible instead of blending into black.

It always starts at the origin. `w` and `h` are the extent to cover, not a
rectangle, and both round *up* to whole 8-pixel cells — a height of 12 paints 16. A
zero or negative extent paints nothing and is not reported.

Until 0.5.0 this was the one function in the library outside the error protocol: it
returned `void`, drew through the global `akgl_renderer` without checking it, and
left the renderer's draw colour changed. It takes a backend now and restores the
colour it found, which is also what makes it testable without a world.

## Where to look next

- [Chapter 08](08-rendering.md) — the backend these all take, and the frame the
  calls sit inside.
- [Chapter 17](17-text-and-fonts.md) — text, which is textures rather than
  primitives.
- [Chapter 05](05-the-heap.md) — the pools, and why `copy_region` returning owned
  memory is the exception rather than the rule.
