The graphics verbs documented a coordinate transform that did not exist. With SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an 800x600 window drew a C128 listing into its corner and left the rest unused -- while the chapter said coordinates were 320x200 and stretching to fit was the host's business. akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it before every verb that draws so a resized window is honoured between two statements, and 320x200 becomes the fallback for a backend that leaves it NULL. It is the record's one optional member, so a host written against the old header keeps the behaviour it had. SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's own field, the GRAPHIC mode. SCALE also mapped xmax onto the width rather than onto the last pixel, so SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel past the surface. Fixed in the same line, because it is what makes "SCALE gives a C128 listing the whole window" true rather than nearly true. The akgl test renders against a 128x128 target, deliberately smaller than the old constants: a SCALE still dividing by them misses it entirely rather than landing somewhere plausible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
265 lines
9.0 KiB
C
265 lines
9.0 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 <string.h>
|
|
|
|
#include <akerror.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());
|
|
|
|
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);
|
|
}
|