mirror of https://github.com/irmen/ksim65.git
257 lines
11 KiB
Kotlin
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
|
|
}
|