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"]
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()

View File

@ -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?

View File

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

View File

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

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
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<ClientWorld, ClientChunk>(world, pos){
override fun foregroundChanges(cell: Cell) {

View File

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

View File

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

View File

@ -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 <E : Comparable<E>> LinkedList<E>.enqueue(value: E) {
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 futureQueue = ConcurrentLinkedQueue<FutureTask<*>>()
private val timerBacklog = ConcurrentLinkedQueue<Timer<*>>()
@ -43,6 +45,12 @@ class ManualExecutorService(val thread: Thread = Thread.currentThread()) : Sched
private val timers = LinkedList<Timer<*>>()
private val repeatableTimers = LinkedList<RepeatableTimer>()
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<RepeatableTimer>()
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<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) {
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<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 {
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 <T : Any?> submit(task: Callable<T>): Future<T> {
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 <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) }
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 <T : Any?> invokeAll(tasks: Collection<Callable<T>>): List<Future<T>> {
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<Future<T>> {
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 <V> join(future: Future<V>): 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 <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))
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),

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.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<WorldType : World<WorldType, This>, This : Chunk<WorldType,
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) {
cellChanges(cell)
tileChangeset++
@ -264,42 +261,48 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
protected open fun onEntityRemoved(entity: Entity) { }
fun addEntity(entity: Entity) {
if (!entities.add(entity)) {
throw IllegalArgumentException("Already having having entity $entity")
}
world.entityMoveLock.withLock {
if (!entities.add(entity)) {
throw IllegalArgumentException("Already having having entity $entity")
}
changeset++
onEntityAdded(entity)
changeset++
onEntityAdded(entity)
}
}
fun transferEntity(entity: Entity, otherChunk: 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 {

View File

@ -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<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val background = TileView.Background(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
@ -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 random: RandomGenerator = RandomGenerator.of("Xoroshiro128PlusPlus")
var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)
var ticks = 0
private set
// used by world chunks to synchronize entity additions/transfer
val entityMoveLock = ReentrantLock()
fun think() {
try {
executor.executeQueuedTasks()
mailbox.executeQueuedTasks()
ForkJoinPool.commonPool().submit(ParallelPerform(entities.spliterator(), Entity::move)).join()
mailbox.executeQueuedTasks()
thinkInner()
ticks++
} catch(err: Throwable) {
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
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> {
val result = ArrayList<CollisionPoly>()
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) {
val cell = getCell(x, y) ?: continue
for (poly in cell.polies) {
result.add(CollisionPoly(poly, cell.foreground.material.collisionKind, Vector2d(EARTH_FREEFALL_ACCELERATION, 0.0)))
}
result.add(CollisionPoly(
BLOCK_POLY + Vector2d(x.toDouble(), y.toDouble()),
cell.foreground.material.collisionKind,
//velocity = Vector2d(EARTH_FREEFALL_ACCELERATION, 0.0)
))
}
}
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
}
val polies: Collection<Poly> get() {
return listOf(rect + Vector2d(this.x.toDouble(), this.y.toDouble()))
}
val foreground: ITileState
val background: ITileState
val liquid: ILiquidState

View File

@ -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<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
/**
@ -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<Poly.Penetration<CollisionPoly>>()
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<Poly.Penetration<CollisionPoly>>()
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
}
}

View File

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

View File

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

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))
}
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<Edge>, val vertices: Imm
}
companion object : TypeAdapterFactory {
const val EPSILON = 0.01
private val identity = Matrix3f.identity()
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {