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, 99 insertions(+), 105 deletions(-) [+]
line wrap: on
line diff
--- a/src/indyvon/async.clj	Wed Dec 10 19:00:17 2014 +0300
+++ b/src/indyvon/async.clj	Wed Dec 10 20:01:45 2014 +0300
@@ -29,16 +29,19 @@
                          ThreadPoolExecutor$DiscardOldestPolicy
                          ArrayBlockingQueue TimeUnit)))
 
-(defrecord Buffer [id image readers state])
-;; Buffer states:
-;;   :front, readers > 0
-;;      being copied on screen
-;;   :back
-;;      being rendered to (offscreen)
-;;   :fresh
-;;      most recently updated
-;;   :free
-;;      not in use
+(defrecord Buffers [;; BufferedImage for copying on screen.
+                    front
+                    ;; Number of processes using the front buffer.
+                    front-readers 
+                    ;; BufferedImage used for asynchronous drawing.
+                    back
+                    ;; True after drawing to the back buffer is finished.
+                    back-ready
+                    ;; True after the buffers were flipped,
+                    ;; indicates that AsyncView needs to be redrawn.
+                    flipped
+                    ;; Used to synchronize initialization of the buffers.
+                    new])
 
 (defn- create-image [async-view ^GraphicsConfiguration device-conf]
   ;; TODO: support different image types.
@@ -47,103 +50,94 @@
                           (:height async-view)
                           Transparency/TRANSLUCENT))
 
-(defn- create-buffer [async-view device-conf]
-  (->Buffer (Object.) (create-image async-view device-conf) 0 :free))
+(defn- create-buffers [async-view device-conf]
+  (->Buffers (create-image async-view device-conf)
+             0
+             (create-image async-view device-conf)
+             false
+             false
+             true))
 
