diff --git a/recipes/woods-recipe-optimized.sexp b/recipes/woods-recipe-optimized.sexp new file mode 100644 index 0000000..4b9b83e --- /dev/null +++ b/recipes/woods-recipe-optimized.sexp @@ -0,0 +1,195 @@ +;; Woods Recipe - OPTIMIZED VERSION +;; +;; Uses fused-pipeline for GPU acceleration when available, +;; falls back to individual primitives on CPU. +;; +;; Key optimizations: +;; 1. Uses streaming_gpu primitives with fast CUDA kernels +;; 2. Uses fused-pipeline to batch effects into single kernel passes +;; 3. GPU persistence - frames stay on GPU throughout pipeline + +(stream "woods-recipe-optimized" + :fps 30 + :width 1920 + :height 1080 + :seed 42 + + ;; Load optimized GPU primitives (falls back to CPU automatically) + (require-primitives "streaming_gpu") ;; Includes fused-pipeline + (require-primitives "geometry_gpu") ;; Fast CUDA rotate/ripple + (require-primitives "color_ops_gpu") ;; Fast CUDA hue-shift/invert + (require-primitives "blending_gpu") ;; Fast CUDA blend + (require-primitives "core") + (require-primitives "image") + + ;; === SOURCES === + (def sources [ + (streaming_gpu:make-video-source "woods-1" 30) + (streaming_gpu:make-video-source "woods-2" 30) + (streaming_gpu:make-video-source "woods-3" 30) + (streaming_gpu:make-video-source "woods-4" 30) + (streaming_gpu:make-video-source "woods-5" 30) + (streaming_gpu:make-video-source "woods-6" 30) + (streaming_gpu:make-video-source "woods-7" 30) + (streaming_gpu:make-video-source "woods-8" 30) + ]) + + ;; Per-pair config + (def pair-configs [ + {:dir -1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5} + {:dir 1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5} + {:dir 1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5} + {:dir -1 :rot-a -45 :rot-b 45 :zoom-a 0.5 :zoom-b 1.5} + {:dir -1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5} + {:dir 1 :rot-a 30 :rot-b -30 :zoom-a 1.3 :zoom-b 0.7} + {:dir -1 :rot-a -45 :rot-b 45 :zoom-a 0.5 :zoom-b 1.5} + {:dir 1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5} + ]) + + ;; Audio + (def music (streaming_gpu:make-audio-analyzer "woods-audio")) + (audio-playback "woods-audio") + + ;; === SCANS === + + ;; Cycle state + (scan cycle (streaming_gpu:audio-beat music t) + :init {:active 0 :beat 0 :clen 16} + :step (if (< (+ beat 1) clen) + (dict :active active :beat (+ beat 1) :clen clen) + (dict :active (mod (+ active 1) (len sources)) :beat 0 + :clen (+ 8 (mod (* (streaming_gpu:audio-beat-count music t) 7) 17))))) + + ;; Spin scan + (scan spin (streaming_gpu:audio-beat music t) + :init {:angle 0 :dir 1 :speed 2} + :step (let [new-dir (if (< (core:rand) 0.05) (* dir -1) dir) + new-speed (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) speed)] + (dict :angle (+ angle (* new-dir new-speed)) + :dir new-dir + :speed new-speed))) + + ;; Ripple scan + (scan ripple-state (streaming_gpu:audio-beat music t) + :init {:gate 0 :cx 960 :cy 540} + :step (let [new-gate (if (< (core:rand) 0.15) (+ 3 (core:rand-int 0 5)) (core:max 0 (- gate 1))) + new-cx (if (> new-gate gate) (+ 200 (core:rand-int 0 1520)) cx) + new-cy (if (> new-gate gate) (+ 200 (core:rand-int 0 680)) cy)] + (dict :gate new-gate :cx new-cx :cy new-cy))) + + ;; Pair states + (scan pairs (streaming_gpu:audio-beat music t) + :init {:states (map (core:range (len sources)) (lambda (_) + {:inv-a 0 :inv-b 0 :hue-a 0 :hue-b 0 :hue-a-val 0 :hue-b-val 0 :mix 0.5 :mix-rem 5 :angle 0 :rot-beat 0 :rot-clen 25}))} + :step (dict :states (map states (lambda (p) + (let [new-inv-a (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- (get p :inv-a) 1))) + new-inv-b (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- (get p :inv-b) 1))) + old-hue-a (get p :hue-a) + old-hue-b (get p :hue-b) + new-hue-a (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- old-hue-a 1))) + new-hue-b (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- old-hue-b 1))) + new-hue-a-val (if (> new-hue-a old-hue-a) (+ 30 (* (core:rand) 300)) (get p :hue-a-val)) + new-hue-b-val (if (> new-hue-b old-hue-b) (+ 30 (* (core:rand) 300)) (get p :hue-b-val)) + mix-rem (get p :mix-rem) + old-mix (get p :mix) + new-mix-rem (if (> mix-rem 0) (- mix-rem 1) (+ 1 (core:rand-int 1 10))) + new-mix (if (> mix-rem 0) old-mix (* (core:rand-int 0 2) 0.5)) + rot-beat (get p :rot-beat) + rot-clen (get p :rot-clen) + old-angle (get p :angle) + new-rot-beat (if (< (+ rot-beat 1) rot-clen) (+ rot-beat 1) 0) + new-rot-clen (if (< (+ rot-beat 1) rot-clen) rot-clen (+ 20 (core:rand-int 0 10))) + new-angle (+ old-angle (/ 360 rot-clen))] + (dict :inv-a new-inv-a :inv-b new-inv-b + :hue-a new-hue-a :hue-b new-hue-b + :hue-a-val new-hue-a-val :hue-b-val new-hue-b-val + :mix new-mix :mix-rem new-mix-rem + :angle new-angle :rot-beat new-rot-beat :rot-clen new-rot-clen)))))) + + ;; === OPTIMIZED PROCESS-PAIR MACRO === + ;; Uses fused-pipeline to batch rotate+hue+invert into single kernel + (defmacro process-pair-fast (idx) + (let [;; Get sources for this pair + src-a (nth sources (* idx 2)) + src-b (nth sources (+ (* idx 2) 1)) + cfg (nth pair-configs idx) + pstate (nth (bind pairs :states) idx) + + ;; Read frames (GPU decode, stays on GPU) + frame-a (streaming_gpu:source-read src-a t) + frame-b (streaming_gpu:source-read src-b t) + + ;; Get state values + dir (get cfg :dir) + rot-max-a (get cfg :rot-a) + rot-max-b (get cfg :rot-b) + pair-angle (get pstate :angle) + inv-a-on (> (get pstate :inv-a) 0) + inv-b-on (> (get pstate :inv-b) 0) + hue-a-on (> (get pstate :hue-a) 0) + hue-b-on (> (get pstate :hue-b) 0) + hue-a-val (get pstate :hue-a-val) + hue-b-val (get pstate :hue-b-val) + mix-ratio (get pstate :mix) + + ;; Calculate rotation angles + angle-a (* dir pair-angle rot-max-a 0.01) + angle-b (* dir pair-angle rot-max-b 0.01) + + ;; Define effect pipelines for each source + ;; These get compiled to single CUDA kernels! + effects-a [{:op "rotate" :angle angle-a} + {:op "hue_shift" :degrees (if hue-a-on hue-a-val 0)} + {:op "invert" :amount (if inv-a-on 1 0)}] + effects-b [{:op "rotate" :angle angle-b} + {:op "hue_shift" :degrees (if hue-b-on hue-b-val 0)} + {:op "invert" :amount (if inv-b-on 1 0)}] + + ;; Apply fused pipelines (single kernel per source!) + processed-a (streaming_gpu:fused-pipeline frame-a effects-a) + processed-b (streaming_gpu:fused-pipeline frame-b effects-b)] + + ;; Blend the two processed frames + (blending_gpu:blend processed-a processed-b mix-ratio))) + + ;; === FRAME PIPELINE === + (frame + (let [now t + e (streaming_gpu:audio-energy music now) + + ;; Get cycle state + active (bind cycle :active) + beat-pos (bind cycle :beat) + clen (bind cycle :clen) + + ;; Transition logic + phase3 (* beat-pos 3) + fading (and (>= phase3 (* clen 2)) (< phase3 (* clen 3))) + fade-amt (if fading (/ (- phase3 (* clen 2)) clen) 0) + next-idx (mod (+ active 1) (len sources)) + + ;; Process active pair with fused pipeline + active-frame (process-pair-fast active) + + ;; Crossfade during transition + result (if fading + (let [next-frame (process-pair-fast next-idx)] + (blending_gpu:blend active-frame next-frame fade-amt)) + active-frame) + + ;; Final effects pipeline (fused!) + spin-angle (bind spin :angle) + rip-gate (bind ripple-state :gate) + rip-amp (* rip-gate (core:map-range e 0 1 5 50)) + rip-cx (bind ripple-state :cx) + rip-cy (bind ripple-state :cy) + + ;; Fused final effects + final-effects [{:op "rotate" :angle spin-angle} + {:op "ripple" :amplitude rip-amp :frequency 8 :decay 2 + :phase (* now 5) :center_x rip-cx :center_y rip-cy}]] + + ;; Apply final fused pipeline + (streaming_gpu:fused-pipeline result final-effects + :rotate_angle spin-angle + :ripple_phase (* now 5)))))