akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
404 lines
14 KiB
C
404 lines
14 KiB
C
/**
|
|
* @file graphics_akgl.c
|
|
* @brief Wires the graphics backend record to libakgl's immediate-mode drawing.
|
|
*
|
|
* A thin adaptor and deliberately so: the interesting decisions about what a
|
|
* BASIC graphics verb means live in src/runtime_graphics.c, and everything here
|
|
* is coordinate and colour conversion plus the shape pool that SSHAPE hands out
|
|
* handles into.
|
|
*
|
|
* The akgl_draw_* family is new in libakgl 42b60f7. Every one of its entry
|
|
* points takes the akgl_RenderBackend the host already initialized rather than
|
|
* reaching for a global, which is exactly what goal 3 needs.
|
|
*/
|
|
|
|
#include <akerror.h>
|
|
#include <akstdlib.h>
|
|
|
|
#include <akgl/draw.h>
|
|
#include <akgl/error.h>
|
|
|
|
#include <akbasic/akgl.h>
|
|
#include <akbasic/error.h>
|
|
|
|
/** @brief The colour conversion, which is the whole impedance mismatch. */
|
|
static SDL_Color to_sdl(akbasic_Color color)
|
|
{
|
|
SDL_Color out;
|
|
|
|
out.r = color.r;
|
|
out.g = color.g;
|
|
out.b = color.b;
|
|
out.a = color.a;
|
|
return out;
|
|
}
|
|
|
|
/** @brief Recover the backend's own state, or say that it has none. */
|
|
static akerr_ErrorContext *state_of(akbasic_GraphicsBackend *self, akbasic_AkglGraphics **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in akgl graphics backend");
|
|
*dest = (akbasic_AkglGraphics *)self->self;
|
|
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER,
|
|
"akgl graphics backend has no state");
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief How big the renderer's output is, which is the space BASIC draws into.
|
|
*
|
|
* SDL_GetCurrentRenderOutputSize rather than the window size: the two differ on
|
|
* a high-DPI display and on a renderer with a logical presentation set, and it
|
|
* is the output that a coordinate handed to akgl_draw_point actually indexes.
|
|
*/
|
|
static akerr_ErrorContext *gfx_size(akbasic_GraphicsBackend *self, int *width, int *height)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
FAIL_ZERO_RETURN(errctx, (width != NULL && height != NULL), AKERR_NULLPOINTER,
|
|
"NULL destination in gfx_size");
|
|
FAIL_ZERO_RETURN(errctx,
|
|
SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, width, height),
|
|
AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *gfx_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
PASS(errctx, akgl_draw_point(state->renderer, (float32_t)x, (float32_t)y, to_sdl(color)));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *gfx_line(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
PASS(errctx, akgl_draw_line(state->renderer, (float32_t)x1, (float32_t)y1,
|
|
(float32_t)x2, (float32_t)y2, to_sdl(color)));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Turn two opposite corners into the rect SDL wants.
|
|
*
|
|
* BASIC gives two corners in whatever order the program felt like; SDL_FRect is
|
|
* an origin and a size and does nothing useful with a negative one.
|
|
*/
|
|
static SDL_FRect corners_to_rect(double x1, double y1, double x2, double y2)
|
|
{
|
|
SDL_FRect rect;
|
|
|
|
rect.x = (float32_t)((x1 < x2) ? x1 : x2);
|
|
rect.y = (float32_t)((y1 < y2) ? y1 : y2);
|
|
rect.w = (float32_t)((x1 < x2) ? (x2 - x1) : (x1 - x2));
|
|
rect.h = (float32_t)((y1 < y2) ? (y2 - y1) : (y1 - y2));
|
|
return rect;
|
|
}
|
|
|
|
static akerr_ErrorContext *gfx_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
SDL_FRect rect;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
rect = corners_to_rect(x1, y1, x2, y2);
|
|
PASS(errctx, akgl_draw_rect(state->renderer, &rect, to_sdl(color)));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *gfx_filled_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
SDL_FRect rect;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
rect = corners_to_rect(x1, y1, x2, y2);
|
|
PASS(errctx, akgl_draw_filled_rect(state->renderer, &rect, to_sdl(color)));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *gfx_paint(akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
/*
|
|
* AKERR_OUTOFBOUNDS out of here means the fill exhausted its span stack and
|
|
* left the region partly filled. It is passed straight through: PAINT is
|
|
* where that is turned into something a BASIC program can see, because PAINT
|
|
* is the only thing that knows a program is watching.
|
|
*/
|
|
PASS(errctx, akgl_draw_flood_fill(state->renderer, x, y, to_sdl(color)));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *gfx_clear(akbasic_GraphicsBackend *self, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
SDL_FRect rect;
|
|
int w = 0;
|
|
int h = 0;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
PASS(errctx, gfx_size(self, &w, &h));
|
|
|
|
/*
|
|
* A filled rectangle over the output rather than SDL_RenderClear, because
|
|
* clearing is the host's prerogative: a game that draws a background and
|
|
* then hands the renderer to a script does not expect GRAPHIC to wipe it to
|
|
* a colour of the script's choosing outside the area the script owns. This
|
|
* covers exactly the render output and nothing beyond it.
|
|
*/
|
|
rect.x = 0.0f;
|
|
rect.y = 0.0f;
|
|
rect.w = (float32_t)w;
|
|
rect.h = (float32_t)h;
|
|
PASS(errctx, akgl_draw_filled_rect(state->renderer, &rect, to_sdl(color)));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *gfx_save_shape(akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
SDL_Rect region;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
FAIL_ZERO_RETURN(errctx, (handle != NULL), AKERR_NULLPOINTER,
|
|
"NULL handle in save_shape");
|
|
FAIL_ZERO_RETURN(errctx, (state->shapecount < AKBASIC_AKGL_MAX_SHAPES),
|
|
AKBASIC_ERR_DEVICE,
|
|
"SSHAPE pool is full at %d saved regions; GRAPHIC CLR releases them",
|
|
AKBASIC_AKGL_MAX_SHAPES);
|
|
|
|
region.x = (x1 < x2) ? x1 : x2;
|
|
region.y = (y1 < y2) ? y1 : y2;
|
|
region.w = (x1 < x2) ? (x2 - x1) : (x1 - x2);
|
|
region.h = (y1 < y2) ? (y2 - y1) : (y1 - y2);
|
|
|
|
/*
|
|
* NULL through dest asks akgl_draw_copy_region to allocate the surface. That
|
|
* is the one allocation in this file and it belongs to the pool below, which
|
|
* is bounded -- so it is a fixed ceiling on memory, not an open one.
|
|
*/
|
|
state->shapes[state->shapecount] = NULL;
|
|
PASS(errctx, akgl_draw_copy_region(state->renderer, ®ion,
|
|
&state->shapes[state->shapecount]));
|
|
*handle = state->shapecount;
|
|
state->shapecount += 1;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *gfx_paste_shape(akbasic_GraphicsBackend *self, int handle, double x, double y)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < state->shapecount),
|
|
AKBASIC_ERR_BOUNDS, "No saved shape %d", handle);
|
|
FAIL_ZERO_RETURN(errctx, (state->shapes[handle] != NULL), AKBASIC_ERR_STATE,
|
|
"Saved shape %d has been released", handle);
|
|
PASS(errctx, akgl_draw_paste_region(state->renderer, state->shapes[handle],
|
|
(float32_t)x, (float32_t)y));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *gfx_free_shapes(akbasic_GraphicsBackend *self)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
int i = 0;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
for ( i = 0; i < state->shapecount; i++ ) {
|
|
if ( state->shapes[i] != NULL ) {
|
|
SDL_DestroySurface(state->shapes[i]);
|
|
state->shapes[i] = NULL;
|
|
}
|
|
}
|
|
state->shapecount = 0;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in graphics_init_akgl");
|
|
FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
|
|
"NULL renderer in graphics_init_akgl: the host creates it, not this");
|
|
PASS(errctx, akgl_error_init());
|
|
|
|
PASS(errctx, aksl_memset(state, 0, sizeof(*state)));
|
|
state->renderer = renderer;
|
|
|
|
obj->self = state;
|
|
obj->size = gfx_size;
|
|
obj->point = gfx_point;
|
|
obj->line = gfx_line;
|
|
obj->rect = gfx_rect;
|
|
obj->filled_rect = gfx_filled_rect;
|
|
obj->paint = gfx_paint;
|
|
obj->clear = gfx_clear;
|
|
obj->save_shape = gfx_save_shape;
|
|
obj->paste_shape = gfx_paste_shape;
|
|
obj->free_shapes = gfx_free_shapes;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ----------------------------------------------------------- the layer -- */
|
|
|
|
/**
|
|
* @brief Make sure the layer texture exists and is the size of the output.
|
|
*
|
|
* Created on demand rather than at init, because it is the size of the window
|
|
* and a program that never draws should not pay for one. Recreated if the
|
|
* output has since changed size, which is what a resized window looks like from
|
|
* here.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *ensure_layer(akbasic_AkglGraphics *state)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_Texture *previous = NULL;
|
|
int w = 0;
|
|
int h = 0;
|
|
float texw = 0.0f;
|
|
float texh = 0.0f;
|
|
|
|
FAIL_ZERO_RETURN(errctx, SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, &w, &h),
|
|
AKGL_ERR_SDL, "Couldn't measure the render output: %s", SDL_GetError());
|
|
if ( state->layer != NULL ) {
|
|
if ( SDL_GetTextureSize(state->layer, &texw, &texh)
|
|
&& (int)texw == w && (int)texh == h ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
SDL_DestroyTexture(state->layer);
|
|
state->layer = NULL;
|
|
}
|
|
state->layer = SDL_CreateTexture(state->renderer->sdl_renderer,
|
|
SDL_PIXELFORMAT_RGBA8888,
|
|
SDL_TEXTUREACCESS_TARGET, w, h);
|
|
FAIL_ZERO_RETURN(errctx, (state->layer != NULL), AKGL_ERR_SDL,
|
|
"Couldn't create the drawing layer: %s", SDL_GetError());
|
|
/*
|
|
* Blended, and cleared to fully transparent. The layer covers the whole
|
|
* window and is composited *under* the text and the sprites, so anywhere the
|
|
* program has not drawn has to let what is behind it through -- otherwise a
|
|
* single DRAW would black out the entire frame.
|
|
*/
|
|
FAIL_ZERO_RETURN(errctx, SDL_SetTextureBlendMode(state->layer, SDL_BLENDMODE_BLEND),
|
|
AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
/*
|
|
* **And clear it, because a fresh target texture's contents are undefined.**
|
|
* Skipping this puts whatever was in that memory on the screen under the
|
|
* first frame's text, which is the kind of defect that looks like a driver
|
|
* bug and is not.
|
|
*/
|
|
previous = SDL_GetRenderTarget(state->renderer->sdl_renderer);
|
|
FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, state->layer),
|
|
AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
if ( !SDL_SetRenderDrawColor(state->renderer->sdl_renderer, 0, 0, 0, 0)
|
|
|| !SDL_RenderClear(state->renderer->sdl_renderer) ) {
|
|
/* Best effort: we are already failing, and leaving the target on the
|
|
layer would make the next unrelated draw land in the wrong place. */
|
|
(void)SDL_SetRenderTarget(state->renderer->sdl_renderer, previous);
|
|
FAIL_RETURN(errctx, AKGL_ERR_SDL, "Couldn't clear the drawing layer: %s", SDL_GetError());
|
|
}
|
|
FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, previous),
|
|
AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_graphics_akgl_begin(akbasic_GraphicsBackend *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
state = (akbasic_AkglGraphics *)obj->self;
|
|
PASS(errctx, ensure_layer(state));
|
|
state->savedtarget = SDL_GetRenderTarget(state->renderer->sdl_renderer);
|
|
FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, state->layer),
|
|
AKGL_ERR_SDL, "Couldn't make the drawing layer current: %s", SDL_GetError());
|
|
state->layeractive = true;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
bool akbasic_graphics_akgl_layer_active(akbasic_GraphicsBackend *obj)
|
|
{
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
return false;
|
|
}
|
|
return ((akbasic_AkglGraphics *)obj->self)->layeractive;
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_graphics_akgl_end(akbasic_GraphicsBackend *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
state = (akbasic_AkglGraphics *)obj->self;
|
|
if ( !state->layeractive ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
state->layeractive = false;
|
|
FAIL_ZERO_RETURN(errctx,
|
|
SDL_SetRenderTarget(state->renderer->sdl_renderer, state->savedtarget),
|
|
AKGL_ERR_SDL, "Couldn't restore the render target: %s", SDL_GetError());
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_graphics_akgl_render(akbasic_GraphicsBackend *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
state = (akbasic_AkglGraphics *)obj->self;
|
|
/* Nothing drawn, nothing to composite: a text-only program pays nothing. */
|
|
if ( state->layer == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_ZERO_RETURN(errctx,
|
|
SDL_RenderTexture(state->renderer->sdl_renderer, state->layer, NULL, NULL),
|
|
AKGL_ERR_SDL, "Couldn't draw the drawing layer: %s", SDL_GetError());
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
void akbasic_graphics_akgl_shutdown(akbasic_GraphicsBackend *obj)
|
|
{
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
return;
|
|
}
|
|
state = (akbasic_AkglGraphics *)obj->self;
|
|
if ( state->layer != NULL ) {
|
|
SDL_DestroyTexture(state->layer);
|
|
state->layer = NULL;
|
|
}
|
|
}
|