fix: 修复修改窗口大小会导致游戏崩溃的问题

This commit is contained in:
2026-02-08 02:44:02 +08:00
parent 4631a59c35
commit cd0e21abc8
3 changed files with 234 additions and 4 deletions

View File

@@ -0,0 +1,100 @@
package net.airgame.compose.ui.client.render
import androidx.compose.runtime.Composable
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.asComposeCanvas
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 kotlinx.coroutines.*
import net.minecraft.client.render.RenderLayer
import org.jetbrains.skiko.FrameDispatcher
import java.util.function.Consumer
@OptIn(InternalComposeUiApi::class)
class ComposeRender {
// private val threadPool = Executors.newSingleThreadExecutor()
// private val composeScope = CoroutineScope(SupervisorJob() + threadPool.asCoroutineDispatcher())
private val composeScope = MainScope()
private val frameDispatcher = FrameDispatcher(composeScope) { render() }
private val skiaRender = SkiaRender()
private val composeScene: ComposeScene = MultiLayerComposeScene(
coroutineContext = composeScope.coroutineContext,
invalidate = { frameDispatcher.scheduleFrame() }
)
lateinit var composeCanvas: Canvas; private set
val output: RenderLayer get() = skiaRender.output
data class ResizeMessage(val width: Int, val height: Int, val uiScale: Float, val fontScale: Float)
// resize防抖消息的容量为1后续多余的resize请求会覆盖旧的resize请求
// compose 渲染线程定期消费 resize 消息
@Volatile
var resizeMessage: ResizeMessage? = null
fun init(width: Int, height: Int, uiScale: Float, fontScale: Float, content: @Composable () -> Unit) {
skiaRender.init(width, height)
val task = composeScope.async {
// 确保线程中的OpenGL状态机已经切换到目标窗口
skiaRender.switchWindow()
composeCanvas = skiaRender.skiaSurface.canvas.asComposeCanvas()
}
runBlocking {
// 确保 SkiaRender 完全初始化之后,才允许 Minecraft UI 线程继续执行
task.await()
}
composeScene.density = if (uiScale <= 0) {
Density(1f, fontScale)
} else {
Density(uiScale / 2, fontScale)
}
composeScene.size = IntSize(width, height)
composeScene.setContent(content)
}
fun render() {
val resizeData = resizeMessage
resizeMessage = null
if (resizeData != null) {
val (width, height, uiScale, fontScale) = resizeData
skiaRender.resize(width, height)
composeCanvas = skiaRender.skiaSurface.canvas.asComposeCanvas()
composeScene.density = if (uiScale <= 0) {
Density(1f, fontScale)
} else {
Density(uiScale / 2, fontScale)
}
composeScene.size = IntSize(width, height)
}
skiaRender.render {
composeScene.render(composeCanvas, System.nanoTime())
}
}
fun post(task: Consumer<ComposeScene>) {
composeScope.launch {
task.accept(composeScene)
}
}
fun resize(width: Int, height: Int, uiScale: Float, fontScale: Float) {
resizeMessage = ResizeMessage(width, height, uiScale, fontScale)
frameDispatcher.scheduleFrame()
}
fun destroy() {
val task = composeScope.async {
skiaRender.destroy()
composeScene.close()
}
runBlocking {
task.await()
composeScope.cancel()
// threadPool.close()
}
}
}

View File

