Skip to content

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() })
        }
    }

internal fun ownedCount(owner: Any): Int = entries.values.count { it.owner === owner }