[Fluxus] Continuations, take 1

David Griffiths dave at pawfal.org
Thu Feb 17 02:44:35 PST 2011


Hi David,

I'll go through this in more detail when I have time, but just to say
this is great, and definately the way we need to be going in.

cheers,

dave

On Thu, 2011-02-17 at 06:19 +0100, David Kaloper wrote:
> 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
> 
> 





More information about the Fluxus mailing list