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 package cloud.kubelet.foundation.gjallarhorn.commands
import cloud.kubelet.foundation.gjallarhorn.render.BlockDiversityRenderer import cloud.kubelet.foundation.gjallarhorn.render.*
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.state.* import cloud.kubelet.foundation.gjallarhorn.state.*
import cloud.kubelet.foundation.gjallarhorn.util.compose import cloud.kubelet.foundation.gjallarhorn.util.compose
import cloud.kubelet.foundation.gjallarhorn.util.savePngFile import cloud.kubelet.foundation.gjallarhorn.util.savePngFile
import cloud.kubelet.foundation.heimdall.view.BlockChangeView import cloud.kubelet.foundation.heimdall.view.BlockChangeView
import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject 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.flag
import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required import com.github.ajalt.clikt.parameters.options.required
@ -25,6 +23,7 @@ import java.awt.Font
import java.awt.font.TextLayout import java.awt.font.TextLayout
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.time.Duration import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.ScheduledThreadPoolExecutor
class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name = "block-change-timelapse") { 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 fromCoordinate by option("--trim-from", help = "Trim From Coordinate")
private val toCoordinate by option("--trim-to", help = "Trim To 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) private val logger = LoggerFactory.getLogger(BlockChangeTimelapseCommand::class.java)
override fun run() { 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 trim = maybeBuildTrim()
val filter = compose( val filter = compose(
combine = { a, b -> a and b }, 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?.first?.z != null } to { BlockChangeView.z greaterEq trim!!.first.z },
{ trim?.second?.x != null } to { BlockChangeView.x lessEq trim!!.second.x }, { trim?.second?.x != null } to { BlockChangeView.x lessEq trim!!.second.x },
{ trim?.second?.z != null } to { BlockChangeView.z lessEq trim!!.second.z } { 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 imagePadCount = slices.size.toString().length
val inMemoryPool = if (inMemoryRender) {
ConcurrentHashMap<ChangelogSlice, BufferedImage>()
} else {
null
}
val pool = BlockMapRenderPool( val pool = BlockMapRenderPool(
changelog = changelog, changelog = changelog,
blockTrackMode = if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete, blockTrackMode = if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete,
@ -96,16 +119,19 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
layout.draw(graphics, 60f, 60f) layout.draw(graphics, 60f, 60f)
graphics.dispose() graphics.dispose()
val index = slices.indexOf(slice) + 1 val index = slices.indexOf(slice) + 1
val suffix = "-${index.toString().padStart(imagePadCount, '0')}" if (inMemoryRender) {
result.savePngFile("${render.id}${suffix}.png") inMemoryPool?.put(slice, result)
} else {
val suffix = "-${index.toString().padStart(imagePadCount, '0')}"
result.savePngFile("${render.id}${suffix}.png")
}
logger.info("Rendered Timelapse Slice $index") logger.info("Rendered Timelapse Slice $index")
} }
pool.render(slices) pool.render(slices)
threadPoolExecutor.shutdown()
} }
fun maybeBuildTrim(): Pair<BlockCoordinate, BlockCoordinate>? { private fun maybeBuildTrim(): Pair<BlockCoordinate, BlockCoordinate>? {
if (fromCoordinate == null || toCoordinate == null) { if (fromCoordinate == null || toCoordinate == null) {
return null return null
} }

View File

@ -1,7 +1,7 @@
package cloud.kubelet.foundation.gjallarhorn.render package cloud.kubelet.foundation.gjallarhorn.render
import cloud.kubelet.foundation.gjallarhorn.state.BlockExpanse 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.state.ChangelogSlice
import cloud.kubelet.foundation.gjallarhorn.util.BlockColorKey import cloud.kubelet.foundation.gjallarhorn.util.BlockColorKey
import cloud.kubelet.foundation.gjallarhorn.util.defaultBlockColorMap import cloud.kubelet.foundation.gjallarhorn.util.defaultBlockColorMap
@ -12,7 +12,7 @@ class BlockDiversityRenderer(val expanse: BlockExpanse, quadPixelSize: Int = def
BlockGridRenderer(quadPixelSize) { BlockGridRenderer(quadPixelSize) {
private val blockColorKey = BlockColorKey(defaultBlockColorMap) 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) val maybeYBlocks = map.blocks[x]?.get(z)
if (maybeYBlocks == null) { if (maybeYBlocks == null) {
setPixelQuad(graphics, x, z, Color.white) 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 { abstract class BlockGridRenderer(val quadPixelSize: Int = defaultQuadPixelSize) : BlockImageRenderer {
protected fun setPixelQuad(graphics: Graphics2D, x: Long, z: Long, color: Color) { protected fun setPixelQuad(graphics: Graphics2D, x: Long, z: Long, color: Color) {
if (globalQuadPixelNoop) {
return
}
drawSquare(graphics, x * quadPixelSize, z * quadPixelSize, quadPixelSize.toLong(), color) drawSquare(graphics, x * quadPixelSize, z * quadPixelSize, quadPixelSize.toLong(), color)
} }
@ -39,5 +42,6 @@ abstract class BlockGridRenderer(val quadPixelSize: Int = defaultQuadPixelSize)
companion object { companion object {
const val defaultQuadPixelSize = 4 const val defaultQuadPixelSize = 4
var globalQuadPixelNoop = false
} }
} }

View File

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

View File

@ -1,8 +1,8 @@
package cloud.kubelet.foundation.gjallarhorn.render 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 import cloud.kubelet.foundation.gjallarhorn.state.ChangelogSlice
interface BlockMapRenderer<T> { 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 package cloud.kubelet.foundation.gjallarhorn.render
import cloud.kubelet.foundation.gjallarhorn.state.BlockCoordinate import cloud.kubelet.foundation.gjallarhorn.state.*
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.util.BlockColorKey import cloud.kubelet.foundation.gjallarhorn.util.BlockColorKey
import cloud.kubelet.foundation.heimdall.table.PlayerPositionTable import cloud.kubelet.foundation.heimdall.table.PlayerPositionTable
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
@ -12,37 +9,44 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import java.awt.Color import java.awt.Color
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.util.*
class PlayerLocationShareRenderer( class PlayerLocationShareRenderer(
val expanse: BlockExpanse, val expanse: BlockExpanse,
val db: Database, val db: Database,
quadPixelSize: Int = defaultQuadPixelSize) : BlockGridRenderer(quadPixelSize) { quadPixelSize: Int = defaultQuadPixelSize
) : BlockGridRenderer(quadPixelSize) {
private val colorKey = BlockColorKey(mapOf()) 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 start = slice.relativeChangeRange.start
val end = slice.relativeChangeRange.endInclusive val end = slice.relativeChangeRange.endInclusive
val playersToUniquePositions = transaction(db) { val playerSparseMap = BlockCoordinateSparseMap<MutableList<UUID>>()
val allPlayerIds = HashSet<UUID>()
transaction(db) {
PlayerPositionTable.select { PlayerPositionTable.select {
(PlayerPositionTable.time greater start) and (PlayerPositionTable.time greater start) and
(PlayerPositionTable.time lessEq end) (PlayerPositionTable.time lessEq end)
}.map { }.forEach {
val x = it[PlayerPositionTable.x].toLong() val x = it[PlayerPositionTable.x].toLong()
val y = it[PlayerPositionTable.y].toLong() val y = it[PlayerPositionTable.y].toLong()
val z = it[PlayerPositionTable.z].toLong() val z = it[PlayerPositionTable.z].toLong()
val coordinate = expanse.offset.applyAsOffset(BlockCoordinate(x, y, z)) val coordinate = expanse.offset.applyAsOffset(BlockCoordinate(x, y, z))
it[PlayerPositionTable.player] to coordinate val player = it[PlayerPositionTable.player]
}.distinct() playerSparseMap.createOrModify(
coordinate,
create = { mutableListOf(player) },
modify = { players -> players.add(player) })
allPlayerIds.add(player)
}
} }
val colorOfPlayers = playersToUniquePositions.map { it.first } val colorOfPlayers = allPlayerIds.associateWith { colorKey.map(it.toString()) }
.distinct()
.associateWith { colorKey.map(it.toString()) }
return buildPixelQuadImage(expanse) { g, x, z -> return buildPixelQuadImage(expanse) { g, x, z ->
val players = playersToUniquePositions.filter { it.second.x == x && it.second.z == z }.map { it.first }.distinct() val players = playerSparseMap.getVerticalSection(x, z)?.flatMap { it.value }?.distinct()
if (players.isNotEmpty()) { if (players != null) {
setPixelQuad(g, x, z, colorOfPlayers[players.first()]!!) setPixelQuad(g, x, z, colorOfPlayers[players.first()]!!)
} else { } else {
setPixelQuad(g, x, z, Color.white) 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 isEmpty() = blocks.isEmpty()
fun isNotEmpty() = !isEmpty() fun isNotEmpty() = !isEmpty()
fun buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero): BlockMap { fun buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero): BlockStateMap {
val map = BlockMap() val map = BlockStateMap()
blocks.forEach { (position, state) -> blocks.forEach { (position, state) ->
val realPosition = offset.applyAsOffset(position) val realPosition = offset.applyAsOffset(position)
map.put(realPosition, state) 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 cloud.kubelet.foundation.gjallarhorn.render.BlockMapRenderer
import org.slf4j.LoggerFactory 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>( class BlockMapRenderPool<T>(
val changelog: BlockChangelog, val changelog: BlockChangelog,
@ -51,7 +53,11 @@ class BlockMapRenderPool<T>(
delegate.onAllPlaybackComplete(this, trackers) delegate.onAllPlaybackComplete(this, trackers)
for (future in renderJobFutures.values) { 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>()