limit time for gpu chunk transfers

"Smooth FPS"

Adds an option to the config, and limits to time the chunk renderer has to transfer to about 3ms. This works good and feels a lot smoother now.
This commit is contained in:
Moritz Zwerger 2025-02-08 18:22:11 +01:00
parent f20f2b5e0b
commit 055b015390
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
5 changed files with 43 additions and 12 deletions

View File

@ -1,6 +1,6 @@
/* /*
* Minosoft * Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger * Copyright (C) 2020-2025 Moritz Zwerger
* *
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* *
@ -36,4 +36,11 @@ class PerformanceC(profile: RenderingProfile) {
* Sleeps 100 ms if the rendering window is not in focus anymore * Sleeps 100 ms if the rendering window is not in focus anymore
*/ */
var slowRendering by BooleanDelegate(profile, true) var slowRendering by BooleanDelegate(profile, true)
/**
* Limits the time of chunk transfers to the gpu.
* Increases time until far chunks are loaded.
* If disabled, lag spikes can occur when crossing chunk borders
*/
var limitChunkTransferTime by BooleanDelegate(profile, true)
} }

View File

@ -90,6 +90,8 @@ class ChunkRenderer(
var cameraChunkPosition = Vec2i.EMPTY var cameraChunkPosition = Vec2i.EMPTY
var cameraSectionHeight = 0 var cameraSectionHeight = 0
var limitChunkTransferTime = true
override fun registerLayers() { override fun registerLayers() {
layers.register(OpaqueLayer, shader, this::drawBlocksOpaque) { visible.opaque.isEmpty() } layers.register(OpaqueLayer, shader, this::drawBlocksOpaque) { visible.opaque.isEmpty() }
layers.register(TranslucentLayer, shader, this::drawBlocksTranslucent) { visible.translucent.isEmpty() } layers.register(TranslucentLayer, shader, this::drawBlocksTranslucent) { visible.translucent.isEmpty() }
@ -128,6 +130,7 @@ class ChunkRenderer(
val rendering = session.profiles.rendering val rendering = session.profiles.rendering
rendering.performance::fastBedrock.observe(this) { clearChunkCache() } rendering.performance::fastBedrock.observe(this) { clearChunkCache() }
rendering.light::ambientOcclusion.observe(this) { clearChunkCache() } rendering.light::ambientOcclusion.observe(this) { clearChunkCache() }
rendering.performance::limitChunkTransferTime.observe(this) { this.limitChunkTransferTime = it }
profile::viewDistance.observe(this) { viewDistance -> profile::viewDistance.observe(this) { viewDistance ->
val distance = maxOf(viewDistance, profile.simulationDistance) val distance = maxOf(viewDistance, profile.simulationDistance)

View File

@ -15,13 +15,13 @@ package de.bixilon.minosoft.gui.rendering.chunk.queue.loading
import de.bixilon.kutil.cast.CastUtil.unsafeNull import de.bixilon.kutil.cast.CastUtil.unsafeNull
import de.bixilon.kutil.concurrent.lock.Lock import de.bixilon.kutil.concurrent.lock.Lock
import de.bixilon.kutil.time.TimeUtil
import de.bixilon.minosoft.data.world.positions.ChunkPosition import de.bixilon.minosoft.data.world.positions.ChunkPosition
import de.bixilon.minosoft.gui.rendering.chunk.ChunkRenderer import de.bixilon.minosoft.gui.rendering.chunk.ChunkRenderer
import de.bixilon.minosoft.gui.rendering.chunk.mesh.ChunkMeshes import de.bixilon.minosoft.gui.rendering.chunk.mesh.ChunkMeshes
import de.bixilon.minosoft.gui.rendering.chunk.queue.QueuePosition import de.bixilon.minosoft.gui.rendering.chunk.queue.QueuePosition
import de.bixilon.minosoft.gui.rendering.chunk.util.ChunkRendererUtil.maxBusyTime import de.bixilon.minosoft.gui.rendering.chunk.util.ChunkRendererUtil.maxBusyTime
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import kotlin.time.TimeSource
class MeshLoadingQueue( class MeshLoadingQueue(
private val renderer: ChunkRenderer, private val renderer: ChunkRenderer,
@ -41,14 +41,18 @@ class MeshLoadingQueue(
} }
var count = 0 var count = 0
val start = TimeUtil.millis() val start = TimeSource.Monotonic.markNow()
val maxTime = renderer.maxBusyTime // If the player is still, then we can load more chunks (to not cause lags) val maxTime = renderer.maxBusyTime
var meshes: Int2ObjectOpenHashMap<ChunkMeshes> = unsafeNull() var meshes: Int2ObjectOpenHashMap<ChunkMeshes> = unsafeNull()
var position: ChunkPosition? = null var position: ChunkPosition? = null
renderer.loaded.lock() renderer.loaded.lock()
while (this.meshes.isNotEmpty() && (TimeUtil.millis() - start < maxTime)) { var index = 0
while (true) {
if (this.meshes.isEmpty()) break
if (index++ % BATCH_SIZE == 0 && TimeSource.Monotonic.markNow() - start >= maxTime) break
val mesh = this.meshes.removeAt(0) val mesh = this.meshes.removeAt(0)
this.positions -= QueuePosition(mesh) this.positions -= QueuePosition(mesh)
@ -152,4 +156,8 @@ class MeshLoadingQueue(
this.lock.unlock() this.lock.unlock()
renderer.lock.release() renderer.lock.release()
} }
companion object {
const val BATCH_SIZE = 5
}
} }

View File

@ -14,11 +14,11 @@
package de.bixilon.minosoft.gui.rendering.chunk.queue.loading package de.bixilon.minosoft.gui.rendering.chunk.queue.loading
import de.bixilon.kutil.concurrent.lock.Lock import de.bixilon.kutil.concurrent.lock.Lock
import de.bixilon.kutil.time.TimeUtil
import de.bixilon.minosoft.gui.rendering.chunk.ChunkRenderer import de.bixilon.minosoft.gui.rendering.chunk.ChunkRenderer
import de.bixilon.minosoft.gui.rendering.chunk.mesh.ChunkMeshes import de.bixilon.minosoft.gui.rendering.chunk.mesh.ChunkMeshes
import de.bixilon.minosoft.gui.rendering.chunk.queue.QueuePosition import de.bixilon.minosoft.gui.rendering.chunk.queue.QueuePosition
import de.bixilon.minosoft.gui.rendering.chunk.util.ChunkRendererUtil.maxBusyTime import de.bixilon.minosoft.gui.rendering.chunk.util.ChunkRendererUtil.maxBusyTime
import kotlin.time.TimeSource
class MeshUnloadingQueue( class MeshUnloadingQueue(
private val renderer: ChunkRenderer, private val renderer: ChunkRenderer,
@ -33,10 +33,14 @@ class MeshUnloadingQueue(
return return
} }
val time = TimeUtil.millis() val start = TimeSource.Monotonic.markNow()
val maxTime = renderer.maxBusyTime val maxTime = renderer.maxBusyTime
while (meshes.isNotEmpty() && (TimeUtil.millis() - time < maxTime)) { var index = 0
while (true) {
if (meshes.isEmpty()) break
if (index++ % MeshLoadingQueue.BATCH_SIZE == 0 && TimeSource.Monotonic.markNow() - start >= maxTime) break
val mesh = meshes.removeAt(0) val mesh = meshes.removeAt(0)
this.positions -= QueuePosition(mesh) this.positions -= QueuePosition(mesh)
renderer.visible.removeMesh(mesh) renderer.visible.removeMesh(mesh)

View File

@ -1,6 +1,6 @@
/* /*
* Minosoft * Minosoft
* Copyright (C) 2020-2024 Moritz Zwerger * Copyright (C) 2020-2025 Moritz Zwerger
* *
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* *
@ -17,14 +17,23 @@ import de.bixilon.minosoft.data.world.chunk.ChunkSection
import de.bixilon.minosoft.gui.rendering.chunk.ChunkRenderer import de.bixilon.minosoft.gui.rendering.chunk.ChunkRenderer
import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3dUtil.isEmpty import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3dUtil.isEmpty
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
object ChunkRendererUtil { object ChunkRendererUtil {
const val STILL_LOADING_TIME = 50L val STILL_LOADING_TIME = 30.milliseconds
const val MOVING_LOADING_TIME = 20L val MOVING_LOADING_TIME = 3.milliseconds
// If the player is still, then we can load more chunks (to not cause lags) // If the player is still, then we can load more chunks (to not cause lags)
val ChunkRenderer.maxBusyTime: Long get() = if (session.player.physics.velocity.isEmpty()) STILL_LOADING_TIME else MOVING_LOADING_TIME // TODO: get of camera val ChunkRenderer.maxBusyTime: Duration
get() {
if (!limitChunkTransferTime) return Duration.INFINITE
if (session.camera.entity.physics.velocity.isEmpty()) {
return STILL_LOADING_TIME
}
return MOVING_LOADING_TIME
}
val ChunkSection.smallMesh: Boolean get() = blocks.count < ProtocolDefinition.SECTION_MAX_X * ProtocolDefinition.SECTION_MAX_Z val ChunkSection.smallMesh: Boolean get() = blocks.count < ProtocolDefinition.SECTION_MAX_X * ProtocolDefinition.SECTION_MAX_Z