src/emulator.lisp @ 2e803dec5d58 crazy-dsl

Attempt at a crazy DSL thing for opcodes.
author Steve Losh <steve@stevelosh.com>
date Thu, 24 Nov 2016 12:45:02 +0000
parents 503bfe5cd173
children (none)
(in-package :chip8)

(setf *print-length* 16)
(setf *print-base* 10)
(declaim (optimize (speed 1) (safety 3) (debug 3)))
(declaim (optimize (speed 3) (safety 1) (debug 3)))


;;;; Reference ----------------------------------------------------------------
;;; http://devernay.free.fr/hacks/chip8/C8TECH10.HTM
;;; http://mattmik.com/files/chip8/mastering/chip8.html
;;; https://github.com/AfBu/haxe-chip-8-emulator/wiki/(Super)CHIP-8-Secrets


;;;; Constants ----------------------------------------------------------------
(defconstant +cycles-per-second+ 500)
(defconstant +cycles-before-sleep+ 10)
(defconstant +screen-width+ 64)
(defconstant +screen-height+ 32)
(defconstant +memory-size+ (* 1024 4))


;;;; Types --------------------------------------------------------------------
(deftype int4 () '(unsigned-byte 4))
(deftype int8 () '(unsigned-byte 8))
(deftype int12 () '(unsigned-byte 12))
(deftype int16 () '(unsigned-byte 16))
(deftype x-coord () `(integer 0 (,+screen-width+)))
(deftype y-coord () `(integer 0 (,+screen-height+)))

(deftype basic-array (element-type size)
  `(simple-array ,(upgraded-array-element-type element-type) (,size)))

(deftype stack (size)
  `(vector ,(upgraded-array-element-type 'int12) ,size))


;;;; Utils --------------------------------------------------------------------
(declaim
  (inline not= +_8 -_8 chop cat-bytes get-bit bcd))

(defun make-simple-array (element-type size &rest args)
  (apply #'make-array size
         :adjustable nil
         :fill-pointer nil
         :element-type element-type
         args))

(defun not= (x y)
  (not (= x y)))

(defun chop (size integer)
  (ldb (byte size 0) integer))

(defun cat-bytes (high-order low-order)
  (dpb high-order (byte 8 8) low-order))

(defun get-bit (position integer)
  (ldb (byte 1 position) integer))

(defun +_8 (x y)
  (let ((result (+ x y)))
    (values (chop 8 result)
            (if (> result 255) 1 0))))

(defun -_8 (x y)
  (let ((result (- x y)))
    (values (chop 8 result)
            (if (> x y) 1 0))))

(defun bcd (integer)
  (values (-<> integer (floor <> 100) (mod <> 10))
          (-<> integer (floor <> 10) (mod <> 10))
          (-<> integer (floor <> 1) (mod <> 10))))

(defmacro macro-map ((lambda-list items) &rest body)
  (with-gensyms (macro)
    `(macrolet ((,macro ,(ensure-list lambda-list) ,@body))
      ,@(iterate (for item :in items)
                 (collect `(,macro ,@(ensure-list item)))))))

(defun required ()
  (error "Required"))


;;;; Data ---------------------------------------------------------------------
(declaim
  (inline chip-flag (setf chip-flag)))

(defstruct debugger
  (paused nil :type boolean)
  (take-step nil :type boolean)
  (print-needed nil :type boolean)
  (callbacks-arrived nil :type list))

(defstruct chip
  (running t :type boolean)
  (memory (make-simple-array 'int8 4096)
          :type (basic-array int8 4096)
          :read-only t)
  (registers (make-simple-array 'int8 16)
             :type (basic-array int8 16)
             :read-only t)
  (keys (make-simple-array 'boolean 16)
        :type (basic-array boolean 16)
        :read-only t)
  (video (make-simple-array 'fixnum (* +screen-height+ +screen-width+))
         :type (basic-array fixnum #.(* +screen-height+ +screen-width+))
         :read-only t)
  (video-dirty t :type boolean)
  (index 0 :type int16)
  (program-counter #x200 :type int12)
  (delay-timer 0 :type int8)
  (sound-timer 0 :type int8)
  (random-state (make-random-state t)
                :type random-state
                :read-only t)
  (stack (make-array 16
           :adjustable nil
           :fill-pointer 0
           :element-type 'int12)
         :type (stack 16)
         :read-only t)
  (loaded-rom nil :type (or null string))
  (debugger (make-debugger) :type debugger :read-only t))


(define-with-macro chip
  running
  memory registers
  flag
  index program-counter
  delay-timer sound-timer
  random-state
  video video-dirty
  keys
  stack
  loaded-rom
  debugger)

(define-with-macro debugger
  paused take-step print-needed
  callbacks-arrived)


(defun chip-flag (chip)
  (aref (chip-registers chip) #xF))

(defun (setf chip-flag) (new-value chip)
  (setf (aref (chip-registers chip) #xF) new-value))


;;;; Disassembler -------------------------------------------------------------
(defun disassemble-instruction (instruction)
  (declare (ignore instruction))
  nil)

(defun bit-diagram (integer)
  (iterate (for high-bit :from 15 :downto 8)
           (for low-bit :from 7 :downto 0)
           (for hi = (logbitp high-bit integer))
           (for lo = (logbitp low-bit integer))
           (collect (cond
                      ((and hi lo) #\full_block)
                      (hi #\upper_half_block)
                      (lo #\lower_half_block)
                      (t #\space))
                    :result-type 'string)))

(defun retrieve-instruction (array index)
  (chip8::cat-bytes (aref array index)
                    ;; ugly hack to handle odd-sized roms
                    (if (< (1+ index) (length array))
                      (aref array (1+ index))
                      0)))

(defun instruction-information (array index)
  (let ((instruction (retrieve-instruction array index)))
    (list index
          instruction
          (disassemble-instruction instruction)
          (bit-diagram instruction))))

(defun print-disassembled-instruction (array index)
  (destructuring-bind (address instruction disassembly bits)
      (instruction-information array index)
    (format t "~3,'0X: ~4,'0X ~24A ~8A~%"
            address
            instruction
            (or disassembly "")
            bits)))

(defun disassemble-instructions (array start)
  (iterate
    (for i :from start :below (length array) :by 2)
    (collect (instruction-information array i) :result-type vector)))

(defun dump-disassembly (array &optional (start 0) (end (length array)))
  (iterate
    (for i :from start :below end :by 2)
    (print-disassembled-instruction array i)
    (sleep 0.001)))


;;;; Debugger -----------------------------------------------------------------
(declaim
  (ftype (function (debugger) boolean) debugger-should-wait-p))

(defun debugger-pause (debugger)
  (with-debugger (debugger)
    (setf paused t print-needed t)))

(defun debugger-unpause (debugger)
  (with-debugger (debugger)
    (setf paused nil print-needed nil)))

(defun debugger-toggle-pause (debugger)
  (if (debugger-paused debugger)
    (debugger-unpause debugger)
    (debugger-pause debugger)))

(defun debugger-step (debugger)
  (with-debugger (debugger)
    (setf take-step t)))

(defun debugger-print (debugger chip)
  (with-debugger (debugger)
    (when (and paused print-needed)
      (let ((pc (chip-program-counter chip)))
        (setf print-needed nil)
        (destructuring-bind (address instruction disassembly bits)
            (instruction-information (chip-memory chip) pc)
          (format t "~3,'0X: ~4,'0X ~24A ~8A~%"
                  address
                  instruction
                  (or disassembly "")
                  bits))
        (mapc (rcurry #'funcall pc) callbacks-arrived))))
  (values))

(defun debugger-paused-p (debugger)
  (debugger-paused debugger))

(defun debugger-should-wait-p (debugger)
  (with-debugger (debugger)
    (if (not paused) ; if we're not paused, we never need to wait
      nil
      (if take-step
        (progn (setf take-step nil ; if we're paused, but are ready to step, go
                     print-needed t)
               nil)
        t)))) ; otherwise we're fully paused -- wait

(defun debugger-add-callback-arrived (debugger function)
  (push function (debugger-callbacks-arrived debugger))
  t)


;;;; Graphics -----------------------------------------------------------------
(declaim
  (inline font-location vref (setf vref))
  (ftype (function (chip int8 int8 int4) null) draw-sprite))


(defun vref (chip x y)
  (aref (chip-video chip) (+ (* +screen-width+ y) x)))

(defun (setf vref) (new-value chip x y)
  (setf (aref (chip-video chip) (+ (* +screen-width+ y) x))
        new-value))


(defun load-font (chip)
  ;; Thanks http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/
  (replace (chip-memory chip)
           (vector #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 font-location (character)
  (+ #x50 (* character 5)))


(defun draw-sprite (chip start-x start-y size)
  (with-chip (chip)
    (assert (< (+ index size) +memory-size+) (index)
      "Sprite data of size ~D starting at #x~4,'0X would be out of bounds"
      size index)
    (setf flag 0)
    (iterate
      (repeat size)
      (for i :from index)
      (for sprite = (aref memory i))
      (for y :modulo +screen-height+ :from start-y)
      (iterate (for x :modulo +screen-width+ :from start-x)
               (for col :from 7 :downto 0)
               (for old-pixel = (plusp (vref chip x y)))
               (for new-pixel = (plusp (get-bit col sprite)))
               (when (and old-pixel new-pixel)
                 (setf flag 1))
               (setf (vref chip x y)
                     (if (xor old-pixel new-pixel) 255 0))))
    (setf video-dirty t))
  nil)


;;;; Keyboard -----------------------------------------------------------------
(declaim
  (ftype (function (chip (integer 0 (16)))) keydown keyup))

(defun keydown (chip key)
  (setf (aref (chip-keys chip) key) t))

(defun keyup (chip key)
  (setf (aref (chip-keys chip) key) nil))


;;;; Opcodes ------------------------------------------------------------------
(defparameter *opcodes* (make-hash-table))

(defun dispatch-instruction (chip opcode)
  (declare (ignore chip opcode))
  nil)

(defstruct opcode
  (pattern (required) :type keyword)
  (matcher (required) :type list)
  (function-name (required) :type symbol)
  (arglist (required) :type list)
  (bindings (required) :type list)
  (format-info (required) :type list))

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defun parse-opcode-argument-bindings (argument-list)
    (flet ((normalize-arg (arg)
             (destructuring-bind (symbol &optional (nibbles 1))
                 (ensure-list arg)
               (list symbol nibbles))))
      (iterate
        (for (symbol nibbles) :in (mapcar #'normalize-arg argument-list))
        (for position :first 3 :then (- position nibbles))
        (when (not (eql symbol '_))
          (collect `(,symbol (ldb (byte ,(* nibbles 4)
                                        ,(* position 4))
                                  opcode)))))))

  (defun parse-opcode-matcher (opcode)
    (map 'list (lambda (char)
                 (or (digit-char-p char 16) '_))
         (symbol-name opcode)))

  (defun record-opcode (opcode function-name arglist format-info)
    (setf (gethash function-name *opcodes*)
          (make-opcode :pattern opcode
                       :matcher (parse-opcode-matcher opcode)
                       :function-name function-name
                       :arglist arglist
                       :bindings (parse-opcode-argument-bindings arglist)
                       :format-info format-info)))

  (defmacro recompile-instruction-matcher
      (function arglist opcode-info-symbol clause-body)
    `(compile ',function
      `(lambda ,',arglist
        (match* ((ldb (byte 4 12) opcode)
                 (ldb (byte 4 8) opcode)
                 (ldb (byte 4 4) opcode)
                 (ldb (byte 4 0) opcode))
          ,@(iterate (for (nil ,opcode-info-symbol) :in-hashtable *opcodes*)
                     (collect (list (opcode-matcher ,opcode-info-symbol)
                                    ,clause-body)))))))

  (defun recompile-disassemble-instruction ()
    (recompile-instruction-matcher disassemble-instruction
        (opcode)
        op-info
      `(let (,@(opcode-bindings op-info))
        (format nil ,@(opcode-format-info op-info)))))

  (defun recompile-dispatch-instruction ()
    (recompile-instruction-matcher dispatch-instruction
        (chip opcode)
        op-info
      `(,(opcode-function-name op-info) chip opcode))))

(defmacro define-opcode (opcode name argument-list format-info &body body)
  `(progn
    (declaim (ftype (function (chip int16)
                              (values null &optional))
                    ,name))
    (defun ,name (chip opcode)
      (declare (ignorable opcode))
      (with-chip (chip)
        (macrolet ((register (index)
                     `(aref registers ,index)))
          (let ,(parse-opcode-argument-bindings argument-list)
            ,@body))
        nil))
    (record-opcode ,opcode ',name ',argument-list ',format-info)
    (recompile-disassemble-instruction)
    (recompile-dispatch-instruction)
    ',name))


(macro-map                                           ;; LD ...
  ((opcode name arglist destination source format-info)
   ((:A___ op-ld-i<imm   (_ (value 3))   index         value         ("LD I, ~3,'0X" value))
    (:6___ op-ld-reg<imm (_ r (value 2)) (register r)  value         ("LD V~X, ~2,'0X" r value))
    (:8__0 op-ld-reg<reg (_ rx ry _)     (register rx) (register ry) ("LD V~X, V~X" rx ry))
    (:F_07 op-ld-reg<dt  (_ r _ _)       (register r)  delay-timer   ("LD V~X, DT" r))
    (:F_15 op-ld-dt<reg  (_ r _ _)       delay-timer   (register r)  ("LD DT, V~X" r))
    (:F_18 op-ld-st<reg  (_ r _ _)       sound-timer   (register r)  ("LD ST, V~X" r))))
  `(define-opcode ,opcode ,name ,arglist ,format-info
    (setf ,destination ,source)))

(define-opcode :00E0 op-cls ()                       ("CLS")
  (fill video 0)
  (setf video-dirty t))

(define-opcode :1___ op-jp-imm (_ (target 3))        ("JP ~3,'0X" target)
  (setf program-counter target))

(define-opcode :B___ op-jp-imm+reg (_ (target 3))    ("JP V0+~3,'0X" target)
  (setf program-counter (+ target (register 0))))

(define-opcode :2___ op-call (_ (target 3))          ("CALL ~3,'0X" target)
  (vector-push program-counter stack)
  (setf program-counter target))

(define-opcode :00EE op-ret ()                       ("RET")
  (setf program-counter (vector-pop stack)))

(macro-map                                           ;; ADD/SUB (8-bit)
  ((opcode name op source-arg source-expr format-info)
   ((:7___ op-add-reg<imm +_8 (immediate 2) immediate ("ADD V~X, ~3,'0X" rx immediate))
    (:8__4 op-add-reg<reg +_8 (ry 1) (register ry)    ("ADD V~X, V~X" rx ry))
    (:8__5 op-sub-reg<reg -_8 (ry 1) (register ry)    ("SUB V~X, V~X" rx ry))))
  `(define-opcode ,opcode ,name (_ rx ,source-arg) ,format-info
    (multiple-value-bind (result carry)
        (,op (register rx) ,source-expr)
      (setf (register rx) result
            flag carry))))

(define-opcode :F_1E op-add-index<reg (_ r)          ("ADD I, V~X" r)
  (zapf index (chop 16 (+ % (register r)))))

(define-opcode :8__7 op-subn-reg<reg (_ rx ry)       ("SUBN V~X, V~X" rx ry)
  (multiple-value-bind (result carry)
      (-_8 (register ry) (register rx)) ; subtraction order is swapped for SUBN
    (setf (register rx) result
          flag carry)))

(macro-map                                           ;; SE/SNE
  ((opcode name test arglist x y format-info)
   ((:3___ op-se-reg-imm  =    (_ r (imm 2)) (register r)  imm           ("SE V~X, ~2,'0X" r imm))
    (:4___ op-sne-reg-imm not= (_ r (imm 2)) (register r)  imm           ("SNE V~X, ~2,'0X" r imm))
    (:5__0 op-se-reg-reg  =    (_ rx ry _)   (register rx) (register ry) ("SE V~X, V~X" rx ry))
    (:9__0 op-sne-reg-reg not= (_ rx ry _)   (register rx) (register ry) ("SNE V~X, V~X" rx ry))))
  `(define-opcode ,opcode ,name ,arglist ,format-info
    (when (,test ,x ,y)
      (incf program-counter 2))))

(macro-map                                           ;; AND/OR/XOR
  ((opcode name function format-string)
   ((:8__1 op-or  logior "OR V~X, V~X")
    (:8__2 op-and logand "AND V~X, V~X")
    (:8__3 op-xor logxor "XOR V~X, V~X")))
  `(define-opcode ,opcode ,name (_ destination source _)
    (,format-string destination source)
    (zapf (register destination)
          (,function % (register source)))))

(define-opcode :C___ op-rand (_ r (mask 2))          ("RND V~X, ~2,'0X" r mask)
  (setf (register r)
        (logand (random 256 random-state) mask)))

(define-opcode :E_9E op-skp (_ r _ _)                ("SKP V~X" r)
  (when (aref keys (register r))
    (incf program-counter 2)))

(define-opcode :E_A1 op-sknp (_ r _ _)               ("SKNP V~X" r)
  (when (not (aref keys (register r)))
    (incf program-counter 2)))

(define-opcode :F_55 op-ld-mem<regs (_ n _ _)        ("LD [I], ~X" n)
  (replace memory registers :start1 index :end2 (1+ n)))

(define-opcode :F_65 op-ld-regs<mem (_ n _ _)        ("LD ~X, [I]" n)
  (replace registers memory :end1 (1+ n) :start2 index))

(define-opcode :F_0A op-ld-reg<key (_ r _ _)         ("LD V~X, K" r)
  ;; I'm unsure how this instruction is supposed to interact with the timers.
  ;;
  ;; Either the timers should continue to count down while we wait for a key, or
  ;; they should pause while waiting, but I can't find anything in the docs that
  ;; spells it out.
  ;;
  ;; This implementation chooses the former (timers keep running) for now.
  (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))))

(define-opcode :8__6 op-shr (_ r _ _)                ("SHR V~X" r)
  (let ((value (register r)))
    (setf flag (get-bit 0 value)
          (register r) (ash value -1))))

(define-opcode :8__E op-shl (_ r _ _)                ("SHL V~X" r)
  (let ((value (register r)))
    (setf flag (get-bit 7 value)
          (register r) (chop 8 (ash value 1)))))

(define-opcode :F_29 op-ld-font<vx (_ r _ _)         ("LD F, V~X" r)
  (setf index (font-location (register r))))

(define-opcode :F_33 op-ld-bcd<vx (_ r _ _)          ("LD B, V~X" r)
  (multiple-value-bind (hundreds tens ones)
      (bcd (register r))
    (setf (aref memory (+ index 0)) hundreds
          (aref memory (+ index 1)) tens
          (aref memory (+ index 2)) ones)))

(define-opcode :D___ op-draw (_ rx ry size)          ("DRW V~X, V~X, ~D" rx ry size)
  (draw-sprite chip (register rx) (register ry) size))


;;;; Sound --------------------------------------------------------------------
(defconstant +pi+ (float pi 1.0))
(defconstant +tau+ (* 2 +pi+))
(defconstant +sample-rate+ 44100d0)
(defconstant +audio-buffer-size+ 512)
(defconstant +audio-buffer-time+ (* +audio-buffer-size+ (/ +sample-rate+)))

(defun square (angle)
  (if (< (mod angle +tau+) +pi+)
    1.0
    -1.0))

(defun saw (angle)
  (let ((a (mod angle +tau+)))
    (if (< a +pi+)
      (map-range 0   +pi+
                 1.0 -1.0
                 a)
      (map-range +pi+ +tau+
                 -1.0 1.0
                 a))))


(defun make-audio-buffer ()
  (make-simple-array 'single-float +audio-buffer-size+ :initial-element 0.0))

(defun fill-buffer (buffer function rate start)
  (iterate
    (for i :index-of-vector buffer)
    (for angle :from start :by rate)
    (setf (aref buffer i) (funcall function angle))
    (finally (return (mod angle +tau+)))))

(defun fill-square (buffer rate start)
  (fill-buffer buffer #'square rate start))

(defun fill-sine (buffer rate start)
  (fill-buffer buffer #'sin rate start))

(defun fill-sawtooth (buffer rate start)
  (fill-buffer buffer #'saw rate start))


(defun audio-rate (frequency)
  (float (* (/ +tau+ +sample-rate+) frequency) 1.0))


(defun run-sound (chip)
  (portaudio:with-audio
    (portaudio:with-default-audio-stream
        (audio-stream 0 1
                      :sample-format :float
                      :sample-rate +sample-rate+
                      :frames-per-buffer +audio-buffer-size+)
      (with-chip (chip)
        (iterate (with buffer = (make-audio-buffer))
                 (with angle = 0.0)
                 (with rate = (audio-rate 440))
                 (while running)
                 (if (and (plusp sound-timer)
                          (not (debugger-paused-p debugger)))
                   (progn
                     (setf angle (fill-sawtooth buffer rate angle))
                     (portaudio:write-stream audio-stream buffer))
                   (sleep +audio-buffer-time+)))))))


;;;; Timers -------------------------------------------------------------------
(declaim
  (ftype (function (chip) null) decrement-timers run-timers))

(defun decrement-timers (chip)
  (flet ((decrement (i)
           (if (plusp i)
             (1- i)
             0)))
    (with-chip (chip)
      (sb-ext:atomic-update delay-timer #'decrement)
      (sb-ext:atomic-update sound-timer #'decrement)))
  nil)

(defun run-timers (chip)
  (with-chip (chip)
    (iterate
      (while running)
      (when (not (debugger-paused-p debugger))
        (decrement-timers chip))
      (sleep 1/60))))


;;;; CPU ----------------------------------------------------------------------
(declaim
  (ftype (function (chip) null) run-cpu emulate-cycle)
  (ftype (function (chip int16) null) dispatch-instruction))

(defparameter *c* nil)


(defun reset (chip)
  (with-chip (chip)
    (fill memory 0)
    (fill registers 0)
    (fill keys nil)
    (fill video 0)
    (load-font chip)
    (replace memory (read-file-into-byte-vector loaded-rom)
             :start1 #x200)
    (setf running t
          video-dirty t
          program-counter #x200
          delay-timer 0
          sound-timer 0
          (fill-pointer stack) 0))
  (values))

(defun load-rom (chip filename)
  (setf (chip-loaded-rom chip) filename)
  (reset chip))


(defun emulate-cycle (chip)
  (with-chip (chip)
    (debugger-print debugger chip)
    (if (debugger-should-wait-p debugger)
      (sleep 10/1000)
      (let ((instruction (cat-bytes (aref memory program-counter)
                                    (aref memory (1+ program-counter)))))
        (zapf program-counter (chop 12 (+ % 2)))
        (dispatch-instruction chip instruction)))
    nil))

(defun run-cpu (chip)
  (iterate
    (while (chip-running chip))
    (emulate-cycle chip)
    (for tick :every-nth +cycles-before-sleep+ :do
         (sleep (/ +cycles-before-sleep+ +cycles-per-second+)))))


;;;; Main ---------------------------------------------------------------------
(defun run (rom-filename)
  (let ((chip (make-chip)))
    (setf *c* chip)
    (load-rom chip rom-filename)
    (chip8.gui::run-gui
      chip
      (lambda ()
        ;; Really it's just the sound that needs to be here...
        (bt:make-thread (curry #'run-cpu chip))
        (bt:make-thread (curry #'run-timers chip))
        (bt:make-thread (curry #'run-sound chip))))))