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
No known key found for this signature in database
GPG Key ID: C4E68E5647420E10
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.types.enum
import com.github.ajalt.clikt.parameters.types.int
import jetbrains.exodus.kotlin.notNull
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.Database
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 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<Database>()
@ -42,13 +40,8 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
override fun run() {
if (timelapseMode != null) {
val (start, end) = transaction(db) {
val minTimeColumn = BlockChangeView.time.min().notNull
val maxTimeColumn = BlockChangeView.time.max().notNull
val row = BlockChangeView.slice(minTimeColumn, maxTimeColumn).selectAll().single()
row[minTimeColumn]!! to row[maxTimeColumn]!!
}
val changelog = BlockChangelog.query(db)
val (start, end) = changelog.changeTimeRange
var intervals = mutableListOf<Instant>()
var current = start
while (!current.isAfter(end)) {
@ -65,7 +58,8 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
for (time in intervals) {
trackerPool.submit {
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()) {
return@submit
}
@ -102,51 +96,33 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
logger.info("Rendering Completed")
} else {
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())
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 image = renderer.render(map)
image.savePngFile("${render.id}${suffix}.png")
}
fun buildTrackerState(time: Instant?, job: String): BlockLogTracker {
val filter = compose(
combine = { a, b -> a and b },
{ time != null } to { BlockChangeView.time lessEq time!! }
)
fun buildTrackerState(changelog: BlockChangelog, job: String): BlockLogTracker {
val tracker =
BlockLogTracker(if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete)
val blockChangeCounter = AtomicLong()
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()}")
tracker.replay(changelog)
logger.info("Job $job Total Block Changes... ${changelog.changes.size}")
val uniqueBlockPositions = tracker.blocks.size
logger.info("Job $job Unique Block Positions... $uniqueBlockPositions")
maybeTrimState(tracker)

View File

@ -6,7 +6,8 @@ import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey
import java.awt.Color
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()
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
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 ->
val value = calculate(x, z)
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
}
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
@Serializable
data class BlockState(val type: String)
data class BlockState(val type: String) {
companion object {
val AirBlock = BlockState("minecraft:air")
}
}