From 10cf0cadacad4fe9fb007f95d950334db7c8e9c1 Mon Sep 17 00:00:00 2001 From: Kenneth Endfinger Date: Fri, 7 Jan 2022 07:29:04 -0500 Subject: [PATCH] Gjallarhorn: Parallel Rendering and Quad Image Improvements --- .../gjallarhorn/GjallarhornCommand.kt | 13 +++- .../gjallarhorn/commands/BlockLogReplay.kt | 66 ++++++++++++++----- .../gjallarhorn/render/BlockStateImage.kt | 24 ++++--- .../gjallarhorn/util/RandomColorKey.kt | 5 +- 4 files changed, 79 insertions(+), 29 deletions(-) diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/GjallarhornCommand.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/GjallarhornCommand.kt index 718cdab..7afd8ce 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/GjallarhornCommand.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/GjallarhornCommand.kt @@ -3,6 +3,9 @@ package cloud.kubelet.foundation.gjallarhorn import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.default 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 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") .default("jdbc:postgresql://localhost/foundation") + private val dbPoolSize by option("--db-pool-size", help = "JDBC Pool Size").int().default(8) + 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 } } } diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt index 3e3ca79..6a9cda6 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt @@ -2,6 +2,7 @@ package cloud.kubelet.foundation.gjallarhorn.commands import cloud.kubelet.foundation.gjallarhorn.compose import cloud.kubelet.foundation.gjallarhorn.render.* +import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey import cloud.kubelet.foundation.gjallarhorn.util.savePngFile import cloud.kubelet.foundation.heimdall.view.BlockChangeView import com.github.ajalt.clikt.core.CliktCommand @@ -18,12 +19,15 @@ import org.slf4j.LoggerFactory import java.awt.image.BufferedImage import java.time.Duration 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 class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-log") { private val db by requireObject() private val exactTimeAsString by option("--time", help = "Replay Time") - private val timeLapseMode by option("--timelapse", help = "Timelapse Mode").enum { it.id } + private val timelapseMode by option("--timelapse", help = "Timelapse Mode").enum { it.id } private val render by option("--render", help = "Render Top Down Image").enum { it.id }.required() 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) override fun run() { - if (timeLapseMode != null) { + if (timelapseMode != null) { val (start, end) = transaction(db) { val minTimeColumn = BlockChangeView.time.min().notNull val maxTimeColumn = BlockChangeView.time.max().notNull @@ -43,40 +47,60 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo var current = start while (!current.isAfter(end)) { intervals.add(current) - current = current.plus(timeLapseMode!!.interval) + current = current.plus(timelapseMode!!.interval) } - val trackers = mutableMapOf() + val trackerPool = ScheduledThreadPoolExecutor(8) + val trackers = ConcurrentHashMap() for (time in intervals) { - val index = intervals.indexOf(time) + 1 - val tracker = buildTrackerState(time, "Timelapse-${index}") - if (tracker.isEmpty()) { - continue + trackerPool.submit { + val index = intervals.indexOf(time) + 1 + val tracker = buildTrackerState(time, "Timelapse-${index}") + 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 globalBlockOffset = BlockPosition.maxOf(allBlockOffsets.asSequence()) val allBlockMaxes = trackers.map { it.value.calculateZeroBlockOffset() } val globalBlockMax = BlockPosition.maxOf(allBlockMaxes.asSequence()) val globalBlockExpanse = BlockExpanse.offsetAndMax(globalBlockOffset, globalBlockMax) + logger.info("Calculations Completed") + + val renderState = render.createState() + val renderPool = ScheduledThreadPoolExecutor(8) 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 { val time = if (exactTimeAsString != null) Instant.parse(exactTimeAsString) else null val tracker = buildTrackerState(time, "Single-Time") 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() tracker.populateStateImage(state, expanse.offset) - val image = render.renderBufferedImage(state, expanse) + val image = render.renderBufferedImage(renderState, state, expanse) image.savePngFile("${render.id}${suffix}.png") } @@ -118,12 +142,18 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo return tracker } - enum class RenderType(val id: String, val renderBufferedImage: (BlockStateImage, BlockExpanse) -> BufferedImage) { - TopDown("top-down", { image, expanse -> image.buildTopDownImage(expanse) }), - HeightMap("height-map", { image, expanse -> image.buildHeightMapImage(expanse) }) + @Suppress("unused") + enum class RenderType(val id: String, val createState: () -> Any, val renderBufferedImage: (Any, BlockStateImage, BlockExpanse) -> BufferedImage) { + 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)), ByDay("days", Duration.ofDays(1)) } diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockStateImage.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockStateImage.kt index 52537f6..c20ec20 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockStateImage.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockStateImage.kt @@ -4,6 +4,7 @@ import cloud.kubelet.foundation.gjallarhorn.util.ColorGradient import cloud.kubelet.foundation.gjallarhorn.util.FloatClamp import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey import java.awt.Color +import java.awt.Rectangle import java.awt.image.BufferedImage import java.util.* @@ -18,8 +19,7 @@ class BlockStateImage { }[position.y] = state } - fun buildTopDownImage(expanse: BlockExpanse): BufferedImage { - val colorKey = RandomColorKey() + fun buildTopDownImage(expanse: BlockExpanse, randomColorKey: RandomColorKey): BufferedImage { return buildPixelQuadImage(expanse) { x, z -> val maybeYBlocks = blocks[x]?.get(z) if (maybeYBlocks == null) { @@ -32,7 +32,7 @@ class BlockStateImage { return@buildPixelQuadImage } - val color = colorKey.map(maxBlockState.type) + val color = randomColorKey.map(maxBlockState.type) setPixelQuad(x, z, color.rgb) } } @@ -59,16 +59,20 @@ class BlockStateImage { } private fun BufferedImage.setPixelQuad(x: Long, z: Long, rgb: Int) { - setRGB(x.toInt() * 2, z.toInt() * 2, 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) + drawSquare(x * quadImageSize, z * quadImageSize, quadImageSize.toLong(), 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 { val width = expanse.size.x 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 (z in 0 until height) { @@ -77,4 +81,8 @@ class BlockStateImage { } return bufferedImage } + + companion object { + const val quadImageSize = 4 + } } diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/util/RandomColorKey.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/util/RandomColorKey.kt index 3e0dd03..eefc725 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/util/RandomColorKey.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/util/RandomColorKey.kt @@ -1,11 +1,12 @@ package cloud.kubelet.foundation.gjallarhorn.util import java.awt.Color +import java.util.concurrent.ConcurrentHashMap class RandomColorKey { - private val colors = mutableMapOf() + private val colors = ConcurrentHashMap() - fun map(key: String) = colors.getOrPut(key) { findUniqueColor() } + fun map(key: String) = colors.computeIfAbsent(key) { findUniqueColor() } private fun findUniqueColor(): Color { var random = randomColor()