Added Swing EDT replacement thread (via active rendering + some tricks) for smooth 30fps animations in swing
This commit is contained in:
parent
d860da2c91
commit
5bc9d2e17b
88
Dorkbox-Util/src/dorkbox/util/swing/ActiveRenderLoop.java
Normal file
88
Dorkbox-Util/src/dorkbox/util/swing/ActiveRenderLoop.java
Normal file
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
Dorkbox-Util/src/dorkbox/util/swing/NullRepaintManager.java
Normal file
48
Dorkbox-Util/src/dorkbox/util/swing/NullRepaintManager.java
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
161
Dorkbox-Util/src/dorkbox/util/swing/SwingActiveRender.java
Normal file
161
Dorkbox-Util/src/dorkbox/util/swing/SwingActiveRender.java
Normal file
@ -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. <br> 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. <br>
|
||||||
|
* <p/>
|
||||||
|
* <b>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. </b>
|
||||||
|
*/
|
||||||
|
public final
|
||||||
|
class SwingActiveRender {
|
||||||
|
private static Thread activeRenderThread = null;
|
||||||
|
|
||||||
|
static final List<JFrame> activeRenders = new CopyOnWriteArrayList<JFrame>();
|
||||||
|
static final List<ActionHandlerLong> activeRenderEvents = new CopyOnWriteArrayList<ActionHandlerLong>();
|
||||||
|
|
||||||
|
// 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. <br> 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<Component> components = new ArrayDeque<Component>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user