package razorvine.c64emu import razorvine.examplemachines.DebugWindow import razorvine.ksim65.* import razorvine.ksim65.components.Address import razorvine.ksim65.components.Ram import razorvine.ksim65.components.Rom import razorvine.ksim65.components.UByte import java.io.File import java.io.FileFilter import java.io.FileNotFoundException import java.io.IOException import java.nio.file.Path import java.nio.file.Paths import javax.swing.ImageIcon import javax.swing.JOptionPane import kotlin.concurrent.scheduleAtFixedRate /** * The virtual representation of the Commodore-64 * * It simulates text-mode video. * It hooks into the LOAD and SAVE kernal routines so you can actually * load and save your basic programs to the host filesystem. */ class C64Machine(title: String) : IVirtualMachine { private val romsPath = determineRomPath() private val chargenRom = Rom(0xd000, 0xdfff).also { val chargenData = romsPath.resolve("chargen").toFile().readBytes() it.load(chargenData) } private val basicRom = Rom(0xa000, 0xbfff).also { val basicData = romsPath.resolve("basic").toFile().readBytes() it.load(basicData) } private val kernalRom = Rom(0xe000, 0xffff).also { val kernalData = romsPath.resolve("kernal").toFile().readBytes() it.load(kernalData) } val cpuIoPort = CpuIoPort() // This bus contains "mmu" logic to control the memory bank switching controlled by the 6510's io port in $00/$01. // Therefore we provide it the various roms directly and not "connect" these to the bus in the default way. override val bus = Bus6510(cpuIoPort, chargenRom, basicRom, kernalRom) override val cpu = Cpu6502() // the C64 has 64KB of RAM. Some of it may be banked out and replaced by ROM. val ram = Ram(0x0000, 0xffff) val vic = VicII(0xd000, 0xd3ff, cpu) val cia1 = Cia(1, 0xdc00, 0xdcff, cpu) val cia2 = Cia(2, 0xdd00, 0xddff, cpu) private val monitor = Monitor(bus, cpu) private val debugWindow = DebugWindow(this) private val hostDisplay = MainC64Window(title, chargenRom, ram, cpu, cia1) private var paused = false init { cpu.addBreakpoint(0xffd5, ::breakpointKernelLoad) // intercept LOAD subroutine in the kernal cpu.addBreakpoint(0xffd8, ::breakpointKernelSave) // intercept SAVE subroutine in the kernal cpu.breakpointForBRK = ::breakpointBRK bus += vic bus += cia1 bus += cia2 bus += cpuIoPort bus += ram // note: the ROMs are mapped depending on the cpu's io port bus += cpu bus.reset() hostDisplay.iconImage = ImageIcon(javaClass.getResource("/icon.png")).image debugWindow.iconImage = hostDisplay.iconImage debugWindow.setLocation(hostDisplay.location.x+hostDisplay.width, hostDisplay.location.y) debugWindow.isVisible = true hostDisplay.isVisible = true //hostDisplay.start(30) } private fun breakpointKernelLoad(cpu: Cpu6502, pc: Address): Cpu6502.BreakpointResultAction { if (cpu.regA == 0) { val fnlen = ram[0xb7] // file name length val fa = ram[0xba] // device number val sa = ram[0xb9] // secondary address val txttab = ram[0x2b]+256*ram[0x2c] // basic load address ($0801 usually) val fnaddr = ram[0xbb]+256*ram[0xbc] // file name address return if (fnlen > 0) { val filename = (0 until fnlen).map { ram[fnaddr+it].toChar() }.joinToString("") val loadEndAddress = searchAndLoadFile(filename, fa, sa, txttab) if (loadEndAddress != null) { ram[0x90] = 0 // status OK ram[0xae] = (loadEndAddress and 0xff).toShort() ram[0xaf] = (loadEndAddress ushr 8).toShort() Cpu6502.BreakpointResultAction(changePC = 0xf5a9) // success! } else Cpu6502.BreakpointResultAction(changePC = 0xf704) // 'file not found' } else Cpu6502.BreakpointResultAction(changePC = 0xf710) // 'missing file name' } else return Cpu6502.BreakpointResultAction(changePC = 0xf707) // 'device not present' (VERIFY command not supported) } private fun breakpointKernelSave(cpu: Cpu6502, pc: Address): Cpu6502.BreakpointResultAction { val fnlen = ram[0xb7] // file name length // val fa = ram[0xba] // device number // val sa = ram[0xb9] // secondary address val fnaddr = ram[0xbb]+256*ram[0xbc] // file name address return if (fnlen > 0) { val fromAddr = ram[cpu.regA]+256*ram[cpu.regA+1] val endAddr = cpu.regX+256*cpu.regY val data = (fromAddr..endAddr).map { ram[it].toByte() }.toByteArray() var filename = (0 until fnlen).map { ram[fnaddr+it].toChar() }.joinToString("").toLowerCase() if (!filename.endsWith(".prg")) filename += ".prg" File(filename).outputStream().use { it.write(fromAddr and 0xff) it.write(fromAddr ushr 8) it.write(data) } ram[0x90] = 0 // status OK Cpu6502.BreakpointResultAction(changePC = 0xf5a9) // success! } else Cpu6502.BreakpointResultAction(changePC = 0xf710) // 'missing file name' } private fun breakpointBRK(cpu: Cpu6502, pc: Address): Cpu6502.BreakpointResultAction { throw Cpu6502.InstructionError("BRK instruction hit at $${hexW(pc)}") } private fun searchAndLoadFile(filename: String, device: UByte, secondary: UByte, basicLoadAddress: Address): Address? { when (filename) { "*" -> { // load the first file in the directory return searchAndLoadFile(File(".").listFiles()?.firstOrNull()?.name ?: "", device, secondary, basicLoadAddress) } "$" -> { // load the directory val files = File(".").listFiles(FileFilter { it.isFile })!!.associate { val name = it.nameWithoutExtension.toUpperCase() val ext = it.extension.toUpperCase() val fileAndSize = Pair(it, it.length()) if (name.isEmpty()) Pair(".$ext", "") to fileAndSize else Pair(name, ext) to fileAndSize } val dirname = File(".").canonicalPath.substringAfterLast(File.separator).toUpperCase() val dirlisting = makeDirListing(dirname, files, basicLoadAddress) ram.load(dirlisting, basicLoadAddress) return basicLoadAddress+dirlisting.size-1 } else -> { fun findHostFile(filename: String): String? { val file = File(".").listFiles(FileFilter { it.isFile })?.firstOrNull { it.name.toUpperCase() == filename } return file?.name } val hostFileName = findHostFile(filename) ?: findHostFile("$filename.PRG") ?: return null return try { return if (secondary == 1.toShort()) { val (loadAddress, size) = ram.loadPrg(hostFileName, null) loadAddress+size-1 } else { val (loadAddress, size) = ram.loadPrg(hostFileName, basicLoadAddress) loadAddress+size-1 } } catch (iox: IOException) { println("LOAD ERROR $iox") null } } } } private fun makeDirListing(dirname: String, files: Map, Pair>, basicLoadAddress: Address): Array { var address = basicLoadAddress val listing = mutableListOf() fun addLine(lineNumber: Int, line: String) { address += line.length+3 listing.add((address and 0xff).toShort()) listing.add((address ushr 8).toShort()) listing.add((lineNumber and 0xff).toShort()) listing.add((lineNumber ushr 8).toShort()) listing.addAll(line.map { it.toShort() }) listing.add(0) } addLine(0, "\u0012\"${dirname.take(16).padEnd(16)}\" 00 2A") var totalBlocks = 0 files.forEach { val blocksize = (it.value.second/256).toInt() totalBlocks += blocksize val filename = it.key.first.take(16) val padding1 = " ".substring(blocksize.toString().length) val padding2 = " ".substring(filename.length) addLine(blocksize, "$padding1 \"$filename\" $padding2${it.key.second.take(3).padEnd(3)}") } addLine(kotlin.math.max(0, 664-totalBlocks), "BLOCKS FREE.") listing.add(0) listing.add(0) return listing.toTypedArray() } override fun loadFileInRam(file: File, loadAddress: Address?) { if (file.extension == "prg" && (loadAddress == null || loadAddress == 0x0801)) ram.loadPrg(file.inputStream(), null) else ram.load(file.readBytes(), loadAddress!!) } override fun getZeroAndStackPages(): Array = ram.getBlock(0, 512) override fun pause(paused: Boolean) { this.paused = paused } override fun step() { // step a single full instruction while (cpu.instrCycles > 0) bus.clock() bus.clock() while (cpu.instrCycles > 0) bus.clock() } override fun reset() { bus.reset() hostDisplay.reset() } override fun executeMonitorCommand(command: String) = monitor.command(command) fun start() { javax.swing.Timer(50) { debugWindow.updateCpu(cpu, bus) }.start() // we synchronise cpu cycles to the vertical blank of the Vic chip // this should result in ~1 Mhz cpu speed val timer = java.util.Timer("cpu-cycle", true) timer.scheduleAtFixedRate(0, 1000L/VicII.framerate) { // if(cpu.isLooping) { // // cpu is jump looping, could do some sleeping here perhaps // // but should still consider irqs occurring in the meantime... // } if (!paused) { try { while (vic.vsync) step() while (!vic.vsync) step() hostDisplay.repaint() // repaint synced with VIC vertical blank } catch (rx: RuntimeException) { JOptionPane.showMessageDialog(hostDisplay, "Run time error: $rx", "Error during execution", JOptionPane.ERROR_MESSAGE) this.cancel() throw rx } catch (ex: Error) { JOptionPane.showMessageDialog(hostDisplay, "Run time error: $ex", "Error during execution", JOptionPane.ERROR_MESSAGE) this.cancel() throw ex } } } } } fun determineRomPath(): Path { val candidates = listOf("./roms", "~/roms/c64", "~/roms", "~/.vice/C64") candidates.forEach { val path = Paths.get(expandUser(it)) if (path.toFile().isDirectory) return path } throw FileNotFoundException("no roms directory found, tried: $candidates") } fun expandUser(path: String): String { return when { path.startsWith("~/") -> System.getProperty("user.home")+path.substring(1) path.startsWith("~"+File.separatorChar) -> System.getProperty("user.home")+path.substring(1) path.startsWith("~") -> throw UnsupportedOperationException("home dir expansion not implemented for other users") else -> path } } fun main() { val machine = C64Machine("virtual Commodore-64 - using KSim65 v${Version.version}") machine.start() }