Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ public List<String> 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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = "<init>", 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 = "<init>", at = @At("TAIL"))
void impl$__init__(CallbackInfo ci) {
SkiaFontRenderer.INSTANCE.init();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> = 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<Any?>()
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -375,35 +375,33 @@ 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()
}

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 {
Expand All @@ -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) {
Expand All @@ -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
Expand Down
Loading
Loading