Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Use effective tpf value in LoopRunner (onUpdate) #1298

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix: Use effective tpf value in LoopRunner (onUpdate)
  • Loading branch information
Jean-René Lavoie committed Oct 10, 2023
commit 907514f2d631cd6335a70eff0a5ba56395ff310f
34 changes: 12 additions & 22 deletions fxgl/src/main/kotlin/com/almasb/fxgl/app/LoopRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ internal class LoopRunner(
var cpuNanoTime = 0L
private set

private var lastFPSUpdateNanos = 0L
private var fpsBuffer2sec = 0
private var lastFrameNanos = 0L

private val impl by lazy {
if (ticksPerSecond <= 0) {
Expand Down Expand Up @@ -83,8 +82,6 @@ internal class LoopRunner(
log.debug("Pausing loop")

impl.pause()

lastFPSUpdateNanos = 0L
}

fun stop() {
Expand All @@ -94,29 +91,22 @@ internal class LoopRunner(
}

private fun frame(now: Long) {
if (lastFPSUpdateNanos == 0L) {
lastFPSUpdateNanos = now
fpsBuffer2sec = 0
}

cpuNanoTime = measureNanoTime {
runnable(tpf)
if (lastFrameNanos == 0L) {
lastFrameNanos = now
}

fpsBuffer2sec++
tpf = (now - lastFrameNanos).toDouble() / 1_000_000_000

// if 2 seconds have passed
if (now - lastFPSUpdateNanos >= 2_000_000_000) {
lastFPSUpdateNanos = now
fps = fpsBuffer2sec / 2
fpsBuffer2sec = 0
// The "executor" will call 60 times per seconds even if the game runs under 60 fps.
// If it's not even "half" a tick long, skip
if(tpf < (1_000_000_000 / (ticksPerSecond * 1.5)) / 1_000_000_000 ) {
return
}

// tweak potentially erroneous reads
if (fps < 5)
fps = 60
lastFrameNanos = now

// update tpf for the next 2 seconds
tpf = 1.0 / fps
cpuNanoTime = measureNanoTime {
runnable(tpf)
}
}
}
Expand Down
49 changes: 49 additions & 0 deletions fxgl/src/test/kotlin/com/almasb/fxgl/app/LoopRunnerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,53 @@ class LoopRunnerTest {
assertThat(count2, greaterThan(0.0))
assertThat(count2, lessThan(0.75))
}

@Test
@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
fun `Lag Recovery`() {
var t = 0.0
var lag = 250L

listOf(
// run with a given ticks per second (via scheduled service tick)
LoopRunner(60) { t += it; Thread.sleep(lag) },

// run with display refresh rate (via JavaFX pulse tick)
LoopRunner { t += it; Thread.sleep(lag) }
).forEach { loop ->
t = 0.0

loop.start()

Thread.sleep(2500) // Sample for more than 2 seconds, to cover the 2SecsBuffer case

loop.pause()

// We know that a single tick will take at least "lag" millis, so TPFs should be around 200 millis
assertThat(loop.tpf, closeTo(lag.toDouble() / 1000.0, 0.02))

// The game loop should have completed 2.5 seconds of game time at this stage
assertThat(t, closeTo(2.5, 0.2))

lag = 1L // Stop Lag

loop.resume()

Thread.sleep(1000)

loop.stop()

// The 2 seconds Buffer shouldn't cause tpf to be 200 millis anymore
assertThat(loop.tpf, closeTo(0.016, 0.09))

assertThat(t, closeTo(3.5, 0.4))

// shouldn't change anything since loop is stopped
Thread.sleep(300)

assertThat(loop.tpf, closeTo(0.016, 0.09))

assertThat(t, closeTo(3.5, 0.4))
}
}
}