/** * @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 #include #include #include #include #include /** @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()); 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; } }