[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