Skip to content

Commit

Permalink
Split async work into separate thread pools
Browse files Browse the repository at this point in the history
  • Loading branch information
Martomate committed Aug 4, 2024
1 parent 02d3416 commit 5ed1e91
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 43 deletions.
12 changes: 8 additions & 4 deletions client/src/main/scala/hexacraft/client/GameClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import hexacraft.infra.audio.AudioSystem
import hexacraft.infra.window.{KeyAction, KeyboardKey, MouseAction, MouseButton}
import hexacraft.renderer.{PixelArray, Renderer, TextureArray, VAO}
import hexacraft.shaders.CrosshairShader
import hexacraft.util.{Channel, Result, TickableTimer}
import hexacraft.util.{Channel, NamedThreadFactory, Result, TickableTimer}
import hexacraft.world.*
import hexacraft.world.block.{Block, BlockSpec, BlockState}
import hexacraft.world.chunk.{Chunk, ChunkColumnData, ChunkColumnHeightMap, ChunkColumnTerrain}
Expand All @@ -18,10 +18,9 @@ import org.joml.{Matrix4f, Vector2f, Vector3d, Vector3f}
import org.zeromq.*

import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.concurrent.{Executors, TimeUnit}
import scala.collection.mutable
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.concurrent.duration.Duration
import scala.util.Random

Expand Down Expand Up @@ -237,6 +236,9 @@ class GameClient(
walkSoundBuffer1: AudioSystem.BufferId,
walkSoundBuffer2: AudioSystem.BufferId
)(using CylinderSize) {
private val executorService = Executors.newFixedThreadPool(4, NamedThreadFactory("client"))
given ExecutionContext = ExecutionContext.fromExecutor(executorService)

private var moveWithMouse: Boolean = false
private var isPaused: Boolean = false
private var isInPopup: Boolean = false
Expand Down Expand Up @@ -818,6 +820,8 @@ class GameClient(
if debugOverlay.isDefined then {
debugOverlay.get.unload()
}

executorService.shutdown()
}
}

Expand Down
67 changes: 52 additions & 15 deletions client/src/main/scala/hexacraft/client/WorldRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import hexacraft.client.render.*
import hexacraft.infra.gpu.OpenGL
import hexacraft.renderer.{GpuState, TextureArray, TextureSingle, VAO}
import hexacraft.shaders.*
import hexacraft.util.TickableTimer
import hexacraft.util.{NamedThreadFactory, TickableTimer}
import hexacraft.world.{BlocksInWorld, Camera, ChunkLoadingPrioritizer, CylinderSize, PosAndDir, WorldGenerator}
import hexacraft.world.block.BlockState
import hexacraft.world.chunk.{Chunk, ChunkColumnHeightMap, ChunkColumnTerrain, ChunkStorage}
Expand All @@ -16,11 +16,10 @@ import org.joml.{Vector2ic, Vector3f}
import org.lwjgl.BufferUtils

import java.nio.ByteBuffer
import java.util.concurrent.Executors
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{ExecutionContext, Future}