-(defn- find-buffer
-  "Find a buffer with the one of the specified states given
-   in the order of preference."
-  [buffers & states]
-  (some identity
-        (for [state states]
-          (some #(if (= (:state %) state) % nil) buffers))))
-
-(defn- replace-buffer [buffers buffer]
-  (conj (remove #(= (:id %) (:id buffer)) buffers)
-        buffer))
+(defn- maybe-flip [buffers]
+  (if (and (:back-ready buffers)
+           (zero? (:front-readers buffers)))
+    (assoc buffers
+      :front (:back buffers)
+      :back (:front buffers)
+      :back-ready false
+      :flipped true)
+    (assoc buffers
+      :flipped false)))
 
-(defn- take-buffer [al type]
-  (dosync
-   (let [buffers @(:buffers al)
-         b (case type
-             :front (find-buffer buffers :front :fresh :free)
-             :back (find-buffer buffers :free :fresh)
-             (throw (IllegalArgumentException.)))
-         readers (if (= type :front)
-                   (inc (:readers b))
-                   (:readers b))
-         b (assoc b
-             :state type
-             :readers readers)]
-     (alter (:buffers al) replace-buffer b)
-     b)))
+(defn- apply-flip [buffers f & args]
+  (maybe-flip (apply f buffers args)))
+
+(defn- take-front! [av]
+  (-> (:buffers av)
+      (swap! update-in [:front-readers] inc)
+      :front))
+
+(defn- release-front! [av]
+  (when (-> (:buffers av)
+            (swap! apply-flip update-in [:front-readers] dec)
+            :flipped)
+    (update av)))
+
+(defn- take-back! [av]
+  (-> (:buffers av)
+      (swap! assoc :back-ready false)
+      :back))
 
-(defn- release-buffer [al buffer]
-  (dosync
-   (let [state (:state buffer)
-         readers (if (= state :front)
-                   (dec (:readers buffer))
-                   (:readers buffer))
-         fresh (delay (find-buffer @(:buffers al) :fresh))
-         state (cond
-                (pos? readers) :front
-                (= :back state) :fresh
-                @fresh :free
-                :default :fresh)]
-     (when (and (= state :fresh) @fresh)
-       ;; Change state of the previously fresh buffer to :free.
-       (alter (:buffers al)
-              replace-buffer (assoc @fresh
-                               :state :free)))
-     (alter (:buffers al)
-            replace-buffer (assoc buffer
-                             :state state
-                             :readers readers)))))
+(defn- release-back! [av]
+  (when (-> (:buffers av)
+            (swap! apply-flip assoc :back-ready true)
+            :flipped)
+    (update av)))
 
-(defmacro with-buffer
-  {:private true}
-  [al type [name] & body]
-  `(let [al# ~al
-         ~name (take-buffer al# ~type)]
-     (try
-       ~@body
-       (finally
-        (release-buffer al# ~name)))))
-
-(defn- draw-offscreen [async-view]
+(defn- draw-offscreen! [async-view]
   ;;(Thread/sleep 1000)
-  (with-buffer async-view :back [b]
-    (let [g (.createGraphics ^BufferedImage (:image b))]
-      ;; Clear the buffer.
-      (.setComposite g AlphaComposite/Clear)
-      (.fillRect g 0 0 (:width async-view) (:height async-view))
-      (.setComposite g AlphaComposite/Src)
-      (draw-scene! (:scene async-view)
-                   g
-                   (:width async-view)
-                   (:height async-view))))
-  (update async-view))
+  (let [b (take-back! async-view)
+        g (.createGraphics ^BufferedImage b)]
+    ;; Clear the buffer.
+    (.setComposite g AlphaComposite/Clear)
+    (.fillRect g 0 0 (:width async-view) (:height async-view))
+    (.setComposite g AlphaComposite/Src)
+    (draw-scene! (:scene async-view)
+                 g
+                 (:width async-view)
+                 (:height async-view))
+    ;; Will not be called if an exception is thrown in the code above.
+    (release-back! async-view)))
 
-(defn- draw-offscreen-async [async-view]
-  (.execute ^ThreadPoolExecutor (:executor async-view)
-            (bound-fn* #(draw-offscreen async-view))))
+(defn- pool-execute! [async-view f]
+  (.execute ^ThreadPoolExecutor (:executor async-view) f))
+
+(defn- ensure-buffers! [async-view device-conf]
+  (let [buffers (:buffers async-view)]
+    (when (and (not @buffers)
+               (:new (swap! buffers
+                            #(if %
+                               (assoc % :new false)
+                               (create-buffers async-view device-conf)))))
+      (let [bound-draw! (bound-fn* draw-offscreen!)
+            async-draw! #(pool-execute! % (partial bound-draw! %))]
+        (add-observer async-view
+                      (:scene async-view)
+                      ;; Must not hold onto async-view.
+                      (fn [a _] (async-draw! a)))
+        (async-draw! async-view)))))
 
 (defrecord AsyncView [scene width height executor buffers]
   View
   (render! [view]
-    (repaint-on-update view)
-    (add-context-observer scene (bound-fn [_ _]
-                                  (draw-offscreen-async view)))
-    (when-not @buffers
-      ;; TODO: dynamic size, recreate buffers when size increases.
-      (let [device-conf (.getDeviceConfiguration *graphics*)
-            new-buffers (repeatedly 2
-                          (partial create-buffer view device-conf))]
-        (dosync
-         (ref-set buffers new-buffers)))
-      (draw-offscreen-async view))
-    (with-buffer view :front [b]
-      (.drawImage *graphics* ^Image (:image b) 0 0 nil)))
+    (let [g *graphics*]
+      (ensure-buffers! view (.getDeviceConfiguration g))
+      (let [^Image b (take-front! view)]
+        (try
+          (.drawImage g b 0 0 nil)
+          (finally
+            (release-front! view))))
+      (repaint-on-update view)))
   (geometry [view]
     (->Size width height)))
 
@@ -167,13 +161,13 @@
 
 (defn async-view 
   "Creates a View that draws the content asynchronously using an
-   offscreen buffer."
+  offscreen buffer."
   ([width height content]
-     (async-view width height nil content))
+   (async-view width height nil content))
   ([width height priority content]
-     ;; TODO: use operational event dispatcher.
-     (->AsyncView (make-scene content)
-                  width
-                  height
-                  (create-executor priority)
-                  (ref nil))))
+   ;; TODO: use operational event dispatcher.
+   (->AsyncView (make-scene content)
+                width
+                height
+                (create-executor priority)
+                (atom nil))))