From 6ae8066ebc3f521e1d838d3eb63866b33c2f1947 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 2 Oct 2023 11:07:55 +0700 Subject: [PATCH] multithreaded chunk geometry batching --- build.gradle.kts | 2 +- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 73 ++---- .../kstarbound/client/StarboundClient.kt | 226 +++++++++++++++--- .../client/gl/vertex/StreamVertexBuilder.kt | 38 +-- .../kstarbound/client/render/Box2DRenderer.kt | 55 +++-- .../client/render/LayeredRenderer.kt | 11 +- .../kstarbound/client/render/TileRenderer.kt | 5 +- .../kstarbound/client/world/ClientWorld.kt | 68 ++++-- .../ru/dbotthepony/kstarbound/world/Chunk.kt | 35 +-- .../world/phys/RectTileFlooderDepthFirst.kt | 177 -------------- .../world/phys/RectTileFlooderSizeFirst.kt | 146 ----------- 11 files changed, 322 insertions(+), 514 deletions(-) delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderDepthFirst.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt diff --git a/build.gradle.kts b/build.gradle.kts index a66c09de..3500cb41 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -82,7 +82,7 @@ dependencies { implementation("net.java.dev.jna:jna:5.13.0") implementation("com.github.jnr:jnr-ffi:2.2.13") - implementation("ru.dbotthepony:kbox2d:2.4.1.7") + implementation("ru.dbotthepony:kbox2d:2.4.1-1.0.2") implementation("ru.dbotthepony:kvector:2.10.2") implementation("com.github.ben-manes.caffeine:caffeine:3.1.5") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 69a36974..928019a1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -7,6 +7,7 @@ import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.io.BTreeDB +import ru.dbotthepony.kstarbound.io.json.BinaryJsonReader import ru.dbotthepony.kstarbound.player.Avatar import ru.dbotthepony.kstarbound.player.QuestDescriptor import ru.dbotthepony.kstarbound.player.QuestInstance @@ -17,6 +18,7 @@ import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.io.json.VersionedJson import ru.dbotthepony.kstarbound.io.readVarInt import ru.dbotthepony.kstarbound.util.AssetPathStack +import ru.dbotthepony.kstarbound.world.entities.Move import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kvector.vector.Vector2d import java.io.BufferedInputStream @@ -24,8 +26,10 @@ import java.io.ByteArrayInputStream import java.io.DataInputStream import java.io.File import java.util.* +import java.util.concurrent.ForkJoinPool import java.util.zip.Inflater import java.util.zip.InflaterInputStream +import kotlin.system.exitProcess private val LOGGER = LogManager.getLogger() @@ -37,6 +41,11 @@ fun main() { val db = BTreeDB(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world")) //val db = BTreeDB(File("world.world")) + //val meta = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(db.read(byteArrayOf(0, 0, 0, 0, 0))), Inflater()))) + + //println(meta.readInt()) + //println(meta.readInt()) + val client = StarboundClient() //Starbound.addFilePath(File("./unpacked_assets/")) @@ -64,6 +73,7 @@ fun main() { //for (chunkX in 17 .. 18) { //for (chunkX in 14 .. 24) { for (chunkX in 0 .. 100) { + //for (chunkX in 0 .. 17) { // for (chunkY in 21 .. 21) { for (chunkY in 18 .. 24) { val data = db.read(byteArrayOf(1, 0, chunkX.toByte(), 0, chunkY.toByte())) @@ -114,8 +124,6 @@ fun main() { val item = Registries.items.values.random() val rand = Random() - client.world!!.physics.gravity = Vector2d.ZERO - for (i in 0 .. 0) { val item = ItemEntity(client.world!!, item.value) @@ -149,7 +157,7 @@ fun main() { } //ent.position += Vector2d(y = 14.0, x = -10.0) - ent.position = Vector2d(600.0 + 16.0, 721.0 + 48.0) + ent.position = Vector2d(238.0, 685.0) client.camera.pos = Vector2d(238.0, 685.0) //client.camera.pos = Vector2f(0f, 0f) @@ -166,57 +174,6 @@ fun main() { //client.camera.pos.y = ent.pos.y.toFloat() } - /*val lightRenderer = LightRenderer(client.gl) - - lightRenderer.resizeFramebuffer(client.viewportWidth, client.viewportHeight) - client.onViewportChanged(lightRenderer::resizeFramebuffer) - - lightRenderer.addShadowGeometry(object : LightRenderer.ShadowGeometryRenderer { - override fun renderHardGeometry(lightPosition: Vector2f, stack: Matrix4fStack, program: GLHardLightGeometryProgram) { - val builder = lightRenderer.builder - - builder.begin() - builder.quad(-6f, 0f, -2f, 2f) - builder.quad(0f, 0f, 2f, 2f) - - builder.upload() - builder.draw(GL_LINES) - } - - override fun renderSoftGeometry(lightPosition: Vector2f, stack: Matrix4fStack, program: GLSoftLightGeometryProgram) { - val builder = lightRenderer.builderSoft - - builder.begin() - builder.shadowQuad(-6f, 0f, -2f, 2f) - builder.shadowQuad(0f, 0f, 2f, 2f) - - builder.upload() - builder.draw(GL_TRIANGLES) - } - }) - - client.onPostDrawWorld { - lightRenderer.begin() - - for ((lightPosition, color) in listOf( - (client.screenToWorld(client.mouseCoordinatesF)) to Color.RED, - (client.screenToWorld(client.mouseCoordinatesF) + Vector2f(0.1f)) to Color.GREEN, - (client.screenToWorld(client.mouseCoordinatesF) + Vector2f(-0.1f)) to Color.BLUE, - )) { - lightRenderer.renderSoftLight(lightPosition, color, radius = 40f) - } - - for ((lightPosition, color) in listOf( - (client.screenToWorld(client.mouseCoordinatesF) + Vector2f(10.1f)) to Color.RED, - (client.screenToWorld(client.mouseCoordinatesF) + Vector2f(10.1f)) to Color.GREEN, - (client.screenToWorld(client.mouseCoordinatesF) + Vector2f(9.9f)) to Color.BLUE, - )) { - //lightRenderer.renderSoftLight(lightPosition, color, radius = 10f) - } - - lightRenderer.renderOutputAdditive() - }*/ - client.box2dRenderer.drawShapes = false client.box2dRenderer.drawPairs = false client.box2dRenderer.drawAABB = false @@ -240,8 +197,8 @@ fun main() { //client.camera.pos.y = ent.position.y.toFloat() client.camera.pos += Vector2d( - (if (client.input.KEY_LEFT_DOWN || client.input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_RIGHT_DOWN || client.input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0), - (if (client.input.KEY_UP_DOWN || client.input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_DOWN_DOWN || client.input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0), + (if (client.input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) ) //println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1) @@ -249,7 +206,7 @@ fun main() { //if (ent.onGround) //ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1 - /*if (client.input.KEY_LEFT_DOWN) { + if (client.input.KEY_LEFT_DOWN) { ent.movement.moveDirection = Move.MOVE_LEFT } else if (client.input.KEY_RIGHT_DOWN) { ent.movement.moveDirection = Move.MOVE_RIGHT @@ -261,7 +218,7 @@ fun main() { ent.movement.requestJump() } else if (client.input.KEY_SPACE_RELEASED) { ent.movement.recallJump() - }*/ + } if (client.input.KEY_ESCAPE_PRESSED) { glfwSetWindowShouldClose(client.window, true) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 534dfe86..70785780 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -49,6 +49,7 @@ import ru.dbotthepony.kstarbound.client.world.ClientWorld import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity +import ru.dbotthepony.kstarbound.util.Either import ru.dbotthepony.kstarbound.util.forEachValid import ru.dbotthepony.kstarbound.util.formatBytesShort import ru.dbotthepony.kstarbound.world.LightCalculator @@ -65,20 +66,32 @@ import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector4f import java.io.Closeable import java.io.File -import java.lang.ref.Cleaner +import java.lang.ref.PhantomReference +import java.lang.ref.ReferenceQueue import java.lang.ref.WeakReference import java.nio.ByteBuffer import java.nio.ByteOrder import java.time.Duration import java.util.* +import java.util.concurrent.Callable +import java.util.concurrent.CancellationException +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.ExecutionException +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.FutureTask +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.ReentrantLock -import java.util.stream.StreamSupport +import java.util.concurrent.locks.ReentrantReadWriteLock +import java.util.function.IntConsumer +import kotlin.NoSuchElementException import kotlin.collections.ArrayList import kotlin.math.absoluteValue import kotlin.math.roundToInt -class StarboundClient : Closeable { +class StarboundClient : Closeable, ExecutorService { val window: Long val camera = Camera(this) val input = UserInput() @@ -111,7 +124,7 @@ class StarboundClient : Closeable { var viewportTopRight = Vector2d() private set - var fullbright = false + var fullbright = true var clientTerminated = false private set @@ -128,25 +141,141 @@ class StarboundClient : Closeable { private set private val scissorStack = LinkedList() - private val cleanerBacklog = ArrayList<() -> Unit>() private val onDrawGUI = ArrayList<() -> Unit>() private val onPreDrawWorld = ArrayList<(LayeredRenderer) -> Unit>() private val onPostDrawWorld = ArrayList<() -> Unit>() private val onPostDrawWorldOnce = ArrayList<(LayeredRenderer) -> Unit>() private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>() private val terminateCallbacks = ArrayList<() -> Unit>() + val loadingLog = LoadingLog() - private val cleaner = Cleaner.create { r -> - val thread = Thread(r, "OpenGL Cleaner for '${thread.name}'") - thread.priority = 2 - thread + private val executeQueue = ConcurrentLinkedQueue() + private val futureQueue = ConcurrentLinkedQueue>() + private val openglCleanQueue = ReferenceQueue() + private var openglCleanQueueHead: CleanRef? = null + + private class CleanRef(ref: Any, queue: ReferenceQueue, val fn: IntConsumer, val value: Int) : PhantomReference(ref, queue) { + var next: CleanRef? = null + var prev: CleanRef? = null } - var objectsCreated = 0L + private data class InPlaceFuture(private val value: V) : Future { + override fun cancel(mayInterruptIfRunning: Boolean): Boolean { + return false + } + + override fun isCancelled(): Boolean { + return false + } + + override fun isDone(): Boolean { + return true + } + + override fun get(): V { + return value + } + + override fun get(timeout: Long, unit: TimeUnit): V { + return value + } + + companion object { + val EMPTY = InPlaceFuture(Unit) + } + } + + override fun execute(command: Runnable) { + if (isSameThread()) { + command.run() + } else { + executeQueue.add(command) + LockSupport.unpark(thread) + } + } + + override fun shutdown() { + throw UnsupportedOperationException() + } + + override fun shutdownNow(): MutableList { + throw UnsupportedOperationException() + } + + override fun isShutdown(): Boolean { + return false + } + + override fun isTerminated(): Boolean { + return false + } + + override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean { + throw UnsupportedOperationException() + } + + override fun submit(task: Callable): Future { + if (isSameThread()) return InPlaceFuture(task.call()) + return FutureTask(task).also { futureQueue.add(it); LockSupport.unpark(thread) } + } + + override fun submit(task: Runnable, result: T): Future { + if (isSameThread()) { task.run(); return InPlaceFuture(result) } + return FutureTask { task.run(); result }.also { futureQueue.add(it); LockSupport.unpark(thread) } + } + + override fun submit(task: Runnable): Future<*> { + if (isSameThread()) { task.run(); return InPlaceFuture.EMPTY } + return FutureTask { task.run() }.also { futureQueue.add(it); LockSupport.unpark(thread) } + } + + override fun invokeAll(tasks: Collection>): List> { + if (isSameThread()) { + return tasks.map { InPlaceFuture(it.call()) } + } else { + return tasks.map { submit(it) }.onEach { it.get() } + } + } + + override fun invokeAll( + tasks: Collection>, + timeout: Long, + unit: TimeUnit + ): List> { + if (isSameThread()) { + return tasks.map { InPlaceFuture(it.call()) } + } else { + return tasks.map { submit(it) }.onEach { it.get(timeout, unit) } + } + } + + override fun invokeAny(tasks: Collection>): T { + if (tasks.isEmpty()) + throw NoSuchElementException("Provided task list is empty") + + if (isSameThread()) { + return tasks.first().call() + } else { + return submit(tasks.first()).get() + } + } + + override fun invokeAny(tasks: Collection>, timeout: Long, unit: TimeUnit): T { + if (tasks.isEmpty()) + throw NoSuchElementException("Provided task list is empty") + + if (isSameThread()) { + return tasks.first().call() + } else { + return submit(tasks.first()).get(timeout, unit) + } + } + + var openglObjectsCreated = 0L private set - var objectsCleaned = 0L + var openglObjectsCleaned = 0L private set init { @@ -278,35 +407,52 @@ class StarboundClient : Closeable { } } - fun registerCleanable(ref: Any, fn: (Int) -> Unit, nativeRef: Int): Cleaner.Cleanable { - objectsCreated++ + fun registerCleanable(ref: Any, fn: IntConsumer, nativeRef: Int) { + openglObjectsCreated++ + val ref0 = CleanRef(ref, openglCleanQueue, fn, nativeRef) - val cleanable = cleaner.register(ref) { - if (isSameThread()) { - objectsCleaned++ - fn(nativeRef) - checkForGLError() - } else { - synchronized(cleanerBacklog) { - cleanerBacklog.add { - objectsCleaned++ - fn(nativeRef) - checkForGLError() - } - } - } + if (openglCleanQueueHead == null) { + openglCleanQueueHead = ref0 + } else { + ref0.next = openglCleanQueueHead + openglCleanQueueHead!!.prev = ref0 + openglCleanQueueHead = ref0 } - - return cleanable } - fun cleanup() { - synchronized(cleanerBacklog) { - for (lambda in cleanerBacklog) { - lambda.invoke() + private fun executeQueuedTasks() { + var next = executeQueue.poll() + + while (next != null) { + next.run() + next = executeQueue.poll() + } + + var next2 = futureQueue.poll() + + while (next2 != null) { + next2.run() + Thread.interrupted() + next2 = futureQueue.poll() + } + + var next3 = openglCleanQueue.poll() as CleanRef? + + while (next3 != null) { + openglObjectsCleaned++ + next3.fn.accept(next3.value) + checkForGLError("Removing unreachable OpenGL object") + + val head = openglCleanQueueHead + + if (next3 === head) { + openglCleanQueueHead = head.next + } else { + next3.prev?.next = next3.next + next3.next?.prev = next3.prev } - cleanerBacklog.clear() + next3 = openglCleanQueue.poll() as CleanRef? } } @@ -731,7 +877,7 @@ class StarboundClient : Closeable { onPostDrawWorldOnce.add(lambda) } - private val layers = LayeredRenderer() + private val layers = LayeredRenderer(this) fun renderFrame(): Boolean { ensureSameThread() @@ -740,6 +886,8 @@ class StarboundClient : Closeable { // try to sleep until next frame as precise as possible while (diff > 0L) { + executeQueuedTasks() + if (diff >= 1_500_000L) { LockSupport.parkNanos(1_000_000L) } else { @@ -760,7 +908,7 @@ class StarboundClient : Closeable { val world = world if (!isRenderingGame) { - cleanup() + executeQueuedTasks() GLFW.glfwPollEvents() if (world != null && Starbound.initialized) @@ -852,7 +1000,9 @@ class StarboundClient : Closeable { layers.render() + box2dRenderer.begin() world.physics.debugDraw() + box2dRenderer.render() for (lambda in onPostDrawWorld) { lambda.invoke() @@ -906,7 +1056,7 @@ class StarboundClient : Closeable { font.render("Latency: ${(averageRenderWait * 1_00000.0).toInt() / 100f}ms", scale = 0.4f) font.render("Frame: ${(averageRenderTime * 1_00000.0).toInt() / 100f}ms", y = font.lineHeight * 0.6f, scale = 0.4f) font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 1.2f, scale = 0.4f) - font.render("OGL C: $objectsCreated D: $objectsCleaned A: ${objectsCreated - objectsCleaned}", y = font.lineHeight * 1.8f, scale = 0.4f) + font.render("OGL C: $openglObjectsCreated D: $openglObjectsCleaned A: ${openglObjectsCreated - openglObjectsCleaned}", y = font.lineHeight * 1.8f, scale = 0.4f) GLFW.glfwSwapBuffers(window) GLFW.glfwPollEvents() @@ -914,7 +1064,7 @@ class StarboundClient : Closeable { camera.think(Starbound.TICK_TIME_ADVANCE) - cleanup() + executeQueuedTasks() return true } finally { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt index 478be315..e3019f4f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt @@ -5,6 +5,7 @@ import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.gl.VertexArrayObject import ru.dbotthepony.kstarbound.client.gl.BufferObject import ru.dbotthepony.kstarbound.client.gl.checkForGLError +import java.util.concurrent.Callable /** * Быстрое наполнение буфера вершинами, загрузка в память видеокарты, и отрисовка @@ -13,29 +14,38 @@ class StreamVertexBuilder( attributes: VertexAttributes, type: GeometryType? = null, initialCapacity: Int = 32, + val client: StarboundClient = StarboundClient.current() ) { - val state = StarboundClient.current() val builder = VertexBuilder(attributes, type, initialCapacity) - private val vao = VertexArrayObject() - private val vbo = BufferObject.VBO() - private val ebo = BufferObject.EBO() - init { - vao.elementBuffer = ebo - vao.bindAttributes(vbo, attributes) - } + private val vao = client.submit(Callable { VertexArrayObject() }) + private val vbo = client.submit(Callable { BufferObject.VBO() }) + private val ebo = client.submit(Callable { BufferObject.EBO() }) + + private var initialized = false fun upload(drawType: Int = GL45.GL_DYNAMIC_DRAW) { - builder.upload(vbo, ebo, drawType) + if (vbo.isDone && ebo.isDone) + builder.upload(vbo.get(), ebo.get(), drawType) } - fun bind() = vao.bind() - fun unbind() = vao.unbind() + fun bind() = vao.get().bind() + fun unbind() = vao.get().unbind() fun draw(primitives: Int = GL45.GL_TRIANGLES) { + if (!initialized) { + vao.get().elementBuffer = ebo.get() + vao.get().bindAttributes(vbo.get(), builder.attributes) + initialized = true + } + bind() - GL45.glDrawElements(primitives, builder.indexCount, builder.indexType, 0L) - checkForGLError() - unbind() + + try { + GL45.glDrawElements(primitives, builder.indexCount, builder.indexType, 0L) + checkForGLError() + } finally { + unbind() + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt index 45c0e004..cc6519ae 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt @@ -16,7 +16,6 @@ import kotlin.math.sin class Box2DRenderer : IDebugDraw { val state = StarboundClient.current() - private val identity = Matrix3f.identity() override var drawShapes: Boolean = false override var drawJoints: Boolean = false @@ -25,51 +24,57 @@ class Box2DRenderer : IDebugDraw { override var drawPairs: Boolean = false override var drawCenterOfMess: Boolean = false - private val builder = StreamVertexBuilder(VertexAttributes.POSITION) + private val identity = Matrix3f.identity() + + private val lines = StreamVertexBuilder(VertexAttributes.POSITION_COLOR, GeometryType.LINES) + private val triangles = StreamVertexBuilder(VertexAttributes.POSITION_COLOR, GeometryType.TRIANGLES) + + fun begin() { + lines.builder.begin() + triangles.builder.begin() + } + + fun render() { + if (lines.builder.isEmpty() && triangles.builder.isEmpty()) return + + lines.upload(GL_STREAM_DRAW) + triangles.upload(GL_STREAM_DRAW) + + state.programs.positionColor.use() + state.programs.positionColor.colorMultiplier = RGBAColor.WHITE + state.programs.positionColor.modelMatrix = identity + + lines.draw(GL_LINES) + triangles.draw(GL_TRIANGLES) + } override fun drawPolygon(vertices: List, color: RGBAColor) { require(vertices.size > 1) { "Vertex list had only ${vertices.size} namings in it" } - builder.builder.begin() + if (!vertices.any { state.viewportRectangle.isInside(it) }) return for (i in vertices.indices) { val current = vertices[i] val next = vertices[(i + 1) % vertices.size] - builder.builder.vertex(current.x.toFloat(), current.y.toFloat()) - builder.builder.vertex(next.x.toFloat(), next.y.toFloat()) + lines.builder.vertex(state.stack.last(), current.x.toFloat(), current.y.toFloat()).color(color) + lines.builder.vertex(state.stack.last(), next.x.toFloat(), next.y.toFloat()).color(color) } - - builder.upload() - - state.programs.position.use() - state.programs.position.colorMultiplier = color - state.programs.position.modelMatrix = state.stack.last() - - builder.draw(GL_LINES) } private fun drawSolid(vertices: List, color: RGBAColor) { require(vertices.size >= 3) { "Vertex list had only ${vertices.size} namings in it" } - builder.builder.begin(GeometryType.TRIANGLES) + if (!vertices.any { state.viewportRectangle.isInside(it) }) return val zero = vertices[0] for (i in 1 until vertices.size) { val current = vertices[i] val next = vertices[(i + 1) % vertices.size] - builder.builder.vertex(zero.x.toFloat(), zero.y.toFloat()) - builder.builder.vertex(current.x.toFloat(), current.y.toFloat()) - builder.builder.vertex(next.x.toFloat(), next.y.toFloat()) + triangles.builder.vertex(state.stack.last(), zero.x.toFloat(), zero.y.toFloat()).color(color) + triangles.builder.vertex(state.stack.last(), current.x.toFloat(), current.y.toFloat()).color(color) + triangles.builder.vertex(state.stack.last(), next.x.toFloat(), next.y.toFloat()).color(color) } - - builder.upload() - - state.programs.position.use() - state.programs.position.colorMultiplier = color - state.programs.position.modelMatrix = state.stack.last() - - builder.draw(GL_TRIANGLES) } override fun drawSolidPolygon(vertices: List, color: RGBAColor) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderer.kt index 22b1e663..b0f894c7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderer.kt @@ -3,8 +3,10 @@ package ru.dbotthepony.kstarbound.client.render import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap import org.lwjgl.opengl.GL15.GL_STREAM_DRAW +import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder +import java.util.concurrent.Callable import java.util.function.Function import java.util.stream.Stream @@ -38,9 +40,10 @@ object OneShotGeometryLayer : IGeometryLayer { } } -class LayeredRenderer { - class Layer(val layer: RenderLayer.Point) : IGeometryLayer { - private data class Tracker(val builder: StreamVertexBuilder, var emptyFrames: Int = 0) +class LayeredRenderer(val client: StarboundClient) { + private data class Tracker(val builder: StreamVertexBuilder, var emptyFrames: Int = 0) + + inner class Layer(val layer: RenderLayer.Point) : IGeometryLayer { private val meshes = ArrayList>() private val callbacks = ArrayList<() -> Unit>() private val builders = Reference2ObjectLinkedOpenHashMap, Tracker>() @@ -51,7 +54,7 @@ class LayeredRenderer { override fun getBuilder(config: RenderConfig<*>): VertexBuilder { return builders.computeIfAbsent(config, Function { - Tracker(StreamVertexBuilder(config.program.attributes, initialCapacity = config.initialBuilderCapacity)) + Tracker(StreamVertexBuilder(config.program.attributes, initialCapacity = config.initialBuilderCapacity, client = client)) }).builder.builder } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt index ad08347a..d9432081 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -20,6 +20,7 @@ import ru.dbotthepony.kvector.arrays.Matrix3f import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.Vector2i import java.time.Duration +import java.util.concurrent.Callable import kotlin.collections.HashMap /** @@ -51,14 +52,14 @@ class TileRenderers(val client: StarboundClient) { fun getMaterialRenderer(defName: String): TileRenderer { return matCache.get(defName) { val def = Registries.tiles[defName] // TODO: Пустой рендерер - TileRenderer(this, def!!.value) + client.submit(Callable { TileRenderer(this, def!!.value) }).get() } } fun getModifierRenderer(defName: String): TileRenderer { return modCache.get(defName) { val def = Registries.tileModifiers[defName] // TODO: Пустой рендерер - TileRenderer(this, def!!.value) + client.submit(Callable { TileRenderer(this, def!!.value) }).get() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index 87c589c3..71c83c8f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -5,7 +5,12 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.LongArraySet import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ReferenceArraySet +import ru.dbotthepony.kbox2d.api.BodyDef +import ru.dbotthepony.kbox2d.api.BodyType +import ru.dbotthepony.kbox2d.api.FixtureDef +import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.render.ConfiguredMesh import ru.dbotthepony.kstarbound.client.render.LayeredRenderer @@ -31,6 +36,9 @@ import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector2i +import java.util.concurrent.Callable +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.Future import kotlin.math.PI import kotlin.math.cos import kotlin.math.sin @@ -80,42 +88,56 @@ class ClientWorld( inner class RenderRegion(val x: Int, val y: Int) { inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) { val bakedMeshes = ArrayList, RenderLayer.Point>>() + private var currentBakeTask: Future? = null var isDirty = true fun bake() { - if (!isDirty) return + if (!isDirty) { + val currentBakeTask = currentBakeTask ?: return + + if (currentBakeTask.isDone) { + bakedMeshes.clear() + + for ((baked, zLevel) in currentBakeTask.get().bakeIntoMeshes()) { + bakedMeshes.add(baked to zLevel) + } + + this.currentBakeTask = null + } + + return + } + isDirty = false - bakedMeshes.clear() + currentBakeTask = ForkJoinPool.commonPool().submit(Callable { + val meshes = LayeredRenderer(client) - val meshes = LayeredRenderer() + for (x in 0 until renderRegionWidth) { + for (y in 0 until renderRegionHeight) { + if (!inBounds(x, y)) continue - for (x in 0 until renderRegionWidth) { - for (y in 0 until renderRegionHeight) { - if (!inBounds(x, y)) continue + val tile = view.getTile(x, y) ?: continue + val material = tile.material - val tile = view.getTile(x, y) ?: continue - val material = tile.material + if (!material.isMeta) { + client.tileRenderers + .getMaterialRenderer(material.materialName) + .tesselate(tile, view, meshes, Vector2i(x, y), isBackground = isBackground) + } - if (!material.isMeta) { - client.tileRenderers - .getMaterialRenderer(material.materialName) - .tesselate(tile, view, meshes, Vector2i(x, y), isBackground = isBackground) - } + val modifier = tile.modifier - val modifier = tile.modifier - - if (modifier != null) { - client.tileRenderers - .getModifierRenderer(modifier.modName) - .tesselate(tile, view, meshes, Vector2i(x, y), isBackground = isBackground, isModifier = true) + if (modifier != null) { + client.tileRenderers + .getModifierRenderer(modifier.modName) + .tesselate(tile, view, meshes, Vector2i(x, y), isBackground = isBackground, isModifier = true) + } } } - } - for ((baked, zLevel) in meshes.bakeIntoMeshes()) { - bakedMeshes.add(baked to zLevel) - } + meshes + }) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index b893cd3c..3df15be3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -2,6 +2,8 @@ package ru.dbotthepony.kstarbound.world import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import ru.dbotthepony.kbox2d.api.BodyDef +import ru.dbotthepony.kbox2d.api.FixtureDef +import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape import ru.dbotthepony.kbox2d.dynamics.B2Fixture import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition @@ -290,38 +292,19 @@ abstract class Chunk, This : Chunk xSpanSize * ySpanSize) { - depthFirst.markSeen() - aabb = AABB(depthFirst.mins.toDoubleVector(), depthFirst.maxs.toDoubleVector() + Vector2d.POSITIVE_XY) - } else { - sizeFirst.markSeen() - aabb = AABB(sizeFirst.mins.toDoubleVector(), sizeFirst.maxs.toDoubleVector() + Vector2d.POSITIVE_XY) - } + val cell = getCell(x, y) + /*if (!cell.foreground.material.collisionKind.isEmpty) { collisionChains.add(body.createFixture(FixtureDef( - shape = PolygonShape().also { it.setAsBox(aabb.width / 2.0, aabb.height / 2.0, aabb.centre, 0.0) }, + shape = PolygonShape().also { it.setAsBox(0.5, 0.5, Vector2d(x + 0.5, y + 0.5), 0.0) }, friction = 0.4, + userData = cell, ))) - - collisionCache.add(aabb + xyAdd) - } + }*/ } - }*/ + } } /** diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderDepthFirst.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderDepthFirst.kt deleted file mode 100644 index 8ee5ef88..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderDepthFirst.kt +++ /dev/null @@ -1,177 +0,0 @@ -package ru.dbotthepony.kstarbound.world.phys - -import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_BITS -import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF -import ru.dbotthepony.kstarbound.world.api.ICellAccess -import ru.dbotthepony.kstarbound.world.api.IChunkCell -import ru.dbotthepony.kvector.arrays.Object2DArray -import ru.dbotthepony.kvector.vector.Vector2i - -class RectTileFlooderDepthFirst( - private val tiles: ICellAccess, - private val seen: BooleanArray, - rootx: Int, - rooty: Int -) { - val mins: Vector2i - val maxs: Vector2i - - private fun filled(x: Int, y: Int): Boolean { - if (x < 0 || x > CHUNK_SIZE_FF) { - return false - } - - if (y < 0 || y > CHUNK_SIZE_FF) { - return false - } - - return !seen[x or (y shl CHUNK_SIZE_BITS)] && tiles.getCell(x, y)?.foreground?.material != null - } - - init { - // expand wide - var widthPositive = 1 - - while (true) { - if (!filled(rootx + widthPositive, rooty)) { - break - } - - widthPositive++ - } - - var widthNegative = 1 - - while (true) { - if (!filled(rootx - widthNegative, rooty)) { - break - } - - widthNegative++ - } - - // expand tall - var heightPositive = 1 - - while (true) { - if (!filled(rootx, rooty + heightPositive)) { - break - } - - heightPositive++ - } - - var heightNegative = 1 - - while (true) { - if (!filled(rootx, rooty - heightNegative)) { - break - } - - heightNegative++ - } - - widthPositive -= 1 - widthNegative -= 1 - - heightNegative -= 1 - heightPositive -= 1 - - if (heightPositive + heightNegative > widthPositive + widthNegative) { - // height is bigger - // try to expand wide - widthPositive = 0 - widthNegative = 0 - - while (true) { - var escape = false - - for (i in -heightNegative .. heightPositive) { - if (!filled(rootx + widthPositive, rooty + i)) { - escape = true - break - } - } - - if (escape) { - break - } - - widthPositive++ - } - - while (true) { - var escape = false - - for (i in -heightNegative .. heightPositive) { - if (!filled(rootx - widthNegative, rooty + i)) { - escape = true - break - } - } - - if (escape) { - break - } - - widthNegative++ - } - - widthNegative -= 1 - widthPositive -= 1 - } else { - // height is equal or lesser than width - // expand high - heightNegative = 0 - heightPositive = 0 - - while (true) { - var escape = false - - for (i in -widthNegative .. widthPositive) { - if (!filled(rootx + i, rooty + heightPositive)) { - escape = true - break - } - } - - if (escape) { - break - } - - heightPositive++ - } - - while (true) { - var escape = false - - for (i in -widthNegative .. widthPositive) { - if (!filled(rootx + i, rooty - heightNegative)) { - escape = true - break - } - } - - if (escape) { - break - } - - heightNegative++ - } - - heightNegative -= 1 - heightPositive -= 1 - } - - mins = Vector2i(rootx - widthNegative, rooty - heightNegative) - maxs = Vector2i(rootx + widthPositive, rooty + heightPositive) - } - - fun markSeen() { - for (x in mins.x .. maxs.x) { - for (y in mins.y .. maxs.y) { - seen[x or (y shl CHUNK_SIZE_BITS)] = true - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt deleted file mode 100644 index ade6cec3..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt +++ /dev/null @@ -1,146 +0,0 @@ -package ru.dbotthepony.kstarbound.world.phys - -import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_BITS -import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF -import ru.dbotthepony.kstarbound.world.api.ICellAccess -import ru.dbotthepony.kstarbound.world.api.IChunkCell -import ru.dbotthepony.kvector.arrays.Object2DArray -import ru.dbotthepony.kvector.vector.Vector2i - -class RectTileFlooderSizeFirst( - private val tiles: ICellAccess, - private val seen: BooleanArray, - private val rootx: Int, - private val rooty: Int -) { - val mins: Vector2i - val maxs: Vector2i - - private fun filled(x: Int, y: Int): Boolean { - if (x < 0 || x > CHUNK_SIZE_FF) { - return false - } - - if (y < 0 || y > CHUNK_SIZE_FF) { - return false - } - - return !seen[x or (y shl CHUNK_SIZE_BITS)] && tiles.getCell(x, y)?.foreground?.material != null - } - - private var widthPositive = 0 - private var widthNegative = 0 - private var heightPositive = 0 - private var heightNegative = 0 - - private fun checkLeft(): Boolean { - for (i in -heightNegative .. heightPositive) { - if (!filled(rootx - widthNegative, rooty + i)) { - return false - } - } - - return true - } - - private fun checkRight(): Boolean { - for (i in -heightNegative .. heightPositive) { - if (!filled(rootx + widthPositive, rooty + i)) { - return false - } - } - - return true - } - - private fun checkUp(): Boolean { - for (i in -widthNegative .. widthPositive) { - if (!filled(rootx + i, rooty + heightPositive)) { - return false - } - } - - return true - } - - private fun checkDown(): Boolean { - for (i in -widthNegative .. widthPositive) { - if (!filled(rootx + i, rooty - heightNegative)) { - return false - } - } - - return true - } - - init { - var expanded = true - var hitLeft = false - var hitRight = false - var hitUp = false - var hitDown = false - - while (expanded) { - expanded = false - - // expand left - if (!hitLeft) { - widthNegative++ - - if (!checkLeft()) { - widthNegative-- - hitLeft = true - } else { - expanded = true - } - } - - // expand up - if (!hitUp) { - heightPositive++ - - if (!checkUp()) { - heightPositive-- - hitUp = true - } else { - expanded = true - } - } - - // expand right - if (!hitRight) { - widthPositive++ - - if (!checkRight()) { - widthPositive-- - hitRight = true - } else { - expanded = true - } - } - - // expand down - if (!hitDown) { - heightNegative++ - - if (!checkDown()) { - heightNegative-- - hitDown = true - } else { - expanded = true - } - } - } - - mins = Vector2i(rootx - widthNegative, rooty - heightNegative) - maxs = Vector2i(rootx + widthPositive, rooty + heightPositive) - } - - fun markSeen() { - for (x in mins.x .. maxs.x) { - for (y in mins.y .. maxs.y) { - seen[x or (y shl CHUNK_SIZE_BITS)] = true - } - } - } -} \ No newline at end of file