8685d199303f

Add input entry
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Mon, 19 Dec 2016 15:43:19 -0500
parents 88f0e0d8b891
children 6709f89d86a8
branches/tags (none)
files content/blog/2008/02/microsoft-entourage-applescript-frustration.markdown content/blog/2016/12/chip8-graphics.markdown content/blog/2016/12/chip8-input.markdown

Changes

--- a/content/blog/2008/02/microsoft-entourage-applescript-frustration.markdown	Mon Dec 19 13:21:27 2016 -0500
+++ b/content/blog/2008/02/microsoft-entourage-applescript-frustration.markdown	Mon Dec 19 15:43:19 2016 -0500
@@ -6,9 +6,6 @@
 
 +++
 
-
-## Hello?
-
 I've been working on a project lately to automate the setup of some rules and
 schedules in Microsoft Entourage. This isn't the easiest thing in the world
 because Entourage doesn't really support AppleScript for creating rules or
--- a/content/blog/2016/12/chip8-graphics.markdown	Mon Dec 19 13:21:27 2016 -0500
+++ b/content/blog/2016/12/chip8-graphics.markdown	Mon Dec 19 15:43:19 2016 -0500
@@ -52,6 +52,9 @@
 [cl-charms]: https://github.com/HiTECNOLOGYs/cl-charms
 
 ## The Emulation Layer
