feat: Compose 改用 Dispatchers.Main 渲染

This commit is contained in:
WCPE
2025-01-14 17:54:27 +08:00
parent 4b515e4903
commit 4a4e2aa296
4 changed files with 108 additions and 96 deletions

View File

@@ -1,7 +1,7 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
id("maven-publish") id("maven-publish")
id("fabric-loom") version "1.9-SNAPSHOT" id("fabric-loom") version "1.9.2"
id("org.jetbrains.compose") id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose") id("org.jetbrains.kotlin.plugin.compose")
@@ -36,6 +36,7 @@ loom {
} }
repositories { repositories {
mavenLocal()
mavenCentral() mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google() google()
@@ -47,9 +48,9 @@ dependencies {
mappings("net.fabricmc:yarn:${project.property("yarn_mappings")}:v2") mappings("net.fabricmc:yarn:${project.property("yarn_mappings")}:v2")
modImplementation("net.fabricmc:fabric-loader:${project.property("loader_version")}") modImplementation("net.fabricmc:fabric-loader:${project.property("loader_version")}")
modImplementation("net.fabricmc:fabric-language-kotlin:${project.property("kotlin_loader_version")}") modImplementation("net.fabricmc:fabric-language-kotlin:${project.property("kotlin_loader_version")}")
modImplementation("net.fabricmc.fabric-api:fabric-api:${project.property("fabric_version")}") modImplementation("net.fabricmc.fabric-api:fabric-api:${project.property("fabric_version")}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.9.0")
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
includeInternal(compose.desktop.currentOs) { includeInternal(compose.desktop.currentOs) {
exclude(module = "kotlin-stdlib") exclude(module = "kotlin-stdlib")

View File

@@ -1,5 +1,6 @@
pluginManagement { pluginManagement {
repositories { repositories {
mavenLocal()
maven("https://maven.fabricmc.net/") { maven("https://maven.fabricmc.net/") {
name = "Fabric" name = "Fabric"
} }

View File

@@ -114,6 +114,9 @@ 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)

View File

@@ -11,11 +11,10 @@ import androidx.compose.ui.scene.ComposeScene
import androidx.compose.ui.scene.MultiLayerComposeScene import androidx.compose.ui.scene.MultiLayerComposeScene
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 com.mojang.blaze3d.platform.GlStateManager
import com.mojang.blaze3d.platform.TextureUtil import com.mojang.blaze3d.platform.TextureUtil
import com.mojang.blaze3d.systems.RenderSystem import com.mojang.blaze3d.systems.RenderSystem
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Runnable import kotlinx.coroutines.*
import net.minecraft.client.MinecraftClient import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.Screen
@@ -27,17 +26,24 @@ import net.minecraft.text.Text
import org.jetbrains.skia.* import org.jetbrains.skia.*
import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.FrameDispatcher
import org.lwjgl.glfw.GLFW.* import org.lwjgl.glfw.GLFW.*
import org.lwjgl.opengl.ARBSync.glClientWaitSync
import org.lwjgl.opengl.ARBSync.glDeleteSync
import org.lwjgl.opengl.ARBSync.glFenceSync
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL30.* import org.lwjgl.opengl.GL30.*
import org.lwjgl.opengl.GL32.GL_SYNC_FLUSH_COMMANDS_BIT
import org.lwjgl.opengl.GL32C.GL_SYNC_GPU_COMMANDS_COMPLETE
import org.lwjgl.system.MemoryUtil.NULL import org.lwjgl.system.MemoryUtil.NULL
import kotlin.coroutines.CoroutineContext
@OptIn(InternalComposeUiApi::class) @OptIn(InternalComposeUiApi::class)
class ComposeScreen( class ComposeScreen(
val composeContent: @Composable (ComposeScreen) -> Unit val composeContent: @Composable (ComposeScreen) -> Unit
) : Screen(Text.literal("FrameRenderComposeScreen")) { ) : Screen(Text.literal("FrameRenderComposeScreen")) {
companion object { private val windowWidth: Int get() = MinecraftClient.getInstance().window.width
private val frameBufferID by lazy { GlStateManager.glGenFramebuffers() } private val windowHeight: Int get() = MinecraftClient.getInstance().window.height
private val textureID by lazy { TextureUtil.generateTextureId() }
private var frameBufferID = -1
private val textureID = TextureUtil.generateTextureId()
private val composeLayer: RenderLayer by lazy { private val composeLayer: RenderLayer by lazy {
RenderLayer.of( RenderLayer.of(
"cpu_render_compose", "cpu_render_compose",
@@ -54,91 +60,83 @@ class ComposeScreen(
.build(false) .build(false)
) )
} }
private val windowID by lazy {
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
val oldWindow = glfwGetCurrentContext()
glfwCreateWindow(1, 1, "", NULL, oldWindow)
}
}
private val windowWidth: Int get() = MinecraftClient.getInstance().window.width
private val windowHeight: Int get() = MinecraftClient.getInstance().window.height
private var windowID: Long = -1
private lateinit var directContext: DirectContext private lateinit var directContext: DirectContext
private lateinit var surface: Surface private lateinit var surface: Surface
private lateinit var canvas: Canvas private lateinit var canvas: Canvas
private lateinit var scene: ComposeScene
private val tasks = mutableListOf<Runnable>() private val mainScope = MainScope()
private val tasksCopy = mutableListOf<Runnable>()
private val coroutineDispatcher = object : CoroutineDispatcher() { private val fenceSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)
override fun dispatch(context: CoroutineContext, block: Runnable) { private val needRedraw = atomic(true)
synchronized(tasks) { private val frameDispatcher = FrameDispatcher(mainScope) {
tasks.add(block) needRedraw.value = true
} }
} private val scene: ComposeScene = MultiLayerComposeScene(
} density = Density(MinecraftClient.getInstance().options.guiScale.value.toFloat() / 2),
private val frameDispatcher = FrameDispatcher(coroutineDispatcher) { size = IntSize(windowWidth, windowHeight),
RenderSystem.recordRenderCall { coroutineContext = Dispatchers.Main,
if (surface.isClosed) { invalidate = { frameDispatcher.scheduleFrame() }
return@recordRenderCall ).apply {
} setContent {
val oldWindowID = glfwGetCurrentContext() composeContent(this@ComposeScreen)
glfwMakeContextCurrent(windowID)
surface.canvas.clear(Color.TRANSPARENT)
scene.render(canvas, System.nanoTime())
directContext.flush()
glfwMakeContextCurrent(oldWindowID)
} }
} }
override fun init() { override fun init() {
val oldFrameBuffer = glGetInteger(GL_FRAMEBUFFER_BINDING) val oldWindow = glfwGetCurrentContext()
val oldTexture = glGetInteger(GL_TEXTURE_BINDING_2D) glfwMakeContextCurrent(0)
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
windowID = glfwCreateWindow(1, 1, "", NULL, oldWindow)
val initTask = mainScope.async {
glfwMakeContextCurrent(windowID)
GL.createCapabilities()
frameBufferID = glGenFramebuffers()
glBindFramebuffer(GL_FRAMEBUFFER, frameBufferID) glBindFramebuffer(GL_FRAMEBUFFER, frameBufferID)
glBindTexture(GL_TEXTURE_2D, textureID) glBindTexture(GL_TEXTURE_2D, textureID)
glTexImage2D( glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, windowWidth, windowHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL)
GL_TEXTURE_2D, 0, GL_RGBA, windowWidth, windowHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL
)
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)
glBindFramebuffer(GL_FRAMEBUFFER, oldFrameBuffer)
glBindTexture(GL_TEXTURE_2D, oldTexture)
directContext = DirectContext.makeGL() directContext = DirectContext.makeGL()
val renderTarget = BackendRenderTarget.makeGL( val renderTarget = BackendRenderTarget.makeGL(
windowWidth, windowHeight, 0, 8, frameBufferID, GL_RGBA8 windowWidth, windowHeight, 0, 8, frameBufferID, GL_RGBA8
) )
surface = Surface.makeFromBackendRenderTarget( surface = Surface.makeFromBackendRenderTarget(
directContext, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB directContext, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB
)!! ) ?: throw IllegalStateException("Surface could not be created")
canvas = surface.canvas.asComposeCanvas() canvas = surface.canvas.asComposeCanvas()
scene = MultiLayerComposeScene( glfwMakeContextCurrent(0)
density = Density(MinecraftClient.getInstance().options.guiScale.value.toFloat() / 2),
size = IntSize(windowWidth, windowHeight),
coroutineContext = coroutineDispatcher,
invalidate = { frameDispatcher.scheduleFrame() }
)
scene.setContent {
composeContent(this@ComposeScreen)
} }
super.init() runBlocking {
initTask.await()
}
glfwMakeContextCurrent(oldWindow)
} }
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
synchronized(tasks) { super.render(context, mouseX, mouseY, delta)
tasksCopy.clear() if (needRedraw.compareAndSet(true, false)) {
tasksCopy.addAll(tasks) val oldWindow = glfwGetCurrentContext()
tasks.clear() glfwMakeContextCurrent(0)
val renderTask = mainScope.async {
if (surface.isClosed) {
return@async
} }
for (runnable in tasksCopy) { glfwMakeContextCurrent(windowID)
runnable.run() surface.canvas.clear(Color.TRANSPARENT)
scene.render(canvas, System.nanoTime())
directContext.flush()
glfwMakeContextCurrent(0)
}
runBlocking {
renderTask.await()
}
glfwMakeContextCurrent(oldWindow)
glClientWaitSync(fenceSync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000)
} }
context.draw { context.draw {
val vertexConsumer = it.getBuffer(composeLayer) val vertexConsumer = it.getBuffer(composeLayer)
@@ -163,7 +161,7 @@ class ComposeScreen(
override fun mouseMoved(mouseX: Double, mouseY: Double) { override fun mouseMoved(mouseX: Double, mouseY: Double) {
val x = MinecraftClient.getInstance().mouse.x.toFloat() val x = MinecraftClient.getInstance().mouse.x.toFloat()
val y = MinecraftClient.getInstance().mouse.y.toFloat() val y = MinecraftClient.getInstance().mouse.y.toFloat()
coroutineDispatcher.dispatch(coroutineDispatcher) { mainScope.launch {
scene.sendPointerEvent( scene.sendPointerEvent(
PointerEventType.Move, PointerEventType.Move,
position = Offset(x, y) position = Offset(x, y)
@@ -172,7 +170,7 @@ class ComposeScreen(
} }
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
coroutineDispatcher.dispatch(coroutineDispatcher) { mainScope.launch {
val x = MinecraftClient.getInstance().mouse.x.toFloat() val x = MinecraftClient.getInstance().mouse.x.toFloat()
val y = MinecraftClient.getInstance().mouse.y.toFloat() val y = MinecraftClient.getInstance().mouse.y.toFloat()
scene.sendPointerEvent( scene.sendPointerEvent(
@@ -185,7 +183,7 @@ class ComposeScreen(
} }
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
coroutineDispatcher.dispatch(coroutineDispatcher) { mainScope.launch {
val x = MinecraftClient.getInstance().mouse.x.toFloat() val x = MinecraftClient.getInstance().mouse.x.toFloat()
val y = MinecraftClient.getInstance().mouse.y.toFloat() val y = MinecraftClient.getInstance().mouse.y.toFloat()
scene.sendPointerEvent( scene.sendPointerEvent(
@@ -203,7 +201,7 @@ class ComposeScreen(
val x = MinecraftClient.getInstance().mouse.x.toFloat() val x = MinecraftClient.getInstance().mouse.x.toFloat()
val y = MinecraftClient.getInstance().mouse.y.toFloat() val y = MinecraftClient.getInstance().mouse.y.toFloat()
println("mouseScrolled: $x $y $horizontalAmount $verticalAmount") println("mouseScrolled: $x $y $horizontalAmount $verticalAmount")
coroutineDispatcher.dispatch(coroutineDispatcher) { mainScope.launch {
scene.sendPointerEvent( scene.sendPointerEvent(
PointerEventType.Scroll, PointerEventType.Scroll,
button = PointerButton.Tertiary, button = PointerButton.Tertiary,
@@ -216,8 +214,17 @@ class ComposeScreen(
override fun close() { override fun close() {
MinecraftClient.getInstance().setScreen(null) MinecraftClient.getInstance().setScreen(null)
if (textureID != 0) {
glDeleteTextures(textureID)
}
if (frameBufferID != -1) {
glDeleteFramebuffers(frameBufferID)
frameBufferID = -1
}
glDeleteSync(fenceSync)
directContext.close()
surface.close() surface.close()
scene.close() scene.close()
directContext.close() mainScope.cancel()
} }
} }