Running racket/gui at 60fps

I'm working on my first project in Racket. I think it's awesome that the language includes a cross platform GUI toolkit, and I'l like to wrap my head around how to make it perform well.

I tried making a small canvas that draws a grid of white and black squares which get randomized every frame. When you run the program below and press a key on your keyboard, it quits and dumps out the overall measured FPS.

In its current state it runs on my macbook M1 at ~20fps. I'd really like to be able to run at least 60 fps. Does anyone have some tips to help me get there?

#lang racket

(require racket/gui)
(require racket/random)

(define *width* 64)
(define *height* 32)
(define *scale* 10)
(define *counted-frames* 0)
(define *start-time* 0)
(define *end-time* 0)

(define *timer*
  (new timer%
       [notify-callback
        (lambda ()
          (update-grid!)
          (send *canvas* refresh-now)
          (flush-output))]))

(define *grid*
  (for/vector ([i (in-range (* *height* *width*))])
    #f))

(define (update-grid!)
  (for ([i (vector-length *grid*)])
    (if (zero? (random 2))
        (vector-set! *grid* i #f)
        (vector-set! *grid* i #t))))

(define (sample-grid)
  (for ([i (in-range 10)])
    (println (vector-ref *grid* i))))

(define (get-element x y)
  (vector-ref *grid* (+ (* y *width*) x)))

(define *frame*
  (new frame% [label "Checkboard Grid"]
       [width (* *scale* *width*)]
       [height (* *scale* *height*)]))

(define rect-canvas%
  (class canvas%
    (inherit get-dc)
    (super-new)
    (define/override (on-char event)
      (begin
             (fprintf (current-error-port)
                      "Received key event: ~a, quitting\n" event)
             (set! *end-time* (current-inexact-milliseconds))
             (println (format "FPS: ~a"
                              (/ (* 1000 *counted-frames*)
                                 (- *end-time* *start-time*))))
             (exit)))
    (define/override (on-paint)
      (set! *counted-frames* (+ 1 *counted-frames*))
      (let ((dc (get-dc)))
        (for* ([x (in-range *width*)] [y (in-range *height*)])
          (if (get-element x y)
              (begin
                (send dc set-pen "black" 1 'solid)
                (send dc set-brush "black" 'solid))
              (begin
                (send dc set-pen "white" 1 'solid)
                (send dc set-brush "white" 'solid)))
          (send dc draw-rectangle (* x 10) (* y 10) 10 10))))))

(define *canvas* (new rect-canvas% [parent *frame*]))

(define (run)
  (set! *start-time* (current-inexact-milliseconds))
  (send *frame* show #t)
  (send *timer* start 1))

(run)
1 Like

I don't think, you'll see much higher fps without doing less work in on-paint.

This might give you a small gain though:

   (define black-pen   (new pen%   [color "black"] [width 1] [style 'solid]))
    (define white-pen   (new pen%   [color "white"] [width 1] [style 'solid]))
    (define black-brush (new brush% [color "black"]           [style 'solid]))
    (define white-brush (new brush% [color "white"]           [style 'solid]))
    (define dc (get-dc))
    (define/override (on-paint)
      (set! *counted-frames* (+ 1 *counted-frames*))
      (for* ([x (in-range *width*)] [y (in-range *height*)])
        (if (get-element x y)
            (begin
              (send dc set-pen   black-pen)
              (send dc set-brush black-brush))
            (begin
              (send dc set-pen   white-pen)
              (send dc set-brush white-brush)))
        (send dc draw-rectangle (* x 10) (* y 10) 10 10)))

If your game is a retro game with 10x10 "pixels", then you can get a speedup
by having a bitmap with a low resolution, say, 100x80 and then
in on-paint finish by drawing a scaled copy to the bitmap to the screen.

This technique is used in here:

https://github.com/soegaard/sketching/blob/main/sketching-examples/examples/pacman/maze-game.rkt

[It's not 60 fps though.]

If that's not fast enough, you'll need to look at how to use the GPU - either
directly or using one of the available libraries.

2 Likes

I am indeed working on retro gaming stuff, I'm finally getting around to writing a CHIP-8 emulator and learning Racket, both of which I've been putting off for years.

The game source you linked is an extremely helpful, thanks so much for sharing it! And your suggestion gave me a nice performance bump to >40fps. I'll give the bitmap approach a try and see how that works. I figured someone with more graphics know-how would help me find some low hanging fruit :slight_smile:

Even if I don't quite hit 60 fps, I still think I would rather use the built in GUI than reach for external libraries in this case. I actually had looked at a CL implementation of a chip-8 emulator that I couldn't run on my system because one of the dependencies does not build on aarch64, and I would rather avoid creating a similar situation for my project if possible. I am a big fan of the "just works" factor that you get with Racket's built in goodies, it strikes me as one of the defining advantages of the language that you don't really get elsewhere.

3 Likes

You might be interested in some of @jeapostrophe’s gaming projects, e.g.:

1 Like

I doubled the fps (from 8 to 15) skipping the squares that didn't change. I added [style (list 'no-autoclear)] to the canvas, and save the last color in grid/old.

In this case that is random, half of the squares can be skipped (In average). In a game with a fixed background this can be better, but in a game with a lot of scrolling and colors it may be worse.

#lang racket

(require racket/gui)
(require racket/random)

(define *width* 64)
(define *height* 32)
(define *scale* 10)
(define *counted-frames* 0)
(define *start-time* 0)
(define *end-time* 0)

(define *timer*
  (new timer%
       [notify-callback
        (lambda ()
          (update-grid!)
          (send *canvas* refresh-now)
          (flush-output))]))

(define *grid*
  (for/vector ([i (in-range (* *height* *width*))])
    (void)))

(define *grid/old*
  (for/vector ([i (in-range (* *height* *width*))])
    (void)))

(define (update-grid!)
  (for ([i (vector-length *grid*)])
    (if (zero? (random 2))
        (vector-set! *grid* i #f)
        (vector-set! *grid* i #t))))

(define (sample-grid)
  (for ([i (in-range 10)])
    (println (vector-ref *grid* i))))

(define (get-element x y)
  (vector-ref *grid* (+ (* y *width*) x)))

(define (get-element/old x y)
  (vector-ref *grid/old* (+ (* y *width*) x)))

(define (set-element/old! x y v)
  (vector-set! *grid/old* (+ (* y *width*) x) v))

(define *frame*
  (new frame% [label "Checkboard Grid"]
       [width (* *scale* *width*)]
       [height (* *scale* *height*)]))

(define rect-canvas%
  (class canvas%
    (inherit get-dc)
    (super-new [style (list 'no-autoclear)])
    (define/override (on-char event)
      (begin
             (fprintf (current-error-port)
                      "Received key event: ~a, quitting\n" event)
             (set! *end-time* (current-inexact-milliseconds))
             (println (format "FPS: ~a"
                              (/ (* 1000 *counted-frames*)
                                 (- *end-time* *start-time*))))
              (exit)))
    (define black-pen   (new pen%   [color "black"] [width 1] [style 'solid]))
    (define white-pen   (new pen%   [color "white"] [width 1] [style 'solid]))
    (define black-brush (new brush% [color "black"]           [style 'solid]))
    (define white-brush (new brush% [color "white"]           [style 'solid]))
    (define dc (get-dc))
    (define/override (on-paint)
      (set! *counted-frames* (+ 1 *counted-frames*))
      (for* ([x (in-range *width*)] [y (in-range *height*)])
        (unless (eq? (get-element x y) (get-element/old x y))
          (set-element/old! x y (get-element x y))
          (if (get-element x y)
              (begin
                (send dc set-pen   black-pen)
                (send dc set-brush black-brush))
              (begin
                (send dc set-pen   white-pen)
                (send dc set-brush white-brush)))
          (send dc draw-rectangle (* x 10) (* y 10) 10 10))))))

(define *canvas* (new rect-canvas% [parent *frame*]))

(define (run)
  (set! *start-time* (current-inexact-milliseconds))
  (send *frame* show #t)
  (send *timer* start 1))

(run)
``
4 Likes

Here is a version that hits 60 FPS on my machine:

#lang racket/base

(require racket/fixnum
         racket/gui)

(define columns 64)
(define rows 32)
(define scale 10)
(define ticks 0)
(define grid
  (make-fxvector (fx* rows columns)))
(define (update-grid!)
  (for ([i (in-range (fxvector-length grid))])
    (fxvector-set! grid i (if (zero? (random 2)) 0 1))))
(define frame
  (new frame%
       [label "Checkboard Grid"]
       [width (fx* scale columns)]
       [height (fx* scale rows)]))
(define canvas
  (new
   (class canvas%
     (inherit get-dc)
     (super-new [parent frame] [style '(no-autoclear)])
     (define/override (on-char event)
       (fprintf (current-error-port) "Received key event: ~a, quitting\n" event)
       (define end-time (current-inexact-milliseconds))
       (println (format "FPS: ~a" (/ (* 1000 ticks) (- end-time start-time))))
       (send frame show #f))
     (define dc (get-dc))
     (send dc set-background "white")
     (send dc set-pen "black" 0 'transparent)
     (send dc set-brush "black" 'solid)
     (define/override (on-paint)
       (send dc clear)
       (for ([(v i) (in-indexed (in-fxvector grid))]
             #:when (fx= v 1))
         (define x (fxremainder i columns))
         (define y (fxquotient i columns))
         (send dc draw-rectangle (fx* x scale) (fx* y scale) scale scale))))))
(define start-time
  (current-inexact-milliseconds))
(send frame show #t)
(void
 (thread
  (lambda ()
    (let tick ()
      (update-grid!)
      (send canvas refresh-now)
      (set! ticks (fx+ ticks 1))
      (sleep 0)
      (tick)))))

The biggest win (over @gus-massa's version) comes from using a transparent pen. The rest of the changes are mostly golfing and don't account for much.

5 Likes

I tried a couple different approaches, and I got the biggest speedup in reported FPS by creating a white and black dc-path%s, adding rectangles to those, and then drawing the paths with draw-path. Adding a bitmap between the dc-paths and the canvas added a few FPS on top of that.

The original ran at about 14 FPS for me. With the dc-path approach the program reported about 93 FPS. However, by watching the window, Racket was clearly not actually displaying every "frame"; in fact, it appeared to be updating more slowly than it did with approaches like @soegaard's. So I think that there are limitations to Racket's canvas flushing that are going to limit your actual FPS beyond the work you do in on-paint.

On the other other hand, I notice you are running the timer as fast as you can (1ms intervals). If I change that to 10ms, I get a reported 50 FPS that looks like it might actually be 50 FPS, with fairly little stutter. I always got always awful stutter in all of the approaches running under a 1ms timer. I thought maybe it was due to GC pauses, but now I think it might be scheduler-related instead.

5 Likes