AutoDisposer.kt
src/jvmMain/kotlin/afield/graphics/vulkan/util/AutoDisposer.kt · 102 lines
package afield.graphics.vulkan.util
import java.lang.ref.PhantomReference
import java.lang.ref.ReferenceQueue
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
Releases Vulkan handles when the Kotlin object that owned them is garbage collected.
A resource calls AutoDisposer.register with a cleanup lambda and keeps the returned
AutoDisposer.Token. When the object has been collected, the renderer's frame loop calls
AutoDisposer.drain and queues the cleanup on its retirement queue. Explicit cleanup calls
AutoDisposer.Token.disable first, so a resource is released at most once whichever path gets
there first. Tests are in AutoDisposerTest.
object AutoDisposer
GC-driven disposal, with optional device ownership for deterministic shutdown. Cleanup actions must capture handles, not the registered object itself.
A renderer drains only its own device's collected objects onto its retirement queue. At shutdown it also releases still-reachable resources, after waiting for the device. Explicit cleanup and an already-enqueued GC action share one token and cannot both win.
class Token internal constructor
class Token internal constructor(
internal val ref: PhantomReference<Any>,
internal val name: String,
internal val site: StackTraceElement?,
internal val owner: Any?,
internal val order: Long,
) {
private var cleanup: (() -> Unit)? = null
fun updateCleanup(c: () -> Unit) {
@Synchronized
fun updateCleanup(c: () -> Unit) {
cleanup = c
entries[ref] = this
}
fun disable(): Boolean = takeCleanup() != null
Claim ownership for explicit cleanup; false means another path already released it.
@Synchronized
private fun takeCleanup(): (() -> Unit)? {
val action = cleanup
cleanup = null
entries.remove(ref, this)
ready.remove(ref, this)
return action
}
internal fun runCleanup()
internal fun runCleanup() {
val action = takeCleanup() ?: return
action()
}
}
private val queue = ReferenceQueue<Any>()
private val entries = ConcurrentHashMap<PhantomReference<Any>, Token>()
private val ready = ConcurrentHashMap<PhantomReference<Any>, Token>()
private val nextOrder = AtomicLong()
fun register
Track target; cleanup runs once after it is collected, or when owner shuts down.
fun register(
target: Any,
name: String = target.javaClass.simpleName.ifEmpty { "unnamed" },
owner: Any? = null,
cleanup: () -> Unit,
): Token {
val trace = Exception().stackTrace
val site = trace.getOrNull(2) ?: trace.getOrNull(1)
val ref = PhantomReference(target, queue)
return Token(ref, name, site, owner, nextOrder.incrementAndGet()).also {
it.updateCleanup(cleanup)
}
}
fun drain(owner: Any? = null, enqueue: (Runnable) -> Unit) {
Poll collected objects, keeping other owners' releases for their own render thread.
fun drain(owner: Any? = null, enqueue: (Runnable) -> Unit) {
while (true) {
val ref = queue.poll() ?: break
entries[ref]?.let { ready[it.ref] = it }
}
if (ready.isEmpty()) return
ready.values.filter { it.owner === owner }.sortedByDescending { it.order }.forEach { token ->
if (ready.remove(token.ref, token)) enqueue(Runnable { token.runCleanup() })
}
}
internal fun releaseOwner(owner: Any, enqueue: (Runnable) -> Unit) {
Release remaining resources, including those already awaiting deferred GC cleanup.
internal fun releaseOwner(owner: Any, enqueue: (Runnable) -> Unit) {
// Reverse construction order puts views after their use and before their images.
entries.values.filter { it.owner === owner }.sortedByDescending { it.order }.forEach { token ->
enqueue(Runnable { token.runCleanup() })
}
}