Every libakgl test suite could report success while failing. libakerror's default unhandled-error handler ended in exit(errctx->status), an exit status is one byte wide, and libakgl's band starts at 256 -- so AKGL_ERR_SDL, the most common failure a library built on SDL can have, exited 0 and CTest recorded a pass. tests/character.c aborted at its second of four tests on a bad renderer and was green for months. 0.5.0 worked around that here with TEST_TRAP_UNHANDLED_ERRORS() in tests/testutil.h, and TODO.md ended the entry saying any consumer's suites have the same problem and it was worth raising upstream. It was. 2.0.1 fixes it at the source: akerr_exit() owns the mapping and the default handler calls it, so 0 exits 0, 1 through 255 exit themselves, and anything else exits AKERR_EXIT_STATUS_UNREPRESENTABLE (125). The trap and its 21 call sites are gone. Verified by putting the original failure back rather than by reading the release notes: a FAIL_BREAK(AKGL_ERR_SDL) in tests/character.c's main exits 125 and CTest reports a failure. A standalone consumer raising the same status unhandled exits 125 where it exited 0 before. tests/actor.c installs its own handler and called exit(errctx->status) from it, which is the same defect one layer up. It calls akerr_exit() now. 2.0.0 also makes the error pool and the status registry thread safe, which libakgl needs more than it knew: audio_stream_callback raises error contexts on SDL's audio thread. With an unlocked pool that callback and the main thread could scan AKERR_ARRAY_ERROR at the same time and be handed the same slot. The comment there says so. This is a hard dependency floor, not a preference. 2.0.0 moved __akerr_last_ignored to thread-local storage and made akerr_next_error() return a context that already holds its reference, and both expand at libakgl's call sites -- and at a consumer's, because akerror.h is part of libakgl's public interface. Mixing headers and libraries across that line double-counts every reference and never returns a pool slot. The soname moved to libakerror.so.2; include/akgl/error.h now also feature- tests AKERR_EXIT_STATUS_UNREPRESENTABLE, which is the narrowest probe for 2.0.1 since libakerror publishes no version macro. 0.7.0 for that reason: libakgl's own ABI is unchanged, but the one it re-exports through its headers is not. TODO.md records the pkg-config gap this makes sharper -- akgl.pc names no dependencies at all, so nothing tells a pkg-config consumer which libakerror it needs. Clean build, 26/26 ctest, memcheck clean, warning-clean at -Wall -Werror. libakgl.so.0.7 links libakerror.so.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
676 lines
24 KiB
C
676 lines
24 KiB
C
/**
|
|
* @file draw.c
|
|
* @brief Unit tests for the immediate-mode drawing primitives.
|
|
*
|
|
* Everything here draws into a small software renderer under the dummy video
|
|
* driver and then reads the target back with SDL_RenderReadPixels, so the
|
|
* assertions are about pixels that actually changed rather than about SDL
|
|
* having been called. No window is shown and no display is required.
|
|
*
|
|
* The target is deliberately tiny: at 64x64 a full readback is 16 KB, which
|
|
* makes it cheap to read the whole thing back after every operation.
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <string.h>
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/error.h>
|
|
#include <akgl/game.h>
|
|
#include <akgl/draw.h>
|
|
#include <akgl/renderer.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
/** @brief Width and height of the offscreen target every test draws into. */
|
|
#define TEST_TARGET_SIZE 64
|
|
|
|
/** @brief Opaque black, what each test clears the target to. */
|
|
static const SDL_Color testblack = { 0x00, 0x00, 0x00, 0xff };
|
|
/** @brief The color most tests draw with. */
|
|
static const SDL_Color testred = { 0xff, 0x00, 0x00, 0xff };
|
|
/** @brief A second color, for tests that need to tell two marks apart. */
|
|
static const SDL_Color testgreen = { 0x00, 0xff, 0x00, 0xff };
|
|
|
|
/** @brief Clear the whole target to opaque black. */
|
|
static akerr_ErrorContext *clear_target(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(
|
|
errctx,
|
|
SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff),
|
|
AKGL_ERR_SDL,
|
|
"%s",
|
|
SDL_GetError());
|
|
FAIL_ZERO_RETURN(
|
|
errctx,
|
|
SDL_RenderClear(akgl_renderer->sdl_renderer),
|
|
AKGL_ERR_SDL,
|
|
"%s",
|
|
SDL_GetError());
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Report whether one pixel of @p shot carries @p color.
|
|
*
|
|
* Alpha is not compared: the render target's own alpha depends on the format
|
|
* SDL picked for it, and none of these tests draw translucently.
|
|
*/
|
|
static bool pixel_is(SDL_Surface *shot, int x, int y, SDL_Color color)
|
|
{
|
|
uint8_t r = 0;
|
|
uint8_t g = 0;
|
|
uint8_t b = 0;
|
|
uint8_t a = 0;
|
|
|
|
if ( shot == NULL ) {
|
|
return false;
|
|
}
|
|
if ( !SDL_ReadSurfacePixel(shot, x, y, &r, &g, &b, &a) ) {
|
|
return false;
|
|
}
|
|
return ((r == color.r) && (g == color.g) && (b == color.b));
|
|
}
|
|
|
|
akerr_ErrorContext *test_draw_point(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_Surface *shot = NULL;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, clear_target());
|
|
TEST_EXPECT_OK(errctx, akgl_draw_point(akgl_renderer, 10.0f, 20.0f, testred),
|
|
"plotting one pixel");
|
|
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_ASSERT(errctx, pixel_is(shot, 10, 20, testred),
|
|
"the plotted pixel at 10,20 is not the color it was drawn with");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 11, 20, testblack),
|
|
"plotting one pixel also changed its neighbour at 11,20");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 10, 21, testblack),
|
|
"plotting one pixel also changed the pixel below it");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_point(NULL, 0.0f, 0.0f, testred),
|
|
"plotting through a NULL backend");
|
|
} CLEANUP {
|
|
if ( shot != NULL ) {
|
|
SDL_DestroySurface(shot);
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_draw_line(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_Surface *shot = NULL;
|
|
bool onthe_line = true;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, clear_target());
|
|
// A vertical line, so every pixel of it is known without reasoning
|
|
// about how SDL rasterises a diagonal.
|
|
TEST_EXPECT_OK(errctx, akgl_draw_line(akgl_renderer, 5.0f, 4.0f, 5.0f, 12.0f, testred),
|
|
"drawing a vertical line");
|
|
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
for ( i = 4; i <= 12; i++ ) {
|
|
TEST_ASSERT_FLAG(onthe_line, pixel_is(shot, 5, i, testred));
|
|
}
|
|
TEST_ASSERT(errctx, onthe_line == true,
|
|
"the vertical line from 5,4 to 5,12 has a gap in it");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 5, 3, testblack),
|
|
"the line ran past its first endpoint");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 5, 13, testblack),
|
|
"the line ran past its second endpoint");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 6, 8, testblack),
|
|
"the line is wider than one pixel");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_line(NULL, 0.0f, 0.0f, 1.0f, 1.0f, testred),
|
|
"drawing a line through a NULL backend");
|
|
} CLEANUP {
|
|
if ( shot != NULL ) {
|
|
SDL_DestroySurface(shot);
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_draw_rects(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_Surface *shot = NULL;
|
|
SDL_FRect box;
|
|
|
|
ATTEMPT {
|
|
box.x = 8.0f;
|
|
box.y = 8.0f;
|
|
box.w = 16.0f;
|
|
box.h = 16.0f;
|
|
|
|
// The outline touches the border and leaves the middle alone.
|
|
CATCH(errctx, clear_target());
|
|
TEST_EXPECT_OK(errctx, akgl_draw_rect(akgl_renderer, &box, testred), "outlining a rectangle");
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the outline is missing its top left corner");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 23, 23, testred),
|
|
"the outline is missing its bottom right corner");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 16, 8, testred), "the outline is missing its top edge");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testblack), "the outline filled its interior");
|
|
SDL_DestroySurface(shot);
|
|
shot = NULL;
|
|
|
|
// The filled form covers the interior as well.
|
|
CATCH(errctx, clear_target());
|
|
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred), "filling a rectangle");
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testred), "the fill left its interior empty");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the fill missed its top left corner");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 24, 24, testblack),
|
|
"the fill ran one pixel past its bottom right corner");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(NULL, &box, testred),
|
|
"outlining through a NULL backend");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(akgl_renderer, NULL, testred),
|
|
"outlining a NULL rectangle");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(NULL, &box, testred),
|
|
"filling through a NULL backend");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(akgl_renderer, NULL, testred),
|
|
"filling a NULL rectangle");
|
|
} CLEANUP {
|
|
if ( shot != NULL ) {
|
|
SDL_DestroySurface(shot);
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_draw_circle(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_Surface *shot = NULL;
|
|
bool symmetric = true;
|
|
int x = 0;
|
|
int y = 0;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, clear_target());
|
|
TEST_EXPECT_OK(errctx, akgl_draw_circle(akgl_renderer, 32.0f, 32.0f, 10.0f, testred),
|
|
"drawing a circle");
|
|
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
// The four axis points are exact for any correct midpoint circle.
|
|
TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testred), "the circle is missing its rightmost pixel");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 22, 32, testred), "the circle is missing its leftmost pixel");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 42, testred), "the circle is missing its bottom pixel");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 22, testred), "the circle is missing its top pixel");
|
|
// It is an outline, not a disc.
|
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack), "the circle filled its center");
|
|
// ...and nothing lands outside the radius.
|
|
TEST_ASSERT(errctx, pixel_is(shot, 43, 32, testred) == false,
|
|
"the circle drew a pixel one past its radius");
|
|
|
|
// Every plotted pixel has a mirror in the other three quadrants. The
|
|
// circle is drawn one octant at a time and reflected seven ways, so a
|
|
// sign error in any single reflection breaks this and nothing else --
|
|
// the four axis points above stay put either way.
|
|
for ( y = 22; y <= 42; y++ ) {
|
|
for ( x = 22; x <= 42; x++ ) {
|
|
if ( !pixel_is(shot, x, y, testred) ) {
|
|
continue;
|
|
}
|
|
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, y, testred));
|
|
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, x, 64 - y, testred));
|
|
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, 64 - y, testred));
|
|
}
|
|
}
|
|
TEST_ASSERT(errctx, symmetric == true,
|
|
"the circle is not symmetric about its center; an octant is reflected wrong");
|
|
SDL_DestroySurface(shot);
|
|
shot = NULL;
|
|
|
|
// A zero radius is the degenerate case, not an error: one pixel.
|
|
CATCH(errctx, clear_target());
|
|
TEST_EXPECT_OK(errctx, akgl_draw_circle(akgl_renderer, 5.0f, 5.0f, 0.0f, testred),
|
|
"drawing a circle of radius zero");
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_ASSERT(errctx, pixel_is(shot, 5, 5, testred),
|
|
"a circle of radius zero did not plot its center");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_draw_circle(akgl_renderer, 5.0f, 5.0f, -1.0f, testred),
|
|
"drawing a circle of negative radius");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_circle(NULL, 5.0f, 5.0f, 4.0f, testred),
|
|
"drawing a circle through a NULL backend");
|
|
} CLEANUP {
|
|
if ( shot != NULL ) {
|
|
SDL_DestroySurface(shot);
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_draw_flood_fill(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_Surface *shot = NULL;
|
|
SDL_FRect box;
|
|
|
|
ATTEMPT {
|
|
// A red box outline on black. Filling inside it must stay inside it,
|
|
// which is the whole contract of PAINT.
|
|
box.x = 10.0f;
|
|
box.y = 10.0f;
|
|
box.w = 20.0f;
|
|
box.h = 20.0f;
|
|
CATCH(errctx, clear_target());
|
|
CATCH(errctx, akgl_draw_rect(akgl_renderer, &box, testred));
|
|
|
|
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 20, 20, testgreen),
|
|
"flooding the inside of a box");
|
|
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen), "the seed pixel was not filled");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 11, 11, testgreen),
|
|
"the fill did not reach the top left of the interior");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 28, 28, testgreen),
|
|
"the fill did not reach the bottom right of the interior");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred), "the fill overwrote the boundary");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 20, 10, testred), "the fill overwrote the top edge");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 20, 5, testblack), "the fill leaked outside the box");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack),
|
|
"the fill leaked into the rest of the target");
|
|
SDL_DestroySurface(shot);
|
|
shot = NULL;
|
|
|
|
// Filling a region that is already the requested color changes nothing
|
|
// and is not an error.
|
|
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 20, 20, testgreen),
|
|
"flooding a region that is already that color");
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen),
|
|
"refilling a region disturbed it");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred),
|
|
"refilling a region disturbed its boundary");
|
|
SDL_DestroySurface(shot);
|
|
shot = NULL;
|
|
|
|
// Flooding the outside reaches every pixel that is not the box.
|
|
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 0, 0, testgreen),
|
|
"flooding the area around a box");
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testgreen), "the seed pixel was not filled");
|
|
TEST_ASSERT(errctx, pixel_is(shot, TEST_TARGET_SIZE - 1, TEST_TARGET_SIZE - 1, testgreen),
|
|
"the fill did not reach the far corner of the target");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred),
|
|
"the fill from outside overwrote the boundary");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_draw_flood_fill(akgl_renderer, -1, 0, testred),
|
|
"flooding from a seed left of the target");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_draw_flood_fill(akgl_renderer, 0, TEST_TARGET_SIZE, testred),
|
|
"flooding from a seed below the target");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_flood_fill(NULL, 0, 0, testred),
|
|
"flooding through a NULL backend");
|
|
} CLEANUP {
|
|
if ( shot != NULL ) {
|
|
SDL_DestroySurface(shot);
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_draw_copy_and_paste_region(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_Surface *shot = NULL;
|
|
SDL_Surface *saved = NULL;
|
|
SDL_Surface *reused = NULL;
|
|
SDL_Rect region;
|
|
SDL_FRect box;
|
|
|
|
ATTEMPT {
|
|
// Put something recognisable in the top left corner and save it.
|
|
box.x = 0.0f;
|
|
box.y = 0.0f;
|
|
box.w = 8.0f;
|
|
box.h = 8.0f;
|
|
CATCH(errctx, clear_target());
|
|
CATCH(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred));
|
|
|
|
region.x = 0;
|
|
region.y = 0;
|
|
region.w = 8;
|
|
region.h = 8;
|
|
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(akgl_renderer, ®ion, &saved),
|
|
"saving a region of the target");
|
|
TEST_ASSERT(errctx, saved != NULL, "akgl_draw_copy_region did not allocate a surface");
|
|
TEST_ASSERT(errctx, saved->w == 8 && saved->h == 8,
|
|
"the saved surface is %dx%d, expected 8x8", saved->w, saved->h);
|
|
|
|
// Wipe the screen and put it back somewhere else.
|
|
CATCH(errctx, clear_target());
|
|
TEST_EXPECT_OK(errctx, akgl_draw_paste_region(akgl_renderer, saved, 32.0f, 32.0f),
|
|
"pasting a saved region");
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testred),
|
|
"the pasted region did not land at its destination");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 39, 39, testred),
|
|
"the pasted region is smaller than what was saved");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack),
|
|
"the pasted region is larger than what was saved");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testblack),
|
|
"pasting also redrew the region at its original position");
|
|
SDL_DestroySurface(shot);
|
|
shot = NULL;
|
|
|
|
// A surface the caller already owns is reused rather than replaced, so
|
|
// saving the same region repeatedly does not churn allocations.
|
|
reused = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32);
|
|
FAIL_ZERO_BREAK(errctx, reused, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
region.x = 32;
|
|
region.y = 32;
|
|
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(akgl_renderer, ®ion, &reused),
|
|
"saving into a caller-owned surface");
|
|
TEST_ASSERT(errctx, pixel_is(reused, 0, 0, testred),
|
|
"the caller-owned surface did not receive the region");
|
|
|
|
// Wrong-sized destinations and regions off the edge of the target are
|
|
// refused rather than silently clipped.
|
|
region.w = 4;
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_draw_copy_region(akgl_renderer, ®ion, &reused),
|
|
"saving into a destination of the wrong size");
|
|
region.x = TEST_TARGET_SIZE - 4;
|
|
region.y = 0;
|
|
region.w = 8;
|
|
region.h = 8;
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_draw_copy_region(akgl_renderer, ®ion, &reused),
|
|
"saving a region that runs off the right edge");
|
|
region.x = 0;
|
|
region.w = 0;
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_draw_copy_region(akgl_renderer, ®ion, &reused),
|
|
"saving a region with no area");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_copy_region(NULL, ®ion, &reused),
|
|
"saving through a NULL backend");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_copy_region(akgl_renderer, NULL, &reused),
|
|
"saving a NULL region");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_copy_region(akgl_renderer, ®ion, NULL),
|
|
"saving into a NULL destination");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_paste_region(NULL, saved, 0.0f, 0.0f),
|
|
"pasting through a NULL backend");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_paste_region(akgl_renderer, NULL, 0.0f, 0.0f),
|
|
"pasting a NULL surface");
|
|
} CLEANUP {
|
|
if ( shot != NULL ) {
|
|
SDL_DestroySurface(shot);
|
|
}
|
|
if ( saved != NULL ) {
|
|
SDL_DestroySurface(saved);
|
|
}
|
|
if ( reused != NULL ) {
|
|
SDL_DestroySurface(reused);
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_draw_preserves_render_draw_color(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_FRect box;
|
|
uint8_t r = 0;
|
|
uint8_t g = 0;
|
|
uint8_t b = 0;
|
|
uint8_t a = 0;
|
|
|
|
ATTEMPT {
|
|
box.x = 0.0f;
|
|
box.y = 0.0f;
|
|
box.w = 4.0f;
|
|
box.h = 4.0f;
|
|
|
|
// Drawing must not leave the renderer's color set to whatever it drew
|
|
// with, or the host's next SDL_RenderClear() paints the wrong color.
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x11, 0x22, 0x33, 0x44),
|
|
AKGL_ERR_SDL,
|
|
"%s",
|
|
SDL_GetError());
|
|
CATCH(errctx, akgl_draw_point(akgl_renderer, 1.0f, 1.0f, testred));
|
|
CATCH(errctx, akgl_draw_line(akgl_renderer, 0.0f, 0.0f, 3.0f, 3.0f, testred));
|
|
CATCH(errctx, akgl_draw_rect(akgl_renderer, &box, testred));
|
|
CATCH(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred));
|
|
CATCH(errctx, akgl_draw_circle(akgl_renderer, 20.0f, 20.0f, 4.0f, testred));
|
|
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_GetRenderDrawColor(akgl_renderer->sdl_renderer, &r, &g, &b, &a),
|
|
AKGL_ERR_SDL,
|
|
"%s",
|
|
SDL_GetError());
|
|
TEST_ASSERT(errctx, (r == 0x11) && (g == 0x22) && (b == 0x33) && (a == 0x44),
|
|
"drawing left the render draw color at %02x%02x%02x%02x, expected 11223344",
|
|
r, g, b, a);
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_draw_backend_without_a_renderer(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_RenderBackend empty;
|
|
SDL_Surface *saved = NULL;
|
|
SDL_Surface *scratch = NULL;
|
|
SDL_FRect box;
|
|
SDL_Rect region;
|
|
|
|
ATTEMPT {
|
|
// A backend that exists but was never given an SDL_Renderer. Every
|
|
// entry point has to say so rather than dereference it -- this is the
|
|
// state a host is in between allocating a backend and initializing it.
|
|
memset(&empty, 0x00, sizeof(akgl_RenderBackend));
|
|
box.x = 0.0f;
|
|
box.y = 0.0f;
|
|
box.w = 4.0f;
|
|
box.h = 4.0f;
|
|
region.x = 0;
|
|
region.y = 0;
|
|
region.w = 4;
|
|
region.h = 4;
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_point(&empty, 0.0f, 0.0f, testred),
|
|
"plotting through an uninitialized backend");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_line(&empty, 0.0f, 0.0f, 1.0f, 1.0f, testred),
|
|
"drawing a line through an uninitialized backend");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_rect(&empty, &box, testred),
|
|
"outlining through an uninitialized backend");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_filled_rect(&empty, &box, testred),
|
|
"filling through an uninitialized backend");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_circle(&empty, 4.0f, 4.0f, 2.0f, testred),
|
|
"drawing a circle through an uninitialized backend");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_flood_fill(&empty, 0, 0, testred),
|
|
"flooding through an uninitialized backend");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_copy_region(&empty, ®ion, &saved),
|
|
"saving through an uninitialized backend");
|
|
TEST_ASSERT(errctx, saved == NULL,
|
|
"a refused save still wrote something to the destination");
|
|
|
|
scratch = SDL_CreateSurface(4, 4, SDL_PIXELFORMAT_RGBA32);
|
|
FAIL_ZERO_BREAK(errctx, scratch, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_paste_region(&empty, scratch, 0.0f, 0.0f),
|
|
"pasting through an uninitialized backend");
|
|
} CLEANUP {
|
|
if ( saved != NULL ) {
|
|
SDL_DestroySurface(saved);
|
|
}
|
|
if ( scratch != NULL ) {
|
|
SDL_DestroySurface(scratch);
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief akgl_draw_background paints its checkerboard and restores the draw colour.
|
|
*
|
|
* Until 0.5.0 this function returned `void`, drew through the global renderer
|
|
* with no check on it, and left the draw colour changed. TODO.md listed it
|
|
* under "needs the offscreen renderer harness" purely because of that global;
|
|
* taking a backend is what makes it testable here alongside everything else in
|
|
* this file.
|
|
*/
|
|
akerr_ErrorContext *test_draw_background(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_Surface *shot = NULL;
|
|
SDL_Color after;
|
|
SDL_Color light = { 0x99, 0x99, 0x99, 0xff };
|
|
SDL_Color dark = { 0x66, 0x66, 0x66, 0xff };
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, clear_target());
|
|
// A deliberately odd colour, so "restored" cannot be confused with
|
|
// "happened to already be black".
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x12, 0x34, 0x56, 0x78),
|
|
AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
|
|
TEST_EXPECT_OK(errctx,
|
|
akgl_draw_background(akgl_renderer, TEST_TARGET_SIZE, TEST_TARGET_SIZE),
|
|
"painting the transparency checkerboard");
|
|
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
|
|
// The pattern is 8x8 cells alternating on ((x ^ y) >> 3) & 1, so (0,0)
|
|
// and (8,8) share a colour and (8,0) is the other one.
|
|
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, dark), "cell (0,0) is not the first checker colour");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 7, 7, dark), "cell (0,0) is not filled to its edge");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 8, 0, light), "cell (1,0) is not the second checker colour");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 0, 8, light), "cell (0,1) is not the second checker colour");
|
|
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, dark), "cell (1,1) is not the first checker colour");
|
|
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_GetRenderDrawColor(akgl_renderer->sdl_renderer, &after.r, &after.g, &after.b, &after.a),
|
|
AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_ASSERT(errctx,
|
|
(after.r == 0x12) && (after.g == 0x34) && (after.b == 0x56) && (after.a == 0x78),
|
|
"the draw colour was left at %02x%02x%02x%02x rather than restored",
|
|
after.r, after.g, after.b, after.a);
|
|
|
|
// Degenerate sizes paint nothing and are not an error.
|
|
CATCH(errctx, clear_target());
|
|
TEST_EXPECT_OK(errctx, akgl_draw_background(akgl_renderer, 0, 0),
|
|
"painting a zero-sized background");
|
|
TEST_EXPECT_OK(errctx, akgl_draw_background(akgl_renderer, -8, -8),
|
|
"painting a negative-sized background");
|
|
SDL_DestroySurface(shot);
|
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testblack),
|
|
"a zero or negative sized background painted something");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_draw_background(NULL, 8, 8),
|
|
"painting a background through a NULL backend");
|
|
} CLEANUP {
|
|
if ( shot != NULL ) {
|
|
SDL_DestroySurface(shot);
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
akgl_renderer = &akgl_default_renderer;
|
|
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_Init(SDL_INIT_VIDEO),
|
|
AKGL_ERR_SDL,
|
|
"Couldn't initialize SDL: %s",
|
|
SDL_GetError());
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_CreateWindowAndRenderer(
|
|
"net/aklabs/libakgl/test_draw",
|
|
TEST_TARGET_SIZE,
|
|
TEST_TARGET_SIZE,
|
|
0,
|
|
&akgl_window,
|
|
&akgl_renderer->sdl_renderer),
|
|
AKGL_ERR_SDL,
|
|
"Couldn't create window/renderer: %s",
|
|
SDL_GetError());
|
|
|
|
CATCH(errctx, test_draw_point());
|
|
CATCH(errctx, test_draw_line());
|
|
CATCH(errctx, test_draw_rects());
|
|
CATCH(errctx, test_draw_circle());
|
|
CATCH(errctx, test_draw_flood_fill());
|
|
CATCH(errctx, test_draw_copy_and_paste_region());
|
|
CATCH(errctx, test_draw_preserves_render_draw_color());
|
|
CATCH(errctx, test_draw_backend_without_a_renderer());
|
|
CATCH(errctx, test_draw_background());
|
|
} CLEANUP {
|
|
SDL_Quit();
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
}
|