diff --git a/build.gradle.kts b/build.gradle.kts index af8b9f2..d156ed0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { kotlin("jvm") id("maven-publish") - id("fabric-loom") version "1.9-SNAPSHOT" + id("fabric-loom") version "1.9.2" id("org.jetbrains.compose") id("org.jetbrains.kotlin.plugin.compose") @@ -36,6 +36,7 @@ loom { } repositories { + mavenLocal() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") google() @@ -47,9 +48,9 @@ dependencies { mappings("net.fabricmc:yarn:${project.property("yarn_mappings")}:v2") modImplementation("net.fabricmc:fabric-loader:${project.property("loader_version")}") modImplementation("net.fabricmc:fabric-language-kotlin:${project.property("kotlin_loader_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) includeInternal(compose.desktop.currentOs) { exclude(module = "kotlin-stdlib") diff --git a/settings.gradle.kts b/settings.gradle.kts index 5514ffc..86e48c4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,6 @@ pluginManagement { repositories { + mavenLocal() maven("https://maven.fabricmc.net/") { name = "Fabric" } 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 daeb8d8..8af090a 100644 --- a/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt +++ b/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt @@ -114,6 +114,9 @@ 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) 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 be91513..945c993 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 @@ -11,11 +11,10 @@ import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.scene.MultiLayerComposeScene import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize -import com.mojang.blaze3d.platform.GlStateManager import com.mojang.blaze3d.platform.TextureUtil import com.mojang.blaze3d.systems.RenderSystem -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Runnable +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.* import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen @@ -27,118 +26,117 @@ import net.minecraft.text.Text import org.jetbrains.skia.* import org.jetbrains.skiko.FrameDispatcher 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.GL32.GL_SYNC_FLUSH_COMMANDS_BIT +import org.lwjgl.opengl.GL32C.GL_SYNC_GPU_COMMANDS_COMPLETE import org.lwjgl.system.MemoryUtil.NULL -import kotlin.coroutines.CoroutineContext @OptIn(InternalComposeUiApi::class) class ComposeScreen( val composeContent: @Composable (ComposeScreen) -> Unit ) : Screen(Text.literal("FrameRenderComposeScreen")) { - companion object { - private val frameBufferID by lazy { GlStateManager.glGenFramebuffers() } - private val textureID by lazy { TextureUtil.generateTextureId() } - private val composeLayer: RenderLayer by lazy { - RenderLayer.of( - "cpu_render_compose", - VertexFormats.POSITION_TEXTURE, - VertexFormat.DrawMode.QUADS, - 786432, - RenderLayer.MultiPhaseParameters.builder() - .texture(RenderPhase.TextureBase({ - RenderSystem.setShaderTexture(0, textureID) - }, {})) - .program(RenderPhase.POSITION_TEXTURE_PROGRAM) - .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) - .depthTest(RenderPhase.LEQUAL_DEPTH_TEST) - .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 frameBufferID = -1 + private val textureID = TextureUtil.generateTextureId() + private val composeLayer: RenderLayer by lazy { + RenderLayer.of( + "cpu_render_compose", + VertexFormats.POSITION_TEXTURE, + VertexFormat.DrawMode.QUADS, + 786432, + RenderLayer.MultiPhaseParameters.builder() + .texture(RenderPhase.TextureBase({ + RenderSystem.setShaderTexture(0, textureID) + }, {})) + .program(RenderPhase.POSITION_TEXTURE_PROGRAM) + .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) + .depthTest(RenderPhase.LEQUAL_DEPTH_TEST) + .build(false) + ) + } + + private var windowID: Long = -1 private lateinit var directContext: DirectContext private lateinit var surface: Surface private lateinit var canvas: Canvas - private lateinit var scene: ComposeScene - private val tasks = mutableListOf() - private val tasksCopy = mutableListOf() + private val mainScope = MainScope() - private val coroutineDispatcher = object : CoroutineDispatcher() { - override fun dispatch(context: CoroutineContext, block: Runnable) { - synchronized(tasks) { - tasks.add(block) - } - } + private val fenceSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0) + private val needRedraw = atomic(true) + private val frameDispatcher = FrameDispatcher(mainScope) { + needRedraw.value = true } - private val frameDispatcher = FrameDispatcher(coroutineDispatcher) { - RenderSystem.recordRenderCall { - if (surface.isClosed) { - return@recordRenderCall - } - val oldWindowID = glfwGetCurrentContext() - glfwMakeContextCurrent(windowID) - - surface.canvas.clear(Color.TRANSPARENT) - scene.render(canvas, System.nanoTime()) - directContext.flush() - - glfwMakeContextCurrent(oldWindowID) + private val scene: ComposeScene = MultiLayerComposeScene( + density = Density(MinecraftClient.getInstance().options.guiScale.value.toFloat() / 2), + size = IntSize(windowWidth, windowHeight), + coroutineContext = Dispatchers.Main, + invalidate = { frameDispatcher.scheduleFrame() } + ).apply { + setContent { + composeContent(this@ComposeScreen) } } override fun init() { - val oldFrameBuffer = glGetInteger(GL_FRAMEBUFFER_BINDING) - val oldTexture = glGetInteger(GL_TEXTURE_BINDING_2D) + val oldWindow = glfwGetCurrentContext() + glfwMakeContextCurrent(0) + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE) + windowID = glfwCreateWindow(1, 1, "", NULL, oldWindow) + val initTask = mainScope.async { + glfwMakeContextCurrent(windowID) + GL.createCapabilities() - glBindFramebuffer(GL_FRAMEBUFFER, frameBufferID) - glBindTexture(GL_TEXTURE_2D, textureID) - glTexImage2D( - 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_MAG_FILTER, GL_LINEAR) - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureID, 0) + frameBufferID = glGenFramebuffers() + glBindFramebuffer(GL_FRAMEBUFFER, frameBufferID) + glBindTexture(GL_TEXTURE_2D, textureID) + glTexImage2D(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_MAG_FILTER, GL_LINEAR) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureID, 0) - glBindFramebuffer(GL_FRAMEBUFFER, oldFrameBuffer) - glBindTexture(GL_TEXTURE_2D, oldTexture) - - directContext = DirectContext.makeGL() - val renderTarget = BackendRenderTarget.makeGL( - windowWidth, windowHeight, 0, 8, frameBufferID, GL_RGBA8 - ) - surface = Surface.makeFromBackendRenderTarget( - directContext, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB - )!! - canvas = surface.canvas.asComposeCanvas() - scene = MultiLayerComposeScene( - density = Density(MinecraftClient.getInstance().options.guiScale.value.toFloat() / 2), - size = IntSize(windowWidth, windowHeight), - coroutineContext = coroutineDispatcher, - invalidate = { frameDispatcher.scheduleFrame() } - ) - scene.setContent { - composeContent(this@ComposeScreen) + directContext = DirectContext.makeGL() + val renderTarget = BackendRenderTarget.makeGL( + windowWidth, windowHeight, 0, 8, frameBufferID, GL_RGBA8 + ) + surface = Surface.makeFromBackendRenderTarget( + directContext, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB + ) ?: throw IllegalStateException("Surface could not be created") + canvas = surface.canvas.asComposeCanvas() + glfwMakeContextCurrent(0) } - super.init() + runBlocking { + initTask.await() + } + glfwMakeContextCurrent(oldWindow) } override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { - synchronized(tasks) { - tasksCopy.clear() - tasksCopy.addAll(tasks) - tasks.clear() - } - for (runnable in tasksCopy) { - runnable.run() + super.render(context, mouseX, mouseY, delta) + if (needRedraw.compareAndSet(true, false)) { + val oldWindow = glfwGetCurrentContext() + glfwMakeContextCurrent(0) + val renderTask = mainScope.async { + if (surface.isClosed) { + return@async + } + glfwMakeContextCurrent(windowID) + 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 { val vertexConsumer = it.getBuffer(composeLayer) @@ -163,7 +161,7 @@ class ComposeScreen( override fun mouseMoved(mouseX: Double, mouseY: Double) { val x = MinecraftClient.getInstance().mouse.x.toFloat() val y = MinecraftClient.getInstance().mouse.y.toFloat() - coroutineDispatcher.dispatch(coroutineDispatcher) { + mainScope.launch { scene.sendPointerEvent( PointerEventType.Move, position = Offset(x, y) @@ -172,7 +170,7 @@ class ComposeScreen( } override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { - coroutineDispatcher.dispatch(coroutineDispatcher) { + mainScope.launch { val x = MinecraftClient.getInstance().mouse.x.toFloat() val y = MinecraftClient.getInstance().mouse.y.toFloat() scene.sendPointerEvent( @@ -185,7 +183,7 @@ class ComposeScreen( } override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { - coroutineDispatcher.dispatch(coroutineDispatcher) { + mainScope.launch { val x = MinecraftClient.getInstance().mouse.x.toFloat() val y = MinecraftClient.getInstance().mouse.y.toFloat() scene.sendPointerEvent( @@ -203,7 +201,7 @@ class ComposeScreen( val x = MinecraftClient.getInstance().mouse.x.toFloat() val y = MinecraftClient.getInstance().mouse.y.toFloat() println("mouseScrolled: $x $y $horizontalAmount $verticalAmount") - coroutineDispatcher.dispatch(coroutineDispatcher) { + mainScope.launch { scene.sendPointerEvent( PointerEventType.Scroll, button = PointerButton.Tertiary, @@ -216,8 +214,17 @@ class ComposeScreen( override fun close() { MinecraftClient.getInstance().setScreen(null) + if (textureID != 0) { + glDeleteTextures(textureID) + } + if (frameBufferID != -1) { + glDeleteFramebuffers(frameBufferID) + frameBufferID = -1 + } + glDeleteSync(fenceSync) + directContext.close() surface.close() scene.close() - directContext.close() + mainScope.cancel() } } \ No newline at end of file