@@ -0,0 +1,128 @@
package net.airgame.compose.ui.client.render
import com.mojang.blaze3d.platform.GlStateManager
import com.mojang.blaze3d.platform.TextureUtil
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.render.RenderLayer
import net.minecraft.client.render.RenderPhase
import net.minecraft.client.render.VertexFormat
import net.minecraft.client.render.VertexFormats
import org.jetbrains.skia.*
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL11.glTexImage2D
import org.lwjgl.opengl.GL40.*
import org.lwjgl.system.MemoryUtil.NULL
/**
* Skia 离屏渲染
*
* 这个类不负责线程安全,除了 init 方法需要在 Minecraft UI 线程中调用以外
*
* 所有绘制方法render、resize、destroy需要确保在同一线程进行调用者应自行保证线程安全
*/
class SkiaRender {
private var windowId: Long = -1
private var frameBufferId = -1
private var textureId = -1
// 所有渲染结果都会绘制到这个纹理中
// Minecraft 直接将这个纹理渲染到窗口(或者世界内)即可
lateinit var output: RenderLayer; private set
lateinit var skiaContext: DirectContext; private set
lateinit var skiaSurface: Surface; private set
fun init(width: Int, height: Int) {
textureId = TextureUtil.generateTextureId()
output = RenderLayer.of(
"cpu_render_compose",
VertexFormats.POSITION_TEXTURE,
VertexFormat.DrawMode.QUADS,
// 顶点数据缓存大小,单位:字节
768 * 1024,
RenderLayer.MultiPhaseParameters.builder()
.texture(RenderPhase.TextureBase({
RenderSystem.setShaderTexture(GL_TEXTURE0, textureId)
}, {}))
.program(RenderPhase.POSITION_TEXTURE_PROGRAM)
.transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
.depthTest(RenderPhase.LEQUAL_DEPTH_TEST)
.build(false)
)
val oldWindow = glfwGetCurrentContext()
// 在 OpenGL 里一个上下文Context在同一时间只能被一个线程拥有。
// 在某些显卡驱动上,如果基于一个已经被占用的上下文去创建它的共享上下文,可能会导致 glfwCreateWindow 返回失败或死锁。
glfwMakeContextCurrent(0)
// 告诉 GLFW接下来创建的窗口不要显示在任务栏和桌面上
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
// 创建一个 1x1 的隐形窗口,并指定它和 oldWindow 共享仓库纹理、VBO 全部通用)。
windowId = glfwCreateWindow(1, 1, "", NULL, oldWindow)
glfwMakeContextCurrent(windowId)
GL.createCapabilities()
frameBufferId = glGenFramebuffers()
glBindFramebuffer(GL_FRAMEBUFFER, frameBufferId)
glBindTexture(GL_TEXTURE_2D, textureId)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 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)
skiaContext = 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
) ?: throw IllegalStateException("Surface could not be created")
glfwMakeContextCurrent(oldWindow)
}
fun switchWindow() {
glfwMakeContextCurrent(windowId)
}
fun render(renderTask: Runnable) {
// 由于在其他线程中渲染,而 OpenGL 的状态机会根据线程储存
// 所以无需对 windowId 做保存与恢复
val oldWindow = glfwGetCurrentContext()
glfwMakeContextCurrent(windowId)
skiaSurface.canvas.clear(Color.TRANSPARENT)
renderTask.run()
skiaContext.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()
val renderTarget = BackendRenderTarget.makeGL(
width, height, 0, 8, frameBufferId, GL_RGBA8
)
skiaSurface = Surface.makeFromBackendRenderTarget(
skiaContext, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB
) ?: throw IllegalStateException("Failed to recreate Surface")
glfwMakeContextCurrent(oldWindow)
}
fun destroy() {
if (::skiaSurface.isInitialized) {
skiaSurface.close()
}
if (::skiaContext.isInitialized) {
skiaContext.close()
}
if (textureId != -1) {
GlStateManager._deleteTexture(textureId)
}
if (windowId != -1L) {
glfwDestroyWindow(windowId)
}
}
}

View File

@@ -26,9 +26,7 @@ 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.ARBSync.*
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL30.*
import org.lwjgl.opengl.GL32.GL_SYNC_FLUSH_COMMANDS_BIT
@@ -161,7 +159,11 @@ class ComposeScreen(
surface = Surface.makeRasterN32Premul(windowWidth, windowHeight)
canvas = surface.canvas.asComposeCanvas()
old.close()
scene.density = Density(client.options.guiScale.value.toFloat() / 2)
scene.density = if (mcGuiScale <= 0) {
Density(1f, fontScaleValue)
} else {
Density(mcGuiScale / 2, fontScaleValue)
}
scene.size = IntSize(windowWidth, windowHeight)
}