// // 68000ArithmeticTests.m // Clock SignalTests // // Created by Thomas Harte on 28/06/2019. // // Largely ported from the tests of the Portable 68k Emulator. // #import #include "68000.hpp" #include "68000Mk2.hpp" #include #include #include #include namespace { struct RandomStore { using CollectionT = std::unordered_map>; CollectionT values; void flag(uint32_t address, uint8_t participant) { values[address].first |= participant; } bool has(uint32_t address, uint8_t participant) { auto entry = values.find(address); if(entry == values.end()) return false; return entry->second.first & participant; } uint8_t value(uint32_t address, uint8_t participant) { auto entry = values.find(address); if(entry != values.end()) { entry->second.first |= participant; return entry->second.second; } const uint8_t value = uint8_t(rand() >> 8); values[address] = std::make_pair(participant, value); return value; } void clear() { values.clear(); } }; struct Transaction { HalfCycles timestamp; uint8_t function_code = 0; uint32_t address = 0; uint16_t value = 0; bool address_strobe = false; bool same_address = false; bool read = false; int data_strobes = 0; bool operator != (const Transaction &rhs) const { if(timestamp != rhs.timestamp) return true; // if(function_code != rhs.function_code) return true; if(address != rhs.address) return true; if(value != rhs.value) return true; if(address_strobe != rhs.address_strobe) return true; if(data_strobes != rhs.data_strobes) return true; if(same_address != rhs.same_address) return true; return false; } void print() const { printf("%d: %d%d%d %c %c%c @ %06x %s %04x\n", timestamp.as(), (function_code >> 2) & 1, (function_code >> 1) & 1, (function_code >> 0) & 1, address_strobe ? 'a' : '-', (data_strobes & 1) ? 'b' : '-', (data_strobes & 2) ? 'w' : '-', address, read ? "->" : "<-", value); } }; struct HarmlessStopException {}; struct BusHandler { BusHandler(RandomStore &_store, uint8_t _participant) : store(_store), participant(_participant) {} void will_perform(uint32_t, uint16_t) { --instructions; if(instructions < 0) { throw HarmlessStopException{}; } } template HalfCycles perform_bus_operation(const Microcycle &cycle, bool is_supervisor) { Transaction transaction; // Fill all of the transaction record except the data field; will do that after // any potential read. if(cycle.operation & Microcycle::InterruptAcknowledge) { transaction.function_code = 0b111; } else { transaction.function_code = is_supervisor ? 0x4 : 0x0; transaction.function_code |= (cycle.operation & Microcycle::IsData) ? 0x1 : 0x2; } transaction.address_strobe = cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress); transaction.same_address = cycle.operation & Microcycle::SameAddress; transaction.data_strobes = cycle.operation & (Microcycle::SelectByte | Microcycle::SelectWord); if(cycle.address) transaction.address = *cycle.address & 0xffff'ff; transaction.timestamp = time; transaction.read = cycle.operation & Microcycle::Read; time += cycle.length; // Do the operation... const uint32_t address = cycle.address ? (*cycle.address & 0xff'ffff) : 0; switch(cycle.operation & (Microcycle::SelectWord | Microcycle::SelectByte | Microcycle::Read)) { default: break; case Microcycle::SelectWord | Microcycle::Read: if(!store.has(address, participant)) { ram[address] = store.value(address, participant); } if(!store.has(address+1, participant)) { ram[address+1] = store.value(address+1, participant); } cycle.set_value16((ram[address] << 8) | ram[address + 1]); break; case Microcycle::SelectByte | Microcycle::Read: if(!store.has(address, participant)) { ram[address] = store.value(address, participant); } if(address & 1) { cycle.set_value8_low(ram[address]); } else { cycle.set_value8_high(ram[address]); } break; case Microcycle::SelectWord: ram[address] = cycle.value8_high(); ram[address+1] = cycle.value8_low(); store.flag(address, participant); store.flag(address+1, participant); break; case Microcycle::SelectByte: ram[address] = (address & 1) ? cycle.value8_low() : cycle.value8_high(); store.flag(address, participant); break; } // Add the data value if relevant. if(transaction.data_strobes) { transaction.value = cycle.value16(); } // Push back only if interesting. if(capture_all_transactions || transaction.address_strobe || transaction.data_strobes || transaction.function_code == 7) { if(transaction_delay) { --transaction_delay; // Start counting time only from the first recorded transaction. if(!transaction_delay) { time = HalfCycles(0); } } else { transactions.push_back(transaction); } } return HalfCycles(0); } int transaction_delay; int instructions; bool capture_all_transactions = false; HalfCycles time; std::vector transactions; std::array ram; void set_default_vectors() { // Establish that all exception vectors point to 1024-byte blocks of memory. for(int c = 0; c < 256; c++) { const uint32_t target = (c + 2) << 10; const uint32_t address = c << 2; ram[address + 0] = uint8_t(target >> 24); ram[address + 1] = uint8_t(target >> 16); ram[address + 2] = uint8_t(target >> 8); ram[address + 3] = uint8_t(target >> 0); store.flag(address+0, participant); store.flag(address+1, participant); store.flag(address+2, participant); store.flag(address+3, participant); } } RandomStore &store; const uint8_t participant; }; using OldProcessor = CPU::MC68000::Processor; using NewProcessor = CPU::MC68000Mk2::Processor; template struct Tester { Tester(RandomStore &store, uint8_t participant) : bus_handler(store, participant), processor(bus_handler) {} void run_instructions(int instructions) { bus_handler.instructions = instructions; try { processor.run_for(HalfCycles(5000)); // Arbitrary, but will definitely exceed any one instruction (by quite a distance). } catch (const HarmlessStopException &) {} } void reset_with_opcode(uint16_t opcode) { bus_handler.transactions.clear(); bus_handler.set_default_vectors(); const uint32_t address = 3 << 10; bus_handler.ram[address + 0] = uint8_t(opcode >> 8); bus_handler.ram[address + 1] = uint8_t(opcode >> 0); bus_handler.store.flag(address, bus_handler.participant); bus_handler.store.flag(address+1, bus_handler.participant); bus_handler.transaction_delay = 12; // i.e. ignore everything from the RESET sequence. bus_handler.time = HalfCycles(0); processor.reset(); } BusHandler bus_handler; M68000 processor; }; void print_state(FILE *target, const CPU::MC68000Mk2::State &state, const std::vector &transactions, bool is_initial) { for(int c = 0; c < 8; c++) { fprintf(target, "\"d%d\": %u, ", c, state.registers.data[c]); } for(int c = 0; c < 7; c++) { fprintf(target, "\"a%d\": %u, ", c, state.registers.address[c]); } fprintf(target, "\"usp\": %u, ", state.registers.user_stack_pointer); fprintf(target, "\"ssp\": %u, ", state.registers.supervisor_stack_pointer); fprintf(target, "\"sr\": %u, ", state.registers.status); fprintf(target, "\"pc\": %u, ", state.registers.program_counter - 4); fprintf(target, "\"prefetch\": [%u, %u], ", state.prefetch[0], state.prefetch[1]); fprintf(target, "\"ram\": ["); // Compute RAM from transactions; if this is the initial state then RAM should // be everything that was subject to a read which had not previously been // subject to a write. Otherwise it can just be everything. std::unordered_map ram; if(is_initial) { std::unordered_set written_addresses; for(const auto &transaction: transactions) { switch(transaction.data_strobes) { default: continue; case 1: if(transaction.read) { if(ram.find(transaction.address) == ram.end()) { ram[transaction.address] = transaction.value; } } else { written_addresses.insert(transaction.address); } break; case 2: if(transaction.read) { if(ram.find(transaction.address) == ram.end()) { ram[transaction.address] = uint8_t(transaction.value >> 8); } if(ram.find(transaction.address+1) == ram.end()) { ram[transaction.address+1] = uint8_t(transaction.value); } } else { written_addresses.insert(transaction.address); written_addresses.insert(transaction.address + 1); } break; } } } else { for(const auto &transaction: transactions) { switch(transaction.data_strobes) { default: continue; case 1: ram[transaction.address] = transaction.value; break; case 2: ram[transaction.address] = uint8_t(transaction.value >> 8); ram[transaction.address+1] = uint8_t(transaction.value); break; } } } bool is_first = true; for(const auto &pair: ram) { if(!is_first) fprintf(target, ", "); is_first = false; fprintf(target, "[%d, %d]", pair.first, pair.second); } fprintf(target, "]"); } void print_transactions(FILE *target, const std::vector &transactions, HalfCycles end) { auto iterator = transactions.begin(); bool is_first = true; do { if(!is_first) fprintf(target, ", "); is_first = false; fprintf(target, "["); auto next = iterator + 1; // Attempt to pair off transactions to reproduct YACHT notation. bool is_access = true; if(!iterator->address_strobe && !iterator->data_strobes) { fprintf(target, "\"n\", "); is_access = false; } else { assert(!iterator->data_strobes); // Check how many transactions this address persists for; // that'll allow a TAS to be recognised here. while(next->same_address && next != transactions.end()) { ++next; } --next; if(next == iterator + 1) { if(next->read) { fprintf(target, "\"r\", "); } else { fprintf(target, "\"w\", "); } } else { fprintf(target, "\"t\", "); } // Include next in the calculation of time below. ++next; } HalfCycles length; if(next == transactions.end()) { length = end - iterator->timestamp; } else { length = next->timestamp - iterator->timestamp; } fprintf(target, "%d", length.as() >> 1); if(is_access) { // Undo the 'move to one after' step that allowed next to be included // in this transaction's cycle count. --next; fprintf(target, ", %d, ", iterator->function_code); fprintf(target, "%d, ", iterator->address & 0xff'ffff); switch(next->data_strobes) { default: assert(false); case 1: fprintf(target, "\".b\", %d", next->value & 0xff); break; case 2: fprintf(target, "\".w\", %d", next->value); break; } ++next; } fprintf(target, "]"); iterator = next; } while(iterator != transactions.end()); } } @interface M68000OldVsNewTests : XCTestCase @end @implementation M68000OldVsNewTests //- (void)testGenerate { - (void)generate { srand(68000); InstructionSet::M68k::Predecoder decoder; RandomStore random_store; auto tester = std::make_unique>(random_store, 0x02); tester->bus_handler.capture_all_transactions = true; // Bucket opcodes by operation. std::unordered_map> opcodesByOperation; for(int c = 0; c < 65536; c++) { // Test only defined opcodes that aren't STOP (which will never teminate). const auto instruction = decoder.decode(uint16_t(c)); if( instruction.operation == InstructionSet::M68k::Operation::Undefined || instruction.operation == InstructionSet::M68k::Operation::STOP ) { continue; } const auto operation = instruction.operation_string(); opcodesByOperation[operation].push_back(c); } // Find somewhere to write to. NSString *const tempDir = NSTemporaryDirectory(); NSLog(@"Outputting to %@", tempDir); // Aim to get at least 1,000,000 tests total. const auto testsPerOperation = int((1'000'000 + (opcodesByOperation.size() - 1)) / opcodesByOperation.size()); // Generate by operation. NSLog(@"Generating %d tests each for %lu operations", testsPerOperation, opcodesByOperation.size()); for(const auto &pair: opcodesByOperation) { NSLog(@"Generating %s", pair.first); NSString *const targetName = [NSString stringWithFormat:@"%@%s.json", tempDir, pair.first]; FILE *const target = fopen(targetName.UTF8String, "wt"); const bool force_addresses_even = decoder.decode(pair.second[0]).operation == InstructionSet::M68k::Operation::UNLINK; bool is_first_test = true; fprintf(target, "["); // Test each for the selected number of iterations. for(int test = 0; test < testsPerOperation; test++) { if(!is_first_test) fprintf(target, ",\n"); is_first_test = false; // Establish with certainty the initial memory state. random_store.clear(); const auto opcodeIndex = int(rand() * pair.second.size() / RAND_MAX); const uint16_t opcode = pair.second[opcodeIndex]; tester->reset_with_opcode(opcode); // Generate a random initial register state. auto initialState = tester->processor.get_state(); // Require address pointers to be even 99% of the time, or always for UNLINK. const bool addresses_are_even = (rand() >= int(float(RAND_MAX) * 0.99f)) || force_addresses_even; for(int c = 0; c < 8; c++) { initialState.registers.data[c] = rand() ^ (rand() << 1); if(c != 7) { initialState.registers.address[c] = rand() ^ (rand() << 1); if(addresses_are_even) initialState.registers.address[c] &= ~1; } } // Pick a random status such that: // // (i) supervisor mode is active 99% of the time; // (ii) trace is inactive; and // (iii) interrupt level is 7. const bool is_supervisor = rand() >= int(float(RAND_MAX) * 0.99f); initialState.registers.status = (rand() | (int(is_supervisor) << 13) | (7 << 8)) & ~(1 << 15); initialState.registers.user_stack_pointer = rand() << 1; initialState.registers.supervisor_stack_pointer = rand() << 1; // Set state. tester->processor.set_state(initialState); // Run for zero instructions to grab the real initial state (i.e. valid prefetch, ssp, etc). // Then make sure no transactions or time carry over into the actual instruction. tester->run_instructions(0); auto populatedInitialState = tester->processor.get_state(); tester->bus_handler.transactions.clear(); tester->bus_handler.time = HalfCycles(0); // Run for another instruction to do the actual work. tester->run_instructions(1); const auto finalState = tester->processor.get_state(); // Output initial state. fprintf(target, "{ \"name\": \"%04x [%s] %d\", ", opcode, decoder.decode(opcode).to_string().c_str(), test + 1); fprintf(target, "\"initial\": {"); print_state(target, populatedInitialState, tester->bus_handler.transactions, true); // Output final state. fprintf(target, "}, \"final\": {"); print_state(target, finalState, tester->bus_handler.transactions, false); // Output total length and bus activity. fprintf(target, "}, \"length\": %d, ", tester->bus_handler.time.as() >> 1); fprintf(target, "\"transactions\": ["); print_transactions(target, tester->bus_handler.transactions, tester->bus_handler.time); fprintf(target, "]}"); } fprintf(target, "\n]\n"); fclose(target); } } - (void)testOldVsNew { RandomStore random_store; auto oldTester = std::make_unique>(random_store, 0x01); auto newTester = std::make_unique>(random_store, 0x02); InstructionSet::M68k::Predecoder decoder; // Use a fixed seed to guarantee continuity across repeated runs. srand(68000); std::set ignore_list = { // // Operations that do the wrong thing on the old 68000: // InstructionSet::M68k::Operation::ABCD, // Old implementation doesn't match flamewing tests, sometimes produces incorrect results. InstructionSet::M68k::Operation::SBCD, // Old implementation doesn't match flamewing tests, sometimes produces incorrect results. InstructionSet::M68k::Operation::JSR, // Old implementation ends up skipping stack space if the destination throws an address error. InstructionSet::M68k::Operation::MOVEtoSR, // Old implementation doesn't repeat a PC fetch. InstructionSet::M68k::Operation::MOVEtoCCR, // Old implementation doesn't repeat a PC fetch. // // Operations with definite timing deficiencies versus Yacht.txt on the old 68000: // InstructionSet::M68k::Operation::CMPAl, // Old implementation omits an idle cycle before -(An) InstructionSet::M68k::Operation::CLRb, // Old implementation omits an idle cycle before -(An) InstructionSet::M68k::Operation::CLRw, // Old implementation omits an idle cycle before -(An) InstructionSet::M68k::Operation::NEGXb, // Old implementation omits an idle cycle before -(An) InstructionSet::M68k::Operation::NEGXw, // Old implementation omits an idle cycle before -(An) InstructionSet::M68k::Operation::NEGb, // Old implementation omits an idle cycle before -(An) InstructionSet::M68k::Operation::NEGw, // Old implementation omits an idle cycle before -(An) InstructionSet::M68k::Operation::NOTb, // Old implementation omits an idle cycle before -(An) InstructionSet::M68k::Operation::NOTw, // Old implementation omits an idle cycle before -(An) InstructionSet::M68k::Operation::TRAP, // Old implementation relocates the idle state near the end to the beginning. InstructionSet::M68k::Operation::TRAPV, // Old implementation relocates the idle state near the end to the beginning. InstructionSet::M68k::Operation::CHK, // Old implementation pauses four cycles too long. InstructionSet::M68k::Operation::TAS, // Old implementation just doesn't match published cycle counts. // // Operations with timing discrepancies between the two 68000 implementations // that I think are _more_ accurate now, but possibly still need work: // InstructionSet::M68k::Operation::MULU, InstructionSet::M68k::Operation::MULS, InstructionSet::M68k::Operation::DIVU, InstructionSet::M68k::Operation::DIVS, }; int testsRun = 0; std::set failing_operations; for(int c = 0; c < 65536; c++) { printf("%04x\n", c); // Test only defined opcodes that aren't STOP (which will never teminate). const auto instruction = decoder.decode(uint16_t(c)); if( instruction.operation == InstructionSet::M68k::Operation::Undefined || instruction.operation == InstructionSet::M68k::Operation::STOP ) { continue; } // If this operation is known to diverge, ignore it. It's dealt with. if(ignore_list.find(instruction.operation) != ignore_list.end()) { continue; } // Test each 1000 times. for(int test = 0; test < 1000; test++) { ++testsRun; // Establish with certainty the initial memory state. random_store.clear(); newTester->reset_with_opcode(c); oldTester->reset_with_opcode(c); // Generate a random initial register state. auto oldState = oldTester->processor.get_state(); auto newState = newTester->processor.get_state(); for(int c = 0; c < 8; c++) { oldState.data[c] = newState.registers.data[c] = rand() ^ (rand() << 1); if(c != 7) oldState.address[c] = newState.registers.address[c] = rand() << 1; } // Fully to paper over the two 68000s' different ways of doing a faked // reset, pick a random status such that: // // (i) supervisor mode is active; // (ii) trace is inactive; and // (iii) interrupt level is 7. oldState.status = newState.registers.status = (rand() | (1 << 13) | (7 << 8)) & ~(1 << 15); oldState.user_stack_pointer = newState.registers.user_stack_pointer = rand() << 1; oldState.supervisor_stack_pointer = newState.registers.supervisor_stack_pointer = 0x800; newTester->processor.set_state(newState); oldTester->processor.set_state(oldState); // Run a single instruction. newTester->run_instructions(1); oldTester->run_instructions(1); // Grab final states. oldState = oldTester->processor.get_state(); newState = newTester->processor.get_state(); // Compare bus activity only if it doesn't look like an address // error occurred. Don't check those as the old 68000 appears to be wrong // most of the time about function codes, and that bleeds into the stacked data. // // Net effect will be 50% fewer transaction comparisons for instructions that // can trigger an address error. const auto &oldTransactions = oldTester->bus_handler.transactions; const auto &newTransactions = newTester->bus_handler.transactions; if(oldState.program_counter != 0x1404 || newState.registers.program_counter != 0x1404) { auto newIt = newTransactions.begin(); auto oldIt = oldTransactions.begin(); while(newIt != newTransactions.end() && oldIt != oldTransactions.end()) { if(*newIt != *oldIt) { printf("Mismatch in %s, test %d:\n", instruction.to_string().c_str(), test); auto repeatIt = newTransactions.begin(); while(repeatIt != newIt) { repeatIt->print(); ++repeatIt; } printf("---\n"); while(newIt != newTransactions.end()) { printf("n: "); newIt->print(); ++newIt; } printf("\n"); while(oldIt != oldTransactions.end()) { printf("o: "); oldIt->print(); ++oldIt; } printf("\n"); failing_operations.insert(instruction.operation); break; } ++newIt; ++oldIt; } } // Compare registers. bool mismatch = false; for(int c = 0; c < 8; c++) { mismatch |= oldState.data[c] != newState.registers.data[c]; if(c != 7) mismatch |= oldState.address[c] != newState.registers.address[c]; } mismatch |= oldState.status != newState.registers.status; mismatch |= oldState.program_counter != newState.registers.program_counter; mismatch |= oldState.user_stack_pointer != newState.registers.user_stack_pointer; mismatch |= oldState.supervisor_stack_pointer != newState.registers.supervisor_stack_pointer; if(mismatch) { failing_operations.insert(instruction.operation); printf("Registers don't match after %s, test %d\n", instruction.to_string().c_str(), test); for(const auto &transaction: newTransactions) { printf("n: "); transaction.print(); } printf("\n"); for(const auto &transaction: oldTransactions) { printf("o: "); transaction.print(); } printf("\n"); // TODO: more detail here! } } } printf("%d tests run\n", testsRun); if(failing_operations.empty()) { printf("No failures\n"); } else { printf("\nAll failing operations:\n"); for(const auto operation: failing_operations) { printf("%d,\n", int(operation)); } } // Mark the test as passed or failed. XCTAssert(failing_operations.empty()); } @end