Draw: implement the BASIC 7.0 graphics verbs

GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.

The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:

- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
  deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
  so the primitive could serve only the fully-defaulted call, and a shape that
  changed character depending on whether the radii happened to be equal would be
  worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
  because a value's string is a fixed 256 bytes and a region is a device surface.
  GSHAPE refuses a string without that prefix instead of parsing whatever digits
  it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
  would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
  anything here -- mode 0 is text -- while still refusing an out-of-range mode,
  since that is a typo worth catching.

PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.

COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.

Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.

65/65 ctest, clean under -Wall -Wextra, doxygen clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 08:17:48 -04:00
parent 9b9dd09c5a
commit 9d99d0a67e
15 changed files with 1264 additions and 0 deletions

View File

@@ -23,6 +23,20 @@
#include <akbasic/types.h>
/**
* @brief The coordinate space the backend receives, in C128 hi-res pixels.
*
* The interpreter does not own the renderer and cannot ask how big it is, so it
* emits the coordinates the program wrote -- BASIC 7.0's 320x200 hi-res screen,
* or whatever SCALE maps onto it. Stretching that to the window is the host's
* business, the same way the host owns the window in the first place.
*/
#define AKBASIC_GRAPHICS_WIDTH 320
#define AKBASIC_GRAPHICS_HEIGHT 200
/** @brief How many COLOR sources there are. BASIC 7.0 numbers them 0 through 6. */
#define AKBASIC_COLOR_SOURCES 7
/**
* @brief An RGBA color, in the shape SDL_Color has without requiring SDL.
*
@@ -38,6 +52,25 @@ typedef struct
uint8_t a;
} akbasic_Color;
/**
* @brief The graphics verbs' own state, which lives on the runtime.
*
* DRAW, BOX, CIRCLE and PAINT name a *color source*, not a color: COLOR binds a
* source to a palette index and the drawing verbs reference the source. That
* indirection is BASIC 7.0's, not an invention here, and it is why `source` is a
* table of palette indices rather than a single current color.
*/
typedef struct
{
int mode; /* GRAPHIC mode; 0 is text, and refuses to draw */
int source[AKBASIC_COLOR_SOURCES]; /* palette index bound to each COLOR source */
double x; /* pixel cursor: where LOCATE put it, or where */
double y; /* the last DRAW ended */
bool scaling; /* SCALE on */
double xmax; /* user-coordinate maxima SCALE maps from */
double ymax;
} akbasic_GraphicsState;
/**
* @brief Where the graphics verbs draw.
*
@@ -102,4 +135,27 @@ typedef struct akbasic_GraphicsBackend
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_palette(int index, akbasic_Color *dest);
/**
* @brief Reset the graphics state to its power-on values.
*
* Text mode, the default color-source bindings, the pixel cursor at the origin
* and scaling off. GRAPHIC CLR and NEW both come back through here.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_state_init(akbasic_GraphicsState *obj);
/**
* @brief Resolve a COLOR source to the RGBA the backend draws with.
* @param obj Graphics state to read the binding from.
* @param source COLOR source number, 0 through 6.
* @param dest Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` or `dest` is NULL.
* @throws AKBASIC_ERR_BOUNDS When `source` is outside 0..6.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_source_color(akbasic_GraphicsState *obj, int source, akbasic_Color *dest);
#endif // _AKBASIC_GRAPHICS_H_

View File

@@ -96,6 +96,14 @@ typedef struct akbasic_Runtime
akbasic_AudioBackend *audio;
akbasic_InputBackend *input;
/*
* The graphics verbs' own state -- mode, color-source bindings, pixel cursor
* and SCALE. It lives here rather than on the backend because it is BASIC's
* state, not the device's: a host that swaps one renderer for another does
* not expect the program's COLOR settings to go with it.
*/
akbasic_GraphicsState gfx;
/*
* The host's clock, in milliseconds, as of its last akbasic_runtime_settime()
* call. The interpreter does not read a clock: it owns no loop and must not