From 5fb6d817fc09835b98a42d2268ee9e4130e9a51b Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sat, 14 Oct 2023 22:23:30 +0700 Subject: [PATCH] Parallel move(), remove more outdated code, complete MailboxExecutorService --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 3 +- .../kstarbound/client/StarboundClient.kt | 8 +- .../client/gl/vertex/StreamVertexBuilder.kt | 6 +- .../kstarbound/client/render/TileRenderer.kt | 4 +- .../client/render/entity/EntityRenderer.kt | 52 ----- .../client/render/entity/ItemRenderer.kt | 22 -- .../kstarbound/client/world/ClientChunk.kt | 2 - .../kstarbound/client/world/ClientWorld.kt | 2 +- .../kstarbound/defs/image/Image.kt | 5 +- ...orService.kt => MailboxExecutorService.kt} | 189 ++++++++++++------ .../kstarbound/util/ParallelPerform.kt | 21 ++ .../ru/dbotthepony/kstarbound/world/Chunk.kt | 59 +++--- .../ru/dbotthepony/kstarbound/world/World.kt | 48 ++--- .../kstarbound/world/api/IChunkCell.kt | 4 - .../kstarbound/world/entities/Entity.kt | 115 ++++++++--- .../kstarbound/world/physics/CollisionPoly.kt | 1 + .../kstarbound/world/physics/CollisionType.kt | 7 +- .../kstarbound/world/physics/Poly.kt | 3 +- 18 files changed, 312 insertions(+), 239 deletions(-) delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/EntityRenderer.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt rename src/main/kotlin/ru/dbotthepony/kstarbound/util/{ManualExecutorService.kt => MailboxExecutorService.kt} (64%) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/util/ParallelPerform.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 80a5630c..310cc70f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -120,11 +120,10 @@ fun main() { //client.world!!.parallax = Starbound.parallaxAccess["garden"] - val item = Registries.items.values.random() val rand = Random() for (i in 0 .. 128) { - val item = ItemEntity(client.world!!, item.value) + val item = ItemEntity(client.world!!, Registries.items.values.random().value) item.position = Vector2d(225.0 - i, 685.0) item.spawn() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 58f97d45..7a4caa2e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -14,7 +14,7 @@ import org.lwjgl.opengl.GLCapabilities import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryUtil import ru.dbotthepony.kstarbound.LoadingLog -import ru.dbotthepony.kstarbound.util.ManualExecutorService +import ru.dbotthepony.kstarbound.util.MailboxExecutorService import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.Starbound @@ -92,7 +92,7 @@ class StarboundClient : Closeable { // client specific executor which will accept tasks which involve probable // callback to foreground executor to initialize thread-unsafe data // In above case too many threads will introduce big congestion for resources, stalling entire workload; wasting cpu resources - val backgroundExecutor = ForkJoinPool(Runtime.getRuntime().availableProcessors().coerceAtMost(4), { + val executor = ForkJoinPool(Runtime.getRuntime().availableProcessors().coerceAtMost(4), { object : ForkJoinWorkerThread(it) { init { name = "Background Executor for '${thread.name}'-${threadCounter.incrementAndGet()}" @@ -108,7 +108,7 @@ class StarboundClient : Closeable { } }, null, false) - val foregroundExecutor = ManualExecutorService(thread) + val mailbox = MailboxExecutorService(thread) val capabilities: GLCapabilities var viewportX: Int = 0 @@ -322,7 +322,7 @@ class StarboundClient : Closeable { } private fun executeQueuedTasks() { - foregroundExecutor.executeQueuedTasks() + mailbox.executeQueuedTasks() var next = openglCleanQueue.poll() as CleanRef? diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt index df8689ca..c2342956 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt @@ -18,9 +18,9 @@ class StreamVertexBuilder( ) { val builder = VertexBuilder(attributes, type, initialCapacity) - private val vao = client.foregroundExecutor.submit(Callable { VertexArrayObject() }) - private val vbo = client.foregroundExecutor.submit(Callable { BufferObject.VBO() }) - private val ebo = client.foregroundExecutor.submit(Callable { BufferObject.EBO() }) + private val vao = client.mailbox.submit(Callable { VertexArrayObject() }) + private val vbo = client.mailbox.submit(Callable { BufferObject.VBO() }) + private val ebo = client.mailbox.submit(Callable { BufferObject.EBO() }) private var initialized = false diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt index 0df41eee..e88164bf 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -49,14 +49,14 @@ class TileRenderers(val client: StarboundClient) { fun getMaterialRenderer(defName: String): TileRenderer { return matCache.get(defName) { val def = Registries.tiles[defName] // TODO: Пустой рендерер - client.foregroundExecutor.submit(Callable { TileRenderer(this, def!!.value) }).get() + client.mailbox.submit(Callable { TileRenderer(this, def!!.value) }).get() } } fun getModifierRenderer(defName: String): TileRenderer { return modCache.get(defName) { val def = Registries.tileModifiers[defName] // TODO: Пустой рендерер - client.foregroundExecutor.submit(Callable { TileRenderer(this, def!!.value) }).get() + client.mailbox.submit(Callable { TileRenderer(this, def!!.value) }).get() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/EntityRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/EntityRenderer.kt deleted file mode 100644 index abcdb6c4..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/EntityRenderer.kt +++ /dev/null @@ -1,52 +0,0 @@ -package ru.dbotthepony.kstarbound.client.render.entity - -import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap -import ru.dbotthepony.kstarbound.client.world.ClientChunk -import ru.dbotthepony.kstarbound.client.StarboundClient -import ru.dbotthepony.kstarbound.world.entities.Entity -import ru.dbotthepony.kvector.arrays.Matrix4fStack -import ru.dbotthepony.kvector.vector.Vector2d - -/** - * Базовый класс, отвечающий за отрисовку определённого ентити в мире - * - * Считается, что процесс отрисовки ограничен лишь одним слоем (т.е. отрисовка происходит в один проход) - */ -open class EntityRenderer(val client: StarboundClient, val entity: Entity, open var chunk: ClientChunk?) { - open val renderPos: Vector2d get() = entity.position - - open fun render(stack: Matrix4fStack) { - - } - - open val layer: Int get() = Z_LEVEL_ENTITIES - - companion object { - /** - * Pseudo Z position for entities, for them to appear behind tile geometry, - * but in front of background walls geometry - */ - const val Z_LEVEL_ENTITIES = 30000 - - private val renderers = Reference2ObjectOpenHashMap, (client: StarboundClient, entity: Entity, chunk: ClientChunk?) -> EntityRenderer>() - - @Suppress("unchecked_cast") - fun registerRenderer(clazz: Class, renderer: (client: StarboundClient, entity: T, chunk: ClientChunk?) -> EntityRenderer) { - check(renderers.put(clazz, renderer as (client: StarboundClient, entity: Entity, chunk: ClientChunk?) -> EntityRenderer) == null) { "Already has renderer for ${clazz.canonicalName}!" } - } - - inline fun registerRenderer(noinline renderer: (client: StarboundClient, entity: T, chunk: ClientChunk?) -> EntityRenderer) { - registerRenderer(T::class.java, renderer) - } - - fun getRender(client: StarboundClient, entity: Entity, chunk: ClientChunk? = null): EntityRenderer { - val factory = renderers[entity::class.java] ?: return EntityRenderer(client, entity, chunk) - return factory.invoke(client, entity, chunk) - } - - init { - registerRenderer(::ItemRenderer) - } - } -} - diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt deleted file mode 100644 index ab025bfb..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt +++ /dev/null @@ -1,22 +0,0 @@ -package ru.dbotthepony.kstarbound.client.render.entity - -import ru.dbotthepony.kstarbound.client.world.ClientChunk -import ru.dbotthepony.kstarbound.client.StarboundClient -import ru.dbotthepony.kstarbound.world.entities.ItemEntity -import ru.dbotthepony.kvector.arrays.Matrix4fStack - -class ItemRenderer(client: StarboundClient, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(client, entity, chunk) { - private val def = entity.def - private val textures = def.inventoryIcon?.stream()?.map { it.image }?.toList() ?: listOf() - - override fun render(stack: Matrix4fStack) { - //client.programs.positionTexture.use() - //client.programs.positionTexture.texture0 = 0 - - //for (texture in textures) { - // client.textures2D[0] = texture.image?.texture ?: continue - //} - - entity.hitboxes.forEach { it.render(client) } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt index 055b6e1a..afa985c2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt @@ -1,9 +1,7 @@ package ru.dbotthepony.kstarbound.client.world -import ru.dbotthepony.kstarbound.client.render.entity.EntityRenderer import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ChunkPos -import ru.dbotthepony.kstarbound.world.entities.Entity class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk(world, pos){ override fun foregroundChanges(cell: Cell) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index b265c93a..2041609c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -92,7 +92,7 @@ class ClientWorld( isDirty = false - currentBakeTask = client.backgroundExecutor.submit(Callable { + currentBakeTask = client.executor.submit(Callable { val meshes = LayeredRenderer(client) for (x in 0 until renderRegionWidth) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt index 30004c53..66ab1b59 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt @@ -1,10 +1,8 @@ package ru.dbotthepony.kstarbound.defs.image import com.github.benmanes.caffeine.cache.AsyncLoadingCache -import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.Caffeine -import com.github.benmanes.caffeine.cache.LoadingCache import com.github.benmanes.caffeine.cache.Scheduler import com.google.common.collect.ImmutableList import com.google.gson.JsonArray @@ -35,7 +33,6 @@ import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector4i import java.io.BufferedInputStream import java.io.FileNotFoundException -import java.lang.ref.Cleaner import java.lang.ref.WeakReference import java.nio.ByteBuffer import java.time.Duration @@ -131,7 +128,7 @@ class Image private constructor( tex.textureMinFilter = GL45.GL_NEAREST tex.textureMagFilter = GL45.GL_NEAREST - }, client.foregroundExecutor) + }, client.mailbox) tex } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ManualExecutorService.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt similarity index 64% rename from src/main/kotlin/ru/dbotthepony/kstarbound/util/ManualExecutorService.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt index 720f62d7..b38ed14e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ManualExecutorService.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt @@ -1,17 +1,19 @@ package ru.dbotthepony.kstarbound.util import com.google.common.util.concurrent.Futures -import it.unimi.dsi.fastutil.objects.ObjectHeapPriorityQueue import java.util.LinkedList import java.util.concurrent.Callable import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.Delayed import java.util.concurrent.Future import java.util.concurrent.FutureTask +import java.util.concurrent.RejectedExecutionException import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit import java.util.concurrent.locks.LockSupport +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock private fun > LinkedList.enqueue(value: E) { if (isEmpty()) { @@ -35,7 +37,7 @@ private fun > LinkedList.enqueue(value: E) { } } -class ManualExecutorService(val thread: Thread = Thread.currentThread()) : ScheduledExecutorService { +class MailboxExecutorService(val thread: Thread = Thread.currentThread()) : ScheduledExecutorService { private val executeQueue = ConcurrentLinkedQueue() private val futureQueue = ConcurrentLinkedQueue>() private val timerBacklog = ConcurrentLinkedQueue>() @@ -43,6 +45,12 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched private val timers = LinkedList>() private val repeatableTimers = LinkedList() + private val executionLock = ReentrantLock() + + @Volatile + private var isShutdown = false + @Volatile + private var isTerminated = false private val timeOrigin = JVMTimeSource() @@ -94,84 +102,108 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched fun executeQueuedTasks() { check(isSameThread()) { "Trying to execute queued tasks in thread ${Thread.currentThread()}, while correct thread is $thread" } - var next = executeQueue.poll() + if (isShutdown) { + if (!isTerminated) { + isTerminated = true - while (next != null) { - next.run() - next = executeQueue.poll() + executionLock.withLock { + timers.clear() + repeatableTimers.clear() + } + + return + } } - var next2 = futureQueue.poll() + executionLock.withLock { + var next = executeQueue.poll() - while (next2 != null) { - next2.run() - Thread.interrupted() - next2 = futureQueue.poll() - } + while (next != null) { + if (isTerminated) return + next.run() + next = executeQueue.poll() + } - var next3 = timerBacklog.poll() + var next2 = futureQueue.poll() - while (next3 != null) { - if (next3.isCancelled) { - // do nothing - } else if (next3.executeAt <= timeOrigin.nanos) { - next3.run() + while (next2 != null) { + if (isTerminated) return + next2.run() Thread.interrupted() - } else { - timers.enqueue(next3) + next2 = futureQueue.poll() } - next3 = timerBacklog.poll() - } + var next3 = timerBacklog.poll() - var next4 = repeatableTimersBacklog.poll() + while (next3 != null) { + if (isTerminated) return + if (next3.isCancelled) { + // do nothing + } else if (next3.executeAt <= timeOrigin.nanos) { + next3.run() + Thread.interrupted() + } else { + timers.enqueue(next3) + } - while (next4 != null) { - if (next4.isCancelled) { - // do nothing - } else { - repeatableTimers.enqueue(next4) + next3 = timerBacklog.poll() } - next4 = repeatableTimersBacklog.poll() - } + var next4 = repeatableTimersBacklog.poll() - while (!timers.isEmpty()) { - val first = timers.first + while (next4 != null) { + if (isTerminated) return - if (first.isCancelled) { - timers.removeFirst() - } else if (first.executeAt <= timeOrigin.nanos) { - first.run() - Thread.interrupted() - timers.removeFirst() - } else { - break + if (next4.isCancelled) { + // do nothing + } else { + repeatableTimers.enqueue(next4) + } + + next4 = repeatableTimersBacklog.poll() } - } - if (repeatableTimers.isNotEmpty()) { - val executed = LinkedList() + while (!timers.isEmpty()) { + if (isTerminated) return + val first = timers.first - while (repeatableTimers.isNotEmpty()) { - val first = repeatableTimers.first - - if (first.isDone) { - repeatableTimers.removeFirst() - } else if (first.next <= timeOrigin.nanos) { - first.runAndReset() - executed.add(first) - repeatableTimers.removeFirst() + if (first.isCancelled) { + timers.removeFirst() + } else if (first.executeAt <= timeOrigin.nanos) { + first.run() + Thread.interrupted() + timers.removeFirst() } else { break } } - executed.forEach { repeatableTimers.enqueue(it) } + if (repeatableTimers.isNotEmpty()) { + val executed = LinkedList() + + while (repeatableTimers.isNotEmpty()) { + if (isTerminated) return + val first = repeatableTimers.first + + if (first.isDone) { + repeatableTimers.removeFirst() + } else if (first.next <= timeOrigin.nanos) { + first.runAndReset() + executed.add(first) + repeatableTimers.removeFirst() + } else { + break + } + } + + executed.forEach { repeatableTimers.enqueue(it) } + } } } override fun execute(command: Runnable) { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") + if (isSameThread()) { command.run() } else { @@ -181,19 +213,42 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched } override fun shutdown() { - throw UnsupportedOperationException() + isShutdown = true } override fun shutdownNow(): MutableList { - throw UnsupportedOperationException() + isShutdown = true + isTerminated = true + + val result = ArrayList() + + executionLock.withLock { + executeQueue.forEach { result.add(it) } + executeQueue.clear() + + futureQueue.forEach { + it.cancel(false) + result.add(it) + } + + futureQueue.clear() + + timers.forEach { it.cancel(false) } + repeatableTimers.forEach { it.cancel(false) } + + timers.clear() + repeatableTimers.clear() + } + + return result } override fun isShutdown(): Boolean { - return false + return isShutdown } override fun isTerminated(): Boolean { - return false + return isTerminated } override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean { @@ -201,21 +256,26 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched } override fun submit(task: Callable): Future { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") if (isSameThread()) return Futures.immediateFuture(task.call()) return FutureTask(task).also { futureQueue.add(it); LockSupport.unpark(thread) } } override fun submit(task: Runnable, result: T): Future { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") if (isSameThread()) { task.run(); return Futures.immediateFuture(result) } return FutureTask { task.run(); result }.also { futureQueue.add(it); LockSupport.unpark(thread) } } override fun submit(task: Runnable): Future<*> { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") if (isSameThread()) { task.run(); return Futures.immediateVoidFuture() } return FutureTask { task.run() }.also { futureQueue.add(it); LockSupport.unpark(thread) } } override fun invokeAll(tasks: Collection>): List> { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") + if (isSameThread()) { return tasks.map { Futures.immediateFuture(it.call()) } } else { @@ -228,6 +288,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched timeout: Long, unit: TimeUnit ): List> { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") + if (isSameThread()) { return tasks.map { Futures.immediateFuture(it.call()) } } else { @@ -239,6 +301,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched if (tasks.isEmpty()) throw NoSuchElementException("Provided task list is empty") + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") + if (isSameThread()) { return tasks.first().call() } else { @@ -250,6 +314,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched if (tasks.isEmpty()) throw NoSuchElementException("Provided task list is empty") + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") + if (isSameThread()) { return tasks.first().call() } else { @@ -258,6 +324,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched } fun join(future: Future): V { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") + if (!isSameThread()) return future.get() @@ -270,6 +338,7 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched } override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") val timer = Timer({ command.run() }, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit)) if (isSameThread() && delay <= 0L) { @@ -285,6 +354,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched } override fun schedule(callable: Callable, delay: Long, unit: TimeUnit): ScheduledFuture { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") + val timer = Timer(callable, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit)) if (isSameThread() && delay <= 0L) { @@ -305,6 +376,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched period: Long, unit: TimeUnit ): ScheduledFuture<*> { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") + return RepeatableTimer( command, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit), @@ -317,6 +390,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched delay: Long, unit: TimeUnit ): ScheduledFuture<*> { + if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down") + return RepeatableTimer( command, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ParallelPerform.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ParallelPerform.kt new file mode 100644 index 00000000..fa86cb21 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ParallelPerform.kt @@ -0,0 +1,21 @@ +package ru.dbotthepony.kstarbound.util + +import java.util.Spliterator +import java.util.concurrent.RecursiveAction + +class ParallelPerform(val spliterator: Spliterator, val action: (E) -> Unit, val targetSize: Long = 32L) : RecursiveAction() { + override fun compute() { + if (spliterator.estimateSize() <= targetSize) { + spliterator.forEachRemaining(action) + } else { + val split = spliterator.trySplit() ?: return spliterator.forEachRemaining(action) + + val task0 = ParallelPerform(spliterator, action, targetSize) + val task1 = ParallelPerform(split, action, targetSize) + task0.fork() + task1.fork() + task0.join() + task1.join() + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 3ad2f794..73678d63 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -17,7 +17,9 @@ import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.Vector2d import java.util.* +import java.util.concurrent.locks.ReentrantLock import kotlin.collections.ArrayList +import kotlin.concurrent.withLock /** * Чанк мира @@ -90,11 +92,6 @@ abstract class Chunk, This : Chunk() - private val collisionCacheView = Collections.unmodifiableCollection(collisionCache) - protected open fun foregroundChanges(cell: Cell) { cellChanges(cell) tileChangeset++ @@ -264,42 +261,48 @@ abstract class Chunk, This : Chunk) { - if (otherChunk == this) - throw IllegalArgumentException("what?") + world.entityMoveLock.withLock { + if (otherChunk == this) + throw IllegalArgumentException("what?") - if (this::class.java != otherChunk::class.java) { - throw IllegalArgumentException("Incompatible types: $this !is $otherChunk") - } + if (this::class.java != otherChunk::class.java) { + throw IllegalArgumentException("Incompatible types: $this !is $otherChunk") + } - if (!entities.add(entity)) { - throw IllegalArgumentException("Already containing $entity") - } + if (!entities.add(entity)) { + throw IllegalArgumentException("Already containing $entity") + } - changeset++ - onEntityTransferedToThis(entity, otherChunk as This) - otherChunk.onEntityTransferedFromThis(entity, this as This) + changeset++ + onEntityTransferedToThis(entity, otherChunk as This) + otherChunk.onEntityTransferedFromThis(entity, this as This) - if (!otherChunk.entities.remove(entity)) { - throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer") + if (!otherChunk.entities.remove(entity)) { + throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer") + } } } fun removeEntity(entity: Entity) { - if (!entities.remove(entity)) { - throw IllegalArgumentException("Already not having entity $entity") - } + world.entityMoveLock.withLock { + if (!entities.remove(entity)) { + throw IllegalArgumentException("Already not having entity $entity") + } - changeset++ - onEntityRemoved(entity) + changeset++ + onEntityRemoved(entity) + } } override fun toString(): String { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 3d199b84..8b02dbfe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -1,20 +1,17 @@ package ru.dbotthepony.kstarbound.world -import com.google.common.base.Predicate import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet -import ru.dbotthepony.kstarbound.client.world.ClientWorld import ru.dbotthepony.kstarbound.math.* -import ru.dbotthepony.kstarbound.util.ManualExecutorService +import ru.dbotthepony.kstarbound.util.MailboxExecutorService +import ru.dbotthepony.kstarbound.util.ParallelPerform import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.physics.CollisionPoly -import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kvector.api.IStruct2d import ru.dbotthepony.kvector.api.IStruct2i @@ -24,6 +21,8 @@ import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2i import java.lang.ref.ReferenceQueue import java.lang.ref.WeakReference +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.locks.ReentrantLock import java.util.random.RandomGenerator @Suppress("UNCHECKED_CAST") @@ -47,7 +46,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk = Predicate { !it.foreground.material.collisionKind.isEmpty }): Boolean { - val tiles = aabb.encasingIntAABB() - - for (x in tiles.mins.x .. tiles.maxs.x) { - for (y in tiles.mins.y .. tiles.maxs.y) { - if (predicate.test(getCell(x, y) ?: continue)) { - return true - } - } - } - - return false - } - fun queryCollisions(aabb: AABB): MutableList { val result = ArrayList() val tiles = aabb.encasingIntAABB() @@ -287,12 +273,18 @@ abstract class World, ChunkType : Chunk get() { - return listOf(rect + Vector2d(this.x.toDouble(), this.y.toDouble())) - } - val foreground: ITileState val background: ITileState val liquid: ILiquidState diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt index 7e8bdc7e..677dcdf3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt @@ -4,12 +4,16 @@ import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.defs.BaseMovementParameters +import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity +import ru.dbotthepony.kstarbound.util.MailboxExecutorService import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.physics.CollisionPoly +import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.Vector2d +import java.util.EnumSet abstract class Entity(val world: World<*, *>) { var chunk: Chunk<*, *>? = null @@ -49,6 +53,7 @@ abstract class Entity(val world: World<*, *>) { val old = field field = Vector2d(world.x.cell(value.x), world.y.cell(value.y)) + physicsSleepTicks = 0 if (isSpawned && !isRemoved) { val oldChunkPos = world.chunkFromCell(old) @@ -61,7 +66,22 @@ abstract class Entity(val world: World<*, *>) { } var velocity = Vector2d.ZERO - val hitboxes = ArrayList() + set(value) { + field = value + physicsSleepTicks = 0 + } + + var physicsSleepTicks = 0 + val mailbox = MailboxExecutorService(world.mailbox.thread) + + protected val hitboxes = ArrayList() + protected val collisionFilter: EnumSet = EnumSet.of(CollisionType.NONE) + + /** + * true - whitelist, false - blacklist + */ + protected var collisionFilterMode = false + open var movementParameters: BaseMovementParameters = GlobalDefaults.movementParameters /** @@ -95,6 +115,7 @@ abstract class Entity(val world: World<*, *>) { throw IllegalStateException("Already removed") isRemoved = true + mailbox.shutdownNow() if (isSpawned) { world.entities.remove(this) @@ -102,53 +123,89 @@ abstract class Entity(val world: World<*, *>) { } } + /** + * this function is executed sequentially + */ fun think() { if (!isSpawned) { throw IllegalStateException("Tried to think before spawning in world") } - move() + mailbox.executeQueuedTasks() thinkInner() } protected abstract fun thinkInner() - protected open fun move() { + /** + * this function is executed in parallel + */ + // TODO: Ghost collisions occur, where objects trip on edges + open fun move() { + if (physicsSleepTicks > PHYSICS_TICKS_UNTIL_SLEEP) return + var physicsSleepTicks = physicsSleepTicks velocity += world.gravity * Starbound.TICK_TIME_ADVANCE - position += velocity * Starbound.TICK_TIME_ADVANCE - if (hitboxes.isEmpty()) return + if (hitboxes.isEmpty()) { + position += velocity * Starbound.TICK_TIME_ADVANCE + return + } - for (i in 0 until 10) { - val localHitboxes = hitboxes.map { it + position } - val polies = world.queryCollisions(localHitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(1.0, 1.0)).filter { !it.type.isEmpty } - if (polies.isEmpty()) break + val steps = roundTowardsPositiveInfinity(velocity.length / 30.0 / hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().let { it.width.coerceAtLeast(it.height).coerceAtLeast(0.1) }) + val dt = Starbound.TICK_TIME_ADVANCE / steps - val intersects = ArrayList>() + for (step in 0 until steps) { + position += velocity * dt - localHitboxes.forEach { hitbox -> - polies.forEach { poly -> hitbox.intersect(poly.poly, poly)?.let { intersects.add(it) } } - } + for (i in 0 until 10) { + val localHitboxes = hitboxes.map { it + position } - if (intersects.isEmpty()) - break - else { - val max = intersects.max() - // resolve collision - position += max.vector - // collision response - velocity -= max.axis * velocity.dot(max.axis) + val polies = world.queryCollisions( + localHitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(1.0, 1.0) + ).filter { + if (collisionFilterMode) + it.type in collisionFilter + else + it.type !in collisionFilter + } - val gravityDot = world.gravity.unitVector.dot(max.axis) - // impulse? - velocity += max.data.velocity * gravityDot * Starbound.TICK_TIME_ADVANCE - // friction - velocity *= 1.0 - gravityDot * 0.08 + if (polies.isEmpty()) break + + val intersects = ArrayList>() + + localHitboxes.forEach { hitbox -> + polies.forEach { poly -> hitbox.intersect(poly.poly, poly)?.let { intersects.add(it) } } + } + + if (intersects.isEmpty()) { + break + } else { + val max = intersects.max() + // resolve collision + position += max.vector + // collision response + val response = max.axis * velocity.dot(max.axis * (1.0 + max.data.bounceFactor) * (1.0 + movementParameters.bounceFactor.orElse(0.0))) + velocity -= response + + val gravityDot = world.gravity.unitVector.dot(max.axis) + // impulse? + velocity += max.data.velocity * gravityDot * dt + // friction + velocity *= 1.0 - gravityDot * 0.08 + + onTouch(response, max.axis, max.data) + } } } + + if (velocity.lengthSquared < 0.25) { + physicsSleepTicks++ + } + + this.physicsSleepTicks = physicsSleepTicks } - open fun onTouchSurface(velocity: Vector2d, normal: Vector2d) { + protected open fun onTouch(velocity: Vector2d, normal: Vector2d, poly: CollisionPoly) { } @@ -162,4 +219,8 @@ abstract class Entity(val world: World<*, *>) { open fun hurt(amount: Double): Boolean { return false } + + companion object { + const val PHYSICS_TICKS_UNTIL_SLEEP = 16 + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionPoly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionPoly.kt index ac280142..b0f18f18 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionPoly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionPoly.kt @@ -5,5 +5,6 @@ import ru.dbotthepony.kvector.vector.Vector2d data class CollisionPoly( val poly: Poly, val type: CollisionType, + val bounceFactor: Double = 0.0, val velocity: Vector2d = Vector2d.ZERO ) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt index f875032d..52813f77 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt @@ -1,10 +1,15 @@ package ru.dbotthepony.kstarbound.world.physics enum class CollisionType(val isEmpty: Boolean) { + // not loaded, block collisions by default NULL(true), + // air NONE(true), + // including stairs made of platforms PLATFORM(false), DYNAMIC(false), SLIPPERY(false), - BLOCK(false); + BLOCK(false), + // stairs made out of blocks + BLOCK_SLOPE(false); } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt index dddcd9b9..5515a022 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt @@ -201,7 +201,7 @@ class Poly private constructor(val edges: ImmutableList, val vertices: Imm intersections.add(Penetration(normal, projectOther.component2() - projectThis.component1(), data)) } - if (intersections.last().penetration.absoluteValue <= EPSILON) { + if (intersections.last().penetration == 0.0) { return null } } @@ -251,7 +251,6 @@ class Poly private constructor(val edges: ImmutableList, val vertices: Imm } companion object : TypeAdapterFactory { - const val EPSILON = 0.01 private val identity = Matrix3f.identity() override fun create(gson: Gson, type: TypeToken): TypeAdapter? {