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