/** * @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 #include #include #include #include #include #include #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); } /* ---------------------------------------------------------------- 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); }