async loading (hopefully)

This commit is contained in:
minjaesong
2026-04-04 01:56:40 +09:00
parent 3cbfb5c10f
commit 981418d0c7
4 changed files with 207 additions and 54 deletions

View File

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

View File

@@ -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<ResourceLoadingDescriptor>()
private val pool = HashMap<String, Any>()
private val pool = ConcurrentHashMap<String, Any>()
private val poolKillFun = HashMap<String, ((Any) -> Unit)?>()
//private val typesMap = HashMap<String, Class<*>>()
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<Pair<List<ResourceLoadingDescriptor>, CountDownLatch>>()
private val glRunnableQueue = ConcurrentLinkedQueue<Pair<() -> Unit, CountDownLatch>>()
private val slowLoadingQueue = ConcurrentLinkedQueue<ResourceLoadingDescriptor>()
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 <T> 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<ResourceLoadingDescriptor>()
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
)
}
}

View File

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

View File

@@ -95,18 +95,25 @@ object IME {
val icons = HashMap<String, TextureRegion>()
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)