2019-09-19 19:29:33 +00:00
|
|
|
package razorvine.c64emu
|
|
|
|
|
2019-09-20 20:12:58 +00:00
|
|
|
import razorvine.examplemachines.DebugWindow
|
2019-09-29 09:29:11 +00:00
|
|
|
import razorvine.ksim65.*
|
2019-09-28 23:51:39 +00:00
|
|
|
import razorvine.ksim65.components.Address
|
|
|
|
import razorvine.ksim65.components.Ram
|
|
|
|
import razorvine.ksim65.components.Rom
|
|
|
|
import razorvine.ksim65.components.UByte
|
2019-09-19 19:29:33 +00:00
|
|
|
import java.io.File
|
2019-09-28 23:26:31 +00:00
|
|
|
import java.io.FileFilter
|
2019-09-26 18:56:13 +00:00
|
|
|
import java.io.FileNotFoundException
|
2019-09-28 23:26:31 +00:00
|
|
|
import java.io.IOException
|
2019-09-26 18:56:13 +00:00
|
|
|
import java.nio.file.Path
|
2019-09-19 19:29:33 +00:00
|
|
|
import java.nio.file.Paths
|
|
|
|
import javax.swing.ImageIcon
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The virtual representation of the Commodore-64
|
2019-09-28 23:51:39 +00:00
|
|
|
*
|
|
|
|
* 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.
|
2019-09-19 19:29:33 +00:00
|
|
|
*/
|
|
|
|
class C64Machine(title: String) : IVirtualMachine {
|
2019-09-26 18:56:13 +00:00
|
|
|
private val romsPath = determineRomPath()
|
2019-09-19 19:41:44 +00:00
|
|
|
private val chargenData = romsPath.resolve("chargen").toFile().readBytes()
|
|
|
|
private val basicData = romsPath.resolve("basic").toFile().readBytes()
|
|
|
|
private val kernalData = romsPath.resolve("kernal").toFile().readBytes()
|
2019-09-19 19:29:33 +00:00
|
|
|
|
|
|
|
override val bus = Bus()
|
2019-09-29 09:29:11 +00:00
|
|
|
override val cpu = Cpu6502()
|
2019-09-19 19:29:33 +00:00
|
|
|
val ram = Ram(0x0000, 0xffff)
|
|
|
|
val vic = VicII(0xd000, 0xd3ff)
|
2019-09-23 23:22:54 +00:00
|
|
|
val cia1 = Cia(1, 0xdc00, 0xdcff)
|
|
|
|
val cia2 = Cia(2, 0xdd00, 0xddff)
|
2019-09-19 19:29:33 +00:00
|
|
|
val basicRom = Rom(0xa000, 0xbfff).also { it.load(basicData) }
|
|
|
|
val kernalRom = Rom(0xe000, 0xffff).also { it.load(kernalData) }
|
|
|
|
|
|
|
|
private val debugWindow = DebugWindow(this)
|
2019-09-25 20:58:58 +00:00
|
|
|
private val hostDisplay = MainC64Window(title, chargenData, ram, cpu, cia1)
|
2019-09-20 20:12:58 +00:00
|
|
|
private var paused = false
|
2019-09-19 19:29:33 +00:00
|
|
|
|
2019-09-28 23:51:39 +00:00
|
|
|
init {
|
|
|
|
cpu.addBreakpoint(0xffd5, ::breakpointKernelLoad) // intercept LOAD subroutine in the kernal
|
|
|
|
cpu.addBreakpoint(0xffd8, ::breakpointKernelSave) // intercept SAVE subroutine in the kernal
|
2019-09-29 09:29:11 +00:00
|
|
|
cpu.breakpointForBRK = ::breakpointBRK
|
2019-09-28 23:51:39 +00:00
|
|
|
|
|
|
|
bus += basicRom
|
|
|
|
bus += kernalRom
|
|
|
|
bus += vic
|
|
|
|
bus += cia1
|
|
|
|
bus += cia2
|
|
|
|
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
|
2019-10-04 18:39:57 +00:00
|
|
|
hostDisplay.start(30)
|
2019-09-28 23:51:39 +00:00
|
|
|
}
|
|
|
|
|
2019-09-29 09:29:11 +00:00
|
|
|
fun breakpointKernelLoad(cpu: Cpu6502, pc: Address): Cpu6502.BreakpointResultAction {
|
2019-09-28 23:51:39 +00:00
|
|
|
if (cpu.regA == 0) {
|
2019-09-28 23:26:31 +00:00
|
|
|
val fnlen = ram[0xb7] // file name length
|
|
|
|
val fa = ram[0xba] // device number
|
|
|
|
val sa = ram[0xb9] // secondary address
|
2019-09-28 23:51:39 +00:00
|
|
|
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("")
|
2019-09-28 23:26:31 +00:00
|
|
|
val loadEndAddress = searchAndLoadFile(filename, fa, sa, txttab)
|
2019-09-28 23:51:39 +00:00
|
|
|
if (loadEndAddress != null) {
|
2019-09-28 23:26:31 +00:00
|
|
|
ram[0x90] = 0 // status OK
|
|
|
|
ram[0xae] = (loadEndAddress and 0xff).toShort()
|
|
|
|
ram[0xaf] = (loadEndAddress ushr 8).toShort()
|
2019-09-29 09:29:11 +00:00
|
|
|
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)
|
2019-09-28 23:26:31 +00:00
|
|
|
}
|
|
|
|
|
2019-09-29 09:29:11 +00:00
|
|
|
fun breakpointKernelSave(cpu: Cpu6502, pc: Address): Cpu6502.BreakpointResultAction {
|
2019-09-28 23:51:39 +00:00
|
|
|
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
|
2019-09-29 09:29:11 +00:00
|
|
|
Cpu6502.BreakpointResultAction(changePC = 0xf5a9) // success!
|
|
|
|
} else Cpu6502.BreakpointResultAction(changePC = 0xf710) // 'missing file name'
|
|
|
|
}
|
|
|
|
|
|
|
|
fun breakpointBRK(cpu: Cpu6502, pc: Address): Cpu6502.BreakpointResultAction {
|
|
|
|
throw Cpu6502.InstructionError("BRK instruction hit at ${hexW(pc)}")
|
2019-09-28 23:51:39 +00:00
|
|
|
}
|
2019-09-28 23:26:31 +00:00
|
|
|
|
2019-09-28 23:51:39 +00:00
|
|
|
private fun searchAndLoadFile(
|
|
|
|
filename: String,
|
|
|
|
device: UByte,
|
|
|
|
secondary: UByte,
|
|
|
|
basicLoadAddress: Address
|
|
|
|
): Address? {
|
2019-09-28 23:26:31 +00:00
|
|
|
when (filename) {
|
2019-09-28 23:51:39 +00:00
|
|
|
"*" -> {
|
2019-09-28 23:26:31 +00:00
|
|
|
// load the first file in the directory
|
2019-09-28 23:51:39 +00:00
|
|
|
return searchAndLoadFile(
|
|
|
|
File(".").listFiles()?.firstOrNull()?.name ?: "",
|
|
|
|
device,
|
|
|
|
secondary,
|
|
|
|
basicLoadAddress
|
|
|
|
)
|
2019-09-28 23:26:31 +00:00
|
|
|
}
|
|
|
|
"$" -> {
|
|
|
|
// 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())
|
2019-09-28 23:51:39 +00:00
|
|
|
if (name.isEmpty())
|
2019-10-05 13:14:26 +00:00
|
|
|
Pair(".$ext", "") to fileAndSize
|
2019-09-28 23:26:31 +00:00
|
|
|
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 {
|
2019-09-28 23:51:39 +00:00
|
|
|
it.name.toUpperCase() == filename
|
2019-09-28 23:26:31 +00:00
|
|
|
}
|
|
|
|
return file?.name
|
|
|
|
}
|
2019-09-28 23:51:39 +00:00
|
|
|
|
2019-09-28 23:26:31 +00:00
|
|
|
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())
|
2019-09-28 23:51:39 +00:00
|
|
|
listing.addAll(line.map { it.toShort() })
|
2019-09-28 23:26:31 +00:00
|
|
|
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)
|
2019-09-28 23:51:39 +00:00
|
|
|
addLine(blocksize, "$padding1 \"$filename\" $padding2 ${it.key.second.take(3).padEnd(3)}")
|
2019-09-28 23:26:31 +00:00
|
|
|
}
|
2019-09-28 23:51:39 +00:00
|
|
|
addLine(kotlin.math.max(0, 664 - totalBlocks), "BLOCKS FREE.")
|
2019-09-28 23:26:31 +00:00
|
|
|
listing.add(0)
|
|
|
|
listing.add(0)
|
|
|
|
return listing.toTypedArray()
|
|
|
|
}
|
|
|
|
|
2019-09-26 18:56:13 +00:00
|
|
|
private fun determineRomPath(): Path {
|
|
|
|
val candidates = listOf("./roms", "~/roms/c64", "~/roms", "~/.vice/C64")
|
|
|
|
candidates.forEach {
|
|
|
|
val path = Paths.get(expandUser(it))
|
2019-09-28 23:51:39 +00:00
|
|
|
if (path.toFile().isDirectory)
|
2019-09-26 18:56:13 +00:00
|
|
|
return path
|
|
|
|
}
|
|
|
|
throw FileNotFoundException("no roms directory found, tried: $candidates")
|
|
|
|
}
|
|
|
|
|
2019-09-19 19:29:33 +00:00
|
|
|
private fun expandUser(path: String): String {
|
2019-09-26 18:56:13 +00:00
|
|
|
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
|
2019-09-19 19:29:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-23 23:22:54 +00:00
|
|
|
override fun loadFileInRam(file: File, loadAddress: Address?) {
|
2019-09-28 23:51:39 +00:00
|
|
|
if (file.extension == "prg" && (loadAddress == null || loadAddress == 0x0801))
|
2019-09-28 23:26:31 +00:00
|
|
|
ram.loadPrg(file.inputStream(), null)
|
2019-09-23 23:22:54 +00:00
|
|
|
else
|
|
|
|
ram.load(file.readBytes(), loadAddress!!)
|
|
|
|
}
|
2019-09-19 19:29:33 +00:00
|
|
|
|
2019-09-21 13:57:14 +00:00
|
|
|
override fun getZeroAndStackPages(): Array<UByte> = ram.getPages(0, 2)
|
|
|
|
|
2019-09-20 20:12:58 +00:00
|
|
|
override fun pause(paused: Boolean) {
|
|
|
|
this.paused = paused
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun step() {
|
|
|
|
// step a single full instruction
|
2019-09-19 19:29:33 +00:00
|
|
|
while (cpu.instrCycles > 0) bus.clock()
|
|
|
|
bus.clock()
|
|
|
|
while (cpu.instrCycles > 0) bus.clock()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun start() {
|
2019-10-02 00:13:29 +00:00
|
|
|
javax.swing.Timer(50) {
|
2019-09-21 13:57:14 +00:00
|
|
|
debugWindow.updateCpu(cpu, bus)
|
|
|
|
}.start()
|
|
|
|
|
2019-09-27 20:38:36 +00:00
|
|
|
// busy waiting loop, averaging cpu speed to ~1 Mhz:
|
|
|
|
var numInstructionsteps = 600
|
|
|
|
val targetSpeedKhz = 1000
|
|
|
|
while (true) {
|
|
|
|
if (paused) {
|
|
|
|
Thread.sleep(100)
|
|
|
|
} else {
|
|
|
|
cpu.startSpeedMeasureInterval()
|
|
|
|
Thread.sleep(0, 1000)
|
|
|
|
repeat(numInstructionsteps) {
|
2019-09-20 20:12:58 +00:00
|
|
|
step()
|
2019-09-28 23:51:39 +00:00
|
|
|
if (vic.currentRasterLine == 255) {
|
2019-09-19 19:29:33 +00:00
|
|
|
// we force an irq here ourselves rather than fully emulating the VIC-II's raster IRQ
|
|
|
|
// or the CIA timer IRQ/NMI.
|
|
|
|
cpu.irq()
|
|
|
|
}
|
|
|
|
}
|
2019-09-27 20:38:36 +00:00
|
|
|
val speed = cpu.measureAvgIntervalSpeedKhz()
|
|
|
|
if (speed < targetSpeedKhz - 50)
|
|
|
|
numInstructionsteps++
|
|
|
|
else if (speed > targetSpeedKhz + 50)
|
|
|
|
numInstructionsteps--
|
2019-09-19 19:29:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun main(args: Array<String>) {
|
|
|
|
val machine = C64Machine("virtual Commodore-64 - using KSim65 v${Version.version}")
|
|
|
|
machine.start()
|
|
|
|
}
|