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
}