changeset 174:f4b82e358751

Reimplementation of async-view. Previous version had known unfixed race conditions.
author Mikhail Kryshen <mikhail@kryshen.net>
date Wed, 10 Dec 2014 20:01:45 +0300
parents 8769a7b50b4f
children eb1bedf22731
files src/indyvon/async.clj
diffstat 1 files changed, 97 insertions(+), 103 deletions(-) [+]
line diff
     1.1 --- a/src/indyvon/async.clj	Wed Dec 10 19:00:17 2014 +0300
     1.2 +++ b/src/indyvon/async.clj	Wed Dec 10 20:01:45 2014 +0300
     1.3 @@ -29,16 +29,19 @@
     1.4                           ThreadPoolExecutor$DiscardOldestPolicy
     1.5                           ArrayBlockingQueue TimeUnit)))
     1.6  
     1.7 -(defrecord Buffer [id image readers state])
     1.8 -;; Buffer states:
     1.9 -;;   :front, readers > 0
    1.10 -;;      being copied on screen
    1.11 -;;   :back
    1.12 -;;      being rendered to (offscreen)
    1.13 -;;   :fresh
    1.14 -;;      most recently updated
    1.15 -;;   :free
    1.16 -;;      not in use
    1.17 +(defrecord Buffers [;; BufferedImage for copying on screen.
    1.18 +                    front
    1.19 +                    ;; Number of processes using the front buffer.
    1.20 +                    front-readers 
    1.21 +                    ;; BufferedImage used for asynchronous drawing.
    1.22 +                    back
    1.23 +                    ;; True after drawing to the back buffer is finished.
    1.24 +                    back-ready
    1.25 +                    ;; True after the buffers were flipped,
    1.26 +                    ;; indicates that AsyncView needs to be redrawn.
    1.27 +                    flipped
    1.28 +                    ;; Used to synchronize initialization of the buffers.
    1.29 +                    new])
    1.30  
    1.31  (defn- create-image [async-view ^GraphicsConfiguration device-conf]
    1.32    ;; TODO: support different image types.
    1.33 @@ -47,103 +50,94 @@
    1.34                            (:height async-view)
    1.35                            Transparency/TRANSLUCENT))
    1.36  
    1.37 -(defn- create-buffer [async-view device-conf]
    1.38 -  (->Buffer (Object.) (create-image async-view device-conf) 0 :free))
    1.39 +(defn- create-buffers [async-view device-conf]
    1.40 +  (->Buffers (create-image async-view device-conf)
    1.41 +             0
    1.42 +             (create-image async-view device-conf)
    1.43 +             false
    1.44 +             false
    1.45 +             true))
    1.46  
    1.47 -(defn- find-buffer
    1.48 -  "Find a buffer with the one of the specified states given
    1.49 -   in the order of preference."
    1.50 -  [buffers & states]
    1.51 -  (some identity
    1.52 -        (for [state states]
    1.53 -          (some #(if (= (:state %) state) % nil) buffers))))
    1.54 +(defn- maybe-flip [buffers]
    1.55 +  (if (and (:back-ready buffers)
    1.56 +           (zero? (:front-readers buffers)))
    1.57 +    (assoc buffers
    1.58 +      :front (:back buffers)
    1.59 +      :back (:front buffers)
    1.60 +      :back-ready false
    1.61 +      :flipped true)
    1.62 +    (assoc buffers
    1.63 +      :flipped false)))
    1.64  
    1.65 -(defn- replace-buffer [buffers buffer]
    1.66 -  (conj (remove #(= (:id %) (:id buffer)) buffers)
    1.67 -        buffer))
    1.68 +(defn- apply-flip [buffers f & args]
    1.69 +  (maybe-flip (apply f buffers args)))
    1.70  
    1.71 -(defn- take-buffer [al type]
    1.72 -  (dosync
    1.73 -   (let [buffers @(:buffers al)
    1.74 -         b (case type
    1.75 -             :front (find-buffer buffers :front :fresh :free)
    1.76 -             :back (find-buffer buffers :free :fresh)
    1.77 -             (throw (IllegalArgumentException.)))
    1.78 -         readers (if (= type :front)
    1.79 -                   (inc (:readers b))
    1.80 -                   (:readers b))
    1.81 -         b (assoc b
    1.82 -             :state type
    1.83 -             :readers readers)]
    1.84 -     (alter (:buffers al) replace-buffer b)
    1.85 -     b)))
    1.86 +(defn- take-front! [av]
    1.87 +  (-> (:buffers av)
    1.88 +      (swap! update-in [:front-readers] inc)
    1.89 +      :front))
    1.90  
    1.91 -(defn- release-buffer [al buffer]
    1.92 -  (dosync
    1.93 -   (let [state (:state buffer)
    1.94 -         readers (if (= state :front)
    1.95 -                   (dec (:readers buffer))
    1.96 -                   (:readers buffer))
    1.97 -         fresh (delay (find-buffer @(:buffers al) :fresh))
    1.98 -         state (cond
    1.99 -                (pos? readers) :front
   1.100 -                (= :back state) :fresh
   1.101 -                @fresh :free
   1.102 -                :default :fresh)]
   1.103 -     (when (and (= state :fresh) @fresh)
   1.104 -       ;; Change state of the previously fresh buffer to :free.
   1.105 -       (alter (:buffers al)
   1.106 -              replace-buffer (assoc @fresh
   1.107 -                               :state :free)))
   1.108 -     (alter (:buffers al)
   1.109 -            replace-buffer (assoc buffer
   1.110 -                             :state state
   1.111 -                             :readers readers)))))
   1.112 +(defn- release-front! [av]
   1.113 +  (when (-> (:buffers av)
   1.114 +            (swap! apply-flip update-in [:front-readers] dec)
   1.115 +            :flipped)
   1.116 +    (update av)))
   1.117  
   1.118 -(defmacro with-buffer
   1.119 -  {:private true}
   1.120 -  [al type [name] & body]
   1.121 -  `(let [al# ~al
   1.122 -         ~name (take-buffer al# ~type)]
   1.123 -     (try
   1.124 -       ~@body
   1.125 -       (finally
   1.126 -        (release-buffer al# ~name)))))
   1.127 +(defn- take-back! [av]
   1.128 +  (-> (:buffers av)
   1.129 +      (swap! assoc :back-ready false)
   1.130 +      :back))
   1.131  
   1.132 -(defn- draw-offscreen [async-view]
   1.133 +(defn- release-back! [av]
   1.134 +  (when (-> (:buffers av)
   1.135 +            (swap! apply-flip assoc :back-ready true)
   1.136 +            :flipped)
   1.137 +    (update av)))
   1.138 +
   1.139 +(defn- draw-offscreen! [async-view]
   1.140    ;;(Thread/sleep 1000)
   1.141 -  (with-buffer async-view :back [b]
   1.142 -    (let [g (.createGraphics ^BufferedImage (:image b))]
   1.143 -      ;; Clear the buffer.
   1.144 -      (.setComposite g AlphaComposite/Clear)
   1.145 -      (.fillRect g 0 0 (:width async-view) (:height async-view))
   1.146 -      (.setComposite g AlphaComposite/Src)
   1.147 -      (draw-scene! (:scene async-view)
   1.148 -                   g
   1.149 -                   (:width async-view)
   1.150 -                   (:height async-view))))
   1.151 -  (update async-view))
   1.152 +  (let [b (take-back! async-view)
   1.153 +        g (.createGraphics ^BufferedImage b)]
   1.154 +    ;; Clear the buffer.
   1.155 +    (.setComposite g AlphaComposite/Clear)
   1.156 +    (.fillRect g 0 0 (:width async-view) (:height async-view))
   1.157 +    (.setComposite g AlphaComposite/Src)
   1.158 +    (draw-scene! (:scene async-view)
   1.159 +                 g
   1.160 +                 (:width async-view)
   1.161 +                 (:height async-view))
   1.162 +    ;; Will not be called if an exception is thrown in the code above.
   1.163 +    (release-back! async-view)))
   1.164  
   1.165 -(defn- draw-offscreen-async [async-view]
   1.166 -  (.execute ^ThreadPoolExecutor (:executor async-view)
   1.167 -            (bound-fn* #(draw-offscreen async-view))))
   1.168 +(defn- pool-execute! [async-view f]
   1.169 +  (.execute ^ThreadPoolExecutor (:executor async-view) f))
   1.170 +
   1.171 +(defn- ensure-buffers! [async-view device-conf]
   1.172 +  (let [buffers (:buffers async-view)]
   1.173 +    (when (and (not @buffers)
   1.174 +               (:new (swap! buffers
   1.175 +                            #(if %
   1.176 +                               (assoc % :new false)
   1.177 +                               (create-buffers async-view device-conf)))))
   1.178 +      (let [bound-draw! (bound-fn* draw-offscreen!)
   1.179 +            async-draw! #(pool-execute! % (partial bound-draw! %))]
   1.180 +        (add-observer async-view
   1.181 +                      (:scene async-view)
   1.182 +                      ;; Must not hold onto async-view.
   1.183 +                      (fn [a _] (async-draw! a)))
   1.184 +        (async-draw! async-view)))))
   1.185  
   1.186  (defrecord AsyncView [scene width height executor buffers]
   1.187    View
   1.188    (render! [view]
   1.189 -    (repaint-on-update view)
   1.190 -    (add-context-observer scene (bound-fn [_ _]
   1.191 -                                  (draw-offscreen-async view)))
   1.192 -    (when-not @buffers
   1.193 -      ;; TODO: dynamic size, recreate buffers when size increases.
   1.194 -      (let [device-conf (.getDeviceConfiguration *graphics*)
   1.195 -            new-buffers (repeatedly 2
   1.196 -                          (partial create-buffer view device-conf))]
   1.197 -        (dosync
   1.198 -         (ref-set buffers new-buffers)))
   1.199 -      (draw-offscreen-async view))
   1.200 -    (with-buffer view :front [b]
   1.201 -      (.drawImage *graphics* ^Image (:image b) 0 0 nil)))
   1.202 +    (let [g *graphics*]
   1.203 +      (ensure-buffers! view (.getDeviceConfiguration g))
   1.204 +      (let [^Image b (take-front! view)]
   1.205 +        (try
   1.206 +          (.drawImage g b 0 0 nil)
   1.207 +          (finally
   1.208 +            (release-front! view))))
   1.209 +      (repaint-on-update view)))
   1.210    (geometry [view]
   1.211      (->Size width height)))
   1.212  
   1.213 @@ -167,13 +161,13 @@
   1.214  
   1.215  (defn async-view 
   1.216    "Creates a View that draws the content asynchronously using an
   1.217 -   offscreen buffer."
   1.218 +  offscreen buffer."
   1.219    ([width height content]
   1.220 -     (async-view width height nil content))
   1.221 +   (async-view width height nil content))
   1.222    ([width height priority content]
   1.223 -     ;; TODO: use operational event dispatcher.
   1.224 -     (->AsyncView (make-scene content)
   1.225 -                  width
   1.226 -                  height
   1.227 -                  (create-executor priority)
   1.228 -                  (ref nil))))
   1.229 +   ;; TODO: use operational event dispatcher.
   1.230 +   (->AsyncView (make-scene content)
   1.231 +                width
   1.232 +                height
   1.233 +                (create-executor priority)
   1.234 +                (atom nil))))