Parallel move(), remove more outdated code, complete MailboxExecutorService

This commit is contained in:
DBotThePony 2023-10-14 22:23:30 +07:00
parent 30b168766d
commit 5fb6d817fc
Signed by: DBot
GPG Key ID: DCC23B5715498507
18 changed files with 312 additions and 239 deletions

View File

@ -120,11 +120,10 @@ fun main() {
//client.world!!.parallax = Starbound.parallaxAccess["garden"] //client.world!!.parallax = Starbound.parallaxAccess["garden"]
val item = Registries.items.values.random()
val rand = Random() val rand = Random()
for (i in 0 .. 128) { 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.position = Vector2d(225.0 - i, 685.0)
item.spawn() item.spawn()

View File

@ -14,7 +14,7 @@ import org.lwjgl.opengl.GLCapabilities
import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kstarbound.LoadingLog 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_UNIT
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -92,7 +92,7 @@ class StarboundClient : Closeable {
// client specific executor which will accept tasks which involve probable // client specific executor which will accept tasks which involve probable
// callback to foreground executor to initialize thread-unsafe data // 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 // 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) { object : ForkJoinWorkerThread(it) {
init { init {
name = "Background Executor for '${thread.name}'-${threadCounter.incrementAndGet()}" name = "Background Executor for '${thread.name}'-${threadCounter.incrementAndGet()}"
@ -108,7 +108,7 @@ class StarboundClient : Closeable {
} }
}, null, false) }, null, false)
val foregroundExecutor = ManualExecutorService(thread) val mailbox = MailboxExecutorService(thread)
val capabilities: GLCapabilities val capabilities: GLCapabilities
var viewportX: Int = 0 var viewportX: Int = 0
@ -322,7 +322,7 @@ class StarboundClient : Closeable {
} }
private fun executeQueuedTasks() { private fun executeQueuedTasks() {
foregroundExecutor.executeQueuedTasks() mailbox.executeQueuedTasks()
var next = openglCleanQueue.poll() as CleanRef? var next = openglCleanQueue.poll() as CleanRef?

View File

@ -18,9 +18,9 @@ class StreamVertexBuilder(
) { ) {
val builder = VertexBuilder(attributes, type, initialCapacity) val builder = VertexBuilder(attributes, type, initialCapacity)
private val vao = client.foregroundExecutor.submit(Callable { VertexArrayObject() }) private val vao = client.mailbox.submit(Callable { VertexArrayObject() })
private val vbo = client.foregroundExecutor.submit(Callable { BufferObject.VBO() }) private val vbo = client.mailbox.submit(Callable { BufferObject.VBO() })
private val ebo = client.foregroundExecutor.submit(Callable { BufferObject.EBO() }) private val ebo = client.mailbox.submit(Callable { BufferObject.EBO() })
private var initialized = false private var initialized = false

View File

@ -49,14 +49,14 @@ class TileRenderers(val client: StarboundClient) {
fun getMaterialRenderer(defName: String): TileRenderer { fun getMaterialRenderer(defName: String): TileRenderer {
return matCache.get(defName) { return matCache.get(defName) {
val def = Registries.tiles[defName] // TODO: Пустой рендерер 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 { fun getModifierRenderer(defName: String): TileRenderer {
return modCache.get(defName) { return modCache.get(defName) {
val def = Registries.tileModifiers[defName] // TODO: Пустой рендерер val def = Registries.tileModifiers[defName] // TODO: Пустой рендерер
client.foregroundExecutor.submit(Callable { TileRenderer(this, def!!.value) }).get() client.mailbox.submit(Callable { TileRenderer(this, def!!.value) }).get()
} }
} }

View File

@ -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<Class<*>, (client: StarboundClient, entity: Entity, chunk: ClientChunk?) -> EntityRenderer>()
@Suppress("unchecked_cast")
fun <T : Entity> registerRenderer(clazz: Class<T>, 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 <reified T : Entity> 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)
}
}
}

View File

@ -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) }
}
}

