Gjallarhorn: Implement render loop.

This commit is contained in:
Kenneth Endfinger
2022-01-29 23:15:05 -05:00
parent ba18fcddbc
commit 74fed8c222
11 changed files with 106 additions and 50 deletions

View File

@ -1,15 +1,13 @@
package cloud.kubelet.foundation.gjallarhorn.commands
import cloud.kubelet.foundation.gjallarhorn.render.BlockDiversityRenderer
import cloud.kubelet.foundation.gjallarhorn.render.BlockHeightMapRenderer
import cloud.kubelet.foundation.gjallarhorn.render.PlayerLocationShareRenderer
import cloud.kubelet.foundation.gjallarhorn.render.BlockImageRenderer
import cloud.kubelet.foundation.gjallarhorn.render.*
import cloud.kubelet.foundation.gjallarhorn.state.*
import cloud.kubelet.foundation.gjallarhorn.util.compose
import cloud.kubelet.foundation.gjallarhorn.util.savePngFile
import cloud.kubelet.foundation.heimdall.view.BlockChangeView
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
@ -25,6 +23,7 @@ import java.awt.Font
import java.awt.font.TextLayout
import java.awt.image.BufferedImage
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ScheduledThreadPoolExecutor
class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name = "block-change-timelapse") {
@ -47,15 +46,33 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
private val fromCoordinate by option("--trim-from", help = "Trim From Coordinate")
private val toCoordinate by option("--trim-to", help = "Trim To Coordinate")
private val parallelPoolSize by option("--pool-size", help = "Task Pool Size").int().default(8)
private val inMemoryRender by option("--in-memory-render", help = "Render Images to Memory").flag()
private val shouldRenderLoop by option("--loop-render", help = "Loop Render").flag()
private val quadPixelNoop by option("--quad-pixel-noop", help = "Disable Quad Pixel Render").flag()
private val logger = LoggerFactory.getLogger(BlockChangeTimelapseCommand::class.java)
override fun run() {
val threadPoolExecutor = ScheduledThreadPoolExecutor(16)
if (quadPixelNoop) {
BlockGridRenderer.globalQuadPixelNoop = true
}
val threadPoolExecutor = ScheduledThreadPoolExecutor(parallelPoolSize)
if (shouldRenderLoop) {
while (true) {
perform(threadPoolExecutor)
}
} else {
perform(threadPoolExecutor)
}
threadPoolExecutor.shutdown()
}
private fun perform(threadPoolExecutor: ScheduledThreadPoolExecutor) {
val trim = maybeBuildTrim()
val filter = compose(
combine = { a, b -> a and b },
{ trim?.first?.x != null } to { BlockChangeView.x greaterEq trim!!.first.x },
{ trim?.first?.x != null } to { BlockChangeView.x greaterEq trim!!.first.x },
{ trim?.first?.z != null } to { BlockChangeView.z greaterEq trim!!.first.z },
{ trim?.second?.x != null } to { BlockChangeView.x lessEq trim!!.second.x },
{ trim?.second?.z != null } to { BlockChangeView.z lessEq trim!!.second.z }
@ -77,6 +94,12 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
val imagePadCount = slices.size.toString().length
val inMemoryPool = if (inMemoryRender) {
ConcurrentHashMap<ChangelogSlice, BufferedImage>()
} else {
null
}
val pool = BlockMapRenderPool(
changelog = changelog,
blockTrackMode = if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete,
@ -96,16 +119,19 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
layout.draw(graphics, 60f, 60f)
graphics.dispose()
val index = slices.indexOf(slice) + 1
val suffix = "-${index.toString().padStart(imagePadCount, '0')}"
result.savePngFile("${render.id}${suffix}.png")
if (inMemoryRender) {
inMemoryPool?.put(slice, result)
} else {
val suffix = "-${index.toString().padStart(imagePadCount, '0')}"
result.savePngFile("${render.id}${suffix}.png")
}
logger.info("Rendered Timelapse Slice $index")
}
pool.render(slices)
threadPoolExecutor.shutdown()
}
fun maybeBuildTrim(): Pair<BlockCoordinate, BlockCoordinate>? {
private fun maybeBuildTrim(): Pair<BlockCoordinate, BlockCoordinate>? {
if (fromCoordinate == null || toCoordinate == null) {
return null
}

View File

@ -1,7 +1,7 @@
package cloud.kubelet.foundation.gjallarhorn.render
import cloud.kubelet.foundation.gjallarhorn.state.BlockExpanse
import cloud.kubelet.foundation.gjallarhorn.state.BlockMap
import cloud.kubelet.foundation.gjallarhorn.state.BlockStateMap
import cloud.kubelet.foundation.gjallarhorn.state.ChangelogSlice
import cloud.kubelet.foundation.gjallarhorn.util.BlockColorKey
import cloud.kubelet.foundation.gjallarhorn.util.defaultBlockColorMap
@ -12,7 +12,7 @@ class BlockDiversityRenderer(val expanse: BlockExpanse, quadPixelSize: Int = def
BlockGridRenderer(quadPixelSize) {
private val blockColorKey = BlockColorKey(defaultBlockColorMap)
override fun render(slice: ChangelogSlice, map: BlockMap): BufferedImage = buildPixelQuadImage(expanse) { graphics, x, z ->
override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage = buildPixelQuadImage(expanse) { graphics, x, z ->
val maybeYBlocks = map.blocks[x]?.get(z)
if (maybeYBlocks == null) {
setPixelQuad(graphics, x, z, Color.white)

View File

@ -8,6 +8,9 @@ import java.awt.image.BufferedImage
abstract class BlockGridRenderer(val quadPixelSize: Int = defaultQuadPixelSize) : BlockImageRenderer {
protected fun setPixelQuad(graphics: Graphics2D, x: Long, z: Long, color: Color) {
if (globalQuadPixelNoop) {
return
}
drawSquare(graphics, x * quadPixelSize, z * quadPixelSize, quadPixelSize.toLong(), color)
}
@ -39,5 +42,6 @@ abstract class BlockGridRenderer(val quadPixelSize: Int = defaultQuadPixelSize)
companion object {
const val defaultQuadPixelSize = 4
var globalQuadPixelNoop = false
}
}

View File

@ -1,14 +1,14 @@
package cloud.kubelet.foundation.gjallarhorn.render
import cloud.kubelet.foundation.gjallarhorn.state.BlockExpanse
import cloud.kubelet.foundation.gjallarhorn.state.BlockMap
import cloud.kubelet.foundation.gjallarhorn.state.BlockStateMap
import cloud.kubelet.foundation.gjallarhorn.state.ChangelogSlice
import cloud.kubelet.foundation.gjallarhorn.util.FloatClamp
import java.awt.image.BufferedImage
class BlockHeightMapRenderer(val expanse: BlockExpanse, quadPixelSize: Int = defaultQuadPixelSize) :
BlockHeatMapRenderer(quadPixelSize) {
override fun render(slice: ChangelogSlice, map: BlockMap): BufferedImage {
override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage {
val yMin = map.blocks.minOf { xSection -> xSection.value.minOf { zSection -> zSection.value.minOf { it.key } } }
val yMax = map.blocks.maxOf { xSection -> xSection.value.maxOf { zSection -> zSection.value.maxOf { it.key } } }
val clamp = FloatClamp(yMin, yMax)

View File

@ -1,8 +1,8 @@
package cloud.kubelet.foundation.gjallarhorn.render
import cloud.kubelet.foundation.gjallarhorn.state.BlockMap
import cloud.kubelet.foundation.gjallarhorn.state.BlockStateMap
import cloud.kubelet.foundation.gjallarhorn.state.ChangelogSlice
interface BlockMapRenderer<T> {
fun render(slice: ChangelogSlice, map: BlockMap): T
fun render(slice: ChangelogSlice, map: BlockStateMap): T
}

View File

@ -1,9 +1,6 @@
package cloud.kubelet.foundation.gjallarhorn.render
import cloud.kubelet.foundation.gjallarhorn.state.BlockCoordinate
import cloud.kubelet.foundation.gjallarhorn.state.BlockExpanse
import cloud.kubelet.foundation.gjallarhorn.state.BlockMap
import cloud.kubelet.foundation.gjallarhorn.state.ChangelogSlice
import cloud.kubelet.foundation.gjallarhorn.state.*
import cloud.kubelet.foundation.gjallarhorn.util.BlockColorKey
import cloud.kubelet.foundation.heimdall.table.PlayerPositionTable
import org.jetbrains.exposed.sql.Database
@ -12,37 +9,44 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import java.awt.Color
import java.awt.image.BufferedImage
import java.util.*
class PlayerLocationShareRenderer(
val expanse: BlockExpanse,
val db: Database,
quadPixelSize: Int = defaultQuadPixelSize) : BlockGridRenderer(quadPixelSize) {
quadPixelSize: Int = defaultQuadPixelSize
) : BlockGridRenderer(quadPixelSize) {
private val colorKey = BlockColorKey(mapOf())
override fun render(slice: ChangelogSlice, map: BlockMap): BufferedImage {
override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage {
val start = slice.relativeChangeRange.start
val end = slice.relativeChangeRange.endInclusive
val playersToUniquePositions = transaction(db) {
val playerSparseMap = BlockCoordinateSparseMap<MutableList<UUID>>()
val allPlayerIds = HashSet<UUID>()
transaction(db) {
PlayerPositionTable.select {
(PlayerPositionTable.time greater start) and
(PlayerPositionTable.time lessEq end)
}.map {
}.forEach {
val x = it[PlayerPositionTable.x].toLong()
val y = it[PlayerPositionTable.y].toLong()
val z = it[PlayerPositionTable.z].toLong()
val coordinate = expanse.offset.applyAsOffset(BlockCoordinate(x, y, z))
it[PlayerPositionTable.player] to coordinate
}.distinct()
val player = it[PlayerPositionTable.player]
playerSparseMap.createOrModify(
coordinate,
create = { mutableListOf(player) },
modify = { players -> players.add(player) })
allPlayerIds.add(player)
}
}
val colorOfPlayers = playersToUniquePositions.map { it.first }
.distinct()
.associateWith { colorKey.map(it.toString()) }
val colorOfPlayers = allPlayerIds.associateWith { colorKey.map(it.toString()) }
return buildPixelQuadImage(expanse) { g, x, z ->
val players = playersToUniquePositions.filter { it.second.x == x && it.second.z == z }.map { it.first }.distinct()
if (players.isNotEmpty()) {
val players = playerSparseMap.getVerticalSection(x, z)?.flatMap { it.value }?.distinct()
if (players != null) {
setPixelQuad(g, x, z, colorOfPlayers[players.first()]!!)
} else {
setPixelQuad(g, x, z, Color.white)

View File

@ -0,0 +1,28 @@
package cloud.kubelet.foundation.gjallarhorn.state
import java.util.*
open class BlockCoordinateSparseMap<T> {
val blocks = TreeMap<Long, TreeMap<Long, TreeMap<Long, T>>>()
fun get(position: BlockCoordinate): T? = blocks[position.x]?.get(position.z)?.get(position.z)
fun getVerticalSection(x: Long, z: Long): Map<Long, T>? = blocks[x]?.get(z)
fun getXSection(x: Long): Map<Long, Map<Long, T>>? = blocks[x]
fun put(position: BlockCoordinate, value: T) {
blocks.getOrPut(position.x) {
TreeMap()
}.getOrPut(position.z) {
TreeMap()
}[position.y] = value
}
fun createOrModify(position: BlockCoordinate, create: () -> T, modify: (T) -> Unit) {
val existing = get(position)
if (existing == null) {
put(position, create())
} else {
modify(existing)
}
}
}

View File

@ -39,8 +39,8 @@ class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOn
fun isEmpty() = blocks.isEmpty()
fun isNotEmpty() = !isEmpty()
fun buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero): BlockMap {
val map = BlockMap()
fun buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero): BlockStateMap {
val map = BlockStateMap()
blocks.forEach { (position, state) ->
val realPosition = offset.applyAsOffset(position)
map.put(realPosition, state)

View File

@ -1,15 +0,0 @@
package cloud.kubelet.foundation.gjallarhorn.state
import java.util.*
class BlockMap {
val blocks = TreeMap<Long, TreeMap<Long, TreeMap<Long, BlockState>>>()
fun put(position: BlockCoordinate, state: BlockState) {
blocks.getOrPut(position.x) {
TreeMap()
}.getOrPut(position.z) {
TreeMap()
}[position.y] = state
}
}

View File

@ -2,7 +2,9 @@ package cloud.kubelet.foundation.gjallarhorn.state
import cloud.kubelet.foundation.gjallarhorn.render.BlockMapRenderer
import org.slf4j.LoggerFactory
import java.util.concurrent.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Future
import java.util.concurrent.ThreadPoolExecutor
class BlockMapRenderPool<T>(
val changelog: BlockChangelog,
@ -51,7 +53,11 @@ class BlockMapRenderPool<T>(
delegate.onAllPlaybackComplete(this, trackers)
for (future in renderJobFutures.values) {
future.get()
try {
future.get()
} catch (e: Exception) {
logger.error("Failed to render slice.", e)
}
}
}

View File

@ -0,0 +1,3 @@
package cloud.kubelet.foundation.gjallarhorn.state
class BlockStateMap : BlockCoordinateSparseMap<BlockState>()