diff --git a/README.md b/README.md index 8f42c39..781997d 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,25 @@ Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, o | --- | --- | | `-d`, `--debug` | Single step through cycles. Each key press advances one instruction. | | `-c`, `--cycles N` | Stop after N cycles rather than running until the program halts. Useful for programs that never halt, and for getting the same output from a run every time. | + +**A cycle is one access to memory**, not one instruction. Fetching an opcode is a cycle, +fetching each byte after it is another, reading or writing Data Memory is one, every byte a +CALL pushes or a RET pops is one, and reaching a device port is one. Nothing overlaps - +there is no fetching the next instruction while this one finishes - so the count is simply +how many times the machine used the bus. + +That makes the numbers describe something buildable. `RSTA` costs 1 and `SETD` costs 4, +because one is a byte and the other is four. `CALL` and `RET` together cost 24 and `RCAL` +and `RRET` cost 8, because the first pair moves twenty bytes of Stack and the second moves +four. Counting instructions said those were the same, which is not true of any machine +anybody could build - and it is the emulator's job to be the thing the hardware is designed +against. + +The average SplitBit instruction costs 3.72 cycles, measured over the native assembler +assembling a program. Whether real hardware would overlap a fetch with the end of the +previous instruction is left open, and deliberately: this is the conservative model, and +pipelining is a decision to make while drawing the hardware rather than one to inherit from +an emulator. | `-f`, `--fast` | Run as fast as the host allows, ignoring the emulated cycle rate. | | `-D`, `--disk ` | Attach a disk image, creating a 128K one if the file is not there. | | `-W`, `--write-protect` | Attach the disk read only. A disk whose image the host will not let you write is read only whether you ask for this or not. | diff --git a/Source/Emulator/cpu.c b/Source/Emulator/cpu.c index 01a3ba7..27f6f59 100644 --- a/Source/Emulator/cpu.c +++ b/Source/Emulator/cpu.c @@ -11,6 +11,40 @@ uint16_t shiftRegister; // Reads one entry out of a vector table. Most significant byte first, matching the // branch instructions and both file formats. +// ---- Every touch of memory, and what it costs ---- +// +// One access, one cycle. These exist so that the cost is counted in the one place the +// access happens, rather than in a table of per instruction costs kept somewhere else - +// a table like that is a second copy of what the code does, and the two drift. +static inline uint8_t fetchProgram(CPURegisters *cpu, uint16_t at) { + cpu->busCycles++; + return cpu->Program[at]; +} + +static inline uint8_t readData(CPURegisters *cpu, uint16_t at) { + cpu->busCycles++; + return cpu->Data[at]; +} + +static inline void writeData(CPURegisters *cpu, uint16_t at, uint8_t value) { + cpu->busCycles++; + cpu->Data[at] = value; +} + +// A device port is reached over the same bus as memory, so it costs the same. That is a +// claim about the hardware rather than an observation - a machine could put devices +// somewhere faster or slower - and it is the simple thing until there is a reason to say +// otherwise. +static inline void portOut(CPURegisters *cpu, uint8_t value, uint8_t port) { + cpu->busCycles++; + OutputHandler(value, port); +} + +static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) { + cpu->busCycles++; + return InputHandler(port); +} + static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t index) { uint16_t address = base + (uint16_t)index * VECTOR_ENTRY_BYTES; return ((uint16_t)programMemory[address] << 8) | (uint16_t)programMemory[address + 1]; @@ -38,23 +72,23 @@ static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, u } // Order mirrors genericCall exactly: low byte then high byte, lowest numbered Data // Pointer first, so that anything walking the Stack sees a familiar shape. - cpu->Data[cpu->StackPointer] = resumeAddress & 0xFF; + writeData(cpu, cpu->StackPointer, resumeAddress & 0xFF); cpu->StackPointer--; - cpu->Data[cpu->StackPointer] = (resumeAddress >> 8) & 0xFF; + writeData(cpu, cpu->StackPointer, (resumeAddress >> 8) & 0xFF); cpu->StackPointer--; for (int i = 0; i < DATA_POINTERS; i++) { - cpu->Data[cpu->StackPointer] = cpu->DataPointer[i] & 0xFF; + writeData(cpu, cpu->StackPointer, cpu->DataPointer[i] & 0xFF); cpu->StackPointer--; - cpu->Data[cpu->StackPointer] = (cpu->DataPointer[i] >> 8) & 0xFF; + writeData(cpu, cpu->StackPointer, (cpu->DataPointer[i] >> 8) & 0xFF); cpu->StackPointer--; } - cpu->Data[cpu->StackPointer] = cpu->B; + writeData(cpu, cpu->StackPointer, cpu->B); cpu->StackPointer--; - cpu->Data[cpu->StackPointer] = cpu->A; + writeData(cpu, cpu->StackPointer, cpu->A); cpu->StackPointer--; - cpu->Data[cpu->StackPointer] = cpu->Q; + writeData(cpu, cpu->StackPointer, cpu->Q); cpu->StackPointer--; - cpu->Data[cpu->StackPointer] = cpu->Status; + writeData(cpu, cpu->StackPointer, cpu->Status); cpu->StackPointer--; // A handler runs with hardware interrupts held off unless it says otherwise, so an // interrupt cannot arrive inside the handler for another one and grow the Stack @@ -112,33 +146,33 @@ void genericBranch(CPURegisters *cpu){ // Byte order is imporant. Most Significant first, then Least Significant. cpu->ProgramCounter++; // Move to the next byte. (MSB) uint16_t DestinationAddress; - DestinationAddress = (uint16_t)cpu->Program[cpu->ProgramCounter] << 8; // Cast the 8 bit value to a 16 bit value and shifts it up to the high byte. + DestinationAddress = (uint16_t)fetchProgram(cpu, cpu->ProgramCounter) << 8; // Cast the 8 bit value to a 16 bit value and shifts it up to the high byte. cpu->ProgramCounter++; // Move to the next byte. (LSB) - DestinationAddress = DestinationAddress | (uint16_t)cpu->Program[cpu->ProgramCounter]; // Cast the 8 bit value to a 16 bit value and or it to add it to the desination. + DestinationAddress = DestinationAddress | (uint16_t)fetchProgram(cpu, cpu->ProgramCounter); // Cast the 8 bit value to a 16 bit value and or it to add it to the desination. cpu->ProgramCounter = DestinationAddress-1; } void genericCall(CPURegisters *cpu){ // Order, low byte, high byte - cpu->Data[cpu->StackPointer] = cpu->ProgramCounter & 0xFF; + writeData(cpu, cpu->StackPointer, cpu->ProgramCounter & 0xFF); cpu->StackPointer--; - cpu->Data[cpu->StackPointer] = (cpu->ProgramCounter >> 8) & 0xFF; + writeData(cpu, cpu->StackPointer, (cpu->ProgramCounter >> 8) & 0xFF); cpu->StackPointer--; // Push the preserved Data Pointers to the Stack, lowest numbered first. // Order within each one, low byte, high byte. // The pointers above PRESERVED_DATA_POINTERS are deliberately left alone, so a // subroutine can use one to hand an address back to whoever called it. for (int i = 0; i < PRESERVED_DATA_POINTERS; i++) { - cpu->Data[cpu->StackPointer] = cpu->DataPointer[i] & 0xFF; + writeData(cpu, cpu->StackPointer, cpu->DataPointer[i] & 0xFF); cpu->StackPointer--; - cpu->Data[cpu->StackPointer] = (cpu->DataPointer[i] >> 8) & 0xFF; + writeData(cpu, cpu->StackPointer, (cpu->DataPointer[i] >> 8) & 0xFF); cpu->StackPointer--; } // Push B to the Stack. - cpu->Data[cpu->StackPointer] = cpu->B; + writeData(cpu, cpu->StackPointer, cpu->B); cpu->StackPointer--; // Push A to the Stack. - cpu->Data[cpu->StackPointer] = cpu->A; + writeData(cpu, cpu->StackPointer, cpu->A); cpu->StackPointer--; // Perform a Generic Branch to the Address. genericBranch(cpu); @@ -150,7 +184,7 @@ uint16_t *selectDataPointer(CPURegisters *cpu) { // rather than rejected, the way a narrow field in hardware would be. It is the // assembler's job to refuse to emit one in the first place. cpu->ProgramCounter++; - return &cpu->DataPointer[cpu->Program[cpu->ProgramCounter] & (DATA_POINTERS - 1)]; + return &cpu->DataPointer[fetchProgram(cpu, cpu->ProgramCounter) & (DATA_POINTERS - 1)]; } uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { @@ -278,9 +312,9 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // It must be returned from with RRET. The two frames are different sizes, so // returning from one through the other walks the Stack to somewhere that was // never a return address. - cpu->Data[cpu->StackPointer] = cpu->ProgramCounter & 0xFF; + writeData(cpu, cpu->StackPointer, cpu->ProgramCounter & 0xFF); cpu->StackPointer--; - cpu->Data[cpu->StackPointer] = (cpu->ProgramCounter >> 8) & 0xFF; + writeData(cpu, cpu->StackPointer, (cpu->ProgramCounter >> 8) & 0xFF); cpu->StackPointer--; genericBranch(cpu); break; @@ -326,7 +360,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // something a device asked for. uint16_t site = cpu->ProgramCounter; cpu->ProgramCounter++; - uint8_t vector = cpu->Program[cpu->ProgramCounter]; + uint8_t vector = fetchProgram(cpu, cpu->ProgramCounter); // Execution resumes after the operand, which the Program Counter is sitting // on, so the resume address is one further on than that. if (enterInterrupt(cpu, SOFTWARE_VECTOR_BASE, vector, cpu->ProgramCounter + 1)) { @@ -339,24 +373,24 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // RETI - Return from an interrupt. Pops the frame in the exact reverse of // the order enterInterrupt pushed it. cpu->StackPointer++; - cpu->Status = cpu->Data[cpu->StackPointer]; + cpu->Status = readData(cpu, cpu->StackPointer); cpu->StackPointer++; - cpu->Q = cpu->Data[cpu->StackPointer]; + cpu->Q = readData(cpu, cpu->StackPointer); cpu->StackPointer++; - cpu->A = cpu->Data[cpu->StackPointer]; + cpu->A = readData(cpu, cpu->StackPointer); cpu->StackPointer++; - cpu->B = cpu->Data[cpu->StackPointer]; + cpu->B = readData(cpu, cpu->StackPointer); for (int i = DATA_POINTERS - 1; i >= 0; i--) { cpu->StackPointer++; - cpu->DataPointer[i] = (uint16_t)cpu->Data[cpu->StackPointer] << 8; + cpu->DataPointer[i] = (uint16_t)readData(cpu, cpu->StackPointer) << 8; cpu->StackPointer++; - cpu->DataPointer[i] |= (uint16_t)cpu->Data[cpu->StackPointer]; + cpu->DataPointer[i] |= (uint16_t)readData(cpu, cpu->StackPointer); } uint16_t resumeAddress; cpu->StackPointer++; - resumeAddress = (uint16_t)cpu->Data[cpu->StackPointer] << 8; + resumeAddress = (uint16_t)readData(cpu, cpu->StackPointer) << 8; cpu->StackPointer++; - resumeAddress = resumeAddress | (uint16_t)cpu->Data[cpu->StackPointer]; + resumeAddress = resumeAddress | (uint16_t)readData(cpu, cpu->StackPointer); // The frame holds the address to carry on from. The Program Counter is // stepped after every instruction, so land one short of it. RET does the // same job with its +2, for the same reason. @@ -365,9 +399,9 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { case 0x1E: // RRET - Return from an RCAL, taking back nothing but the return address. cpu->StackPointer++; - cpu->ProgramCounter = (uint16_t)cpu->Data[cpu->StackPointer] << 8; + cpu->ProgramCounter = (uint16_t)readData(cpu, cpu->StackPointer) << 8; cpu->StackPointer++; - cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)cpu->Data[cpu->StackPointer]; + cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)readData(cpu, cpu->StackPointer); // Two on, to step over the address the RCAL branched through, exactly as RET // does. Everything else RET restores, this deliberately does not. cpu->ProgramCounter += 2; @@ -376,24 +410,24 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // RET - Return from subroutine, restore the registers and set the Program Counter to the Return Address. // Pop A from the Stack. cpu->StackPointer++; - cpu->A = cpu->Data[cpu->StackPointer]; + cpu->A = readData(cpu, cpu->StackPointer); // Pop B from the Stack. cpu->StackPointer++; - cpu->B = cpu->Data[cpu->StackPointer]; + cpu->B = readData(cpu, cpu->StackPointer); // Pop the preserved Data Pointers from the Stack. This walks the pointers // in the opposite order to genericCall, and takes the high byte before the // low byte, so that it exactly mirrors the way they were pushed. for (int i = PRESERVED_DATA_POINTERS - 1; i >= 0; i--) { cpu->StackPointer++; - cpu->DataPointer[i] = (uint16_t)cpu->Data[cpu->StackPointer] << 8; + cpu->DataPointer[i] = (uint16_t)readData(cpu, cpu->StackPointer) << 8; cpu->StackPointer++; - cpu->DataPointer[i] |= (uint16_t)cpu->Data[cpu->StackPointer]; + cpu->DataPointer[i] |= (uint16_t)readData(cpu, cpu->StackPointer); } // Pop the Return Address from the Stack. cpu->StackPointer++; - cpu->ProgramCounter = (uint16_t)cpu->Data[cpu->StackPointer] << 8; + cpu->ProgramCounter = (uint16_t)readData(cpu, cpu->StackPointer) << 8; cpu->StackPointer++; - cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)cpu->Data[cpu->StackPointer]; + cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)readData(cpu, cpu->StackPointer); // Add 2 to the Program Counter to skip over the address when it returns. cpu->ProgramCounter += 2; @@ -452,12 +486,12 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { case 0x26: // INIA - Initialize A Immediately from Program Memory. cpu->ProgramCounter++; - cpu->A = cpu->Program[cpu->ProgramCounter]; + cpu->A = fetchProgram(cpu, cpu->ProgramCounter); break; case 0x27: // INIB - Initialize A Immediately from Program Memory. cpu->ProgramCounter++; - cpu->B = cpu->Program[cpu->ProgramCounter]; + cpu->B = fetchProgram(cpu, cpu->ProgramCounter); break; case 0x28: // CCF - Clear the Carry Flag. @@ -486,17 +520,17 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // case 0x30: // PSHQ - Push Q to the Stack. - cpu->Data[cpu->StackPointer] = cpu->Q; + writeData(cpu, cpu->StackPointer, cpu->Q); cpu->StackPointer--; break; case 0x31: // PSHA - Push A to the Stack. - cpu->Data[cpu->StackPointer] = cpu->A; + writeData(cpu, cpu->StackPointer, cpu->A); cpu->StackPointer--; break; case 0x32: // PSHB - Push B to the Stack. - cpu->Data[cpu->StackPointer] = cpu->B; + writeData(cpu, cpu->StackPointer, cpu->B); cpu->StackPointer--; break; case 0x33: { @@ -504,30 +538,30 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // Order, high byte, low byte // This ordering makes it easier to add offsets with register math. uint16_t pushed = *selectDataPointer(cpu); - cpu->Data[cpu->StackPointer] = (pushed >> 8) & 0xFF; + writeData(cpu, cpu->StackPointer, (pushed >> 8) & 0xFF); cpu->StackPointer--; - cpu->Data[cpu->StackPointer] = pushed & 0xFF; + writeData(cpu, cpu->StackPointer, pushed & 0xFF); cpu->StackPointer--; } break; case 0x34: // POPA - Pop A from the Stack. cpu->StackPointer++; - cpu->A = cpu->Data[cpu->StackPointer]; + cpu->A = readData(cpu, cpu->StackPointer); break; case 0x35: // POPB - Pop B from the Stack. cpu->StackPointer++; - cpu->B = cpu->Data[cpu->StackPointer]; + cpu->B = readData(cpu, cpu->StackPointer); break; case 0x36: { // POPD - Pop a Data Address from the Stack into the selected Data Pointer. uint16_t *popped = selectDataPointer(cpu); cpu->StackPointer++; - *popped = (uint16_t)cpu->Data[cpu->StackPointer]; + *popped = (uint16_t)readData(cpu, cpu->StackPointer); cpu->StackPointer++; - *popped |= (uint16_t)cpu->Data[cpu->StackPointer] << 8; + *popped |= (uint16_t)readData(cpu, cpu->StackPointer) << 8; } break; // @@ -543,32 +577,32 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { break; case 0x42: // LDA - Load A from Data. - cpu->A = cpu->Data[*selectDataPointer(cpu)]; + cpu->A = readData(cpu, *selectDataPointer(cpu)); break; case 0x43: // LDB - Load B from Data. - cpu->B = cpu->Data[*selectDataPointer(cpu)]; + cpu->B = readData(cpu, *selectDataPointer(cpu)); break; case 0x44: // STQ - Store Q into Data. - cpu->Data[*selectDataPointer(cpu)] = cpu->Q; + writeData(cpu, *selectDataPointer(cpu), cpu->Q); break; case 0x45: // STA - Store A into Data. - cpu->Data[*selectDataPointer(cpu)] = cpu->A; + writeData(cpu, *selectDataPointer(cpu), cpu->A); break; case 0x46: // STB - Store B into Data. - cpu->Data[*selectDataPointer(cpu)] = cpu->B; + writeData(cpu, *selectDataPointer(cpu), cpu->B); break; case 0x47: { // SETD - Set the selected Data Pointer. uint16_t *destination = selectDataPointer(cpu); cpu->ProgramCounter++; uint16_t Address; - Address = (uint16_t)cpu->Program[cpu->ProgramCounter] << 8; // Cast the 8 bits to a 16 bit value and shift them to the high byte. + Address = (uint16_t)fetchProgram(cpu, cpu->ProgramCounter) << 8; // Cast the 8 bits to a 16 bit value and shift them to the high byte. cpu->ProgramCounter++; - Address |= (uint16_t)cpu->Program[cpu->ProgramCounter]; + Address |= (uint16_t)fetchProgram(cpu, cpu->ProgramCounter); *destination = Address; } break; @@ -576,14 +610,14 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // DPUP - Offset the selected Data Pointer up by the value of the next byte of Program Memory. uint16_t *target = selectDataPointer(cpu); cpu->ProgramCounter++; - *target += cpu->Program[cpu->ProgramCounter]; + *target += fetchProgram(cpu, cpu->ProgramCounter); } break; case 0x49: { // DPDN - Offset the selected Data Pointer down by the value of the next byte of Program Memory. uint16_t *target = selectDataPointer(cpu); cpu->ProgramCounter++; - *target -= cpu->Program[cpu->ProgramCounter]; + *target -= fetchProgram(cpu, cpu->ProgramCounter); } break; case 0x4E: { @@ -624,8 +658,8 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // follows the pointer in DP0 rather than tripping over itself. uint16_t *destination = selectDataPointer(cpu); uint16_t source = *selectDataPointer(cpu); - *destination = (uint16_t)cpu->Data[source] << 8; - *destination |= (uint16_t)cpu->Data[(uint16_t)(source + 1)]; + *destination = (uint16_t)readData(cpu, source) << 8; + *destination |= (uint16_t)readData(cpu, (uint16_t)(source + 1)); } break; case 0x4B: { @@ -634,8 +668,8 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // Data Memory when the pointer sits at the very top of it. uint16_t value = *selectDataPointer(cpu); uint16_t address = *selectDataPointer(cpu); - cpu->Data[address] = (value >> 8) & 0xFF; - cpu->Data[(uint16_t)(address + 1)] = value & 0xFF; + writeData(cpu, address, (value >> 8) & 0xFF); + writeData(cpu, (uint16_t)(address + 1), value & 0xFF); } break; case 0x4C: { @@ -674,7 +708,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // OUTQ - Write the value of Q to an output port. uint16_t site = cpu->ProgramCounter; cpu->ProgramCounter++; - OutputHandler(cpu->Q, cpu->Program[cpu->ProgramCounter]); + portOut(cpu, cpu->Q, fetchProgram(cpu, cpu->ProgramCounter)); answerRefusal(cpu, site); } break; @@ -682,7 +716,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // OUTA - Write the value of A to an output port. uint16_t site = cpu->ProgramCounter; cpu->ProgramCounter++; - OutputHandler(cpu->A, cpu->Program[cpu->ProgramCounter]); + portOut(cpu, cpu->A, fetchProgram(cpu, cpu->ProgramCounter)); answerRefusal(cpu, site); } break; @@ -690,7 +724,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // OUTB - Write the value of B to an output port. uint16_t site = cpu->ProgramCounter; cpu->ProgramCounter++; - OutputHandler(cpu->B, cpu->Program[cpu->ProgramCounter]); + portOut(cpu, cpu->B, fetchProgram(cpu, cpu->ProgramCounter)); answerRefusal(cpu, site); } break; @@ -701,7 +735,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // INA - Read an Input to A. uint16_t site = cpu->ProgramCounter; cpu->ProgramCounter++; - cpu->A = InputHandler(cpu->Program[cpu->ProgramCounter]); + cpu->A = portIn(cpu, fetchProgram(cpu, cpu->ProgramCounter)); answerRefusal(cpu, site); } break; @@ -709,7 +743,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { // INB - Read an Input to B. uint16_t site = cpu->ProgramCounter; cpu->ProgramCounter++; - cpu->B = InputHandler(cpu->Program[cpu->ProgramCounter]); + cpu->B = portIn(cpu, fetchProgram(cpu, cpu->ProgramCounter)); answerRefusal(cpu, site); } break; @@ -762,7 +796,7 @@ void stepCPU(CPURegisters *cpu) { } } // The CPU is not halted, so do a cycle. - if (executeOperation(cpu->Program[cpu->ProgramCounter], cpu)) { + if (executeOperation(fetchProgram(cpu, cpu->ProgramCounter), cpu)) { // Nothing decodes that byte. Hand it to the fault vector, which gets the // address of the offending byte itself rather than the one after it, so // that a handler can read the byte that failed and say what it was. diff --git a/Source/Emulator/cpu.h b/Source/Emulator/cpu.h index 658315f..f4671ac 100644 --- a/Source/Emulator/cpu.h +++ b/Source/Emulator/cpu.h @@ -62,6 +62,19 @@ typedef struct { uint16_t StackPointer; uint8_t *Program; uint8_t *Data; + + // ---- What the machine has cost so far ---- + // + // ONE BUS ACCESS IS ONE CYCLE, and every access goes through it: fetching an opcode, + // fetching the bytes after it, reading or writing Data Memory, pushing or popping the + // Stack, and reaching a device port. Nothing is overlapped - no fetching the next + // instruction while this one finishes - because that is a thing hardware may or may + // not do and this is the model to design against before deciding. + // + // It replaces counting instructions. Counting instructions said an RSTA and a SETD + // cost the same, and that a CALL moving ten bytes of Stack cost what a branch costs, + // which is not true of any machine anybody could build. + unsigned long busCycles; // Set alongside the Fault Flag, and read only by whatever reports the stop. uint8_t Fault; // A FaultCause. uint8_t FaultVector; // Which vector was empty, when Fault is FAULT_NO_HANDLER. diff --git a/Source/Emulator/emulator.c b/Source/Emulator/emulator.c index 10adeb8..e897eda 100644 --- a/Source/Emulator/emulator.c +++ b/Source/Emulator/emulator.c @@ -126,9 +126,17 @@ int main (int argc, char *argv[]) { } else { cycles = cycle_timer_tick(&timer); } - for (int i = 0; i < cycles; i++) { + // ---- 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; ) { + unsigned long before = cpu.busCycles; stepCPU(&cpu); - cycleCount++; + unsigned long took = cpu.busCycles - before; + spent += (long)took; + cycleCount += took; if (cpu.Status & STATUS_HALT) { // We've halted. break; diff --git a/Tests/expected/16x16Life.out b/Tests/expected/16x16Life.out index 2690f2a..7e11637 100644 --- a/Tests/expected/16x16Life.out +++ b/Tests/expected/16x16Life.out @@ -222,5 +222,240 @@ -Execution stopped. (cycle limit reached) + + + + + # + # # + ## + + + + + + + + + + + + + + # + ## + ## + + + + + + + + + + + + + + # + # + ### + + + + + + + + + + + + + + + # # + ## + # + + + + + + + + + + + + + + # + # # + ## + + + + + + + + + + + + + + # + ## + ## + + + + + + + + + + + + + + # + # + ### + + + + + + + + + + + + + + + # # + ## + # + + + + + + + + + + + + + + # + # # + ## + + + + + + + + + + + + + + # + ## + ## + + + + + + + + + + + + + + # + # + ### + + + + + + + + + + + + + + + # # + ## + # + + + + + + + + + + + + + + # + # # + ## + + + + + + + + + + + + + + # + ## + ## + + + + + + + + + + + + + + # + # + ### + + Execution stopped. (cycle limit reached) [exit 0] diff --git a/Tests/expected/16x16LifeModern.out b/Tests/expected/16x16LifeModern.out index 2690f2a..68e198c 100644 --- a/Tests/expected/16x16LifeModern.out +++ b/Tests/expected/16x16LifeModern.out @@ -222,5 +222,261 @@ + + + + + # + # # + ## + + + + + + + + + + + + + + # + ## + ## + + + + + + + + + + + + + + # + # + ### + + + + + + + + + + + + + + + # # + ## + # + + + + + + + + + + + + + + # + # # + ## + + + + + + + + + + + + + + # + ## + ## + + + + + + + + + + + + + + # + # + ### + + + + + + + + + + + + + + + # # + ## + # + + + + + + + + + + + + + + # + # # + ## + + + + + + + + + + + + + + # + ## + ## + + + + + + + + + + + + + + # + # + ### + + + + + + + + + + + + + + + # # + ## + # + + + + + + + + + + + + + + # + # # + ## + + + + + + + + + + + + + + # + ## + ## + + + + + + + + + + + + + + # + # + ### + + + + + + + + + + + + + + + # # + ## + # + + + + + Execution stopped. (cycle limit reached) [exit 0] diff --git a/Tests/expected/replCalculator.out b/Tests/expected/replCalculator.out index 2f61824..d4d86dc 100644 --- a/Tests/expected/replCalculator.out +++ b/Tests/expected/replCalculator.out @@ -13,5 +13,5 @@ SplitBit calculator (+ - * & | ^), Q quits. > 18 -> Goodbye!Execution halted after 2960 cycles. +> Goodbye!Execution halted. [exit 0] diff --git a/Tests/manifest b/Tests/manifest index ab96300..d29ea87 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -460,15 +460,19 @@ inputTestOld | testPrograms/inputTest.asm | run | inputTest replCalculator | Examples/replCalculator.asm | run | replCalculator.in | - # ---- Programs that run forever by design, bounded by a cycle count ---- -# 3,000,000 cycles is about fourteen generations of the glider, which puts -# evolveBoard and its pointer juggling through its paces many times over. -16x16Life | Examples/gameOfLife/16x16Life.asm | run | - | 3000000 +# 11,200,000 cycles is about fourteen generations of the glider, which puts evolveBoard and +# its pointer juggling through its paces many times over. +# +# It was 3,000,000, and the number changed rather than the amount of work: a cycle used to +# be an instruction and is now a memory access, and the average SplitBit instruction makes +# 3.72 of those. Nothing about these two programs changed at all. +16x16Life | Examples/gameOfLife/16x16Life.asm | run | - | 11200000 # The four pointer rewrite, on the same budget so the two can be compared directly. # Note that this cannot show a speed difference: frameDelay is 255 by 255 and swamps # the simulation, so both versions render the same fourteen generations and produce # identical bytes. What it checks is that the rewrite still evolves the board the same # way, which is what a regression test is for. -16x16LifeModern | Examples/gameOfLife/16x16LifeModern.asm | run | - | 3000000 +16x16LifeModern | Examples/gameOfLife/16x16LifeModern.asm | run | - | 11200000 # ---- Libraries: no entry point, so only check that they assemble ---- # The CosmOS libraries assemble on their own, unlike print.asm below, which cannot: it diff --git a/Tests/run.sh b/Tests/run.sh index 7996ac3..a49c33a 100755 --- a/Tests/run.sh +++ b/Tests/run.sh @@ -98,8 +98,13 @@ trim() { # own rather than every test carrying the measurement and nothing asserting anything about # it. settle() { - sed -i -E 's/^Execution halted after [0-9]+ cycles\.$/Execution halted./; - s/^Execution stopped after [0-9]+ cycles\. \(cycle limit reached\)$/Execution stopped. (cycle limit reached)/' "$1" + # NOT ANCHORED TO THE START OF A LINE. A program whose last output has no newline on it + # leaves the cursor mid line, and the halt message is printed there - so the count this + # exists to remove was sitting inside a line rather than at the head of one, and + # survived. replCalculator is the one that does that, and it was the only test to churn + # when the machine started charging for memory instead of counting instructions. + sed -i -E 's/Execution halted after [0-9]+ cycles\./Execution halted./; + s/Execution stopped after [0-9]+ cycles\. \(cycle limit reached\)/Execution stopped. (cycle limit reached)/' "$1" } check() {