Split the machine from its front end, and add Voyager

The Segan Voyager is the same SplitBit with a screen and a speaker instead of a terminal,
and this is the rung that makes there be two of them at all. Everything that is actually
the machine - the CPU, the controller, the devices, the run loop, the reporting - moves to
machine.c, and each front end brings one file of its own. emulator.c is now sixty lines of
argument handling and a three line loop.

The machine runs in SLICES rather than to completion, because that is the cut a window
needs: run a slice, present a frame, run another. A terminal runs slices until the machine
stops. Both loops are three lines, which is why the cut is there rather than anywhere else.

At this stage Voyager's window is empty. There is no video device yet and inventing a
temporary way to draw would mean building something to throw away.

PLAIN MAKE STILL WORKS WITH NO GRAPHICS LIBRARY. Raylib is probed by compiling and linking
against it rather than by looking for a file, because a header with no library behind it
passes a file check and then fails at link time. Where it is missing, make says so once and
builds everything else - the machine, the assembler, the disk tool, the linter and the whole
suite. A project about a small understandable CPU should not need OpenGL to run its tests.
That nearly broke here: make strict globs Source/Emulator/*.c, so it would have tried to
compile voyager.c and failed on precisely the machines the split exists to support, and this
machine has Raylib so nothing would have caught it.

Tests/voyager.sh runs the WHOLE MANIFEST through Voyager and holds it to the recorded
results SplitBit is held to. Not that the two look alike: that one satisfies every recording
the other does, byte for byte, exit status included. It reuses run.sh, which now takes the
machine from SPLITBIT_EMULATOR, rather than keeping a second copy of the runner that would
drift. Voyager not being built is not a failure - it says so and passes.

Verified both ways. Made Voyager print one extra line, and 114 of 165 failed: exactly the
tests that run the emulator, with the 51 assemble-only and xfail cases correctly untouched.
Removed the binary, and the script skipped. Built with HAVE_RAYLIB=no, and everything else
still built and checked clean.

--headless is taken out of the arguments in voyager.c rather than in the shared parser,
which should not learn about a window only one binary has. It exists so the suite can run
this binary at all: a front end that could only be exercised by a person looking at it would
be a front end nothing checks.

loadFile takes a const char * now, which it always should have.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-28 21:57:09 -04:00
co-authored by Claude Opus 5
parent 4c3eac8d9c
commit e3ef25e3b3
14 changed files with 628 additions and 243 deletions
+1 -1
View File
@@ -178,7 +178,7 @@ uint8_t loadROM(const unsigned char *bytes, unsigned long length,
return failed;
}
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
uint8_t loadFile(const char *path, uint8_t *Program, uint8_t *Data) {
FILE *file = fopen(path, "rb");
if (file == NULL) {
fprintf(stderr, "Error: Couldn't open file: %s\n", path);
+1 -1
View File
@@ -8,7 +8,7 @@
#include <stdint.h>
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data);
uint8_t loadFile(const char *path, uint8_t *Program, uint8_t *Data);
// The same, from bytes the emulator carries rather than a file it opens. See loadROM.
uint8_t loadROM(const unsigned char *bytes, unsigned long length,
+26 -218
View File
@@ -4,87 +4,23 @@
// Small 8-Bit Harvard Architecture CPU
// Written by Anachronaut
// 10/15/2024
#include "rom.h"
#include "bootstrap.h"
#include "../Assembler/assembly.h"
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include "cpu.h"
#include "controller.h"
#include "io.h"
#include "utility.h"
#include <string.h>
#include <getopt.h>
#include <time.h>
// nanoseconds per second
#define NS_PER_SEC 1000000000LL
#define CYCLE_RATE 1000000
typedef struct {
long long cycles_per_sec; // e.g. 1000000 for 1 MHz
long long accumulator_ns; // unspent nanoseconds
struct timespec prev;
} CycleTimer;
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
unsigned long cycleCount = 0;
char *programFile = NULL;
// Memory Banks:
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);
}
}
// ---- The machine with a terminal attached ----
//
// This is a front end and nothing else. The machine itself is in machine.c, shared with
// Voyager, which is the same machine with a screen and a speaker instead of a terminal.
//
// Keeping this file small is the point rather than a side effect: anything that ends up
// here is behaviour Voyager does not have, and the two are supposed to differ only in
// what they present. This builds and runs anywhere, with no graphics library, which is
// what keeps the whole toolchain and the whole suite dependency free.
int main (int argc, char *argv[]) {
#include "machine.h"
#include "utility.h"
#include <stdio.h>
#include <getopt.h>
int main(int argc, char *argv[]) {
EmulatorOptions options;
uint8_t result = parseOptions(argc, argv, &options);
if (result == OPTIONS_HELP) {
@@ -94,6 +30,7 @@ int main (int argc, char *argv[]) {
// Bad command line, don't execute.
return 1;
}
char *programFile = NULL;
if (optind < argc) {
programFile = argv[optind];
optind++;
@@ -102,150 +39,21 @@ int main (int argc, char *argv[]) {
fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]);
return 1;
}
// ---- 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) {
Machine machine;
uint8_t started = machineStart(&machine, &options, programFile);
if (started == MACHINE_NOTHING_TO_RUN) {
fprintf(stderr, "Error: No boot image and no disk, so there is nothing to run.\n");
printHelp(argv[0]);
return 1;
}
if (programFile != NULL) {
if (loadFile(programFile, Program, Data)) {
fprintf(stderr, "Error: Couldn't read file: %s\n", programFile);
return 1;
}
} else if (loadROM(bootROM, bootROMBytes, Program, Data)) {
fprintf(stderr, "Error: The boot ROM is not a boot image.\n");
} else if (started != MACHINE_OK) {
return 1;
}
if (options.disk != NULL && attachDisk(options.disk, options.writeProtect)) {
return 1;
}
CPURegisters cpu;
// 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(&cpu, Program, Data);
if(options.debug) {
printRegisters(&cpu, Program, Data);
while (machineRunning(&machine)) {
machineRunSlice(&machine);
}
CycleTimer timer;
setDiskLatency(options.diskCycles);
cycle_timer_init(&timer, CYCLE_RATE);
uint8_t limitReached = 0;
while (!(cpu.Status & STATUS_HALT) && !limitReached) {
if (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 (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 (options.fast) {
cycles = FAST_BATCH;
} else {
cycles = cycle_timer_tick(&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 = cpu.busCycles + cpu.idleCycles;
stepCPU(&cpu);
unsigned long took = (cpu.busCycles + cpu.idleCycles) - before;
spent += (long)took;
cycleCount += took;
// Time has passed, so anything waiting on it may be finished.
deviceTick(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 = (programFile != NULL)
? loadFile(programFile, Program, Data)
: loadROM(bootROM, bootROMBytes, Program, Data);
if (failed) {
fprintf(stderr, "Error: The machine could not be started again.\n");
return 1;
}
initializeCPU(&cpu, Program, Data);
break; // Out of this batch; the loop above carries on with a new CPU.
}
if (cpu.Status & STATUS_HALT) {
// We've halted.
break;
}
if (options.cycles && cycleCount >= options.cycles) {
limitReached = 1;
break;
}
}
if (options.debug) {
printRegisters(&cpu, Program, Data);
printf("Cycle: %lu\n", cycleCount);
}
}
detachDisk();
if (limitReached) {
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
} else if (cpu.Status & STATUS_FAULT) {
// The Program Counter is still pointing at whatever the CPU could not get past.
reportCycles(&cpu, cycleCount);
if (cpu.Fault == FAULT_NO_HANDLER) {
fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n",
cpu.FaultVector, cpu.ProgramCounter);
} else if (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",
cpu.FaultVector, cpu.ProgramCounter);
} else if (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",
cpu.FaultVector, cpu.ProgramCounter, cpu.FaultVector);
} else {
fprintf(stderr, "Fault: 0x%02X at Program Address 0x%04X is not an instruction.\n",
Program[cpu.ProgramCounter], cpu.ProgramCounter);
}
return 1;
} else {
reportCycles(&cpu, cycleCount);
}
return 0;
machineStop(&machine);
return machineReport(&machine);
}
+237
View File
@@ -0,0 +1,237 @@
// 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 "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);
}
}
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 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);
}
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;
}
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) {
(void)m;
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;
}
+73
View File
@@ -0,0 +1,73 @@
// machine.h
// The SplitBit machine, with nothing attached to look at it.
// Written by Anachronaut
#ifndef MACHINE_H
#define MACHINE_H
#include <stdint.h>
#include <time.h>
#include "cpu.h"
#include "utility.h"
// ---- Why the machine is not a main ----
//
// There are two front ends: SplitBit, which is a terminal, and Voyager, which is a window
// and a speaker. THEY ARE THE SAME MACHINE. Keeping it here means the difference between
// them is presentation and nothing else - neither can quietly acquire behaviour the other
// lacks, and the suite can hold them to it by running a program through both and comparing
// what came out.
//
// It also decides where the devices live. A video device that only existed in the windowed
// binary would be untestable, because the suite has no display; here it is part of the
// machine, advances on emulated cycles, and Raylib only presents what it already produced.
// How fast the machine runs when it is not being told to hurry.
#define CYCLE_RATE 1000000
typedef struct {
long long cycles_per_sec;
long long accumulator_ns;
struct timespec prev;
} CycleTimer;
void cycle_timer_init(CycleTimer *t, long long cycles_per_sec);
// Call once per host frame. Returns how many SplitBit cycles to execute.
int cycle_timer_tick(CycleTimer *t);
typedef struct {
CPURegisters cpu;
CycleTimer timer;
unsigned long cycleCount;
uint8_t limitReached;
uint8_t restartFailed;
const char *programFile;
EmulatorOptions options;
} Machine;
#define MACHINE_OK 0
#define MACHINE_ERROR 1
// No image and no disk. Told apart from an ordinary failure so that the front end can
// print its own help, which is the one thing here that knows what it is called.
#define MACHINE_NOTHING_TO_RUN 2
uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *programFile);
// Whether there is any more running to do. False once the machine has halted, faulted, or
// spent the cycle limit it was given.
int machineRunning(const Machine *m);
// ---- One slice, not the whole run ----
//
// A front end with a window has to get a turn: run a slice, present a frame, run another.
// A terminal simply runs slices until the machine stops. Both loops are three lines, which
// is the point of cutting it here rather than anywhere else.
void machineRunSlice(Machine *m);
void machineStop(Machine *m);
// Says how the run went and returns what the process should exit with.
int machineReport(const Machine *m);
#endif // MACHINE_H
+1 -1
View File
@@ -28,7 +28,7 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options);
void printHelp(const char *programName);
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data);
uint8_t loadFile(const char *path, uint8_t *Program, uint8_t *Data);
void bootStrap(uint8_t *Program, uint8_t *Data);
+123
View File
@@ -0,0 +1,123 @@
// voyager.c
// The Segan Voyager
// A SplitBit with a screen and a speaker attached
// Written by Anachronaut
//
// ---- What this is ----
//
// The same machine SplitBit runs, presented through a window instead of a terminal. Every
// instruction, every device and every cycle is in machine.c and shared; this file opens a
// window, gives the machine a slice of time per frame, and shows what came out.
//
// THAT ORDER MATTERS AND IS THE WHOLE DESIGN. The devices belong to the machine and advance
// on emulated cycles, so the same program produces the same frames and the same samples
// whether or not anybody is looking. Raylib presents; it does not decide. Which is what
// lets a test suite with no display hold this binary to the same behaviour as the other
// one.
//
// At this stage the window is empty. There is no video device yet, and inventing a
// temporary way to draw would mean building something to throw away.
#include "machine.h"
#include "utility.h"
#include "raylib.h"
#include <stdio.h>
#include <string.h>
#include <getopt.h>
// The screen the Voyager will have, scaled up because a 320 by 200 window is a postage
// stamp on a modern display. Both numbers are provisional until the video device decides
// them for real.
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 200
#define SCREEN_SCALE 3
// ---- Running without a window ----
//
// Taken out of the arguments here rather than in the shared parser, because it is a fact
// about this front end and the shared parser should not learn about a window that only one
// binary has. Everything else on the command line means exactly what it means to SplitBit.
//
// It exists so the suite can run this binary at all: a test machine has no display, and a
// front end that could only be exercised by a person looking at it would be a front end
// nothing checks. Headless, Voyager must print byte for byte what SplitBit prints, and
// Tests/voyager.sh holds it to that.
static int takeHeadless(int *argc, char *argv[]) {
int headless = 0;
int out = 0;
for (int i = 0; i < *argc; i++) {
if (strcmp(argv[i], "--headless") == 0) {
headless = 1;
continue;
}
argv[out++] = argv[i];
}
argv[out] = NULL;
*argc = out;
return headless;
}
int main(int argc, char *argv[]) {
int headless = takeHeadless(&argc, argv);
EmulatorOptions options;
uint8_t result = parseOptions(argc, argv, &options);
if (result == OPTIONS_HELP) {
printf(" --headless Run with no window, which is how the tests run it.\n");
return 0;
} else if (result == OPTIONS_ERROR) {
return 1;
}
char *programFile = NULL;
if (optind < argc) {
programFile = argv[optind];
optind++;
}
if (optind < argc) {
fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]);
return 1;
}
Machine machine;
uint8_t started = machineStart(&machine, &options, programFile);
if (started == MACHINE_NOTHING_TO_RUN) {
fprintf(stderr, "Error: No boot image and no disk, so there is nothing to run.\n");
printHelp(argv[0]);
return 1;
} else if (started != MACHINE_OK) {
return 1;
}
if (headless) {
// The same three lines SplitBit runs, and deliberately so: a headless Voyager is
// not a reduced machine, it is the machine with nobody watching.
while (machineRunning(&machine)) {
machineRunSlice(&machine);
}
} else {
InitWindow(SCREEN_WIDTH * SCREEN_SCALE, SCREEN_HEIGHT * SCREEN_SCALE,
"Segan Voyager");
SetTargetFPS(60);
// ---- A slice a frame ----
//
// The machine gets its turn, then the window gets its turn. Closing the window
// stops the machine, and the machine halting leaves the window up so that whatever
// it drew is still there to look at - a program that ends should not take its
// output off the screen with it.
while (!WindowShouldClose()) {
if (machineRunning(&machine)) {
machineRunSlice(&machine);
}
BeginDrawing();
// Not black. A screen with nothing driving it should look like a screen that
// is on, rather than like a window that failed to open.
ClearBackground((Color){ 18, 22, 20, 255 });
EndDrawing();
}
CloseWindow();
}
machineStop(&machine);
return machineReport(&machine);
}