refactor: 优化Skia渲染器实现
This commit is contained in:
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user