View File

@ -1,9 +1,7 @@
package ru.dbotthepony.kstarbound.client.world 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.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.entities.Entity
class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos){ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos){
override fun foregroundChanges(cell: Cell) { override fun foregroundChanges(cell: Cell) {

View File

@ -92,7 +92,7 @@ class ClientWorld(
isDirty = false isDirty = false
currentBakeTask = client.backgroundExecutor.submit(Callable { currentBakeTask = client.executor.submit(Callable {
val meshes = LayeredRenderer(client) val meshes = LayeredRenderer(client)
for (x in 0 until renderRegionWidth) { for (x in 0 until renderRegionWidth) {

View File

@ -1,10 +1,8 @@
package ru.dbotthepony.kstarbound.defs.image package ru.dbotthepony.kstarbound.defs.image
import com.github.benmanes.caffeine.cache.AsyncLoadingCache 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.CacheLoader
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
import com.github.benmanes.caffeine.cache.Scheduler import com.github.benmanes.caffeine.cache.Scheduler
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.gson.JsonArray import com.google.gson.JsonArray
@ -35,7 +33,6 @@ import ru.dbotthepony.kvector.vector.Vector2i
import ru.dbotthepony.kvector.vector.Vector4i import ru.dbotthepony.kvector.vector.Vector4i
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.lang.ref.Cleaner
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.time.Duration import java.time.Duration
@ -131,7 +128,7 @@ class Image private constructor(
tex.textureMinFilter = GL45.GL_NEAREST tex.textureMinFilter = GL45.GL_NEAREST
tex.textureMagFilter = GL45.GL_NEAREST tex.textureMagFilter = GL45.GL_NEAREST
}, client.foregroundExecutor) }, client.mailbox)
tex tex
} }

View File

@ -1,17 +1,19 @@
package ru.dbotthepony.kstarbound.util package ru.dbotthepony.kstarbound.util
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import it.unimi.dsi.fastutil.objects.ObjectHeapPriorityQueue
import java.util.LinkedList import java.util.LinkedList
import java.util.concurrent.Callable import java.util.concurrent.Callable
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Delayed import java.util.concurrent.Delayed
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.concurrent.FutureTask import java.util.concurrent.FutureTask
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
private fun <E : Comparable<E>> LinkedList<E>.enqueue(value: E) { private fun <E : Comparable<E>> LinkedList<E>.enqueue(value: E) {
if (isEmpty()) { if (isEmpty()) {
@ -35,7 +37,7 @@ private fun <E : Comparable<E>> LinkedList<E>.enqueue(value: E) {
} }
} }
class ManualExecutorService(val thread: Thread = Thread.currentThread()) : ScheduledExecutorService { class MailboxExecutorService(val thread: Thread = Thread.currentThread()) : ScheduledExecutorService {
private val executeQueue = ConcurrentLinkedQueue<Runnable>() private val executeQueue = ConcurrentLinkedQueue<Runnable>()
private val futureQueue = ConcurrentLinkedQueue<FutureTask<*>>() private val futureQueue = ConcurrentLinkedQueue<FutureTask<*>>()
private val timerBacklog = ConcurrentLinkedQueue<Timer<*>>() private val timerBacklog = ConcurrentLinkedQueue<Timer<*>>()
@ -43,6 +45,12 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
private val timers = LinkedList<Timer<*>>() private val timers = LinkedList<Timer<*>>()
private val repeatableTimers = LinkedList<RepeatableTimer>() private val repeatableTimers = LinkedList<RepeatableTimer>()
private val executionLock = ReentrantLock()
@Volatile
private var isShutdown = false
@Volatile
private var isTerminated = false
private val timeOrigin = JVMTimeSource() private val timeOrigin = JVMTimeSource()
@ -94,84 +102,108 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
fun executeQueuedTasks() { fun executeQueuedTasks() {
check(isSameThread()) { "Trying to execute queued tasks in thread ${Thread.currentThread()}, while correct thread is $thread" } 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) { executionLock.withLock {
next.run() timers.clear()
next = executeQueue.poll() repeatableTimers.clear()
}
return
}
} }
var next2 = futureQueue.poll() executionLock.withLock {
var next = executeQueue.poll()
while (next2 != null) { while (next != null) {
next2.run() if (isTerminated) return
Thread.interrupted() next.run()
next2 = futureQueue.poll() next = executeQueue.poll()
} }
var next3 = timerBacklog.poll() var next2 = futureQueue.poll()
while (next3 != null) { while (next2 != null) {
if (next3.isCancelled) { if (isTerminated) return
// do nothing next2.run()
} else if (next3.executeAt <= timeOrigin.nanos) {
next3.run()
Thread.interrupted() Thread.interrupted()
} else { next2 = futureQueue.poll()
timers.enqueue(next3)
} }
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) { next3 = timerBacklog.poll()
if (next4.isCancelled) {
// do nothing
} else {
repeatableTimers.enqueue(next4)
} }
next4 = repeatableTimersBacklog.poll() var next4 = repeatableTimersBacklog.poll()
}
while (!timers.isEmpty()) { while (next4 != null) {
val first = timers.first if (isTerminated) return
if (first.isCancelled) { if (next4.isCancelled) {
timers.removeFirst() // do nothing
} else if (first.executeAt <= timeOrigin.nanos) { } else {
first.run() repeatableTimers.enqueue(next4)
Thread.interrupted() }
timers.removeFirst()
} else { next4 = repeatableTimersBacklog.poll()
break
} }
}
if (repeatableTimers.isNotEmpty()) { while (!timers.isEmpty()) {
val executed = LinkedList<RepeatableTimer>() if (isTerminated) return
val first = timers.first
while (repeatableTimers.isNotEmpty()) { if (first.isCancelled) {
val first = repeatableTimers.first timers.removeFirst()
} else if (first.executeAt <= timeOrigin.nanos) {
if (first.isDone) { first.run()
repeatableTimers.removeFirst() Thread.interrupted()
} else if (first.next <= timeOrigin.nanos) { timers.removeFirst()
first.runAndReset()
executed.add(first)
repeatableTimers.removeFirst()
} else { } else {
break break
} }
} }
executed.forEach { repeatableTimers.enqueue(it) } if (repeatableTimers.isNotEmpty()) {
val executed = LinkedList<RepeatableTimer>()
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) { override fun execute(command: Runnable) {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { if (isSameThread()) {
command.run() command.run()
} else { } else {
@ -181,19 +213,42 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
} }
override fun shutdown() { override fun shutdown() {
throw UnsupportedOperationException() isShutdown = true
} }
override fun shutdownNow(): MutableList<Runnable> { override fun shutdownNow(): MutableList<Runnable> {
throw UnsupportedOperationException() isShutdown = true
isTerminated = true
val result = ArrayList<Runnable>()
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 { override fun isShutdown(): Boolean {
return false return isShutdown
} }
override fun isTerminated(): Boolean { override fun isTerminated(): Boolean {
return false return isTerminated
} }
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean { override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
@ -201,21 +256,26 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
} }
override fun <T : Any?> submit(task: Callable<T>): Future<T> { override fun <T : Any?> submit(task: Callable<T>): Future<T> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) return Futures.immediateFuture(task.call()) if (isSameThread()) return Futures.immediateFuture(task.call())
return FutureTask(task).also { futureQueue.add(it); LockSupport.unpark(thread) } return FutureTask(task).also { futureQueue.add(it); LockSupport.unpark(thread) }
} }
override fun <T : Any?> submit(task: Runnable, result: T): Future<T> { override fun <T : Any?> submit(task: Runnable, result: T): Future<T> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { task.run(); return Futures.immediateFuture(result) } if (isSameThread()) { task.run(); return Futures.immediateFuture(result) }
return FutureTask { task.run(); result }.also { futureQueue.add(it); LockSupport.unpark(thread) } return FutureTask { task.run(); result }.also { futureQueue.add(it); LockSupport.unpark(thread) }
} }
override fun submit(task: Runnable): Future<*> { override fun submit(task: Runnable): Future<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { task.run(); return Futures.immediateVoidFuture() } if (isSameThread()) { task.run(); return Futures.immediateVoidFuture() }
return FutureTask { task.run() }.also { futureQueue.add(it); LockSupport.unpark(thread) } return FutureTask { task.run() }.also { futureQueue.add(it); LockSupport.unpark(thread) }
} }
override fun <T : Any?> invokeAll(tasks: Collection<Callable<T>>): List<Future<T>> { override fun <T : Any?> invokeAll(tasks: Collection<Callable<T>>): List<Future<T>> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { if (isSameThread()) {
return tasks.map { Futures.immediateFuture(it.call()) } return tasks.map { Futures.immediateFuture(it.call()) }
} else { } else {
@ -228,6 +288,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
timeout: Long, timeout: Long,
unit: TimeUnit unit: TimeUnit
): List<Future<T>> { ): List<Future<T>> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { if (isSameThread()) {
return tasks.map { Futures.immediateFuture(it.call()) } return tasks.map { Futures.immediateFuture(it.call()) }
} else { } else {
@ -239,6 +301,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
if (tasks.isEmpty()) if (tasks.isEmpty())
throw NoSuchElementException("Provided task list is empty") throw NoSuchElementException("Provided task list is empty")
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { if (isSameThread()) {
return tasks.first().call() return tasks.first().call()
} else { } else {
@ -250,6 +314,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
if (tasks.isEmpty()) if (tasks.isEmpty())
throw NoSuchElementException("Provided task list is empty") throw NoSuchElementException("Provided task list is empty")
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { if (isSameThread()) {
return tasks.first().call() return tasks.first().call()
} else { } else {
@ -258,6 +324,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
} }
fun <V> join(future: Future<V>): V { fun <V> join(future: Future<V>): V {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (!isSameThread()) if (!isSameThread())
return future.get() 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<*> { 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)) val timer = Timer({ command.run() }, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit))
if (isSameThread() && delay <= 0L) { if (isSameThread() && delay <= 0L) {
@ -285,6 +354,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
} }
override fun <V : Any?> schedule(callable: Callable<V>, delay: Long, unit: TimeUnit): ScheduledFuture<V> { override fun <V : Any?> schedule(callable: Callable<V>, delay: Long, unit: TimeUnit): ScheduledFuture<V> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
val timer = Timer(callable, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit)) val timer = Timer(callable, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit))
if (isSameThread() && delay <= 0L) { if (isSameThread() && delay <= 0L) {
@ -305,6 +376,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
period: Long, period: Long,
unit: TimeUnit unit: TimeUnit
): ScheduledFuture<*> { ): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
return RepeatableTimer( return RepeatableTimer(
command, command,
timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit), timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit),
@ -317,6 +390,8 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
delay: Long, delay: Long,
unit: TimeUnit unit: TimeUnit
): ScheduledFuture<*> { ): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
return RepeatableTimer( return RepeatableTimer(
command, command,
timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit), timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit),

View File

@ -0,0 +1,21 @@
package ru.dbotthepony.kstarbound.util
import java.util.Spliterator
import java.util.concurrent.RecursiveAction
class ParallelPerform<E>(val spliterator: Spliterator<E>, 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()
}
}
}

View File

@ -17,7 +17,9 @@ import ru.dbotthepony.kvector.arrays.Object2DArray
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
import java.util.* import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.concurrent.withLock
/** /**
* Чанк мира * Чанк мира
@ -90,11 +92,6 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble()) val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble())
var isPhysicsDirty = false
private val collisionCache = ArrayList<AABB>()
private val collisionCacheView = Collections.unmodifiableCollection(collisionCache)
protected open fun foregroundChanges(cell: Cell) { protected open fun foregroundChanges(cell: Cell) {
cellChanges(cell) cellChanges(cell)
tileChangeset++ tileChangeset++
@ -264,42 +261,48 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
protected open fun onEntityRemoved(entity: Entity) { } protected open fun onEntityRemoved(entity: Entity) { }
fun addEntity(entity: Entity) { fun addEntity(entity: Entity) {
if (!entities.add(entity)) { world.entityMoveLock.withLock {
throw IllegalArgumentException("Already having having entity $entity") if (!entities.add(entity)) {
} throw IllegalArgumentException("Already having having entity $entity")
}
changeset++ changeset++
onEntityAdded(entity) onEntityAdded(entity)
}
} }
fun transferEntity(entity: Entity, otherChunk: Chunk<*, *>) { fun transferEntity(entity: Entity, otherChunk: Chunk<*, *>) {
if (otherChunk == this) world.entityMoveLock.withLock {
throw IllegalArgumentException("what?") if (otherChunk == this)
throw IllegalArgumentException("what?")
if (this::class.java != otherChunk::class.java) { if (this::class.java != otherChunk::class.java) {
throw IllegalArgumentException("Incompatible types: $this !is $otherChunk") throw IllegalArgumentException("Incompatible types: $this !is $otherChunk")
} }
if (!entities.add(entity)) { if (!entities.add(entity)) {
throw IllegalArgumentException("Already containing $entity") throw IllegalArgumentException("Already containing $entity")
} }
changeset++ changeset++
onEntityTransferedToThis(entity, otherChunk as This) onEntityTransferedToThis(entity, otherChunk as This)
otherChunk.onEntityTransferedFromThis(entity, this as This) otherChunk.onEntityTransferedFromThis(entity, this as This)
if (!otherChunk.entities.remove(entity)) { if (!otherChunk.entities.remove(entity)) {
throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer") throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer")
}
} }
} }
fun removeEntity(entity: Entity) { fun removeEntity(entity: Entity) {
if (!entities.remove(entity)) { world.entityMoveLock.withLock {
throw IllegalArgumentException("Already not having entity $entity") if (!entities.remove(entity)) {
} throw IllegalArgumentException("Already not having entity $entity")
}
changeset++ changeset++
onEntityRemoved(entity) onEntityRemoved(entity)
}
} }
override fun toString(): String { override fun toString(): String {

View File

@ -1,20 +1,17 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import com.google.common.base.Predicate
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap 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.ReferenceLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.math.* 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.ICellAccess
import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kvector.api.IStruct2d import ru.dbotthepony.kvector.api.IStruct2d
import ru.dbotthepony.kvector.api.IStruct2i import ru.dbotthepony.kvector.api.IStruct2i
@ -24,6 +21,8 @@ import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import java.lang.ref.ReferenceQueue import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.locks.ReentrantLock
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -47,7 +46,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val background = TileView.Background(this) val background = TileView.Background(this)
val foreground = TileView.Foreground(this) val foreground = TileView.Foreground(this)
val executor = ManualExecutorService() val mailbox = MailboxExecutorService()
final override fun randomLongFor(x: Int, y: Int) = super.randomLongFor(x, y) xor seed final override fun randomLongFor(x: Int, y: Int) = super.randomLongFor(x, y) xor seed
@ -233,17 +232,18 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val chunkMap: ChunkMap = if (size != null) ArrayChunkMap() else HashChunkMap() val chunkMap: ChunkMap = if (size != null) ArrayChunkMap() else HashChunkMap()
val random: RandomGenerator = RandomGenerator.of("Xoroshiro128PlusPlus") val random: RandomGenerator = RandomGenerator.of("Xoroshiro128PlusPlus")
var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION) var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)
var ticks = 0 // used by world chunks to synchronize entity additions/transfer
private set val entityMoveLock = ReentrantLock()
fun think() { fun think() {
try { try {
executor.executeQueuedTasks() mailbox.executeQueuedTasks()
ForkJoinPool.commonPool().submit(ParallelPerform(entities.spliterator(), Entity::move)).join()
mailbox.executeQueuedTasks()
thinkInner() thinkInner()
ticks++
} catch(err: Throwable) { } catch(err: Throwable) {
throw RuntimeException("Ticking world $this", err) throw RuntimeException("Ticking world $this", err)
} }
@ -265,20 +265,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
protected abstract fun chunkFactory(pos: ChunkPos): ChunkType protected abstract fun chunkFactory(pos: ChunkPos): ChunkType
fun testSpace(aabb: AABB, predicate: Predicate<IChunkCell> = 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<CollisionPoly> { fun queryCollisions(aabb: AABB): MutableList<CollisionPoly> {
val result = ArrayList<CollisionPoly>() val result = ArrayList<CollisionPoly>()
val tiles = aabb.encasingIntAABB() val tiles = aabb.encasingIntAABB()
@ -287,12 +273,18 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
for (y in tiles.mins.y .. tiles.maxs.y) { for (y in tiles.mins.y .. tiles.maxs.y) {
val cell = getCell(x, y) ?: continue val cell = getCell(x, y) ?: continue
for (poly in cell.polies) { result.add(CollisionPoly(
result.add(CollisionPoly(poly, cell.foreground.material.collisionKind, Vector2d(EARTH_FREEFALL_ACCELERATION, 0.0))) BLOCK_POLY + Vector2d(x.toDouble(), y.toDouble()),
} cell.foreground.material.collisionKind,
//velocity = Vector2d(EARTH_FREEFALL_ACCELERATION, 0.0)
))
} }
} }
return result return result
} }
companion object {
private val BLOCK_POLY = Poly(listOf(Vector2d.ZERO, Vector2d(0.0, 1.0), Vector2d(1.0, 1.0), Vector2d(1.0, 0.0)))
}
} }

View File

@ -31,10 +31,6 @@ interface IChunkCell : IStruct2i {
return y return y
} }
val polies: Collection<Poly> get() {
return listOf(rect + Vector2d(this.x.toDouble(), this.y.toDouble()))
}
val foreground: ITileState val foreground: ITileState
val background: ITileState val background: ITileState
val liquid: ILiquidState val liquid: ILiquidState

View File

@ -4,12 +4,16 @@ import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.defs.BaseMovementParameters 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.Chunk
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
import java.util.EnumSet
abstract class Entity(val world: World<*, *>) { abstract class Entity(val world: World<*, *>) {
var chunk: Chunk<*, *>? = null var chunk: Chunk<*, *>? = null
@ -49,6 +53,7 @@ abstract class Entity(val world: World<*, *>) {
val old = field val old = field
field = Vector2d(world.x.cell(value.x), world.y.cell(value.y)) field = Vector2d(world.x.cell(value.x), world.y.cell(value.y))
physicsSleepTicks = 0
if (isSpawned && !isRemoved) { if (isSpawned && !isRemoved) {
val oldChunkPos = world.chunkFromCell(old) val oldChunkPos = world.chunkFromCell(old)
@ -61,7 +66,22 @@ abstract class Entity(val world: World<*, *>) {
} }
var velocity = Vector2d.ZERO var velocity = Vector2d.ZERO
val hitboxes = ArrayList<Poly>() set(value) {
field = value
physicsSleepTicks = 0
}
var physicsSleepTicks = 0
val mailbox = MailboxExecutorService(world.mailbox.thread)
protected val hitboxes = ArrayList<Poly>()
protected val collisionFilter: EnumSet<CollisionType> = EnumSet.of(CollisionType.NONE)
/**
* true - whitelist, false - blacklist
*/
protected var collisionFilterMode = false
open var movementParameters: BaseMovementParameters = GlobalDefaults.movementParameters open var movementParameters: BaseMovementParameters = GlobalDefaults.movementParameters
/** /**
@ -95,6 +115,7 @@ abstract class Entity(val world: World<*, *>) {
throw IllegalStateException("Already removed") throw IllegalStateException("Already removed")
isRemoved = true isRemoved = true
mailbox.shutdownNow()
if (isSpawned) { if (isSpawned) {
world.entities.remove(this) world.entities.remove(this)
@ -102,53 +123,89 @@ abstract class Entity(val world: World<*, *>) {
} }
} }
/**
* this function is executed sequentially
*/
fun think() { fun think() {
if (!isSpawned) { if (!isSpawned) {
throw IllegalStateException("Tried to think before spawning in world") throw IllegalStateException("Tried to think before spawning in world")
} }
move() mailbox.executeQueuedTasks()
thinkInner() thinkInner()
} }
protected abstract fun 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 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 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 localHitboxes = hitboxes.map { it + position } val dt = Starbound.TICK_TIME_ADVANCE / steps
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 intersects = ArrayList<Poly.Penetration<CollisionPoly>>() for (step in 0 until steps) {
position += velocity * dt
localHitboxes.forEach { hitbox -> for (i in 0 until 10) {
polies.forEach { poly -> hitbox.intersect(poly.poly, poly)?.let { intersects.add(it) } } val localHitboxes = hitboxes.map { it + position }
}
if (intersects.isEmpty()) val polies = world.queryCollisions(
break localHitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(1.0, 1.0)
else { ).filter {
val max = intersects.max() if (collisionFilterMode)
// resolve collision it.type in collisionFilter
position += max.vector else
// collision response it.type !in collisionFilter
velocity -= max.axis * velocity.dot(max.axis) }
val gravityDot = world.gravity.unitVector.dot(max.axis) if (polies.isEmpty()) break
// impulse?
velocity += max.data.velocity * gravityDot * Starbound.TICK_TIME_ADVANCE val intersects = ArrayList<Poly.Penetration<CollisionPoly>>()
// friction
velocity *= 1.0 - gravityDot * 0.08 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 { open fun hurt(amount: Double): Boolean {
return false return false
} }
companion object {
const val PHYSICS_TICKS_UNTIL_SLEEP = 16
}
} }

View File

@ -5,5 +5,6 @@ import ru.dbotthepony.kvector.vector.Vector2d
data class CollisionPoly( data class CollisionPoly(
val poly: Poly, val poly: Poly,
val type: CollisionType, val type: CollisionType,
val bounceFactor: Double = 0.0,
val velocity: Vector2d = Vector2d.ZERO val velocity: Vector2d = Vector2d.ZERO
) )

View File

@ -1,10 +1,15 @@
package ru.dbotthepony.kstarbound.world.physics package ru.dbotthepony.kstarbound.world.physics
enum class CollisionType(val isEmpty: Boolean) { enum class CollisionType(val isEmpty: Boolean) {
// not loaded, block collisions by default
NULL(true), NULL(true),
// air
NONE(true), NONE(true),
// including stairs made of platforms
PLATFORM(false), PLATFORM(false),
DYNAMIC(false), DYNAMIC(false),
SLIPPERY(false), SLIPPERY(false),
BLOCK(false); BLOCK(false),
// stairs made out of blocks
BLOCK_SLOPE(false);
} }

View File

@ -201,7 +201,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
intersections.add(Penetration(normal, projectOther.component2() - projectThis.component1(), data)) intersections.add(Penetration(normal, projectOther.component2() - projectThis.component1(), data))
} }
if (intersections.last().penetration.absoluteValue <= EPSILON) { if (intersections.last().penetration == 0.0) {
return null return null
} }
} }
@ -251,7 +251,6 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
} }
companion object : TypeAdapterFactory { companion object : TypeAdapterFactory {
const val EPSILON = 0.01
private val identity = Matrix3f.identity() private val identity = Matrix3f.identity()
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {