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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
106 lines
4.2 KiB
C
106 lines
4.2 KiB
C
/**
|
|
* @file graphics_tables.c
|
|
* @brief Implements the BASIC 7.0 color palette lookup.
|
|
*
|
|
* This is a table, so it is laid out as one. The RGB values are the widely
|
|
* reproduced VIC-II palette (Pepto's measurement of a PAL 6569R3), which is what
|
|
* an emulator shows and therefore what somebody porting a listing expects to
|
|
* see. They are not a C128 ROM constant -- the real machine has no RGB anywhere
|
|
* in it, it has a chroma/luma encoder -- so this is a choice, not a transcription.
|
|
*/
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/graphics.h>
|
|
|
|
/**
|
|
* @brief The sixteen BASIC 7.0 colors, indexed from 1 as BASIC indexes them.
|
|
*
|
|
* Slot 0 is unused and left black so an off-by-one reads as black rather than as
|
|
* whatever the last entry happened to be.
|
|
*/
|
|
static const akbasic_Color PALETTE[17] = {
|
|
{ 0x00, 0x00, 0x00, 0xff }, /* 0 -- unused; BASIC counts colors from 1 */
|
|
{ 0x00, 0x00, 0x00, 0xff }, /* 1 -- black */
|
|
{ 0xff, 0xff, 0xff, 0xff }, /* 2 -- white */
|
|
{ 0x88, 0x39, 0x32, 0xff }, /* 3 -- red */
|
|
{ 0x67, 0xb6, 0xbd, 0xff }, /* 4 -- cyan */
|
|
{ 0x8b, 0x3f, 0x96, 0xff }, /* 5 -- purple */
|
|
{ 0x55, 0xa0, 0x49, 0xff }, /* 6 -- green */
|
|
{ 0x40, 0x31, 0x8d, 0xff }, /* 7 -- blue */
|
|
{ 0xbf, 0xce, 0x72, 0xff }, /* 8 -- yellow */
|
|
{ 0x8b, 0x54, 0x29, 0xff }, /* 9 -- orange */
|
|
{ 0x57, 0x42, 0x00, 0xff }, /* 10 -- brown */
|
|
{ 0xb8, 0x69, 0x62, 0xff }, /* 11 -- light red */
|
|
{ 0x50, 0x50, 0x50, 0xff }, /* 12 -- dark grey */
|
|
{ 0x78, 0x78, 0x78, 0xff }, /* 13 -- medium grey */
|
|
{ 0x94, 0xe0, 0x89, 0xff }, /* 14 -- light green */
|
|
{ 0x78, 0x69, 0xc4, 0xff }, /* 15 -- light blue */
|
|
{ 0x9f, 0x9f, 0x9f, 0xff } /* 16 -- light grey */
|
|
};
|
|
|
|
/**
|
|
* @brief What each COLOR source is bound to before a program says otherwise.
|
|
*
|
|
* A C128 powers on with a blue screen and light blue text. That is not
|
|
* reproduced: a graphics screen defaulting to blue-on-blue makes an unset COLOR
|
|
* look like a bug in the interpreter, so the defaults here are the readable ones.
|
|
* The choice is worth naming rather than hiding, since it is the one place these
|
|
* tables are not a transcription.
|
|
*/
|
|
static const int SOURCE_DEFAULTS[AKBASIC_COLOR_SOURCES] = {
|
|
1, /* 0 -- 40-column background: black */
|
|
2, /* 1 -- 40-column foreground: white */
|
|
3, /* 2 -- multicolor 1: red */
|
|
4, /* 3 -- multicolor 2: cyan */
|
|
1, /* 4 -- 40-column border: black */
|
|
2, /* 5 -- character color: white */
|
|
1 /* 6 -- 80-column background: black */
|
|
};
|
|
|
|
akerr_ErrorContext *akbasic_graphics_palette(int index, akbasic_Color *dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL destination in palette");
|
|
FAIL_ZERO_RETURN(errctx, (index >= 1 && index <= 16), AKBASIC_ERR_BOUNDS,
|
|
"Color index %d out of range (1 to 16)", index);
|
|
*dest = PALETTE[index];
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_graphics_state_init(akbasic_GraphicsState *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
int source = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
|
"NULL graphics state in init");
|
|
obj->mode = 0;
|
|
obj->x = 0.0;
|
|
obj->y = 0.0;
|
|
obj->scaling = false;
|
|
obj->xmax = (double)AKBASIC_GRAPHICS_WIDTH;
|
|
obj->ymax = (double)AKBASIC_GRAPHICS_HEIGHT;
|
|
for ( source = 0; source < AKBASIC_COLOR_SOURCES; source++ ) {
|
|
obj->source[source] = SOURCE_DEFAULTS[source];
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_graphics_source_color(akbasic_GraphicsState *obj, int source, akbasic_Color *dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in source_color");
|
|
FAIL_ZERO_RETURN(errctx, (source >= 0 && source < AKBASIC_COLOR_SOURCES),
|
|
AKBASIC_ERR_BOUNDS,
|
|
"Color source %d out of range (0 to %d)",
|
|
source, AKBASIC_COLOR_SOURCES - 1);
|
|
PASS(errctx, akbasic_graphics_palette(obj->source[source], dest));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|