Added support for window/canvas page flipping. Code cleanup/org.

This commit is contained in:
Robinson 2023-12-18 14:16:05 +01:00
parent 0723fc432c
commit 6851b1483b
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
2 changed files with 75 additions and 72 deletions

View File

@ -15,9 +15,11 @@
*/ */
package dorkbox.swingActiveRender package dorkbox.swingActiveRender
import dorkbox.propertyLoader.Property import dorkbox.swingActiveRender.SwingActiveRender.TARGET_FPS
import java.awt.Component
import java.awt.Graphics import java.awt.Graphics
import java.awt.Toolkit import java.awt.Toolkit
import java.util.concurrent.*
import java.util.concurrent.locks.* import java.util.concurrent.locks.*
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
@ -33,22 +35,12 @@ class ActiveRenderLoop : Runnable {
private val condition = lock.newCondition() private val condition = lock.newCondition()
private val runTimeCondition = lock.newCondition() private val runTimeCondition = lock.newCondition()
companion object {
/** // volatile, so that access triggers thread synchrony
* How many frames per second we want the Swing ActiveRender thread to run at @Volatile
* private var hasActiveRenders = false
* ### NOTE: private val activeRenders: MutableList<Component> = CopyOnWriteArrayList()
*
* The ActiveRenderLoop replaces the Swing EDT (only for specified JFrames) in order to enable smoother animations. It is also
* important to REMEMBER -- if you add a component to an actively managed JFrame, YOU MUST make sure to call
* [javax.swing.JComponent.setIgnoreRepaint] otherwise this component will "fight" on the EDT for updates.
*
* You can ALSO completely disable the Swing EDT by calling [NullRepaintManager.install]
*/
@Property(description = "How many frames per second the Swing ActiveRender thread will run at.")
@Volatile
var TARGET_FPS = 30
}
override fun run() { override fun run() {
currentThread = Thread.currentThread() currentThread = Thread.currentThread()
@ -66,23 +58,23 @@ class ActiveRenderLoop : Runnable {
val OPTIMAL_TIME = (1000000000 / TARGET_FPS).toLong() val OPTIMAL_TIME = (1000000000 / TARGET_FPS).toLong()
var graphics: Graphics? = null var graphics: Graphics? = null
// is a copy-on-write, and it must eventually be correct.
val renderEvents = SwingActiveRender.activeRenderEvents
while (true) { while (true) {
// if we have NO active renderers, just wait for one. // if we have NO active renderers, just wait for one.
while (SwingActiveRender.hasActiveRenders) { while (hasActiveRenders) {
try { try {
val now = System.nanoTime() val now = System.nanoTime()
val updateDeltaNanos = now - lastTime val updateDeltaNanos = now - lastTime
lastTime = now lastTime = now
// is a copy-on-write, and it must eventually be correct. for (event in renderEvents) {
val renderEvents = SwingActiveRender.activeRenderEvents event.invoke(updateDeltaNanos)
renderEvents.forEach {
it.invoke(updateDeltaNanos)
} }
// this needs to be synchronized because we don't want to our canvas removed WHILE we are rendering it. val activeRenders = this.activeRenders
val activeRenders = SwingActiveRender.activeRenders
for (component in activeRenders) { for (component in activeRenders) {
if (!component.isDisplayable) { if (!component.isDisplayable) {
continue continue
@ -111,7 +103,7 @@ class ActiveRenderLoop : Runnable {
// see: http://www.cs.nuim.ie/~jpower/Research/Papers/2008/lambert-qapl08.pdf // 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 // Also, down-casting (long -> int) is not expensive w.r.t IDIV/LDIV
val l = (lastTime - System.nanoTime() + OPTIMAL_TIME).toInt() val l = (lastTime - System.nanoTime() + OPTIMAL_TIME).toInt()
val millis = l / 1000000 val millis = l / 1_000_000
if (millis > 1) { if (millis > 1) {
Thread.sleep(millis.toLong()) Thread.sleep(millis.toLong())
@ -133,7 +125,18 @@ class ActiveRenderLoop : Runnable {
} }
} }
fun signalStart() { fun maybeShutdown(component: Component): Boolean {
activeRenders.remove(component)
val hasActiveRenders = activeRenders.isNotEmpty()
this.hasActiveRenders = hasActiveRenders
return hasActiveRenders
}
fun signalStart(component: Component) {
hasActiveRenders = true
activeRenders.add(component)
lock.withLock { lock.withLock {
runTimeCondition.signal() runTimeCondition.signal()
} }

View File

@ -15,10 +15,12 @@
*/ */
package dorkbox.swingActiveRender package dorkbox.swingActiveRender
import dorkbox.propertyLoader.Property
import dorkbox.updates.Updates.add import dorkbox.updates.Updates.add
import java.awt.Canvas
import java.awt.Component import java.awt.Component
import java.awt.Window
import java.util.concurrent.* import java.util.concurrent.*
import java.util.concurrent.atomic.*
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
/** /**
@ -32,26 +34,41 @@ import javax.swing.SwingUtilities
*/ */
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
object SwingActiveRender { object SwingActiveRender {
/**
* How many frames per second we want the Swing ActiveRender thread to run at
*
* ### NOTE:
*
* The ActiveRenderLoop replaces the Swing EDT (only for specified JFrames) in order to enable smoother animations. It is also
* important to REMEMBER -- if you add a component to an actively managed JFrame, YOU MUST make sure to call
* [javax.swing.JComponent.setIgnoreRepaint] otherwise this component will "fight" on the EDT for updates.
*
* You can ALSO completely disable the Swing EDT by calling [NullRepaintManager.install]
*/
@Property(description = "How many frames per second the Swing Active Render thread will run at.")
@Volatile @Volatile
private var activeRenderThread: Thread? = null var TARGET_FPS = 30
private val activeRenderThread: Thread
internal val activeRenders: MutableList<Component> = CopyOnWriteArrayList()
internal val activeRenderEvents: MutableList<(deltaInNanos: Long)->Unit> = CopyOnWriteArrayList() internal val activeRenderEvents: MutableList<(deltaInNanos: Long)->Unit> = CopyOnWriteArrayList()
// volatile, so that access triggers thread synchrony
@Volatile
internal var hasActiveRenders = false
private val renderLoop = ActiveRenderLoop() private val renderLoop = ActiveRenderLoop()
/** /**
* Gets the version number. * Gets the version number.
*/ */
const val version = "1.4" const val version = "1.5"
init { init {
// Add this project to the updates system, which verifies this class + UUID + version information // Add this project to the updates system, which verifies this class + UUID + version information
add(SwingActiveRender::class.java, "0dfec3d996f3420d82a864c6cd5a2646", version) add(SwingActiveRender::class.java, "0dfec3d996f3420d82a864c6cd5a2646", version)
// this will pause and wait for a signal. Since this class is used, this means that we should create the threads
activeRenderThread = Thread(renderLoop, "Swing-ActiveRender")
activeRenderThread.isDaemon = true
activeRenderThread.start()
} }
/** /**
@ -65,22 +82,24 @@ object SwingActiveRender {
* @param component the component to add to the ActiveRender thread. * @param component the component to add to the ActiveRender thread.
*/ */
fun add(component: Component) { fun add(component: Component) {
SwingUtilities.invokeLater { if (SwingUtilities.isEventDispatchThread()) {
// this part has to be on the swing EDT // this part has to be on the swing EDT
component.ignoreRepaint = true component.ignoreRepaint = true
} } else {
SwingUtilities.invokeAndWait {
// this part has to be on the swing EDT
synchronized(renderLoop) { component.ignoreRepaint = true
if (!hasActiveRenders) {
hasActiveRenders = true
setupActiveRenderThread()
} }
activeRenders.add(component)
} }
renderLoop.signalStart() if (component is Window) {
component.createBufferStrategy(2)
} else if (component is Canvas) {
component.createBufferStrategy(2)
}
setupActiveRenderThread()
renderLoop.signalStart(component)
} }
/** /**
@ -89,22 +108,17 @@ object SwingActiveRender {
* @param component the component to remove * @param component the component to remove
*/ */
fun remove(component: Component) { fun remove(component: Component) {
SwingUtilities.invokeLater { if (SwingUtilities.isEventDispatchThread()) {
// this part has to be on the swing EDT // this part has to be on the swing EDT
component.ignoreRepaint = false component.ignoreRepaint = false
} } else {
SwingUtilities.invokeAndWait {
// this part has to be on the swing EDT
synchronized(renderLoop) { component.ignoreRepaint = false
activeRenders.remove(component)
val hasActiveRenders = activeRenders.isNotEmpty()
this.hasActiveRenders = hasActiveRenders
if (!hasActiveRenders) {
// our thread loop will kill itself
activeRenderThread = null
} }
} }
renderLoop.maybeShutdown(component)
} }
/** /**
@ -138,13 +152,7 @@ object SwingActiveRender {
fun isDispatchThread(): Boolean { fun isDispatchThread(): Boolean {
// make sure we are initialized! // make sure we are initialized!
if (activeRenderThread == null) { setupActiveRenderThread()
synchronized(renderLoop) {
if (activeRenderThread == null) {
setupActiveRenderThread()
}
}
}
return Thread.currentThread() == renderLoop.currentThread return Thread.currentThread() == renderLoop.currentThread
} }
@ -153,14 +161,6 @@ object SwingActiveRender {
* Creates (if necessary) the active-render thread. When there are no active-render targets, this thread will exit * Creates (if necessary) the active-render thread. When there are no active-render targets, this thread will exit
*/ */
private fun setupActiveRenderThread() { private fun setupActiveRenderThread() {
if (activeRenderThread != null) {
return
}
activeRenderThread = Thread(renderLoop, "Swing-ActiveRender")
activeRenderThread!!.isDaemon = true
activeRenderThread!!.start()
renderLoop.waitForStartup() renderLoop.waitForStartup()
} }
} }