+
+We'll start with the emulation side of things.
+
 ### Video Memory and Performance
 
 The CHIP-8 has a 64x32 pixel display, and each pixel has only two colors: on and
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/content/blog/2016/12/chip8-input.markdown	Mon Dec 19 15:43:19 2016 -0500
@@ -0,0 +1,326 @@
++++
+title = "CHIP-8 in Common Lisp: Input"
+snip = "Adding user interaction."
+date = 2016-12-21T14:50:00Z
+draft = true
+
++++
+
+In the previous posts we looked at how to emulate a [CHIP-8][] CPU with Common
+Lisp, and added a screen to see the results.  This is enough for graphical demos
+like `maze.rom`, but in this post we'll add user input so we can actually *play*
+games.
+
+The full emulator source is on [BitBucket][] and [GitHub][].
+
+[CHIP-8]: https://en.wikipedia.org/wiki/CHIP-8
+[BitBucket]: https://bitbucket.org/sjl/cl-chip8
+[GitHub]: https://github.com/sjl/cl-chip8
+
+<div id="toc"></div>
+
+## The Keypad
+
+The CHIP-8 was designed to work with a hexadecimal keypad like this:
+
+<pre class="lineart">
+    ┌─┬─┬─┬─┐
+    │1│2│3│C│
+    ├─┼─┼─┼─┤
+    │4│5│6│D│
+    ├─┼─┼─┼─┤
+    │7│8│9│E│
+    ├─┼─┼─┼─┤
+    │A│0│B│F│
+    └─┴─┴─┴─┘
+</pre>
+
+This is kind of a strange input device compared to most other keyboards and
+controllers.  Often it can be a bit tricky to figure out how to control
+a particular game, since most don't include any instructions.  But at least the
+*implementation* is pretty simple.
+
+## Architecture
+
+Once again we'll separate the emulation layer from the user interface layer,
+like we did with the graphics.  The `chip` struct will have an array of
+"keyboard memory" to read from, and the user interface will write into this
+array as the user presses keys.
+
+## The Emulation Layer
+
+We'll start with the emulation side of things.
+
+### Keyboard Memory
+
+First we'll add an array of sixteen booleans to the `chip` struct to hold the
+currently-pressed state of each keypad key:
+
+```lisp
+(defstruct chip
+  ; ...
+  (keys (make-array 16 :element-type 'boolean :initial-element nil)
+        :type (simple-array boolean (16))
+        :read-only t)
+  ; ...
+  )
+```
+
+We'll also add two functions for writing to this array that the user interface
+will eventually use:
+
+```lisp
+(defun keydown (chip key)
+  (setf (aref (chip-keys chip) key) t))
+
+(defun keyup (chip key)
+  (setf (aref (chip-keys chip) key) nil))
+```
+
+And let's make sure to clear it when we reset the emulator:
+
+```lisp
+(defun reset (chip)
+  (with-chip (chip)
+    ; ...
+    (fill keys nil)
+    ; ...
+    )
+  (values))
+```
+
+### Keyboard Branching: SKP and SKNP
+
+The CHIP-8 has two keyboard-related branching instructions: `SKP` and `SKNP`
+("skip when pressed" and "skip when not pressed").
+
+Much like the other branching instructions the make the CPU skip the next
+instruction if a particular key is pressed.  Their implementation is pretty
+simple — to check if key `N` is pressed we just look at the value of index `N`
+in the keyboard memory:
+
+```lisp
+(define-instruction op-skp (_ r _ _)                    ;; SKP
+  (when (aref keys (register r))
+    (incf program-counter 2)))
+
+(define-instruction op-sknp (_ r _ _)                   ;; SKNP
+  (when (not (aref keys (register r)))
+    (incf program-counter 2)))
+```
+
+### Waiting for Input: LD Vx, K
+
+The other keyboard-related instruction is a bit trickier.  From [Cowgod's
+documentation][cg-key]:
+
+    Fx0A - LD Vx, K
+    Wait for a key press, store the value of the key in Vx.
+
+    All execution stops until a key is pressed, then the value of
+    that key is stored in Vx.
+
+It's not 100% clear what "all execution" means, as we'll see when we get to the
+post on audio.  But we can put together a basic implementation pretty easily:
+
+```lisp
+(define-instruction op-ld-reg<key (_ r _ _)    ;; LD Vx, Key (await)
+  (let ((key (position t keys)))
+    (if key
+      (setf (register r) key)
+      ;; If we don't have a key, just execute this instruction
+      ;; again next time.
+      (decf program-counter 2))))
+```
+
+The `(position t keys)` here takes advantage of the fact that the index of
+a slot in the `keys` array also happens to be the name/number of the key.  If
+`position` doesn't find a `t` in the array it will just return `nil`.
+
+To simulate the stopping of execution we just use a little trick: we rewind the
+program counter so this instruction gets executed again on the next cycle.
+
+We could probably come up with a way to avoid this busy-looping, but it would
+add extra complexity, especially when we get to the pausing and debugging
+infrastructure later.  This should work fine for our needs.
+
+[cg-key]: http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#Fx0A
+
+## The User Interface Layer
+
+That's all we have to do on the emulation side of things.  Now we need to get Qt
+to read our keyboard input and pass it along to the `chip`.
+
+### Key Mappings
+
+The first thing we'll need to do is decide how we'd like to map the CHIP-8's
+keyboard onto our modern keyboard.  If you've got a keyboard with a number pad
+you might prefer something like this:
+
+<pre class="lineart">
+    Original Chip-8 Pad → Modern Numpad
+    ┌─┬─┬─┬─┐             ┌─┬─┬─┬─┐
+    │1│2│3│C│             │←│/│*│-│
+    ├─┼─┼─┼─┤             ├─┼─┼─┼─┤
+    │4│5│6│D│             │7│8│9│+│
+    ├─┼─┼─┼─┤             ├─┼─┼─┤ │
+    │7│8│9│E│             │4│5│6│ │
+    ├─┼─┼─┼─┤             ├─┼─┼─┼─┤
+    │A│0│B│F│             │1│2│3│↲│
+    └─┴─┴─┴─┘             ├─┴─┼─┤ │
+                          │0  │.│ │
+                          └───┴─┴─┘
+</pre>
+
+Which we can implement with a big `qtenumcase`:
+
+```lisp
+(defun pad-key-for (code)
+  (qtenumcase code
+    ((q+:qt.key_clear) #x1)
+    ((q+:qt.key_slash) #x2)
+    ((q+:qt.key_asterisk) #x3)
+    ((q+:qt.key_minus) #xC)
+
+    ((q+:qt.key_7) #x4)
+    ((q+:qt.key_8) #x5)
+    ((q+:qt.key_9) #x6)
+    ((q+:qt.key_plus) #xD)
+
+    ((q+:qt.key_4) #x7)
+    ((q+:qt.key_5) #x8)
+    ((q+:qt.key_6) #x9)
+    ((q+:qt.key_enter) #xE)
+
+    ((q+:qt.key_1) #xA)
+    ((q+:qt.key_2) #x0)
+    ((q+:qt.key_3) #xB)
+    ((q+:qt.key_0) #xF)))
+```
+
+Note that even though the constants like `q+:qt.key_clear` end up being numbers,
+you still need to surround them with parentheses for qtools' magic name-mangling
+to take effect properly.  If you just say `(qtenumcase code (q+:qt.key_clear #x1) ...)`
+you'll get an error like:
+
+```
+;     (QTOOLS:QTENUMCASE CHIP8.GUI.SCREEN::CODE
+;       (QTOOLS:Q+ "QT.KEY_CLEAR" 1))
+; --> LET COND IF
+; ==>
+;   (QT:ENUM-EQUAL #:KEY0 QTOOLS:Q+)
+;
+; caught WARNING:
+;   undefined variable: Q+
+;
+; compilation unit finished
+;   Undefined variable:
+;     Q+
+;   caught 1 WARNING condition
+```
+
+The magic name-mangling in qtools bothers me a little, but I'm sure the
+alternative would be far more verbose, so I live with it.
+
+Anyway, moving on.  If you've got a laptop or a tenkeyless keyboard you might
+prefer a key mapping scheme more like this:
+
+<pre class="lineart">
+    Original Chip-8 Pad → Laptop
+    ┌─┬─┬─┬─┐             ┌─┬─┬─┬─┐
+    │1│2│3│C│             │1│2│3│4│
+    ├─┼─┼─┼─┤             ├─┼─┼─┼─┤
+    │4│5│6│D│             │Q│W│E│R│
+    ├─┼─┼─┼─┤             ├─┼─┼─┼─┤
+    │7│8│9│E│             │A│S│D│F│
+    ├─┼─┼─┼─┤             ├─┼─┼─┼─┤
+    │A│0│B│F│             │Z│X│C│V│
+    └─┴─┴─┴─┘             └─┴─┴─┴─┘
+</pre>
+
+```lisp
+(defun pad-key-for (code)
+  (qtenumcase code
+    ((q+:qt.key_1) #x1)
+    ((q+:qt.key_2) #x2)
+    ((q+:qt.key_3) #x3)
+    ((q+:qt.key_4) #xC)
+
+    ((q+:qt.key_q) #x4)
+    ((q+:qt.key_w) #x5)
+    ((q+:qt.key_e) #x6)
+    ((q+:qt.key_r) #xD)
+
+    ((q+:qt.key_a) #x7)
+    ((q+:qt.key_s) #x8)
+    ((q+:qt.key_d) #x9)
+    ((q+:qt.key_f) #xE)
+
+    ((q+:qt.key_z) #xA)
+    ((q+:qt.key_x) #x0)
+    ((q+:qt.key_c) #xB)
+    ((q+:qt.key_v) #xF)))
+```
+
+Of course the ideal solution is to make the mapping configurable at run time,
+but I'll leave that as an exercise you can do if you're interested.
+
+### Qt Keyboard Overrides
+
+Now that we have `pad-key-for` to turn a Qt key into a CHIP-8 key we can write
+the overrides for our Qt `screen`.  We'll start with the `key-press-event`:
+
+```lisp
+(define-override (screen key-press-event) (ev)
+  (let* ((key (q+:key ev))
+         (pad-key (pad-key-for key)))
+    (when pad-key
+      (chip8::keydown chip pad-key)))
+  (stop-overriding))
+```
+
+We check to see if the key we got was a pad key, and if so we call `keyup`
+from earlier to mark it in the keyboard array.  We'll ignore any presses of
+unknown keys in this handler.
+
+Next we'll handle when the user releases a key:
+
+```lisp
+(define-override (screen key-release-event) (ev)
+  (let* ((key (q+:key ev))
+         (pad-key (pad-key-for key)))
+    (if pad-key
+      (chip8::keyup chip pad-key)
+      (qtenumcase key
+        ((q+:qt.key_escape) (die screen))
+        ((q+:qt.key_f1)     (chip8::reset chip))
+        (t (pr :unknown-key (format nil "~X" key))))))
+  (stop-overriding))
+```
+
+Much like `key-press-event` we check for a pad key and call `keyup` if we got
+one.  We also set up a couple of other handy mappings to control the emulator:
+
+* `F1` calls `reset` so we can easily reset the emulator without having to tab
+  back to the Lisp REPL.
+* `Esc` will quit the emulator (and GUI).
+
+I've also included a fallback case that will print out any unknown keys in hex.
+This was useful to cross reference with the Qt docs when I was trying to figure
+out what the Qt constant name was for a particular key like "clear" on the
+number pad.
+
+## Results
+
+That's all we need to do to handle user input!  Now we can finally *play* some
+games!  `ufo.rom` is one of my favorites, as is `blitz.rom` (once I figured out
+the screen clipping bug).
+
+## Future
+
+We're nearing the end of the emulator — the only thing strictly necessary is
+adding sound, which we'll take care of in the next post.
+
+After that I'll talk about adding some debugging infrastructure to the emulator
+so we can look at what's going on, as well as adding a basic graphical debugger
+UI with Qt.