Hacker News new | past | comments | ask | show | jobs | submit login
CHIP-8 in Common Lisp (stevelosh.com)
157 points by tosh on Aug 6, 2017 | hide | past | favorite | 20 comments



Cool. I made a so called dynamically recompiling (dynarec) CHIP-8 emulator in Lisp myself, but never got around to implementing the actual graphics, input, and sound.

https://github.com/flambard/chip-8-dynarec

Perhaps my project will be completed after all.. :-)


Very interesting read! But could somebody explain why the following piece of code is needed? And why is it defined inline?

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


vref and (setf vref) define getter and setter functions for manipulating pixel arrays. Since these functions are used a lot, it makes sense to allow compiler to inline them for maximum speed.

Normally function must be accessed trough the symbol and it can be redefined on the fly.


Functions don't need to be accessed through them symbol. A file compiler can assume that a function does not change inside a file, etc. Inlining and being called via a symbol are slightly different things...

I would also think that lexical functions are not called via symbols...


I understand what they do, and I appreciate that it helps performance to inline them. What I don't understand is the (setf vref) syntax. I thought that after a defun statement you had to have a symbol, not an S-expression? Something like so:

    (defun-inline set-vref (new-value chip x y)
       ...)
And how would you actually call the original function to set a pixel? (I'm not very familiar with inlines, maybe that's my problem here.)


Common lisp has this universal setter-macro, SETF, which can be used to set values to all sorts of places.

In this case you would use

    (setf (vref chip x y) new-value)
to call the function (just like with AREF above). Using SETF has the advantage that you can define "modify macros" that read and write a place. For example,

    (incf (vref chip x y) delta)
would increment the place by DELTA. Or

    (rotatef (vref chip x1 y1)
             (vref chip x2 y2))
would swap two pixels.


This is special syntax in Common Lisp. (SETF F) actually names a function too. It allows you to define a setter (a setf-expansion) with a normal DEFUN.


It would be interesting to do a measurement of inline vs non-inline to see how much of a speed difference it makes.


Why is DEFUN-INLINE used over:

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


Less typing/reading.


Wouldn't a lookup table be much faster here? That multiply on every screen access can't help the speed.


+screen-width+ is 64, which means SBCL can convert the multiply to a left shift if we care about speed and declare types:

    (defun foo (chip y)
      (declare (optimize speed)
               (type chip chip)
               (type int16 y))
      (vref chip 0 y))
    
    (disassemble 'foo)

    ; disassembly for FOO
    ; Size: 48 bytes. Origin: #x100EF5E39D
    ; 9D:       488B423D         MOV RAX, [RDX+61]                ; no-arg-parsing entry point
    ;; ---------------------- Look ma, no multiplication!
    ;;                           vvvvvvvvvvv
    ; A1:       48C1E706         SHL RDI, 6
    ; A5:       B900100000       MOV ECX, 4096
    ; AA:       40F6C701         TEST DIL, 1
    ; AE:       7513             JNE L0
    ; B0:       4839F9           CMP RCX, RDI
    ; B3:       760E             JBE L0
    ; B5:       488B54B801       MOV RDX, [RAX+RDI*4+1]
    ; BA:       488BE5           MOV RSP, RBP
    ; BD:       F8               CLC
    ; BE:       5D               POP RBP
    ; BF:       C3               RET
    ; C0:       0F0B10           BREAK 16                         ; Invalid argument count trap
    ; C3: L0:   0F0B0A           BREAK 10                         ; error trap
    ; C6:       06               BYTE #X06
    ; C7:       0C               BYTE #X0C                        ; INVALID-ARRAY-INDEX-ERROR
    ; C8:       1B               BYTE #X1B                        ; RAX
    ; C9:       9A               BYTE #X9A                        ; RCX
    ; CA:       FE9A03           BYTE #XFE, #X9A, #X03            ; RDI


This is one of the coolest things i've ever seen!

It's a rare treat to see low level programming in Common Lisp!!

+100 for Steve Losh!



> Also note that the program counter starts at address #x200, because that’s where the ROM data eventually gets loaded into the CHIP-8 memory.

If I understand correctly, that's because 0-1FF was ROM (including the interpreter itself) on the original machine, and you typed CHIP-8 programs into RAM.


Yes, traditionally the interpreter was located there. However, there is no "original machine". CHIP-8 was originally designed as a virtual machine for portability, similar to Java.

https://en.wikipedia.org/wiki/CHIP-8


Yes; I mean the machines on which the original interpreter ran, not that CHIP-8 itself was originally released in a hardware implementation.


So here, Steve Losh, one of the best Vimmers out there, is also an excellent Lisper! It would be interesting to hear his views on Emacs since he transitioned to Clojure. I am curious to know whether he uses Vim for Clojure/Lisp development.


I moved from Clojure to Common Lisp a few years ago.

I still use (Neo)Vim, using VLIME for Common Lisp interaction: https://github.com/l04m33/vlime It works great.


I missed your response, really hard to see responses in comments on #HN. I'm now curious why you moved from clojure to CLISP! I thought most people move the other way around!




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: