Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
591 lines
19 KiB
C
591 lines
19 KiB
C
/**
|
|
* @file draw.c
|
|
* @brief Implements the draw subsystem.
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_image/SDL_image.h>
|
|
#include <SDL3_mixer/SDL_mixer.h>
|
|
#include <akerror.h>
|
|
#include <akgl/draw.h>
|
|
#include <akgl/error.h>
|
|
#include <akgl/game.h>
|
|
|
|
/** @brief One horizontal run of pixels the flood fill has still to examine. */
|
|
typedef struct {
|
|
int x1; /**< Leftmost column of the run, inclusive. */
|
|
int x2; /**< Rightmost column of the run, inclusive. */
|
|
int y; /**< The row the run is on. */
|
|
} 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)
|
|
{
|
|
SDL_Color col[2] = {
|
|
{ 0x66, 0x66, 0x66, 0xff },
|
|
{ 0x99, 0x99, 0x99, 0xff },
|
|
};
|
|
int i, x, y;
|
|
SDL_FRect rect;
|
|
const int dx = 8, dy = 8;
|
|
|
|
rect.w = (float)dx;
|
|
rect.h = (float)dy;
|
|
for (y = 0; y < h; y += dy) {
|
|
for (x = 0; x < w; x += dx) {
|
|
/* use an 8x8 checkerboard pattern */
|
|
i = (((x ^ y) >> 3) & 1);
|
|
SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
|
|
|
|
rect.x = (float)x;
|
|
rect.y = (float)y;
|
|
SDL_RenderFillRect(akgl_renderer->sdl_renderer, &rect);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
*
|
|
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`;
|
|
* every caller has already checked both.
|
|
* @param color The colour to install.
|
|
* @param previous Receives the colour that was in place. Assumed non-`NULL`.
|
|
* Pre-filled with opaque black, so it is safe to restore from
|
|
* even if the query below fails.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKGL_ERR_SDL If the current draw colour cannot be read or the new one
|
|
* cannot be set. The message carries `SDL_GetError()`.
|
|
*/
|
|
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.
|
|
*
|
|
* Called from `CLEANUP` blocks under `IGNORE()`, so its error is logged rather
|
|
* than propagated -- failing to restore a colour must not mask the failure the
|
|
* cleanup is unwinding from.
|
|
*
|
|
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`.
|
|
* @param previous The colour push_draw_color() recorded. Assumed non-`NULL`.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKGL_ERR_SDL If the draw colour cannot be set.
|
|
*/
|
|
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.
|
|
*
|
|
* @param surface The pixels to fill, in SDL_PIXELFORMAT_RGBA32. Assumed
|
|
* non-`NULL` and locked-or-lockless; the caller converts.
|
|
* @param x Seed column. Assumed inside the surface -- akgl_draw_flood_fill
|
|
* has already range-checked it.
|
|
* @param y Seed row, likewise.
|
|
* @param oldpixel The packed pixel value the region is made of. Everything else
|
|
* is a boundary.
|
|
* @param newpixel The packed pixel value to write. Must differ from @p oldpixel:
|
|
* if they are equal the fill never terminates making progress,
|
|
* which is why the caller checks that case first.
|
|
* @param dirty Receives the bounding box of everything written, so the caller
|
|
* can put back only the pixels that changed. Assumed non-`NULL`.
|
|
* Left describing an empty box if nothing matched.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_OUTOFBOUNDS If the region needs more than
|
|
* #AKGL_DRAW_MAX_FLOOD_SPANS pending spans. @p dirty is then not written
|
|
* and the surface is partially filled.
|
|
*/
|
|
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 *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 *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 *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 *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 *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 *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;
|
|
// Only written by a successful flood_region(), and only read after one, but
|
|
// the paths in between are far enough apart that the compiler cannot see it.
|
|
SDL_Rect dirty = { 0, 0, 0, 0 };
|
|
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 *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 *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);
|
|
}
|