From 1de2b4c167613ee6b18db0197bab77cd435b1b41 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 18 Sep 2023 00:02:16 +0700 Subject: [PATCH] Better frame scheduling --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 4 +- .../ru/dbotthepony/kstarbound/Starbound.kt | 5 +- .../kstarbound/client/StarboundClient.kt | 341 ++++++++++-------- 3 files changed, 192 insertions(+), 158 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index d365e562..cfa690e7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -242,8 +242,8 @@ fun main() { //client.camera.pos.y = ent.position.y.toFloat() client.camera.pos += Vector2f( - (if (client.input.KEY_LEFT_DOWN || client.input.KEY_A_DOWN) -client.frameRenderTime.toFloat() * 32f / client.settings.zoom else 0f) + (if (client.input.KEY_RIGHT_DOWN || client.input.KEY_D_DOWN) client.frameRenderTime.toFloat() * 32f / client.settings.zoom else 0f), - (if (client.input.KEY_UP_DOWN || client.input.KEY_W_DOWN) client.frameRenderTime.toFloat() * 32f / client.settings.zoom else 0f) + (if (client.input.KEY_DOWN_DOWN || client.input.KEY_S_DOWN) -client.frameRenderTime.toFloat() * 32f / client.settings.zoom else 0f) + (if (client.input.KEY_LEFT_DOWN || client.input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE.toFloat() * 32f / client.settings.zoom else 0f) + (if (client.input.KEY_RIGHT_DOWN || client.input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE.toFloat() * 32f / client.settings.zoom else 0f), + (if (client.input.KEY_UP_DOWN || client.input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE.toFloat() * 32f / client.settings.zoom else 0f) + (if (client.input.KEY_DOWN_DOWN || client.input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE.toFloat() * 32f / client.settings.zoom else 0f) ) //println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 81e55b1c..e062b401 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -85,6 +85,8 @@ import ru.dbotthepony.kstarbound.util.set import ru.dbotthepony.kstarbound.util.traverseJsonPath import java.io.* import java.text.DateFormat +import java.time.Duration +import java.time.temporal.ChronoUnit import java.util.function.BiConsumer import java.util.function.BinaryOperator import java.util.function.Function @@ -95,7 +97,8 @@ import kotlin.collections.ArrayList import kotlin.random.Random object Starbound : ISBFileLocator { - const val TICK_TIME_ADVANCE = 1.0 / 60.0 + const val TICK_TIME_ADVANCE = 0.01666666666666664 + const val TICK_TIME_ADVANCE_NANOS = 16_666_666L // currently it saves only 4 megabytes of ram on pretty big modpack // Hrm. diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 67ae6f11..c35469c8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -49,7 +49,6 @@ import ru.dbotthepony.kstarbound.client.world.ClientWorld import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity -import ru.dbotthepony.kstarbound.util.JVMTimeSource import ru.dbotthepony.kstarbound.util.formatBytesShort import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.api.ICellAccess @@ -742,26 +741,41 @@ class StarboundClient : Closeable { blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA } - var frameRenderTime = 0.0 + // nanoseconds + var frameRenderTime = 0L private set - private var lastRender = JVMTimeSource.INSTANCE.seconds - val framesPerSecond get() = if (frameRenderTime == 0.0) 1.0 else 1.0 / frameRenderTime - - private val frameRenderTimes = DoubleArray(60) { 1.0 } + private var nextRender = System.nanoTime() + private val frameRenderTimes = LongArray(60) { 1L } private var frameRenderIndex = 0 + private val renderWaitTimes = LongArray(60) { 1L } + private var renderWaitIndex = 0 + private var lastRender = System.nanoTime() - val averageFramesPerSecond: Double get() { + val averageRenderWait: Double get() { var sum = 0.0 - for (value in frameRenderTimes) { + for (value in renderWaitTimes) sum += value - } if (sum == 0.0) return 0.0 - return frameRenderTimes.size / sum + sum /= 1_000_000_000.0 + return sum / renderWaitTimes.size + } + + val averageRenderTime: Double get() { + var sum = 0.0 + + for (value in frameRenderTimes) + sum += value + + if (sum == 0.0) + return 0.0 + + sum /= 1_000_000_000.0 + return sum / frameRenderTimes.size } val settings = ClientSettings() @@ -828,164 +842,181 @@ class StarboundClient : Closeable { fun renderFrame(): Boolean { ensureSameThread() - val diff = JVMTimeSource.INSTANCE.seconds - lastRender + var diff = nextRender - System.nanoTime() + var yields = 0 - if (diff < Starbound.TICK_TIME_ADVANCE) - LockSupport.parkNanos(((Starbound.TICK_TIME_ADVANCE - diff) * 1_000_000_000.0).toLong()) + // try to sleep until next frame as precise as possible + while (diff > 0L) { + if (diff >= 1_500_000L) { + LockSupport.parkNanos(1_000_000L) + } else { + Thread.yield() + yields++ + } - frameRenderTime = JVMTimeSource.INSTANCE.seconds - lastRender - frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime - lastRender = JVMTimeSource.INSTANCE.seconds - - if (GLFW.glfwWindowShouldClose(window)) { - close() - return false + diff = nextRender - System.nanoTime() } - val world = world + val mark = System.nanoTime() - if (!isRenderingGame) { - cleanup() - GLFW.glfwPollEvents() + try { + if (GLFW.glfwWindowShouldClose(window)) { + close() + return false + } + + val world = world + + if (!isRenderingGame) { + cleanup() + GLFW.glfwPollEvents() + + if (world != null) { + if (Starbound.initialized) + world.think() + } + + return true + } if (world != null) { + updateViewportParams() + val layers = LayeredRenderer() + if (Starbound.initialized) world.think() + + clearColor = RGBAColor.SLATE_GRAY + glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) + matrixStack.clear(viewportMatrixWorld) + + matrixStack.push().last() + .translateWithMultiplication(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира + .scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера + .translateWithMultiplication(-camera.pos.x, -camera.pos.y) // перемещаем вид к камере + + for (lambda in onPreDrawWorld) { + lambda.invoke(layers) + } + + for (i in onPostDrawWorldOnce.size - 1 downTo 0) { + onPostDrawWorldOnce[i].invoke(layers) + onPostDrawWorldOnce.removeAt(i) + } + + viewportLighting.clear() + + world.addLayers( + layers = layers, + size = viewportRectangle) + + layers.render(matrixStack) + + val viewportLightingMem = viewportLightingMem + + if (viewportLightingMem != null && !fullbright) { + viewportLightingMem.position(0) + BufferUtils.zeroBuffer(viewportLightingMem) + viewportLightingMem.position(0) + viewportLighting.calculate(viewportLightingMem, viewportLighting.width.coerceAtMost(4096), viewportLighting.height.coerceAtMost(4096)) + viewportLightingMem.position(0) + + val old = textureUnpackAlignment + textureUnpackAlignment = if (viewportLighting.width.coerceAtMost(4096) % 4 == 0) 4 else 1 + + viewportLightingTexture.upload( + GL_RGB, + viewportLighting.width.coerceAtMost(4096), + viewportLighting.height.coerceAtMost(4096), + GL_RGB, + GL_UNSIGNED_BYTE, + viewportLightingMem + ) + + textureUnpackAlignment = old + + viewportLightingTexture.textureMinFilter = GL_LINEAR + //viewportLightingTexture.textureMagFilter = GL_NEAREST + + //viewportLightingTexture.generateMips() + + blendFunc = BlendFunc.MULTIPLY_BY_SRC + + quadTexture(viewportLightingTexture) { + it.quad( + (viewportCellX).toFloat(), + (viewportCellY).toFloat(), + (viewportCellX + viewportCellWidth).toFloat(), + (viewportCellY + viewportCellHeight).toFloat(), + QuadTransformers.uv() + ) + } + + blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA + } + + world.physics.debugDraw() + + for (lambda in onPostDrawWorld) { + lambda.invoke() + } + + matrixStack.pop() } + matrixStack.clear(viewportMatrixScreen) + + val thisTime = System.currentTimeMillis() + + if (startupTextList.isNotEmpty() && thisTime <= finishStartupRendering) { + var alpha = 1f + + if (finishStartupRendering - thisTime < 1000L) { + alpha = (finishStartupRendering - thisTime) / 1000f + } + + matrixStack.push() + matrixStack.last().translateWithMultiplication(y = viewportHeight.toFloat()) + var shade = 255 + + for (i in startupTextList.size - 1 downTo 0) { + val size = font.render(startupTextList[i], alignY = TextAlignY.BOTTOM, scale = 0.4f, color = RGBAColor(shade / 255f, shade / 255f, shade / 255f, alpha)) + matrixStack.last().translateWithMultiplication(y = -size.height * 1.2f) + + if (shade > 120) { + shade -= 10 + } + } + + matrixStack.pop() + } + + for (fn in onDrawGUI) { + fn.invoke() + } + + val runtime = Runtime.getRuntime() + + font.render("Latency: ${(averageRenderWait * 1_00000.0).toInt() / 100f}ms", scale = 0.4f) + font.render("Frame: ${(averageRenderTime * 1_00000.0).toInt() / 100f}ms", y = font.lineHeight * 0.6f, scale = 0.4f) + font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 1.2f, scale = 0.4f) + + GLFW.glfwSwapBuffers(window) + GLFW.glfwPollEvents() + input.think() + + camera.think(Starbound.TICK_TIME_ADVANCE) + + cleanup() + return true + } finally { + frameRenderTime = System.nanoTime() - mark + frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime + renderWaitTimes[++renderWaitIndex % renderWaitTimes.size] = System.nanoTime() - lastRender + lastRender = System.nanoTime() + nextRender = mark + Starbound.TICK_TIME_ADVANCE_NANOS } - - if (world != null) { - updateViewportParams() - val layers = LayeredRenderer() - - if (Starbound.initialized) - world.think() - - clearColor = RGBAColor.SLATE_GRAY - glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) - matrixStack.clear(viewportMatrixWorld) - - matrixStack.push().last() - .translateWithMultiplication(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира - .scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера - .translateWithMultiplication(-camera.pos.x, -camera.pos.y) // перемещаем вид к камере - - for (lambda in onPreDrawWorld) { - lambda.invoke(layers) - } - - for (i in onPostDrawWorldOnce.size - 1 downTo 0) { - onPostDrawWorldOnce[i].invoke(layers) - onPostDrawWorldOnce.removeAt(i) - } - - viewportLighting.clear() - - world.addLayers( - layers = layers, - size = viewportRectangle) - - layers.render(matrixStack) - - val viewportLightingMem = viewportLightingMem - - if (viewportLightingMem != null && !fullbright) { - viewportLightingMem.position(0) - BufferUtils.zeroBuffer(viewportLightingMem) - viewportLightingMem.position(0) - viewportLighting.calculate(viewportLightingMem, viewportLighting.width.coerceAtMost(4096), viewportLighting.height.coerceAtMost(4096)) - viewportLightingMem.position(0) - - val old = textureUnpackAlignment - textureUnpackAlignment = if (viewportLighting.width.coerceAtMost(4096) % 4 == 0) 4 else 1 - - viewportLightingTexture.upload( - GL_RGB, - viewportLighting.width.coerceAtMost(4096), - viewportLighting.height.coerceAtMost(4096), - GL_RGB, - GL_UNSIGNED_BYTE, - viewportLightingMem - ) - - textureUnpackAlignment = old - - viewportLightingTexture.textureMinFilter = GL_LINEAR - //viewportLightingTexture.textureMagFilter = GL_NEAREST - - //viewportLightingTexture.generateMips() - - blendFunc = BlendFunc.MULTIPLY_BY_SRC - - quadTexture(viewportLightingTexture) { - it.quad( - (viewportCellX).toFloat(), - (viewportCellY).toFloat(), - (viewportCellX + viewportCellWidth).toFloat(), - (viewportCellY + viewportCellHeight).toFloat(), - QuadTransformers.uv() - ) - } - - blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA - } - - world.physics.debugDraw() - - for (lambda in onPostDrawWorld) { - lambda.invoke() - } - - matrixStack.pop() - } - - matrixStack.clear(viewportMatrixScreen) - - val thisTime = System.currentTimeMillis() - - if (startupTextList.isNotEmpty() && thisTime <= finishStartupRendering) { - var alpha = 1f - - if (finishStartupRendering - thisTime < 1000L) { - alpha = (finishStartupRendering - thisTime) / 1000f - } - - matrixStack.push() - matrixStack.last().translateWithMultiplication(y = viewportHeight.toFloat()) - var shade = 255 - - for (i in startupTextList.size - 1 downTo 0) { - val size = font.render(startupTextList[i], alignY = TextAlignY.BOTTOM, scale = 0.4f, color = RGBAColor(shade / 255f, shade / 255f, shade / 255f, alpha)) - matrixStack.last().translateWithMultiplication(y = -size.height * 1.2f) - - if (shade > 120) { - shade -= 10 - } - } - - matrixStack.pop() - } - - for (fn in onDrawGUI) { - fn.invoke() - } - - val runtime = Runtime.getRuntime() - - font.render("FPS: ${(averageFramesPerSecond * 100f).toInt() / 100f}", scale = 0.4f) - font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 0.5f, scale = 0.4f) - - GLFW.glfwSwapBuffers(window) - GLFW.glfwPollEvents() - input.think() - - camera.think(Starbound.TICK_TIME_ADVANCE) - - cleanup() - - return true } fun onTermination(lambda: () -> Unit) {