Gjallarhorn: Introduce concept of block changelogs, which makes timelapse rendering more efficient.

This commit is contained in:
Kenneth Endfinger
2022-01-08 02:21:42 -05:00
parent 08ba582931
commit 643567dfb5
8 changed files with 112 additions and 48 deletions

View File

@ -14,17 +14,15 @@ import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.enum import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.int
import jetbrains.exodus.kotlin.notNull import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.and
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
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.ConcurrentHashMap
import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
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>()
@ -42,13 +40,8 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
override fun run() { override fun run() {
if (timelapseMode != null) { if (timelapseMode != null) {
val (start, end) = transaction(db) { val changelog = BlockChangelog.query(db)
val minTimeColumn = BlockChangeView.time.min().notNull val (start, end) = changelog.changeTimeRange
val maxTimeColumn = BlockChangeView.time.max().notNull
val row = BlockChangeView.slice(minTimeColumn, maxTimeColumn).selectAll().single()
row[minTimeColumn]!! to row[maxTimeColumn]!!
}
var intervals = mutableListOf<Instant>() var intervals = mutableListOf<Instant>()
var current = start var current = start
while (!current.isAfter(end)) { while (!current.isAfter(end)) {
@ -65,7 +58,8 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
for (time in intervals) { for (time in intervals) {
trackerPool.submit { trackerPool.submit {
val index = intervals.indexOf(time) + 1 val index = intervals.indexOf(time) + 1
val tracker = buildTrackerState(time, "Timelapse-${index}") val tracker =
buildTrackerState(changelog.slice(time.minus(timelapseMode!!.interval) to time), "Timelapse-${index}")
if (tracker.isEmpty()) { if (tracker.isEmpty()) {
return@submit return@submit
} }
@ -102,51 +96,33 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
logger.info("Rendering Completed") 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 filter = compose(
combine = { a, b -> a and b },
{ time != null } to { BlockChangeView.time lessEq time!! }
)
val changelog = BlockChangelog.query(db, filter)
val tracker = buildTrackerState(changelog, "Single-Time")
val expanse = BlockExpanse.offsetAndMax(tracker.calculateZeroBlockOffset(), tracker.calculateMaxBlock()) val expanse = BlockExpanse.offsetAndMax(tracker.calculateZeroBlockOffset(), tracker.calculateMaxBlock())
saveRenderImage(render.create(expanse), tracker, expanse) saveRenderImage(render.create(expanse), tracker, expanse)
} }
} }
fun saveRenderImage(renderer: BlockImageRenderer, tracker: BlockLogTracker, expanse: BlockExpanse, suffix: String = "") { fun saveRenderImage(
renderer: BlockImageRenderer,
tracker: BlockLogTracker,
expanse: BlockExpanse,
suffix: String = ""
) {
val map = tracker.buildBlockMap(expanse.offset) val map = tracker.buildBlockMap(expanse.offset)
val image = renderer.render(map) val image = renderer.render(map)
image.savePngFile("${render.id}${suffix}.png") image.savePngFile("${render.id}${suffix}.png")
} }
fun buildTrackerState(time: Instant?, job: String): BlockLogTracker { fun buildTrackerState(changelog: BlockChangelog, job: String): BlockLogTracker {
val filter = compose(
combine = { a, b -> a and b },
{ time != null } to { BlockChangeView.time lessEq time!! }
)
val tracker = val tracker =
BlockLogTracker(if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete) BlockLogTracker(if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete)
tracker.replay(changelog)
val blockChangeCounter = AtomicLong() logger.info("Job $job Total Block Changes... ${changelog.changes.size}")
transaction(db) {
BlockChangeView.select(filter).orderBy(BlockChangeView.time).forEach { row ->
val changeIsBreak = row[BlockChangeView.isBreak]
val x = row[BlockChangeView.x]
val y = row[BlockChangeView.y]
val z = row[BlockChangeView.z]
val block = row[BlockChangeView.block]
val location = BlockCoordinate(x.toLong(), y.toLong(), z.toLong())
if (changeIsBreak) {
tracker.delete(location)
} else {
tracker.place(location, BlockState(block))
}
val count = blockChangeCounter.addAndGet(1)
if (count % 1000L == 0L) {
logger.info("Job $job Calculating Block Changes... $count")
}
}
}
logger.info("Job $job Total Block Changes... ${blockChangeCounter.get()}")
val uniqueBlockPositions = tracker.blocks.size val uniqueBlockPositions = tracker.blocks.size
logger.info("Job $job Unique Block Positions... $uniqueBlockPositions") logger.info("Job $job Unique Block Positions... $uniqueBlockPositions")
maybeTrimState(tracker) maybeTrimState(tracker)

View File

@ -6,7 +6,8 @@ import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey
import java.awt.Color import java.awt.Color
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
class BlockDiversityRenderer(val expanse: BlockExpanse, quadPixelSize: Int = defaultQuadPixelSize) : BlockGridRenderer(quadPixelSize) { class BlockDiversityRenderer(val expanse: BlockExpanse, quadPixelSize: Int = defaultQuadPixelSize) :
BlockGridRenderer(quadPixelSize) {
private val randomColorKey = RandomColorKey() private val randomColorKey = RandomColorKey()
override fun render(map: BlockMap): BufferedImage = buildPixelQuadImage(expanse) { x, z -> override fun render(map: BlockMap): BufferedImage = buildPixelQuadImage(expanse) { x, z ->

View File

@ -7,7 +7,11 @@ import java.awt.Color
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
abstract class BlockHeatMapRenderer(quadPixelSize: Int = defaultQuadPixelSize) : BlockGridRenderer(quadPixelSize) { abstract class BlockHeatMapRenderer(quadPixelSize: Int = defaultQuadPixelSize) : BlockGridRenderer(quadPixelSize) {
protected fun buildHeatMapImage(expanse: BlockExpanse, clamp: FloatClamp, calculate: (Long, Long) -> Long?): BufferedImage = protected fun buildHeatMapImage(
expanse: BlockExpanse,
clamp: FloatClamp,
calculate: (Long, Long) -> Long?
): BufferedImage =
buildPixelQuadImage(expanse) { x, z -> buildPixelQuadImage(expanse) { x, z ->
val value = calculate(x, z) val value = calculate(x, z)
val color = if (value != null) { val color = if (value != null) {

View File

@ -0,0 +1,11 @@
package cloud.kubelet.foundation.gjallarhorn.state
import java.time.Instant
data class BlockChange(
val time: Instant,
val type: BlockChangeType,
val location: BlockCoordinate,
val from: BlockState,
val to: BlockState
)

View File

@ -0,0 +1,6 @@
package cloud.kubelet.foundation.gjallarhorn.state
enum class BlockChangeType {
Place,
Break
}

View File

@ -0,0 +1,54 @@
package cloud.kubelet.foundation.gjallarhorn.state
import cloud.kubelet.foundation.heimdall.view.BlockChangeView
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import java.time.Instant
class BlockChangelog(
val changes: List<BlockChange>
) {
fun slice(range: Pair<Instant, Instant>): BlockChangelog = BlockChangelog(changes.filter {
it.time >= range.first &&
it.time <= range.second
})
val changeTimeRange: Pair<Instant, Instant>
get() = changes.minOf { it.time } to changes.maxOf { it.time }
companion object {
fun query(db: Database, filter: Op<Boolean> = Op.TRUE): BlockChangelog = transaction(db) {
BlockChangelog(BlockChangeView.select(filter).orderBy(BlockChangeView.time).map { row ->
val time = row[BlockChangeView.time]
val changeIsBreak = row[BlockChangeView.isBreak]
val x = row[BlockChangeView.x]
val y = row[BlockChangeView.y]
val z = row[BlockChangeView.z]
val block = row[BlockChangeView.block]
val location = BlockCoordinate(x.toLong(), y.toLong(), z.toLong())
val fromBlock = if (changeIsBreak) {
BlockState(block)
} else {
BlockState.AirBlock
}
val toBlock = if (changeIsBreak) {
BlockState.AirBlock
} else {
BlockState(block)
}
BlockChange(
time,
if (changeIsBreak) BlockChangeType.Break else BlockChangeType.Place,
location,
fromBlock,
toBlock
)
})
}
}
}

View File

@ -57,4 +57,12 @@ class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOn
} }
return map return map
} }
fun replay(changelog: BlockChangelog) = changelog.changes.forEach { change ->
if (change.type == BlockChangeType.Break) {
delete(change.location)
} else {
place(change.location, change.to)
}
}
} }

View File

@ -3,4 +3,8 @@ package cloud.kubelet.foundation.gjallarhorn.state
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class BlockState(val type: String) data class BlockState(val type: String) {
companion object {
val AirBlock = BlockState("minecraft:air")
}
}