From 513c5a17ebba3fb0a06a2420927e07e4160f978c Mon Sep 17 00:00:00 2001 From: Minjae Song Date: Fri, 14 Dec 2018 22:53:25 +0900 Subject: [PATCH] new thread pooling strategy and test program WIP --- src/net/torvald/terrarum/IngameInstance.kt | 21 +- .../terrarum/concurrent/ThreadParallel.kt | 30 ++- .../torvald/terrarum/modulebasegame/Ingame.kt | 2 +- .../gameactors/ActorInventory.kt | 12 +- .../gameworld/WorldSimulator.kt | 6 +- .../torvald/terrarum/tests/NoiseGenerator.kt | 180 ++++++++++++++++++ src/net/torvald/terrarum/tests/UITestPad1.kt | 4 + src/net/torvald/terrarum/ui/UINSMenu.kt | 8 +- 8 files changed, 231 insertions(+), 32 deletions(-) create mode 100644 src/net/torvald/terrarum/tests/NoiseGenerator.kt diff --git a/src/net/torvald/terrarum/IngameInstance.kt b/src/net/torvald/terrarum/IngameInstance.kt index f73dbbbd4..6be484d41 100644 --- a/src/net/torvald/terrarum/IngameInstance.kt +++ b/src/net/torvald/terrarum/IngameInstance.kt @@ -204,7 +204,7 @@ open class IngameInstance(val batch: SpriteBatch) : Screen { fun insertionSortLastElem(arr: ArrayList) { - lock(ReentrantLock()) { + ReentrantLock().lock { var j = arr.lastIndex - 1 val x = arr.last() while (j >= 0 && arr[j] > x) { @@ -215,13 +215,14 @@ open class IngameInstance(val batch: SpriteBatch) : Screen { } } - inline fun lock(lock: Lock, body: () -> Unit) { - lock.lock() - try { - body() - } - finally { - lock.unlock() - } +} + +inline fun Lock.lock(body: () -> Unit) { + this.lock() + try { + body() } -} \ No newline at end of file + finally { + this.unlock() + } +} diff --git a/src/net/torvald/terrarum/concurrent/ThreadParallel.kt b/src/net/torvald/terrarum/concurrent/ThreadParallel.kt index 756c42dfa..f3c11aec0 100644 --- a/src/net/torvald/terrarum/concurrent/ThreadParallel.kt +++ b/src/net/torvald/terrarum/concurrent/ThreadParallel.kt @@ -1,6 +1,10 @@ package net.torvald.terrarum.concurrent import net.torvald.terrarum.Terrarum +import net.torvald.terrarum.lock +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock +import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.collections.ArrayList typealias RunnableFun = () -> Unit @@ -57,7 +61,7 @@ object ThreadParallel { * Primitive locking */ fun allFinished(): Boolean { - pool.forEach { if (it?.state != Thread.State.TERMINATED) return false } + pool.forEach { if (it != null && it.state != Thread.State.TERMINATED) return false } return true } } @@ -71,24 +75,30 @@ object BlockingThreadPool { val threadCount = Terrarum.THREADS // modify this to your taste private val pool: Array = Array(threadCount, { null }) private var tasks: List = ArrayList() - private var allTasksDone = false - private var dispatchedTasks = 0 + @Volatile private var dispatchedTasks = 0 private var threadPrefix = "" + /** @return false on failure (likely the previous jobs not finished), true on success */ fun map(prefix: String, tasks: List) = setTasks(tasks, prefix) - fun setTasks(tasks: List, prefix: String) { + /** @return false on failure (likely the previous jobs not finished), true on success */ + fun setTasks(tasks: List, prefix: String): Boolean { + if (!allFinished()) + return false + this.tasks = tasks dispatchedTasks = 0 threadPrefix = prefix + return true } - fun dequeueTask(): RunnableFun { + private fun dequeueTask(): RunnableFun { dispatchedTasks += 1 return tasks[dispatchedTasks - 1] } + fun startAllWaitForDie() { - while (!allTasksDone) { + while (dispatchedTasks <= tasks.lastIndex) { // marble rolling down the slanted channel-track of threads, if a channel is empty (a task assigned // to the thread is dead) the marble will roll into the channel, and the marble is a task #MarbleMachineX for (i in 0 until threadCount) { @@ -96,7 +106,8 @@ object BlockingThreadPool { // of marbles and put it into an empty channel whenever we encounter one // SO WHAT WE DO is first fill any empty channels: - if (pool[i] == null || pool[i]!!.state == Thread.State.TERMINATED) { + if (dispatchedTasks <= tasks.lastIndex && // because cache invalidation damnit + (pool[i] == null || pool[i]!!.state == Thread.State.TERMINATED)) { pool[i] = Thread(dequeueTask().makeRunnable(), "$threadPrefix-$dispatchedTasks") // thread name index is one-based pool[i]!!.start() } @@ -109,6 +120,11 @@ object BlockingThreadPool { } } + fun allFinished(): Boolean { + pool.forEach { if (it != null && it.state != Thread.State.TERMINATED) return false } + return true + } + private fun RunnableFun.makeRunnable() = Runnable { this.invoke() } } diff --git a/src/net/torvald/terrarum/modulebasegame/Ingame.kt b/src/net/torvald/terrarum/modulebasegame/Ingame.kt index 987d58a1f..722514d2e 100644 --- a/src/net/torvald/terrarum/modulebasegame/Ingame.kt +++ b/src/net/torvald/terrarum/modulebasegame/Ingame.kt @@ -869,7 +869,7 @@ open class Ingame(batch: SpriteBatch) : IngameInstance(batch) { } private fun insertionSortLastElemAV(arr: ArrayList) { // out-projection doesn't work, duh - lock(ReentrantLock()) { + ReentrantLock().lock { var j = arr.lastIndex - 1 val x = arr.last() while (j >= 0 && arr[j] > x) { diff --git a/src/net/torvald/terrarum/modulebasegame/gameactors/ActorInventory.kt b/src/net/torvald/terrarum/modulebasegame/gameactors/ActorInventory.kt index fc08c1011..29115a78c 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameactors/ActorInventory.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameactors/ActorInventory.kt @@ -9,6 +9,7 @@ import net.torvald.terrarum.itemproperties.ItemCodex import net.torvald.terrarum.itemproperties.ItemCodex.ITEM_DYNAMIC import net.torvald.terrarum.itemproperties.ItemCodex.ITEM_WALLS import net.torvald.terrarum.itemproperties.ItemID +import net.torvald.terrarum.lock import net.torvald.terrarum.modulebasegame.Ingame import java.util.* import java.util.concurrent.locks.Lock @@ -253,7 +254,7 @@ class ActorInventory(val actor: Pocketed, var maxCapacity: Int, var capacityMode return itemList[index] } private fun insertionSortLastElem(arr: ArrayList) { - lock(ReentrantLock()) { + ReentrantLock().lock { var j = arr.lastIndex - 1 val x = arr.last() while (j >= 0 && arr[j].item > x.item) { @@ -284,15 +285,6 @@ class ActorInventory(val actor: Pocketed, var maxCapacity: Int, var capacityMode } return -(low + 1) // key not found } - inline fun lock(lock: Lock, body: () -> Unit) { - lock.lock() - try { - body() - } - finally { - lock.unlock() - } - } } data class InventoryPair(val item: GameItem, var amount: Int) \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/gameworld/WorldSimulator.kt b/src/net/torvald/terrarum/modulebasegame/gameworld/WorldSimulator.kt index 0883d9a67..5a987ff60 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameworld/WorldSimulator.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameworld/WorldSimulator.kt @@ -76,8 +76,8 @@ object WorldSimulator { fun moveFluids(delta: Float) { makeFluidMapFromWorld() - //simCompression() - for (y in 1 until fluidMap.size - 1) { + simCompression() + /*for (y in 1 until fluidMap.size - 1) { for (x in 1 until fluidMap[0].size - 1) { val worldX = x + updateXFrom val worldY = y + updateYFrom @@ -98,7 +98,7 @@ object WorldSimulator { fluidNewMap[y + 1][x] += remainingMass } } - } + }*/ fluidmapToWorld() } diff --git a/src/net/torvald/terrarum/tests/NoiseGenerator.kt b/src/net/torvald/terrarum/tests/NoiseGenerator.kt new file mode 100644 index 000000000..b11fb1a59 --- /dev/null +++ b/src/net/torvald/terrarum/tests/NoiseGenerator.kt @@ -0,0 +1,180 @@ +package net.torvald.terrarum.tests + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Input +import com.badlogic.gdx.InputAdapter +import com.badlogic.gdx.ScreenAdapter +import com.badlogic.gdx.backends.lwjgl.LwjglApplication +import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.graphics.Pixmap +import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import com.badlogic.gdx.graphics.glutils.ShaderProgram +import com.sudoplay.joise.Joise +import com.sudoplay.joise.module.ModuleBasisFunction +import com.sudoplay.joise.module.ModuleFractal +import com.sudoplay.joise.module.ModuleScaleDomain +import com.sudoplay.joise.module.ModuleScaleOffset +import net.torvald.random.HQRNG +import net.torvald.terrarum.AppLoader +import net.torvald.terrarum.AppLoader.printdbg +import net.torvald.terrarum.concurrent.BlockingThreadPool +import net.torvald.terrarum.concurrent.RunnableFun +import net.torvald.terrarum.concurrent.ParallelUtils.sliceEvenly +import net.torvald.terrarum.concurrent.ThreadParallel +import net.torvald.terrarum.inUse +import net.torvald.terrarum.modulebasegame.Ingame +import net.torvald.terrarum.roundInt +import kotlin.math.absoluteValue +import kotlin.system.measureNanoTime +import kotlin.system.measureTimeMillis + +/** + * Created by minjaesong on 2018-12-14. + */ +class NoiseGenerator : ScreenAdapter() { + + lateinit var batch: SpriteBatch + lateinit var camera: OrthographicCamera + lateinit var pixmap: Pixmap + lateinit var texture: Texture + private val IMAGE_SIZE = 1024 + private val IMAGE_SIZEF = IMAGE_SIZE.toFloat() + private val IMAGE_SIZED = IMAGE_SIZE.toDouble() + private val RNG = HQRNG() + + override fun show() { + Gdx.input.inputProcessor = NoiseGeneratorController(this) + + batch = SpriteBatch() + camera = OrthographicCamera(AppLoader.appConfig.width.toFloat(), AppLoader.appConfig.height.toFloat()) + + camera.setToOrtho(true, AppLoader.appConfig.width.toFloat(), AppLoader.appConfig.height.toFloat()) + camera.update() + Gdx.gl20.glViewport(0, 0, AppLoader.appConfig.width, AppLoader.appConfig.height) + + pixmap = Pixmap(IMAGE_SIZE, IMAGE_SIZE, Pixmap.Format.RGBA8888) + texture = Texture(1, 1, Pixmap.Format.RGBA8888) + } + + var regenerate = true + + private var pixelsInSingleJob = (IMAGE_SIZE * IMAGE_SIZE) / 16 // CHANGE THIS VALUE HERE + + private val jobsCount: Int + get() = (IMAGE_SIZE * IMAGE_SIZE) / pixelsInSingleJob + private val rawPixelsList: List + get() = (0 until IMAGE_SIZE * IMAGE_SIZE).sliceEvenly(jobsCount) + private fun makeGenFun(seed: Long, index: Int) = { //i: Int -> + val module = ModuleFractal() + module.setType(ModuleFractal.FractalType.BILLOW) + module.setAllSourceBasisTypes(ModuleBasisFunction.BasisType.GRADIENT) + module.setAllSourceInterpolationTypes(ModuleBasisFunction.InterpolationType.QUINTIC) + module.setNumOctaves(10) + module.setFrequency(8.0) + module.seed = seed + + val moduleScale = ModuleScaleOffset() + moduleScale.setSource(module) + moduleScale.setScale(0.5) + moduleScale.setOffset(0.0) + + val noiseModule = Joise(moduleScale) + + for (c in rawPixelsList[index]) { + val x = c % IMAGE_SIZE + val y = c / IMAGE_SIZE + val uvX = x / IMAGE_SIZED + val uvY = y / IMAGE_SIZED + + val noiseValue = noiseModule.get(uvX, uvY).absoluteValue + val rgb = (noiseValue * 255.0).roundInt() + + pixmap.drawPixel(x, y, (rgb shl 24) or (rgb shl 16) or (rgb shl 8) or 0xFF) + } + } + + private var timerStart = 0L + private var timerFired = false + + override fun render(delta: Float) { + Gdx.graphics.setTitle(Ingame.getCanonicalTitle()) + + // regen + if (timerFired && BlockingThreadPool.allFinished()) { + val timeTook = System.currentTimeMillis() - timerStart + timerFired = false + + printdbg(this, "> $timeTook ms") + } + + if (regenerate && BlockingThreadPool.allFinished()) { + printdbg(this, "Reticulating splines...") + + regenerate = false + // don't join while rendering noise + + timerStart = System.currentTimeMillis() + timerFired = true + + val seed = RNG.nextLong() + val jobs = List(jobsCount) { makeGenFun(seed, it) } + BlockingThreadPool.setTasks(jobs, "") + BlockingThreadPool.startAllWaitForDie() + } + + + // render + texture.dispose() + texture = Texture(pixmap) + + batch.inUse { + batch.color = Color.WHITE + batch.draw(texture, 0f, 0f) + } + + } + + override fun pause() { + super.pause() + } + + override fun resume() { + super.resume() + } + + override fun resize(width: Int, height: Int) { + super.resize(width, height) + } + + override fun dispose() { + pixmap.dispose() + texture.dispose() + } +} + +class NoiseGeneratorController(val host: NoiseGenerator) : InputAdapter() { + override fun keyDown(keycode: Int): Boolean { + if (keycode == Input.Keys.SPACE) { + host.regenerate = true + } + return true + } +} + +fun main(args: Array) { + ShaderProgram.pedantic = false + + val appConfig = LwjglApplicationConfiguration() + appConfig.vSyncEnabled = false + appConfig.resizable = false//true; + appConfig.width = 1024 + appConfig.height = 1024 + appConfig.backgroundFPS = 9999 + appConfig.foregroundFPS = 9999 + appConfig.forceExit = false + + LwjglApplication(AppLoader(appConfig, NoiseGenerator()), appConfig) +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/tests/UITestPad1.kt b/src/net/torvald/terrarum/tests/UITestPad1.kt index 14ead40b1..24ae9e38e 100644 --- a/src/net/torvald/terrarum/tests/UITestPad1.kt +++ b/src/net/torvald/terrarum/tests/UITestPad1.kt @@ -9,6 +9,7 @@ import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.glutils.ShaderProgram import net.torvald.terrarum.* +import net.torvald.terrarum.modulebasegame.Ingame import net.torvald.terrarum.ui.UINSMenu /** @@ -83,6 +84,9 @@ class UITestPad1 : ScreenAdapter() { var _dct = 0f override fun render(delta: Float) { + Gdx.graphics.setTitle(Ingame.getCanonicalTitle()) + + // UPDATE nsMenu.update(delta) diff --git a/src/net/torvald/terrarum/ui/UINSMenu.kt b/src/net/torvald/terrarum/ui/UINSMenu.kt index 27f7cdd89..f35893bf1 100644 --- a/src/net/torvald/terrarum/ui/UINSMenu.kt +++ b/src/net/torvald/terrarum/ui/UINSMenu.kt @@ -111,7 +111,13 @@ class UINSMenu( } private fun popSubMenu() { - if (listStack.size == 1) throw Error("Tried to pop root menu") + if (listStack.size == 1) { + System.err.println("[UINSMenu] Tried to pop root menu") + Thread.currentThread().getStackTrace().forEach { + System.err.println(it) + } + return + } val poppedUIItem = listStack.pop() width -= poppedUIItem.ui.width