/** * @file draw.c * @brief Implements the draw subsystem. */ #include #include #include #include #include #include #include /** @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]; /** * @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); } /* Draw a Gimpish background pattern to show transparency in the image */ akerr_ErrorContext *akgl_draw_background(akgl_RenderBackend *self, int w, int h) { PREPARE_ERROR(errctx); SDL_Color col[2] = { { 0x66, 0x66, 0x66, 0xff }, { 0x99, 0x99, 0x99, 0xff }, }; SDL_Color previous; SDL_FRect rect; const int dx = 8, dy = 8; bool pushed = false; bool drawfailed = false; int i = 0; int x = 0; int y = 0; FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); rect.w = (float)dx; rect.h = (float)dy; ATTEMPT { CATCH(errctx, push_draw_color(self, col[0], &previous)); pushed = true; // The two SDL calls are checked through a flag rather than with // FAIL_ZERO_BREAK. Inside these loops a `break` would leave the loop, // not the ATTEMPT block, and the fill would carry on with the failure // unnoticed -- the hazard AGENTS.md describes for CATCH inside a loop. for ( y = 0; (y < h) && (drawfailed == false); y += dy ) { for ( x = 0; x < w; x += dx ) { /* use an 8x8 checkerboard pattern */ i = (((x ^ y) >> 3) & 1); if ( SDL_SetRenderDrawColor(self->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a) == false ) { drawfailed = true; break; } rect.x = (float)x; rect.y = (float)y; if ( SDL_RenderFillRect(self->sdl_renderer, &rect) == false ) { drawfailed = true; break; } } } FAIL_NONZERO_BREAK(errctx, drawfailed, AKGL_ERR_SDL, "%s", SDL_GetError()); } CLEANUP { if ( pushed == true ) { IGNORE(pop_draw_color(self, &previous)); } } PROCESS(errctx) { } FINISH(errctx, true); 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); } /** * @brief Fill one quarter-circle corner as a triangle fan. * * `SDL_RenderGeometry` colours from its vertices rather than the renderer's * draw colour, so this neither pushes nor pops it. * * @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`; * the caller has already checked both. * @param cx Horizontal position of the corner's centre -- the point the * fan radiates from, one radius inside the rectangle. * @param cy Vertical position of the centre. * @param radius Radius of the quarter circle. Assumed positive. * @param start_deg Angle the quarter starts at; it sweeps 90 degrees clockwise * from there. * @param color Colour to fill with. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKGL_ERR_SDL If the fan cannot be drawn. */ static akerr_ErrorContext *fill_corner_fan(akgl_RenderBackend *self, float32_t cx, float32_t cy, float32_t radius, float32_t start_deg, SDL_Color color) { SDL_Vertex vertices[AKGL_DRAW_ROUNDED_RECT_SEGMENTS + 2]; int indices[AKGL_DRAW_ROUNDED_RECT_SEGMENTS * 3]; SDL_FColor fcolor; float32_t angle = 0.0f; int i = 0; PREPARE_ERROR(errctx); fcolor.r = (float)color.r / 255.0f; fcolor.g = (float)color.g / 255.0f; fcolor.b = (float)color.b / 255.0f; fcolor.a = (float)color.a / 255.0f; vertices[0].position.x = cx; vertices[0].position.y = cy; vertices[0].color = fcolor; vertices[0].tex_coord.x = 0.0f; vertices[0].tex_coord.y = 0.0f; for ( i = 0; i <= AKGL_DRAW_ROUNDED_RECT_SEGMENTS; i++ ) { angle = (start_deg + ((90.0f / AKGL_DRAW_ROUNDED_RECT_SEGMENTS) * (float)i)) * (SDL_PI_F / 180.0f); vertices[i + 1].position.x = cx + (radius * SDL_cosf(angle)); vertices[i + 1].position.y = cy + (radius * SDL_sinf(angle)); vertices[i + 1].color = fcolor; vertices[i + 1].tex_coord.x = 0.0f; vertices[i + 1].tex_coord.y = 0.0f; } for ( i = 0; i < AKGL_DRAW_ROUNDED_RECT_SEGMENTS; i++ ) { indices[(i * 3) + 0] = 0; indices[(i * 3) + 1] = i + 1; indices[(i * 3) + 2] = i + 2; } FAIL_ZERO_RETURN( errctx, SDL_RenderGeometry( self->sdl_renderer, NULL, vertices, AKGL_DRAW_ROUNDED_RECT_SEGMENTS + 2, indices, AKGL_DRAW_ROUNDED_RECT_SEGMENTS * 3), AKGL_ERR_SDL, "%s", SDL_GetError()); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_draw_filled_rounded_rect(akgl_RenderBackend *self, SDL_FRect *rect, float32_t radius, SDL_Color color) { SDL_Color previous; SDL_FRect band; float32_t half = 0.0f; bool pushed = false; bool drawfailed = false; 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, "NULL rectangle"); if ( (rect->w <= 0.0f) || (rect->h <= 0.0f) ) { SUCCEED_RETURN(errctx); } if ( radius <= 0.0f ) { PASS(errctx, akgl_draw_filled_rect(self, rect, color)); SUCCEED_RETURN(errctx); } half = (((rect->w < rect->h) ? rect->w : rect->h) / 2.0f); if ( radius > half ) { radius = half; } ATTEMPT { CATCH(errctx, push_draw_color(self, color, &previous)); pushed = true; // The body is three bands: one the full width between the corner rows, // and one between the corners along each of the top and bottom edges. // A radius of exactly half the shorter side leaves one or more of them // with no area, which SDL fills as nothing -- at the limit the whole // shape is the four fans. band.x = rect->x; band.y = rect->y + radius; band.w = rect->w; band.h = rect->h - (radius * 2.0f); if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) { drawfailed = true; } band.x = rect->x + radius; band.y = rect->y; band.w = rect->w - (radius * 2.0f); band.h = radius; if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) { drawfailed = true; } band.y = rect->y + rect->h - radius; if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) { drawfailed = true; } FAIL_NONZERO_BREAK(errctx, drawfailed, AKGL_ERR_SDL, "%s", SDL_GetError()); CATCH(errctx, fill_corner_fan(self, rect->x + radius, rect->y + radius, radius, 180.0f, color)); CATCH(errctx, fill_corner_fan(self, rect->x + rect->w - radius, rect->y + radius, radius, 270.0f, color)); CATCH(errctx, fill_corner_fan(self, rect->x + rect->w - radius, rect->y + rect->h - radius, radius, 0.0f, color)); CATCH(errctx, fill_corner_fan(self, rect->x + radius, rect->y + rect->h - radius, radius, 90.0f, color)); } CLEANUP { if ( pushed == true ) { IGNORE(pop_draw_color(self, &previous)); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_draw_arc(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, float32_t start_deg, float32_t end_deg, float32_t thickness, SDL_Color color) { SDL_Vertex vertices[AKGL_DRAW_ARC_MAX_POINTS]; int indices[(AKGL_DRAW_ARC_MAX_POINTS - 2) * 3]; SDL_FColor fcolor; float32_t span = 0.0f; float32_t inner = 0.0f; float32_t angle = 0.0f; int segments = 0; int base = 0; int i = 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_NONZERO_RETURN(errctx, (radius <= 0.0f), AKERR_OUTOFBOUNDS, "Arc radius %f is not positive", (double)radius); FAIL_NONZERO_RETURN(errctx, (thickness <= 0.0f), AKERR_OUTOFBOUNDS, "Arc thickness %f is not positive", (double)thickness); span = end_deg - start_deg; if ( span <= 0.0f ) { SUCCEED_RETURN(errctx); } if ( span > 360.0f ) { span = 360.0f; } inner = radius - thickness; if ( inner < 0.0f ) { inner = 0.0f; } // Steps in proportion to the angle swept, so a sliver of arc does not // spend the whole vertex budget and a full circle uses all of it: the // quarter-circle density of AKGL_DRAW_ROUNDED_RECT_SEGMENTS, capped by // what AKGL_DRAW_ARC_MAX_POINTS vertices can carry at two per step. segments = (int)((span / 90.0f) * (float)AKGL_DRAW_ROUNDED_RECT_SEGMENTS) + 1; if ( segments > ((AKGL_DRAW_ARC_MAX_POINTS / 2) - 1) ) { segments = (AKGL_DRAW_ARC_MAX_POINTS / 2) - 1; } fcolor.r = (float)color.r / 255.0f; fcolor.g = (float)color.g / 255.0f; fcolor.b = (float)color.b / 255.0f; fcolor.a = (float)color.a / 255.0f; for ( i = 0; i <= segments; i++ ) { angle = (start_deg + ((span / (float)segments) * (float)i)) * (SDL_PI_F / 180.0f); vertices[i * 2].position.x = x + (radius * SDL_cosf(angle)); vertices[i * 2].position.y = y + (radius * SDL_sinf(angle)); vertices[i * 2].color = fcolor; vertices[i * 2].tex_coord.x = 0.0f; vertices[i * 2].tex_coord.y = 0.0f; vertices[(i * 2) + 1].position.x = x + (inner * SDL_cosf(angle)); vertices[(i * 2) + 1].position.y = y + (inner * SDL_sinf(angle)); vertices[(i * 2) + 1].color = fcolor; vertices[(i * 2) + 1].tex_coord.x = 0.0f; vertices[(i * 2) + 1].tex_coord.y = 0.0f; } for ( i = 0; i < segments; i++ ) { base = i * 2; indices[(i * 6) + 0] = base; indices[(i * 6) + 1] = base + 2; indices[(i * 6) + 2] = base + 1; indices[(i * 6) + 3] = base + 1; indices[(i * 6) + 4] = base + 2; indices[(i * 6) + 5] = base + 3; } FAIL_ZERO_RETURN( errctx, SDL_RenderGeometry( self->sdl_renderer, NULL, vertices, (segments + 1) * 2, indices, segments * 6), AKGL_ERR_SDL, "%s", SDL_GetError()); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_draw_set_clip(akgl_RenderBackend *self, SDL_Rect *rect) { 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"); // NULL rect deliberately passes straight through: it is how the clip is // cleared, and the header says so. FAIL_ZERO_RETURN( errctx, SDL_SetRenderClipRect(self->sdl_renderer, rect), AKGL_ERR_SDL, "%s", SDL_GetError()); SUCCEED_RETURN(errctx); }