multithreaded chunk geometry batching

This commit is contained in:
DBotThePony 2023-10-02 11:07:55 +07:00
parent 71a6f388fc
commit 6ae8066ebc
Signed by: DBot
GPG Key ID: DCC23B5715498507
11 changed files with 322 additions and 514 deletions

View File

@ -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")

View File

@ -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)

View File

@ -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<ScissorRect>()
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<Runnable>()
private val futureQueue = ConcurrentLinkedQueue<FutureTask<*>>()
private val openglCleanQueue = ReferenceQueue<Any>()
private var openglCleanQueueHead: CleanRef? = null
private class CleanRef(ref: Any, queue: ReferenceQueue<Any>, val fn: IntConsumer, val value: Int) : PhantomReference<Any>(ref, queue) {
var next: CleanRef? = null
var prev: CleanRef? = null
}
var objectsCreated = 0L
private data class InPlaceFuture<V>(private val value: V) : Future<V> {
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<Runnable> {
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 <T : Any?> submit(task: Callable<T>): Future<T> {
if (isSameThread()) return InPlaceFuture(task.call())
return FutureTask(task).also { futureQueue.add(it); LockSupport.unpark(thread) }
}
override fun <T : Any?> submit(task: Runnable, result: T): Future<T> {
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 <T : Any?> invokeAll(tasks: Collection<Callable<T>>): List<Future<T>> {
if (isSameThread()) {
return tasks.map { InPlaceFuture(it.call()) }
} else {
return tasks.map { submit(it) }.onEach { it.get() }
}
}
override fun <T : Any?> invokeAll(
tasks: Collection<Callable<T>>,
timeout: Long,
unit: TimeUnit
): List<Future<T>> {
if (isSameThread()) {
return tasks.map { InPlaceFuture(it.call()) }
} else {
return tasks.map { submit(it) }.onEach { it.get(timeout, unit) }
}
}
override fun <T : Any?> invokeAny(tasks: Collection<Callable<T>>): 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 <T : Any?> invokeAny(tasks: Collection<Callable<T>>, 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 {

View File

@ -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()
}
}
}

View File

@ -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<Vector2d>, 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<Vector2d>, 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<Vector2d>, color: RGBAColor) {

View File

@ -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<ConfiguredMesh<*>>()
private val callbacks = ArrayList<() -> Unit>()
private val builders = Reference2ObjectLinkedOpenHashMap<RenderConfig<*>, 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
}

View File

@ -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()
}
}

View File

@ -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<Pair<ConfiguredMesh<*>, RenderLayer.Point>>()
private var currentBakeTask: Future<LayeredRenderer>? = 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
})
}
}

View File

@ -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<WorldType : World<WorldType, This>, This : Chunk<WorldType,
collisionChains.clear()
val xyAdd = Vector2d(pos.x * CHUNK_SIZEd, pos.y * CHUNK_SIZEd)
val seen = BooleanArray(CHUNK_SIZE * CHUNK_SIZE)
/*for (y in 0 .. CHUNK_SIZE_FF) {
for (y in 0 .. CHUNK_SIZE_FF) {
for (x in 0..CHUNK_SIZE_FF) {
if (!seen[x or (y shl CHUNK_SIZE_BITS)] && cells[x, y].foreground.material != null) {
val depthFirst = RectTileFlooderDepthFirst(this, seen, x, y)
val sizeFirst = RectTileFlooderSizeFirst(this, seen, x, y)
val xSpanDepth = depthFirst.maxs.x - depthFirst.mins.x
val ySpanDepth = depthFirst.maxs.y - depthFirst.mins.y
val xSpanSize = sizeFirst.maxs.x - sizeFirst.mins.x
val ySpanSize = sizeFirst.maxs.y - sizeFirst.mins.y
val aabb: AABB
if (xSpanDepth * ySpanDepth > 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)
}
}*/
}
}*/
}
}
/**

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}
}