In part 1 I implemented a CHIP-8 in Swift. In this part I will describe the process of adding a debugger.
The code can be found on Github in repo .
Writing an emulator with only print statements to aid me in spotting my mistakes was tedious and error-prone. The logs would have to be combed meticulously and compared with the code in the ROM (which had to be decoded by hand) to work out where the mistakes were.
A debugger would allow me to look at the values in memory in the GUI, and step through the program opcode by opcode.
Before building the debugger UI, there were a couple of design issues in the code that needed to be addressed:
- Almost all the code was inside the default ViewController class, hindering multiple views backed by the emulator state.
- Everything except rendering the display was done in a background thread. If I wanted to display the emulator state, any updates to the state would need to be done on the main thread.
The first step was to fix these issues.
Breaking down the code
In the initial design every CHIP-8 memory array was stored directly in the ViewController:
These were accompanied by the implementation of every single opcode.
The new design looks like this:
The ViewController now has responsibility more in accordance with its status. It powers the user interface, and kicks off the emulator when required. The CPU is responsible for actually running the instructions, and modifying the various regions of memory accordingly, which are now segregated into classes.
Swift on macOS benefits from Grand Central Dispatch, which means that resolving the threading issue is relatively straightforward: identify areas which cause UI changes, and make sure they are dispatched to the main thread.
Showing the contents of registers
To show the contents of the registers I needed a table view with two columns: a register label and the contents of the register itself. The table view would be backed by an
ArrayController which would bridge between the registers array and the table.
At this point I hit a couple of snags:
UInt8is not a subclass of
NSObject, and so can’t be the source of an
UInt8array provides no binding for the array index.
The solution to both these problems was to create
ByteCell, a trivial sublass of
NSObject containing a register index and the value of the register. An array of
ByteCells would then be wrapped in a class which would allow the rest of the application to ignore this implementational detail.
I then bound the
ArrayController to the array of
ByteCells and the table view to the
ArrayController, running the index through an
NSFormatter to get a more sensible label.
Showing the contents of the stack
Showing the stack is basically the same as showing the registers (albeit with
UInt16s rather than
UInt8s). The main difference is that it would be nice to highlight the value that is currently at the top of the stack.
Selection is achieved by binding the
selectionIndexes property of a
TableView to an
IndexSet, and adding and removing the value of the
SP register as it changes. An
IndexSet can contain multiple items, but this isn’t a feature that I require here.
At this point it was becoming apparent that the debugger was a drag on the performance of the rest of the emulator. It was time to break out the profiler and identify areas to optimise.
The macOS Developer Tools include Instruments for this purpose.
By running Swift8 through Instruments and recording the CPU times, it becomes clear that the biggest chunk of time is spent inside Core Animation transactions. Performance here can be improved by creating a separate Core Animation layer for any UI component which is redrawn frequently.
Running the profiler again with these changes shows an approximately 50% speedup in this area, giving improved emulator performance.
RAM is similar to the registers, but is obviously much larger. As such, it can be represented as an array of
However, the RAM stores the code of the currently executing program as well as its data. It would be nice to display any cells containing instructions in their disassembled form for easier debugging. It’s also useful to keep the current instruction in view as the code is executed, which was not an issue for the Stack.
Some cells may contain values which do not correspond to an opcode, in which case no sensible value can be displayed. Other byte pairs may look like instructions, but will never actually be executed. Given that CHIP-8 code may jump arbitrarily, it is not possible to identify these codes up front.
Given that there is no segregation of code and data, any RAM update may potentially be modifying code, and requires the instructions in the corresponding cells to be updated.
CHIP-8 instructions are two bytes long. This means that any single RAM write requires us to compute two instructions:
- The instruction starting at the current address
- The instruction starting at the previous addressed
If multiple sequential writes are preformed, I can optimise this by only updating the previous instruction until I get to end of the modified address range, before finally updating the last instruction.
This requires a new structure, similar to a
ByteCell but capable of storing an instruction string. However, if I’m going to decode instructions as they are written to RAM, it makes sense to cache this and reuse it when the instruction comes to be executed. Therefore I need to store a mapping from the instruction to the corresponding CPU function.
Putting this together, I wind up with the following:
I then need an
Instruction implementation for every instruction, whose
mnemonic is the display string and whose
execute method calls the correct method on the CPU.
This changes the CPU’s run loop to simply reading the next instruction from RAM and calling the execute method, passing itself as the
Pause, Continue and Step
Any good debugger allows the ability to pause code at arbitrary points. I can implement this with a flag which when set prevents execution of the next instruction. I then need three new UI controls:
- Set the pause flag
- Clear the pause flag, and execute the next instruction
- Execute the next instruction, but do not clear the pause flag
It should only be possible to pause the emulator if the program is running, and it should only be possible to step and continue if the program is paused.
By adding some synthetic properties to the view controller based on the flags, this is straightforward to implement.
A logical extension of the pause and resume functionality is the ability to set breakpoints - being able to determine ahead of time where to pause without needing cat-like reflexes to pause at the correct point.
This can be implemented by adding a checkbox column to the RAM table in the UI, and adding a boolean breakpoint property to each instruction which binds to the value of the checkbox.
All we need to do then is to check the value of the instructions breakpoint property before it is executed, and setting the pause flag as necessary. The step and continue buttons can then be used to navigate, and it all just works.
Removing the performance overhead
Despite the optimisations made above, the debugger still acts as a drag on performance. Even if the debugger remains unused, the views still get rendered and all that scrolling doesn’t come cheap.
The solution to this is to offload the debugger to a separate window, which can be opened or closed as desired.
This requires the creation of a separate view controller, which handles the debugger related functionality, and rearranging the Storyboard accordingly.
At this point the debugger – and the emulator itself – are now complete. This provides a good foundation for the construction of a more sophisticated emulator.