Files
SplitBit-Emulator/Source/Emulator/utility.c
T
AnachronautandClaude Opus 5 a163c670d0 A demo recorder: --record-pad writes what --pad reads
One byte a frame, in exactly the format the player takes, so a recording
needs no conversion and there is no second format to keep in step. That
symmetry is the feature, and it makes the strongest form of the claim
testable: a recording is made OF a playback, and the bytes coming out have
to be the bytes that went in.

It exists because some inputs cannot sensibly be written by hand. Flying a
lander from one base to another is a few hundred frames of steering that
has to arrive somewhere eight cells wide, and several attempts at
authoring one got within two columns and no closer. That is a piloting
exercise rather than a test. Playing it once and keeping what happened is
the answer.

A BYTE FOR EVERY FRAME, written inside the loop that advances the
recordings rather than after it, so a machine that jumped several frames
at once still writes one for each. A recording is a timeline: one that
skipped the frames nobody looked at would play back faster than it was
flown.

What is recorded is what the DEVICE WOULD REPORT, not the live state - a
recording of a playback that wrote the live state would be a file of
noughts. And it is flushed as it goes, because a recording is usually
stopped by whoever is playing rather than by the program ending, and a
demo lost to a buffer is a demo flown twice.

Tests/replay.sh is where this and whatever follows it are checked. Twelve
scripts now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 12:38:43 -04:00

195 lines
9.1 KiB
C

