mirror of
https://github.com/irmen/ksim65.git
synced 2024-09-27 07:54:29 +00:00
259 lines
11 KiB
Kotlin
259 lines
11 KiB
Kotlin
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 chargenData = romsPath.resolve("chargen").toFile().readBytes()
|
|
private val basicData = romsPath.resolve("basic").toFile().readBytes()
|
|
private val kernalData = romsPath.resolve("kernal").toFile().readBytes()
|
|
|
|
override val cpu = Cpu6502()
|
|
val cpuIoPort = CpuIoPort(cpu)
|
|
override val bus = Bus6510(cpuIoPort, chargenData)
|
|
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)
|
|
val basicRom = Rom(0xa000, 0xbfff).also { it.load(basicData) }
|
|
val kernalRom = Rom(0xe000, 0xffff).also { it.load(kernalData) }
|
|
|
|
private val monitor = Monitor(bus, cpu)
|
|
private val debugWindow = DebugWindow(this)
|
|
private val hostDisplay = MainC64Window(title, chargenData, 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 += basicRom
|
|
bus += kernalRom
|
|
bus += vic
|
|
bus += cia1
|
|
bus += cia2
|
|
bus += cpuIoPort
|
|
bus += ram
|
|
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<String, String>, Pair<File, Long>>,
|
|
basicLoadAddress: Address): Array<UByte> {
|
|
var address = basicLoadAddress
|
|
val listing = mutableListOf<UByte>()
|
|
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()
|
|
}
|
|
|
|
private 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")
|
|
}
|
|
|
|
private 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
|
|
}
|
|
}
|
|
|
|
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<UByte> = 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 executeMonitorCommand(command: String) = monitor.command(command)
|
|
|
|
fun start() {
|
|
javax.swing.Timer(50) {
|
|
debugWindow.updateCpu(cpu, bus)
|
|
}.start()
|
|
|
|
val timer = java.util.Timer("cpu-cycle", true)
|
|
timer.scheduleAtFixedRate(0, 1000L/VicII.framerate) {
|
|
if (!paused) {
|
|
// we synchronise cpu cycles to the vertical blank of the Vic chip
|
|
// this should result in ~1 Mhz cpu speed
|
|
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 main() {
|
|
val machine = C64Machine("virtual Commodore-64 - using KSim65 v${Version.version}")
|
|
machine.start()
|
|
}
|