ksim65/src/main/kotlin/razorvine/examplemachines/DebugWindow.kt

257 lines
11 KiB
Kotlin

package razorvine.examplemachines
import razorvine.ksim65.*
import java.awt.*
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.WindowEvent
import java.io.File
import javax.swing.*
import javax.swing.text.DefaultCaret
class DebugWindow(private val vm: IVirtualMachine) : JFrame("Debugger - ksim65 v${Version.version}"),
ActionListener {
private class UnaliasedTextBox(rows: Int, columns: Int) : JTextArea(rows, columns) {
override fun paintComponent(g: Graphics) {
g as Graphics2D
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF)
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF)
super.paintComponent(g)
}
}
private val cyclesTf = JTextField("00000000000000")
private val speedKhzTf = JTextField("0000000")
private val regAtf = JTextField("000")
private val regXtf = JTextField("000")
private val regYtf = JTextField("000")
private val regPCtf = JTextField("00000")
private val regSPtf = JTextField("000")
private val regPtf = JTextArea("NV-BDIZC\n000000000")
private val disassemTf = JTextField("00 00 00 lda (fffff),x")
private val pauseBt = JButton("Pause").also { it.actionCommand = "pause" }
private val zeropageTf = UnaliasedTextBox(8, 102).also {
it.border = BorderFactory.createEtchedBorder()
it.isEnabled = false
it.disabledTextColor = Color.DARK_GRAY
it.font = Font(Font.MONOSPACED, Font.PLAIN, 12)
}
private val stackpageTf = UnaliasedTextBox(8, 102).also {
it.border = BorderFactory.createEtchedBorder()
it.isEnabled = false
it.disabledTextColor = Color.DARK_GRAY
it.font = Font(Font.MONOSPACED, Font.PLAIN, 12)
}
private val disassembler = Disassembler(vm.cpu)
init {
contentPane.layout = GridBagLayout()
defaultCloseOperation = EXIT_ON_CLOSE
val cpuPanel = JPanel(GridBagLayout())
cpuPanel.border = BorderFactory.createTitledBorder("CPU: ${vm.cpu.name}")
val gc = GridBagConstraints()
gc.insets = Insets(2, 2, 2, 2)
gc.anchor = GridBagConstraints.EAST
val cyclesLb = JLabel("cycles")
val speedKhzLb = JLabel("speed (kHz)")
val regAlb = JLabel("A")
val regXlb = JLabel("X")
val regYlb = JLabel("Y")
val regSPlb = JLabel("SP")
val regPClb = JLabel("PC")
val regPlb = JLabel("Status")
val disassemLb = JLabel("Instruction")
cpuPanel.add(cyclesLb, gc.update(gridx=0, gridy=0))
cpuPanel.add(speedKhzLb, gc.update(gridx=5, gridy=0))
cpuPanel.add(regAlb, gc.update(gridx=0, gridy=1))
cpuPanel.add(regXlb, gc.update(gridx=2, gridy=1))
cpuPanel.add(regYlb, gc.update(gridx=4, gridy=1))
cpuPanel.add(regPClb, gc.update(gridx=0, gridy=2))
cpuPanel.add(regSPlb, gc.update(gridx=2, gridy=2))
cpuPanel.add(regPlb, gc.update(gridx=0, gridy=3))
cpuPanel.add(disassemLb, gc.update(gridx=0, gridy=4))
gc.anchor = GridBagConstraints.WEST
cpuPanel.add(cyclesTf, gc.update(gridx=1, gridy=0, gridwidth = 3))
cpuPanel.add(speedKhzTf, gc.update(gridx=6, gridy=0))
cpuPanel.add(regAtf, gc.update(gridx=1, gridy=1))
cpuPanel.add(regXtf, gc.update(gridx=3, gridy=1))
cpuPanel.add(regYtf, gc.update(gridx=5, gridy=1))
cpuPanel.add(regPCtf, gc.update(gridx=1, gridy=2))
cpuPanel.add(regSPtf, gc.update(gridx=3, gridy=2))
cpuPanel.add(regPtf, gc.update(gridx=1, gridy=3, gridwidth=2))
cpuPanel.add(disassemTf, gc.update(gridx=1, gridy=4, gridwidth=5))
listOf(cyclesTf, speedKhzTf, regAtf, regXtf, regYtf, regSPtf, regPCtf, disassemTf, regPtf).forEach {
it.font = Font(Font.MONOSPACED, Font.PLAIN, 14)
it.disabledTextColor = Color.DARK_GRAY
it.isEnabled = false
if (it is JTextField) {
it.columns = it.text.length
} else if (it is JTextArea) {
it.border = BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY),
BorderFactory.createEmptyBorder(2, 2, 2, 2))
}
}
val buttonPanel = JPanel(FlowLayout())
buttonPanel.border = BorderFactory.createTitledBorder("Control")
val loadBt = JButton("Inject program").also { it.actionCommand = "inject" }
val resetBt = JButton("Reset").also { it.actionCommand = "reset" }
val stepBt = JButton("Step").also { it.actionCommand = "step" }
val irqBt = JButton("IRQ").also { it.actionCommand = "irq" }
val nmiBt = JButton("NMI").also { it.actionCommand = "nmi" }
val quitBt = JButton("Quit").also { it.actionCommand = "quit" }
listOf(loadBt, resetBt, irqBt, nmiBt, pauseBt, stepBt, quitBt).forEach {
it.addActionListener(this)
buttonPanel.add(it)
}
val zeropagePanel = JPanel()
zeropagePanel.layout = BoxLayout(zeropagePanel, BoxLayout.Y_AXIS)
zeropagePanel.border = BorderFactory.createTitledBorder("Zeropage and Stack")
val showZp = JCheckBox("show Zero page and Stack dumps", true)
showZp.addActionListener {
val visible = (it.source as JCheckBox).isSelected
zeropageTf.isVisible = visible
stackpageTf.isVisible = visible
}
zeropagePanel.add(showZp)
zeropagePanel.add(zeropageTf)
zeropagePanel.add(stackpageTf)
val monitorPanel = JPanel()
monitorPanel.layout = BoxLayout(monitorPanel, BoxLayout.Y_AXIS)
monitorPanel.border = BorderFactory.createTitledBorder("Built-in Monitor")
val output = JTextArea(10, 80)
output.font = Font(Font.MONOSPACED, Font.PLAIN, 14)
output.isEditable = false
val outputScroll = JScrollPane(output)
monitorPanel.add(outputScroll)
outputScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS
(output.caret as DefaultCaret).updatePolicy = DefaultCaret.ALWAYS_UPDATE
val input = JTextField(50)
input.border = BorderFactory.createLineBorder(Color.LIGHT_GRAY)
input.font = Font(Font.MONOSPACED, Font.PLAIN, 14)
input.addActionListener {
output.append("\n")
val command = input.text.trim()
val result = vm.executeMonitorCommand(command)
if (result.echo) output.append("> $command\n")
output.append(result.output)
input.text = result.prompt
}
monitorPanel.add(input)
output.append(vm.executeMonitorCommand("h").output)
gc.gridx = 0
gc.gridy = 0
gc.fill = GridBagConstraints.BOTH
contentPane.add(cpuPanel, gc)
gc.gridy++
contentPane.add(zeropagePanel, gc)
gc.gridy++
contentPane.add(monitorPanel, gc)
gc.gridy++
contentPane.add(buttonPanel, gc)
pack()
}
override fun actionPerformed(e: ActionEvent) {
when (e.actionCommand) {
"inject" -> {
val chooser = JFileChooser()
chooser.dialogTitle = "Choose binary program or .prg to load"
chooser.currentDirectory = File(".")
chooser.isMultiSelectionEnabled = false
val result = chooser.showOpenDialog(this)
if (result == JFileChooser.APPROVE_OPTION) {
if (chooser.selectedFile.extension == "prg") {
vm.loadFileInRam(chooser.selectedFile, null)
} else {
val addressStr = JOptionPane.showInputDialog(this,
"The selected file isn't a .prg.\nSpecify memory load address (hexadecimal) manually.",
"Load address", JOptionPane.QUESTION_MESSAGE, null, null,
"$") as String
val loadAddress = Integer.parseInt(addressStr.removePrefix("$"), 16)
vm.loadFileInRam(chooser.selectedFile, loadAddress)
}
}
}
"reset" -> {
vm.reset()
vm.bus.reset()
updateCpu(vm.cpu, vm.bus)
}
"step" -> {
vm.step()
updateCpu(vm.cpu, vm.bus)
}
"pause" -> {
vm.pause(true)
pauseBt.actionCommand = "continue"
pauseBt.text = "Continue"
}
"continue" -> {
vm.pause(false)
pauseBt.actionCommand = "pause"
pauseBt.text = "Pause"
}
"irq" -> vm.cpu.irqAsserted = true
"nmi" -> vm.cpu.nmiAsserted = true
"quit" -> {
dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
}
}
}
fun updateCpu(cpu: Cpu6502, bus: Bus) {
val state = cpu.snapshot()
cyclesTf.text = state.cycles.toString()
regAtf.text = hexB(state.A)
regXtf.text = hexB(state.X)
regYtf.text = hexB(state.Y)
regPtf.text = "NV-BDIZC\n"+state.P.asInt().toString(2).padStart(8, '0')
regPCtf.text = hexW(state.PC)
regSPtf.text = hexB(state.SP)
val memory = listOf(bus[state.PC], bus[state.PC+1], bus[state.PC+2]).toTypedArray()
val disassem = disassembler.disassembleOneInstruction(memory, 0, state.PC).first.substringAfter(' ').trim()
disassemTf.text = disassem
if (zeropageTf.isVisible || stackpageTf.isVisible) {
val pages = vm.getZeroAndStackPages()
if (pages.isNotEmpty()) {
val zpLines = (0..0xff step 32).map { location ->
" ${'$'}${location.toString(16).padStart(2, '0')} "+((0..31).joinToString(" ") { lineoffset ->
pages[location+lineoffset].toString(16).padStart(2, '0')
})
}
val stackLines = (0x100..0x1ff step 32).map { location ->
"${'$'}${location.toString(16).padStart(2, '0')} "+((0..31).joinToString(" ") { lineoffset ->
pages[location+lineoffset].toString(16).padStart(2, '0')
})
}
zeropageTf.text = zpLines.joinToString("\n")
stackpageTf.text = stackLines.joinToString("\n")
}
}
speedKhzTf.text = "%.1f".format(cpu.averageSpeedKhzSinceReset)
}
}
private fun GridBagConstraints.update(gridx: Int, gridy: Int, gridwidth: Int?=null): GridBagConstraints {
val gc = this.clone() as GridBagConstraints
gc.gridx = gridx
gc.gridy = gridy
if(gridwidth!=null)
gc.gridwidth=gridwidth
return gc
}