class WorldRenderer(
world: BlocksInWorld,
Expand All @@ -29,6 +28,8 @@ class WorldRenderer(
blockTextureColors: Map[String, IndexedSeq[Vector3f]],
initialFrameBufferSize: Vector2ic
)(using CylinderSize) {
private val executorService = Executors.newFixedThreadPool(8, NamedThreadFactory("render"))
given ExecutionContext = ExecutionContext.fromExecutor(executorService)

private val skyShader = new SkyShader()
private val entityShader = new EntityShader(isSide = false)
Expand Down Expand Up @@ -99,12 +100,37 @@ class WorldRenderer(
}

def tick(camera: Camera, renderDistance: Double, worldTickResult: WorldTickResult): Unit = {
val blockDataToUpdate = handleChunkUpdateQueue(camera, renderDistance, worldTickResult.chunksNeedingRenderUpdate)

// Step 3: Perform render updates using data calculated in the background since the previous frame
updateBlockData(blockDataToUpdate)

// performTerrainUpdates(camera)
}

private def handleChunkUpdateQueue(
camera: Camera,
renderDistance: Double,
chunksNeedingRenderUpdate: Seq[ChunkRelWorld]
): collection.Seq[(ChunkRelWorld, ChunkRenderData)] = {
// Step 1: Collect render data calculated since the previous frame (used in step 3)
val blockDataToUpdate = futureRenderData.map((coords, fut) => (coords, Await.result(fut, Duration.Inf))).toSeq
val (blockDataToUpdate, blockDataNotReady) = {
// Only collect the completed tasks in the beginning (to keep the order of the updates)
val idxFirstPending = futureRenderData.indexWhere(!_._2.isCompleted)
val (completed, rest) = if idxFirstPending != -1 then {
futureRenderData.splitAt(idxFirstPending)
} else {
(futureRenderData, new ArrayBuffer(0))
}
(completed.map((coords, fut) => coords -> fut.value.get.get), rest)
}
futureRenderData.clear()
futureRenderData ++= blockDataNotReady
val chunksAlreadyUpdating = futureRenderData.filter(!_._2.isCompleted).map(_._1)
val numUpdatesPending = chunksAlreadyUpdating.size

// Step 2: Start calculating render updates in the background
for coords <- worldTickResult.chunksNeedingRenderUpdate do {
for coords <- chunksNeedingRenderUpdate do {
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)
Expand All @@ -119,15 +145,23 @@ class WorldRenderer(
chunkRenderUpdateQueue.reorderAndFilter(camera, renderDistance)
}

var numUpdatesToPerform = math.min(chunkRenderUpdateQueue.length, 15)
val chunkRenderUpdatesSkipped = mutable.ArrayBuffer.empty[ChunkRelWorld]

var numUpdatesToPerform = math.min(chunkRenderUpdateQueue.length, math.max(15 - numUpdatesPending, 0))
while numUpdatesToPerform > 0 do {
chunkRenderUpdateQueue.pop() match {
case Some(coords) =>
world.getChunk(coords) match {
case Some(chunk) =>
if coords.neighbors.forall(n => world.getChunk(n).isDefined) then {
futureRenderData += coords -> Future(ChunkRenderData(coords, chunk.blocks, world, blockTextureIndices))
numUpdatesToPerform -= 1
if !chunksAlreadyUpdating.contains(coords) then {
futureRenderData += coords -> Future(
ChunkRenderData(coords, chunk.blocks, world, blockTextureIndices)
)
numUpdatesToPerform -= 1
} else {
chunkRenderUpdatesSkipped += coords
}
} else {
futureRenderData += coords -> Future.successful(ChunkRenderData.empty)
}
Expand All @@ -139,10 +173,11 @@ class WorldRenderer(
}
}

// Step 3: Perform render updates using data calculated in the background since the previous frame
updateBlockData(blockDataToUpdate)
for coords <- chunkRenderUpdatesSkipped do {
chunkRenderUpdateQueue.insert(coords)
}

// performTerrainUpdates(camera)
blockDataToUpdate
}

private def performTerrainUpdates(camera: Camera): Unit = {
Expand Down Expand Up @@ -335,7 +370,7 @@ class WorldRenderer(
renderTerrain()
}

private def updateBlockData(chunks: Seq[(ChunkRelWorld, ChunkRenderData)]): Unit = {
private def updateBlockData(chunks: collection.Seq[(ChunkRelWorld, ChunkRenderData)]): Unit = {
val opaqueData = chunks.partitionMap((coords, data) =>
data.opaqueBlocks match {
case Some(content) => Right((coords, content))
Expand Down Expand Up @@ -396,8 +431,8 @@ class WorldRenderer(
)

private def updateBlockData(
chunksToClear: Seq[ChunkRelWorld],
chunksToUpdate: Seq[(ChunkRelWorld, IndexedSeq[ByteBuffer])],
chunksToClear: collection.Seq[ChunkRelWorld],
chunksToUpdate: collection.Seq[(ChunkRelWorld, IndexedSeq[ByteBuffer])],
transmissive: Boolean
): Unit = {
val clear = chunksToClear.groupBy(c => chunkGroup(c))
Expand Down Expand Up @@ -515,5 +550,7 @@ class WorldRenderer(
terrainShader.free()

mainFrameBuffer.unload()

executorService.shutdown()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class BlockFaceBatchRenderer(bufferHandler: BufferHandler[?]) {
}

def update(
chunksToClear: Seq[ChunkRelWorld],
chunksToUpdate: Seq[(ChunkRelWorld, ByteBuffer)]
chunksToClear: collection.Seq[ChunkRelWorld],
chunksToUpdate: collection.Seq[(ChunkRelWorld, ByteBuffer)]
): Unit = {
// Step 1: mark old data as unused
for coords <- chunksToClear do {
Expand Down
17 changes: 17 additions & 0 deletions common/src/main/scala/hexacraft/util/NamedThreadFactory.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package hexacraft.util

import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger

class NamedThreadFactory(name: String) extends ThreadFactory {
final private val group: ThreadGroup = Thread.currentThread.getThreadGroup
final private val threadNumber = new AtomicInteger(1)
final private val namePrefix: String = name + "-"

override def newThread(r: Runnable) = {
val t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement, 0)
if (t.isDaemon) t.setDaemon(false)
if (t.getPriority != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY)
t
}
}
15 changes: 15 additions & 0 deletions game/src/main/scala/hexacraft/world/coord/grid.scala
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,19 @@ case class ColumnRelWorld(value: Long) extends AnyVal { // XXXXXZZZZZ
val dz = math.min(dz1, dz2)
dx * dx + dz * dz
}

def neighbors(using CylinderSize): Seq[ColumnRelWorld] = {
val buf = new mutable.ArrayBuffer[ColumnRelWorld](8)

for {
dz <- -1 to 1
dx <- -1 to 1
} do {
if dz != 0 || dx != 0 then {
buf += offset(dx, dz)
}
}

buf.toSeq
}
}
Loading

0 comments on commit 5ed1e91

Please sign in to comment.