Gjallarhorn: Parallel Rendering and Quad Image Improvements

This commit is contained in:
Kenneth Endfinger 2022-01-07 07:29:04 -05:00
parent bc2d3e28ae
commit 10cf0cadac
No known key found for this signature in database
GPG Key ID: C4E68E5647420E10
4 changed files with 79 additions and 29 deletions

View File

@ -3,6 +3,9 @@ package cloud.kubelet.foundation.gjallarhorn
import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.int
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
class GjallarhornCommand : CliktCommand(invokeWithoutSubcommand = true) { class GjallarhornCommand : CliktCommand(invokeWithoutSubcommand = true) {
@ -15,8 +18,16 @@ class GjallarhornCommand : CliktCommand(invokeWithoutSubcommand = true) {
private val jdbcConnectionPassword by option("-p", "--connection-password", help = "JDBC Connection Password") private val jdbcConnectionPassword by option("-p", "--connection-password", help = "JDBC Connection Password")
.default("jdbc:postgresql://localhost/foundation") .default("jdbc:postgresql://localhost/foundation")
private val dbPoolSize by option("--db-pool-size", help = "JDBC Pool Size").int().default(8)
override fun run() { override fun run() {
val db = Database.connect(jdbcConnectionUrl, user = jdbcConnectionUsername, password = jdbcConnectionPassword) val pool = HikariDataSource(HikariConfig().apply {
jdbcUrl = jdbcConnectionUrl
username = jdbcConnectionUsername
password = jdbcConnectionPassword
maximumPoolSize = dbPoolSize
})
val db = Database.connect(pool)
currentContext.findOrSetObject { db } currentContext.findOrSetObject { db }
} }
} }

View File

@ -2,6 +2,7 @@ package cloud.kubelet.foundation.gjallarhorn.commands
import cloud.kubelet.foundation.gjallarhorn.compose import cloud.kubelet.foundation.gjallarhorn.compose
import cloud.kubelet.foundation.gjallarhorn.render.* import cloud.kubelet.foundation.gjallarhorn.render.*
import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey
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
@ -18,12 +19,15 @@ import org.slf4j.LoggerFactory
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-log") { class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-log") {
private val db by requireObject<Database>() private val db by requireObject<Database>()
private val exactTimeAsString by option("--time", help = "Replay Time") private val exactTimeAsString by option("--time", help = "Replay Time")
private val timeLapseMode by option("--timelapse", help = "Timelapse Mode").enum<TimeLapseMode> { it.id } private val timelapseMode by option("--timelapse", help = "Timelapse Mode").enum<TimelapseMode> { it.id }
private val render by option("--render", help = "Render Top Down Image").enum<RenderType> { it.id }.required() private val render by option("--render", help = "Render Top Down Image").enum<RenderType> { it.id }.required()
private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag() private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag()
@ -31,7 +35,7 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
private val logger = LoggerFactory.getLogger(BlockLogReplay::class.java) private val logger = LoggerFactory.getLogger(BlockLogReplay::class.java)
override fun run() { override fun run() {
if (timeLapseMode != null) { if (timelapseMode != null) {
val (start, end) = transaction(db) { val (start, end) = transaction(db) {
val minTimeColumn = BlockChangeView.time.min().notNull val minTimeColumn = BlockChangeView.time.min().notNull
val maxTimeColumn = BlockChangeView.time.max().notNull val maxTimeColumn = BlockChangeView.time.max().notNull
@ -43,40 +47,60 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
var current = start var current = start
while (!current.isAfter(end)) { while (!current.isAfter(end)) {
intervals.add(current) intervals.add(current)
current = current.plus(timeLapseMode!!.interval) current = current.plus(timelapseMode!!.interval)
} }
val trackers = mutableMapOf<Int, BlockStateTracker>() val trackerPool = ScheduledThreadPoolExecutor(8)
val trackers = ConcurrentHashMap<Int, BlockStateTracker>()
for (time in intervals) { for (time in intervals) {
val index = intervals.indexOf(time) + 1 trackerPool.submit {
val tracker = buildTrackerState(time, "Timelapse-${index}") val index = intervals.indexOf(time) + 1
if (tracker.isEmpty()) { val tracker = buildTrackerState(time, "Timelapse-${index}")
continue if (tracker.isEmpty()) {
return@submit
}
trackers[index] = tracker
} }
trackers[index] = tracker
} }
trackerPool.shutdown()
if (!trackerPool.awaitTermination(12, TimeUnit.HOURS)) {
throw RuntimeException("Failed to wait for tracker pool.")
}
logger.info("State Tracking Completed")
val allBlockOffsets = trackers.map { it.value.calculateZeroBlockOffset() } val allBlockOffsets = trackers.map { it.value.calculateZeroBlockOffset() }
val globalBlockOffset = BlockPosition.maxOf(allBlockOffsets.asSequence()) val globalBlockOffset = BlockPosition.maxOf(allBlockOffsets.asSequence())
val allBlockMaxes = trackers.map { it.value.calculateZeroBlockOffset() } val allBlockMaxes = trackers.map { it.value.calculateZeroBlockOffset() }
val globalBlockMax = BlockPosition.maxOf(allBlockMaxes.asSequence()) val globalBlockMax = BlockPosition.maxOf(allBlockMaxes.asSequence())
val globalBlockExpanse = BlockExpanse.offsetAndMax(globalBlockOffset, globalBlockMax) val globalBlockExpanse = BlockExpanse.offsetAndMax(globalBlockOffset, globalBlockMax)
logger.info("Calculations Completed")
val renderState = render.createState()
val renderPool = ScheduledThreadPoolExecutor(8)
for ((i, tracker) in trackers.entries) { for ((i, tracker) in trackers.entries) {
saveRenderImage(tracker, globalBlockExpanse, "-${i}") renderPool.submit {
val count = trackers.size.toString().length
saveRenderImage(renderState, tracker, globalBlockExpanse, "-${i.toString().padStart(count, '0')}")
logger.info("Rendered Timelapse $i")
}
} }
renderPool.shutdown()
if (!renderPool.awaitTermination(12, TimeUnit.HOURS)) {
throw RuntimeException("Failed to wait for render pool.")
}
logger.info("Rendering Completed")
} else { } else {
val time = if (exactTimeAsString != null) Instant.parse(exactTimeAsString) else null val time = if (exactTimeAsString != null) Instant.parse(exactTimeAsString) else null
val tracker = buildTrackerState(time, "Single-Time") val tracker = buildTrackerState(time, "Single-Time")
val expanse = BlockExpanse.offsetAndMax(tracker.calculateZeroBlockOffset(), tracker.calculateMaxBlock()) val expanse = BlockExpanse.offsetAndMax(tracker.calculateZeroBlockOffset(), tracker.calculateMaxBlock())
saveRenderImage(tracker, expanse) saveRenderImage(render.createState(), tracker, expanse)
} }
} }
fun saveRenderImage(tracker: BlockStateTracker, expanse: BlockExpanse, suffix: String = "") { fun saveRenderImage(renderState: Any, tracker: BlockStateTracker, expanse: BlockExpanse, suffix: String = "") {
val state = BlockStateImage() val state = BlockStateImage()
tracker.populateStateImage(state, expanse.offset) tracker.populateStateImage(state, expanse.offset)
val image = render.renderBufferedImage(state, expanse) val image = render.renderBufferedImage(renderState, state, expanse)
image.savePngFile("${render.id}${suffix}.png") image.savePngFile("${render.id}${suffix}.png")
} }
@ -118,12 +142,18 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
return tracker return tracker
} }
enum class RenderType(val id: String, val renderBufferedImage: (BlockStateImage, BlockExpanse) -> BufferedImage) { @Suppress("unused")
TopDown("top-down", { image, expanse -> image.buildTopDownImage(expanse) }), enum class RenderType(val id: String, val createState: () -> Any, val renderBufferedImage: (Any, BlockStateImage, BlockExpanse) -> BufferedImage) {
HeightMap("height-map", { image, expanse -> image.buildHeightMapImage(expanse) }) TopDown("top-down",
{ TopDownState(RandomColorKey()) },
{ state, image, expanse -> image.buildTopDownImage(expanse, (state as TopDownState).randomColorKey) }),
HeightMap("height-map", { }, { _, image, expanse -> image.buildHeightMapImage(expanse) })
} }
enum class TimeLapseMode(val id: String, val interval: Duration) { class TopDownState(val randomColorKey: RandomColorKey)
@Suppress("unused")
enum class TimelapseMode(val id: String, val interval: Duration) {
ByHour("hours", Duration.ofHours(1)), ByHour("hours", Duration.ofHours(1)),
ByDay("days", Duration.ofDays(1)) ByDay("days", Duration.ofDays(1))
} }

View File

@ -4,6 +4,7 @@ import cloud.kubelet.foundation.gjallarhorn.util.ColorGradient
import cloud.kubelet.foundation.gjallarhorn.util.FloatClamp import cloud.kubelet.foundation.gjallarhorn.util.FloatClamp
import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey
import java.awt.Color import java.awt.Color
import java.awt.Rectangle
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.util.* import java.util.*
@ -18,8 +19,7 @@ class BlockStateImage {
}[position.y] = state }[position.y] = state
} }
fun buildTopDownImage(expanse: BlockExpanse): BufferedImage { fun buildTopDownImage(expanse: BlockExpanse, randomColorKey: RandomColorKey): BufferedImage {
val colorKey = RandomColorKey()
return buildPixelQuadImage(expanse) { x, z -> return buildPixelQuadImage(expanse) { x, z ->
val maybeYBlocks = blocks[x]?.get(z) val maybeYBlocks = blocks[x]?.get(z)
if (maybeYBlocks == null) { if (maybeYBlocks == null) {
@ -32,7 +32,7 @@ class BlockStateImage {
return@buildPixelQuadImage return@buildPixelQuadImage
} }
val color = colorKey.map(maxBlockState.type) val color = randomColorKey.map(maxBlockState.type)
setPixelQuad(x, z, color.rgb) setPixelQuad(x, z, color.rgb)
} }
} }
@ -59,16 +59,20 @@ class BlockStateImage {
} }
private fun BufferedImage.setPixelQuad(x: Long, z: Long, rgb: Int) { private fun BufferedImage.setPixelQuad(x: Long, z: Long, rgb: Int) {
setRGB(x.toInt() * 2, z.toInt() * 2, rgb) drawSquare(x * quadImageSize, z * quadImageSize, quadImageSize.toLong(), rgb)
setRGB((x.toInt() * 2) + 1, z.toInt() * 2, rgb) }
setRGB(x.toInt() * 2, (z.toInt() * 2) + 1, rgb)
setRGB((x.toInt() * 2) + 1, (z.toInt() * 2) + 1, rgb) private fun BufferedImage.drawSquare(x: Long, y: Long, side: Long, rgb: Int) {
val graphics = createGraphics()
graphics.color = Color(rgb)
graphics.fill(Rectangle(x.toInt(), y.toInt(), side.toInt(), side.toInt()))
graphics.dispose()
} }
private fun buildPixelQuadImage(expanse: BlockExpanse, callback: BufferedImage.(Long, Long) -> Unit): BufferedImage { private fun buildPixelQuadImage(expanse: BlockExpanse, callback: BufferedImage.(Long, Long) -> Unit): BufferedImage {
val width = expanse.size.x val width = expanse.size.x
val height = expanse.size.z val height = expanse.size.z
val bufferedImage = BufferedImage(width.toInt() * 2, height.toInt() * 2, BufferedImage.TYPE_4BYTE_ABGR) val bufferedImage = BufferedImage(width.toInt() * quadImageSize, height.toInt() * quadImageSize, BufferedImage.TYPE_4BYTE_ABGR)
for (x in 0 until width) { for (x in 0 until width) {
for (z in 0 until height) { for (z in 0 until height) {
@ -77,4 +81,8 @@ class BlockStateImage {
} }
return bufferedImage return bufferedImage
} }
companion object {
const val quadImageSize = 4
}
} }

View File

@ -1,11 +1,12 @@
package cloud.kubelet.foundation.gjallarhorn.util package cloud.kubelet.foundation.gjallarhorn.util
import java.awt.Color import java.awt.Color
import java.util.concurrent.ConcurrentHashMap
class RandomColorKey { class RandomColorKey {
private val colors = mutableMapOf<String, Color>() private val colors = ConcurrentHashMap<String, Color>()
fun map(key: String) = colors.getOrPut(key) { findUniqueColor() } fun map(key: String) = colors.computeIfAbsent(key) { findUniqueColor() }
private fun findUniqueColor(): Color { private fun findUniqueColor(): Color {
var random = randomColor() var random = randomColor()