diff --git a/CMakeLists.txt b/CMakeLists.txt index 930ecbb..8a784f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -157,6 +157,7 @@ set(AKGL_TEST_SUITES bitmasks character controller + draw error game heap diff --git a/TODO.md b/TODO.md index 0eee623..ac6f199 100644 --- a/TODO.md +++ b/TODO.md @@ -683,6 +683,35 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha harness described under "Remaining work": render a known shape, read the target back, and compare against a reference surface with the existing `akgl_compare_sdl_surfaces`. + **Resolved.** `include/akgl/draw.h` now declares `akgl_draw_point`, `_line`, + `_rect`, `_filled_rect`, `_circle`, `_flood_fill`, `_copy_region` and + `_paste_region`, all taking the `akgl_RenderBackend *` the host initialized, + in the shape of `akgl_render_2d_draw_texture`. Decisions worth knowing: + + - **Color is an argument, not state.** There is no current-color global to + get out of step with the caller's own. Each call saves and restores the + renderer's draw color, so drawing a line does not change what the host's + next `SDL_RenderClear` paints. `tests/draw.c` asserts that. + - **The circle is a midpoint circle**, integer arithmetic with eight-way + symmetry, plotted eight points per step through `SDL_RenderPoints`. + - **The flood fill reads the target back, fills on the CPU, and blits only + the bounding box of what changed.** It keeps a fixed + `AKGL_DRAW_MAX_FLOOD_SPANS` (4096) stack of horizontal runs at file scope + rather than recursing per pixel; running out reports `AKERR_OUTOFBOUNDS` + and leaves the region partially filled, which is stated in the header. It + is therefore not reentrant — neither is anything else that draws to a + single `SDL_Renderer`. + - `_copy_region` allocates when `*dest` is `NULL` and otherwise copies into + the caller's surface, matching `akgl_get_json_string_value` and friends. A + region that would be clipped by the target edge is refused rather than + silently returning a smaller surface. + + `tests/draw.c` draws into a 64x64 software renderer under the dummy video + driver and reads pixels back, so it did not need the offscreen harness. That + harness is still wanted for `src/renderer.c`, `src/text.c` and `src/assets.c`. + `akgl_draw_background` is untouched and still outside the error protocol + (item 35). + 3. **No audio API at all.** `SDL3_mixer` is a vendored dependency and `registry.h` declares `AKGL_REGISTRY_MUSIC`, but there is no `src/audio.c`, no `include/akgl/audio.h`, and no `akgl_*` symbol that opens a mixer, loads a chunk, or plays a note. BASIC 7.0's sound verbs diff --git a/include/akgl/draw.h b/include/akgl/draw.h index 71d01f4..37454dc 100644 --- a/include/akgl/draw.h +++ b/include/akgl/draw.h @@ -1,11 +1,41 @@ /** * @file draw.h * @brief Declares the public draw API. + * + * Immediate-mode plotting against whichever renderer the caller hands in. This + * is the shape a BASIC-style graphics vocabulary needs -- DRAW, BOX, CIRCLE, + * PAINT, SSHAPE and GSHAPE all say "put this on the screen now" rather than + * "add this to the scene" -- and it sits alongside the actor and tilemap + * rendering rather than replacing it. + * + * Every entry point takes its color as an argument instead of reading a + * current-color global. A caller that has a notion of a current color (a BASIC + * COLOR statement, say) already owns that state and does not need the library + * to keep a second copy that can disagree with it. The renderer's own draw + * color is saved and restored around each call, so drawing a line never changes + * what the next SDL_RenderClear() paints. */ #ifndef _DRAW_H_ #define _DRAW_H_ +#include + +#include + +#include +#include + +/** + * @brief Spans akgl_draw_flood_fill() will hold while walking a region. + * + * The fill keeps a fixed stack of horizontal runs still to be examined rather + * than recursing per pixel. A region complicated enough to need more than this + * many pending runs at once reports AKERR_OUTOFBOUNDS instead of overflowing; + * an ordinary convex or moderately concave shape needs a few dozen. + */ +#define AKGL_DRAW_MAX_FLOOD_SPANS 4096 + /** * @brief Draw background. * @param w Destination width. @@ -13,4 +43,129 @@ */ void akgl_draw_background(int w, int h); +/** + * @brief Plot a single pixel. + * @param self Backend or object instance to operate on. + * @param x Horizontal destination coordinate. + * @param y Vertical destination coordinate. + * @param color Color to draw with. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color); + +/** + * @brief Draw a line between two points. + * @param self Backend or object instance to operate on. + * @param x1 Horizontal coordinate of the first endpoint. + * @param y1 Vertical coordinate of the first endpoint. + * @param x2 Horizontal coordinate of the second endpoint. + * @param y2 Vertical coordinate of the second endpoint. + * @param color Color to draw with. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color); + +/** + * @brief Draw the outline of a rectangle. + * @param self Backend or object instance to operate on. + * @param rect Rectangle to outline. + * @param color Color to draw with. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color); + +/** + * @brief Fill a rectangle. + * @param self Backend or object instance to operate on. + * @param rect Rectangle to fill. + * @param color Color to draw with. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color); + +/** + * @brief Draw the outline of a circle. + * + * SDL3 has no circle primitive, so this plots one with the midpoint circle + * algorithm -- integer arithmetic, eight-way symmetry, one pass per octant. A + * radius of zero draws the center pixel and nothing else. + * + * @param self Backend or object instance to operate on. + * @param x Horizontal coordinate of the center. + * @param y Vertical coordinate of the center. + * @param radius Circle radius in pixels. + * @param color Color to draw with. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color); + +/** + * @brief Flood the connected region containing one pixel with a color. + * + * SDL3 has no flood fill either, and unlike the shape primitives it cannot be + * done on the GPU side: the region is defined by what is already on the screen. + * This reads the render target back, walks the region on the CPU with a + * bounded span stack, and blits the result over the area it touched. + * + * Filling a region that already holds @p color is a no-op rather than an error. + * A seed outside the render target reports AKERR_OUTOFBOUNDS. + * + * @param self Backend or object instance to operate on. + * @param x Horizontal coordinate of the seed pixel. + * @param y Vertical coordinate of the seed pixel. + * @param color Color to fill with. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color); + +/** + * @brief Read a rectangle of the render target into a surface. + * + * The save half of SSHAPE/GSHAPE. When `*dest` is `NULL` the function allocates + * the surface and the caller owns it from then on -- release it with + * SDL_DestroySurface(). When `*dest` already points at a surface of exactly + * @p src's dimensions the pixels are copied into it instead, so a caller + * saving the same region repeatedly does not churn allocations. + * + * @param self Backend or object instance to operate on. + * @param src Rectangle of the render target to read. + * @param dest Output destination populated by the function. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest); + +/** + * @brief Draw a saved surface back onto the render target. + * + * The restore half of SSHAPE/GSHAPE, taking what akgl_draw_copy_region() + * produced. The surface is not consumed and may be pasted as many times as the + * caller likes. + * + * @param self Backend or object instance to operate on. + * @param src Surface to draw. + * @param x Horizontal destination coordinate. + * @param y Vertical destination coordinate. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y); + #endif //_DRAW_H_ diff --git a/src/draw.c b/src/draw.c index 52e6820..95a4527 100644 --- a/src/draw.c +++ b/src/draw.c @@ -6,8 +6,28 @@ #include #include #include +#include +#include +#include #include +/** @brief One horizontal run of pixels the flood fill has still to examine. */ +typedef struct { + int x1; + int x2; + int y; +} FloodSpan; + +/* + * The flood fill's working stack. File scope and fixed size rather than a local + * array because AKGL_DRAW_MAX_FLOOD_SPANS spans is 48 KB, which does not belong + * on the stack of a function a game may call every frame. The consequence is + * that akgl_draw_flood_fill is not reentrant -- it is a single-threaded + * immediate-mode operation on a single render target, and so is everything else + * that touches an SDL_Renderer. + */ +static FloodSpan floodspans[AKGL_DRAW_MAX_FLOOD_SPANS]; + /* Draw a Gimpish background pattern to show transparency in the image */ void akgl_draw_background(int w, int h) { @@ -33,3 +53,497 @@ void akgl_draw_background(int w, int h) } } } + +/** + * @brief Remember the renderer's draw color and replace it with @p color. + * + * @p previous is written before anything that can fail, so a caller may restore + * it unconditionally from a CLEANUP block. + */ +static akerr_ErrorContext *push_draw_color(akgl_RenderBackend *self, SDL_Color color, SDL_Color *previous) +{ + PREPARE_ERROR(errctx); + previous->r = 0x00; + previous->g = 0x00; + previous->b = 0x00; + previous->a = SDL_ALPHA_OPAQUE; + FAIL_ZERO_RETURN( + errctx, + SDL_GetRenderDrawColor(self->sdl_renderer, &previous->r, &previous->g, &previous->b, &previous->a), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + FAIL_ZERO_RETURN( + errctx, + SDL_SetRenderDrawColor(self->sdl_renderer, color.r, color.g, color.b, color.a), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + SUCCEED_RETURN(errctx); +} + +/** @brief Put back the draw color push_draw_color() recorded. */ +static akerr_ErrorContext *pop_draw_color(akgl_RenderBackend *self, SDL_Color *previous) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN( + errctx, + SDL_SetRenderDrawColor(self->sdl_renderer, previous->r, previous->g, previous->b, previous->a), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + SUCCEED_RETURN(errctx); +} + +/** + * @brief Fill the four-connected region of @p oldpixel around a seed pixel. + * + * A scanline fill: each entry on the stack is a run of pixels on one row that + * still has to be examined. Finding a matching pixel expands it to the whole + * run it belongs to, fills that run, and pushes the rows above and below. + * Filled pixels no longer match @p oldpixel, which is what terminates it. + * + * @p surface must be SDL_PIXELFORMAT_RGBA32; the fill compares and writes whole + * 32-bit words rather than going through SDL_ReadSurfacePixel per pixel. + * + * @p dirty is set to the bounding box of everything written, so the caller can + * put back only the pixels that changed. + * + * Running out of stack leaves the region partially filled and reports + * AKERR_OUTOFBOUNDS. There is no way to unwind a partial fill short of keeping + * a copy of the whole surface, and the caller asked for a bounded operation. + */ +static akerr_ErrorContext *flood_region(SDL_Surface *surface, int x, int y, uint32_t oldpixel, uint32_t newpixel, SDL_Rect *dirty) +{ + uint32_t *pixels = (uint32_t *)surface->pixels; + int pitch = surface->pitch / (int)sizeof(uint32_t); + int count = 0; + int col = 0; + int left = 0; + int right = 0; + int i = 0; + int minx = surface->w; + int miny = surface->h; + int maxx = -1; + int maxy = -1; + FloodSpan span; + + PREPARE_ERROR(errctx); + + floodspans[0].x1 = x; + floodspans[0].x2 = x; + floodspans[0].y = y; + count = 1; + + while ( count > 0 ) { + count -= 1; + span = floodspans[count]; + col = span.x1; + while ( col <= span.x2 ) { + if ( pixels[(span.y * pitch) + col] != oldpixel ) { + col += 1; + continue; + } + left = col; + while ( left > 0 && pixels[(span.y * pitch) + (left - 1)] == oldpixel ) { + left -= 1; + } + right = col; + while ( right < (surface->w - 1) && pixels[(span.y * pitch) + (right + 1)] == oldpixel ) { + right += 1; + } + for ( i = left; i <= right; i++ ) { + pixels[(span.y * pitch) + i] = newpixel; + } + if ( left < minx ) { + minx = left; + } + if ( right > maxx ) { + maxx = right; + } + if ( span.y < miny ) { + miny = span.y; + } + if ( span.y > maxy ) { + maxy = span.y; + } + // Two pushes per run, so the check is for room for both. + FAIL_NONZERO_RETURN( + errctx, + ((count + 2) > AKGL_DRAW_MAX_FLOOD_SPANS), + AKERR_OUTOFBOUNDS, + "Region needs more than %d pending spans; it is partially filled", + AKGL_DRAW_MAX_FLOOD_SPANS); + if ( span.y > 0 ) { + floodspans[count].x1 = left; + floodspans[count].x2 = right; + floodspans[count].y = span.y - 1; + count += 1; + } + if ( span.y < (surface->h - 1) ) { + floodspans[count].x1 = left; + floodspans[count].x2 = right; + floodspans[count].y = span.y + 1; + count += 1; + } + col = right + 1; + } + } + + dirty->x = minx; + dirty->y = miny; + dirty->w = (maxx - minx) + 1; + dirty->h = (maxy - miny) + 1; + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color) +{ + SDL_Color previous; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); + FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); + ATTEMPT { + CATCH(errctx, push_draw_color(self, color, &previous)); + FAIL_ZERO_BREAK( + errctx, + SDL_RenderPoint(self->sdl_renderer, x, y), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + } CLEANUP { + IGNORE(pop_draw_color(self, &previous)); + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color) +{ + SDL_Color previous; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); + FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); + ATTEMPT { + CATCH(errctx, push_draw_color(self, color, &previous)); + FAIL_ZERO_BREAK( + errctx, + SDL_RenderLine(self->sdl_renderer, x1, y1, x2, y2), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + } CLEANUP { + IGNORE(pop_draw_color(self, &previous)); + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color) +{ + SDL_Color previous; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); + FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); + FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect"); + ATTEMPT { + CATCH(errctx, push_draw_color(self, color, &previous)); + FAIL_ZERO_BREAK( + errctx, + SDL_RenderRect(self->sdl_renderer, rect), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + } CLEANUP { + IGNORE(pop_draw_color(self, &previous)); + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color) +{ + SDL_Color previous; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); + FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); + FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect"); + ATTEMPT { + CATCH(errctx, push_draw_color(self, color, &previous)); + FAIL_ZERO_BREAK( + errctx, + SDL_RenderFillRect(self->sdl_renderer, rect), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + } CLEANUP { + IGNORE(pop_draw_color(self, &previous)); + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color) +{ + SDL_Color previous; + SDL_FPoint octants[8]; + int centerx = 0; + int centery = 0; + int r = 0; + int offsetx = 0; + int offsety = 0; + int decision = 0; + bool plotted = true; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); + FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); + FAIL_NONZERO_RETURN(errctx, (radius < 0), AKERR_OUTOFBOUNDS, "Negative radius %f", radius); + + centerx = (int)SDL_lroundf(x); + centery = (int)SDL_lroundf(y); + r = (int)SDL_lroundf(radius); + offsety = r; + // The midpoint decision variable, started so the first step chooses between + // (0, r) and (1, r-1) correctly. + decision = 1 - r; + + ATTEMPT { + CATCH(errctx, push_draw_color(self, color, &previous)); + while ( offsety >= offsetx ) { + // Eight-way symmetry: one computed point in the second octant gives + // the seven others by reflection. + octants[0].x = (float)(centerx + offsetx); + octants[0].y = (float)(centery + offsety); + octants[1].x = (float)(centerx - offsetx); + octants[1].y = (float)(centery + offsety); + octants[2].x = (float)(centerx + offsetx); + octants[2].y = (float)(centery - offsety); + octants[3].x = (float)(centerx - offsetx); + octants[3].y = (float)(centery - offsety); + octants[4].x = (float)(centerx + offsety); + octants[4].y = (float)(centery + offsetx); + octants[5].x = (float)(centerx - offsety); + octants[5].y = (float)(centery + offsetx); + octants[6].x = (float)(centerx + offsety); + octants[6].y = (float)(centery - offsetx); + octants[7].x = (float)(centerx - offsety); + octants[7].y = (float)(centery - offsetx); + // A CATCH here would break this loop rather than leave the function, + // so failure is recorded and reported once the loop is done. + if ( !SDL_RenderPoints(self->sdl_renderer, octants, 8) ) { + plotted = false; + } + offsetx += 1; + if ( decision < 0 ) { + decision += (2 * offsetx) + 1; + } else { + offsety -= 1; + decision += 2 * (offsetx - offsety) + 1; + } + } + FAIL_ZERO_BREAK(errctx, plotted, AKGL_ERR_SDL, "%s", SDL_GetError()); + } CLEANUP { + IGNORE(pop_draw_color(self, &previous)); + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color) +{ + SDL_Surface *target = NULL; + SDL_Surface *rgba = NULL; + SDL_Texture *patch = NULL; + SDL_Rect dirty; + SDL_FRect src; + SDL_FRect dest; + uint32_t *pixels = NULL; + uint32_t oldpixel = 0; + uint32_t newpixel = 0; + int width = 0; + int height = 0; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); + FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); + ATTEMPT { + FAIL_ZERO_BREAK( + errctx, + SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + FAIL_NONZERO_BREAK( + errctx, + ((x < 0) || (y < 0) || (x >= width) || (y >= height)), + AKERR_OUTOFBOUNDS, + "Seed pixel %d,%d is outside the %dx%d render target", + x, y, width, height); + + target = SDL_RenderReadPixels(self->sdl_renderer, NULL); + FAIL_ZERO_BREAK(errctx, target, AKGL_ERR_SDL, "%s", SDL_GetError()); + // The fill works on 32-bit words, so the layout has to be known rather + // than whatever the render target happens to use. + rgba = SDL_ConvertSurface(target, SDL_PIXELFORMAT_RGBA32); + FAIL_ZERO_BREAK(errctx, rgba, AKGL_ERR_SDL, "%s", SDL_GetError()); + + pixels = (uint32_t *)rgba->pixels; + oldpixel = pixels[(y * (rgba->pitch / (int)sizeof(uint32_t))) + x]; + newpixel = SDL_MapSurfaceRGBA(rgba, color.r, color.g, color.b, color.a); + if ( oldpixel == newpixel ) { + // Already the requested color. Walking it would compare filled + // pixels against themselves and find nothing, so say so up front. + SUCCEED_BREAK(errctx); + } + CATCH(errctx, flood_region(rgba, x, y, oldpixel, newpixel, &dirty)); + + patch = SDL_CreateTextureFromSurface(self->sdl_renderer, rgba); + FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError()); + // Replace rather than blend: this is a framebuffer operation, and the + // pixels being put back are the ones that were just read out of it. + FAIL_ZERO_BREAK( + errctx, + SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + + // Only the bounding box of what changed goes back to the target. + src.x = (float)dirty.x; + src.y = (float)dirty.y; + src.w = (float)dirty.w; + src.h = (float)dirty.h; + dest = src; + FAIL_ZERO_BREAK( + errctx, + SDL_RenderTexture(self->sdl_renderer, patch, &src, &dest), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + } CLEANUP { + if ( patch != NULL ) { + SDL_DestroyTexture(patch); + } + if ( rgba != NULL ) { + SDL_DestroySurface(rgba); + } + if ( target != NULL ) { + SDL_DestroySurface(target); + } + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest) +{ + SDL_Surface *saved = NULL; + int width = 0; + int height = 0; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); + FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); + FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src"); + FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest"); + ATTEMPT { + FAIL_NONZERO_BREAK( + errctx, + ((src->w <= 0) || (src->h <= 0)), + AKERR_OUTOFBOUNDS, + "Region %dx%d has no area", + src->w, src->h); + FAIL_ZERO_BREAK( + errctx, + SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + // SDL clips a read to the target and hands back a smaller surface than + // was asked for, which a caller pasting it back would not notice. + FAIL_NONZERO_BREAK( + errctx, + ((src->x < 0) || (src->y < 0) || + ((src->x + src->w) > width) || ((src->y + src->h) > height)), + AKERR_OUTOFBOUNDS, + "Region %d,%d %dx%d does not fit inside the %dx%d render target", + src->x, src->y, src->w, src->h, width, height); + + saved = SDL_RenderReadPixels(self->sdl_renderer, src); + FAIL_ZERO_BREAK(errctx, saved, AKGL_ERR_SDL, "%s", SDL_GetError()); + + if ( *dest == NULL ) { + *dest = saved; + // Ownership has moved to the caller; CLEANUP must not free it. + saved = NULL; + } else { + FAIL_NONZERO_BREAK( + errctx, + (((*dest)->w != src->w) || ((*dest)->h != src->h)), + AKERR_OUTOFBOUNDS, + "Destination surface is %dx%d, region is %dx%d", + (*dest)->w, (*dest)->h, src->w, src->h); + FAIL_ZERO_BREAK( + errctx, + SDL_SetSurfaceBlendMode(saved, SDL_BLENDMODE_NONE), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + FAIL_ZERO_BREAK( + errctx, + SDL_BlitSurface(saved, NULL, *dest, NULL), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + } + } CLEANUP { + if ( saved != NULL ) { + SDL_DestroySurface(saved); + } + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y) +{ + SDL_Texture *patch = NULL; + SDL_FRect dest; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); + FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); + FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src"); + ATTEMPT { + patch = SDL_CreateTextureFromSurface(self->sdl_renderer, src); + FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError()); + // Replace what is on the target, the way GSHAPE does by default. + FAIL_ZERO_BREAK( + errctx, + SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + dest.x = x; + dest.y = y; + dest.w = (float)src->w; + dest.h = (float)src->h; + FAIL_ZERO_BREAK( + errctx, + SDL_RenderTexture(self->sdl_renderer, patch, NULL, &dest), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + } CLEANUP { + if ( patch != NULL ) { + SDL_DestroyTexture(patch); + } + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} diff --git a/tests/draw.c b/tests/draw.c new file mode 100644 index 0000000..5ea0b8f --- /dev/null +++ b/tests/draw.c @@ -0,0 +1,515 @@ +/** + * @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 +#include +#include + +#include +#include +#include +#include + +#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(renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + FAIL_ZERO_RETURN( + errctx, + SDL_RenderClear(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(renderer, 10.0f, 20.0f, testred), + "plotting one pixel"); + + shot = SDL_RenderReadPixels(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(renderer, 5.0f, 4.0f, 5.0f, 12.0f, testred), + "drawing a vertical line"); + + shot = SDL_RenderReadPixels(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(renderer, &box, testred), "outlining a rectangle"); + shot = SDL_RenderReadPixels(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(renderer, &box, testred), "filling a rectangle"); + shot = SDL_RenderReadPixels(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(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(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; + + ATTEMPT { + CATCH(errctx, clear_target()); + TEST_EXPECT_OK(errctx, akgl_draw_circle(renderer, 32.0f, 32.0f, 10.0f, testred), + "drawing a circle"); + + shot = SDL_RenderReadPixels(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"); + 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(renderer, 5.0f, 5.0f, 0.0f, testred), + "drawing a circle of radius zero"); + shot = SDL_RenderReadPixels(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(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(renderer, &box, testred)); + + TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 20, 20, testgreen), + "flooding the inside of a box"); + + shot = SDL_RenderReadPixels(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(renderer, 20, 20, testgreen), + "flooding a region that is already that color"); + shot = SDL_RenderReadPixels(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(renderer, 0, 0, testgreen), + "flooding the area around a box"); + shot = SDL_RenderReadPixels(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(renderer, -1, 0, testred), + "flooding from a seed left of the target"); + TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, + akgl_draw_flood_fill(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(renderer, &box, testred)); + + region.x = 0; + region.y = 0; + region.w = 8; + region.h = 8; + TEST_EXPECT_OK(errctx, akgl_draw_copy_region(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(renderer, saved, 32.0f, 32.0f), + "pasting a saved region"); + shot = SDL_RenderReadPixels(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(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(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(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(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(renderer, NULL, &reused), + "saving a NULL region"); + TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, + akgl_draw_copy_region(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(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(renderer->sdl_renderer, 0x11, 0x22, 0x33, 0x44), + AKGL_ERR_SDL, + "%s", + SDL_GetError()); + CATCH(errctx, akgl_draw_point(renderer, 1.0f, 1.0f, testred)); + CATCH(errctx, akgl_draw_line(renderer, 0.0f, 0.0f, 3.0f, 3.0f, testred)); + CATCH(errctx, akgl_draw_rect(renderer, &box, testred)); + CATCH(errctx, akgl_draw_filled_rect(renderer, &box, testred)); + CATCH(errctx, akgl_draw_circle(renderer, 20.0f, 20.0f, 4.0f, testred)); + + FAIL_ZERO_BREAK( + errctx, + SDL_GetRenderDrawColor(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); +} + +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()); + renderer = &_akgl_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, + &window, + &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()); + } CLEANUP { + SDL_Quit(); + } PROCESS(errctx) { + } FINISH_NORETURN(errctx); +}