From 50dc60d3f113bcf60da9d8efac088e4a2b5a4f84 Mon Sep 17 00:00:00 2001 From: Bixilon Date: Thu, 22 Dec 2022 17:53:27 +0100 Subject: [PATCH] mesh loading queue --- .../gui/rendering/world/WorldRenderer.kt | 104 +++----------- .../gui/rendering/world/WorldRendererUtil.kt | 2 +- .../rendering/world/queue/MeshLoadingQueue.kt | 132 ++++++++++++++++++ .../world/queue/MeshUnloadingQueue.kt | 4 +- 4 files changed, 157 insertions(+), 85 deletions(-) create mode 100644 src/main/java/de/bixilon/minosoft/gui/rendering/world/queue/MeshLoadingQueue.kt diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/world/WorldRenderer.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/world/WorldRenderer.kt index ef2cbd110..96d80732c 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/world/WorldRenderer.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/world/WorldRenderer.kt @@ -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> = mutableMapOf() // all prepared (and up to date) meshes - private val loadedMeshesLock = SimpleLock() + val loadedMeshes: MutableMap> = mutableMapOf() // all prepared (and up to date) meshes + val loadedMeshesLock = SimpleLock() val maxPreparingTasks = maxOf(DefaultThreadPool.threadCount - 2, 1) private val preparingTasks: MutableSet = 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 = mutableListOf() // prepared meshes, that can be loaded in the (next) frame - private val meshesToLoadSet: MutableSet = 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() { diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/world/WorldRendererUtil.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/world/WorldRendererUtil.kt index d71d85458..cbc79191c 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/world/WorldRendererUtil.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/world/WorldRendererUtil.kt @@ -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 } diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/world/queue/MeshLoadingQueue.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/world/queue/MeshLoadingQueue.kt new file mode 100644 index 000000000..fa9c6baa1 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/world/queue/MeshLoadingQueue.kt @@ -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 . + * + * 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 = mutableListOf() // prepared meshes, that can be loaded in the (next) frame + private val positions: MutableSet = 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 = 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() + } +} diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/world/queue/MeshUnloadingQueue.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/world/queue/MeshUnloadingQueue.kt index 1b324adfe..78e17d25d 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/world/queue/MeshUnloadingQueue.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/world/queue/MeshUnloadingQueue.kt @@ -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)