`RGR(1)` and `RGR(2)` gave the window in pixels and nothing gave columns, rows or the cell size -- so anything placing a character *and* a sprite at the same spot had to hardcode a number measured by hand against whatever font the host loaded. The Breakout in examples/ does exactly that, `CW# = 16`, and it is the one thing in that listing that breaks on a different font or window. **`RWINDOW` is BASIC 7.0's own answer and had never been implemented here.** `RWINDOW(0)` is the current text window's rows and `RWINDOW(1)` its columns. `RWINDOW(2)` reports a C128's 40 or 80 column screen mode, and this interpreter has neither -- refused by name, because answering 0 would be a plausible lie, which is worse than a refusal that says why. The cell size in pixels is `RGR(3)` and `RGR(4)`, beside the surface's own dimensions rather than on `RWINDOW`. Two reasons: a cell size is a fact about the surface, and `RWINDOW` reports the *window*, so dividing `RGR(1)` by a column count stops being right the moment a program calls `WINDOW`. Both read a new optional `grid` entry point on `akbasic_TextSink` -- columns, rows, cell width, cell height -- implemented by the akgl sink and forwarded by the tee, in the shape `moveto` and `window` already had. NULL everywhere else, so both verbs refuse by name against a sink with no grid. `akbasic_sink_init_ stdio()` clears it for the same reason it now clears the other two. Measured on the standalone build: `RGR(3)` answers 16 and `RWINDOW` answers 50 columns by 37 rows -- the three numbers the Breakout listing had written out as constants -- and `RWINDOW` follows a `WINDOW` call while `RGR(3)` does not. tests/console_verbs.c drives the answers through a stand-in sink with a grid, since the harness sink is stdio and has none; tests/graphics_verbs.c covers the new `RGR` fields, their refusal, and the moved range bound. The `c excerpt=` block in docs/10-embedding.md moves with the header, which is `docs_examples` doing its job. TODO.md section 6 item 31's second half, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
333 lines
13 KiB
C
333 lines
13 KiB
C
/**
|
|
* @file runtime_console.c
|
|
* @brief The group E verbs: SLEEP, WAIT, KEY, WINDOW, and the TI clock.
|
|
*
|
|
* Everything here that waits does so by *holding the step loop*, the way GETKEY
|
|
* already does (see akbasic_input_service). Section 1.6 forbids the library
|
|
* blocking: a host calls akbasic_runtime_step() once a frame and it must always
|
|
* come back, so "wait" means "do not advance this step" rather than "do not
|
|
* return". A bounded akbasic_runtime_run() still returns on time, and a host
|
|
* that wants to abandon the wait can change the mode.
|
|
*/
|
|
|
|
#include <inttypes.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akbasic/args.h>
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#include "verbs.h"
|
|
|
|
/* Most verbs answer "did something happen"; this is that answer. */
|
|
#define SUCCEED_TRUE(__obj, __dest) \
|
|
do { \
|
|
*(__dest) = &(__obj)->staticTrueValue; \
|
|
} while ( 0 )
|
|
|
|
/** @brief Jiffies per second. A Commodore counts time in sixtieths. */
|
|
#define JIFFIES_PER_SECOND 60
|
|
|
|
/* ------------------------------------------------------------------ SLEEP -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_sleep(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
double args[1];
|
|
int count = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in SLEEP");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "SLEEP", args, 1, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count == 1), AKBASIC_ERR_SYNTAX, "Expected SLEEP (seconds)");
|
|
FAIL_ZERO_RETURN(errctx, (args[0] >= 0.0), AKBASIC_ERR_VALUE,
|
|
"SLEEP cannot wait a negative number of seconds");
|
|
|
|
/*
|
|
* Records when to stop and returns. akbasic_console_service() holds the step
|
|
* loop until then -- so a program that sleeps still lets its host draw
|
|
* frames, and a sleeping program in an embedded game does not freeze the
|
|
* game.
|
|
*
|
|
* A host that never calls akbasic_runtime_settime() leaves the clock at
|
|
* zero, and a deadline computed from a clock that never advances is never
|
|
* reached. So no clock means no sleep: the verb does nothing rather than
|
|
* hanging the program, which is the same trade the audio durations make
|
|
* (§5 deviation 20) and the same direction -- fail fast, not silently
|
|
* forever.
|
|
*/
|
|
if ( obj->timems <= 0 ) {
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
obj->console_state.sleepuntilms = obj->timems + (int64_t)(args[0] * 1000.0);
|
|
obj->console_state.sleeping = true;
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------- WAIT -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_wait(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
double args[3];
|
|
int count = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in WAIT");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "WAIT", args, 3, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count >= 2), AKBASIC_ERR_SYNTAX,
|
|
"Expected WAIT (address), (mask) [, (xor)]");
|
|
|
|
/*
|
|
* WAIT polls a byte until `(PEEK(addr) XOR xor) AND mask` is non-zero. On a
|
|
* C128 that byte is a hardware register an interrupt is changing; here it is
|
|
* ordinary process memory, and the only thing that can change it is the host
|
|
* -- or another thread, or a POKE from a TRAP handler.
|
|
*
|
|
* So this is honest but rarely useful: a program that waits on memory
|
|
* nothing writes waits forever, which is exactly what the same program does
|
|
* on a C128 with the wrong address. It holds the step loop rather than
|
|
* blocking, so the host keeps its frame rate and can stop the program.
|
|
*/
|
|
obj->console_state.waitaddress = (uintptr_t)args[0];
|
|
obj->console_state.waitmask = (uint8_t)args[1];
|
|
obj->console_state.waitxor = (count >= 3 ? (uint8_t)args[2] : 0);
|
|
obj->console_state.waiting = true;
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* -------------------------------------------------------------------- KEY -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_key(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
akbasic_Value *value = NULL;
|
|
/* Room for the longest macro plus `KEY n, ""` around it. */
|
|
char line[AKBASIC_MAX_STRING_LENGTH + 32];
|
|
int64_t number = 0;
|
|
int i = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in KEY");
|
|
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
if ( arg == NULL ) {
|
|
/* Bare KEY lists the definitions, which is what a C128 does. */
|
|
for ( i = 0; i < AKBASIC_MAX_FUNCTION_KEYS; i++ ) {
|
|
snprintf(line, sizeof(line), "KEY %d, \"%s\"",
|
|
i + 1, obj->console_state.keys[i]);
|
|
PASS(errctx, akbasic_runtime_println(obj, line));
|
|
}
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"KEY expected a key number");
|
|
number = value->intval;
|
|
FAIL_ZERO_RETURN(errctx, (number >= 1 && number <= AKBASIC_MAX_FUNCTION_KEYS),
|
|
AKBASIC_ERR_BOUNDS, "KEY %" PRId64 " is outside 1..%d",
|
|
number, AKBASIC_MAX_FUNCTION_KEYS);
|
|
|
|
arg = arg->next;
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected KEY (number), (string)");
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"KEY expected a string");
|
|
snprintf(obj->console_state.keys[number - 1],
|
|
sizeof(obj->console_state.keys[0]), "%s", value->stringval);
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- WINDOW -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_window(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
double args[5];
|
|
int count = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in WINDOW");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "WINDOW", args, 5, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count >= 4), AKBASIC_ERR_SYNTAX,
|
|
"Expected WINDOW (left), (top), (right), (bottom) [, (clear)]");
|
|
/*
|
|
* The geometry first, and deliberately: an inside-out rectangle is a mistake
|
|
* in the program whether or not a device is attached, and reporting "no
|
|
* device" for it would send the author looking in the wrong place.
|
|
*/
|
|
FAIL_ZERO_RETURN(errctx, (args[0] <= args[2] && args[1] <= args[3]), AKBASIC_ERR_VALUE,
|
|
"WINDOW's bottom right must not be above or left of its top left");
|
|
FAIL_ZERO_RETURN(errctx, (obj->sink != NULL), AKBASIC_ERR_DEVICE,
|
|
"WINDOW needs a text device and this runtime has none");
|
|
FAIL_ZERO_RETURN(errctx, (obj->sink->window != NULL), AKBASIC_ERR_DEVICE,
|
|
"WINDOW needs a text device with a character grid, and this one has none");
|
|
|
|
PASS(errctx, obj->sink->window(obj->sink, (int)args[0], (int)args[1],
|
|
(int)args[2], (int)args[3]));
|
|
if ( count >= 5 && args[4] != 0.0 ) {
|
|
PASS(errctx, obj->sink->clear(obj->sink));
|
|
}
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- RWINDOW -- */
|
|
|
|
/**
|
|
* @brief How big the current text window is, in characters.
|
|
*
|
|
* BASIC 7.0's own answer to the question, and it had never been implemented
|
|
* here: `RWINDOW(0)` is the number of lines and `RWINDOW(1)` the number of
|
|
* columns. Without it a program had no way to find out, so anything placing a
|
|
* character *and* a sprite at the same spot had to hardcode a cell size measured
|
|
* against whatever font the host loaded. The Breakout in `examples/` did exactly
|
|
* that, and it was the one thing in that listing that broke on a different
|
|
* window. TODO.md section 6 item 31.
|
|
*
|
|
* **`RWINDOW(2)` is refused by name.** On a C128 it answers 0 for 40-column mode
|
|
* and 1 for 80-column, and this interpreter has no such mode -- the grid is
|
|
* whatever the window divided by the font gives. Answering 0 would be a
|
|
* plausible lie, which is worse than a refusal that says why.
|
|
*
|
|
* The cell size in pixels is `RGR(3)` and `RGR(4)` rather than a field here: it
|
|
* is a question about the drawing surface, and it belongs beside the surface's
|
|
* own dimensions.
|
|
*/
|
|
akerr_ErrorContext *akbasic_fn_rwindow(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;
|
|
int columns = 0;
|
|
int rows = 0;
|
|
int cellw = 0;
|
|
int cellh = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in RWINDOW");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "RWINDOW", args, 1, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "RWINDOW expected a field number");
|
|
field = (int)args[0];
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (field == 2), AKBASIC_ERR_DEVICE,
|
|
"RWINDOW(2) reports a C128's 40 or 80 column mode, and this interpreter has neither");
|
|
FAIL_ZERO_RETURN(errctx, (field == 0 || field == 1), AKBASIC_ERR_BOUNDS,
|
|
"RWINDOW: field %d is outside 0..1", field);
|
|
FAIL_ZERO_RETURN(errctx, (obj->sink != NULL), AKBASIC_ERR_DEVICE,
|
|
"RWINDOW needs a text device and this runtime has none");
|
|
FAIL_ZERO_RETURN(errctx, (obj->sink->grid != NULL), AKBASIC_ERR_DEVICE,
|
|
"RWINDOW needs a text device with a character grid, and this one has none");
|
|
PASS(errctx, obj->sink->grid(obj->sink, &columns, &rows, &cellw, &cellh));
|
|
|
|
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
|
|
PASS(errctx, akbasic_value_zero(out));
|
|
out->valuetype = AKBASIC_TYPE_INTEGER;
|
|
out->intval = (field == 0 ? (int64_t)rows : (int64_t)columns);
|
|
*dest = out;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- service -- */
|
|
|
|
akerr_ErrorContext *akbasic_console_state_init(akbasic_ConsoleState *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL console state in init");
|
|
memset(obj, 0, sizeof(*obj));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_console_service(akbasic_Runtime *obj, bool *blocked)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && blocked != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in console_service");
|
|
*blocked = false;
|
|
|
|
if ( obj->console_state.sleeping ) {
|
|
if ( obj->timems >= obj->console_state.sleepuntilms ) {
|
|
obj->console_state.sleeping = false;
|
|
} else {
|
|
*blocked = true;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
}
|
|
|
|
if ( obj->console_state.waiting ) {
|
|
const volatile uint8_t *address = (const volatile uint8_t *)obj->console_state.waitaddress;
|
|
uint8_t byte = 0;
|
|
|
|
/*
|
|
* `volatile`, because the whole point is that something outside this
|
|
* program changes it. Without it the compiler is entitled to read the
|
|
* byte once and spin on the register.
|
|
*/
|
|
byte = *address;
|
|
if ( ((byte ^ obj->console_state.waitxor) & obj->console_state.waitmask) != 0 ) {
|
|
obj->console_state.waiting = false;
|
|
} else {
|
|
*blocked = true;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_console_update_clock(akbasic_Runtime *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Variable *variable = NULL;
|
|
int64_t zerosubscript[1] = { 0 };
|
|
int64_t jiffies = 0;
|
|
int64_t seconds = 0;
|
|
char text[16];
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in update_clock");
|
|
if ( obj->environment == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* `TI` and `TI$` are `TI#` and `TI$` here, and they are written rather than
|
|
* computed on read: this dialect has no bare variable names and no
|
|
* pseudo-variable mechanism, so they are ordinary globals refreshed once per
|
|
* step. Same decision as `ER#` and `EL#`, for the same reason.
|
|
*
|
|
* Counted in jiffies -- sixtieths of a second -- from the host's clock,
|
|
* which is what `TI` means on a Commodore. A host that never calls
|
|
* akbasic_runtime_settime() leaves both at zero, which is a stopped clock
|
|
* rather than a wrong one.
|
|
*/
|
|
jiffies = (obj->timems * JIFFIES_PER_SECOND) / 1000;
|
|
PASS(errctx, akbasic_runtime_global(obj, "TI#", &variable));
|
|
PASS(errctx, akbasic_variable_set_integer(variable, jiffies, zerosubscript, 1));
|
|
|
|
seconds = obj->timems / 1000;
|
|
snprintf(text, sizeof(text), "%02" PRId64 "%02" PRId64 "%02" PRId64,
|
|
(seconds / 3600) % 24, (seconds / 60) % 60, seconds % 60);
|
|
PASS(errctx, akbasic_runtime_global(obj, "TI$", &variable));
|
|
PASS(errctx, akbasic_variable_set_string(variable, text, zerosubscript, 1));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|