[Fluxus] Continuations, take 1

David Kaloper dkaloper at mjesec.ffzg.hr
Wed Feb 16 21:19:40 PST 2011


Hello, list!

I know I should have consulted the devs ahead, but I got carried away... and
patched continuations into fluxus.

The story is this: after recently discovering this wonderful piece of software
and playing around for a bit, I got tired of the inversion of control that
every-frame imposes. So I went after spawn-timed-task, thinking it's a good
start and expecting to define-syntax something around it and go on my merry way.

Well, it turned out that timed tasks didn't want to spawn other timed tasks. So
that got fixed. While at it, I whipped up a few priority queues and replaced the
list there with the fastest one. Well, one thing led to another...  and this is
what I currently have:


* The spawn macro (probably a misnomer). It is executed synchronously, and by
  itself, it doesn't really do a thing.

  (let ([t #f])
    (list
      (time-now)
      (begin (spawn (set! t (time-now))) t)
      (time-now)))

* The restart-after procedure. If called during a dynamic extent of a spawn, it
  does just that - restarts after some time.

  (define (cube v)
    (with-state (colour (rndvec)) (translate v)
      (build-cube)))

  (let ([c1 (cube #(-2 0 0))]
        [c2 (cube #(2 0 0))])
    (spawn
      (do () (#f)
        (with-primitive c1 (translate #(0 0.5 0)))
        (restart-after 0.5)
        (with-primitive c2 (translate #(0 -0.5 0)))
        (restart-after 0.5))))

This creates two cubes and alternates between pushing each every half a second.

Well, actually, it can be simplified:

  (define (push-forever v t)
    (do () (#f) (translate v) (restart-after t)))

  (with-primitive (cube #(-2 0 0))
    (spawn (push-forever #(0 0.5 0) 1)))

  (with-primitive (cube #(2 0 0))
    (spawn (restart-after 0.5) (push-forever #(0 -0.5 0) 1)))


That is, spawn picks up the current grab. And restart-after keeps it.

  (define c1 (cube #(-2 0 0)))
  (define c2 (cube #(0 0 0)))

  (with-primitive c1
    (spawn (do () (#f)
             (restart-after 0.5)
             (translate #(0 1 0)) (restart-after 0.5)
             (with-primitive c2
               (translate #(0 1 0)) (restart-after 0.5)
               (translate #(0 -1 0)))
             (restart-after 0.5)
             (translate #(0 0.5 0)))))


If not within the context of a grab, they pick up the global state, instead:

  (translate #(0 2 0))

  (build-cube)

  (spawn (restart-after 2)
         (colour (rndvec)) (translate #(2 0 0))
         (build-cube))

  (identity)
  (build-cube)


... for each spawn independently:

  (define (sequence n d)
    (spawn (for ([_ (in-range n)])
      (translate d) (scale #3(0.9)) (build-cube)
      (restart-after 0.1))))

  (sequence 10 #(0.1 -1 0))
  (sequence 10 #(-0.1 -1 0))
  (sequence 20 #(0 1 0))


* Then there's restart-next-frame procedure. It can be used to synchronize with
  the frame rate:

  (define (rndvec* [c 1])
    (vmul (vadd (rndvec) #3(-0.5)) c))

  (define (walkabout)
    (with-primitive (build-cube)
      (colour (rndvec)) (scale (vadd #3(1) (rndvec* 0.5)))
      (spawn (do () (#f)
               (restart-after (random 4))
               (let* ([point (vmul (rndvec*) 3)]
                      [direction (vnormalise point)])
                 (let loop ([hop #3(0)] [path (vmag point)])
                   (when (positive? path)
                     (translate hop)
                     (restart-next-frame)
                     (loop (vmul direction (* 5 (delta)))
                           (- path (vmag hop))))))))))

  (for ([_ (in-range 10)])
    (translate (rndvec* 5)) (walkabout))


Using restart-after and restart-next-frame moves the spawn'd task between
spawn-task and spawn-timed-task scheduling mechanisms.

Here's an example that tests if everything is working:

  ; For cartoonish effect
  (define (clear*)
    (clear-colour #3(0.1)) (clear)
    (rm-all-tasks)
    (hint-on 'wire) (wire-colour #3()) (line-width 2)
    (show-fps 1))

  (define (cube-stream t1 t2)
    (spawn
      (do () (#f)
        (translate (rndvec* 3))
        (scale (let ([c (+ (* (random) 0.3) 0.85)])
                 (vector c c c)))
        (take-cube (with-state
                     (rotate (rndvec* 90))
                     (build-cube)) t2)
        (restart-after t1))))

  (define (take-cube c t)
    (define (rot)
      (rotate (vector 0 (* (delta) 90) 0)))

    (with-primitive c
      (spawn
        (do ([stop (+ (time-now) t)]) ((> (time-now) stop))
          (rot) (restart-next-frame))
        (for ([o (in-range 1 0 -0.1)])
          (rot) (opacity o) (restart-next-frame))
        (destroy c))))


  (clear*)

  (for ([_ (in-range 5)])
    (cube-stream (random) (* (random) 3)))


***

every-frame seems to work best when the vis is mostly a function of time and
other inputs, and for attending to created objects and animating them. But for
large, discrete events, that programmatically happen here and there and make big
edits to the scene graph, I think this really comes in handy. And it meshes well
with regular spawn-task and spawn-timed-task.

***

Changes are mostly localized to tasks.ss and restartable.ss. The latter
implements this trickery, the former got a little streamlined.

Existing stuff visibly changed:

- spawn-timed-task works recursively :)

- tasks are no longer executed in lexicographical order of their keys; sorting
  and traversing the list gets a little slow for large numbers of tasks, so this
  is now handled by a persistent hash. If the ordered execution is widely used,
  I can add an ordered-dict? structure in its place. Splay trees and skip lists
  that come with racket were way too heavy.

- clear clears all the timed tasks, often a saving grace. ...

Internally changed stuff:

- the structure supporting timed tasks can take a much larger beating now.

- with-primitive and with-state don't longer simply expand to (begin (grab/push)
  (let ([res ...]) (ungrab/pop) res)); now they guard their dynamic extent and
  redo the initialization task every time they are reactivated by a continuation
  entry (dynamic-wind). with-state furthermore saves the opengl state on exit
  and restores it on enter, so the state observed within the bracket is
  consistent between exits/enters. This comes at negligible cost for normal code
  paths, but the way state is saved and restored if continuations are involved
  is open for debate.

To do:

- saving / restoring of opengl state was the _intention_, currently only the
  transformation matrix is handled. I don't know how to get hold of the rest.
  Maybe a little help from C++? Ideally, we could get an opaque reference to
  everything push/pop acts on, and avoid marshalling data into scheme. Affected
  functions are get-ogl-state and apply-saved-ogl-state in building-blocks.ss.

- if there is a way to check if a grab is currently active, with-... can be a
  little simplified (no dynamic parameter).


Umm.. That's all. I think this post has more lines than the code! So, like, a
pull request?

github.com/pqwy/fluxus, branch restartable-tasks.


Cheers, David


-- 
"Linear Time is wrong and suicidal." -- Gene Ray



More information about the Fluxus mailing list