Files
SplitBit-Emulator/Source/Emulator/machine.c
T
AnachronautandClaude Opus 5 f7657081be Put a reset button on the case, and stop Escape closing the window
ESCAPE WAS A BUG I LEFT. This machine sends Escape to the console like any other key, and
Raylib closes a window on Escape unless it is told not to - so a program reading keys could
be ended by one of them, taking whatever was in memory with it. SetExitKey(KEY_NULL), and it
is a byte again.

F12 is the reset button. A button on the case rather than a key the machine can see: nothing
sends a function key to the console, so nothing can be surprised by one. It does what writing
MACHINE_RESET does, which is that the machine starts the way it started - the boot chain runs
again and finds whatever the disk now says to run.

Which is what makes a bare metal program escapable. Once puts a demo in front of the next
start and deletes the request before jumping, so a demo that has taken the whole machine is
one keypress from the system coming back, instead of closing the window and opening it again.

IT HAD TO REACH A MACHINE THAT IS WAITING, and that took two more things. A reset is acted on
between instructions, and a machine blocked on a key is part way through one - so the button
would have set a flag that nothing ever came along to notice, in exactly the situation a
reset button is for. The wait ends now: the console is told its input is over, which it is
for a machine about to stop existing.

And the reset puts the console's input back - nothing pushed back, no line half gathered, and
not at the end of input. That was already wrong before the button existed: a reset after the
input ran out left a console that had run out afterwards, so a machine could be restarted
once and then never typed at again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-29 11:05:16 -04:00

304 lines
13 KiB
C

