From bdd8f6b6ef3ead6ff3413fe8d21df6ee0cb87972 Mon Sep 17 00:00:00 2001 From: MiniDay <372403923@qq.com> Date: Sun, 8 Feb 2026 16:11:03 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96Skia=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=99=A8=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../airgame/compose/ui/client/ComposeUiMod.kt | 3 ++ .../net/airgame/compose/ui/client/TestUI.kt | 7 +-- .../compose/ui/client/render/ComposeRender.kt | 9 +++- .../compose/ui/client/render/SkiaRender.kt | 49 +++++++++++-------- .../compose/ui/client/screen/ComposeScreen.kt | 32 +++++++----- 5 files changed, 62 insertions(+), 38 deletions(-) diff --git a/src/client/kotlin/net/airgame/compose/ui/client/ComposeUiMod.kt b/src/client/kotlin/net/airgame/compose/ui/client/ComposeUiMod.kt index 79bc4f7..f7394c2 100644 --- a/src/client/kotlin/net/airgame/compose/ui/client/ComposeUiMod.kt +++ b/src/client/kotlin/net/airgame/compose/ui/client/ComposeUiMod.kt @@ -20,12 +20,15 @@ class ComposeUiMod : ClientModInitializer { KeyBindingHelper.registerKeyBinding(key) ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { client -> if (key.wasPressed()) { + val timer = DebugTimer() val screen = ComposeScreen { CenterUI { TestUI() } } + timer.log("new compose ui instance") client.setScreen(screen) + timer.log("set compose ui screen") return@EndTick } }) diff --git a/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt b/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt index 8af090a..403f124 100644 --- a/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt +++ b/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt @@ -1,7 +1,6 @@ package net.airgame.compose.ui.client import androidx.compose.animation.animateContentSize -import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -12,6 +11,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -114,9 +114,6 @@ fun TestUI() { """.trimIndent() ), ) - for (i in 0..100) { - mails.add(Mail("新手启航大礼包来袭$i", "2025-01-14", """test""".trimIndent())) - } Column( modifier = Modifier.size(800.dp, 600.dp).background(Color(0x80808080)), verticalArrangement = Arrangement.spacedBy(3.dp) @@ -190,7 +187,7 @@ fun MainContent(mail: Mail) { } } -class Mail( +data class Mail( val title: String, val sendTime: String, val content: String diff --git a/src/client/kotlin/net/airgame/compose/ui/client/render/ComposeRender.kt b/src/client/kotlin/net/airgame/compose/ui/client/render/ComposeRender.kt index 9b10576..6160e25 100644 --- a/src/client/kotlin/net/airgame/compose/ui/client/render/ComposeRender.kt +++ b/src/client/kotlin/net/airgame/compose/ui/client/render/ComposeRender.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import kotlinx.coroutines.* +import net.airgame.compose.ui.client.DebugTimer import net.minecraft.client.render.RenderLayer import org.jetbrains.skiko.FrameDispatcher import java.util.concurrent.Executors @@ -38,21 +39,25 @@ class ComposeRender { private var resizeMessage: ResizeMessage? = null fun init(width: Int, height: Int, uiScale: Float, fontScale: Float, content: @Composable () -> Unit) { + val timer = DebugTimer() skiaRender.prepareTexture() + timer.log("skia render -> prepare texture") val task = composeScope.async { // 确保渲染线程中的OpenGL状态机已经准备完毕 skiaRender.prepareRender(width, height) - composeCanvas = skiaRender.skiaSurface.canvas.asComposeCanvas() + composeCanvas = skiaRender.surface.canvas.asComposeCanvas() } runBlocking { // 确保 SkiaRender 完全初始化之后,才允许 Minecraft UI 线程继续执行 task.await() } + timer.log("skia render -> prepare render") // 因为修改 composeScene 状态会触发重绘 // 必须先准备好渲染线程,才能设置 composeScene composeScene.density = Density(uiScale, fontScale) composeScene.size = IntSize(width, height) composeScene.setContent(content) + timer.log("compose scene -> init content") } fun render() { @@ -61,7 +66,7 @@ class ComposeRender { if (resizeData != null) { val (width, height, uiScale, fontScale) = resizeData skiaRender.resize(width, height) - composeCanvas = skiaRender.skiaSurface.canvas.asComposeCanvas() + composeCanvas = skiaRender.surface.canvas.asComposeCanvas() composeScene.density = Density(uiScale, fontScale) composeScene.size = IntSize(width, height) } diff --git a/src/client/kotlin/net/airgame/compose/ui/client/render/SkiaRender.kt b/src/client/kotlin/net/airgame/compose/ui/client/render/SkiaRender.kt index c30c6ab..7ab20db 100644 --- a/src/client/kotlin/net/airgame/compose/ui/client/render/SkiaRender.kt +++ b/src/client/kotlin/net/airgame/compose/ui/client/render/SkiaRender.kt @@ -16,10 +16,6 @@ import org.lwjgl.system.MemoryUtil.NULL /** * Skia 离屏渲染 - * - * 这个类不负责线程安全,除了 init 方法需要在 Minecraft UI 线程中调用以外 - * - * 所有绘制方法(render、resize、destroy)需要确保在同一线程进行,调用者应自行保证线程安全 */ class SkiaRender { private var windowId: Long = -1 @@ -30,8 +26,8 @@ class SkiaRender { // Minecraft 直接将这个纹理渲染到窗口(或者世界内)即可 lateinit var output: RenderLayer; private set - lateinit var skiaContext: DirectContext; private set - lateinit var skiaSurface: Surface; private set + lateinit var context: DirectContext; private set + lateinit var surface: Surface; private set /** * 初始化离屏渲染 @@ -69,7 +65,9 @@ class SkiaRender { } /** - * 这个方法必须在渲染线程中调用,否则渲染线程可能因为没有初始化 OpenGL 而无法绘制任何内容 + * 这个方法必须在渲染线程中调用 + * + * 否则渲染线程可能因为没有初始化 OpenGL 而无法绘制任何内容 */ fun prepareRender(width: Int, height: Int) { glfwMakeContextCurrent(windowId) @@ -82,53 +80,64 @@ class SkiaRender { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId, 0) - skiaContext = DirectContext.makeGL() + context = DirectContext.makeGL() val renderTarget = BackendRenderTarget.makeGL( width, height, 0, 8, frameBufferId, GL_RGBA8 ) - skiaSurface = Surface.makeFromBackendRenderTarget( - skiaContext, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB + surface = Surface.makeFromBackendRenderTarget( + context, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB ) ?: throw IllegalStateException("Surface could not be created") } + /** + * 这个方法必须在渲染线程中调用 + */ fun render(renderTask: Runnable) { // 由于在其他线程中渲染,而 OpenGL 的状态机会根据线程储存 // 所以无需对 windowId 做保存与恢复 val oldWindow = glfwGetCurrentContext() glfwMakeContextCurrent(windowId) - skiaSurface.canvas.clear(Color.WHITE) - skiaSurface.canvas.resetMatrix() + surface.canvas.clear(Color.TRANSPARENT) renderTask.run() - skiaContext.flush() + context.flush() glfwMakeContextCurrent(oldWindow) } + /** + * 这个方法必须在渲染线程中调用 + */ fun resize(width: Int, height: Int) { val oldWindow = glfwGetCurrentContext() glfwMakeContextCurrent(windowId) glBindTexture(GL_TEXTURE_2D, textureId) glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL) - skiaSurface.close() + surface.close() val renderTarget = BackendRenderTarget.makeGL( width, height, 0, 8, frameBufferId, GL_RGBA8 ) - skiaSurface = Surface.makeFromBackendRenderTarget( - skiaContext, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB + surface = Surface.makeFromBackendRenderTarget( + context, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB ) ?: throw IllegalStateException("Failed to recreate Surface") glfwMakeContextCurrent(oldWindow) } + /** + * 这个方法必须在渲染线程中调用 + */ fun destroyRender() { - if (::skiaSurface.isInitialized) { - skiaSurface.close() + if (::surface.isInitialized) { + surface.close() } - if (::skiaContext.isInitialized) { - skiaContext.close() + if (::context.isInitialized) { + context.close() } } + /** + * 这个方法必须在 Minecraft UI 线程中调用 + */ fun destroyTexture() { if (textureId != -1) { GlStateManager._deleteTexture(textureId) diff --git a/src/client/kotlin/net/airgame/compose/ui/client/screen/ComposeScreen.kt b/src/client/kotlin/net/airgame/compose/ui/client/screen/ComposeScreen.kt index b3680c0..5b2122a 100644 --- a/src/client/kotlin/net/airgame/compose/ui/client/screen/ComposeScreen.kt +++ b/src/client/kotlin/net/airgame/compose/ui/client/screen/ComposeScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerEventType import net.airgame.compose.ui.client.ComposeUiMod +import net.airgame.compose.ui.client.DebugTimer import net.airgame.compose.ui.client.render.ComposeRender import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawContext @@ -20,7 +21,7 @@ class ComposeScreen( private val windowWidth: Int get() = MinecraftClient.getInstance().window.width private val windowHeight: Int get() = MinecraftClient.getInstance().window.height - private val guiScale: Float + private val uiScale: Float get() { val scale = MinecraftClient.getInstance().options.guiScale.value.toFloat() if (scale <= 0) return 1f @@ -29,15 +30,23 @@ class ComposeScreen( private val fontScale: Float get() = 1f override fun init() { - composeRender.init(windowWidth, windowHeight, guiScale, fontScale) { + val timer = DebugTimer() + composeRender.init(windowWidth, windowHeight, uiScale, fontScale) { composeContent(this) } + timer.log("init compose render") ComposeUiMod.logger.info( - "init: width={} height={} guiScale={} fontScale={}", - windowWidth, windowHeight, guiScale, fontScale + "init: width={} height={} uiScale={} fontScale={}", + windowWidth, windowHeight, uiScale, fontScale ) } + override fun clearAndInit() { + this.clearChildren() + this.blur() + this.setInitialFocus() + } + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { super.render(context, mouseX, mouseY, delta) context.draw { @@ -51,13 +60,14 @@ class ComposeScreen( } override fun resize(client: MinecraftClient, width: Int, height: Int) { - this.width = width - this.height = height - composeRender.resize(windowWidth, windowHeight, guiScale, fontScale) - ComposeUiMod.logger.info( - "resize: width={} height={} guiScale={} fontScale={}", - windowWidth, windowHeight, guiScale, fontScale - ) + MinecraftClient.getInstance().executeTask { + ComposeUiMod.logger.info( + "resize: width={} height={} uiScale={} fontScale={}", + windowWidth, windowHeight, uiScale, fontScale + ) + composeRender.resize(windowWidth, windowHeight, uiScale, fontScale) + } + super.resize(client, width, height) } private fun getMouseButton(button: Int) = when (button) {