feat: 拆分 compose 和 sika 渲染代码

This commit is contained in:
2026-02-08 04:25:59 +08:00
parent 33ae82a39c
commit 38aca17e70
7 changed files with 105 additions and 339 deletions

View File

@@ -36,6 +36,7 @@ loom {
}
repositories {
maven("https://maven.aliyun.com/repository/public")
mavenLocal()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")

View File

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

View File

@@ -1,7 +1,6 @@
package net.airgame.compose.ui.client
import net.airgame.compose.ui.client.screen.ComposeScreen
import net.airgame.compose.ui.client.screen.ComposeScreenV2
import net.fabricmc.api.ClientModInitializer
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
@@ -15,13 +14,12 @@ class ComposeUiMod : ClientModInitializer {
val logger: Logger = LoggerFactory.getLogger("ComposeUiMod")
}
private val keyV1 = KeyBinding("测试V1", InputUtil.GLFW_KEY_G, "测试")
private val keyV2 = KeyBinding("测试V2", InputUtil.GLFW_KEY_J, "测试")
private val key = KeyBinding("测试", InputUtil.GLFW_KEY_G, "测试")
override fun onInitializeClient() {
KeyBindingHelper.registerKeyBinding(keyV1)
KeyBindingHelper.registerKeyBinding(key)
ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { client ->
if (keyV1.wasPressed()) {
if (key.wasPressed()) {
val screen = ComposeScreen {
CenterUI {
TestUI()
@@ -30,15 +28,6 @@ class ComposeUiMod : ClientModInitializer {
client.setScreen(screen)
return@EndTick
}
if (keyV2.wasPressed()) {
val screen = ComposeScreenV2 {
CenterUI {
TestUI()
}
}
client.setScreen(screen)
return@EndTick
}
})
}
}

View File

@@ -1,27 +1,28 @@
package net.airgame.compose.ui.client.render
import androidx.compose.runtime.Composable
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.scene.CanvasLayersComposeScene
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.concurrent.Executors
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 threadPool = Executors.newSingleThreadExecutor { Thread(it, "ComposeRenderThread") }
private val composeScope = CoroutineScope(SupervisorJob() + threadPool.asCoroutineDispatcher())
private val frameDispatcher = FrameDispatcher(composeScope) { render() }
private val skiaRender = SkiaRender()
private val composeScene: ComposeScene = MultiLayerComposeScene(
private val composeScene: ComposeScene = CanvasLayersComposeScene(
coroutineContext = composeScope.coroutineContext,
invalidate = { frameDispatcher.scheduleFrame() }
)
@@ -29,29 +30,27 @@ class ComposeRender {
val output: RenderLayer get() = skiaRender.output
data class ResizeMessage(val width: Int, val height: Int, val uiScale: Float, val fontScale: Float)
private 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
private var resizeMessage: ResizeMessage? = null
fun init(width: Int, height: Int, uiScale: Float, fontScale: Float, content: @Composable () -> Unit) {
skiaRender.init(width, height)
skiaRender.prepareTexture()
val task = composeScope.async {
// 确保线程中的OpenGL状态机已经切换到目标窗口
skiaRender.switchWindow()
// 确保渲染线程中的OpenGL状态机已经准备完毕
skiaRender.prepareRender(width, height)
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 状态会触发重绘
// 必须先准备好渲染线程,才能设置 composeScene
composeScene.density = Density(uiScale, fontScale)
composeScene.size = IntSize(width, height)
composeScene.setContent(content)
}
@@ -63,15 +62,16 @@ class ComposeRender {
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.density = Density(uiScale, fontScale)
composeScene.size = IntSize(width, height)
}
Snapshot.sendApplyNotifications()
skiaRender.render {
composeScene.render(composeCanvas, System.nanoTime())
try {
composeScene.render(composeCanvas, System.nanoTime())
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@@ -88,13 +88,14 @@ class ComposeRender {
fun destroy() {
val task = composeScope.async {
skiaRender.destroy()
skiaRender.destroyRender()
composeScene.close()
}
runBlocking {
task.await()
composeScope.cancel()
// threadPool.close()
}
composeScope.cancel()
threadPool.close()
skiaRender.destroyTexture()
}
}

View File

@@ -33,7 +33,14 @@ class SkiaRender {
lateinit var skiaContext: DirectContext; private set
lateinit var skiaSurface: Surface; private set
fun init(width: Int, height: Int) {
/**
* 初始化离屏渲染
*
* 由于需要创建新的窗口,并且在新窗口中与 Minecraft 窗口共享资源
*
* 这个方法必须在 Minecraft UI 线程中调用
*/
fun prepareTexture() {
textureId = TextureUtil.generateTextureId()
output = RenderLayer.of(
"cpu_render_compose",
@@ -43,14 +50,13 @@ class SkiaRender {
768 * 1024,
RenderLayer.MultiPhaseParameters.builder()
.texture(RenderPhase.TextureBase({
RenderSystem.setShaderTexture(GL_TEXTURE0, textureId)
RenderSystem.setShaderTexture(0, textureId)
}, {}))
.program(RenderPhase.POSITION_TEXTURE_PROGRAM)
.transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
.depthTest(RenderPhase.LEQUAL_DEPTH_TEST)
.build(false)
)
val oldWindow = glfwGetCurrentContext()
// 在 OpenGL 里一个上下文Context在同一时间只能被一个线程拥有。
// 在某些显卡驱动上,如果基于一个已经被占用的上下文去创建它的共享上下文,可能会导致 glfwCreateWindow 返回失败或死锁。
@@ -59,6 +65,13 @@ class SkiaRender {
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
// 创建一个 1x1 的隐形窗口,并指定它和 oldWindow 共享仓库纹理、VBO 全部通用)。
windowId = glfwCreateWindow(1, 1, "", NULL, oldWindow)
glfwMakeContextCurrent(oldWindow)
}
/**
* 这个方法必须在渲染线程中调用,否则渲染线程可能因为没有初始化 OpenGL 而无法绘制任何内容
*/
fun prepareRender(width: Int, height: Int) {
glfwMakeContextCurrent(windowId)
GL.createCapabilities()
@@ -76,11 +89,6 @@ class SkiaRender {
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) {
@@ -88,7 +96,8 @@ class SkiaRender {
// 所以无需对 windowId 做保存与恢复
val oldWindow = glfwGetCurrentContext()
glfwMakeContextCurrent(windowId)
skiaSurface.canvas.clear(Color.TRANSPARENT)
skiaSurface.canvas.clear(Color.WHITE)
skiaSurface.canvas.resetMatrix()
renderTask.run()
skiaContext.flush()
glfwMakeContextCurrent(oldWindow)
@@ -111,13 +120,16 @@ class SkiaRender {
glfwMakeContextCurrent(oldWindow)
}
fun destroy() {
fun destroyRender() {
if (::skiaSurface.isInitialized) {
skiaSurface.close()
}
if (::skiaContext.isInitialized) {
skiaContext.close()
}
}
fun destroyTexture() {
if (textureId != -1) {
GlStateManager._deleteTexture(textureId)
}

View File

@@ -3,148 +3,45 @@ package net.airgame.compose.ui.client.screen
import androidx.compose.runtime.Composable
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.scene.CanvasLayersComposeScene
import androidx.compose.ui.scene.ComposeScene
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import com.mojang.blaze3d.platform.TextureUtil
import com.mojang.blaze3d.systems.RenderSystem
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import net.airgame.compose.ui.client.ComposeUiMod
import net.airgame.compose.ui.client.render.ComposeRender
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
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 net.minecraft.text.Text
import org.jetbrains.skia.*
import org.jetbrains.skiko.FrameDispatcher
import org.lwjgl.glfw.GLFW.*
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
import org.lwjgl.opengl.GL32C.GL_SYNC_GPU_COMMANDS_COMPLETE
import org.lwjgl.system.MemoryUtil.NULL
@OptIn(InternalComposeUiApi::class)
class ComposeScreen(
val composeContent: @Composable (ComposeScreen) -> Unit
) : Screen(Text.literal("FrameRenderComposeScreen")) {
private val composeRender = ComposeRender()
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 val mainScope = MainScope()
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 mcGuiScale = MinecraftClient.getInstance().options.guiScale.value.toFloat()
private val fontScaleValue = 1f
private val scene: ComposeScene = CanvasLayersComposeScene(
density = if (mcGuiScale <= 0) {
Density(1f, fontScaleValue)
} else {
Density(mcGuiScale / 2, fontScaleValue)
},
size = IntSize(windowWidth, windowHeight),
coroutineContext = Dispatchers.Main,
invalidate = { frameDispatcher.scheduleFrame() }
).apply {
setContent {
composeContent(this@ComposeScreen)
private val guiScale: Float
get() {
val scale = MinecraftClient.getInstance().options.guiScale.value.toFloat()
if (scale <= 0) return 1f
return scale / 2
}
}
private val fontScale: Float get() = 1f
override fun init() {
val oldWindow = glfwGetCurrentContext()
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)
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)
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)
composeRender.init(windowWidth, windowHeight, guiScale, fontScale) {
composeContent(this)
}
runBlocking {
initTask.await()
}
glfwMakeContextCurrent(oldWindow)
ComposeUiMod.logger.info(
"init: width={} height={} guiScale={} fontScale={}",
windowWidth, windowHeight, guiScale, fontScale
)
}
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
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)
val vertexConsumer = it.getBuffer(composeRender.output)
val matrix4f = context.matrices.peek().positionMatrix
vertexConsumer.vertex(matrix4f, 0f, 0f, 0f).texture(0f, 0f)
vertexConsumer.vertex(matrix4f, 0f, height.toFloat(), 0f).texture(0f, 1f)
@@ -154,51 +51,55 @@ class ComposeScreen(
}
override fun resize(client: MinecraftClient, width: Int, height: Int) {
super.resize(client, width, height)
val old = surface
surface = Surface.makeRasterN32Premul(windowWidth, windowHeight)
canvas = surface.canvas.asComposeCanvas()
old.close()
scene.density = if (mcGuiScale <= 0) {
Density(1f, fontScaleValue)
} else {
Density(mcGuiScale / 2, fontScaleValue)
}
scene.size = IntSize(windowWidth, windowHeight)
this.width = width
this.height = height
composeRender.resize(windowWidth, windowHeight, guiScale, fontScale)
ComposeUiMod.logger.info(
"resize: width={} height={} guiScale={} fontScale={}",
windowWidth, windowHeight, guiScale, fontScale
)
}
private fun getMouseButton(button: Int) = when (button) {
0 -> PointerButton.Primary
1 -> PointerButton.Secondary
2 -> PointerButton.Tertiary
3 -> PointerButton.Back
4 -> PointerButton.Forward
else -> PointerButton.Primary
}
private fun getMousePosition() = Offset(
MinecraftClient.getInstance().mouse.x.toFloat(),
MinecraftClient.getInstance().mouse.y.toFloat()
)
override fun mouseMoved(mouseX: Double, mouseY: Double) {
val x = MinecraftClient.getInstance().mouse.x.toFloat()
val y = MinecraftClient.getInstance().mouse.y.toFloat()
mainScope.launch {
scene.sendPointerEvent(
composeRender.post {
it.sendPointerEvent(
PointerEventType.Move,
position = Offset(x, y)
position = getMousePosition()
)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
mainScope.launch {
val x = MinecraftClient.getInstance().mouse.x.toFloat()
val y = MinecraftClient.getInstance().mouse.y.toFloat()
scene.sendPointerEvent(
composeRender.post {
it.sendPointerEvent(
PointerEventType.Press,
button = PointerButton.Primary,
position = Offset(x, y)
button = getMouseButton(button),
position = getMousePosition()
)
}
return true
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
mainScope.launch {
val x = MinecraftClient.getInstance().mouse.x.toFloat()
val y = MinecraftClient.getInstance().mouse.y.toFloat()
scene.sendPointerEvent(
composeRender.post {
it.sendPointerEvent(
PointerEventType.Release,
button = PointerButton.Primary,
position = Offset(x, y)
button = getMouseButton(button),
position = getMousePosition()
)
}
return true
@@ -207,13 +108,11 @@ class ComposeScreen(
override fun mouseScrolled(
mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double
): Boolean {
val x = MinecraftClient.getInstance().mouse.x.toFloat()
val y = MinecraftClient.getInstance().mouse.y.toFloat()
mainScope.launch {
scene.sendPointerEvent(
composeRender.post {
it.sendPointerEvent(
PointerEventType.Scroll,
button = PointerButton.Tertiary,
position = Offset(x, y),
position = getMousePosition(),
scrollDelta = Offset(horizontalAmount.toFloat(), -verticalAmount.toFloat())
)
}
@@ -222,17 +121,6 @@ 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()
mainScope.cancel()
composeRender.destroy()
}
}

View File

@@ -1,126 +0,0 @@
package net.airgame.compose.ui.client.screen
import androidx.compose.runtime.Composable
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.input.pointer.PointerEventType
import net.airgame.compose.ui.client.ComposeUiMod
import net.airgame.compose.ui.client.render.ComposeRender
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
import net.minecraft.text.Text
@OptIn(InternalComposeUiApi::class)
class ComposeScreenV2(
val composeContent: @Composable (ComposeScreenV2) -> Unit
) : Screen(Text.literal("FrameRenderComposeScreen")) {
private val composeRender = ComposeRender()
private val windowWidth: Int get() = MinecraftClient.getInstance().window.width
private val windowHeight: Int get() = MinecraftClient.getInstance().window.height
private val guiScale: Float
get() {
val scale = MinecraftClient.getInstance().options.guiScale.value.toFloat()
if (scale <= 0) return 1f
return scale / 2
}
private val fontScale: Float get() = 1f
override fun init() {
composeRender.init(windowWidth, windowHeight, guiScale, fontScale) {
composeContent(this)
}
ComposeUiMod.logger.info(
"init: width={} height={} guiScale={} fontScale={}",
windowWidth, windowHeight, guiScale, fontScale
)
}
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
context.draw {
val vertexConsumer = it.getBuffer(composeRender.output)
val matrix4f = context.matrices.peek().positionMatrix
vertexConsumer.vertex(matrix4f, 0f, 0f, 0f).texture(0f, 0f)
vertexConsumer.vertex(matrix4f, 0f, height.toFloat(), 0f).texture(0f, 1f)
vertexConsumer.vertex(matrix4f, width.toFloat(), height.toFloat(), 0f).texture(1f, 1f)
vertexConsumer.vertex(matrix4f, width.toFloat(), 0f, 0f).texture(1f, 0f)
}
}
override fun resize(client: MinecraftClient, width: Int, height: Int) {
this.width = width
this.height = height
composeRender.resize(windowWidth, windowHeight, guiScale, fontScale)
ComposeUiMod.logger.info(
"resize: width={} height={} guiScale={} fontScale={}",
windowWidth, windowHeight, guiScale, fontScale
)
}
private fun getMouseButton(button: Int) = when (button) {
0 -> PointerButton.Primary
1 -> PointerButton.Secondary
2 -> PointerButton.Tertiary
3 -> PointerButton.Back
4 -> PointerButton.Forward
else -> PointerButton.Primary
}
private fun getMousePosition() = Offset(
MinecraftClient.getInstance().mouse.x.toFloat(),
MinecraftClient.getInstance().mouse.y.toFloat()
)
override fun mouseMoved(mouseX: Double, mouseY: Double) {
composeRender.post {
it.sendPointerEvent(
PointerEventType.Move,
position = getMousePosition()
)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
composeRender.post {
it.sendPointerEvent(
PointerEventType.Press,
button = getMouseButton(button),
position = getMousePosition()
)
}
return true
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
composeRender.post {
it.sendPointerEvent(
PointerEventType.Release,
button = getMouseButton(button),
position = getMousePosition()
)
}
return true
}
override fun mouseScrolled(
mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double
): Boolean {
composeRender.post {
it.sendPointerEvent(
PointerEventType.Scroll,
button = PointerButton.Tertiary,
position = getMousePosition(),
scrollDelta = Offset(horizontalAmount.toFloat(), -verticalAmount.toFloat())
)
}
return true
}
override fun close() {
MinecraftClient.getInstance().setScreen(null)
composeRender.destroy()
}
}