CHIP-8 in Common Lisp: Menus

Posted on January 10th, 2017.

Our CHIP-8 emulator in Common Lisp is almost complete. It can play games, and we've got a rudimentary debugging system in place so we can figure out where things go wrong.

Up to now we've been communicating with the running emulator mostly through NREPL or SLIME. This is fine for development, but in this post we'll add some much-needed polish in the form of menus. This is the kind of boring work that often gets left until the end during game development, so let's just get it out of the way.

The full series of posts so far:

  1. CHIP-8 in Common Lisp: The CPU
  2. CHIP-8 in Common Lisp: Graphics
  3. CHIP-8 in Common Lisp: Input
  4. CHIP-8 in Common Lisp: Sound
  5. CHIP-8 in Common Lisp: Disassembly
  6. CHIP-8 in Common Lisp: Debugging Infrastructure
  7. CHIP-8 in Common Lisp: Menus

The full emulator source is on BitBucket and GitHub.

  1. Architecture
  2. Adding a Main Window
  3. Updating the Screen
  4. Adding Menus
    1. The File Menu
    2. The Display Menu
    3. The Sound Menu
  5. Results
  6. Future

Architecture

Qtools has some rudimentary support for menus. Unfortunately it won't quite work with our emulator as-is, so we'll need to shuffle things around a bit. When we added the screen to the emulator back in the graphics post we just created a subclass of QGLWidget and passed it along to with-main-window. This works for displaying the screen, but if you try to define-menu on this widget Qtools will signal an error.

What we need to do is create a QMainWindow widget instead, and put our QGLWidget inside of that. Then we can add some menus to the main window and everything should work great. It will end up being structured like this:

                QMainWindow
    ╔══════════════════════════════════╗
    ║ Menu1 │ Menu2 │ ...              ║
    ║───────┴───────┴──────────────────║
    ║ ┌──────────────────────────────┐ ║
    ║ │..............................│ ║
    ║ │..............................│ ║
    ║ │..........QGLWidget...........│ ║
    ║ │.........(the screen).........│ ║
    ║ │..............................│ ║
    ║ │..............................│ ║
    ║ │..............................│ ║
    ║ └──────────────────────────────┘ ║
    ╚══════════════════════════════════╝

Adding a Main Window

We'll start by creating the QMainWindow widget. It will just have a single slot to keep track of the chip struct it's displaying:

(define-widget main-window (QMainWindow)
  ((chip :accessor main-chip :initarg :chip)))

Next we'll define our screen widget to be a subwidget of the main window:

