// 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 "pad.h" #include "video.h" #include "sound.h" #include "utility.h" #include "../Assembler/assembly.h" #include #include #include // 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; } // ---- Starting over ---- // // 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. // // 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. static int machineRestart(Machine *m) { 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 0; } videoReset(); padReset(); soundReset(); timerReset(); consoleHome(); consoleResetInput(); // ---- And every line down ---- // // The same reasoning that clears the vector table. A handler left behind would aim an // interrupt into a program that is no longer running; a line left behind arrives at one // that never asked the device for anything. The devices reset above take their own down, // and this is the rest of them - the disk in particular, which is not unplugged by a // reset and keeps whatever it was doing. clearAllInterrupts(); initializeCPU(&m->cpu, Program, Data); // A machine that had stopped is running again, which is the entire point of asking from // outside: the interesting time to restart something is when it is not going anywhere. m->limitReached = 0; return 1; } int machineTakeReset(Machine *m) { if (!takeResetRequest()) { return 0; } return machineRestart(m); } 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; } // Every drive named, in the order it was named. Write protection is the machine's rather // than a drive's for now: a tab on one floppy and not another is a thing to add when // somebody wants it, and pretending otherwise here would be a promise the option cannot // keep. for (int at = 0; at < options->diskCount; at++) { if (attachDisk(options->disks[at], options->writeProtect)) { return MACHINE_ERROR; } } // After the images, so the drive numbers a command line asks for are the order it asks // in. A disk made of memory is still a drive and still has to be brought up by whatever // system is running; the machine only supplies the blocks. if (options->ramDisk > 0 && attachRamDisk((uint32_t)options->ramDisk)) { 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(); padReset(); soundReset(); timerReset(); if (options->sound != NULL) { soundKeepSamples(); } 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(machineController(), Program, Data); initializeCPU(&m->cpu, Program, Data); if (m->options.debug) { printRegisters(&m->cpu, Program, Data); } // ---- The pads, from files ---- // // Opened here beside the keyboard because they are the same kind of thing: a recording // standing in for a person, so that what a person would exercise is reachable from a // suite. They are never closed, for the same reason the keyboard is not - the machine // outlives the call and the host reclaims them when it stops. for (int n = 0; n < options->padCount; n++) { FILE *pad = fopen(options->pads[n], "rb"); if (pad == NULL) { fprintf(stderr, "Error: Couldn't open pad file: %s\n", options->pads[n]); return 0; } padFromFile(n, pad); } for (int n = 0; n < options->padRecordCount; n++) { FILE *pad = fopen(options->padRecord[n], "wb"); if (pad == NULL) { fprintf(stderr, "Error: Couldn't write pad file: %s\n", options->padRecord[n]); return 0; } padRecordTo(n, pad); } 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 (machineTakeReset(m)) { 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); } // Every sample the machine made, for the same reason a picture is saved: there is no // speaker on a machine running tests, and a sound nothing can hear is a sound nothing // can check. if (m->options.sound != NULL) { soundWriteSamples(m->options.sound); } 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; }