diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java index 81c544735..17f521400 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java @@ -183,6 +183,7 @@ public List getMixins() { /*mixins.add("skia.Mixin_ScreenshotComposite"); *///? } mixins.add("skia.Mixin_InitSkiaFontRenderer"); + mixins.add("skia.Mixin_StartupWarmupOverlay"); //? if >= 1.21.10 { mixins.add("keybind.Mixin_KeybindCategoryLabel"); diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/skia/Mixin_InitSkiaFontRenderer.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/skia/Mixin_InitSkiaFontRenderer.java index c2805e50c..d6086857b 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/skia/Mixin_InitSkiaFontRenderer.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/skia/Mixin_InitSkiaFontRenderer.java @@ -1,14 +1,26 @@ package org.polyfrost.oneconfig.internal.mixin.skia; import net.minecraft.client.Minecraft; +import net.minecraft.server.packs.resources.ReloadableResourceManager; import org.polyfrost.oneconfig.internal.ui.compose.SkiaFontRenderer; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(Minecraft.class) public class Mixin_InitSkiaFontRenderer { + @Shadow + @Final + private ReloadableResourceManager resourceManager; + + @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/resources/ReloadableResourceManager;createReload(Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;Ljava/util/concurrent/CompletableFuture;Ljava/util/List;)Lnet/minecraft/server/packs/resources/ReloadInstance;")) + private void impl$registerFontReloadListener(CallbackInfo ci) { + this.resourceManager.registerReloadListener(SkiaFontRenderer.INSTANCE); + } + @Inject(method = "", at = @At("TAIL")) void impl$__init__(CallbackInfo ci) { SkiaFontRenderer.INSTANCE.init(); diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/skia/Mixin_StartupWarmupOverlay.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/skia/Mixin_StartupWarmupOverlay.java new file mode 100644 index 000000000..210f68a1b --- /dev/null +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/skia/Mixin_StartupWarmupOverlay.java @@ -0,0 +1,55 @@ +package org.polyfrost.oneconfig.internal.mixin.skia; + +import net.minecraft.client.gui.screens.LoadingOverlay; +//? if >= 1.21.11 { +import net.minecraft.util.Util; +//?} else +//import net.minecraft.Util; +import org.polyfrost.oneconfig.internal.ui.compose.ComposePreloader; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LoadingOverlay.class) +public class Mixin_StartupWarmupOverlay { + @Shadow + @Final + private boolean fadeIn; + + @Shadow + private long fadeOutStart; + + @Unique + private boolean oneconfig$holdingFade; + + @Unique + private long oneconfig$holdStartedNanos; + + @Unique + private static final long ONECONFIG_HOLD_TIMEOUT_NANOS = 15_000_000_000L; + + //~ if >= 26.1 'render' -> 'extractRenderState' + @Inject(method = "extractRenderState", at = @At("HEAD")) + private void oneconfig$holdStartupReveal(CallbackInfo ci) { + // Only hold the initial startup overlay once resource loading has finished + if (this.fadeIn || this.fadeOutStart < 0L) return; + boolean hold = !ComposePreloader.INSTANCE.getStopped(); + if (hold) { + long now = System.nanoTime(); + if (!this.oneconfig$holdingFade) this.oneconfig$holdStartedNanos = now; + if (now - this.oneconfig$holdStartedNanos >= ONECONFIG_HOLD_TIMEOUT_NANOS) { + ComposePreloader.INSTANCE.failStartup("startup held for more than 15 seconds", null); + hold = false; + } + } + if (hold || this.oneconfig$holdingFade) { + // Restart normal fade when warm up finishes + this.fadeOutStart = Util.getMillis(); + } + this.oneconfig$holdingFade = hold; + } +} diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposePreloader.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposePreloader.kt index 8288a4e34..f8ef5e018 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposePreloader.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposePreloader.kt @@ -1,179 +1,68 @@ package org.polyfrost.oneconfig.internal.ui.compose -import net.fabricmc.loader.api.FabricLoader import net.minecraft.client.Minecraft -import net.minecraft.client.gui.screens.ConnectScreen -import net.minecraft.client.gui.screens.LevelLoadingScreen -import net.minecraft.client.gui.screens.ProgressScreen +import net.minecraft.client.gui.screens.LoadingOverlay import org.polyfrost.oneconfig.api.notifications.v1.NotificationsManager -import org.polyfrost.oneconfig.api.platform.v1.Platform -import org.polyfrost.oneconfig.internal.ui.api.ConfigRegistry import org.polyfrost.oneconfig.internal.ui.compose.impls.HudEditorUIScreen import org.polyfrost.oneconfig.internal.ui.compose.impls.OneConfigUIScreen -import org.polyfrost.oneconfig.utils.v1.Multithreading import org.slf4j.LoggerFactory -import java.nio.file.Files -/** - * Builds the OneConfig UI while nobody is waiting for a frame, so that opening it costs nothing - */ object ComposePreloader { private val LOG = LoggerFactory.getLogger(ComposePreloader::class.java) @Volatile private var gpuWarmed = false + private var menuComplete = false + private var warmupStartedNanos = 0L + var stopped = false + private set fun preloadGpuWarmup() { if (gpuWarmed) return gpuWarmed = true NotificationsManager.ensureInitialized() - warmClasses() SkiaCtx.queueWarmup(::warmUp) } - private fun warmClasses() { - val loader = ComposePreloader::class.java.classLoader ?: return - Multithreading.submit { - val startNanos = System.nanoTime() - val names = ourUiClasses() + LAZY_LIBRARY_CLASSES - val warmed = names.count { runCatching { Class.forName(it, false, loader) }.isSuccess } - LOG.info("Warmed {} of {} UI classes in {} ms", warmed, names.size, - (System.nanoTime() - startNanos) / 1_000_000) - } + fun failStartup(reason: String, cause: Throwable? = null) { + if (stopped) return + LOG.warn("OneConfig startup warm-up failed: $reason.", cause) + finish() } - private val WARM_PACKAGES = listOf( - "org/polyfrost/oneconfig/internal/ui/api/", - "org/polyfrost/oneconfig/internal/ui/components/", - "org/polyfrost/oneconfig/internal/ui/hud/", - "org/polyfrost/oneconfig/internal/ui/keybind/", - "org/polyfrost/oneconfig/internal/ui/screens/", - "org/polyfrost/oneconfig/internal/ui/search/", - "org/polyfrost/oneconfig/internal/ui/themes/", - ) - - private fun ourUiClasses(): List = FabricLoader.getInstance().allMods - .filter { it.metadata.id.startsWith("org_polyfrost_oneconfig") } - .flatMap { it.rootPaths } - .flatMap { root -> - runCatching { - Files.walk(root).use { paths -> - paths.map { path -> root.relativize(path).joinToString("/") } - .filter { name -> name.endsWith(".class") && WARM_PACKAGES.any(name::startsWith) } - .map { name -> name.removeSuffix(".class").replace('/', '.') } - .toList() - } - }.getOrDefault(emptyList()) - } - - private val LAZY_LIBRARY_CLASSES = listOf( - "androidx.compose.ui.graphics.SkiaBackedPath_skikoKt", - "androidx.compose.foundation.lazy.LazyListItemProviderKt", - "androidx.compose.ui.text.SkiaParagraph", - "androidx.compose.ui.text.platform.DesktopFont_desktopKt", - "androidx.compose.ui.text.platform.FontCache", - "androidx.compose.foundation.lazy.LazyListKt", - "androidx.compose.foundation.lazy.LazyListMeasureKt", - "androidx.compose.foundation.lazy.LazyListState", - "androidx.compose.foundation.lazy.LazyListMeasuredItem", - "androidx.compose.foundation.lazy.LazyListMeasuredItemProvider", - "androidx.compose.foundation.lazy.LazyListMeasureResult", - "androidx.compose.foundation.lazy.LazyListIntervalContent", - "androidx.compose.foundation.lazy.LazyListItemProviderImpl", - "androidx.compose.foundation.lazy.LazyDslKt", - "androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScopeImpl", - "androidx.compose.foundation.lazy.layout.LazyLayoutItemContentFactory", - "androidx.compose.foundation.lazy.layout.LazySaveableStateHolder", - "androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItemKt", - "kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest", - "kotlinx.coroutines.flow.internal.ChannelFlowOperator", - "kotlinx.coroutines.flow.internal.ChannelFlow", - "kotlinx.coroutines.flow.internal.MergeKt", - "org.commonmark.parser.Parser", - "org.commonmark.internal.DocumentParser", - "org.commonmark.internal.ParagraphParser", - "org.commonmark.internal.LinkReferenceDefinitionParser", - "androidx.compose.ui.text.TextMeasurer", - "androidx.compose.ui.text.TextLayoutCache", - "androidx.compose.ui.text.SpanStyle", - "androidx.compose.ui.text.TextStyle", - "androidx.compose.ui.text.ParagraphStyle", - "androidx.compose.ui.text.ParagraphKt", - "androidx.compose.ui.text.platform.ParagraphBuilder", - "androidx.compose.foundation.text.TextFieldDelegateKt", - "androidx.compose.foundation.text.TextFieldSize", - "androidx.compose.foundation.text.TextFieldScrollKt", - "androidx.compose.foundation.text.KeyMapping_skikoKt", - "androidx.compose.foundation.text.TextFieldKeyInput", - ) - - private data class Inputs(val width: Int, val height: Int, val configs: Int, val inWorld: Boolean) - - private var warmed: Inputs? = null - private var passes = 0 + private fun finish() { + stopped = true + OneConfigUIScreen.endPrewarmShared() + HudEditorUIScreen.endPrewarmShared() + } - private var passNanos = 0L - private var passFrames = 0 + private fun warmUp() { + if (stopped) return - private var worstFrameNanos = 0L - private var deadline = 0L + //? if >= 26.2 { + val overlay = Minecraft.getInstance().gui.overlay() + //?} else + //val overlay = Minecraft.getInstance().overlay - private fun warmUp() { - if (waitDeadline == 0L) waitDeadline = System.nanoTime() + WAIT_NANOS - if (!readyToWarm()) { - if (System.nanoTime() < waitDeadline) SkiaCtx.queueWarmup(::warmUp) + if (overlay !is LoadingOverlay) { + finish() return } - if (deadline == 0L) deadline = System.nanoTime() + WATCH_NANOS - val inputs = Inputs( - width = Platform.screen().windowWidth(), - height = Platform.screen().windowHeight(), - configs = ConfigRegistry.configs.size, - inWorld = Minecraft.getInstance().level != null, - ) - if (inputs != warmed) { - val startNanos = System.nanoTime() - val done = OneConfigUIScreen.prewarmShared() - val frameNanos = System.nanoTime() - startNanos - passNanos += frameNanos - passFrames++ - if (frameNanos > worstFrameNanos) worstFrameNanos = frameNanos - if (done) { - passes++ - HudEditorUIScreen.prewarmShared() - warmed = inputs - LOG.info( - "OneConfig UI warm-up pass {} in {} ms over {} frame(s), worst {} ms ({} configs, {}x{}, {})", - passes, passNanos / 1_000_000, passFrames, worstFrameNanos / 1_000_000, - inputs.configs, inputs.width, inputs.height, - if (inputs.inWorld) "in world" else "no world", - ) - passNanos = 0L - passFrames = 0 - worstFrameNanos = 0L + try { + if (SkiaFontRenderer.isReadyForWarmup()) { + if (warmupStartedNanos == 0L) warmupStartedNanos = System.nanoTime() + if (!menuComplete) { + menuComplete = OneConfigUIScreen.prewarmShared() + } else if (HudEditorUIScreen.prewarmShared()) { + finish() + LOG.info("OneConfig UI warm-up completed in {} ms", (System.nanoTime() - warmupStartedNanos) / 1_000_000) + } } + } catch (t: Throwable) { + failStartup("${t.javaClass.simpleName}: ${t.message}", t) } - if (warmed?.inWorld != true && passes < MAX_PASSES && System.nanoTime() < deadline) { - SkiaCtx.queueWarmup(::warmUp) - } else { - OneConfigUIScreen.endPrewarmShared() - HudEditorUIScreen.endPrewarmShared() - } - } - - private fun readyToWarm(): Boolean { - if (Minecraft.getInstance().level != null) return true - val screen = Platform.screen().current() - return screen is LevelLoadingScreen || screen is ConnectScreen || screen is ProgressScreen + if (!stopped) SkiaCtx.queueWarmup(::warmUp) } - - private const val WAIT_NANOS = 600_000_000_000L - - private var waitDeadline = 0L - - private const val MAX_PASSES = 8 - - private const val WATCH_NANOS = 300_000_000_000L } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt index f453bc7c0..e0b66b6aa 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt @@ -375,10 +375,6 @@ abstract class ComposeScreen( } fun endPrewarm() { - if (prewarmCursor > 0) { - // Release hidden search focus so it doesn't suppress keybinds - withScene { it.focusManager.releaseFocus() } - } prewarmCursor = 0 releasePrewarmSurface() } @@ -386,24 +382,26 @@ abstract class ComposeScreen( protected fun prewarm(frames: Int, budget: Int = frames, step: (Int) -> Unit): Boolean { val name = this::class.java.simpleName if (ensureScene() == null) { - LOGGER.warn("{} warm-up: no scene ({})", name, ComposeSupport.unavailableReason() ?: "createScene failed") + ComposePreloader.failStartup("$name: no scene (${ComposeSupport.unavailableReason() ?: "createScene failed"})") return false } syncSceneMetrics() if (lastSceneW <= 0 || lastSceneH <= 0) { - LOGGER.warn("{} warm-up: window is {}x{}", name, lastSceneW, lastSceneH) closeSceneQuietly() + ComposePreloader.failStartup("$name: window is ${lastSceneW}x${lastSceneH}") return false } val hadContent = contentSet if (!bindContent()) { - LOGGER.warn("{} warm-up: setContent did not take (poisoned={})", name, scenePoisoned) + val reason = "$name: setContent did not take (poisoned=$scenePoisoned)" closeSceneQuietly() + ComposePreloader.failStartup(reason) return false } if (!hadContent) return false val surface = prewarmSurface() ?: run { closeSceneQuietly() + ComposePreloader.failStartup("$name: could not allocate warm-up surface") return false } try { @@ -413,6 +411,7 @@ abstract class ComposeScreen( val scope = renderScopeOrNull if (recomposer == null || scope == null) { closeSceneQuietly() + ComposePreloader.failStartup("$name: missing recomposer or render scope") return false } while (prewarmCursor < until) { @@ -426,7 +425,7 @@ abstract class ComposeScreen( prewarmCursor = 0 releasePrewarmSurface() closeSceneQuietly() - LOGGER.warn("Compose warm-up failed; the first open will build the UI instead", t) + ComposePreloader.failStartup("Compose warm-up failed; the first open will build the UI instead", t) return false } sceneDirty = true diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaFontRenderer.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaFontRenderer.kt index f338e4e01..cea668550 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaFontRenderer.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaFontRenderer.kt @@ -5,7 +5,6 @@ import com.google.gson.JsonParser import net.minecraft.client.Minecraft import net.minecraft.resources.Identifier import net.minecraft.server.packs.resources.PreparableReloadListener -import net.minecraft.server.packs.resources.ReloadableResourceManager import net.minecraft.server.packs.resources.ResourceManager //? < 1.21.4 //import net.minecraft.util.profiling.ProfilerFiller @@ -15,6 +14,7 @@ import org.polyfrost.compose.mc.McFontQueue import org.polyfrost.compose.render.FontManager import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream +import java.util.Optional import java.util.concurrent.CompletableFuture import java.util.concurrent.Executor import java.util.zip.ZipInputStream @@ -118,10 +118,6 @@ object SkiaFontRenderer : PreparableReloadListener { McFontQueue.measureWidth = { text, scale -> measureWidth(text) * scale } McFontQueue.measureHeight = { scale -> LINE_HEIGHT * scale } McFontQueue.renderer = ::draw - val resourceManager = Minecraft.getInstance().resourceManager - if (resourceManager is ReloadableResourceManager) { - resourceManager.registerReloadListener(this) - } ComposePreloader.preloadGpuWarmup() } @@ -189,9 +185,11 @@ object SkiaFontRenderer : PreparableReloadListener { val now = System.currentTimeMillis() if (now - lastLoadAttempt < RETRY_INTERVAL_MS) return lastLoadAttempt = now - if (rebuild(Minecraft.getInstance().resourceManager)) loaded = true + prepare(Minecraft.getInstance().resourceManager, fontOptionsMask())?.let(::applyPrepared) } + internal fun isReadyForWarmup(): Boolean = loaded && builtOptions == fontOptionsMask() + private fun drawGlyphs(canvas: Canvas, text: String, x: Float, y: Float, color: Int, scale: Float, isShadow: Boolean) { var curX = x var curColor = color @@ -476,11 +474,22 @@ object SkiaFontRenderer : PreparableReloadListener { } } - private fun rebuild(rm: ResourceManager): Boolean { - val options = fontOptionsMask() + private class PreparedFont( + val glyphs: Map, + val fast: Array, + val spaces: Map, + val atlases: List, + val unihex: UnihexGlyphs?, + val options: Int, + ) { + fun close() = atlases.forEach { it.close() } + } + + // Uses only local data and CPU-backed Skia images, never the shared GPU context. + private fun prepare(rm: ResourceManager, options: Int): PreparedFont? { val providers = ArrayList() runCatching { collectProviders(rm, ROOT_FONT, providers, HashSet(), options) } - if (providers.isEmpty()) return false + if (providers.isEmpty()) return null val newGlyphs = HashMap(4096) val newSpace = HashMap(4) @@ -488,47 +497,58 @@ object SkiaFontRenderer : PreparableReloadListener { val claimed = HashSet(4096) val hex = HexAccumulator() - for (provider in providers) { - when (provider.get("type")?.asString) { - "space" -> { - val advances = provider.getAsJsonObject("advances") ?: continue - for ((key, value) in advances.entrySet()) { - if (key.isEmpty()) continue - val cp = key.codePointAt(0) - if (claimed.add(cp)) newSpace[cp] = value.asFloat + var prepared = false + try { + for (provider in providers) { + when (provider.get("type")?.asString) { + "space" -> { + val advances = provider.getAsJsonObject("advances") ?: continue + for ((key, value) in advances.entrySet()) { + if (key.isEmpty()) continue + val cp = key.codePointAt(0) + if (claimed.add(cp)) newSpace[cp] = value.asFloat + } } + "bitmap" -> runCatching { loadBitmap(rm, provider, newGlyphs, newAtlases, claimed) } + .onFailure { LOGGER.warn("Failed to load bitmap font provider, skipping", it) } + "unihex" -> runCatching { loadUnihex(rm, provider, hex, claimed) } + .onFailure { LOGGER.warn("Failed to load unihex font provider, skipping", it) } + else -> {} } - "bitmap" -> runCatching { loadBitmap(rm, provider, newGlyphs, newAtlases, claimed) } - .onFailure { LOGGER.warn("Failed to load bitmap font provider, skipping", it) } - "unihex" -> runCatching { loadUnihex(rm, provider, hex, claimed) } - .onFailure { LOGGER.warn("Failed to load unihex font provider, skipping", it) } - else -> {} } - } - if (newGlyphs.isEmpty() && hex.size == 0) return false + if (newGlyphs.isEmpty() && hex.size == 0) return null - val newFast = arrayOfNulls(FAST_GLYPH_LIMIT) - for ((cp, glyph) in newGlyphs) if (cp in 0 until FAST_GLYPH_LIMIT) newFast[cp] = glyph + val newFast = arrayOfNulls(FAST_GLYPH_LIMIT) + for ((cp, glyph) in newGlyphs) if (cp in 0 until FAST_GLYPH_LIMIT) newFast[cp] = glyph + return PreparedFont(newGlyphs, newFast, newSpace, newAtlases, hex.build(), options).also { + prepared = true + } + } finally { + if (!prepared) newAtlases.forEach { it.close() } + } + } + + private fun applyPrepared(prepared: PreparedFont) { val oldAtlases = atlases val oldImages = unihexImages val oldShaders = shaders - glyphs = newGlyphs - glyphFast = newFast - spaceAdvances = if (newSpace.isEmpty()) mapOf(' '.code to 4f) else newSpace - atlases = newAtlases - unihex = hex.build() + glyphs = prepared.glyphs + glyphFast = prepared.fast + spaceAdvances = prepared.spaces.ifEmpty { mapOf(' '.code to 4f) } + atlases = prepared.atlases + unihex = prepared.unihex unihexImages = HashMap() shaders = HashMap() shaderImage = null - builtOptions = options + builtOptions = prepared.options val retiring = ArrayList(oldAtlases.size + oldImages.size + oldShaders.size) retiring.addAll(oldAtlases) retiring.addAll(oldImages.values) retiring.addAll(oldShaders.values) retire(retiring) - return true + loaded = true } private class HexAccumulator { @@ -779,15 +799,20 @@ object SkiaFontRenderer : PreparableReloadListener { //? >= 1.21.10 val resourceManager = sharedState.resourceManager() - return CompletableFuture.supplyAsync({ }, executor) - .thenCompose { + val options = fontOptionsMask() + return CompletableFuture.supplyAsync({ Optional.ofNullable(prepare(resourceManager, options)) }, executor) + .thenCompose { prepared -> //? >= 1.21.10 { - preparationBarrier.wait(Unit) + preparationBarrier.wait(prepared) //? } else - //preparationBarrier!!.wait(Unit) - }.thenAcceptAsync({ - if (rebuild(resourceManager)) { - loaded = true + //preparationBarrier!!.wait(prepared) + .whenComplete { _, failure -> + if (failure != null) prepared.ifPresent { it.close() } + } + }.thenAcceptAsync({ result -> + val prepared = result.orElse(null) + if (prepared != null) { + applyPrepared(prepared) } else { loaded = false lastLoadAttempt = 0L diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/HudEditorUIScreen.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/HudEditorUIScreen.kt index 2d4f94835..bbd3ec6a5 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/HudEditorUIScreen.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/HudEditorUIScreen.kt @@ -17,6 +17,7 @@ import org.polyfrost.oneconfig.api.platform.v1.Platform import org.polyfrost.oneconfig.api.ui.v1.keybind.KeybindManager import org.polyfrost.oneconfig.internal.OneConfigConfig import org.polyfrost.oneconfig.internal.ui.compose.ComposeScreen +import org.polyfrost.oneconfig.internal.ui.compose.ComposePreloader import org.polyfrost.oneconfig.internal.ui.components.RetainedVisibility import org.polyfrost.oneconfig.internal.ui.guiCloseAnimationMillis import org.polyfrost.oneconfig.internal.ui.keybind.KeybindRecordingBus @@ -55,9 +56,9 @@ class HudEditorUIScreen private constructor() : ComposeScreen() { private fun runPrewarm(): Boolean { if (everOpened || Platform.screen().current() === this) return true return try { - prewarm(PREWARM_FRAMES) { } + prewarm(PREWARM_FRAMES, budget = 1) { } } catch (t: Throwable) { - LOGGER.warn("HUD editor warm-up failed; the first open will build it instead", t) + ComposePreloader.failStartup("HUD editor warm-up failed", t) false } } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/OneConfigUIScreen.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/OneConfigUIScreen.kt index f7b49fb1a..7cec3d78c 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/OneConfigUIScreen.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/OneConfigUIScreen.kt @@ -22,6 +22,7 @@ import org.polyfrost.oneconfig.internal.ui.OneConfigInterface import org.polyfrost.oneconfig.internal.ui.components.warmIconCache import org.polyfrost.oneconfig.internal.ui.guiCloseAnimationMillis import org.polyfrost.oneconfig.internal.ui.compose.BlurRenderer +import org.polyfrost.oneconfig.internal.ui.compose.ComposePreloader import org.polyfrost.oneconfig.internal.ui.compose.ComposeScreen import org.polyfrost.oneconfig.internal.ui.compose.SkiaCtx import org.polyfrost.oneconfig.internal.ui.navigation.graph.ModConfigRoute @@ -119,15 +120,19 @@ class OneConfigUIScreen @JvmOverloads constructor( @JvmStatic fun endPrewarmShared() { - sharedScreen?.endPrewarm() + sharedScreen?.let { + if (!it.everOpened && Platform.screen().current() !== it) { + it.restorePrewarmScroll() + it.restorePrewarmNavigation() + } + it.endPrewarm() + } } private const val PREWARM_FRAME_BUDGET = 1 private const val PREWARM_OPEN_FRAME = 1 - private const val PREWARM_FOCUS_FRAME = 2 - private val PREWARM_SCROLL_FRAMES = 5..17 private const val PREWARM_RESTORE_FRAME = 18 @@ -195,34 +200,36 @@ class OneConfigUIScreen @JvmOverloads constructor( @Volatile private var everOpened = false + private fun restorePrewarmScroll() { + scrollModGrid(0) + } + + private fun restorePrewarmNavigation() { + warmRoute(ModsGraph) + LocalNavController.wrapper.reset() + ShellState.lastRoute = null + } + private fun runPrewarm(): Boolean { if (everOpened || Platform.screen().current() === this) return true prewarming = true return try { ConfigRegistry.loadFrom(ConfigManager.active(), ConfigSource.OC) warmIconCache(ConfigRegistry.modCardConfigs.mapNotNull { it.icon }) - var restoreTo = 0 prewarm(PREWARM_FRAMES, PREWARM_FRAME_BUDGET) { frame -> when (frame) { PREWARM_OPEN_FRAME -> requestOpenCallback?.invoke() - PREWARM_FOCUS_FRAME -> ShellState.focusSearchField = true - PREWARM_CLOSE_FRAME -> { - ShellState.focusSearchField = false - ShellState.searchFieldFocused = false - ShellState.searchQuery = "" - requestCloseCallback?.invoke() - } - PREWARM_RESTORE_FRAME -> scrollModGrid(restoreTo) + PREWARM_CLOSE_FRAME -> requestCloseCallback?.invoke() + PREWARM_RESTORE_FRAME -> restorePrewarmScroll() in PREWARM_PAGE_FRAMES -> { val step = frame - PREWARM_PAGE_FRAMES.first if (step % PREWARM_FRAMES_PER_PAGE == 0) { warmRoute(PREWARM_ROUTES[step / PREWARM_FRAMES_PER_PAGE]) } } - PREWARM_FORGET_FRAME -> LocalNavController.wrapper.reset() + PREWARM_FORGET_FRAME -> restorePrewarmNavigation() in PREWARM_SCROLL_FRAMES -> { val grid = ShellState.gridStates[MOD_GRID_KEY] ?: return@prewarm - if (frame == PREWARM_SCROLL_FRAMES.first) restoreTo = grid.firstVisibleItemIndex val last = (grid.layoutInfo.totalItemsCount - 1).coerceAtLeast(0) val step = frame - PREWARM_SCROLL_FRAMES.first val span = PREWARM_SCROLL_FRAMES.last - PREWARM_SCROLL_FRAMES.first @@ -232,7 +239,7 @@ class OneConfigUIScreen @JvmOverloads constructor( } } catch (t: Throwable) { endPrewarm() - LOGGER.warn("OneConfig UI warm-up failed; the first open will build the UI instead", t) + ComposePreloader.failStartup("menu warm-up failed", t) false } finally { prewarming = false diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Icon.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Icon.kt index 2a6d90122..6626b719c 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Icon.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Icon.kt @@ -89,7 +89,8 @@ private object IconResource { }.bytes fun warm(names: Collection) { - val pending = names.filterTo(HashSet()) { !cache.containsKey(it) && warming.add(it) } + val pending = names.asSequence().map { it.toIconResourcePath() } + .filter { !cache.containsKey(it) && warming.add(it) }.toSet() if (pending.isEmpty()) return Multithreading.submit { for (name in pending) { @@ -207,7 +208,7 @@ fun Icon( return } } else { - val icon = rememberAsyncRasterIcon(iconName, lastModified) { file.readBytes() } + val icon = rememberAsyncRasterIcon(iconName, lastModified) { IconResource.fileBytes(file, lastModified) } val imageModifier = modifier.then(iconSizeModifier(null)).clip(DefaultRasterIconShape) if (icon != null) { Image(