diff --git a/Dorkbox-Util/src/dorkbox/util/swing/ActiveRenderLoop.java b/Dorkbox-Util/src/dorkbox/util/swing/ActiveRenderLoop.java new file mode 100644 index 0000000..dd384e5 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/swing/ActiveRenderLoop.java @@ -0,0 +1,88 @@ +package dorkbox.util.swing; + +import dorkbox.util.ActionHandlerLong; +import dorkbox.util.SystemProps; + +import javax.swing.JFrame; +import java.awt.Graphics; +import java.awt.Toolkit; +import java.awt.image.BufferStrategy; + +/** + * Loop that controls the active rendering process + */ +class ActiveRenderLoop implements Runnable { + @SuppressWarnings("WhileLoopReplaceableByForEach") + @Override + public + void run() { + long lastTime = System.nanoTime(); + + // 30 FPS is usually just fine. This isn't a game where we need 60+ FPS. We permit this to be changed though, just in case it is. + final int TARGET_FPS = SystemProps.ActiveRenderTargetFPS; + final long OPTIMAL_TIME = 1000000000 / TARGET_FPS; + Graphics graphics = null; + + while (SwingActiveRender.hasActiveRenders) { + long now = System.nanoTime(); + long updateDeltaNanos = now - lastTime; + lastTime = now; + + // not synchronized, because we don't care. The worst case, is one frame of animation behind. + for (int i = 0; i < SwingActiveRender.activeRenderEvents.size(); i++) { + ActionHandlerLong actionHandlerLong = SwingActiveRender.activeRenderEvents.get(i); + + //noinspection unchecked + actionHandlerLong.handle(updateDeltaNanos); + } + + for (int i = 0; i < SwingActiveRender.activeRenders.size(); i++) { + JFrame jFrame = SwingActiveRender.activeRenders.get(i); + + final BufferStrategy buffer = jFrame.getBufferStrategy(); + + // maybe the frame was closed + if (buffer != null) { + try { + graphics = buffer.getDrawGraphics(); + jFrame.paint(graphics); + } catch (IllegalStateException ignored) { + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (graphics != null) { + graphics.dispose(); + + // blit the back buffer to the screen + if (!buffer.contentsLost()) { + buffer.show(); + } + } + } + } + } + + // Sync the display on some systems (on Linux, this fixes event queue problems) + Toolkit.getDefaultToolkit() + .sync(); + + try { + // Converted to int before the division, because IDIV is + // 1 order magnitude faster than LDIV (and int's work for us anyways) + // see: http://www.cs.nuim.ie/~jpower/Research/Papers/2008/lambert-qapl08.pdf + // Also, down-casting (long -> int) is not expensive w.r.t IDIV/LDIV + //noinspection NumericCastThatLosesPrecision + final int l = (int) (lastTime - System.nanoTime() + OPTIMAL_TIME); + final int millis = l / 1000000; + if (millis > 1) { + Thread.sleep(millis); + } + else { + // try to keep the CPU from getting slammed. We couldn't match our target FPS, so loop again + Thread.yield(); + } + } catch (InterruptedException ignored) { + } + } + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/swing/NullRepaintManager.java b/Dorkbox-Util/src/dorkbox/util/swing/NullRepaintManager.java new file mode 100644 index 0000000..d84964c --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/swing/NullRepaintManager.java @@ -0,0 +1,48 @@ +package dorkbox.util.swing; + +import javax.swing.JComponent; +import javax.swing.RepaintManager; +import javax.swing.SwingUtilities; + +/** + * The NullRepaintManager is a RepaintManager that doesn't do any repainting. Useful when all of the rendering is done manually by the + * application. + */ +public +class NullRepaintManager extends RepaintManager { + /** + * Installs the NullRepaintManager onto the EDT (WARNING: This disables painting/rendering by the EDT, for the entire JVM) + */ + public static + void install() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public + void run() { + RepaintManager repaintManager = new NullRepaintManager(); + repaintManager.setDoubleBufferingEnabled(false); + RepaintManager.setCurrentManager(repaintManager); + } + }); + } + + public + void addInvalidComponent(JComponent c) { + // do nothing + } + + public + void addDirtyRegion(JComponent c, int x, int y, int w, int h) { + // do nothing + } + + public + void markCompletelyDirty(JComponent c) { + // do nothing + } + + public + void paintDirtyRegions() { + // do nothing + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/swing/SwingActiveRender.java b/Dorkbox-Util/src/dorkbox/util/swing/SwingActiveRender.java new file mode 100644 index 0000000..50875ec --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/swing/SwingActiveRender.java @@ -0,0 +1,161 @@ +package dorkbox.util.swing; + +import dorkbox.util.ActionHandlerLong; + +import javax.swing.JComponent; +import javax.swing.JFrame; +import java.awt.Component; +import java.awt.EventQueue; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Contains all of the appropriate logic to setup and render via "Active" rendering (instead of "Passive" rendering). This permits us to + * render JFrames (and their contents), OFF of the EDT - even though there are other frames/components that are ON the EDT.
Because we + * still want to react to mouse events, etc on the EDT, we do not completely remove the EDT -- we merely allow us to "synchronize" the EDT + * object to our thread. It's a little bit hacky, but it works beautifully, and permits MUCH nicer animations.
+ *

+ * It is also important to REMEMBER -- if you add a component to an actively managed JFrame, YOU MUST make sure to call {@link + * JComponent#setIgnoreRepaint(boolean)} otherwise this component will "fight" on the EDT for updates. + */ +public final +class SwingActiveRender { + private static Thread activeRenderThread = null; + + static final List activeRenders = new CopyOnWriteArrayList(); + static final List activeRenderEvents = new CopyOnWriteArrayList(); + + // volatile, so that access triggers thread synchrony, since 1.6. See the Java Language Spec, Chapter 17 + static volatile boolean hasActiveRenders = false; + + private static final Runnable renderLoop = new ActiveRenderLoop(); + + private + SwingActiveRender() { + } + + + /** + * Enables the jFrame to to added to an "Active Render" thread, at a target "Frames-per-second". This is to support smooth, swing-based + * animations.
This works by removing this object from EDT updates, and instead manually calls paint(g) on the jFrame, updating it + * on our own thread. + * + * @param jFrame + * the jFrame to add to the ActiveRender thread. + */ + public static + void addActiveRender(final JFrame jFrame) { + // this should be on the EDT + if (!EventQueue.isDispatchThread()) { + throw new RuntimeException("adding a swing JFrame to be actively rendered, must be done on the EDT."); + } + + // setup double-buffering, so we can properly use Active-Rendering, so the animations will be smooth + jFrame.createBufferStrategy(2); + + // have to specify ALL children in jFrame to ignore EDT paint requests + Deque components = new ArrayDeque(); + components.add(jFrame); + + Component[] c; + Component pop; + while ((pop = components.poll()) != null) { + pop.setIgnoreRepaint(true); + + if (pop instanceof JComponent) { + c = ((JComponent) pop).getComponents(); + Collections.addAll(components, c); + } + } + + synchronized (activeRenders) { + if (!hasActiveRenders) { + setupActiveRenderThread(); + } + + hasActiveRenders = true; + activeRenders.add(jFrame); + } + } + + /** + * Specifies an ActionHandler to be called when the ActiveRender thread starts to render at each tick. + * + * @param handler + * the handler to add + */ + public static + void addActiveRenderFrameStart(final ActionHandlerLong handler) { + synchronized (activeRenders) { + activeRenderEvents.add(handler); + } + } + + /** + * Potentially SLOW calculation, as it compares each entry in a queue for equality + * + * @param handler + * this is the handler to check + * + * @return true if this handler already exists in the active render, on-frame-start queue + */ + public static + boolean containsActiveRenderFrameStart(final ActionHandlerLong handler) { + synchronized (activeRenders) { + return activeRenderEvents.contains(handler); + } + } + + /** + * Removes the handler from the on-frame-start queue + * + * @param handler + * the handler to remove + */ + public static + void removeActiveRenderFrameStart(final ActionHandlerLong handler) { + synchronized (activeRenders) { + activeRenderEvents.remove(handler); + } + } + + + /** + * Removes a jFrame from the ActiveRender queue. This should happen when the jFrame is closed. + * + * @param jFrame + * the jFrame to remove + */ + public static + void removeActiveRender(final JFrame jFrame) { + synchronized (activeRenders) { + activeRenders.remove(jFrame); + + final boolean hadActiveRenders = !activeRenders.isEmpty(); + hasActiveRenders = hadActiveRenders; + + if (!hadActiveRenders) { + activeRenderThread = null; + } + } + } + + /** + * Creates (if necessary) the active-render thread. When there are no active-render targets, this thread will exit + */ + private static + void setupActiveRenderThread() { + if (activeRenderThread != null) { + return; + } + + SynchronizedEventQueue.install(); + + activeRenderThread = new Thread(renderLoop, "AWT-ActiveRender"); + activeRenderThread.setDaemon(true); + activeRenderThread.start(); + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/swing/SynchronizedEventQueue.java b/Dorkbox-Util/src/dorkbox/util/swing/SynchronizedEventQueue.java new file mode 100644 index 0000000..901c2f9 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/swing/SynchronizedEventQueue.java @@ -0,0 +1,42 @@ +package dorkbox.util.swing; + +import java.awt.AWTEvent; +import java.awt.EventQueue; +import java.awt.Toolkit; + +final +class SynchronizedEventQueue extends EventQueue { + public static final Object MUTEX = new Object(); + + private static final SynchronizedEventQueue instance = new SynchronizedEventQueue(); + private static boolean alreadyInUse = false; + + public static synchronized + void install() { + if (!alreadyInUse) { + // set up the synchronized event queue + EventQueue eventQueue = Toolkit.getDefaultToolkit() + .getSystemEventQueue(); + eventQueue.push(instance); + alreadyInUse = true; + } + } + + /** + * Enforce singleton property. + */ + private + SynchronizedEventQueue() { + } + + protected + void dispatchEvent(AWTEvent aEvent) { + synchronized (MUTEX) { + try { + super.dispatchEvent(aEvent); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +}