Skip to content

FabricatePassGraph.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassGraph.kt · 116 lines

package afield.graphics.scene.fabricated

The ordering primitive the Fabricate renderer's pass collections are built on.

Both FabricatePostStack and FabricateFrameGraph hold a FabricatePassGraph, so what a pass name means, and where a new pass may go relative to one already there, is settled once here rather than once per collection. The same file carries the rule that decides where a replacement chain picks up its input: the output of the pass before the first one it replaces, or the chain's original input when that pass comes first.

internal fun <T> inputBeforeReplacement

New chains append; replacements keep the input before their first existing pass.

internal fun <T> inputBeforeReplacement(
    entries: List<Pair<String, T>>,
    replacing: Set<String>,
    initialInput: T
): T = entries.takeWhile { (name, _) -> name !in replacing }.lastOrNull()?.second ?: initialInput

The ordered, name-keyed map both of this package's pass collections are built on.

FabricatePassGraph is a LinkedHashMap with an API that makes the ordering part of the contract rather than a property of whichever map a caller happened to reach for. Names are unique and non-blank, and the three ways of putting one in differ in what they will accept: addLast refuses a name already present, replace refuses one that is not, and putOrReplace takes either. insertBefore and insertAfter position a new pass relative to one already there, and both rebuild the map end to end, because a LinkedHashMap has no way to place an entry anywhere but last.

Two collections here are this map with a type argument and a vocabulary on top. FabricatePostStack holds FabricatePostEffects, which is what lets a feature insert a post pass beside one it did not add; FabricateFrameGraph holds Field 4 targets, which is what gives the renderer's off-screen passes names and an order before any of them reach a RenderTarget. Nothing in this file imports anything, so the ordering rules can be exercised on plain strings, which is what FabricatePassGraphTestMain.kt does.

class FabricatePassGraph<T>(initial: Iterable<Pair<String, T>> = emptyList())

class FabricatePassGraph<T>(initial: Iterable<Pair<String, T>> = emptyList()) {
    private val ordered = LinkedHashMap<String, T>()

init

    init {
        initial.forEach { (name, target) -> addLast(name, target) }
    }

val size: Int

    val size: Int
        get() = ordered.size

fun names(): List<String> = ordered.keys.toList()

fun entries(): List<Pair<String, T>> = ordered.entries.map { it.key to it.value }

operator fun contains(name: String): Boolean = ordered.containsKey(name)

operator fun get(name: String): T? = ordered[name]

fun addLast(name: String, target: T): FabricatePassGraph<T>

    fun addLast(name: String, target: T): FabricatePassGraph<T> {
        require(name.isNotBlank()) { "pass name must not be blank" }
        require(name !in ordered) { "pass '$name' already exists" }
        ordered[name] = target
        return this
    }

fun replace(name: String, target: T): FabricatePassGraph<T>

    fun replace(name: String, target: T): FabricatePassGraph<T> {
        require(name in ordered) { "cannot replace missing pass '$name'" }
        ordered[name] = target
        return this
    }

fun putOrReplace(name: String, target: T): FabricatePassGraph<T>

    fun putOrReplace(name: String, target: T): FabricatePassGraph<T> {
        require(name.isNotBlank()) { "pass name must not be blank" }
        ordered[name] = target
        return this
    }

fun insertBefore(name: String, before: String, target: T): FabricatePassGraph<T>

    fun insertBefore(name: String, before: String, target: T): FabricatePassGraph<T> {
        require(name.isNotBlank()) { "pass name must not be blank" }
        require(name !in ordered) { "pass '$name' already exists" }
        require(before in ordered) { "anchor pass '$before' does not exist" }
        rebuild { rebuilt ->
            ordered.forEach { (existingName, existingTarget) ->
                if (existingName == before) rebuilt[name] = target
                rebuilt[existingName] = existingTarget
            }
        }
        return this
    }

fun insertAfter(name: String, after: String, target: T): FabricatePassGraph<T>

    fun insertAfter(name: String, after: String, target: T): FabricatePassGraph<T> {
        require(name.isNotBlank()) { "pass name must not be blank" }
        require(name !in ordered) { "pass '$name' already exists" }
        require(after in ordered) { "anchor pass '$after' does not exist" }
        rebuild { rebuilt ->
            ordered.forEach { (existingName, existingTarget) ->
                rebuilt[existingName] = existingTarget
                if (existingName == after) rebuilt[name] = target
            }
        }
        return this
    }

fun remove(name: String): T? = ordered.remove(name)

fun clear(): FabricatePassGraph<T>

    fun clear(): FabricatePassGraph<T> {
        ordered.clear()
        return this
    }

    private fun rebuild(populate: (LinkedHashMap<String, T>) -> Unit) {
        val rebuilt = LinkedHashMap<String, T>(ordered.size + 1)
        populate(rebuilt)
        ordered.clear()
        ordered.putAll(rebuilt)
    }
}