# 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 #include /* 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 #include 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 #include /* 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 16](16-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.