Skip to content

FabricatePostStack.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePostStack.kt · 127 lines

package afield.graphics.scene.fabricated

import afield.graphics.vulkan.targets.RenderToTexture
import afield.graphics.render.ModulePointer

The ordered post-processing stack a Fabricate pass lands in, and the record describing one pass.

One source texture enters the stack, named passes run in order, and the last one's target is the image to present; a stack with no passes presents its input unchanged. Ordering is delegated to FabricatePassGraph, so passes are addressed by name: a feature can insert before or after a pass it did not add, and two features that each append a pass compose rather than overwriting one another.

FabricatePostEffect is what the stack stores: a name, the target the pass renders into, the module holding its pipeline, and a kind tag. That tag is how FabricateFrameGraph recognises the two passes with a fixed place in the standard pass order, motion blur and anti-aliasing; every other pass is named after its effect.

class FabricatePostStack

Ordered post-processing stack for Fabricate, following Three.js' EffectComposer model: one source texture enters the stack, named passes run in order, and the final pass is the image to present.

class FabricatePostStack(
    val input: RenderToTexture,
    initial: Iterable<FabricatePostEffect> = emptyList()
) {
    private val graph = FabricatePassGraph<FabricatePostEffect>()

internal val changeListeners = java.util.concurrent.CopyOnWriteArrayList<() -> Unit>()

Called after every edit, so whatever has the stack attached can re-attach it.

    private fun changed() {
        effects().forEach { register(it.target, this) }
        changeListeners.forEach { it() }
    }

init

    init {
        initial.forEach { effect -> append(effect) }
    }

fun names(): List<String> = graph.names()

fun effects(): List<FabricatePostEffect> = graph.entries().map { it.second }

operator fun get(name: String): FabricatePostEffect? = graph[name]

fun append(effect: FabricatePostEffect): FabricatePostStack

    fun append(effect: FabricatePostEffect): FabricatePostStack {
        graph.addLast(effect.name, effect)
        changed()
        return this
    }

fun insertBefore(effect: FabricatePostEffect, before: String): FabricatePostStack

    fun insertBefore(effect: FabricatePostEffect, before: String): FabricatePostStack {
        graph.insertBefore(effect.name, before, effect)
        changed()
        return this
    }

fun insertAfter(effect: FabricatePostEffect, after: String): FabricatePostStack

    fun insertAfter(effect: FabricatePostEffect, after: String): FabricatePostStack {
        graph.insertAfter(effect.name, after, effect)
        changed()
        return this
    }

fun replace(name: String, effect: FabricatePostEffect): FabricatePostStack

    fun replace(name: String, effect: FabricatePostEffect): FabricatePostStack {
        require(name == effect.name) { "replacement effect name '${effect.name}' must match '$name'" }
        graph.replace(name, effect)
        changed()
        return this
    }

fun remove(name: String): FabricatePostEffect? = graph.remove(name)?.also { changed() }

fun output(): RenderToTexture = effects().lastOrNull()?.target ?: input

companion object

    companion object {

Stacks that have held each target, weakly, so a pass that is closing can leave them.

        private val holders = java.util.WeakHashMap<RenderToTexture, MutableSet<FabricatePostStack>>()

        private fun register(target: RenderToTexture, stack: FabricatePostStack) = synchronized(holders) {
            holders.getOrPut(target) { java.util.Collections.newSetFromMap(java.util.WeakHashMap()) }.add(stack)
        }

internal fun retire(target: RenderToTexture): Int

Remove every effect rendering into target from each stack it was added to. Each stack then re-attaches to the windows it is attached to, so the closed pass leaves their frame graphs. Returns how many effects were removed.

        internal fun retire(target: RenderToTexture): Int {
            val stacks = synchronized(holders) { holders.remove(target)?.toList().orEmpty() }
            return stacks.sumOf { stack ->
                stack.effects().filter { it.target === target }.count { stack.remove(it.name) != null }
            }
        }
    }
}

data class FabricatePostEffect

One pass in a FabricatePostStack: its name, output target, optional shader module and kind. Shader effects own a module. Native effects record compute from their target and leave module null. FabricateFrameGraph gives motion blur and anti-aliasing their reserved names in the standard pass order; other effects use their own name. Native effects are excluded from shader inspection and reload.

data class FabricatePostEffect(
    val name: String,
    val target: RenderToTexture,
    val module: ModulePointer?,
    val kind: Kind = Kind.SHADER
) {

init

    init {
        require((kind==Kind.NATIVE)==(module==null)) { "Native effects have no shader module; shader effects require one" }
    }

enum class Kind

Which reserved slot in the standard pass order this pass claims, if any.

    enum class Kind {
        SHADER,
        MOTION_BLUR,
        FXAA,
        CUSTOM,

Target records a native compute pass outside a render pass. No fragment module.

        NATIVE
    }
}