diff --git a/src/main/java/com/loomcom/symon/Cpu.java b/src/main/java/com/loomcom/symon/Cpu.java
index 03d7576..e4eb9dc 100644
--- a/src/main/java/com/loomcom/symon/Cpu.java
+++ b/src/main/java/com/loomcom/symon/Cpu.java
@@ -24,6 +24,7 @@
package com.loomcom.symon;
import com.loomcom.symon.exceptions.MemoryAccessException;
+import com.loomcom.symon.util.HexUtil;
/**
* This class provides a simulation of the MOS 6502 CPU's state machine.
@@ -1134,23 +1135,23 @@ public class Cpu implements InstructionTable {
}
public String getAccumulatorStatus() {
- return String.format("$%02X", state.a);
+ return "$" + HexUtil.byteToHex(state.a);
}
public String getXRegisterStatus() {
- return String.format("$%02X", state.x);
+ return "$" + HexUtil.byteToHex(state.x);
}
public String getYRegisterStatus() {
- return String.format("$%02X", state.y);
+ return "$" + HexUtil.byteToHex(state.y);
}
public String getProgramCounterStatus() {
- return String.format("$%04X", state.pc);
+ return "$" + HexUtil.wordToHex(state.pc);
}
public String getStackPointerStatus() {
- return String.format("$%02X", state.sp);
+ return "$" + HexUtil.byteToHex(state.sp);
}
public int getProcessorStatus() {
@@ -1270,7 +1271,7 @@ public class Cpu implements InstructionTable {
/**
- * A compact representation of CPU state;
+ * A compact, struct-like representation of CPU state.
*/
public static class CpuState {
/**
@@ -1301,6 +1302,7 @@ public class Cpu implements InstructionTable {
public int[] args = new int[2];
public int instSize;
public boolean opTrap;
+
/* Status Flag Register bits */
public boolean carryFlag;
public boolean negativeFlag;
@@ -1319,7 +1321,8 @@ public class Cpu implements InstructionTable {
/**
* Snapshot a copy of the CpuState.
*
- * (This is a copy constructor rather than an implementation of Clonable based on Josh Bloch's recommendation)
+ * (This is a copy constructor rather than an implementation of Clonable
+ * based on Josh Bloch's recommendation)
*
* @param s The CpuState to copy.
*/
@@ -1346,19 +1349,21 @@ public class Cpu implements InstructionTable {
}
/**
- * Returns a string representing the CPU state.
+ * Returns a string formatted for the trace log.
+ *
+ * @return a string formatted for the trace log.
*/
- public String toString() {
+ public String toTraceEvent() {
String opcode = disassembleOp();
StringBuilder sb = new StringBuilder(getInstructionByteStatus());
sb.append(" ");
sb.append(String.format("%-14s", opcode));
- sb.append("A:" + String.format("%02x", a) + " ");
- sb.append("X:" + String.format("%02x", x) + " ");
- sb.append("Y:" + String.format("%02x", y) + " ");
- sb.append("F:" + String.format("%02x", getStatusFlag()) + " ");
- sb.append("S:" + String.format("1%02x", sp) + " ");
- sb.append(getProcessorStatusString());
+ sb.append("A:" + HexUtil.byteToHex(a) + " ");
+ sb.append("X:" + HexUtil.byteToHex(x) + " ");
+ sb.append("Y:" + HexUtil.byteToHex(y) + " ");
+ sb.append("F:" + HexUtil.byteToHex(getStatusFlag()) + " ");
+ sb.append("S:1" + HexUtil.byteToHex(sp) + " ");
+ sb.append(getProcessorStatusString() + "\n");
return sb.toString();
}
@@ -1395,11 +1400,17 @@ public class Cpu implements InstructionTable {
switch (Cpu.instructionSizes[ir]) {
case 0:
case 1:
- return String.format("%04X %02X ", lastPc, ir);
+ return HexUtil.wordToHex(lastPc) + " " +
+ HexUtil.byteToHex(ir) + " ";
case 2:
- return String.format("%04X %02X %02X ", lastPc, ir, args[0]);
+ return HexUtil.wordToHex(lastPc) + " " +
+ HexUtil.byteToHex(ir) + " " +
+ HexUtil.byteToHex(args[0]) + " ";
case 3:
- return String.format("%04X %02X %02X %02X", lastPc, ir, args[0], args[1]);
+ return HexUtil.wordToHex(lastPc) + " " +
+ HexUtil.byteToHex(ir) + " " +
+ HexUtil.byteToHex(args[0]) + " " +
+ HexUtil.byteToHex(args[1]);
default:
return null;
}
@@ -1422,35 +1433,35 @@ public class Cpu implements InstructionTable {
switch (instructionModes[ir]) {
case ABS:
- sb.append(String.format(" $%04X", address(args[0], args[1])));
+ sb.append(" $" + HexUtil.wordToHex(address(args[0], args[1])));
break;
case ABX:
- sb.append(String.format(" $%04X,X", address(args[0], args[1])));
+ sb.append(" $" + HexUtil.wordToHex(address(args[0], args[1])) + ",X");
break;
case ABY:
- sb.append(String.format(" $%04X,Y", address(args[0], args[1])));
+ sb.append(" $" + HexUtil.wordToHex(address(args[0], args[1])) + ",Y");
break;
case IMM:
- sb.append(String.format(" #$%02X", args[0]));
+ sb.append(" #$" + HexUtil.byteToHex(args[0]));
break;
case IND:
- sb.append(String.format(" ($%04X)", address(args[0], args[1])));
+ sb.append(" ($" + HexUtil.wordToHex(address(args[0], args[1])) + ")");
break;
case XIN:
- sb.append(String.format(" ($%02X,X)", args[0]));
+ sb.append(" ($" + HexUtil.byteToHex(args[0]) + ",X)");
break;
case INY:
- sb.append(String.format(" ($%02X),Y", args[0]));
+ sb.append(" ($" + HexUtil.byteToHex(args[0]) + "),Y");
break;
case REL:
case ZPG:
- sb.append(String.format(" $%02X", args[0]));
+ sb.append(" $" + HexUtil.byteToHex(args[0]));
break;
case ZPX:
- sb.append(String.format(" $%02X,X", a));
+ sb.append(" $" + HexUtil.byteToHex(a) + ",X");
break;
case ZPY:
- sb.append(String.format(" $%02X,Y", a));
+ sb.append(" $" + HexUtil.byteToHex(a) + ",Y");
break;
}
diff --git a/src/main/java/com/loomcom/symon/Simulator.java b/src/main/java/com/loomcom/symon/Simulator.java
index df8d16f..cf58a1f 100644
--- a/src/main/java/com/loomcom/symon/Simulator.java
+++ b/src/main/java/com/loomcom/symon/Simulator.java
@@ -247,6 +247,7 @@ public class Simulator implements Observer {
// Spin up the new run loop
runLoop = new RunLoop();
runLoop.start();
+ traceLog.simulatorDidStart();
}
private void handleStop() {
@@ -268,6 +269,7 @@ public class Simulator implements Observer {
statusPane.updateState(cpu);
}
});
+ traceLog.simulatorDidStop();
if (traceLog.isVisible()) {
traceLog.refresh();
}
@@ -291,6 +293,8 @@ public class Simulator implements Observer {
ram.fill(0x00);
// Clear the console.
console.reset();
+ // Reset the trace log.
+ traceLog.reset();
// Update status.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
@@ -320,13 +324,9 @@ public class Simulator implements Observer {
* Perform a single step of the simulated system.
*/
private void step() throws MemoryAccessException {
-
cpu.step();
- // TODO: We need to profile this for performance. Possibly allow
- // a flag to turn trace on/off
- synchronized(traceLog) {
- traceLog.append(cpu.getCpuState());
- }
+
+ traceLog.append(cpu.getCpuState());
// Read from the ACIA and immediately update the console if there's
// output ready.
diff --git a/src/main/java/com/loomcom/symon/ui/TraceLog.java b/src/main/java/com/loomcom/symon/ui/TraceLog.java
index 85c05b1..9641fa9 100644
--- a/src/main/java/com/loomcom/symon/ui/TraceLog.java
+++ b/src/main/java/com/loomcom/symon/ui/TraceLog.java
@@ -31,54 +31,99 @@ import javax.swing.*;
import java.awt.*;
/**
- * This frame displays a trace of CPU execution. The most recent TRACE_LENGTH lines
+ * This frame displays a trace of CPU execution. The most recent TRACE_LENGTH lines
* are captured in a buffer and rendered to the JFrame's main text area upon request.
*/
public class TraceLog {
private FifoRingBuffer traceLog;
- private JFrame traceLogWindow;
- private JTextArea logArea;
+ private JFrame traceLogFrame;
+ private JTextArea traceLogTextArea;
- private static final Dimension SIZE = new Dimension(640, 480);
- private static final int MAX_LOG_LENGTH = 10000;
+ private static final Dimension MIN_SIZE = new Dimension(320, 200);
+ private static final Dimension PREFERRED_SIZE = new Dimension(640, 480);
+ private static final int MAX_LOG_LENGTH = 50000;
public TraceLog() {
traceLog = new FifoRingBuffer(MAX_LOG_LENGTH);
- traceLogWindow = new JFrame();
- traceLogWindow.setPreferredSize(SIZE);
- traceLogWindow.setResizable(true);
+ traceLogFrame = new JFrame();
+ traceLogFrame.setMinimumSize(MIN_SIZE);
+ traceLogFrame.setPreferredSize(PREFERRED_SIZE);
+ traceLogFrame.setResizable(true);
+ traceLogFrame.setTitle("Trace Log");
- traceLogWindow.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+ traceLogFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
- logArea = new JTextArea();
- logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+ traceLogTextArea = new JTextArea();
+ traceLogTextArea.setFont(new Font(Font.MONOSPACED, Font.BOLD, 12));
+ traceLogTextArea.setEditable(false);
- JScrollPane scrollableView = new JScrollPane(logArea);
+ JScrollPane scrollableView = new JScrollPane(traceLogTextArea);
- traceLogWindow.getContentPane().add(scrollableView);
- traceLogWindow.pack();
+ traceLogFrame.getContentPane().add(scrollableView);
+ traceLogFrame.pack();
// Don't show the frame. That action is controlled by the Simulator.
}
+ /**
+ * Redraw the display with the most recent MAX_LOG_LENGTH
+ * trace events. CAUTION: This can be a very expensive
+ * call.
+ */
public void refresh() {
- StringBuilder logString = new StringBuilder();
- for (Cpu.CpuState state : traceLog) {
- logString.append(state.toString());
- logString.append("\n");
+ synchronized (this) {
+ StringBuilder logString = new StringBuilder();
+ for (Cpu.CpuState state : traceLog) {
+ logString.append(state.toTraceEvent());
+ }
+ traceLogTextArea.setText(logString.toString());
}
- logArea.setText(logString.toString());
}
+ /**
+ * Reset the log area.
+ */
+ public void reset() {
+ synchronized (this) {
+ traceLog.reset();
+ traceLogTextArea.setText("");
+ traceLogTextArea.setEnabled(true);
+ }
+ }
+
+ /**
+ * Append a CPU State to the trace log.
+ *
+ * @param state The CPU State to append.
+ */
public void append(Cpu.CpuState state) {
- traceLog.push(new Cpu.CpuState(state));
+ synchronized(this) {
+ traceLog.push(new Cpu.CpuState(state));
+ }
}
+ public void simulatorDidStart() {
+ traceLogTextArea.setEnabled(false);
+ }
+
+ public void simulatorDidStop() {
+ traceLogTextArea.setEnabled(true);
+ }
+
+ /**
+ * Returns true if the Trace Log window is currently visible.
+ * @return ture if the Trace Log window is currently visible.
+ */
public boolean isVisible() {
- return traceLogWindow.isVisible();
+ return traceLogFrame.isVisible();
}
+ /**
+ * Sets the visibility of the Trace Log window.
+ *
+ * @param b True to make the Trace Log window visible, false to hide it.
+ */
public void setVisible(boolean b) {
- traceLogWindow.setVisible(b);
+ traceLogFrame.setVisible(b);
}
}
diff --git a/src/main/java/com/loomcom/symon/util/HexUtil.java b/src/main/java/com/loomcom/symon/util/HexUtil.java
new file mode 100644
index 0000000..b03c92b
--- /dev/null
+++ b/src/main/java/com/loomcom/symon/util/HexUtil.java
@@ -0,0 +1,77 @@
+package com.loomcom.symon.util;
+
+/**
+ * Hex String Utilities.
+ *
+ *
+ *
+ * But why? Java, after all, has a number of ways to convert an integer into a hex string,
+ * so it may look absurd to go to the trouble of writing yet another conversion! The answer is
+ * performance.
+ *
+ *
+ *
+ * The most convenient way to get a formatted hex value from an integer is with the String.format
+ * method, but this turns out to be extremely inefficient. Formatting a million integers
+ * with String.format takes something like 1600ms. Formatting the same number of integers
+ * with HexUtil takes only 160ms. This is on part with Integer.toHexString,
+ * but also allows the desired padding.
+ *
+ */
+public class HexUtil {
+ private static final String[] HEX_CONSTANTS = {"00", "01", "02", "03", "04", "05", "06", "07",
+ "08", "09", "0A", "0B", "0C", "0D", "0E", "0F",
+ "10", "11", "12", "13", "14", "15", "16", "17",
+ "18", "19", "1A", "1B", "1C", "1D", "1E", "1F",
+ "20", "21", "22", "23", "24", "25", "26", "27",
+ "28", "29", "2A", "2B", "2C", "2D", "2E", "2F",
+ "30", "31", "32", "33", "34", "35", "36", "37",
+ "38", "39", "3A", "3B", "3C", "3D", "3E", "3F",
+ "40", "41", "42", "43", "44", "45", "46", "47",
+ "48", "49", "4A", "4B", "4C", "4D", "4E", "4F",
+ "50", "51", "52", "53", "54", "55", "56", "57",
+ "58", "59", "5A", "5B", "5C", "5D", "5E", "5F",
+ "60", "61", "62", "63", "64", "65", "66", "67",
+ "68", "69", "6A", "6B", "6C", "6D", "6E", "6F",
+ "70", "71", "72", "73", "74", "75", "76", "77",
+ "78", "79", "7A", "7B", "7C", "7D", "7E", "7F",
+ "80", "81", "82", "83", "84", "85", "86", "87",
+ "88", "89", "8A", "8B", "8C", "8D", "8E", "8F",
+ "90", "91", "92", "93", "94", "95", "96", "97",
+ "98", "99", "9A", "9B", "9C", "9D", "9E", "9F",
+ "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7",
+ "A8", "A9", "AA", "AB", "AC", "AD", "AE", "AF",
+ "B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7",
+ "B8", "B9", "BA", "BB", "BC", "BD", "BE", "BF",
+ "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7",
+ "C8", "C9", "CA", "CB", "CC", "CD", "CE", "CF",
+ "D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7",
+ "D8", "D9", "DA", "DB", "DC", "DD", "DE", "DF",
+ "E0", "E1", "E2", "E3", "E4", "E5", "E6", "E7",
+ "E8", "E9", "EA", "EB", "EC", "ED", "EE", "EF",
+ "F0", "F1", "F2", "F3", "F4", "F5", "F6", "F7",
+ "F8", "F9", "FA", "FB", "FC", "FD", "FE", "FF"};
+
+ /**
+ * Very fast 8-bit int to hex conversion, with zero-padded output.
+ *
+ * @param val The unsigned 8-bit value to convert to a zero padded hexadecimal string.
+ * @return Two digit, zero padded hexadecimal string.
+ */
+ public static String byteToHex(int val) {
+ return HEX_CONSTANTS[val & 0xff];
+ }
+
+ /**
+ * Very fast 16-bit int to hex conversion, with zero-padded output.
+ *
+ * @param val The unsigned 16-bit value to convert to a zero padded hexadecimal string.
+ * @return Four digit, zero padded hexadecimal string.
+ */
+ public static String wordToHex(int val) {
+ StringBuilder sb = new StringBuilder(4);
+ sb.append(HEX_CONSTANTS[(val >> 8) & 0xff]);
+ sb.append(HEX_CONSTANTS[val & 0xff]);
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/loomcom/symon/HexUtilTest.java b/src/test/java/com/loomcom/symon/HexUtilTest.java
new file mode 100644
index 0000000..7dfa55a
--- /dev/null
+++ b/src/test/java/com/loomcom/symon/HexUtilTest.java
@@ -0,0 +1,36 @@
+package com.loomcom.symon;
+
+import com.loomcom.symon.util.HexUtil;
+import junit.framework.TestCase;
+
+public class HexUtilTest extends TestCase {
+ public void testByteToHex() {
+ assertEquals("FE", HexUtil.byteToHex(0xfe));
+ assertEquals("00", HexUtil.byteToHex(0));
+ assertEquals("0A", HexUtil.byteToHex(10));
+ }
+
+ public void testByteToHexIgnoresSign() {
+ assertEquals("FF", HexUtil.byteToHex(-1));
+ }
+
+ public void testByteToHexMasksLowByte() {
+ assertEquals("FE", HexUtil.byteToHex(0xfffe));
+ assertEquals("00", HexUtil.byteToHex(0xff00));
+ }
+
+ public void testWordToHex() {
+ assertEquals("0000", HexUtil.wordToHex(0));
+ assertEquals("FFFF", HexUtil.wordToHex(65535));
+ assertEquals("FFFE", HexUtil.wordToHex(65534));
+ }
+
+ public void testWordToHexIgnoresSign() {
+ assertEquals("FFFF", HexUtil.wordToHex(-1));
+ }
+
+ public void testWordToHexMasksTwoLowBytes() {
+ assertEquals("FFFE", HexUtil.wordToHex(0xfffffe));
+ assertEquals("FF00", HexUtil.wordToHex(0xffff00));
+ }
+}