diff --git a/src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/particle/ParticleRendererTest.kt b/src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/particle/ParticleRendererTest.kt new file mode 100644 index 000000000..518e84de3 --- /dev/null +++ b/src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/particle/ParticleRendererTest.kt @@ -0,0 +1,150 @@ +/* + * Minosoft + * Copyright (C) 2020-2023 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.particle + +import de.bixilon.kotlinglm.vec3.Vec3d +import de.bixilon.kutil.observer.DataObserver +import de.bixilon.kutil.reflection.ReflectionUtil.forceSet +import de.bixilon.minosoft.data.registries.identified.Namespaces.minosoft +import de.bixilon.minosoft.data.registries.particle.ParticleType +import de.bixilon.minosoft.data.registries.particle.data.ParticleData +import de.bixilon.minosoft.gui.rendering.RenderContext +import de.bixilon.minosoft.gui.rendering.RenderingStates +import de.bixilon.minosoft.gui.rendering.camera.Camera +import de.bixilon.minosoft.gui.rendering.light.RenderLight +import de.bixilon.minosoft.gui.rendering.particle.types.Particle +import de.bixilon.minosoft.gui.rendering.system.dummy.DummyRenderSystem +import de.bixilon.minosoft.gui.rendering.system.dummy.texture.DummyTextureManager +import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3dUtil.EMPTY +import de.bixilon.minosoft.protocol.network.connection.play.ConnectionTestUtil.createConnection +import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection +import de.bixilon.minosoft.test.ITUtil.allocate +import org.testng.Assert.assertEquals +import org.testng.Assert.assertFalse +import org.testng.annotations.Test + +@Test(groups = ["particle"]) +class ParticleRendererTest { + + private fun create(): ParticleRenderer { + val context = RenderContext::class.java.allocate() + context::connection.forceSet(createConnection(1)) + context::state.forceSet(DataObserver(RenderingStates.RUNNING)) + context::system.forceSet(DummyRenderSystem(context)) + context::textures.forceSet(DummyTextureManager(context)) + context::light.forceSet(RenderLight(context)) + context::camera.forceSet(Camera(context)) + val renderer = ParticleRenderer(context.connection, context) + + + return renderer + } + + private fun ParticleRenderer.draw() { + prePrepareDraw() + prepareDrawAsync() + postPrepareDraw() + } + + + fun setup() { + create() + } + + fun `draw once`() { + val renderer = create() + assertEquals(renderer.size, 0) + val particle = TestParticle(renderer.context.connection) + renderer += particle + renderer.draw() + assertEquals(particle.vertices, 1) + assertEquals(particle.tryTicks, 1) + assertFalse(particle.dead) + assertEquals(renderer.size, 1) + } + + fun `draw twice`() { + val renderer = create() + val particle = TestParticle(renderer.context.connection) + renderer += particle + renderer.draw(); renderer.draw() + assertEquals(particle.vertices, 2) + assertEquals(particle.tryTicks, 2) + assertEquals(renderer.size, 1) + } + + fun kill() { + val renderer = create() + val particle = TestParticle(renderer.context.connection) + renderer += particle + renderer.draw(); renderer.draw() + particle.dead = true + renderer.draw() + assertEquals(particle.vertices, 2) + assertEquals(particle.tryTicks, 2) + assertEquals(renderer.size, 0) + } + + fun `add 2 particles`() { + val renderer = create() + assertEquals(renderer.size, 0) + val a = TestParticle(renderer.context.connection) + val b = TestParticle(renderer.context.connection) + renderer += a; renderer += b + assertEquals(renderer.size, 0) // queue not updated yet + renderer.draw() + assertEquals(renderer.size, 2) + assertEquals(a.vertices, 1); assertEquals(a.tryTicks, 1) + assertEquals(b.vertices, 1); assertEquals(b.tryTicks, 1) + } + + fun `discard with maxAmount`() { + val renderer = create() + assertEquals(renderer.size, 0) + renderer.maxAmount = 1 + val a = TestParticle(renderer.context.connection) + val b = TestParticle(renderer.context.connection) + renderer += a; renderer += b + assertEquals(renderer.size, 0) // queue not updated yet + renderer.draw() + assertEquals(renderer.size, 1) + assertEquals(a.vertices, 1); assertEquals(a.tryTicks, 1) + assertEquals(b.vertices, 0); assertEquals(b.tryTicks, 0) + } + + + // TODO: queue, maxAmount, auto ticking + + private class TestParticle(connection: PlayConnection) : Particle(connection, Vec3d.EMPTY, Vec3d.EMPTY, DATA) { + var vertices = 0 + var tryTicks = 0 + + init { + maxAge = 10 + } + + override fun tryTick(time: Long) { + tryTicks++ + } + + override fun addVertex(mesh: ParticleMesh, translucentMesh: ParticleMesh, time: Long) { + vertices++ + } + + + companion object { + val DATA = ParticleData(ParticleType(minosoft("test"), emptyList(), false, null)) + } + } +} diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleList.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleList.kt new file mode 100644 index 000000000..a7bdd8279 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleList.kt @@ -0,0 +1,30 @@ +/* + * Minosoft + * Copyright (C) 2020-2023 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.particle + +import de.bixilon.kutil.concurrent.lock.simple.SimpleLock +import de.bixilon.minosoft.gui.rendering.particle.types.Particle + +class ParticleList(maxAmount: Int) { + val particles: MutableList = ArrayList(maxAmount) + val lock = SimpleLock() + + val size get() = particles.size + + fun clear() { + lock.lock() + particles.clear() + lock.unlock() + } +} diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleQueue.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleQueue.kt new file mode 100644 index 000000000..4af561e3d --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleQueue.kt @@ -0,0 +1,59 @@ +/* + * Minosoft + * Copyright (C) 2020-2023 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.particle + +import de.bixilon.kutil.concurrent.lock.simple.SimpleLock +import de.bixilon.minosoft.gui.rendering.particle.types.Particle + +class ParticleQueue(val renderer: ParticleRenderer) { + private val lock = SimpleLock() + private val queue: MutableList = ArrayList(QUEUE_CAPACITY) + + + operator fun plusAssign(particle: Particle) = queue(particle) + fun queue(particle: Particle) { + lock.lock() + val size = queue.size + if (size > QUEUE_CAPACITY || renderer.size + size > renderer.maxAmount) { + // already overloaded, ignore + lock.unlock() + return + } + queue += particle + lock.unlock() + } + + + fun clear() { + lock.lock() + queue.clear() + lock.unlock() + } + + fun add(list: MutableList) { + if (queue.isEmpty()) return + lock.lock() + + while (queue.isNotEmpty() && list.size < renderer.maxAmount) { + list.add(queue.removeFirst()) + } + + lock.unlock() + } + + + companion object { + const val QUEUE_CAPACITY = 1000 + } +} diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleRenderer.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleRenderer.kt index d5979c668..f2b30d0ce 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleRenderer.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleRenderer.kt @@ -14,12 +14,8 @@ package de.bixilon.minosoft.gui.rendering.particle import de.bixilon.kotlinglm.vec3.Vec3 -import de.bixilon.kutil.concurrent.lock.simple.SimpleLock -import de.bixilon.kutil.concurrent.schedule.RepeatedTask -import de.bixilon.kutil.concurrent.schedule.TaskScheduler import de.bixilon.kutil.latch.AbstractLatch import de.bixilon.kutil.observer.DataObserver.Companion.observe -import de.bixilon.kutil.time.TimeUtil.millis import de.bixilon.minosoft.data.registries.identified.Namespaces.minosoft import de.bixilon.minosoft.data.world.particle.AbstractParticleRenderer import de.bixilon.minosoft.gui.rendering.RenderContext @@ -35,10 +31,7 @@ import de.bixilon.minosoft.gui.rendering.system.base.layer.TranslucentLayer import de.bixilon.minosoft.gui.rendering.system.base.phases.SkipAll import de.bixilon.minosoft.modding.event.listener.CallbackEventListener.Companion.listen import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection -import de.bixilon.minosoft.protocol.network.connection.play.PlayConnectionStates -import de.bixilon.minosoft.protocol.network.connection.play.PlayConnectionStates.Companion.disconnected import de.bixilon.minosoft.protocol.packets.s2c.play.block.chunk.ChunkUtil.isInViewDistance -import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition import de.bixilon.minosoft.util.collections.floats.BufferedArrayFloatList @@ -53,49 +46,33 @@ class ParticleRenderer( private val translucentShader = renderSystem.createShader(minosoft("particle")) { ParticleShader(it) } // There is no opaque mesh because it is simply not needed (every particle has transparency) - private var mesh = ParticleMesh(context, BufferedArrayFloatList(MAXIMUM_AMOUNT * ParticleMesh.ParticleMeshStruct.FLOATS_PER_VERTEX)) - private var translucentMesh = ParticleMesh(context, BufferedArrayFloatList(MAXIMUM_AMOUNT * ParticleMesh.ParticleMeshStruct.FLOATS_PER_VERTEX)) + var mesh = ParticleMesh(context, BufferedArrayFloatList(profile.maxAmount * ParticleMesh.ParticleMeshStruct.FLOATS_PER_VERTEX)) + var translucentMesh = ParticleMesh(context, BufferedArrayFloatList(profile.maxAmount * ParticleMesh.ParticleMeshStruct.FLOATS_PER_VERTEX)) - private val particlesLock = SimpleLock() - private var particles: MutableList = mutableListOf() - private var particleQueueLock = SimpleLock() - private var particleQueue: MutableList = mutableListOf() + val particles = ParticleList(profile.maxAmount) + val queue = ParticleQueue(this) + val ticker = ParticleTicker(this) private var matrixUpdate = true - private lateinit var particleTask: RepeatedTask - override val skipAll: Boolean get() = !enabled - private var enabled = true + var enabled = true set(value) { if (!value) { - particlesLock.lock() particles.clear() - particlesLock.unlock() - - particleQueueLock.lock() - particleQueue.clear() - particleQueueLock.unlock() + queue.clear() } field = value } - private var maxAmount = MAXIMUM_AMOUNT + var maxAmount = MAXIMUM_AMOUNT set(value) { - check(value > 1) { "Can not have negative particle max amount" } - particlesLock.lock() - while (particles.size > value) { - particles.removeAt(0) + if (value < 0) throw IllegalStateException("Can not set negative amount of particles!") + if (value < field) { + removeAllParticles() } - val particlesSize = particles.size - particlesLock.unlock() - particleQueueLock.lock() - while (particlesSize + particleQueue.size > value) { - particleQueue.removeAt(0) - } - particleQueueLock.unlock() field = value } @@ -107,90 +84,45 @@ class ParticleRenderer( layers.register(TranslucentLayer, translucentShader, this::drawTranslucent) } + private fun loadTextures() { + for (particle in connection.registries.particleType) { + for (file in particle.textures) { + context.textures.static.create(file) + } + } + } + override fun init(latch: AbstractLatch) { profile::maxAmount.observe(this, true) { maxAmount = minOf(it, MAXIMUM_AMOUNT) } profile::enabled.observe(this, true) { enabled = it } - connection.events.listen { - matrixUpdate = true - } + connection.events.listen { matrixUpdate = true } mesh.load() translucentMesh.load() - for (particle in connection.registries.particleType) { - for (resourceLocation in particle.textures) { - context.textures.static.create(resourceLocation) - } - } + loadTextures() DefaultParticleBehavior.register(connection, this) } override fun postInit(latch: AbstractLatch) { shader.load() translucentShader.load() + ticker.init() connection.world.particle = this - - particleTask = RepeatedTask(ProtocolDefinition.TICK_TIME, maxDelay = ProtocolDefinition.TICK_TIME / 2) { - if (!context.state.running || !enabled || connection.state != PlayConnectionStates.PLAYING) { - return@RepeatedTask - } - val cameraPosition = connection.player.physics.positionInfo.chunkPosition - val particleViewDistance = connection.world.view.particleViewDistance - - - particlesLock.lock() - try { - val time = millis() - val iterator = particles.iterator() - for (particle in iterator) { - if (!particle.chunkPosition.isInViewDistance(particleViewDistance, cameraPosition)) { // ToDo: Check fog distance - particle.dead = true - iterator.remove() - } else if (particle.dead) { - iterator.remove() - } else { - particle.tryTick(time) - } - } - - particleQueueLock.lock() - particles += particleQueue - particleQueue.clear() - particleQueueLock.unlock() - } finally { - particlesLock.unlock() - } - } - TaskScheduler += particleTask - - connection::state.observe(this) { - if (!it.disconnected) { - return@observe - } - TaskScheduler -= particleTask - } } override fun addParticle(particle: Particle) { if (!context.state.running || !enabled) { return } - val particleCount = particles.size + particleQueue.size - if (particleCount >= maxAmount) { - return - } - if (!particle.chunkPosition.isInViewDistance(connection.world.view.particleViewDistance, connection.player.physics.positionInfo.chunkPosition)) { particle.dead = true return } - particle.tryTick(millis()) - particleQueueLock.lock() - particleQueue += particle - particleQueueLock.unlock() + queue += particle } private fun updateShaders() { @@ -214,34 +146,16 @@ class ParticleRenderer( translucentMesh.unload() } - override fun prepareDrawAsync() { + private fun prepareMesh() { mesh.data.clear() translucentMesh.data.clear() mesh = ParticleMesh(context, mesh.data) translucentMesh = ParticleMesh(context, translucentMesh.data) + } - particlesLock.acquire() - - val start = millis() - var time = start - for ((index, particle) in particles.withIndex()) { - particle.tryTick(time) - if (particle.dead) { - continue - } - particle.addVertex(mesh, translucentMesh, time) - - if (index % 1000 == 0) { - time = millis() - if (time - start > MAX_FRAME_TIME) { - // particles are heavily lagging out the game, reducing it. - // TODO: introduce slow particle mode and optimize out slow particles - break - } - } - } - - particlesLock.release() + override fun prepareDrawAsync() { + prepareMesh() + ticker.tick(true) } override fun postPrepareDraw() { @@ -258,18 +172,12 @@ class ParticleRenderer( } override fun removeAllParticles() { - particlesLock.lock() particles.clear() - particlesLock.unlock() - particleQueueLock.lock() - particleQueue.clear() - particleQueueLock.unlock() + queue.clear() } - companion object : RendererBuilder { const val MAXIMUM_AMOUNT = 50000 - const val MAX_FRAME_TIME = 5 override fun build(connection: PlayConnection, context: RenderContext): ParticleRenderer? { if (connection.profiles.particle.skipLoading) { diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleTicker.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleTicker.kt new file mode 100644 index 000000000..879ab5357 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/particle/ParticleTicker.kt @@ -0,0 +1,114 @@ +/* + * Minosoft + * Copyright (C) 2020-2023 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.particle + +import de.bixilon.kutil.concurrent.schedule.RepeatedTask +import de.bixilon.kutil.concurrent.schedule.TaskScheduler +import de.bixilon.kutil.exception.ExceptionUtil.ignoreAll +import de.bixilon.kutil.observer.DataObserver.Companion.observe +import de.bixilon.kutil.time.TimeUtil.millis +import de.bixilon.minosoft.data.world.positions.ChunkPosition +import de.bixilon.minosoft.gui.rendering.particle.types.Particle +import de.bixilon.minosoft.protocol.network.connection.play.PlayConnectionStates +import de.bixilon.minosoft.protocol.packets.s2c.play.block.chunk.ChunkUtil.isInViewDistance +import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition + +class ParticleTicker(val renderer: ParticleRenderer) { + private val particles = renderer.particles + private val context = renderer.context + private var task: RepeatedTask? = null + + + private fun canTick(): Boolean { + if (context.connection.state != PlayConnectionStates.PLAYING) return false + if (!renderer.enabled) return false + if (!context.state.running) return false + + + return true + } + + private fun Particle.tick(viewDistance: Int, cameraPosition: ChunkPosition, millis: Long) { + if (!chunkPosition.isInViewDistance(viewDistance, cameraPosition)) { // ToDo: Check fog distance + dead = true + } + if (dead) return + ignoreAll { tryTick(millis) } + } + + fun tick(collect: Boolean) { + if (!canTick()) return + + val camera = context.connection.camera.entity.physics.positionInfo + val cameraPosition = camera.chunkPosition + val viewDistance = context.connection.world.view.particleViewDistance + val start = millis() + var time = start + + + particles.lock.lock() + renderer.queue.add(particles.particles) + + val iterator = particles.particles.iterator() + var index = 0 + for (particle in iterator) { + particle.tick(viewDistance, cameraPosition, time) + + if (particle.dead) { + iterator.remove() + continue + } + if (collect) { + particle.addVertex(renderer.mesh, renderer.translucentMesh, time) + } + if (index % 1000 == 0) { + // check periodically if time is exceeded + time = millis() + if (time - start > MAX_TICK_TIME) { + break + } + } + index++ + } + renderer.queue.add(particles.particles) + particles.lock.unlock() + } + + private fun unregister() { + val task = this.task ?: return + TaskScheduler -= task + this.task = null + } + + private fun register() { + if (this.task != null) unregister() + val task = RepeatedTask(ProtocolDefinition.TICK_TIME, maxDelay = ProtocolDefinition.TICK_TIME / 2) { tick(false) } + this.task = task + TaskScheduler += task + } + + + fun init() { + context.connection::state.observe(this) { + unregister() + if (it == PlayConnectionStates.PLAYING) { + register() + } + } + } + + companion object { + const val MAX_TICK_TIME = 5 + } +} diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/particle/types/Particle.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/particle/types/Particle.kt index 3a5355582..ea2c6beca 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/particle/types/Particle.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/particle/types/Particle.kt @@ -180,7 +180,7 @@ abstract class Particle( forceMove(velocity) } - fun tryTick(time: Long) { + open fun tryTick(time: Long) { if (dead) { return }