572 lines
48 KiB
HTML
572 lines
48 KiB
HTML
|
<!DOCTYPE html>
|
||
|
<html lang='en'><head><meta charset='utf-8' /><meta name='pinterest' content='nopin' /><link href='http://stevelosh.com/static/css/style.css' rel='stylesheet' type='text/css' /><link href='http://stevelosh.com/static/css/print.css' rel='stylesheet' type='text/css' media='print' /><title>CHIP-8 in Common Lisp: Graphics / Steve Losh</title></head><body><header><a id='logo' href='http://stevelosh.com/'>Steve Losh</a><nav><a href='http://stevelosh.com/blog/'>Blog</a> - <a href='http://stevelosh.com/projects/'>Projects</a> - <a href='http://stevelosh.com/photography/'>Photography</a> - <a href='http://stevelosh.com/links/'>Links</a> - <a href='http://stevelosh.com/rss.xml'>Feed</a></nav></header><hr class='main-separator' /><main id='page-blog-entry'><article><h1><a href='index.html'>CHIP-8 in Common Lisp: Graphics</a></h1><p class='date'>Posted on December 21st, 2016.</p><p>In the previous post we looked at how to emulate a <a href="https://en.wikipedia.org/wiki/CHIP-8">CHIP-8</a> 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 <a href="https://www.qt.io/">Qt</a>.</p>
|
||
|
|
||
|
<p>The full series of posts so far:</p>
|
||
|
|
||
|
<ol>
|
||
|
<li><a href="../chip8-cpu/index.html">CHIP-8 in Common Lisp: The CPU</a></li>
|
||
|
<li><a href="index.html">CHIP-8 in Common Lisp: Graphics</a></li>
|
||
|
<li><a href="../chip8-input/index.html">CHIP-8 in Common Lisp: Input</a></li>
|
||
|
<li><a href="../chip8-sound/index.html">CHIP-8 in Common Lisp: Sound</a></li>
|
||
|
<li><a href="../../../2017/01/chip8-disassembly/index.html">CHIP-8 in Common Lisp: Disassembly</a></li>
|
||
|
<li><a href="../../../2017/01/chip8-debugging-infrastructure/index.html">CHIP-8 in Common Lisp: Debugging Infrastructure</a></li>
|
||
|
<li><a href="../../../2017/01/chip8-menus/index.html">CHIP-8 in Common Lisp: Menus</a></li>
|
||
|
</ol>
|
||
|
|
||
|
<p>The full emulator source is on <a href="https://bitbucket.org/sjl/cl-chip8">BitBucket</a> and <a href="https://github.com/sjl/cl-chip8">GitHub</a>.</p>
|
||
|
|
||
|
<ol class="table-of-contents"><li><a href="index.html#s1-qtools">Qtools</a></li><li><a href="index.html#s2-architecture">Architecture</a></li><li><a href="index.html#s3-the-emulation-layer">The Emulation Layer</a><ol><li><a href="index.html#s4-video-memory-and-performance">Video Memory and Performance</a></li><li><a href="index.html#s5-fonts">Fonts</a></li><li><a href="index.html#s6-clearing-the-screen-cls">Clearing the Screen: CLS</a></li><li><a href="index.html#s7-loading-fonts-ld-f-vx">Loading Fonts: LD F, Vx</a></li><li><a href="index.html#s8-drawing-sprites-drw-x-y-size">Drawing Sprites: DRW X, Y, Size</a></li></ol></li><li><a href="index.html#s9-the-user-interface-layer">The User Interface Layer</a><ol><li><a href="index.html#s10-basic-plan">Basic Plan</a></li><li><a href="index.html#s11-screen-widget">Screen Widget</a></li><li><a href="index.html#s12-drawing-frames">Drawing Frames</a></li><li><a href="index.html#s13-wrapping-up">Wrapping Up</a></li></ol></li><li><a href="index.html#s14-results">Results</a></li><li><a href="index.html#s15-future">Future</a></li></ol>
|
||
|
|
||
|
<h2 id="s1-qtools"><a href="index.html#s1-qtools">Qtools</a></h2>
|
||
|
|
||
|
<p><a href="https://shinmera.github.io/qtools/">Qtools</a> 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 <em>exactly</em> how some code works you should
|
||
|
check out its documentation.</p>
|
||
|
|
||
|
<h2 id="s2-architecture"><a href="index.html#s2-architecture">Architecture</a></h2>
|
||
|
|
||
|
<p>Let's take a moment to look at the overall architecture of the project. So far
|
||
|
we've got a <code>chip</code> struct that holds the state of the emulated system, and
|
||
|
a bunch of <code>op-...</code> 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.</p>
|
||
|
|
||
|
<p>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
|
||
|
<em>nothing</em> 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
|
||
|
<a href="https://en.wikipedia.org/wiki/Ncurses">ncurses</a> and <a href="https://github.com/HiTECNOLOGYs/cl-charms">cl-charms</a> some day!</p>
|
||
|
|
||
|
<p>With that said, we will make a <em>few</em> concessions to performance along the way.</p>
|
||
|
|
||
|
<h2 id="s3-the-emulation-layer"><a href="index.html#s3-the-emulation-layer">The Emulation Layer</a></h2>
|
||
|
|
||
|
<p>We'll start with the emulation side of things.</p>
|
||
|
|
||
|
<h3 id="s4-video-memory-and-performance"><a href="index.html#s4-video-memory-and-performance">Video Memory and Performance</a></h3>
|
||
|
|
||
|
<p>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.</p>
|
||
|
|
||
|
<p>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.</p>
|
||
|
|
||
|
<p>There are other ways we could have separated things, but let's run with this
|
||
|
strategy for now.</p>
|
||
|
|
||
|
<p>We'll add a video memory array to our <code>chip</code> struct:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defconstant</span></i> +screen-width+ 64</span>)</span>
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defconstant</span></i> +screen-height+ 32</span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defstruct</span></i> chip
|
||
|
<span class="comment">; ...
|
||
|
</span> <span class="paren2">(<span class="code">video <span class="paren3">(<span class="code">make-array <span class="paren4">(<span class="code">* +screen-height+ +screen-width+</span>)</span> <span class="keyword">:element-type</span> 'fixnum</span>)</span>
|
||
|
<span class="keyword">:type</span> <span class="paren3">(<span class="code">simple-array fixnum <span class="paren4">(<span class="code">#.<span class="paren5">(<span class="code">* +screen-height+ +screen-width+</span>)</span></span>)</span></span>)</span>
|
||
|
<span class="keyword">:read-only</span> t</span>)</span>
|
||
|
<span class="comment">; ...
|
||
|
</span> </span>)</span></span></code></pre>
|
||
|
|
||
|
<p>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.</p>
|
||
|
|
||
|
<p>OpenGL is going to want this array to be in "X-major" order, so the array will
|
||
|
need to look like:</p>
|
||
|
|
||
|
<pre><code>[(x₀, y₀), (x₁, y₀), (x₂, y₀), ...,
|
||
|
(x₀, y₁), (x₁, y₁), (x₂, y₁), ...,
|
||
|
...]
|
||
|
</code></pre>
|
||
|
|
||
|
<p>We'll add a couple of helper functions to make the indexing of this array a bit
|
||
|
less painful:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun-inline</span></i> vref <span class="paren2">(<span class="code">chip x y</span>)</span>
|
||
|
<span class="paren2">(<span class="code">aref <span class="paren3">(<span class="code">chip-video chip</span>)</span> <span class="paren3">(<span class="code">+ <span class="paren4">(<span class="code">* +screen-width+ y</span>)</span> x</span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defun-inline</span></i> <span class="paren2">(<span class="code">setf vref</span>)</span> <span class="paren2">(<span class="code">new-value chip x y</span>)</span>
|
||
|
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">aref <span class="paren4">(<span class="code">chip-video chip</span>)</span> <span class="paren4">(<span class="code">+ <span class="paren5">(<span class="code">* +screen-width+ y</span>)</span> x</span>)</span></span>)</span>
|
||
|
new-value</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Now we can simply say <code>(vref chip 5 15)</code> to get the pixel at (5, 15) instead of
|
||
|
manually calculating out the <code>aref</code>.</p>
|
||
|
|
||
|
<p>We'll add one more field to <code>chip</code> before moving on, a "dirty" flag:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defstruct</span></i> chip
|
||
|
<span class="comment">; ...
|
||
|
</span> <span class="paren2">(<span class="code">video-dirty t <span class="keyword">:type</span> boolean</span>)</span>
|
||
|
<span class="comment">; ...
|
||
|
</span> </span>)</span></span></code></pre>
|
||
|
|
||
|
<p>This will make it easier for any interface to determine whether it needs to
|
||
|
update the display or not.</p>
|
||
|
|
||
|
<h3 id="s5-fonts"><a href="index.html#s5-fonts">Fonts</a></h3>
|
||
|
|
||
|
<p>The CHIP-8 spec sets aside a portion of main memory starting at address <code>#x50</code>
|
||
|
to contain sprites for the hex digits 0 through F. Check out <a href="http://devernay.free.fr/hacks/chip8/C8TECH10.HTM">the display
|
||
|
chapter of Cowgod's guide</a> for an overview of how the CHIP-8 defines
|
||
|
sprites.</p>
|
||
|
|
||
|
<p>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:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> load-font <span class="paren2">(<span class="code">chip</span>)</span>
|
||
|
<span class="comment">;; Thanks http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/
|
||
|
</span> <span class="paren2">(<span class="code">replace <span class="paren3">(<span class="code">chip-memory chip</span>)</span>
|
||
|
#<span class="paren3">(<span class="code">#xF0 #x90 #x90 #x90 #xF0 <span class="comment">; 0
|
||
|
</span> #x20 #x60 #x20 #x20 #x70 <span class="comment">; 1
|
||
|
</span> #xF0 #x10 #xF0 #x80 #xF0 <span class="comment">; 2
|
||
|
</span> #xF0 #x10 #xF0 #x10 #xF0 <span class="comment">; 3
|
||
|
</span> #x90 #x90 #xF0 #x10 #x10 <span class="comment">; 4
|
||
|
</span> #xF0 #x80 #xF0 #x10 #xF0 <span class="comment">; 5
|
||
|
</span> #xF0 #x80 #xF0 #x90 #xF0 <span class="comment">; 6
|
||
|
</span> #xF0 #x10 #x20 #x40 #x40 <span class="comment">; 7
|
||
|
</span> #xF0 #x90 #xF0 #x90 #xF0 <span class="comment">; 8
|
||
|
</span> #xF0 #x90 #xF0 #x10 #xF0 <span class="comment">; 9
|
||
|
</span> #xF0 #x90 #xF0 #x90 #x90 <span class="comment">; A
|
||
|
</span> #xE0 #x90 #xE0 #x90 #xE0 <span class="comment">; B
|
||
|
</span> #xF0 #x80 #x80 #x80 #xF0 <span class="comment">; C
|
||
|
</span> #xE0 #x90 #x90 #x90 #xE0 <span class="comment">; D
|
||
|
</span> #xF0 #x80 #xF0 #x80 #xF0 <span class="comment">; E
|
||
|
</span> #xF0 #x80 #xF0 #x80 #x80</span>)</span> <span class="comment">; F
|
||
|
</span> <span class="keyword">:start1</span> #x50</span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> reset <span class="paren2">(<span class="code">chip</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">with-chip</span></i> <span class="paren3">(<span class="code">chip</span>)</span>
|
||
|
<span class="paren3">(<span class="code">fill memory 0</span>)</span>
|
||
|
<span class="paren3">(<span class="code">fill registers 0</span>)</span>
|
||
|
<span class="paren3">(<span class="code">fill video 0</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren3">(<span class="code">load-font chip</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren3">(<span class="code">replace memory <span class="paren4">(<span class="code">read-file-into-byte-vector loaded-rom</span>)</span>
|
||
|
<span class="keyword">:start1</span> #x200</span>)</span>
|
||
|
<span class="paren3">(<span class="code">setf running t
|
||
|
video-dirty t <span class="comment">; NEW
|
||
|
</span> program-counter #x200
|
||
|
<span class="paren4">(<span class="code">fill-pointer stack</span>)</span> 0</span>)</span></span>)</span>
|
||
|
<span class="paren2">(<span class="code">values</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Once again the handy <code>replace</code> function makes things easy.</p>
|
||
|
|
||
|
<h3 id="s6-clearing-the-screen-cls"><a href="index.html#s6-clearing-the-screen-cls">Clearing the Screen: CLS</a></h3>
|
||
|
|
||
|
<p>Now we can start implementing the graphics-related instructions. The first is
|
||
|
the very simple <code>CLS</code> to clear the screen:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-instruction</span></i> op-cls <span class="paren2">(<span class="code"></span>)</span> <span class="comment">;; CLS
|
||
|
</span> <span class="paren2">(<span class="code">fill video 0</span>)</span>
|
||
|
<span class="paren2">(<span class="code">setf video-dirty t</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<h3 id="s7-loading-fonts-ld-f-vx"><a href="index.html#s7-loading-fonts-ld-f-vx">Loading Fonts: LD F, Vx</a></h3>
|
||
|
|
||
|
<p>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 <code>LD F, V2</code>
|
||
|
where register 2 contains <code>6</code> would set the index register to the address of the
|
||
|
<code>6</code> sprite.</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun-inline</span></i> font-location <span class="paren2">(<span class="code">character</span>)</span>
|
||
|
<span class="paren2">(<span class="code">+ #x50 <span class="paren3">(<span class="code">* character 5</span>)</span></span>)</span></span>)</span> <span class="comment">; each sprite is 5 bytes wide
|
||
|
</span>
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">define-instruction</span></i> op-ld-font<vx <span class="paren2">(<span class="code">_ r _ _</span>)</span> <span class="comment">;; LD F, Vx
|
||
|
</span> <span class="paren2">(<span class="code">setf index <span class="paren3">(<span class="code">font-location <span class="paren4">(<span class="code">register r</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<h3 id="s8-drawing-sprites-drw-x-y-size"><a href="index.html#s8-drawing-sprites-drw-x-y-size">Drawing Sprites: DRW X, Y, Size</a></h3>
|
||
|
|
||
|
<p>The most complicated part of the emulator's code is certainly the portion that
|
||
|
draws sprites. The instruction itself will delegate to a helper function:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-instruction</span></i> op-draw <span class="paren2">(<span class="code">_ rx ry size</span>)</span> <span class="comment">;; DRW Vx, Vy, size
|
||
|
</span> <span class="paren2">(<span class="code">draw-sprite chip <span class="paren3">(<span class="code">register rx</span>)</span> <span class="paren3">(<span class="code">register ry</span>)</span> size</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Check out <a href="http://devernay.free.fr/hacks/chip8/C8TECH10.HTM">Cowgod's guide</a> for a good overview of how the CHIP-8
|
||
|
drawing system works. I'll assume you've read that before moving on.</p>
|
||
|
|
||
|
<p>Let's implement the <code>draw-sprite</code> function in chunks, because it can be a bit
|
||
|
intimidating if I just slap it all down at once. Before we start drawing
|
||
|
anything we reset <code>flag</code> to 0, and we'll mark the dirty flag at the end:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> draw-sprite <span class="paren2">(<span class="code">chip start-x start-y size</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">with-chip</span></i> <span class="paren3">(<span class="code">chip</span>)</span>
|
||
|
<span class="paren3">(<span class="code">setf flag 0</span>)</span>
|
||
|
<span class="comment">; ... draw the sprite ...
|
||
|
</span> <span class="paren3">(<span class="code">setf video-dirty t</span>)</span></span>)</span>
|
||
|
nil</span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Simple enough. Now we need to loop through each row of the sprite and draw it.
|
||
|
The address of the sprite we're drawing is given by the index register, and each
|
||
|
row in the sprite is represented by a byte of memory. Again, check out Cowgod's
|
||
|
guide for the full details.</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> draw-sprite <span class="paren2">(<span class="code">chip start-x start-y size</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">with-chip</span></i> <span class="paren3">(<span class="code">chip</span>)</span>
|
||
|
<span class="paren3">(<span class="code">setf flag 0</span>)</span>
|
||
|
<span class="paren3">(<span class="code">iterate <span class="comment">; NEW
|
||
|
</span> <span class="paren4">(<span class="code">repeat size</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren4">(<span class="code">for i <span class="keyword">:from</span> index</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren4">(<span class="code">for y <span class="keyword">:from</span> start-y</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren4">(<span class="code">for sprite = <span class="paren5">(<span class="code">aref memory i</span>)</span></span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="comment">; ... draw the row ...
|
||
|
</span> </span>)</span>
|
||
|
<span class="paren3">(<span class="code">setf video-dirty t</span>)</span></span>)</span>
|
||
|
nil</span>)</span></span></code></pre>
|
||
|
|
||
|
<p>To draw a row, we just have to draw each of the eight pixels in it:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> draw-sprite <span class="paren2">(<span class="code">chip start-x start-y size</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">with-chip</span></i> <span class="paren3">(<span class="code">chip</span>)</span>
|
||
|
<span class="paren3">(<span class="code">setf flag 0</span>)</span>
|
||
|
<span class="paren3">(<span class="code">iterate
|
||
|
<span class="paren4">(<span class="code">repeat size</span>)</span>
|
||
|
<span class="paren4">(<span class="code">for i <span class="keyword">:from</span> index</span>)</span>
|
||
|
<span class="paren4">(<span class="code">for y <span class="keyword">:from</span> start-y</span>)</span>
|
||
|
<span class="paren4">(<span class="code">for sprite = <span class="paren5">(<span class="code">aref memory i</span>)</span></span>)</span>
|
||
|
<span class="paren4">(<span class="code">iterate <span class="comment">; NEW
|
||
|
</span> <span class="paren5">(<span class="code">for x <span class="keyword">:from</span> start-x</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren5">(<span class="code">for col <span class="keyword">:from</span> 7 <span class="keyword">:downto</span> 0</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="comment">; ... draw the pixel ...
|
||
|
</span> </span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">setf video-dirty t</span>)</span></span>)</span>
|
||
|
nil</span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Unfortunately we hit a snag at this point. All the references I've found say
|
||
|
that if any X or Y values go outside of the range of valid screen coordinates
|
||
|
the sprite should wrap around the screen. And indeed, some ROMs (e.g.
|
||
|
<code>ufo.rom</code>) require this behavior to work properly. But unfortunately some
|
||
|
<em>other</em> ROMs (e.g. <code>blitz.rom</code>) expect the screen to <em>clip</em>, not wrap!</p>
|
||
|
|
||
|
<p>This is the first case where our emulator will need to bend the rules of the
|
||
|
spec to accommodate buggy ROMs. Sadly this is not uncommon in the emulation
|
||
|
world.</p>
|
||
|
|
||
|
<p>We'll deal with this by adding a setting to the emulator:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defstruct</span></i> chip
|
||
|
<span class="comment">; ...
|
||
|
</span> <span class="paren2">(<span class="code">screen-wrapping-enabled t <span class="keyword">:type</span> boolean</span>)</span>
|
||
|
<span class="comment">; ...
|
||
|
</span> </span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Then we can hide the "to wrap, or not to wrap" logic in its own helper:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun-inline</span></i> wrap <span class="paren2">(<span class="code">chip x y</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">cond</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">chip-screen-wrapping-enabled chip</span>)</span>
|
||
|
<span class="paren4">(<span class="code">values <span class="paren5">(<span class="code">mod x +screen-width+</span>)</span>
|
||
|
<span class="paren5">(<span class="code">mod y +screen-height+</span>)</span>
|
||
|
t</span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code"><span class="paren4">(<span class="code">and <span class="paren5">(<span class="code">in-range-p 0 x +screen-width+</span>)</span>
|
||
|
<span class="paren5">(<span class="code">in-range-p 0 y +screen-height+</span>)</span></span>)</span>
|
||
|
<span class="paren4">(<span class="code">values x y t</span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">t <span class="paren4">(<span class="code">values nil nil nil</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p><code>wrap</code> will take <code>x</code> and <code>y</code> coordinates and return three values:</p>
|
||
|
|
||
|
<ul>
|
||
|
<li>The screen X coordinate to draw to (if any).</li>
|
||
|
<li>The screen Y coordinate to draw to (if any).</li>
|
||
|
<li>A boolean that will be <code>t</code> when the pixel should be drawn, or <code>nil</code> if not.</li>
|
||
|
</ul>
|
||
|
|
||
|
<p><a href="https://github.com/sjl/cl-losh/blob/master/DOCUMENTATION.markdown#in-range-p-function"><code>in-range-p</code></a> is a predicate from my utility library that checks if
|
||
|
<code>low <= val < high</code> (which is <a href="https://www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html">often useful</a>).</p>
|
||
|
|
||
|
<p>Now we can use this in <code>draw-sprite</code> to determine whether and where to draw each
|
||
|
pixel:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> draw-sprite <span class="paren2">(<span class="code">chip start-x start-y size</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">with-chip</span></i> <span class="paren3">(<span class="code">chip</span>)</span>
|
||
|
<span class="paren3">(<span class="code">setf flag 0</span>)</span>
|
||
|
<span class="paren3">(<span class="code">iterate
|
||
|
<span class="paren4">(<span class="code">repeat size</span>)</span>
|
||
|
<span class="paren4">(<span class="code">for i <span class="keyword">:from</span> index</span>)</span>
|
||
|
<span class="paren4">(<span class="code">for y <span class="keyword">:from</span> start-y</span>)</span>
|
||
|
<span class="paren4">(<span class="code">for sprite = <span class="paren5">(<span class="code">aref memory i</span>)</span></span>)</span>
|
||
|
<span class="paren4">(<span class="code">iterate
|
||
|
<span class="paren5">(<span class="code">for x <span class="keyword">:from</span> start-x</span>)</span>
|
||
|
<span class="paren5">(<span class="code">for col <span class="keyword">:from</span> 7 <span class="keyword">:downto</span> 0</span>)</span>
|
||
|
<span class="paren5">(<span class="code">multiple-value-bind <span class="paren6">(<span class="code">x y should-draw</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren6">(<span class="code">wrap chip x y</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren6">(<span class="code">when should-draw <span class="comment">; NEW
|
||
|
</span> <span class="comment">; ... actually draw the damn pixel ...
|
||
|
</span> </span>)</span></span>)</span></span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">setf video-dirty t</span>)</span></span>)</span>
|
||
|
nil</span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Now we come to the second concession to performance. Ideally we'd store the
|
||
|
pixel values as <code>t</code> and <code>nil</code> or <code>1</code> and <code>0</code>. But due to a bug in Qt 4 (which
|
||
|
we'll talk about later) we need to store <code>255</code> and <code>0</code>. So we need to do a bit
|
||
|
of an ugly dance to <code>XOR</code> the pixels together:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> draw-sprite <span class="paren2">(<span class="code">chip start-x start-y size</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">with-chip</span></i> <span class="paren3">(<span class="code">chip</span>)</span>
|
||
|
<span class="paren3">(<span class="code">setf flag 0</span>)</span>
|
||
|
<span class="paren3">(<span class="code">iterate
|
||
|
<span class="paren4">(<span class="code">repeat size</span>)</span>
|
||
|
<span class="paren4">(<span class="code">for i <span class="keyword">:from</span> index</span>)</span>
|
||
|
<span class="paren4">(<span class="code">for y <span class="keyword">:from</span> start-y</span>)</span>
|
||
|
<span class="paren4">(<span class="code">for sprite = <span class="paren5">(<span class="code">aref memory i</span>)</span></span>)</span>
|
||
|
<span class="paren4">(<span class="code">iterate
|
||
|
<span class="paren5">(<span class="code">for x <span class="keyword">:from</span> start-x</span>)</span>
|
||
|
<span class="paren5">(<span class="code">for col <span class="keyword">:from</span> 7 <span class="keyword">:downto</span> 0</span>)</span>
|
||
|
<span class="paren5">(<span class="code">multiple-value-bind <span class="paren6">(<span class="code">x y should-draw</span>)</span>
|
||
|
<span class="paren6">(<span class="code">wrap chip x y</span>)</span>
|
||
|
<span class="paren6">(<span class="code">when should-draw
|
||
|
<span class="paren1">(<span class="code">for old-pixel = <span class="paren2">(<span class="code">plusp <span class="paren3">(<span class="code">vref chip x y</span>)</span></span>)</span></span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren1">(<span class="code">for new-pixel = <span class="paren2">(<span class="code">plusp <span class="paren3">(<span class="code">get-bit col sprite</span>)</span></span>)</span></span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren1">(<span class="code">when <span class="paren2">(<span class="code">and old-pixel new-pixel</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren2">(<span class="code">setf flag 1</span>)</span></span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren1">(<span class="code">setf <span class="paren2">(<span class="code">vref chip x y</span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren2">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren3">(<span class="code">xor old-pixel new-pixel</span>)</span> 255 0</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren3">(<span class="code">setf video-dirty t</span>)</span></span>)</span>
|
||
|
nil</span>)</span></span></code></pre>
|
||
|
|
||
|
<p><code>xor</code> is the exclusive-or variant of <code>and</code>/<code>or</code> from Alexandria.</p>
|
||
|
|
||
|
<p>And we're finally done. <code>draw-sprite</code> is 22 lines, which is getting a bit long
|
||
|
by Lisp standards, so it might be worth breaking into separate functions. But
|
||
|
it's also the most performance-critical function in the emulator, so keeping it
|
||
|
as a single loop will let us optimize it heavily later if necessary (spoiler: it
|
||
|
won't be necessary).</p>
|
||
|
|
||
|
<h2 id="s9-the-user-interface-layer"><a href="index.html#s9-the-user-interface-layer">The User Interface Layer</a></h2>
|
||
|
|
||
|
<p>That's it for the emulation side of things. If we run a ROM now the video
|
||
|
memory array in the <code>chip</code> struct will be updated properly. But unless we want
|
||
|
to look at raw memory, we need some kind of a screen.</p>
|
||
|
|
||
|
<p>We'll start off with a fresh package:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">in-package <span class="keyword">:chip8.gui.screen</span></span>)</span>
|
||
|
<span class="paren1">(<span class="code">named-readtables:in-readtable <span class="keyword">:qtools</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<h3 id="s10-basic-plan"><a href="index.html#s10-basic-plan">Basic Plan</a></h3>
|
||
|
|
||
|
<p>We're going to use OpenGL to draw the actual pixels for our screen. The basic
|
||
|
plan will be:</p>
|
||
|
|
||
|
<ul>
|
||
|
<li>Ship the video memory up to the graphics card as a texture each frame.</li>
|
||
|
<li>Draw a single quad with this texture to get the actual pixels onto the display.</li>
|
||
|
</ul>
|
||
|
|
||
|
<p>We can use a tiny 64 by 64 texture so this will be fine for our performance
|
||
|
needs.</p>
|
||
|
|
||
|
<h3 id="s11-screen-widget"><a href="index.html#s11-screen-widget">Screen Widget</a></h3>
|
||
|
|
||
|
<p>The main UI for our screen will be a <code>QGLWidget</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-widget</span></i> screen <span class="paren2">(<span class="code">QGLWidget</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><span class="paren3">(<span class="code">texture <span class="keyword">:accessor</span> screen-texture</span>)</span>
|
||
|
<span class="paren3">(<span class="code">chip <span class="keyword">:accessor</span> screen-chip <span class="keyword">:initarg</span> <span class="keyword">:chip</span></span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> make-screen <span class="paren2">(<span class="code">chip</span>)</span>
|
||
|
<span class="paren2">(<span class="code">make-instance 'screen <span class="keyword">:chip</span> chip</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>We'll define some initializers for this widget:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defparameter</span></i> <span class="special">*scale*</span> 8</span>)</span>
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defparameter</span></i> <span class="special">*width*</span> <span class="paren2">(<span class="code">* <span class="special">*scale*</span> 64</span>)</span></span>)</span>
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defparameter</span></i> <span class="special">*height*</span> <span class="paren2">(<span class="code">* <span class="special">*scale*</span> 32</span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">define-initializer</span></i> <span class="paren2">(<span class="code">screen setup</span>)</span>
|
||
|
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">q+:window-title screen</span>)</span> <span class="string">"cl-chip8"</span>
|
||
|
<span class="paren3">(<span class="code">q+:fixed-size screen</span>)</span> <span class="paren3">(<span class="code">values <span class="special">*width*</span> <span class="special">*height*</span></span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">define-override</span></i> <span class="paren2">(<span class="code">screen <span class="string">"initializeGL"</span></span>)</span> <span class="paren2">(<span class="code"></span>)</span>
|
||
|
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">screen-texture screen</span>)</span> <span class="paren3">(<span class="code">initialize-texture 64</span>)</span></span>)</span>
|
||
|
<span class="paren2">(<span class="code">stop-overriding</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Single pixels are almost impossible to see on today's high-resolution displays,
|
||
|
so we'll scale them up by 8 to make them bigger.</p>
|
||
|
|
||
|
<p>We need to do the OpenGL texture initialization in the <code>initializeGL</code> method,
|
||
|
<em>not</em> the normal initializer, because we need the OpenGL context to be ready.
|
||
|
The actual texture initialization code is typical verbose ugly OpenGL, so we'll
|
||
|
tuck it away in a helper function:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> initialize-texture <span class="paren2">(<span class="code">size</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">handle <span class="paren5">(<span class="code">gl:gen-texture</span>)</span></span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">gl:bind-texture <span class="keyword">:texture-2d</span> handle</span>)</span>
|
||
|
|
||
|
<span class="paren3">(<span class="code">gl:tex-image-2d <span class="keyword">:texture-2d</span> 0 <span class="keyword">:luminance</span> size size 0 <span class="keyword">:luminance</span>
|
||
|
<span class="keyword">:unsigned-byte</span> <span class="paren4">(<span class="code">cffi:null-pointer</span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">gl:tex-parameter <span class="keyword">:texture-2d</span> <span class="keyword">:texture-min-filter</span> <span class="keyword">:nearest</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">gl:tex-parameter <span class="keyword">:texture-2d</span> <span class="keyword">:texture-mag-filter</span> <span class="keyword">:nearest</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">gl:enable <span class="keyword">:texture-2d</span></span>)</span>
|
||
|
|
||
|
<span class="paren3">(<span class="code">gl:bind-texture <span class="keyword">:texture-2d</span> 0</span>)</span>
|
||
|
|
||
|
handle</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>We'll use <a href="https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation">nearest-neighbor interpolation</a> to get nice sharp sprites.</p>
|
||
|
|
||
|
<p>The texture array will be <code>unsigned-byte</code>s of luminance values, with <code>0</code> being
|
||
|
black and <code>255</code> being white. This explains the dance we had to do back in the
|
||
|
<code>draw-sprite</code>.</p>
|
||
|
|
||
|
<p>A better way to do this would be to have video memory contain <code>0</code> and <code>1</code>, and
|
||
|
use an OpenGL fragment shader to map these to the desired colors. Unfortunately
|
||
|
due to <a href="https://github.com/Shinmera/qtools/issues/17">a nasty bug</a> in Qt 4 on OS X we can't use shaders, so we're
|
||
|
stuck with this workaround for the time being. Computers are awful.</p>
|
||
|
|
||
|
<h3 id="s12-drawing-frames"><a href="index.html#s12-drawing-frames">Drawing Frames</a></h3>
|
||
|
|
||
|
<p>Now that we've got a widget we'll need to paint it on each frame. We'll use
|
||
|
a Qtimer to handle firing off the paint events:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defparameter</span></i> <span class="special">*fps*</span> 60</span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">define-subwidget</span></i> <span class="paren2">(<span class="code">screen timer</span>)</span> <span class="paren2">(<span class="code">q+:make-qtimer screen</span>)</span>
|
||
|
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">q+:single-shot timer</span>)</span> NIL</span>)</span>
|
||
|
<span class="paren2">(<span class="code">q+:start timer <span class="paren3">(<span class="code">round 1000 <span class="special">*fps*</span></span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">define-slot</span></i> <span class="paren2">(<span class="code">screen update</span>)</span> <span class="paren2">(<span class="code"></span>)</span>
|
||
|
<span class="paren2">(<span class="code">declare <span class="paren3">(<span class="code">connected timer <span class="paren4">(<span class="code">timeout</span>)</span></span>)</span></span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren3">(<span class="code">chip8::chip-running <span class="paren4">(<span class="code">screen-chip screen</span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">q+:repaint screen</span>)</span>
|
||
|
<span class="paren3">(<span class="code">die screen</span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> die <span class="paren2">(<span class="code">screen</span>)</span>
|
||
|
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">chip8::chip-running <span class="paren4">(<span class="code">screen-chip screen</span>)</span></span>)</span> nil</span>)</span>
|
||
|
<span class="paren2">(<span class="code">q+:close screen</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>The <code>timer</code> widget will fire a Qt <code>timeout</code> signal sixty times per second. The
|
||
|
screen's <code>update</code> slot is connected to this signal, and will either initiate
|
||
|
a repaint or kill the screen, depending on whether the <code>chip</code> is still running.</p>
|
||
|
|
||
|
<p>The <code>die</code> function also tells the <code>chip</code> to stop running. This obviously isn't
|
||
|
necessary here (we just checked that it's not running!), but we'll be using
|
||
|
<code>die</code> in another place later.</p>
|
||
|
|
||
|
<p>(It's really a shame that Qt and Common Lisp both use the words "signal" and
|
||
|
"slot" to mean wildly different things. It makes using Qt with Common Lisp more
|
||
|
painful than it should be...)</p>
|
||
|
|
||
|
<p>Now on to the meat of the code, repainting. We'll define the <code>paint-event</code> and
|
||
|
delegate to a helper function:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-override</span></i> <span class="paren2">(<span class="code">screen paint-event</span>)</span> <span class="paren2">(<span class="code">ev</span>)</span>
|
||
|
<span class="paren2">(<span class="code">declare <span class="paren3">(<span class="code">ignore ev</span>)</span></span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">with-finalizing</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">painter <span class="paren5">(<span class="code">q+:make-qpainter screen</span>)</span></span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">render-screen screen painter</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>And here we go with another pile of verbose graphics code:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> render-screen <span class="paren2">(<span class="code">screen painter</span>)</span>
|
||
|
<span class="paren2">(<span class="code">q+:begin-native-painting painter</span>)</span>
|
||
|
|
||
|
<span class="comment">;; Clear the screen
|
||
|
</span> <span class="paren2">(<span class="code">gl:clear-color 0.0 0.0 0.0 1.0</span>)</span>
|
||
|
<span class="paren2">(<span class="code">gl:clear <span class="keyword">:color-buffer-bit</span></span>)</span>
|
||
|
|
||
|
<span class="paren2">(<span class="code">gl:bind-texture <span class="keyword">:texture-2d</span> <span class="paren3">(<span class="code">screen-texture screen</span>)</span></span>)</span>
|
||
|
|
||
|
<span class="comment">;; Update the texture
|
||
|
</span> <span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">chip <span class="paren5">(<span class="code">screen-chip screen</span>)</span></span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">when <span class="paren4">(<span class="code">chip8::chip-video-dirty chip</span>)</span>
|
||
|
<span class="paren4">(<span class="code">setf <span class="paren5">(<span class="code">chip8::chip-video-dirty chip</span>)</span> nil</span>)</span>
|
||
|
<span class="paren4">(<span class="code">gl:tex-sub-image-2d <span class="keyword">:texture-2d</span> 0 0 0 64 32
|
||
|
<span class="keyword">:luminance</span> <span class="keyword">:unsigned-byte</span>
|
||
|
<span class="paren5">(<span class="code">chip8::chip-video chip</span>)</span></span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="comment">;; Draw the quad
|
||
|
</span> <span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">tw 1</span>)</span>
|
||
|
<span class="paren4">(<span class="code">th 0.5</span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code"><i><span class="symbol">gl:with-primitives</span></i> <span class="keyword">:quads</span>
|
||
|
<span class="paren4">(<span class="code">gl:tex-coord 0 0</span>)</span>
|
||
|
<span class="paren4">(<span class="code">gl:vertex 0 0</span>)</span>
|
||
|
|
||
|
<span class="paren4">(<span class="code">gl:tex-coord tw 0</span>)</span>
|
||
|
<span class="paren4">(<span class="code">gl:vertex <span class="special">*width*</span> 0</span>)</span>
|
||
|
|
||
|
<span class="paren4">(<span class="code">gl:tex-coord tw th</span>)</span>
|
||
|
<span class="paren4">(<span class="code">gl:vertex <span class="special">*width*</span> <span class="special">*height*</span></span>)</span>
|
||
|
|
||
|
<span class="paren4">(<span class="code">gl:tex-coord 0 th</span>)</span>
|
||
|
<span class="paren4">(<span class="code">gl:vertex 0 <span class="special">*height*</span></span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren2">(<span class="code">gl:bind-texture <span class="keyword">:texture-2d</span> 0</span>)</span>
|
||
|
|
||
|
<span class="paren2">(<span class="code">q+:end-native-painting painter</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Each frame, if <code>video-dirty</code> is set we'll update the texture with the contents
|
||
|
of the <code>chip</code>'s video memory. Then we draw a quad using this texture to get the
|
||
|
pixels on the actual display.</p>
|
||
|
|
||
|
<pre class="lineart">
|
||
|
Screen Texture
|
||
|
┌───────────────────────────────┐ ┌──────────────────────┐
|
||
|
│ │ │ ▉ ▉ ▉ ▉ │
|
||
|
│ ▉▉ ▉▉ ▉▉ ▉▉ │ │ ▉ ▉ ▉ ▉ │
|
||
|
│ ▉▉ ▉▉ ▉▉ ▉▉ │ │ ▉▉▉▉▉ ▉ ▉ │
|
||
|
│ ▉▉ ▉▉ ▉▉ ▉▉ │ │ ▉ ▉ ▉ │
|
||
|
│ ▉▉ ▉▉ ▉▉ ▉▉ │ │ ▉ ▉ ▉ ▉ │
|
||
|
│ ▉▉▉▉▉▉▉▉▉▉ ▉▉ ▉▉ │ │░░░░░░░░░░░░░░░░░░░░░░│
|
||
|
│ ▉▉ ▉▉ ▉▉ ▉▉ │ │░░░░░░░░░░░░░░░░░░░░░░│
|
||
|
│ ▉▉ ▉▉ ▉▉ ▉▉ │ │░░░░░░░░░░░░░░░░░░░░░░│
|
||
|
│ ▉▉ ▉▉ ▉▉ │ │░░░░░░░░░░░░░░░░░░░░░░│
|
||
|
│ ▉▉ ▉▉ ▉▉ ▉▉ │ │░░░░░░░░░░░░░░░░░░░░░░│
|
||
|
│ ▉▉ ▉▉ ▉▉ ▉▉ │ └──────────────────────┘
|
||
|
└───────────────────────────────┘
|
||
|
</pre>
|
||
|
|
||
|
<p>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.</p>
|
||
|
|
||
|
<p>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.</p>
|
||
|
|
||
|
<h3 id="s13-wrapping-up"><a href="index.html#s13-wrapping-up">Wrapping Up</a></h3>
|
||
|
|
||
|
<p>The last thing we need is a function to actually create the GUI:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run-gui <span class="paren2">(<span class="code">chip</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">with-main-window</span></i>
|
||
|
<span class="paren3">(<span class="code">window <span class="paren4">(<span class="code">make-screen chip</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>And now we can modify our emulator's <code>run</code> function to start up a GUI in
|
||
|
addition to the system emulation:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run <span class="paren2">(<span class="code">rom-filename</span>)</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">chip <span class="paren5">(<span class="code">make-chip</span>)</span></span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">setf <span class="special">*c*</span> chip</span>)</span>
|
||
|
<span class="paren3">(<span class="code">load-rom chip rom-filename</span>)</span>
|
||
|
<span class="paren3">(<span class="code">bt:make-thread <span class="paren4">(<span class="code">curry #'run-cpu chip</span>)</span></span>)</span> <span class="comment">; NEW
|
||
|
</span> <span class="paren3">(<span class="code">chip8.gui.screen::run-gui chip</span>)</span></span>)</span></span>)</span> <span class="comment">; NEW</span></span></code></pre>
|
||
|
|
||
|
<p>Qt will take control of the thread and block when run, so we'll need to run our
|
||
|
CPU emulation in a separate thread.</p>
|
||
|
|
||
|
<p>The only thing they both write to is the <code>video-dirty</code> 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.</p>
|
||
|
|
||
|
<p>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.</p>
|
||
|
|
||
|
<h2 id="s14-results"><a href="index.html#s14-results">Results</a></h2>
|
||
|
|
||
|
<p>And with all that done we've <em>finally</em> got a screen to play games on!</p>
|
||
|
|
||
|
<p><a href="http://stevelosh.com/static/images/blog/2016/12/chip8-screen.png"><img src="http://stevelosh.com/static/images/blog/2016/12/chip8-screen.png" alt="Screenshot of CHIP-8 screen running UFO.rom"></a></p>
|
||
|
|
||
|
<h2 id="s15-future"><a href="index.html#s15-future">Future</a></h2>
|
||
|
|
||
|
<p>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.</p>
|
||
|
</article></main><hr class='main-separator' /><footer><nav><a href='https://github.com/sjl/'>GitHub</a> ・ <a href='https://twitter.com/stevelosh/'>Twitter</a> ・ <a href='https://instagram.com/thirtytwobirds/'>Instagram</a> ・ <a href='https://hg.stevelosh.com/.plan/'>.plan</a></nav></footer></body></html>
|