diff --git a/src/net/torvald/terrarum/App.java b/src/net/torvald/terrarum/App.java index 23fefab66..780acb759 100644 --- a/src/net/torvald/terrarum/App.java +++ b/src/net/torvald/terrarum/App.java @@ -363,6 +363,11 @@ public class App implements ApplicationListener { public static Thread audioManagerThread; + private volatile Thread postInitLoadingThread; + private volatile boolean loadingThreadDone = false; + private volatile boolean loadingThreadNoModules = false; + private volatile boolean postLoadInitDone = false; + public static long loadedTime_t; public static void updateBogoflops(long iteration) { @@ -723,6 +728,21 @@ public class App implements ApplicationListener { if (loadTimer >= showupTime) { firePostInit(); + } + + // Process resource loading requests from the loading thread + CommonResourcePool.INSTANCE.update(); + + // When loading thread is done and all resources are loaded, finish init + show title + if (loadingThreadDone && CommonResourcePool.INSTANCE.getLoaded()) { + if (!postLoadInitDone) { + if (loadingThreadNoModules) { + postLoadInitDone = true; + } + else { + postLoadInit(); + } + } // hand over the scene control to this single class; Terrarum must call // 'AppLoader.getINSTANCE().screen.render(delta)', this is not redundant at all! @@ -1187,6 +1207,8 @@ public class App implements ApplicationListener { shaderColLUT = loadShaderFromClasspath("shaders/default.vert", "shaders/rgbonly.frag"); shaderGhastlyWhite = loadShaderFromClasspath("shaders/default.vert", "shaders/ghastlywhite.frag"); + printdbg(this, "CommTex+Shader done at "+((System.nanoTime() - t1) / 1000000000.0)+" seconds"); + // make gamepad(s) if (App.getConfigBoolean("usexinput")) { try { @@ -1255,17 +1277,11 @@ public class App implements ApplicationListener { ); Lang.invoke(); - - - ModMgr.INSTANCE.invoke(); // invoke Module Manager - + printdbg(this, "Font done at "+((System.nanoTime() - t1) / 1000000000.0)+" seconds"); fontSmallNumbers = TinyAlphNum.INSTANCE; fontBigNumbers = BigAlphNum.INSTANCE; - IME.invoke(); - inputStrober = InputStrober.INSTANCE; - try { audioDevice = Gdx.audio.newAudioDevice(48000, false); } @@ -1274,7 +1290,43 @@ public class App implements ApplicationListener { System.err.println("[AppLoader] failed to create audio device: Audio device occupied by Exclusive Mode Device? (e.g. ASIO4all)"); } - CommonResourcePool.INSTANCE.loadAll(); + IME.invoke(); + inputStrober = InputStrober.INSTANCE; + printdbg(this, "IME done (loading thread) at "+((System.nanoTime() - t1) / 1000000000.0)+" seconds"); + + // Set GL thread reference for CommonResourcePool dispatch + CommonResourcePool.INSTANCE.setGLThread(Thread.currentThread()); + // Launch loading thread for ModMgr + slow resource loading + postInitLoadingThread = new Thread(() -> { + ModMgr.INSTANCE.invoke(); // triggers module init block + EntryPoint.invoke() calls + + printdbg(this, "ModMgr done (loading thread) at "+((System.nanoTime() - t1) / 1000000000.0)+" seconds"); + + + if (ModMgr.INSTANCE.getModuleInfo().isEmpty()) { + loadingThreadNoModules = true; + loadingThreadDone = true; + return; + } + + CommonResourcePool.INSTANCE.loadAllSlowly(); + + printdbg(this, "loadAllSlowly done (loading thread) at "+((System.nanoTime() - t1) / 1000000000.0)+" seconds"); + + loadingThreadDone = true; + }, "Terrarum-PostInitLoader"); + postInitLoadingThread.start(); + + long t2 = System.nanoTime(); + double tms = (t2 - t1) / 1000000000.0; + printdbg(this, "PostInit done; took "+tms+" seconds"); + } + + /** + * Called on the GL thread after the loading thread finishes and all resources are loaded. + */ + private void postLoadInit() { + long t1 = System.nanoTime(); // check if selected IME is accessible; if not, set selected IME to none String selectedIME = getConfigString("inputmethod"); @@ -1282,18 +1334,8 @@ public class App implements ApplicationListener { setConfig("inputmethod", "none"); } - if (ModMgr.INSTANCE.getModuleInfo().isEmpty()) { - - - - return; - } - - - printdbg(this, "all modules loaded successfully"); - // test print if (IS_DEVELOPMENT_BUILD) { System.out.println("[App] Test printing every registered item"); @@ -1302,7 +1344,6 @@ public class App implements ApplicationListener { System.out.println(); } - try { // create tile atlas printdbg(this, "Making terrain textures..."); @@ -1313,6 +1354,7 @@ public class App implements ApplicationListener { throw new Error("TileMaker failed to load", e); } + printdbg(this, "TileMaker done at "+((System.nanoTime() - t1) / 1000000000.0)+" seconds"); audioBufferSize = getConfigInt("audio_buffer_size"); audioMixer = new AudioMixer(); @@ -1322,8 +1364,11 @@ public class App implements ApplicationListener { audioManagerThread.setPriority(MAX_PRIORITY); // higher = more predictable; audio delay is very noticeable so it gets high priority audioManagerThread.start(); + printdbg(this, "AudioEngine done at "+((System.nanoTime() - t1) / 1000000000.0)+" seconds"); + Terrarum.initialise(); + printdbg(this, "TerrarumInit done at "+((System.nanoTime() - t1) / 1000000000.0)+" seconds"); // if there is a predefined screen, open that screen after my init process if (injectScreen != null) { @@ -1333,14 +1378,11 @@ public class App implements ApplicationListener { IngameRenderer.initialise(); } - hasUpdate = CheckUpdate.INSTANCE.hasUpdate(); printdbg(this, "Has update: " + hasUpdate); - - long t2 = System.nanoTime(); - double tms = (t2 - t1) / 1000000000.0; - printdbg(this, "PostInit done; took "+tms+" seconds"); + postLoadInitDone = true; + printdbg(this, "PostLoadInit done; took "+((System.nanoTime() - t1) / 1000000000.0)+" seconds"); } public static void reloadAudioProcessor(int bufferSize) { diff --git a/src/net/torvald/terrarum/CommonResourcePool.kt b/src/net/torvald/terrarum/CommonResourcePool.kt index 16b3c3ac5..d35a0a497 100644 --- a/src/net/torvald/terrarum/CommonResourcePool.kt +++ b/src/net/torvald/terrarum/CommonResourcePool.kt @@ -4,8 +4,13 @@ import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.TextureRegion import com.badlogic.gdx.utils.Disposable import com.badlogic.gdx.utils.Queue +import net.torvald.terrarum.App.printdbg import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack import net.torvald.unsafe.UnsafePtr +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicInteger /** * Created by minjaesong on 2019-03-10. @@ -13,12 +18,41 @@ import net.torvald.unsafe.UnsafePtr object CommonResourcePool { private val loadingList = Queue() - private val pool = HashMap() + private val pool = ConcurrentHashMap() private val poolKillFun = HashMap Unit)?>() - //private val typesMap = HashMap>() private var loadCounter = -1 // using counters so that the loading can be done on separate thread (gg if the asset requires GL context to be loaded) - val loaded: Boolean // see if there's a thing to load - get() = loadCounter == 0 + val loaded: Boolean + get() = loadCounter <= 0 && slowLoadingRemaining.get() == 0 + + @Volatile private var glThread: Thread? = null + + private val glDispatchQueue = ConcurrentLinkedQueue, CountDownLatch>>() + private val glRunnableQueue = ConcurrentLinkedQueue Unit, CountDownLatch>>() + + private val slowLoadingQueue = ConcurrentLinkedQueue() + private val slowLoadingRemaining = AtomicInteger(0) + + fun setGLThread(thread: Thread) { + glThread = thread + } + + fun isOnGLThread(): Boolean { + return glThread == null || Thread.currentThread() == glThread + } + + /** + * Runs [block] on the GL thread, blocking the calling thread until it completes. + * If already on the GL thread, runs [block] directly. + */ + fun runOnGLThread(block: () -> T): T { + if (isOnGLThread()) return block() + var result: Any? = null + val latch = CountDownLatch(1) + glRunnableQueue.add(Pair({ result = block() }, latch)) + latch.await() + @Suppress("UNCHECKED_CAST") + return result as T + } init { addToLoadingList("itemplaceholder_16") { @@ -80,24 +114,84 @@ object CommonResourcePool { } /** - * Consumes the loading list. After the load, the list will be empty + * Consumes the loading list. After the load, the list will be empty. + * When called from a non-GL thread, dispatches the actual loading to the GL thread and blocks until complete. */ fun loadAll() { if (loaded) return + if (loadingList.isEmpty) return + // Drain the loadingList into a local list + val batch = mutableListOf() while (!loadingList.isEmpty) { - val (name, loadfun, killfun) = loadingList.removeFirst() + batch.add(loadingList.removeFirst()) + } - // no need for the collision checking; quarantine is done when the loading list is being appended - /*if (pool.containsKey(name)) { - throw IllegalArgumentException("Assets with identifier '$name' already exists.") - }*/ + if (isOnGLThread()) { + // Load directly on GL thread + for ((name, loadfun, killfun) in batch) { + pool[name] = loadfun.invoke() + poolKillFun[name] = killfun + loadCounter -= 1 + } + } + else { + // Dispatch to GL thread and block until done + val latch = CountDownLatch(1) + glDispatchQueue.add(batch to latch) + latch.await() + } + } - //typesMap[name] = type + /** + * Moves all pending items in the loading list to the slow loading queue, + * then blocks until the GL thread has processed all of them (one per frame via [update]). + */ + fun loadAllSlowly() { + while (!loadingList.isEmpty) { + val desc = loadingList.removeFirst() + slowLoadingQueue.add(desc) + slowLoadingRemaining.incrementAndGet() + } + // Block until the GL thread has processed all slow items + while (slowLoadingRemaining.get() > 0) { + Thread.sleep(16) + } + } + + /** + * Called every frame from App.render() on the GL thread. + * Processes dispatched loadAll() requests and one slow-loading item per frame. + */ + fun update() { +// printdbg(this, "CommonResPool update!") + // 1. Process all immediate dispatch requests (from loadAll() on background thread) + while (true) { + val request = glDispatchQueue.poll() ?: break + val (batch, latch) = request + for ((name, loadfun, killfun) in batch) { + pool[name] = loadfun.invoke() + poolKillFun[name] = killfun + loadCounter -= 1 + } + latch.countDown() + } + + // 2. Process all generic GL runnables (from runOnGLThread() on background thread) + while (true) { + val (runnable, latch) = glRunnableQueue.poll() ?: break + runnable() + latch.countDown() + } + + // 3. Process one item from the slow loading queue (timesliced) + val desc = slowLoadingQueue.poll() + if (desc != null) { + val (name, loadfun, killfun) = desc pool[name] = loadfun.invoke() poolKillFun[name] = killfun - loadCounter -= 1 + slowLoadingRemaining.decrementAndGet() } } @@ -109,8 +203,15 @@ object CommonResourcePool { fun getOrPut(name: String, loadfun: () -> Any) = CommonResourcePool.getOrPut(name, loadfun, null) fun getOrPut(name: String, loadfun: () -> Any, killfun: ((Any) -> Unit)?): Any { if (pool.containsKey(name)) return pool[name]!! - pool[name] = loadfun.invoke() - poolKillFun[name] = killfun + if (isOnGLThread()) { + pool[name] = loadfun.invoke() + poolKillFun[name] = killfun + } + else { + val latch = CountDownLatch(1) + glDispatchQueue.add(listOf(ResourceLoadingDescriptor(name, loadfun, killfun)) to latch) + latch.await() + } return pool[name]!! } @@ -142,4 +243,4 @@ object CommonResourcePool { val loadfun: () -> Any, val killfun: ((Any) -> Unit)? = null ) -} \ No newline at end of file +} diff --git a/src/net/torvald/terrarum/ModMgr.kt b/src/net/torvald/terrarum/ModMgr.kt index 4dd843ea2..64ec6a11c 100644 --- a/src/net/torvald/terrarum/ModMgr.kt +++ b/src/net/torvald/terrarum/ModMgr.kt @@ -353,7 +353,6 @@ object ModMgr { val newClassInstance = newClassConstructor.newInstance(/* no args defined */) entryPointClasses.add(newClassInstance as ModuleEntryPoint) - (newClassInstance as ModuleEntryPoint).invoke() printdbg(this, "Module loaded successfully: $moduleName") } @@ -414,7 +413,11 @@ object ModMgr { private class ScriptModDisallowedException : RuntimeException("Script Mods disabled") - operator fun invoke() { } + operator fun invoke() { + entryPointClasses.forEach { ep -> + CommonResourcePool.runOnGLThread { ep.invoke() } + } + } /*fun reloadModules() { loadOrder.forEach { @@ -575,7 +578,7 @@ object ModMgr { val loadedClass = Class.forName(className) val loadedClassConstructor = loadedClass.getConstructor(*constructorTypes) try { - return loadedClassConstructor.newInstance(*initArgs) as T + return CommonResourcePool.runOnGLThread { loadedClassConstructor.newInstance(*initArgs) as T } } catch (e: InvocationTargetException) { throw InvocationTargetException(e, "Failed to load class '$className' with given constructor arguments") @@ -585,7 +588,7 @@ object ModMgr { val loadedClass = it.loadClass(className) val loadedClassConstructor = loadedClass.getConstructor(*constructorTypes) try { - return loadedClassConstructor.newInstance(*initArgs) as T + return CommonResourcePool.runOnGLThread { loadedClassConstructor.newInstance(*initArgs) as T } } catch (e: InvocationTargetException) { throw InvocationTargetException(e, "Failed to load class '$className' with given constructor arguments") @@ -601,7 +604,7 @@ object ModMgr { val loadedClass = Class.forName(className) val loadedClassConstructor = loadedClass.getConstructor() try { - return loadedClassConstructor.newInstance() as T + return CommonResourcePool.runOnGLThread { loadedClassConstructor.newInstance() as T } } catch (e: InvocationTargetException) { throw InvocationTargetException(e, "Failed to load class '$className' with zero constructor arguments") @@ -611,7 +614,7 @@ object ModMgr { val loadedClass = it.loadClass(className) val loadedClassConstructor = loadedClass.getConstructor() try { - return loadedClassConstructor.newInstance() as T + return CommonResourcePool.runOnGLThread { loadedClassConstructor.newInstance() as T } } catch (e: InvocationTargetException) { throw InvocationTargetException(e, "Failed to load class '$className' with zero constructor arguments") diff --git a/src/net/torvald/terrarum/gamecontroller/IME.kt b/src/net/torvald/terrarum/gamecontroller/IME.kt index 75399e532..b4cf745b3 100644 --- a/src/net/torvald/terrarum/gamecontroller/IME.kt +++ b/src/net/torvald/terrarum/gamecontroller/IME.kt @@ -95,18 +95,25 @@ object IME { val icons = HashMap() + lateinit var layoutLoadingThread: Thread + private set + init { - context.getBindings("js").putMember("IMEProvider", IMEProviderDelegate(this)) + layoutLoadingThread = Thread({ + context.getBindings("js").putMember("IMEProvider", IMEProviderDelegate(this)) - File(KEYLAYOUT_DIR).listFiles { file, s -> s.endsWith(".$KEYLAYOUT_EXTENSION") }.sortedBy { it.name }.forEach { - printdbg(this, "Registering Low layer ${it.nameWithoutExtension.lowercase()}") - registerLowLayer(it.nameWithoutExtension.lowercase(), parseKeylayoutFile(it)) - } + File(KEYLAYOUT_DIR).listFiles { _, s -> s.endsWith(".$KEYLAYOUT_EXTENSION") }.sortedBy { it.name }.forEach { + printdbg(this, "Registering Low layer ${it.nameWithoutExtension.lowercase()}") + registerLowLayer(it.nameWithoutExtension.lowercase(), parseKeylayoutFile(it)) + } - File(KEYLAYOUT_DIR).listFiles { file, s -> s.endsWith(".$IME_EXTENSION") }.sortedBy { it.name }.forEach { - printdbg(this, "Registering High layer ${it.nameWithoutExtension.lowercase()}") - registerHighLayer(it.nameWithoutExtension.lowercase(), parseImeFile(it)) - } + File(KEYLAYOUT_DIR).listFiles { _, s -> s.endsWith(".$IME_EXTENSION") }.sortedBy { it.name }.forEach { + printdbg(this, "Registering High layer ${it.nameWithoutExtension.lowercase()}") + registerHighLayer(it.nameWithoutExtension.lowercase(), parseImeFile(it)) + } + }, "Terrarum-IMELoader") + layoutLoadingThread.isDaemon = true + layoutLoadingThread.start() val iconSheet = TextureRegionPack(AssetCache.getFileHandle("graphics/gui/ime_icons_by_language.tga"), 20, 20)