refactor: 优化Skia渲染器实现

This commit is contained in:
2026-02-08 16:11:03 +08:00
parent 38aca17e70
commit bdd8f6b6ef
5 changed files with 62 additions and 38 deletions

View File

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

View File

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

View File

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

View File

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

View File

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