Skip to content

Commit

Permalink
Improved performance and reduced stack complexity
Browse files Browse the repository at this point in the history
  • Loading branch information
Martomate committed Jul 18, 2024
1 parent 5dd46e7 commit b6328c0
Show file tree
Hide file tree
Showing 14 changed files with 325 additions and 195 deletions.
14 changes: 11 additions & 3 deletions client/src/main/scala/hexacraft/client/DebugOverlay.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,26 @@ object DebugOverlay {
cameraRotation: Vector3f,
viewDistance: Double,
regularChunkBufferFragmentation: IndexedSeq[Float],
transmissiveChunkBufferFragmentation: IndexedSeq[Float]
transmissiveChunkBufferFragmentation: IndexedSeq[Float],
renderQueueLength: Int
)

object Content {
def fromCamera(
camera: Camera,
viewDistance: Double,
regularChunkBufferFragmentation: IndexedSeq[Float],
transmissiveChunkBufferFragmentation: IndexedSeq[Float]
transmissiveChunkBufferFragmentation: IndexedSeq[Float],
renderQueueLength: Int
)(using CylinderSize): Content = {
Content(
CylCoords(camera.position),
camera.blockCoords.getChunkRelWorld,
camera.rotation,
viewDistance,
regularChunkBufferFragmentation,
transmissiveChunkBufferFragmentation
transmissiveChunkBufferFragmentation,
renderQueueLength
)
}
}
Expand Down Expand Up @@ -67,6 +70,7 @@ class DebugOverlay {
addDebugText("viewDist", "viewDistance")
addDebugText("fragmentation.opaque", "fragmentation (opaque)")
addDebugText("fragmentation.transmissive", "fragmentation (transmissive)")
addDebugText("renderQueueLength", "render queue")

private def addLabel(text: String): Unit = {
yOff += 0.02f
Expand Down Expand Up @@ -117,6 +121,10 @@ class DebugOverlay {
"fragmentation.transmissive",
info.transmissiveChunkBufferFragmentation.sortBy(-_).map(v => f"$v%.2f").mkString(" ")
)
setValue(
"renderQueueLength",
info.renderQueueLength
)
}

def render(transformation: GUITransformation)(using context: RenderContext): Unit = {
Expand Down
13 changes: 10 additions & 3 deletions client/src/main/scala/hexacraft/client/GameClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ class GameClient(

prio.tick(PosAndDir.fromCameraView(camera.view))

var chunksToLoad = 2
var chunksToLoad = 5
while chunksToLoad > 0 do {
prio.nextAddableChunk match {
case Some(chunkCoords) =>
Expand Down Expand Up @@ -553,7 +553,7 @@ class GameClient(
chunksToLoad -= 1
}

var chunksToUnload = 3
var chunksToUnload = 6
while chunksToUnload > 0 do {
prio.nextRemovableChunk match {
case Some(chunkCoords) =>
Expand Down Expand Up @@ -630,9 +630,16 @@ class GameClient(
if debugOverlay.isDefined then {
val regularFragmentation = worldRenderer.regularChunkBufferFragmentation
val transmissiveFragmentation = worldRenderer.transmissiveChunkBufferFragmentation
val renderQueueLength = worldRenderer.renderQueueLength

debugOverlay.get.updateContent(
DebugOverlay.Content.fromCamera(camera, world.renderDistance, regularFragmentation, transmissiveFragmentation)
DebugOverlay.Content.fromCamera(
camera,
world.renderDistance,
regularFragmentation,
transmissiveFragmentation,
renderQueueLength
)
)
}

Expand Down
17 changes: 14 additions & 3 deletions client/src/main/scala/hexacraft/client/WorldRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ class WorldRenderer(
def transmissiveChunkBufferFragmentation: IndexedSeq[Float] =
transmissiveBlockRenderers.map(a => a.values.map(_.fragmentation).sum / a.keys.size)

def renderQueueLength: Int = {
chunkRenderUpdateQueue.length
}

def tick(camera: Camera, renderDistance: Double, worldTickResult: WorldTickResult): Unit = {
// Step 1: Perform render updates using data calculated in the background since the previous frame
updateBlockData(futureRenderData.map((coords, fut) => (coords, Await.result(fut, Duration.Inf))).toSeq)
Expand All @@ -92,6 +96,9 @@ class WorldRenderer(
if world.getChunk(coords).isEmpty then {
// clear the chunk immediately so it doesn't have to be drawn (the PQ is in closest first order)
futureRenderData += coords -> Future.successful(ChunkRenderData.empty)
} else if !coords.neighbors.forall(n => world.getChunk(n).isDefined) then {
// some neighbor has not been loaded yet, so let's not render this chunk yet
futureRenderData += coords -> Future.successful(ChunkRenderData.empty)
}
chunkRenderUpdateQueue.insert(coords)
}
Expand All @@ -100,14 +107,18 @@ class WorldRenderer(
chunkRenderUpdateQueue.reorderAndFilter(camera, renderDistance)
}

var numUpdatesToPerform = math.min(chunkRenderUpdateQueue.length, 5)
var numUpdatesToPerform = math.min(chunkRenderUpdateQueue.length, 15)
while numUpdatesToPerform > 0 do {
chunkRenderUpdateQueue.pop() match {
case Some(coords) =>
world.getChunk(coords) match {
case Some(chunk) =>
futureRenderData += coords -> Future(ChunkRenderData(coords, chunk.blocks, world, blockTextureIndices))
numUpdatesToPerform -= 1
if coords.neighbors.forall(n => world.getChunk(n).isDefined) then {
futureRenderData += coords -> Future(ChunkRenderData(coords, chunk.blocks, world, blockTextureIndices))
numUpdatesToPerform -= 1
} else {
futureRenderData += coords -> Future.successful(ChunkRenderData.empty)
}
case None =>
futureRenderData += coords -> Future.successful(ChunkRenderData.empty)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package hexacraft.client.render

import hexacraft.util.UniquePQ
import hexacraft.util.UniqueLongPQ
import hexacraft.world.{Camera, CylinderSize, PosAndDir}
import hexacraft.world.coord.{BlockCoords, BlockRelWorld, ChunkRelWorld, CylCoords}

class ChunkRenderUpdateQueue(using CylinderSize) {
private val origin = PosAndDir(CylCoords(0, 0, 0))

private val queue: UniquePQ[ChunkRelWorld] = new UniquePQ(makeChunkToUpdatePriority, Ordering.by(-_))
private val queue: UniqueLongPQ = new UniqueLongPQ(makeChunkToUpdatePriority, Ordering.by(-_))

def reorderAndFilter(camera: Camera, renderDistance: Double): Unit = {
origin.setPosAndDirFrom(camera.view)
Expand All @@ -20,17 +20,19 @@ class ChunkRenderUpdateQueue(using CylinderSize) {

def pop(): Option[ChunkRelWorld] = {
if !queue.isEmpty then {
Some(queue.dequeue())
Some(ChunkRelWorld(queue.dequeue()))
} else {
None
}
}

def insert(coords: ChunkRelWorld): Unit = {
queue.enqueue(coords)
queue.enqueue(coords.value)
}

private def makeChunkToUpdatePriority(coords: ChunkRelWorld): Double = {
private def makeChunkToUpdatePriority(coordsValue: Long): Double = {
val coords = ChunkRelWorld(coordsValue)

def distTo(x: Int, y: Int, z: Int): Double = {
val cyl = BlockCoords(BlockRelWorld(x, y, z, coords)).toCylCoords
val cDir = cyl.toNormalCoords(origin.pos).toVector3d.normalize()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ package hexacraft.util
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer

class UniquePQ[S](func: S => Double, ord: Ordering[Double]) { // PQ with fast lookup and resorting
private type DS = (Double, S)
class UniqueLongPQ(func: Long => Double, ord: Ordering[Double]) { // PQ with fast lookup and resorting
private type DS = (Double, Long)

private val pq: mutable.PriorityQueue[DS] = mutable.PriorityQueue.empty(using ord.on(_._1))
private val set: mutable.Set[S] = mutable.HashSet.empty
private val set: LongSet = new LongSet

def enqueue(elem: S): Unit = {
def enqueue(elem: Long): Unit = {
if set.add(elem) then {
pq.enqueue((func(elem), elem))
}
}

def dequeue(): S = {
def dequeue(): Long = {
val elem = pq.dequeue()._2
set -= elem
set.remove(elem)
elem
}

Expand All @@ -37,11 +37,11 @@ class UniquePQ[S](func: S => Double, ord: Ordering[Double]) { // PQ with fast lo
val elem = (func(t._2), t._2)
if filterFunc(elem) then {
buffer += elem
set.add(t._2)
}
}
pq.clear()
val seq = buffer.toSeq
pq.enqueue(seq*)
buffer.foreach(set += _._2)
}
}
76 changes: 76 additions & 0 deletions common/src/main/scala/hexacraft/util/UniqueLongQueue.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package hexacraft.util

import scala.collection.mutable

class LongSet {
private val sets: mutable.LongMap[mutable.BitSet] = mutable.LongMap.empty

def add(elem: Long): Boolean = {
val chunk = elem >>> 12
val block = (elem & ((1 << 12) - 1)).toInt
val set = sets.getOrElseUpdate(chunk, new mutable.BitSet(16 * 16 * 16))

val isNew = !set.contains(block)
if isNew then {
set.addOne(block)
}
isNew
}

def remove(elem: Long): Unit = {
val chunk = elem >>> 12
val block = (elem & ((1 << 12) - 1)).toInt
val set = sets.getOrElseUpdate(chunk, new mutable.BitSet(16 * 16 * 16))

set.subtractOne(block)
}

def clear(): Unit = {
sets.clear()
}
}

class UniqueLongQueue {
private val q: mutable.Queue[Long] = mutable.Queue.empty
private val set: LongSet = new LongSet

def enqueue(elem: Long): Unit = {
if set.add(elem) then {
q.enqueue(elem)
}
}

def enqueueMany(elems: Iterable[Long]): Unit = {
q.ensureSize(q.size + elems.size)

val it = elems.iterator
while it.hasNext do {
val elem = it.next
if set.add(elem) then {
q.enqueue(elem)
}
}
}

def dequeue(): Long = {
val elem = q.dequeue()
set.remove(elem)
elem
}

inline def drainInto(inline f: Long => Unit): Unit = {
set.clear()
while q.nonEmpty do {
f(q.dequeue())
}
}

def size: Int = q.length

def isEmpty: Boolean = q.isEmpty

def clear(): Unit = {
q.clear()
set.clear()
}
}
29 changes: 0 additions & 29 deletions common/src/main/scala/hexacraft/util/UniqueQueue.scala

This file was deleted.

Loading

0 comments on commit b6328c0

Please sign in to comment.