diff --git a/client/src/main/scala/hexacraft/client/DebugOverlay.scala b/client/src/main/scala/hexacraft/client/DebugOverlay.scala index 6676587f..f63cb44f 100644 --- a/client/src/main/scala/hexacraft/client/DebugOverlay.scala +++ b/client/src/main/scala/hexacraft/client/DebugOverlay.scala @@ -17,7 +17,8 @@ object DebugOverlay { cameraRotation: Vector3f, viewDistance: Double, regularChunkBufferFragmentation: IndexedSeq[Float], - transmissiveChunkBufferFragmentation: IndexedSeq[Float] + transmissiveChunkBufferFragmentation: IndexedSeq[Float], + renderQueueLength: Int ) object Content { @@ -25,7 +26,8 @@ object DebugOverlay { camera: Camera, viewDistance: Double, regularChunkBufferFragmentation: IndexedSeq[Float], - transmissiveChunkBufferFragmentation: IndexedSeq[Float] + transmissiveChunkBufferFragmentation: IndexedSeq[Float], + renderQueueLength: Int )(using CylinderSize): Content = { Content( CylCoords(camera.position), @@ -33,7 +35,8 @@ object DebugOverlay { camera.rotation, viewDistance, regularChunkBufferFragmentation, - transmissiveChunkBufferFragmentation + transmissiveChunkBufferFragmentation, + renderQueueLength ) } } @@ -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 @@ -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 = { diff --git a/client/src/main/scala/hexacraft/client/GameClient.scala b/client/src/main/scala/hexacraft/client/GameClient.scala index 36f3569f..1ff69e2e 100644 --- a/client/src/main/scala/hexacraft/client/GameClient.scala +++ b/client/src/main/scala/hexacraft/client/GameClient.scala @@ -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) => @@ -553,7 +553,7 @@ class GameClient( chunksToLoad -= 1 } - var chunksToUnload = 3 + var chunksToUnload = 6 while chunksToUnload > 0 do { prio.nextRemovableChunk match { case Some(chunkCoords) => @@ -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 + ) ) } diff --git a/client/src/main/scala/hexacraft/client/WorldRenderer.scala b/client/src/main/scala/hexacraft/client/WorldRenderer.scala index 3c14cff0..053ddf7b 100644 --- a/client/src/main/scala/hexacraft/client/WorldRenderer.scala +++ b/client/src/main/scala/hexacraft/client/WorldRenderer.scala @@ -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) @@ -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) } @@ -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) } diff --git a/client/src/main/scala/hexacraft/client/render/ChunkRenderUpdateQueue.scala b/client/src/main/scala/hexacraft/client/render/ChunkRenderUpdateQueue.scala index 914bfcf9..3f3b418c 100644 --- a/client/src/main/scala/hexacraft/client/render/ChunkRenderUpdateQueue.scala +++ b/client/src/main/scala/hexacraft/client/render/ChunkRenderUpdateQueue.scala @@ -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) @@ -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() diff --git a/common/src/main/scala/hexacraft/util/UniquePQ.scala b/common/src/main/scala/hexacraft/util/UniqueLongPQ.scala similarity index 72% rename from common/src/main/scala/hexacraft/util/UniquePQ.scala rename to common/src/main/scala/hexacraft/util/UniqueLongPQ.scala index 01ef8969..28e5118c 100644 --- a/common/src/main/scala/hexacraft/util/UniquePQ.scala +++ b/common/src/main/scala/hexacraft/util/UniqueLongPQ.scala @@ -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 } @@ -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) } } diff --git a/common/src/main/scala/hexacraft/util/UniqueLongQueue.scala b/common/src/main/scala/hexacraft/util/UniqueLongQueue.scala new file mode 100644 index 00000000..de9d37b0 --- /dev/null +++ b/common/src/main/scala/hexacraft/util/UniqueLongQueue.scala @@ -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() + } +} diff --git a/common/src/main/scala/hexacraft/util/UniqueQueue.scala b/common/src/main/scala/hexacraft/util/UniqueQueue.scala deleted file mode 100644 index 16430bea..00000000 --- a/common/src/main/scala/hexacraft/util/UniqueQueue.scala +++ /dev/null @@ -1,29 +0,0 @@ -package hexacraft.util - -import scala.collection.mutable - -class UniqueQueue[S] { - private val q: mutable.Queue[S] = mutable.Queue.empty - private val set: mutable.Set[S] = mutable.HashSet.empty - - def enqueue(elem: S): Unit = { - if set.add(elem) then { - q.enqueue(elem) - } - } - - def dequeue(): S = { - val elem = q.dequeue() - set -= elem - elem - } - - def size: Int = q.size - - def isEmpty: Boolean = q.isEmpty - - def clear(): Unit = { - q.clear() - set.clear() - } -} diff --git a/common/src/test/scala/hexacraft/util/UniquePQTest.scala b/common/src/test/scala/hexacraft/util/UniquePQTest.scala index 5d1baa52..1c8aff5a 100644 --- a/common/src/test/scala/hexacraft/util/UniquePQTest.scala +++ b/common/src/test/scala/hexacraft/util/UniquePQTest.scala @@ -6,114 +6,114 @@ class UniquePQTest extends FunSuite { private val defOrder = Ordering.Double.TotalOrdering test("an empty queue should have size 0 from the beginning") { - assertEquals(new UniquePQ[String](_ => 1, defOrder).size, 0) + assertEquals(new UniqueLongPQ(_ => 1, defOrder).size, 0) } test("an empty queue should be empty") { - assert(new UniquePQ[String](_ => 1, defOrder).isEmpty) + assert(new UniqueLongPQ(_ => 1, defOrder).isEmpty) } test("an empty queue should have size 1 after enqueue") { - val q = new UniquePQ[String](_ => 1, defOrder) - q.enqueue("a") + val q = new UniqueLongPQ(_ => 1, defOrder) + q.enqueue(13) assertEquals(q.size, 1) } test("an empty queue should fail to dequeue") { - intercept[NoSuchElementException](new UniquePQ[String](_ => 1, defOrder).dequeue()) + intercept[NoSuchElementException](new UniqueLongPQ(_ => 1, defOrder).dequeue()) } test("a non-empty queue should not be empty") { - val q = new UniquePQ[String](_ => 1, defOrder) - q.enqueue("a") + val q = new UniqueLongPQ(_ => 1, defOrder) + q.enqueue(13) assert(!q.isEmpty) } test("a non-empty queue should have the right size from the beginning") { - val q = new UniquePQ[String](_ => 1, defOrder) - q.enqueue("a") - q.enqueue("b") - q.enqueue("c") + val q = new UniqueLongPQ(_ => 1, defOrder) + q.enqueue(13) + q.enqueue(14) + q.enqueue(15) assertEquals(q.size, 3) } test("a non-empty queue should increase it's size after enqueueing a new element") { - val q = new UniquePQ[String](_ => 1, defOrder) - q.enqueue("a") - q.enqueue("b") - q.enqueue("a") + val q = new UniqueLongPQ(_ => 1, defOrder) + q.enqueue(13) + q.enqueue(14) + q.enqueue(13) q.dequeue() - q.enqueue("c") + q.enqueue(15) q.dequeue() - q.enqueue("c") + q.enqueue(15) val s = q.size - q.enqueue("d") + q.enqueue(16) assertEquals(q.size, s + 1) } test("a non-empty queue should not increase it's size after enqueueing an existing element") { - val q = new UniquePQ[String](_ => 1, defOrder) - q.enqueue("a") + val q = new UniqueLongPQ(_ => 1, defOrder) + q.enqueue(13) val s = q.size - q.enqueue("a") + q.enqueue(13) assertEquals(q.size, s) } test("a non-empty queue should decrease it's size after dequeue") { - val q = new UniquePQ[String](_ => 1, defOrder) - q.enqueue("a") - q.enqueue("b") - q.enqueue("a") - q.enqueue("c") + val q = new UniqueLongPQ(_ => 1, defOrder) + q.enqueue(13) + q.enqueue(14) + q.enqueue(13) + q.enqueue(15) val s = q.size q.dequeue() assertEquals(q.size, s - 1) } test("a non-empty queue should allow items to be re-added after removal") { - val q = new UniquePQ[String](_ => 1, defOrder) - q.enqueue("a") + val q = new UniqueLongPQ(_ => 1, defOrder) + q.enqueue(13) assertEquals(q.size, 1) q.dequeue() assertEquals(q.size, 0) - q.enqueue("a") + q.enqueue(13) assertEquals(q.size, 1) } test("dequeue should return items with highest priority first") { - val q = new UniquePQ[String](s => s.length, defOrder) - q.enqueue("aa") - q.enqueue("a") - q.enqueue("aaa") - assertEquals(q.dequeue(), "aaa") - assertEquals(q.dequeue(), "aa") - assertEquals(q.dequeue(), "a") + val q = new UniqueLongPQ(s => -s.toDouble, defOrder) + q.enqueue(10) + q.enqueue(13) + q.enqueue(11) + assertEquals(q.dequeue(), 11L) + assertEquals(q.dequeue(), 10L) + assertEquals(q.dequeue(), 13L) } test("clear should remove all items, resetting the queue") { - val q = new UniquePQ[String](_ => 1, defOrder) - q.enqueue("a") - q.enqueue("b") + val q = new UniqueLongPQ(_ => 1, defOrder) + q.enqueue(13) + q.enqueue(14) q.clear() assertEquals(q.size, 0) assert(q.isEmpty) - q.enqueue("a") + q.enqueue(13) assertEquals(q.size, 1) } test("reprioritizeAndFilter should reorder items") { var reference = 1 - def f(s: String): Double = math.abs(s.length - reference) - val q = new UniquePQ[String](f, defOrder) - q.enqueue("a") // a -> |1-1| = 0 - q.enqueue("aa") // aa -> |2-1| = 1 - q.enqueue("aaa") // aaa -> |3-1| = 2 + def f(s: Long): Double = math.abs(s - reference).toDouble + val q = new UniqueLongPQ(f, defOrder) + q.enqueue(13) // a -> |1-1| = 0 + q.enqueue(10) // aa -> |2-1| = 1 + q.enqueue(11) // aaa -> |3-1| = 2 reference = 3 q.reprioritizeAndFilter(_ => true) - assertEquals(q.dequeue(), "a") // a -> |1-3| = 2 - assertEquals(q.dequeue(), "aa") // aa -> |2-3| = 1 - assertEquals(q.dequeue(), "aaa") // aaa -> |3-3| = 0 + assertEquals(q.dequeue(), 13L) // a -> |1-3| = 2 + assertEquals(q.dequeue(), 10L) // aa -> |2-3| = 1 + assertEquals(q.dequeue(), 11L) // aaa -> |3-3| = 0 } test("reprioritizeAndFilter should filter out items") { - val q = new UniquePQ[String](s => s.length, defOrder) - q.enqueue("a") - q.enqueue("aa") - q.enqueue("aaa") + val q = new UniqueLongPQ(s => -s.toDouble, defOrder) + q.enqueue(13) + q.enqueue(10) + q.enqueue(11) - q.reprioritizeAndFilter { case (_, s) => !s.equals("aa") } + q.reprioritizeAndFilter { case (_, s) => !s.equals(10) } - assertEquals(q.dequeue(), "aaa") - assertEquals(q.dequeue(), "a") + assertEquals(q.dequeue(), 11L) + assertEquals(q.dequeue(), 13L) } } diff --git a/common/src/test/scala/hexacraft/util/UniqueQueueTest.scala b/common/src/test/scala/hexacraft/util/UniqueQueueTest.scala index 02e027dd..66be4a0b 100644 --- a/common/src/test/scala/hexacraft/util/UniqueQueueTest.scala +++ b/common/src/test/scala/hexacraft/util/UniqueQueueTest.scala @@ -4,84 +4,84 @@ import munit.FunSuite class UniqueQueueTest extends FunSuite { test("an empty queue should have size 0 from the beginning") { - assertEquals(new UniqueQueue[String].size, 0) + assertEquals((new UniqueLongQueue).size, 0) } test("an empty queue should be empty") { - assert(new UniqueQueue[String].isEmpty) + assert((new UniqueLongQueue).isEmpty) } test("an empty queue should have size 1 after enqueue") { - val q = new UniqueQueue[String] - q.enqueue("a") + val q = new UniqueLongQueue + q.enqueue(13) assertEquals(q.size, 1) } test("an empty queue should fail to dequeue") { - intercept[NoSuchElementException](new UniqueQueue[String].dequeue()) + intercept[NoSuchElementException]((new UniqueLongQueue).dequeue()) } test("a non-empty queue should not be empty") { - val q = new UniqueQueue[String] - q.enqueue("a") + val q = new UniqueLongQueue + q.enqueue(13) assert(!q.isEmpty) } test("a non-empty queue should have the right size from the beginning") { - val q = new UniqueQueue[String] - q.enqueue("a") - q.enqueue("b") - q.enqueue("c") + val q = new UniqueLongQueue + q.enqueue(13) + q.enqueue(14) + q.enqueue(15) assertEquals(q.size, 3) } test("a non-empty queue should not increase it's size after enqueueing an existing element") { - val q = new UniqueQueue[String] - q.enqueue("a") + val q = new UniqueLongQueue + q.enqueue(13) val s = q.size - q.enqueue("a") + q.enqueue(13) assertEquals(q.size, s) } test("a non-empty queue should decrease it's size after dequeue") { - val q = new UniqueQueue[String] - q.enqueue("a") - q.enqueue("b") - q.enqueue("a") - q.enqueue("c") + val q = new UniqueLongQueue + q.enqueue(13) + q.enqueue(14) + q.enqueue(13) + q.enqueue(15) val s = q.size q.dequeue() assertEquals(q.size, s - 1) } test("a non-empty queue should allow items to be re-added after removal") { - val q = new UniqueQueue[String] - q.enqueue("a") + val q = new UniqueLongQueue + q.enqueue(13) assertEquals(q.size, 1) q.dequeue() assertEquals(q.size, 0) - q.enqueue("a") + q.enqueue(13) assertEquals(q.size, 1) } test("dequeue should return the first element added") { - val q = new UniqueQueue[String] - q.enqueue("aa") - q.enqueue("a") - q.enqueue("aaa") - assertEquals(q.dequeue(), "aa") - assertEquals(q.dequeue(), "a") - assertEquals(q.dequeue(), "aaa") + val q = new UniqueLongQueue + q.enqueue(10) + q.enqueue(13) + q.enqueue(11) + assertEquals(q.dequeue(), 10L) + assertEquals(q.dequeue(), 13L) + assertEquals(q.dequeue(), 11L) } test("clear should remove all items, resetting the queue") { - val q = new UniqueQueue[String] - q.enqueue("a") - q.enqueue("b") + val q = new UniqueLongQueue + q.enqueue(13) + q.enqueue(14) q.clear() assertEquals(q.size, 0) assert(q.isEmpty) - q.enqueue("a") + q.enqueue(13) assertEquals(q.size, 1) } } diff --git a/game/src/main/scala/hexacraft/world/WorldPlanner.scala b/game/src/main/scala/hexacraft/world/WorldPlanner.scala index d84aebe2..d738ee94 100644 --- a/game/src/main/scala/hexacraft/world/WorldPlanner.scala +++ b/game/src/main/scala/hexacraft/world/WorldPlanner.scala @@ -13,7 +13,9 @@ class WorldPlanner(world: BlocksInWorldExtended, mainSeed: Long)(using CylinderS def decorate(chunkCoords: ChunkRelWorld, chunk: Chunk): Unit = { if !chunk.isDecorated then { - for p <- planners do { + val pIt = planners.iterator + while pIt.hasNext do { + val p = pIt.next p.decorate(chunkCoords, chunk) } chunk.setDecorated() @@ -21,11 +23,14 @@ class WorldPlanner(world: BlocksInWorldExtended, mainSeed: Long)(using CylinderS } def prepare(coords: ChunkRelWorld): Unit = { - for { - ch <- coords.extendedNeighbors(4) - p <- planners - } do { - p.plan(ch) + val neighbors = coords.extendedNeighbors(4) + val nIt = neighbors.iterator + while nIt.hasNext do { + val ch = nIt.next + val pIt = planners.iterator + while pIt.hasNext do { + pIt.next.plan(ch) + } } } } diff --git a/game/src/main/scala/hexacraft/world/coord/grid.scala b/game/src/main/scala/hexacraft/world/coord/grid.scala index a16e7e94..70df8321 100644 --- a/game/src/main/scala/hexacraft/world/coord/grid.scala +++ b/game/src/main/scala/hexacraft/world/coord/grid.scala @@ -5,6 +5,9 @@ import hexacraft.world.CylinderSize import org.joml.Vector2d +import scala.collection.immutable.ArraySeq +import scala.collection.mutable + case class Offset(dx: Int, dy: Int, dz: Int) { def +(other: Offset): Offset = { Offset( @@ -28,7 +31,7 @@ case class Offset(dx: Int, dy: Int, dz: Int) { } object NeighborOffsets { - val all: Seq[Offset] = IndexedSeq( + val all: Array[Offset] = Array( Offset(0, 1, 0), Offset(0, -1, 0), Offset(1, 0, 0), @@ -44,9 +47,9 @@ object NeighborOffsets { * @return * The offset of the neighboring block on the given side */ - def apply(side: Int): Offset = all(side) + inline def apply(side: Int): Offset = all(side) - val indices: Range = all.indices + inline def indices: Range = all.indices } object BlockRelChunk { @@ -160,13 +163,18 @@ case class ChunkRelWorld(value: Long) extends AnyVal { // XXXXXZZZZZYYY ChunkRelWorld.neighborOffsets.map(offset) def extendedNeighbors(radius: Int)(using CylinderSize): Seq[ChunkRelWorld] = { + val s = 2 * radius + 1 + val buf = new mutable.ArrayBuffer[ChunkRelWorld](s * s * s) + for { y <- -radius to radius z <- -radius to radius x <- -radius to radius - } yield { - offset(x, y, z) + } do { + buf += offset(x, y, z) } + + buf.toSeq } def offset(t: Offset)(using CylinderSize): ChunkRelWorld = offset(t.dx, t.dy, t.dz) diff --git a/game/src/main/scala/hexacraft/world/gen/planners.scala b/game/src/main/scala/hexacraft/world/gen/planners.scala index e57348b2..2ba7ca8c 100644 --- a/game/src/main/scala/hexacraft/world/gen/planners.scala +++ b/game/src/main/scala/hexacraft/world/gen/planners.scala @@ -1,5 +1,6 @@ package hexacraft.world.gen +import hexacraft.util.LongSet import hexacraft.world.{BlocksInWorldExtended, CylinderSize} import hexacraft.world.block.{Block, BlockState} import hexacraft.world.chunk.{Chunk, ChunkColumnTerrain, LocalBlockState} @@ -41,17 +42,20 @@ class WoodChoice(val log: Block, val leaves: Block) class TreePlanner(world: BlocksInWorldExtended, mainSeed: Long)(using cylSize: CylinderSize) extends WorldFeaturePlanner { - private val plannedChanges: mutable.Map[ChunkRelWorld, mutable.Buffer[LocalBlockState]] = mutable.Map.empty - private val chunksPlanned: mutable.Set[ChunkRelWorld] = mutable.Set.empty + private val plannedChanges: mutable.LongMap[mutable.ArrayBuffer[LocalBlockState]] = + mutable.LongMap.empty + private val chunksPlanned: LongSet = new LongSet private val maxTreesPerChunk = 5 override def decorate(chunkCoords: ChunkRelWorld, chunk: Chunk): Unit = { - for { - ch <- plannedChanges.remove(chunkCoords) - LocalBlockState(c, b) <- ch - } do { - chunk.setBlock(c, b) + val changesOpt = plannedChanges.remove(chunkCoords.value) + if changesOpt.isDefined then { + val cIt = changesOpt.get.iterator + while cIt.hasNext do { + val LocalBlockState(c, b) = cIt.next + chunk.setBlock(c, b) + } } } @@ -67,7 +71,7 @@ class TreePlanner(world: BlocksInWorldExtended, mainSeed: Long)(using cylSize: C } def plan(coords: ChunkRelWorld): Unit = { - if !chunksPlanned(coords) then { + if chunksPlanned.add(coords.value) then { val column = world.provideColumn(coords.getColumnRelWorld) val terrainHeight = column.originalTerrainHeight @@ -81,7 +85,6 @@ class TreePlanner(world: BlocksInWorldExtended, mainSeed: Long)(using cylSize: C generateTree(coords, cx, cz, yy, allowBig) } } - chunksPlanned(coords) = true } } @@ -117,7 +120,7 @@ class TreePlanner(world: BlocksInWorldExtended, mainSeed: Long)(using cylSize: C private def generateChanges(tree: PlannedWorldChange): Unit = { for (c, ch) <- tree.chunkChanges do { - plannedChanges.getOrElseUpdate(c, mutable.Buffer.empty).appendAll(ch) + plannedChanges.getOrElseUpdate(c.value, mutable.ArrayBuffer.empty).appendAll(ch) } } } @@ -126,28 +129,29 @@ class EntityGroupPlanner(world: BlocksInWorldExtended, entityFactory: CylCoords CylinderSize ) extends WorldFeaturePlanner { - private val plannedEntities: mutable.Map[ChunkRelWorld, Seq[Entity]] = mutable.Map.empty - private val chunksPlanned: mutable.Set[ChunkRelWorld] = mutable.Set.empty + private val plannedEntities: mutable.LongMap[Seq[Entity]] = mutable.LongMap.empty + private val chunksPlanned: LongSet = new LongSet private val maxEntitiesPerGroup = 7 override def decorate(chunkCoords: ChunkRelWorld, chunk: Chunk): Unit = { - for { - entities <- plannedEntities.get(chunkCoords) - entity <- entities - } do { - chunk.addEntity(entity) + val entitiesOpt = plannedEntities.get(chunkCoords.value) + if entitiesOpt.isDefined then { + val eIt = entitiesOpt.get.iterator + while eIt.hasNext do { + val entity = eIt.next + chunk.addEntity(entity) + } } } override def plan(coords: ChunkRelWorld): Unit = { - if !chunksPlanned(coords) then { + if chunksPlanned.add(coords.value) then { val rand = new Random(mainSeed ^ coords.value + 364453868) if rand.nextDouble() < 0.01 then { val column = world.provideColumn(coords.getColumnRelWorld) - plannedEntities(coords) = makePlan(rand, coords, column) + plannedEntities(coords.value) = makePlan(rand, coords, column) } - chunksPlanned += coords } } diff --git a/main/src/main/scala/hexacraft/main/MainWindow.scala b/main/src/main/scala/hexacraft/main/MainWindow.scala index 5741ed16..9580b974 100644 --- a/main/src/main/scala/hexacraft/main/MainWindow.scala +++ b/main/src/main/scala/hexacraft/main/MainWindow.scala @@ -88,7 +88,8 @@ class MainWindow( switchSceneIfNeeded() - for _ <- 0 until delta do { + var tIdx = 0 + while tIdx < delta do { tick() ticks += 1 titleTicker += 1 @@ -99,6 +100,7 @@ class MainWindow( frames = 0 } prevTime += 1e9.toLong / 60 + tIdx += 1 } OpenGL.glClear(OpenGL.ClearMask.colorBuffer | OpenGL.ClearMask.depthBuffer) diff --git a/server/src/main/scala/hexacraft/server/ServerWorld.scala b/server/src/main/scala/hexacraft/server/ServerWorld.scala index 2dba559d..5d8731f6 100644 --- a/server/src/main/scala/hexacraft/server/ServerWorld.scala +++ b/server/src/main/scala/hexacraft/server/ServerWorld.scala @@ -60,7 +60,7 @@ class ServerWorld(worldProvider: WorldProvider, val worldInfo: WorldInfo) private val chunksLoading: mutable.Map[ChunkRelWorld, Future[(Chunk, Boolean)]] = mutable.Map.empty private val chunksUnloading: mutable.Map[ChunkRelWorld, Future[Unit]] = mutable.Map.empty - private val blocksToUpdate: UniqueQueue[BlockRelWorld] = new UniqueQueue + private val blocksToUpdate: UniqueLongQueue = new UniqueLongQueue private val savedChunkModCounts = mutable.Map.empty[ChunkRelWorld, Long] @@ -165,20 +165,38 @@ class ServerWorld(worldProvider: WorldProvider, val worldInfo: WorldInfo) requestRenderUpdate(chunkCoords) requestRenderUpdateForNeighborChunks(chunkCoords) - for block <- ch.blocks do { - requestBlockUpdate(BlockRelWorld.fromChunk(block.coords, chunkCoords)) + val allBlocks = ch.blocks - for side <- 0 until 8 do { + // Add all blocks in the chunk to the update queue + val newBlockUpdates = new mutable.ArrayBuffer[Long](allBlocks.length) + var bIdx = 0 + while bIdx < allBlocks.length do { + newBlockUpdates += BlockRelWorld.fromChunk(allBlocks(bIdx).coords, chunkCoords).value + bIdx += 1 + } + blocksToUpdate.enqueueMany(newBlockUpdates) + newBlockUpdates.clear() + + // Add all blocks in neighboring chunks to the update queue + bIdx = 0 + while bIdx < allBlocks.length do { + val block = allBlocks(bIdx) + + var side = 0 + while side < 8 do { if block.coords.isOnChunkEdge(side) then { val neighCoords = block.coords.neighbor(side) val neighChunkCoords = chunkCoords.offset(ChunkRelWorld.neighborOffsets(side)) - for neighbor <- getChunk(neighChunkCoords) do { - requestBlockUpdate(BlockRelWorld.fromChunk(neighCoords, neighChunkCoords)) + if getChunk(neighChunkCoords).isDefined then { + newBlockUpdates += BlockRelWorld.fromChunk(neighCoords, neighChunkCoords).value } } + side += 1 } + bIdx += 1 } + blocksToUpdate.enqueueMany(newBlockUpdates) } private def updateHeightmapAfterChunkUpdate(col: ChunkColumnTerrain, chunkCoords: ChunkRelWorld, chunk: Chunk)(using @@ -256,8 +274,14 @@ class ServerWorld(worldProvider: WorldProvider, val worldInfo: WorldInfo) var chunkWasRemoved = false - for col <- columns.get(columnCoords.value) do { - for removedChunk <- chunks.remove(chunkCoords.value) do { + val columnOpt = columns.get(columnCoords.value) + if columnOpt.isDefined then { + val col = columnOpt.get + + val removedChunkOpt = chunks.remove(chunkCoords.value) + if removedChunkOpt.isDefined then { + val removedChunk = removedChunkOpt.get + chunkWasRemoved = true if removedChunk.modCount != savedChunkModCounts.getOrElse(chunkCoords, -1L) then { @@ -287,7 +311,10 @@ class ServerWorld(worldProvider: WorldProvider, val worldInfo: WorldInfo) performEntityRelocation() } - for ch <- chunks.values do { + val chIt = chunks.values.iterator + while chIt.hasNext do { + val ch = chIt.next + ch.optimizeStorage() tickEntities(ch.entities) } @@ -317,11 +344,11 @@ class ServerWorld(worldProvider: WorldProvider, val worldInfo: WorldInfo) chunkLoadingPrioritizer.tick(PosAndDir.fromCameraView(cameras.head.view)) - val chunksToLoadPerTick = 4 - val chunksToUnloadPerTick = 6 + val chunksToLoadPerTick = 8 + val chunksToUnloadPerTick = 12 for _ <- 1 to chunksToLoadPerTick do { - val maxQueueLength = if ServerWorld.shouldChillChunkLoader then 1 else 8 + val maxQueueLength = if ServerWorld.shouldChillChunkLoader then 1 else 16 if chunksLoading.size < maxQueueLength then { chunkLoadingPrioritizer.popChunkToLoad() match { case Some(coords) => @@ -337,7 +364,7 @@ class ServerWorld(worldProvider: WorldProvider, val worldInfo: WorldInfo) } for _ <- 1 to chunksToUnloadPerTick do { - val maxQueueLength = if ServerWorld.shouldChillChunkLoader then 2 else 10 + val maxQueueLength = if ServerWorld.shouldChillChunkLoader then 2 else 20 if chunksUnloading.size < maxQueueLength then { chunkLoadingPrioritizer.popChunkToRemove() match { case Some(coords) => @@ -360,16 +387,18 @@ class ServerWorld(worldProvider: WorldProvider, val worldInfo: WorldInfo) val chunksAdded = mutable.ArrayBuffer.empty[ChunkRelWorld] val chunksRemoved = mutable.ArrayBuffer.empty[ChunkRelWorld] - for (chunkCoords, chunk, isNew) <- chunksFinishedLoading do { + val lIt = chunksFinishedLoading.iterator + while lIt.hasNext do { + val (chunkCoords, chunk, isNew) = lIt.next savedChunkModCounts(chunkCoords) = if isNew then -1L else chunk.modCount - setChunk(chunkCoords, chunk) - chunksAdded += chunkCoords } - for chunkCoords <- chunksFinishedUnloading do { - val chunkWasRemoved = removeChunk(chunkCoords) + val uIt = chunksFinishedUnloading.iterator + while uIt.hasNext do { + val chunkCoords = uIt.next + val chunkWasRemoved = removeChunk(chunkCoords) if chunkWasRemoved then { chunksRemoved += chunkCoords } @@ -451,13 +480,20 @@ class ServerWorld(worldProvider: WorldProvider, val worldInfo: WorldInfo) private def performBlockUpdates(): Seq[BlockRelWorld] = { val recordingWorld = RecordingBlockRepository(this) - val blocksToUpdateNow = ArrayBuffer.empty[BlockRelWorld] - while !blocksToUpdate.isEmpty do { - blocksToUpdateNow += blocksToUpdate.dequeue() - } - for c <- blocksToUpdateNow do { + val blocksToUpdateNow = new ArrayBuffer[Long](blocksToUpdate.size) + blocksToUpdate.drainInto(value => blocksToUpdateNow += value) + + var bIdx = 0 + while bIdx < blocksToUpdateNow.length do { + val c = BlockRelWorld(blocksToUpdateNow(bIdx)) + val block = getBlock(c).blockType - block.behaviour.foreach(_.onUpdated(c, block, recordingWorld)) + val behaviour = block.behaviour + if behaviour.isDefined then { + behaviour.get.onUpdated(c, block, recordingWorld) + } + + bIdx += 1 } recordingWorld.collectUpdates.distinct @@ -547,7 +583,7 @@ class ServerWorld(worldProvider: WorldProvider, val worldInfo: WorldInfo) } private def requestBlockUpdate(coords: BlockRelWorld): Unit = { - blocksToUpdate.enqueue(coords) + blocksToUpdate.enqueue(coords.value) } private def onSetBlock(coords: BlockRelWorld, block: BlockState): Unit = {