Skip to content

TileResidency.kt

src/jvmMain/kotlin/afield/graphics/render/TileResidency.kt · 127 lines

@file:afield.runtime.completion.CompletionCategory("Simulation")

package afield.graphics.render

import afield.data.collections.get
import afield.data.collections.invoke
import afield.data.collections.times
import afield.document.graph.minusAssign
import afield.document.graph.plusAssign
import afield.document.properties.compareTo
import afield.document.workspace.file
import afield.geometry.mesh.contains
import afield.math.autodiff.compareTo
import afield.math.autodiff.div
import afield.math.autodiff.minus
import afield.math.autodiff.plus
import afield.math.autodiff.times
import afield.math.linalg.invoke
import afield.math.linalg.remAssign
import afield.math.linalg.set
import afield.math.linalg.times
import afield.math.units.div
import afield.math.units.minus
import afield.math.units.plus
import afield.math.units.times

import afield.audio.synth.*
import afield.data.collections.s
import afield.math.linalg.i
import afield.math.linalg.x
import kotlin.math.floor

Bookkeeping for a scrolling window of world-aligned tiles: TileResidency maps tile coordinates to reusable toroidal slots and reports which slots changed after a move.

It allocates no GPU resources or threads; callers use its slot arrays to decide what to regenerate.

class TileResidency(val columns: Int, val rows: Int, val tileSize: Float)

A bounded, world-aligned rectangle of tiles, stored in reusable toroidal slots. Moving one tile replaces only the entering row/column. Teleporting replaces at most capacity tiles. No resources or threads are created here.

A slot index alone is not an identity: check coordinates with resident, and use generation to reject old GPU/particle data after a slot is recycled. Arrays are borrowed, read-only to callers, and owned by the updating thread.

init

    init {
        require(columns > 0 && rows > 0 && columns.toLong() * rows <= Int.MAX_VALUE / 4)
        require(tileSize.isFinite() && tileSize > 0f)
    }

val capacity = columns * rows

val x = IntArray(capacity)

val y = IntArray(capacity)

val generation = IntArray(capacity)

val changed = IntArray(capacity)

    private val occupied = BooleanArray(capacity)

var changedCount

    var changedCount = 0
        private set

var minX

    var minX = 0
        private set

var minY

    var minY = 0
        private set

var relocations

    var relocations = 0L
        private set

fun slot(tx: Int, ty: Int): Int = Math.floorMod(ty, rows) * columns + Math.floorMod(tx, columns)

fun resident(tx: Int, ty: Int): Int

-1 outside the resident rectangle, even if the modulo slot contains another tile.

    fun resident(tx: Int, ty: Int): Int {
        val s = slot(tx, ty)
        return if (occupied[s] && x[s] == tx && y[s] == ty) s else -1
    }

fun invalidate() { occupied.fill(false) }

Drop contents at the next move, preserving generation identities.

fun moveTo(px: Double, py: Double): Int

    fun moveTo(px: Double, py: Double): Int {
        require(px.isFinite() && py.isFinite())
        val nx = floor(px / tileSize) - columns / 2
        val ny = floor(py / tileSize) - rows / 2
        require(nx >= Int.MIN_VALUE.toDouble() && nx + columns - 1 <= Int.MAX_VALUE.toDouble())
        require(ny >= Int.MIN_VALUE.toDouble() && ny + rows - 1 <= Int.MAX_VALUE.toDouble())
        val lowX = nx.toInt()
        val lowY = ny.toInt()
        // Preflight before mutation: overflow may not silently reuse an old generation.
        for (j in 0 until rows) for (i in 0 until columns) {
            val tx = lowX + i
            val ty = lowY + j
            val s = slot(tx, ty)
            if (resident(tx, ty) < 0) check(generation[s] < Int.MAX_VALUE) { "Tile generation exhausted" }
        }
        changedCount = 0
        minX = lowX
        minY = lowY
        for (j in 0 until rows) for (i in 0 until columns) {
            val tx = lowX + i
            val ty = lowY + j
            val s = slot(tx, ty)
            if (resident(tx, ty) < 0) {
                x[s] = tx
                y[s] = ty
                generation[s]++
                occupied[s] = true
                changed[changedCount++] = s
                relocations++
            }
        }
        return changedCount
    }

fun pack(out: IntArray, offset: Int = 0): IntArray

Four ints per slot: world tile x/y, generation, occupied. No Float precision loss.

    fun pack(out: IntArray, offset: Int = 0): IntArray {
        require(offset >= 0 && out.size.toLong() >= offset.toLong() + capacity * 4L)
        for (s in 0 until capacity) {
            val at = offset + s * 4
            out[at] = x[s]
            out[at + 1] = y[s]
            out[at + 2] = generation[s]
            out[at + 3] = if (occupied[s]) 1 else 0
        }
        return out
    }
}