This weekend, I wanted to have dynamic access to SDL for some interactive graphing, and again ran into the problem from More Fun and Swearing with Scheme. With more experienced eyes, I read the Chez Scheme User's Guide and discovered everything I'd done wrong a year+ ago.
First, install Thunderchez somewhere, I just put it in ~/Code/CodeChezScheme
In particular, read ffi-utils.sls which provides somewhat easier wrapping of binary flags; thunderchez is a very low-level port.
Second, install SDL2, I used port install libsdl2
and that puts it in /opt/local/lib
, but in production I'll use a local dir. Read the SDL2 API for reference.
Took me a few hours to translate the basic functions I needed, and now I have:
sdltest.ss:
#!/usr/bin/env scheme-script
;; sdltest.ss
;; Copyright © 2020 by Mark Damon Hughes. All Rights Reserved.
(import (chezscheme)
(sdl2) ;; thunderchez
)
;; Set this to your location. I used `port install libsdl2`, YMMV.
;; In production, I'll use a local dir with libs.
(load-shared-object "/opt/local/lib/libSDL2.dylib")
(define kWindowWidth 320)
(define kWindowHeight 240)
(define gWindow #f)
(define gRender #f)
(define gState 'quit)
(define (gr-init title x y w h)
(sdl-set-main-ready)
(sdl-init sdl-initialization-everything)
(set! gWindow (sdl-create-window title x y w h (sdl-window-flags 'shown 'allow-highdpi)) )
(set! gRender (sdl-create-renderer gWindow -1 (sdl-renderer-flags 'accelerated)) )
(sdl-show-window gWindow)
(sdl-raise-window gWindow)
)
(define (gr-event)
(make-ftype-pointer sdl-event-t (foreign-alloc (ftype-sizeof sdl-event-t)))
)
(define (gr-rect x y w h)
(let [ (rect (make-ftype-pointer sdl-rect-t (foreign-alloc (ftype-sizeof sdl-rect-t)))) ]
(ftype-set! sdl-rect-t (x) rect x)
(ftype-set! sdl-rect-t (y) rect y)
(ftype-set! sdl-rect-t (w) rect w)
(ftype-set! sdl-rect-t (h) rect h)
rect
))
(define (gr-mainloop)
(let [ (event (gr-event)) ]
(let mainloop []
;; event
(sdl-poll-event event)
(let [ (evt-type (ftype-ref sdl-event-t (type) event)) ]
(cond
[(eqv? evt-type (sdl-event-type 'quit)) (set! gState 'quit)]
[(eq? gState 'main) (main-event evt-type event) ]
))
;; TODO: loop polling until no event left
;; update
(case gState
[(main) (main-update) ]
)
;; render
(sdl-render-clear gRender)
(case gState
[(main) (main-render) ]
)
(sdl-render-present gRender)
;; loop
;; TODO: wait timer, currently burns CPU
(unless (eq? gState 'quit) (mainloop))
))
(gr-shutdown)
)
(define (gr-shutdown)
(sdl-quit)
)
(define (main-event evt-type event)
#f ;; no event handling yet
)
(define (main-update)
#f ;; no updates yet
)
(define (main-render)
(sdl-set-render-draw-color gRender 0 255 0 255) ;; green
(sdl-render-fill-rect gRender (gr-rect 0 0 kWindowWidth kWindowHeight))
)
(define (main argv)
(gr-init "Hello, Thunderchez!" 100 100 kWindowWidth kWindowHeight)
(set! gState 'main)
(gr-mainloop)
)
(main (command-line-arguments))
To start it I just need:
% CHEZSCHEMELIBDIRS=thunderchez-trunk: ./sdltest.ss
Green window means it's working! And it can be closed with the window close button or Cmd-Q, but menu or other events don't work yet.
So… to port my graphics library from Chicken to Chez is probably a week's work, maybe less—I only fill rects, blit images, play sounds, and read a few events. My sound support on Chicken is minimal because the maintainer didn't bother to port the audio API, I had to do that myself.
Rehosting my game on Chez is an unknowably difficult debugging problem, but I code in a way that's usually portable. Large chunks of my large marklib
library exist only to work around the non-portable parts and missing features of R5RS.
But the advantages are:
- Chez's R6RS is a better language than Chicken's R5RS++/R7RS-not-really.
- Chez is much faster, sometimes just 10-25%, sometimes hundreds of times, even compiled.
- Chez's JIT compiler is fast and invisible. Working on Chez is instant gratification. Chicken has a 30-second compile before I see anything, which has been impossibly frustrating.
At the very least, if I get my graphics library ported I can do the graphing I wanted last weekend.