# HG changeset patch # User Steve Losh # Date 1482117303 18000 # Node ID 6911e751d39af2b5970f06600219d57202bcc7a5 # Parent 64e956e4603b860ea48982561ff9e4ed3ba0fc9f Add draft of CHIP-8 graphics post diff -r 64e956e4603b -r 6911e751d39a config.toml --- a/config.toml Sun Dec 18 12:55:25 2016 -0500 +++ b/config.toml Sun Dec 18 22:15:03 2016 -0500 @@ -4,6 +4,7 @@ PluralizeListTitles = false PygmentsCodeFences = true pygmentsuseclasses = true +disableHugoGeneratorInject = true [Params] DateForm = "January 2, 2006" diff -r 64e956e4603b -r 6911e751d39a content/blog/2016/12/chip8-cpu.markdown --- a/content/blog/2016/12/chip8-cpu.markdown Sun Dec 18 12:55:25 2016 -0500 +++ b/content/blog/2016/12/chip8-cpu.markdown Sun Dec 18 22:15:03 2016 -0500 @@ -581,7 +581,7 @@ ## Instructions -The CHIP-8 support thirty six instructions, all of which we'll need to +The CHIP-8 supports thirty-six instructions, all of which we'll need to implement. We'll start with the simpler ones, and we'll be leaving some of the others (the graphics/sound related ones) for later articles. @@ -627,7 +627,7 @@ We'll start by removing the need to use the `with-chip` macro. Every instruction needs to deal with the `chip` struct, so let's not repeat ourselves -thirty six times. Every instruction will also take `chip` and `instruction` +thirty-six times. Every instruction will also take `chip` and `instruction` arguments, so we can remove those too: ```lisp @@ -688,26 +688,6 @@ Now instead of `(aref registers ...)` we can just say `(register ...)`. Cool. -```lisp -(defmacro define-instruction (name &body body) - `(progn - (declaim (ftype (function (chip int16) null) ,name)) - (defun ,name (chip instruction) - (declare (ignorable instruction)) - (with-chip (chip) - (macrolet ((register (index) - `(aref registers ,index))) - (let ,(parse-instruction-argument-bindings argument-list) - ,@body)) - nil)))) - -(define-instruction op-rand - (let ((reg (logand #x0F00 instruction)) - (mask (logand #x00FF instruction))) - (setf (aref registers reg) - (logand (random 256) mask)))) -``` - The one thing that still bothers me is having to manually pull the instruction arguments out of the instruction with bitmasking. It would be much nicer if we could just declare what the arguments look like and have the computer generate @@ -844,7 +824,7 @@ (logand (random 256) mask))) ``` -This is going to pay off nicely as we implement the other thirty five +This is going to pay off nicely as we implement the other thirty-five instructions. ### Jumps and Calls @@ -948,13 +928,13 @@ ```lisp (digit 0 135) -1 +5 (digit 1 135) 3 (digit 2 135) -5 +1 (digit 0 #xD6 16) 6 @@ -977,7 +957,7 @@ ### Arithmetic -Next up at the arithmetic instructions. The CHIP-8 only supports addition and +Next up are the arithmetic instructions. The CHIP-8 only supports addition and subtraction — there are no multiplication or division instructions. The first two instructions are `ADD` and `SUB`: @@ -1228,7 +1208,7 @@ loads. Normally we'd implement these first, but I wanted to introduce `macro-map` as gently as possible. -Most of the `LD` instructions simple take a value from a source and stick it +Most of the `LD` instructions simply take a value from a source and stick it into a destination, and we can implement them as a single `setf` form: ```lisp @@ -1299,3 +1279,6 @@ * Graphics and input * Sound * Debugging + +*Thanks to [James Cash](https://twitter.com/jamesnvc) for reading a draft of +this post.* diff -r 64e956e4603b -r 6911e751d39a content/blog/2016/12/chip8-graphics.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/blog/2016/12/chip8-graphics.markdown Sun Dec 18 22:15:03 2016 -0500 @@ -0,0 +1,625 @@ ++++ +title = "CHIP-8 in Common Lisp: Graphics" +snip = "Let's draw some pixels." +date = 2016-12-20T14:50:00Z +draft = true + ++++ + +In the previous post we looked at how to emulate a [CHIP-8][] CPU with Common +Lisp. But a CPU alone isn't much fun to play, so in this post we'll add +a screen to the emulator with [Qt][]. + +The full emulator source is on [BitBucket][] and [GitHub][]. + +[CHIP-8]: https://en.wikipedia.org/wiki/CHIP-8 +[Qt]: https://www.qt.io/ +[BitBucket]: https://bitbucket.org/sjl/cl-chip8 +[GitHub]: https://github.com/sjl/cl-chip8 + +
+ +## Qtools + +[Qtools][] is a library that wraps up a few other libraries to make it easier to +write Qt interfaces with Common Lisp. It's a big library so I'm not going to +try to explain everything about it here. Most of the code here should be pretty +easy to follow even if you haven't used it before (I'll explain the high-level +concepts), but if you're interested in *exactly* how some code works you should +check out its documentation. + +[qtools]: https://shinmera.github.io/qtools/ + +## Architecture + +Let's take a moment to look at the overall architecture of the project. So far +we've got a `chip` struct that holds the state of the emulated system, and +a bunch of `op-...` instruction functions that emulate the instructions. We +could start plugging in drawing calls right in the appropriate instructions, and +this is the approach a lot of emulators take. But I'd like to separate things +a bit more strictly. + +My goal is to keep the emulated system entirely self-contained, and then layer +the screen and user interface on top. The emulator should ideally know +*nothing* about the existence of an interface. This keeps the emulator simple +and (mostly) free of cruft. It will also let us play around with alternate +interfaces if we want to — I think it might be fun to add an ASCII screen with +[ncurses][] and [cl-charms][] some day! + +With that said, we will make a *few* concessions to performance along the way. + +[ncurses]: https://en.wikipedia.org/wiki/Ncurses +[cl-charms]: https://github.com/HiTECNOLOGYs/cl-charms + +## The Emulation Layer +### Video Memory and Performance + +The CHIP-8 has a 64x32 pixel display, and each pixel has only two colors: on and +off. It's about as simple a screen as you can get. + +To keep the emulator from having to know about the user interface we'll model +the screen as a big array of video memory. The emulator can set the video +memory appropriately, and the user interface can read it to determine what to +draw to the screen at any given point. + +There are other ways we could have separated things, but let's run with this +strategy for now. + +We'll add a video memory array to our `chip` struct: + +```lisp +(defconstant +screen-width+ 64) +(defconstant +screen-height+ 32) + +(defstruct chip + ; ... + (video (make-array (* +screen-height+ +screen-width+) :element-type 'fixnum) + :type (simple-array fixnum (#.(* +screen-height+ +screen-width+))) + :read-only t) + ; ... + ) +``` + +Here we already see a first concession to performance. We could have used +a multidimensional array to make the indexing a bit nicer, but by using a simple +flat array we'll be able to pass it directly to OpenGL later. + +We'll add a couple of helper functions to make the indexing of this array a bit +less painful: + +```lisp +(defun-inline vref (chip x y) + (aref (chip-video chip) (+ (* +screen-width+ y) x))) + +(defun-inline (setf vref) (new-value chip x y) + (setf (aref (chip-video chip) (+ (* +screen-width+ y) x)) + new-value)) +``` + +Now we can simply say `(vref chip 5 15)` to get the pixel at (5, 15). + +We'll add one more field to `chip` before moving on, a "dirty" flag: + +```lisp +(defstruct chip + ; ... + (video-dirty t :type boolean) + ; ... + ) +``` + +This will make it easier for any interface to determine whether it needs to +update the display or not. + + + +### Fonts + +The CHIP-8 spec sets aside a portion of main memory starting at address `#x50` +to contain sprites for the hex digits 0 through F. Check out [the display +chapter of Cowgod's guide][cg-display] for an overview of how the CHIP-8 defines +sprites. + +We'll need to load these sprites into our emulator's memory at the correct +location when resetting it. We'll also clear out the video memory while we're +at it: + +```lisp +(defun load-font (chip) + ;; Thanks http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/ + (replace (chip-memory chip) + #(#xF0 #x90 #x90 #x90 #xF0 ; 0 + #x20 #x60 #x20 #x20 #x70 ; 1 + #xF0 #x10 #xF0 #x80 #xF0 ; 2 + #xF0 #x10 #xF0 #x10 #xF0 ; 3 + #x90 #x90 #xF0 #x10 #x10 ; 4 + #xF0 #x80 #xF0 #x10 #xF0 ; 5 + #xF0 #x80 #xF0 #x90 #xF0 ; 6 + #xF0 #x10 #x20 #x40 #x40 ; 7 + #xF0 #x90 #xF0 #x90 #xF0 ; 8 + #xF0 #x90 #xF0 #x10 #xF0 ; 9 + #xF0 #x90 #xF0 #x90 #x90 ; A + #xE0 #x90 #xE0 #x90 #xE0 ; B + #xF0 #x80 #x80 #x80 #xF0 ; C + #xE0 #x90 #x90 #x90 #xE0 ; D + #xF0 #x80 #xF0 #x80 #xF0 ; E + #xF0 #x80 #xF0 #x80 #x80) ; F + :start1 #x50)) + +(defun reset (chip) + (with-chip (chip) + (fill memory 0) + (fill registers 0) + (fill video 0) ; NEW + (load-font chip) ; NEW + (replace memory (read-file-into-byte-vector loaded-rom) + :start1 #x200) + (setf running t + video-dirty t ; NEW + program-counter #x200 + (fill-pointer stack) 0)) + (values)) +``` + +Once again the handy `replace` function makes things easy. + +[cg-display]: http://devernay.free.fr/hacks/chip8/C8TECH10.HTM + +### Clearing the Screen: CLS + +Now we can start implementing the graphics-related instructions. The first is +the very simple `CLS` to clear the screen: + +```lisp +(define-instruction op-cls () ;; CLS + (fill video 0) + (setf video-dirty t)) +``` + +### Loading Fonts: LD F, Vx + +Next up is the "load font" instruction, which sets the index register to the +address of the sprite for the digit in the argument register. So `LD F, V2` +where register 2 contains `6` would set the index register to the address of the +`6` sprite. + +```lisp +(defun-inline font-location (character) + (+ #x50 (* character 5))) ; each sprite is 5 bytes wide + +(define-instruction op-ld-font + Screen Texture + ┌───────────────────────────────┐ ┌──────────────────────┐ + │ │ │ ▉ ▉ ▉ ▉ │ + │ ▉▉ ▉▉ ▉▉ ▉▉ │ │ ▉ ▉ ▉ ▉ │ + │ ▉▉ ▉▉ ▉▉ ▉▉ │ │ ▉▉▉▉▉ ▉ ▉ │ + │ ▉▉ ▉▉ ▉▉ ▉▉ │ │ ▉ ▉ ▉ │ + │ ▉▉ ▉▉ ▉▉ ▉▉ │ │ ▉ ▉ ▉ ▉ │ + │ ▉▉▉▉▉▉▉▉▉▉ ▉▉ ▉▉ │ │░░░░░░░░░░░░░░░░░░░░░░│ + │ ▉▉ ▉▉ ▉▉ ▉▉ │ │░░░░░░░░░░░░░░░░░░░░░░│ + │ ▉▉ ▉▉ ▉▉ ▉▉ │ │░░░░░░░░░░░░░░░░░░░░░░│ + │ ▉▉ ▉▉ ▉▉ │ │░░░░░░░░░░░░░░░░░░░░░░│ + │ ▉▉ ▉▉ ▉▉ ▉▉ │ │░░░░░░░░░░░░░░░░░░░░░░│ + │ ▉▉ ▉▉ ▉▉ ▉▉ │ └──────────────────────┘ + └───────────────────────────────┘ + + +Note that we're only every using the top half of the texture — the screen is +a 2:1 rectangle but OpenGL likes square textures. + +Getting the texture coordinates on the quad's vertices correct is important, +otherwise you'll end up drawing whatever garbage happened to be in memory at the +time which, while entertaining, is probably not what you want. + +### Wrapping Up + +The last thing we need is a function to actually create the GUI: + +```lisp +(defun run-gui (chip) + (with-main-window + (window (make-screen chip)))) +``` + +And now we can modify our emulator's `run` function to start up a GUI in +addition to the system emulation: + +```lisp +(defun run (rom-filename) + (let ((chip (make-chip))) + (setf *c* chip) + (load-rom chip rom-filename) + (bt:make-thread (curry #'run-cpu chip)) ; NEW + (chip8.gui.screen::run-gui chip))) ; NEW +``` + +Qt will take control of the thread and block when run, so we'll need to run our +CPU emulation in a separate thread. + +The only thing they both write to is the `video-dirty` flag, so there's not much +synchronization to deal with (yet). It's theoretically possible that a badly +timed repaint could draw a half-finished sprite on the screen, but in practice +it's not noticeable. + +A different architecture (e.g. passing down pixel-drawing functions into the +emulator from the UI) could solve that problem while keeping the layer separate, +but it didn't seem worth the extra effort for this little toy project. + +## Results + +And with all that done we've *finally* got a screen to play games on! + +[![Screenshot of CHIP-8 screen running UFO.rom](/media/images/blog/2016/12/chip8-screen.png)](/media/images/blog/2016/12/chip8-screen.png) + +## Future + +That's all for the graphics. In the next post we'll add user input, and then +later we'll look at sound and debugging. diff -r 64e956e4603b -r 6911e751d39a static/media/images/blog/2016/12/chip8-screen.png Binary file static/media/images/blog/2016/12/chip8-screen.png has changed