Draw into the whole window, not its top-left 320x200
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>
This commit is contained in:
@@ -47,6 +47,27 @@ static akerr_ErrorContext *state_of(akbasic_GraphicsBackend *self, akbasic_AkglG
|
||||
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);
|
||||
@@ -134,9 +155,7 @@ static akerr_ErrorContext *gfx_clear(akbasic_GraphicsBackend *self, akbasic_Colo
|
||||
int h = 0;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx,
|
||||
SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, &w, &h),
|
||||
AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
PASS(errctx, gfx_size(self, &w, &h));
|
||||
|
||||
/*
|
||||
* A filled rectangle over the output rather than SDL_RenderClear, because
|
||||
@@ -231,6 +250,7 @@ akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akb
|
||||
state->renderer = renderer;
|
||||
|
||||
obj->self = state;
|
||||
obj->size = gfx_size;
|
||||
obj->point = gfx_point;
|
||||
obj->line = gfx_line;
|
||||
obj->rect = gfx_rect;
|
||||
|
||||
@@ -85,6 +85,14 @@ akerr_ErrorContext *akbasic_graphics_state_init(akbasic_GraphicsState *obj)
|
||||
obj->linewidth = 1;
|
||||
obj->xmax = (double)AKBASIC_GRAPHICS_WIDTH;
|
||||
obj->ymax = (double)AKBASIC_GRAPHICS_HEIGHT;
|
||||
/*
|
||||
* The size assumed until a device says otherwise, which require_graphics()
|
||||
* asks it before every verb that draws. GRAPHIC CLR comes back through here
|
||||
* and deliberately resets it: the next verb re-asks, so a stale size cannot
|
||||
* outlive the device that reported it.
|
||||
*/
|
||||
obj->devwidth = AKBASIC_GRAPHICS_WIDTH;
|
||||
obj->devheight = AKBASIC_GRAPHICS_HEIGHT;
|
||||
for ( source = 0; source < AKBASIC_COLOR_SOURCES; source++ ) {
|
||||
obj->source[source] = SOURCE_DEFAULTS[source];
|
||||
}
|
||||
|
||||
@@ -39,40 +39,73 @@
|
||||
#define CIRCLE_DEFAULT_INC 2.0
|
||||
|
||||
/**
|
||||
* @brief Refuse politely when the host lent us no graphics device.
|
||||
* @brief Refuse politely when the host lent us no graphics device, and ask its size.
|
||||
*
|
||||
* Every verb in this file opens with it. The standalone driver attaches no
|
||||
* backend at all, so this is the common path rather than an edge case, and it
|
||||
* has to name the verb -- "no graphics device" on its own tells a program author
|
||||
* nothing about which line to look at.
|
||||
*
|
||||
* It is also where the drawing surface's size is refreshed, which is why the two
|
||||
* jobs are one function: the size is wanted by every verb that draws, every verb
|
||||
* that draws already opens with this, and a window can be resized between two
|
||||
* statements. A backend with no `size` -- it is the record's one optional entry
|
||||
* point -- keeps whatever akbasic_graphics_state_init() left, which is 320x200.
|
||||
*/
|
||||
static akerr_ErrorContext *require_graphics(akbasic_Runtime *obj, const char *verb)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in require_graphics");
|
||||
FAIL_ZERO_RETURN(errctx, (obj->graphics != NULL), AKBASIC_ERR_DEVICE,
|
||||
"%s needs a graphics device and this runtime has none", verb);
|
||||
if ( obj->graphics->size != NULL ) {
|
||||
PASS(errctx, obj->graphics->size(obj->graphics, &width, &height));
|
||||
/*
|
||||
* A device reporting nothing useful is not an error -- an SDL window
|
||||
* mid-resize legitimately measures zero -- but it must not become the
|
||||
* space a program draws into, because dividing by it in scale_point
|
||||
* would send every scaled coordinate to infinity.
|
||||
*/
|
||||
if ( width > 0 && height > 0 ) {
|
||||
obj->gfx.devwidth = width;
|
||||
obj->gfx.devheight = height;
|
||||
}
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Map a user coordinate onto the 320x200 space the backend receives.
|
||||
* @brief Map a user coordinate onto the device pixels the backend receives.
|
||||
*
|
||||
* With SCALE off this is the identity. With it on, BASIC's user coordinates run
|
||||
* 0..xmax across the same screen, so the ratio is all there is to it.
|
||||
* With SCALE off this is the identity, and a coordinate is a device pixel: the
|
||||
* whole of the host's window is reachable from BASIC. With SCALE on, the
|
||||
* program's coordinates run 0..xmax across the same surface, so the ratio is all
|
||||
* there is to it -- and the surface is however big the device last said it was,
|
||||
* which is what makes `SCALE 1, 319, 199` the way to run a C128 listing full
|
||||
* screen.
|
||||
*
|
||||
* **`xmax` maps onto the last pixel, not onto the width**, which is a
|
||||
* one-character difference and the whole difference between that sentence being
|
||||
* true and being nearly true. Dividing by the width sends `SCALE 1, 319, 199`
|
||||
* plus `DRAW 1, 319, 199` to (width, height) -- one past the bottom-right
|
||||
* corner, off the surface, drawing nothing. A user maximum is a coordinate the
|
||||
* program is entitled to plot, the same way 7.0's own `SCALE 1` then
|
||||
* `DRAW 1, 1023, 1023` reaches the corner of a C128 screen rather than missing it.
|
||||
*/
|
||||
static void scale_point(akbasic_GraphicsState *gfx, double *x, double *y)
|
||||
{
|
||||
if ( !gfx->scaling ) {
|
||||
return;
|
||||
}
|
||||
if ( gfx->xmax > 0.0 ) {
|
||||
*x = (*x / gfx->xmax) * (double)AKBASIC_GRAPHICS_WIDTH;
|
||||
if ( gfx->xmax > 0.0 && gfx->devwidth > 1 ) {
|
||||
*x = (*x / gfx->xmax) * (double)(gfx->devwidth - 1);
|
||||
}
|
||||
if ( gfx->ymax > 0.0 ) {
|
||||
*y = (*y / gfx->ymax) * (double)AKBASIC_GRAPHICS_HEIGHT;
|
||||
if ( gfx->ymax > 0.0 && gfx->devheight > 1 ) {
|
||||
*y = (*y / gfx->ymax) * (double)(gfx->devheight - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +282,61 @@ akerr_ErrorContext *akbasic_cmd_scale(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- RGR --- */
|
||||
|
||||
akerr_ErrorContext *akbasic_fn_rgr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Value *out = NULL;
|
||||
double args[1];
|
||||
int count = 0;
|
||||
int field = 0;
|
||||
int64_t answer = 0;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in RGR");
|
||||
PASS(errctx, akbasic_args_numbers(obj, expr, "RGR", args, 1, &count));
|
||||
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "RGR expected a field number");
|
||||
field = (int)args[0];
|
||||
|
||||
/*
|
||||
* Field 0 is BASIC 7.0's whole RGR: the current GRAPHIC mode. It answers
|
||||
* from state and works with no device, because a program can ask what mode
|
||||
* it set without a screen to set it on.
|
||||
*
|
||||
* Fields 1 and 2 are ours, and they are the half of item 9 a program can
|
||||
* see: the drawing surface's real width and height in pixels, so a listing
|
||||
* can fill whatever window the host opened instead of assuming 320x200.
|
||||
* They go through require_graphics(), which is both the refusal when there
|
||||
* is no device -- asking how big the screen is when there is no screen is a
|
||||
* program bug worth reporting -- and the refresh that makes the answer
|
||||
* current rather than whatever it was at attach.
|
||||
*/
|
||||
switch ( field ) {
|
||||
case 0:
|
||||
answer = (int64_t)obj->gfx.mode;
|
||||
break;
|
||||
case 1:
|
||||
PASS(errctx, require_graphics(obj, "RGR"));
|
||||
answer = (int64_t)obj->gfx.devwidth;
|
||||
break;
|
||||
case 2:
|
||||
PASS(errctx, require_graphics(obj, "RGR"));
|
||||
answer = (int64_t)obj->gfx.devheight;
|
||||
break;
|
||||
default:
|
||||
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS, "RGR: field %d is outside 0..2", field);
|
||||
}
|
||||
|
||||
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
|
||||
PASS(errctx, akbasic_value_zero(out));
|
||||
out->valuetype = AKBASIC_TYPE_INTEGER;
|
||||
out->intval = answer;
|
||||
*dest = out;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- DRAW --- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_draw(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
|
||||
@@ -136,6 +136,7 @@ static const akbasic_Verb VERBS[] = {
|
||||
{ "RESTORE", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_restore },
|
||||
{ "RESUME", AKBASIC_TOK_COMMAND, -1, akbasic_parse_resume, akbasic_cmd_resume },
|
||||
{ "RETURN", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_return },
|
||||
{ "RGR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rgr },
|
||||
{ "RIGHT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_right },
|
||||
{ "RSPCOLOR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rspcolor },
|
||||
{ "RSPPOS", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsppos },
|
||||
|
||||
@@ -171,6 +171,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_locate(struct akbasic_Runtime *ob
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_paint(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_scale(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_sshape(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rgr(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
|
||||
/* Group I sound verbs -- src/runtime_audio.c */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_envelope(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
|
||||
Reference in New Issue
Block a user