// machine.c
// The SplitBit machine: everything both front ends share.
// Written by Anachronaut
#include "machine.h"
#include "rom.h"
#include "bootstrap.h"
#include "cpu.h"
#include "controller.h"
#include "io.h"
#include "video.h"
#include "utility.h"
#include "../Assembler/assembly.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// nanoseconds per second
#define NS_PER_SEC 1000000000LL
static inline long long timespec_diff_ns(struct timespec a, struct timespec b) {
return (a.tv_sec - b.tv_sec) * NS_PER_SEC + (a.tv_nsec - b.tv_nsec);
}
void cycle_timer_init(CycleTimer *t, long long cycles_per_sec) {
t->cycles_per_sec = cycles_per_sec;
t->accumulator_ns = 0;
clock_gettime(CLOCK_MONOTONIC, &t->prev);
}
// Call once per host frame. Returns how many SplitBit cycles to execute.
int cycle_timer_tick(CycleTimer *t) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long long elapsed = timespec_diff_ns(now, t->prev);
t->prev = now;
// optional: clamp to avoid spiral-of-death on hitches
if (elapsed > NS_PER_SEC / 10) elapsed = NS_PER_SEC / 10;
t->accumulator_ns += elapsed;
long long period_ns = NS_PER_SEC / t->cycles_per_sec;
int cycles = (int)(t->accumulator_ns / period_ns);
t->accumulator_ns %= period_ns;
return cycles;
}
// How many cycles to run between glances at the wall clock. In fast mode there is
// no clock to keep pace with, so run a large batch before looking up.
#define FAST_BATCH 65536
// Memory Banks. Static, because a front end has no business reaching into them: what it
// needs to know about the machine it asks the machine.
static uint8_t Program[0x10000], Data[0x10000];
// How the run is reported. The idle half is mentioned only when there is one, so that
// every program written before WAIT existed prints exactly the line it always did.
//
// THE TWO ARE NOT THE SAME KIND OF TIME. A bus cycle is the machine using memory; an idle
// cycle is the machine stopped in a WAIT while a device catches up. Added together they
// are elapsed time, which is what a cycle limit measures; told apart they say whether a
// program was working or waiting.
static void reportCycles(const CPURegisters *cpu, unsigned long cycleCount) {
if (cpu->idleCycles > 0) {
printf("Execution halted after %lu cycles, %lu of them waiting.\n",
cycleCount, cpu->idleCycles);
} else {
printf("Execution halted after %lu cycles.\n", cycleCount);
}
}
// ---- A keyboard made of a file ----
//
// THE CONSOLE BEHIND A WINDOW IS NOT THE CONSOLE BEHIND A TERMINAL, and until this existed
// the difference was untestable. A terminal does the line editing; a window has none, so the
// console does it itself - gathering a line, rubbing out a backspace, handing it over only
// when Return arrives. That is real logic, it broke twice in two days, and both times it was
// found by a person typing rather than by anything here.
//
// So a file can be a keyboard. It installs the same hook a window does, which means the same
// path runs, and the suite can check what happens when a backspace arrives with nobody to
// interpret it. It does not test the window - Voyager's own key queue is still beyond reach
// - but it tests the console, which is where the logic is.
static FILE *keyboardFile = NULL;
static int keyboardHook(int mayWait) {
(void)mayWait; // There is no window to keep alive, so both questions are the same.
if (keyboardFile == NULL) {
return CONSOLE_GONE;
}
const int byte = fgetc(keyboardFile);
if (byte == EOF) {
return CONSOLE_GONE;
}
// ---- A zero is a moment of nobody typing ----
//
// The commonest thing that happens behind a window is NOTHING: sixty times a second the
// console asks and is told to come back later, and everything that goes on while that is
// true - the clock advancing, a cursor blinking, a disk finishing - was unreachable from
// here, because a file always has another byte. A zero is a byte no keyboard sends, so it
// is free to mean the one thing a file otherwise cannot say.
if (byte == 0x00) {
return CONSOLE_NOTHING_YET;
}
return byte & 0xFF;
}
uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *programFile) {
m->options = *options;
m->programFile = programFile;
m->cycleCount = 0;
m->limitReached = 0;
m->restartFailed = 0;
// ---- Where the machine's first instruction comes from ----
//
// Named an image, it is placed into memory and started - which is what a debugger
// does, and is how every test here runs. That path is not a shortcut to apologise
// for: placing memory from outside is a real thing real machines allow.
//
// Named none, the machine starts the way hardware would: the ROM is shadowed into
// Program Memory and it reads the disk for the rest. There has to be a disk for that
// to mean anything, and no image and no disk is a machine with nothing to run.
if (programFile == NULL && options->disk == NULL) {
return MACHINE_NOTHING_TO_RUN;
}
if (programFile != NULL) {
if (loadFile(programFile, Program, Data)) {
fprintf(stderr, "Error: Couldn't read file: %s\n", programFile);
return MACHINE_ERROR;
}
} else if (loadROM(bootROM, bootROMBytes, Program, Data)) {
fprintf(stderr, "Error: The boot ROM is not a boot image.\n");
return MACHINE_ERROR;
}
if (options->disk != NULL && attachDisk(options->disk, options->writeProtect)) {
return MACHINE_ERROR;
}
// The screen starts blank, and starts blank again on a warm restart: video memory is
// the device's, and a reset that left last program's screen up would be a reset that
// did not happen.
videoReset();
consoleHome();
// The controller has to know where the memories are before anything can reach
// them through it. Banks 0 and 1 are those two arrays.
initializeController(Program, Data);
initializeCPU(&m->cpu, Program, Data);
if (m->options.debug) {
printRegisters(&m->cpu, Program, Data);
}
if (options->keyboard != NULL) {
keyboardFile = fopen(options->keyboard, "rb");
if (keyboardFile == NULL) {
fprintf(stderr, "Error: Couldn't read the keyboard file: %s\n", options->keyboard);
return MACHINE_ERROR;
}
consoleSetInputHook(keyboardHook);
}
setDiskLatency(m->options.diskCycles);
cycle_timer_init(&m->timer, CYCLE_RATE);
return MACHINE_OK;
}
int machineRunning(const Machine *m) {
return !(m->cpu.Status & STATUS_HALT) && !m->limitReached && !m->restartFailed;
}
void machineRunSlice(Machine *m) {
if (m->options.debug) {
// Wait before advancing, not after, so that a keypress is what moves the
// machine on rather than something that happens once it already has.
// Through the console rather than getchar, so that everything reading standard
// input reads it the same way and the console's pushback stays the only place
// a byte can be sitting.
consoleReadByte();
}
int cycles;
if (m->options.debug) {
// Debug mode advances one instruction per keypress, so the wall clock
// has no say in how many cycles to run.
cycles = 1;
} else if (m->options.fast) {
cycles = FAST_BATCH;
} else {
cycles = cycle_timer_tick(&m->timer);
}
// ---- Spending a budget of cycles, not running a count of instructions ----
//
// An instruction costs what it touches, so a batch is finished when the cycles are
// gone rather than after so many steps. In debug mode the budget is one, and any
// instruction costs at least the fetch of its own opcode, so one step still runs.
for (long spent = 0; spent < cycles; ) {
// Both kinds of cycle, because both are time passing. A step that waits
// spends no bus at all, and a budget measured only in bus cycles would never
// be spent - the machine would sit inside one batch forever and the device it
// was waiting for would never be given a moment to finish.
unsigned long before = m->cpu.busCycles + m->cpu.idleCycles;
stepCPU(&m->cpu);
unsigned long took = (m->cpu.busCycles + m->cpu.idleCycles) - before;
spent += (long)took;
m->cycleCount += took;
// Time has passed, so anything waiting on it may be finished.
deviceTick(m->cycleCount);
// ---- Starting over ----
//
// Between instructions, which is the only place it can happen: a device cannot
// restart the machine from inside the instruction that asked for it.
//
// WHAT A RESET REPEATS IS HOW THIS MACHINE STARTED. Named an image, it is
// placed again; named none, the ROM is shadowed again and reads the disk for
// the rest. Anything else would mean a reset changed what the machine is,
// which is the one thing a reset must not do.
//
// The disk is not unplugged and its image keeps everything written to it. That
// is what warm means: the machine starts again, the world it starts into does
// not.
if (takeResetRequest()) {
// The vector table goes, and that is a deliberate departure from leaving
// memory alone. A vector points into whatever installed it, and after this
// that program is not running - so a handler left behind would aim an
// interrupt at an address belonging to something gone. It is the argument
// CosmOS already makes when it takes a program's vectors back at exit.
memset(Program + SOFTWARE_VECTOR_BASE, 0,
(size_t)(0x10000 - SOFTWARE_VECTOR_BASE));
uint8_t failed = (m->programFile != NULL)
? loadFile(m->programFile, Program, Data)
: loadROM(bootROM, bootROMBytes, Program, Data);
if (failed) {
fprintf(stderr, "Error: The machine could not be started again.\n");
m->restartFailed = 1;
return;
}
videoReset();
consoleHome();
consoleResetInput();
initializeCPU(&m->cpu, Program, Data);
break; // Out of this batch; the loop above carries on with a new CPU.
}
if (m->cpu.Status & STATUS_HALT) {
// We've halted.
break;
}
if (m->options.cycles && m->cycleCount >= m->options.cycles) {
m->limitReached = 1;
break;
}
}
if (m->options.debug) {
printRegisters(&m->cpu, Program, Data);
printf("Cycle: %lu\n", m->cycleCount);
}
}
void machineStop(Machine *m) {
// ---- Saving the screen ----
//
// Written when the machine stops, and it is what makes the screen testable at all: a
// suite has no display, so the only way to check what was drawn is to be handed it. A
// picture out of a headless run is also the quickest way for a person to see what a
// program actually put on the screen without sitting and watching it happen.
if (m->options.screen != NULL) {
videoWriteImage(m->options.screen);
}
if (keyboardFile != NULL) {
consoleSetInputHook(NULL);
fclose(keyboardFile);
keyboardFile = NULL;
}
detachDisk();
}
int machineReport(const Machine *m) {
if (m->restartFailed) {
return 1;
}
if (m->limitReached) {
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", m->cycleCount);
} else if (m->cpu.Status & STATUS_FAULT) {
// The Program Counter is still pointing at whatever the CPU could not get past.
reportCycles(&m->cpu, m->cycleCount);
if (m->cpu.Fault == FAULT_NO_HANDLER) {
fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n",
m->cpu.FaultVector, m->cpu.ProgramCounter);
} else if (m->cpu.Fault == FAULT_DEVICE_REFUSED) {
fprintf(stderr, "Fault: The device on port %u refused the access at Program Address 0x%04X, and nothing is installed to deal with it.\n",
m->cpu.FaultVector, m->cpu.ProgramCounter);
} else if (m->cpu.Fault == FAULT_NO_DEVICE_HANDLER) {
fprintf(stderr, "Fault: The device on port %u interrupted at Program Address 0x%04X, and hardware vector %u has no handler installed.\n",
m->cpu.FaultVector, m->cpu.ProgramCounter, m->cpu.FaultVector);
} else {
fprintf(stderr, "Fault: 0x%02X at Program Address 0x%04X is not an instruction.\n",
Program[m->cpu.ProgramCounter], m->cpu.ProgramCounter);
}
return 1;
} else {
reportCycles(&m->cpu, m->cycleCount);
}
return 0;
}