// utility.c
// Utilities for the SplitBit CPU Emulator
// Written by Anachronaut
// 10/15/2024
#include "utility.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include "../Assembler/assembly.h"
void printHelp(const char *programName) {
printf("Usage: %s [OPTIONS] [boot image]\n", programName);
printf("\n");
printf("Named an image, it is placed into memory and started, which is what a\n");
printf("debugger does and how the test suite runs. Given only a disk, the machine\n");
printf("starts the way hardware would: the built in ROM is shadowed into Program\n");
printf("Memory, and it reads the disk for everything else.\n");
printf("\n");
printf("Options:\n");
printf(" -d, --debug Enable debug mode.\n");
printf(" -c, --cycles N Stop after N cycles instead of running until the program halts.\n");
printf(" -f, --fast Run as fast as possible, ignoring the emulated cycle rate.\n");
printf(" -D, --disk FILE Attach a disk image, making one if it is not there.\n");
printf(" -L, --disk-cycles N How many cycles a block read or write takes. Zero, the\n");
printf(" default, finishes before the next instruction starts.\n");
printf(" -W, --write-protect Attach the disk read only. A disk the host will not let\n");
printf(" you write is read only whether you ask for this or not.\n");
printf(" -S, --screen FILE Save a picture of the screen, as a PPM, when the machine\n");
printf(" stops. Works with or without a window, which is how the\n");
printf(" tests look at a screen on a host that has no display.\n");
printf(" -K, --keyboard FILE Feed the console from this file as though it were a\n");
printf(" keyboard rather than a terminal. Which means the console does\n");
printf(" its own line editing, the way it must when a window is open\n");
printf(" and there is no terminal behind it to do it.\n");
printf(" -P, --pad FILE Hold a controller from a file, one byte a frame. Given\n");
printf(" again for the next pad. A byte is the buttons held:\n");
printf(" 1 right, 2 left, 4 down, 8 up, 16 A, 32 B, 64 start,\n");
printf(" 128 select.\n");
printf(" -Y, --record-pad FILE\n");
printf(" Write what a controller held, one byte a frame, in the\n");
printf(" format --pad reads. Given again for the next pad. Play a\n");
printf(" thing once and keep what happened.\n");
printf(" -N, --sound FILE Save every sample the machine made, as raw signed 16 bit\n");
printf(" at 48kHz. What --screen is for a picture: the only way to\n");
printf(" check a sound on a machine with no speaker.\n");
printf(" -h, --help Display this help message.\n");
}
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
static struct option long_options[] = {
{"debug", no_argument, 0, 'd'},
{"cycles", required_argument, 0, 'c'},
{"fast", no_argument, 0, 'f'},
{"disk", required_argument, 0, 'D'},
{"ram-disk", required_argument, 0, 'R'},
{"write-protect", no_argument, 0, 'W'},
{"disk-cycles", required_argument, 0, 'L'},
{"screen", required_argument, 0, 'S'},
{"keyboard", required_argument, 0, 'K'},
{"sound", required_argument, 0, 'N'},
{"pad", required_argument, 0, 'P'},
{"record-pad", required_argument, 0, 'Y'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0 }
};
int opt;
int option_index = 0;
// ---- Everything off, in one line rather than nine ----
//
// This was a list of assignments, one per field, and a list beside a struct drifts from
// the struct: adding `disks` and `diskCount` left them holding whatever was on the stack,
// so a machine given one disk was told it already had four drives. The same struct
// growing a field once before left Voyager linking against an object that disagreed
// about its size.
//
// Every default here is nought or nothing, and a default that is not can be written
// below this line where it will be read as the exception it is.
*options = (EmulatorOptions){0};
// Parse options
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:N:R:P:Y:", long_options, &option_index)) != -1) {
switch (opt) {
case 'd':
options->debug = 1;
break;
case 'c': {
// The count has to be a plain positive number. Anything else is
// almost certainly a mistyped command line rather than a request
// to run zero cycles.
char *end;
long value = strtol(optarg, &end, 10);
if (*end != '\0' || value <= 0) {
fprintf(stderr, "Error: --cycles needs a positive number, not \"%s\".\n", optarg);
return OPTIONS_ERROR;
}
options->cycles = (unsigned long)value;
}
break;
case 'f':
options->fast = 1;
break;
case 'D':
// Each one is the next drive. The first is also left in `disk`, because a
// machine with one disk is what almost every caller means and reading it
// that way keeps them all unchanged.
if (options->diskCount >= DISK_DRIVE_COUNT) {
fprintf(stderr, "Error: This machine has %d drives.\n",
DISK_DRIVE_COUNT);
return 1;
}
options->disks[options->diskCount++] = optarg;
if (options->disk == NULL) {
options->disk = optarg;
}
break;
case 'R': {
char *end;
const unsigned long blocks = strtoul(optarg, &end, 10);
if (*optarg == '\0' || *end != '\0' || blocks == 0) {
fprintf(stderr, "Error: --ram-disk wants a number of blocks.\n");
return 1;
}
options->ramDisk = blocks;
}
break;
case 'W':
options->writeProtect = 1;
break;
case 'L':
options->diskCycles = strtoul(optarg, NULL, 0);
break;
case 'S':
options->screen = optarg;
break;
case 'K':
options->keyboard = optarg;
break;
case 'N':
options->sound = optarg;
break;
case 'Y':
if (options->padRecordCount < PAD_DRIVE_COUNT) {
options->padRecord[options->padRecordCount++] = optarg;
}
break;
case 'P':
// Fills the pads in turn, the same way --disk fills the drives, so the first
// one named is pad nought and the machine has as many as were asked for.
if (options->padCount < PAD_DRIVE_COUNT) {
options->pads[options->padCount++] = optarg;
}
break;
case 'h':
printHelp(argv[0]);
return OPTIONS_HELP;
default:
printHelp(argv[0]);
return OPTIONS_ERROR;
}
}
return OPTIONS_OK;
}
// Writes a byte out as eight binary digits, most significant first. printf's %b is
// a recent addition to C and not available everywhere, so this does it by hand.
// The buffer must have room for nine characters.
static void formatBinary(uint8_t value, char *out) {
for (int i = 0; i < 8; i++) {
out[i] = (value & (0x80 >> i)) ? '1' : '0';
}
out[8] = '\0';
}
void printRegisters(CPURegisters *cpu, uint8_t *Program, uint8_t *Data) {
char status[9];
formatBinary(cpu->Status, status);
printf("***** CPU Registers *****\n");
printf("A: 0x%02X\tB: 0x%02X\tQ: 0x%02X\tStatus: 0b%s\n", cpu->A, cpu->B, cpu->Q, status);
printf("Program Counter: 0x%04X Current Instruction: 0x%02X (%s)\n", cpu->ProgramCounter, Program[cpu->ProgramCounter],getMnemonic(Program[cpu->ProgramCounter]));
for (int i = 0; i < DATA_POINTERS; i++) {
printf(" Data Pointer %d: 0x%04X Current Data Value: 0x%02X%s\n",
i, cpu->DataPointer[i], Data[cpu->DataPointer[i]],
i >= PRESERVED_DATA_POINTERS ? " (volatile)" : "");
}
// The two casts keep these inside Data Memory. The Stack Pointer starts at the
// very top, so without them the display would read off the end of the array
// before a single byte has been pushed.
printf(" Stack Pointer: 0x%04X Current Value: (0x%02X) (0x%02X)\n", cpu->StackPointer,
Data[(uint16_t)(cpu->StackPointer + 1)], Data[(uint16_t)(cpu->StackPointer + 2)]);
}