(define-subwidget (main-window screen) (make-instance 'screen)
  (setf (screen-chip screen) chip))

This ensures the screen widget will get cleaned up properly when the main window is closed.

Now we need to move some of the initialization code that used to be in the screen widget up into the main window, and also add a bit more to connect everything properly:

(define-initializer (main-window main-setup)
  (setf (q+:window-title main-window) "cl-chip8"
        (q+:central-widget main-window) screen
        (q+:focus-proxy main-window) screen))

The window-title needs to go on the top-level widget, so we can pull that out of the screen's initializer. We also set the screen to be the "central widget" of the main window. You can read the Qt docs for the full story, but essentially a QMainWindow is just a container for other widgets and we need to designate one as the primary widget.

We'll also want to set the focus proxy of the main window to be the screen, because we want the screen to handle keyboard input just like before. Setting the focus proxy tells Qt that whenever the main window gets focused it should actually focus the screen instead. If we didn't do this, then if the main window itself got focused (which can happen when tabbing through applications, clicking the title bar, etc) it wouldn't propagate the keystrokes down to the screen.

This is all kind of fiddly stuff, but it's the polish that separates a toy project from something that actually feels finished.

We'll also want to change our die function to close this main window, not just the screen. I'm going to cheat just a little bit and use a global variable to hold the currently-running main window for easy access:

(defparameter *main-window* nil) ; will get set later

(defun die ()
  (setf (chip8::chip-running (main-chip *main-window*)) nil)
  (q+:close *main-window*))

Finally, run-gui will need to start up a main-window instead of a screen:

(defun make-main-window (chip)
  (make-instance 'main-window :chip chip))

(defun run-gui (chip thunk)
  (with-main-window
      (window (setf *main-window* (make-main-window chip))) ; NEW
    (funcall thunk)))

I usually try not to use the return value of a setf form because I think it's kind of ugly, but it saved an entire let here so decided to I break my own style rule.

Updating the Screen

The only thing we need to change for the screen is its initializer:

(define-initializer (screen screen-setup)
  (setf (q+:focus-policy screen) (q+:qt.strong-focus)
        (q+:fixed-size screen) (values *width* *height*)))

We've removed the useless window-title that used to be here. Instead we set the focus policy to the screen to accept all kinds of focus. If you don't set this the widget will never be able to get focus (and thus receive keyboard events). We also leave in the size setting. The QMainWindow seems to pick up this size and scale itself appropriately, which is nice.

That's it for the architectural changes. All the screen's drawing and input-handling code can remain unchanged.

Adding Menus

We'll add a couple of menus to make it a bit easier to use the emulator without falling back to the Lisp REPL. There's a lot of things we could add, so I'll just cover a couple basic options.

The File Menu

Let's start with a simple File menu that will just have two items:

The menu definition is pretty straightforward:

(define-menu (main-window File)
  (:item ("Load ROM..." (ctrl o)) (load-rom main-window))
  (:item ("Quit" (ctrl q))        (die)))

Note that we use the Windows-centric shortcut key names. Qt will handle translating those to Mac-friendly versions when running on OS X.

We'll hide the details of loading the ROM in a load-rom helper function:

(defun load-rom (main-window)
  (let ((rom (get-rom-path main-window)))
    (when rom
      (chip8::load-rom (main-chip main-window) rom))))

load-rom just gets the path to load and loads it into the chip struct, assuming it's not nil. Once again we use a helper function to hide the details of getting the path to the ROM, because I'm a firm believer in one function to a function:

(defparameter *default-directory*
  (uiop:native-namestring
    (asdf:system-source-directory :cl-chip8)))

(defun get-rom-path (window)
  (let ((path (q+:qfiledialog-get-open-file-name
                window                ; parent widget
                "Load ROM"            ; dialog title
                *default-directory*   ; starting directory
                "ROM Files (*.rom);;All Files (*)"))) ; filters
    (if (string= path "")
      nil
      path)))

Qt has a nice static method QFileDialog.getOpenFileName that we can use to do the heavy lifting. It takes a parent widget (our main window), a title, a directory to start in, and a file filter string.

I've used some handy ASDF and UIOP functions to tell the file dialog to start in the directory where the emulator's code is located, because that's where I store my own ROMs. Another option would be to have it start in the user's home directory, or to make the location configurable.

The filter string is actually parsed by Qt, and by default will prevent the user from selecting any file that doesn't end in .rom. You might want to add a few more options extensions here if you think people will have named their ROMs differently. We also add a second filter that will let the user select any file, in case they have a ROM with a filename we haven't anticipated. The result looks like this:

Screenshot of the file selection dialog

Note that if the user cancels out of the file selection dialog Qt will return an empty string. We'll check for that and return a more Lispy nil from the function.

That's it for the File menu. It's basic, but it's a lot nicer to load ROMs through a normal dialog than to have to poke at the chip in the REPL.

The Display Menu

Back in the graphics post we saw how some ROMs expect the CHIP-8 to wrap sprites around the screen when their coordinates get too large, and other ROMs require that they not wrap. There's no good way to automatically detect this (aside from hashing particular ROMs and hard-coding the setting for those) so we'll expose this as an option to the user in a Display menu.

The menu itself is simple — we'll make a submenu that will contain the options and a helper function to actually do the work:

(define-menu (main-window Display)
  (:menu "Screen Wrapping"
    (:item "On" (set-screen-wrapping main-window t))
    (:item "Off" (set-screen-wrapping main-window nil))))

(defun set-screen-wrapping (main-window enabled)
  (setf (chip8::chip-screen-wrapping-enabled (main-chip main-window))
        enabled))

Now we've got a simple little menu for turning screen wrapping off and on:

Screenshot of the display menu

You might also want to reset the emulator automatically whenever this option is changed, because toggle screen wrapping as the emulator is running can produce... interesting results. But I like breaking games in fun ways, so I left it as-is.

In a perfect world these options wouldn't be vanilla :items but would instead be part of a QActionGroup. This would tell Qt to treat these items as a group, put a checkmark next to the currently-selected one, and so on. Unfortunately Qtools doesn't have a menu content type for action groups and the thought of implementing something like this makes me nauseas, so I'm satisfied with the :item kludge.

The Sound Menu

Our final menu will allow the user to select what kind of sound the buzzer should play, because it would be a shame to let all our work in the sound post go to waste. We'll implement the Sound menu just like the Display menu:

(define-menu (main-window Sound)
  (:menu "Sound Type"
    (:item "Sine" (set-sound-type main-window :sine))
    (:item "Square" (set-sound-type main-window :square))
    (:item "Sawtooth" (set-sound-type main-window :sawtooth))
    (:item "Triangle" (set-sound-type main-window :triangle))))

(defun set-sound-type (main-window type)
  (setf (chip8::chip-sound-type (main-chip main-window)) type))

Results

And with that we've got a basic menu system for the emulator:

Screenshot of the full menu

Future

We're nearing the end of the series. The next post will be about adding a graphical debugging interface, and will probably be the last in the series.

(Unless I get ambitious and try making a curses-based ASCII UI...)