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

master
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
import dorkbox.propertyLoader.Property
import dorkbox.swingActiveRender.SwingActiveRender.TARGET_FPS
import java.awt.Component
import java.awt.Graphics
import java.awt.Toolkit
import java.util.concurrent.*
import java.util.concurrent.locks.*
import kotlin.concurrent.withLock
@ -33,22 +35,12 @@ class ActiveRenderLoop : Runnable {
private val condition = lock.newCondition()
private val runTimeCondition = lock.newCondition()
companion object {
/**
* 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 ActiveRender thread will run at.")
@Volatile
var TARGET_FPS = 30
}
// volatile, so that access triggers thread synchrony
@Volatile
private var hasActiveRenders = false
private val activeRenders: MutableList<Component> = CopyOnWriteArrayList()
override fun run() {
currentThread = Thread.currentThread()
@ -66,23 +58,23 @@ class ActiveRenderLoop : Runnable {
val OPTIMAL_TIME = (1000000000 / TARGET_FPS).toLong()
var graphics: Graphics? = null
// is a copy-on-write, and it must eventually be correct.
val renderEvents = SwingActiveRender.activeRenderEvents
while (true) {
// if we have NO active renderers, just wait for one.
while (SwingActiveRender.hasActiveRenders) {
while (hasActiveRenders) {
try {
val now = System.nanoTime()
val updateDeltaNanos = now - lastTime
lastTime = now
// is a copy-on-write, and it must eventually be correct.
val renderEvents = SwingActiveRender.activeRenderEvents
renderEvents.forEach {
it.invoke(updateDeltaNanos)
for (event in renderEvents) {
event.invoke(updateDeltaNanos)
}
// this needs to be synchronized because we don't want to our canvas removed WHILE we are rendering it.
val activeRenders = SwingActiveRender.activeRenders
val activeRenders = this.activeRenders
for (component in activeRenders) {
if (!component.isDisplayable) {
continue
@ -111,7 +103,7 @@ class ActiveRenderLoop : Runnable {
// 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
val l = (lastTime - System.nanoTime() + OPTIMAL_TIME).toInt()
val millis = l / 1000000
val millis = l / 1_000_000
if (millis > 1) {
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 {
runTimeCondition.signal()
}

View File

@ -15,10 +15,12 @@
*/
package dorkbox.swingActiveRender
import dorkbox.propertyLoader.Property
import dorkbox.updates.Updates.add
import java.awt.Canvas
import java.awt.Component
import java.awt.Window
import java.util.concurrent.*
import java.util.concurrent.atomic.*
import javax.swing.SwingUtilities
/**
@ -32,26 +34,41 @@ import javax.swing.SwingUtilities
*/
@Suppress("MemberVisibilityCanBePrivate")
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
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()
// volatile, so that access triggers thread synchrony
@Volatile
internal var hasActiveRenders = false
private val renderLoop = ActiveRenderLoop()
/**
* Gets the version number.
*/
const val version = "1.4"
const val version = "1.5"
init {
// Add this project to the updates system, which verifies this class + UUID + version information
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.
*/
fun add(component: Component) {
SwingUtilities.invokeLater {
if (SwingUtilities.isEventDispatchThread()) {
// this part has to be on the swing EDT
component.ignoreRepaint = true
}
synchronized(renderLoop) {
if (!hasActiveRenders) {
hasActiveRenders = true
setupActiveRenderThread()
} else {
SwingUtilities.invokeAndWait {
// this part has to be on the swing EDT
component.ignoreRepaint = true
}
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
*/
fun remove(component: Component) {
SwingUtilities.invokeLater {
if (SwingUtilities.isEventDispatchThread()) {
// this part has to be on the swing EDT
component.ignoreRepaint = false
}
synchronized(renderLoop) {
activeRenders.remove(component)
val hasActiveRenders = activeRenders.isNotEmpty()
this.hasActiveRenders = hasActiveRenders
if (!hasActiveRenders) {
// our thread loop will kill itself
activeRenderThread = null
} else {
SwingUtilities.invokeAndWait {
// this part has to be on the swing EDT
component.ignoreRepaint = false
}
}
renderLoop.maybeShutdown(component)
}
/**
@ -138,13 +152,7 @@ object SwingActiveRender {
fun isDispatchThread(): Boolean {
// make sure we are initialized!
if (activeRenderThread == null) {
synchronized(renderLoop) {
if (activeRenderThread == null) {
setupActiveRenderThread()
}
}
}
setupActiveRenderThread()
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
*/
private fun setupActiveRenderThread() {
if (activeRenderThread != null) {
return
}
activeRenderThread = Thread(renderLoop, "Swing-ActiveRender")
activeRenderThread!!.isDaemon = true
activeRenderThread!!.start()
renderLoop.waitForStartup()
}
}