mesh loading queue

This commit is contained in:
Bixilon 2022-12-22 17:53:27 +01:00
parent f3d8b74607
commit 50dc60d3f1
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
4 changed files with 157 additions and 85 deletions

View File

@ -23,7 +23,6 @@ import de.bixilon.kutil.concurrent.pool.ThreadPool.Priorities.LOW
import de.bixilon.kutil.concurrent.pool.ThreadPoolRunnable
import de.bixilon.kutil.latch.CountUpAndDownLatch
import de.bixilon.kutil.observer.DataObserver.Companion.observe
import de.bixilon.kutil.time.TimeUtil.millis
import de.bixilon.minosoft.config.key.KeyActions
import de.bixilon.minosoft.config.key.KeyBinding
import de.bixilon.minosoft.config.key.KeyCodes
@ -43,7 +42,6 @@ import de.bixilon.minosoft.gui.rendering.system.base.RenderingCapabilities
import de.bixilon.minosoft.gui.rendering.system.base.phases.OpaqueDrawable
import de.bixilon.minosoft.gui.rendering.system.base.phases.TranslucentDrawable
import de.bixilon.minosoft.gui.rendering.system.base.phases.TransparentDrawable
import de.bixilon.minosoft.gui.rendering.util.VecUtil.empty
import de.bixilon.minosoft.gui.rendering.util.VecUtil.inSectionHeight
import de.bixilon.minosoft.gui.rendering.util.VecUtil.of
import de.bixilon.minosoft.gui.rendering.util.vec.vec2.Vec2iUtil.EMPTY
@ -60,6 +58,7 @@ import de.bixilon.minosoft.gui.rendering.world.preparer.FluidSectionPreparer
import de.bixilon.minosoft.gui.rendering.world.preparer.SolidSectionPreparer
import de.bixilon.minosoft.gui.rendering.world.preparer.cull.FluidCullSectionPreparer
import de.bixilon.minosoft.gui.rendering.world.preparer.cull.SolidCullSectionPreparer
import de.bixilon.minosoft.gui.rendering.world.queue.MeshLoadingQueue
import de.bixilon.minosoft.gui.rendering.world.queue.MeshUnloadingQueue
import de.bixilon.minosoft.gui.rendering.world.shader.WorldShader
import de.bixilon.minosoft.gui.rendering.world.shader.WorldTextShader
@ -86,7 +85,7 @@ class WorldRenderer(
) : Renderer, OpaqueDrawable, TranslucentDrawable, TransparentDrawable {
private val profile = connection.profiles.block
override val renderSystem: RenderSystem = renderWindow.renderSystem
private val visibilityGraph = renderWindow.camera.visibilityGraph
val visibilityGraph = renderWindow.camera.visibilityGraph
private val shader = renderSystem.createShader("minosoft:world".toResourceLocation()) { WorldShader(it, false) }
private val transparentShader = renderSystem.createShader("minosoft:world".toResourceLocation()) { WorldShader(it, true) }
private val textShader = renderSystem.createShader("minosoft:world/text".toResourceLocation()) { WorldTextShader(it) }
@ -94,8 +93,8 @@ class WorldRenderer(
private val solidSectionPreparer: SolidSectionPreparer = SolidCullSectionPreparer(renderWindow)
private val fluidSectionPreparer: FluidSectionPreparer = FluidCullSectionPreparer(renderWindow)
private val loadedMeshes: MutableMap<Vec2i, Int2ObjectOpenHashMap<WorldMesh>> = mutableMapOf() // all prepared (and up to date) meshes
private val loadedMeshesLock = SimpleLock()
val loadedMeshes: MutableMap<Vec2i, Int2ObjectOpenHashMap<WorldMesh>> = mutableMapOf() // all prepared (and up to date) meshes
val loadedMeshesLock = SimpleLock()
val maxPreparingTasks = maxOf(DefaultThreadPool.threadCount - 2, 1)
private val preparingTasks: MutableSet<SectionPrepareTask> = mutableSetOf() // current running section preparing tasks
@ -110,9 +109,7 @@ class WorldRenderer(
private val culledQueueLock = SimpleLock()
val maxMeshesToLoad = if (SystemInformation.RUNTIME.maxMemory() > 1_000_000_000) 150 else 80
private val meshesToLoad: MutableList<WorldQueueItem> = mutableListOf() // prepared meshes, that can be loaded in the (next) frame
private val meshesToLoadSet: MutableSet<WorldQueueItem> = HashSet()
private val meshesToLoadLock = SimpleLock()
private val loadingQueue = MeshLoadingQueue(this)
private val unloadingQueue = MeshUnloadingQueue(this)
// all meshes that will be rendered in the next frame (might be changed, when the frustum changes or a chunk gets loaded, ...)
@ -122,14 +119,14 @@ class WorldRenderer(
private var previousViewDistance = connection.world.view.viewDistance
private var cameraPosition = Vec3.EMPTY
private var cameraChunkPosition = Vec2i.EMPTY
var cameraChunkPosition = Vec2i.EMPTY
private var cameraSectionHeight = 0
val visibleSize: String
get() = visible.sizeString
val loadedMeshesSize: Int by loadedMeshes::size
val culledQueuedSize: Int by culledQueue::size
val meshesToLoadSize: Int by meshesToLoad::size
val meshesToLoadSize: Int by loadingQueue::size
val queueSize: Int by queue::size
val preparingTasksSize: Int by preparingTasks::size
@ -263,7 +260,7 @@ class WorldRenderer(
// Unload all chunks(-sections) that are out of view distance
culledQueueLock.lock()
queueLock.lock()
meshesToLoadLock.lock()
loadingQueue.lock()
unloadingQueue.lock()
loadedMeshesLock.lock()
@ -289,8 +286,7 @@ class WorldRenderer(
queue.removeAll { !visibilityGraph.isChunkVisible(it.chunkPosition) }
queueSet.removeAll { !visibilityGraph.isChunkVisible(it.chunkPosition) }
meshesToLoad.removeAll { !visibilityGraph.isChunkVisible(it.chunkPosition) }
meshesToLoadSet.removeAll { !visibilityGraph.isChunkVisible(it.chunkPosition) }
loadingQueue.cleanup(false)
preparingTasksLock.acquire()
for (task in preparingTasks) {
@ -303,7 +299,7 @@ class WorldRenderer(
loadedMeshesLock.unlock()
queueLock.unlock()
culledQueueLock.unlock()
meshesToLoadLock.unlock()
loadingQueue.unlock()
unloadingQueue.unlock()
} else {
prepareWorld()
@ -334,7 +330,7 @@ class WorldRenderer(
private fun unloadWorld() {
culledQueueLock.lock()
queueLock.lock()
meshesToLoadLock.lock()
loadingQueue.lock()
loadedMeshesLock.lock()
unloadingQueue.lock()
@ -347,8 +343,7 @@ class WorldRenderer(
loadedMeshes.clear()
queue.clear()
queueSet.clear()
meshesToLoad.clear()
meshesToLoadSet.clear()
loadingQueue.clear()
clearVisibleNextFrame = true
@ -361,13 +356,13 @@ class WorldRenderer(
loadedMeshesLock.unlock()
queueLock.unlock()
culledQueueLock.unlock()
meshesToLoadLock.unlock()
loadingQueue.unlock()
}
private fun unloadChunk(chunkPosition: Vec2i) {
culledQueueLock.lock()
queueLock.lock()
meshesToLoadLock.lock()
loadingQueue.lock()
unloadingQueue.lock()
loadedMeshesLock.lock()
@ -378,8 +373,7 @@ class WorldRenderer(
queue.removeAll { it.chunkPosition == chunkPosition }
queueSet.removeAll { it.chunkPosition == chunkPosition }
meshesToLoad.removeAll { it.chunkPosition == chunkPosition }
meshesToLoadSet.removeAll { it.chunkPosition == chunkPosition }
loadingQueue.abort(chunkPosition, false)
preparingTasksLock.acquire()
for (task in preparingTasks) {
@ -392,7 +386,7 @@ class WorldRenderer(
loadedMeshesLock.unlock()
culledQueueLock.unlock()
meshesToLoadLock.unlock()
loadingQueue.unlock()
unloadingQueue.unlock()
queueLock.unlock()
}
@ -411,7 +405,7 @@ class WorldRenderer(
private fun workQueue() {
val size = preparingTasks.size
if (queue.isEmpty() || size >= maxPreparingTasks || meshesToLoad.size >= maxMeshesToLoad) {
if (queue.isEmpty() || size >= maxPreparingTasks || loadingQueue.size >= maxMeshesToLoad) {
return
}
if (workingOnQueue) {
@ -466,18 +460,7 @@ class WorldRenderer(
}
mesh.finish()
item.mesh = mesh
meshesToLoadLock.lock()
if (meshesToLoadSet.remove(item)) {
meshesToLoad.remove(item) // Remove duplicates
}
if (item.chunkPosition == cameraChunkPosition) {
// still higher priority
meshesToLoad.add(0, item)
} else {
meshesToLoad += item
}
meshesToLoadSet += item
meshesToLoadLock.unlock()
loadingQueue.queue(mesh)
} catch (exception: Throwable) {
if (exception !is InterruptedException) {
// otherwise task got interrupted (probably because of chunk unload)
@ -496,7 +479,7 @@ class WorldRenderer(
private fun queueItemUnload(item: WorldQueueItem) {
culledQueueLock.lock()
queueLock.lock()
meshesToLoadLock.lock()
loadingQueue.lock()
unloadingQueue.lock()
loadedMeshesLock.lock()
loadedMeshes[item.chunkPosition]?.let {
@ -517,9 +500,7 @@ class WorldRenderer(
queue.remove(item)
}
if (meshesToLoadSet.remove(item)) {
meshesToLoad.remove(item)
}
loadingQueue.abort(item.chunkPosition, false)
preparingTasksLock.acquire()
for (task in preparingTasks) {
@ -532,7 +513,7 @@ class WorldRenderer(
loadedMeshesLock.unlock()
queueLock.unlock()
culledQueueLock.unlock()
meshesToLoadLock.unlock()
loadingQueue.unlock()
unloadingQueue.unlock()
}
@ -613,47 +594,6 @@ class WorldRenderer(
}
}
private fun loadMeshes() {
meshesToLoadLock.lock()
if (meshesToLoad.isEmpty()) {
meshesToLoadLock.unlock()
return
}
var addedMeshes = 0
val start = millis()
val maxTime = if (connection.player.velocity.empty) 50L else 20L // If the player is still, then we can load more chunks (to not cause lags)
while (meshesToLoad.isNotEmpty() && (millis() - start < maxTime)) {
val item = meshesToLoad.removeAt(0)
meshesToLoadSet.remove(item)
val mesh = item.mesh ?: continue
mesh.load()
loadedMeshesLock.lock()
val meshes = loadedMeshes.getOrPut(item.chunkPosition) { Int2ObjectOpenHashMap() }
meshes.put(item.sectionHeight, mesh)?.let {
this.visible.removeMesh(it)
it.unload()
}
loadedMeshesLock.unlock()
val visible = visibilityGraph.isSectionVisible(item.chunkPosition, item.sectionHeight, mesh.minPosition, mesh.maxPosition, true)
if (visible) {
addedMeshes++
this.visible.addMesh(mesh)
}
}
meshesToLoadLock.unlock()
if (addedMeshes > 0) {
visible.sort()
}
}
override fun postPrepareDraw() {
if (clearVisibleNextFrame) {
visible.clear()
@ -661,7 +601,7 @@ class WorldRenderer(
}
unloadingQueue.work()
loadMeshes()
loadingQueue.work()
}
override fun setupOpaque() {

View File

@ -21,5 +21,5 @@ object WorldRendererUtil {
// If the player is still, then we can load more chunks (to not cause lags)
val WorldRenderer.loadingTime: Long get() = if (connection.player.velocity.empty) STILL_LOADING_TIME else MOVING_LOADING_TIME // TODO: get of camera
val WorldRenderer.maxBusyTime: Long get() = if (connection.player.velocity.empty) STILL_LOADING_TIME else MOVING_LOADING_TIME // TODO: get of camera
}

View File

@ -0,0 +1,132 @@
/*
* Minosoft
* Copyright (C) 2020-2022 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 distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.rendering.world.queue
import de.bixilon.kutil.concurrent.lock.simple.SimpleLock
import de.bixilon.kutil.time.TimeUtil
import de.bixilon.minosoft.data.world.positions.ChunkPosition
import de.bixilon.minosoft.gui.rendering.world.WorldRenderer
import de.bixilon.minosoft.gui.rendering.world.WorldRendererUtil.maxBusyTime
import de.bixilon.minosoft.gui.rendering.world.mesh.WorldMesh
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
class MeshLoadingQueue(
private val renderer: WorldRenderer,
) {
private val meshes: MutableList<WorldMesh> = mutableListOf() // prepared meshes, that can be loaded in the (next) frame
private val positions: MutableSet<ChunkPosition> = HashSet()
private val lock = SimpleLock()
val size: Int get() = meshes.size
fun work() {
lock.lock()
if (meshes.isEmpty()) {
lock.unlock()
return
}
var count = 0
val start = TimeUtil.millis()
val maxTime = renderer.maxBusyTime // If the player is still, then we can load more chunks (to not cause lags)
renderer.loadedMeshesLock.lock()
while (meshes.isNotEmpty() && (TimeUtil.millis() - start < maxTime)) {
val mesh = meshes.removeAt(0)
this.positions -= mesh.chunkPosition
mesh.load()
val meshes = renderer.loadedMeshes.getOrPut(mesh.chunkPosition) { Int2ObjectOpenHashMap() }
meshes.put(mesh.sectionHeight, mesh)?.let {
renderer.visible.removeMesh(it)
it.unload()
}
val visible = renderer.visibilityGraph.isSectionVisible(mesh.chunkPosition, mesh.sectionHeight, mesh.minPosition, mesh.maxPosition, true)
if (visible) {
count++
renderer.visible.addMesh(mesh)
}
}
renderer.loadedMeshesLock.unlock()
lock.unlock()
if (count > 0) {
renderer.visible.sort()
}
}
fun queue(mesh: WorldMesh) {
lock.lock()
if (!this.positions.add(mesh.chunkPosition)) {
// already inside, remove
meshes.remove(mesh)
}
if (mesh.chunkPosition == renderer.cameraChunkPosition) {
// still higher priority
meshes.add(0, mesh)
} else {
meshes += mesh
}
lock.unlock()
}
fun abort(position: ChunkPosition, lock: Boolean = true) {
if (lock) this.lock.lock()
if (this.positions.remove(position)) {
this.meshes.removeIf { it.chunkPosition == position }
}
if (lock) this.lock.unlock()
}
fun cleanup(lock: Boolean) {
val remove: MutableSet<ChunkPosition> = mutableSetOf()
if (lock) this.lock.lock()
this.positions.removeAll {
if (renderer.visibilityGraph.isChunkVisible(it)) {
return@removeAll false
}
remove += it
return@removeAll true
}
this.meshes.removeAll { it.chunkPosition in remove }
if (lock) this.lock.unlock()
}
fun clear(lock: Boolean) {
if (lock) this.lock.lock()
this.positions.clear()
this.meshes.clear()
if (lock) this.lock.unlock()
}
fun lock() {
this.lock.lock()
}
fun unlock() {
this.lock.unlock()
}
}

View File

@ -17,7 +17,7 @@ import de.bixilon.kutil.concurrent.lock.simple.SimpleLock
import de.bixilon.kutil.time.TimeUtil
import de.bixilon.minosoft.data.world.positions.ChunkPosition
import de.bixilon.minosoft.gui.rendering.world.WorldRenderer
import de.bixilon.minosoft.gui.rendering.world.WorldRendererUtil.loadingTime
import de.bixilon.minosoft.gui.rendering.world.WorldRendererUtil.maxBusyTime
import de.bixilon.minosoft.gui.rendering.world.mesh.WorldMesh
class MeshUnloadingQueue(
@ -34,7 +34,7 @@ class MeshUnloadingQueue(
}
val time = TimeUtil.millis()
val maxTime = renderer.loadingTime
val maxTime = renderer.maxBusyTime
while (meshes.isNotEmpty() && (TimeUtil.millis() - time < maxTime)) {
val mesh = meshes.removeAt(0)