fix: 修复修改窗口大小会导致游戏崩溃的问题
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user