Skip to content

StreamDiffusionTemporalOutput.kt

src/jvmMain/kotlin/afield/media/video/diffusion/StreamDiffusionTemporalOutput.kt · 189 lines

package afield.media.video.diffusion

import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.device.CommandBuffer
import afield.document.context.*
import afield.graphics.render.*
import afield.graphics.scene.*
import afield.graphics.shader.*
import afield.graphics.render.Module
import afield.graphics.vulkan.targets.RenderToTexture
import afield.graphics.shader.fromViews
import afield.graphics.texture.HasImageViewArray
import afield.graphics.texture.PreparesSampledImages
import org.lwjgl.vulkan.VK10.*

The temporal half of a diffusion view: it holds the last two results the worker returned and renders the texture the view publishes.

Results arrive late and in groups, so geometry has moved on by the time a frame can be shown. Given a StreamDiffusionMotionProvider, a StreamDiffusionView routes its output through this file to settle both ends of that delay; without one it publishes the worker's output directly. Each arriving result is written into one of two history targets while the other is read, blended with the previous result where the provider's correspondence agrees and both came from the same worker generation, and the carried colours are clamped to the 3x3 neighbourhood of the fresh ones so a stroke that moved does not trail. The display target reprojects the newest history onto the current frame and mixes it with the live source where confidence runs out; with no correspondence for the current frame it shows that history as it stands.

internal class StreamDiffusionTemporalOutput

Two history targets at their source times, plus a display target at the current render time.

internal class StreamDiffusionTemporalOutput(
    private val renderer: Renderer, private val source: RenderToTexture,
    private val raw: RenderToTexture, private val motion: StreamDiffusionMotionProvider
) : AutoCloseable {
    private val targets = mutableListOf<RenderToTexture>()
    private val modules = mutableListOf<Module>()
    private fun target(format: Int = source.colorFormats[0]) = RenderToTexture(renderer) {
        width = source.width; height = source.height; useDepth = false; this.format = format
    }.also { it.setDefaultSampler(anisotropyEnable = false, filter = if (format == VK_FORMAT_R32G32B32A32_SFLOAT) VK_FILTER_NEAREST else VK_FILTER_LINEAR); targets += it }
    private val history = arrayOf(target(), target())

val output = target()

    private val invalid = target(VK_FORMAT_R32G32B32A32_SFLOAT)
    private var freshImage = raw
    private var historyImage = raw
    private var motionImage = invalid
    private fun pass(target: RenderToTexture, name: String): Pair<Module, Mesh> {
        val module = Module(parameters(renderer) {
            +target; vertex = VERTEX; fragment = FRAGMENT
            depthTest = false; depthWrite = false; blend = false
            textureResolver = { t ->
                val select = when (t.name) {
                    "freshImage" -> { { freshImage } }
                    "historyImage" -> { { historyImage } }
                    "motionImage" -> { { motionImage } }
                    else -> error("Unknown temporal binding ${t.name}")
                }
                // Resolvers run at pipeline construction. Forward views/layout preparation
                // on every draw so ping-pong history and provider targets actually change.
                val provider = object : HasImageViewArray, PreparesSampledImages {
                    override val imageViewArray get() = select().imageViewArray
                    override fun prepareSampledImagesFor(command: CommandBuffer) =
                        select().prepareSampledImagesFor(command)
                }
                fromViews(provider, t.name, select().defaultSampler!!)
            }
        })
        modules += module
        check(module.valid) { "Diffusion temporal shader failed" }
        val pointer = module.pointer()
        val mesh = Mesh(renderer, pointer).resize(3, 0)
        module.meshes[name] = mesh; target.modules[name] = pointer
        return module to mesh
    }
    private var closed = false
    private val historyPasses: Array<Pair<Module, Mesh>>
    private val displayPass: Pair<Module, Mesh>

init

    init {
        try {
            historyPasses = arrayOf(pass(history[0], "history-0"), pass(history[1], "history-1"))
            displayPass = pass(output, "reproject-display")
        } catch (e: Throwable) { close(); throw e }
    }
    private var historyIndex = 0
    private var historyFrame: StreamDiffusionFrame? = null
    private var invalidInitialized = false

var displayReprojection = false

var blend = 0.0

var displayRequests = 0L; private set

var historyRequests = 0L; private set

fun resolve(command: CommandBuffer, index: Int, current: StreamDiffusionFrame, arrived: StreamDiffusionFrame?)

    fun resolve(command: CommandBuffer, index: Int, current: StreamDiffusionFrame, arrived: StreamDiffusionFrame?) {
        if (!invalidInitialized) { invalid.render(command, index); invalidInitialized = true }
        if (arrived != null) {
            val previous = historyFrame
            val mapping = if (previous != null && previous.generation == arrived.generation && blend > 0) {
                historyRequests++
                checkedMotion(motion.record(command, StreamDiffusionMotionRequest(arrived, previous, StreamDiffusionMotionUse.HISTORY)), source)
            } else null
            val next = 1 - historyIndex
            freshImage = raw; historyImage = if (previous != null) history[historyIndex] else raw
            motionImage = mapping ?: invalid
            historyPasses[next].second.push["mode"] = 0
            historyPasses[next].second.push["amount"] = if (mapping != null) blend.toFloat() else 0f
            history[next].render(command, index)
            historyIndex = next; historyFrame = arrived
        }
        val reference = historyFrame
        val request = if (displayReprojection && reference != null)
            StreamDiffusionMotionRequest(current, reference, StreamDiffusionMotionUse.DISPLAY) else null
        val mapping = request?.let {
            displayRequests++
            checkedMotion(motion.record(command, it), source)
        }
        historyImage = if (reference != null) history[historyIndex] else raw
        val fallback = if (mapping != null) motion.recordFallback(command, request, source, historyImage) else null
        if (fallback != null) require(fallback.width == source.width && fallback.height == source.height &&
            fallback.colorLayerCount == 1 && fallback.colorFormats[0] == source.colorFormats[0] &&
            fallback.parameters.colorSampled && fallback.defaultSampler != null &&
            fallback.colorImageRefs[0]?.imageData?.sampleCount == VK_SAMPLE_COUNT_1_BIT) {
            "Diffusion display fallback must be sampled, single-sample and match source extent/colour format"
        }
        freshImage = fallback ?: source
        motionImage = mapping ?: invalid
        displayPass.second.push["mode"] = if (mapping != null) 1 else 2
        displayPass.second.push["amount"] = 0f
        output.render(command, index)
    }

override fun close()

    override fun close() {
        if (closed) return
        closed = true
        renderer.cleanup += Runnable {
            modules.forEach { it.cleanup() }
            targets.forEach { target ->
                target.imageViewArray.forEach { it.imageView.cleanup() }
                target.colorImageRefs.forEach { it?.cleanup() }; target.defaultSampler?.cleanup()
            }
        }
    }

companion object

fun checkedMotion(target: RenderToTexture?, source: RenderToTexture): RenderToTexture?

body — 47 lines, from line 142
        fun checkedMotion(target: RenderToTexture?, source: RenderToTexture): RenderToTexture? {
            if (target == null) return null
            require(target.width == source.width && target.height == source.height && target.colorLayerCount == 1 &&
                target.colorFormats[0] == VK_FORMAT_R32G32B32A32_SFLOAT && target.defaultSampler != null &&
                target.parameters.colorTransferSrc && target.parameters.colorSampled &&
                target.colorImageRefs[0]?.imageData?.sampleCount == VK_SAMPLE_COUNT_1_BIT) {
                "Diffusion motion must be a sampled, transfer-source, single-sample RGBA32F RTT at source resolution"
            }
            return target
        }
        private const val VERTEX = """#version 460
            layout(location=0) out vec2 uv;
            void main() {
                vec2 p=vec2((gl_VertexIndex<<1)&2,gl_VertexIndex&2);
                uv=vec2(p.x,1-p.y); gl_Position=vec4(p*2-1,0,1);
            }
        """
        private const val FRAGMENT = """#version 460
            layout(location=0) in vec2 uv;
            layout(set=0,binding=0) uniform sampler2D freshImage;
            layout(set=0,binding=1) uniform sampler2D historyImage;
            layout(set=0,binding=2) uniform sampler2D motionImage;
            layout(push_constant) uniform P { int mode; float amount; } p;
            layout(location=0) out vec4 color;
            void main() {
                if (p.mode==2) { color=texture(historyImage,uv); return; }
                vec4 fresh=texture(freshImage,uv);
                vec4 m=texelFetch(motionImage,ivec2(gl_FragCoord.xy),0);
                bool valid=!any(isnan(m)) && !any(isinf(m)) &&
                    all(greaterThanEqual(m.xy,vec2(0))) && all(lessThanEqual(m.xy,vec2(1)));
                float trust=valid?clamp(m.z,0,1)*(1-clamp(m.w,0,1)):0;
                if (trust<=0) { color=fresh; return; }
                vec4 old=texture(historyImage,m.xy);
                if (p.mode==1) { color=mix(fresh,old,trust); return; }
                // Clamp history to fresh local colours to reduce trails when geometry changes.
                vec3 lo=fresh.rgb, hi=fresh.rgb;
                vec2 texel=1./vec2(textureSize(freshImage,0));
                for(int y=-1;y<=1;y++) for(int x=-1;x<=1;x++) {
                    vec3 c=texture(freshImage,uv+vec2(x,y)*texel).rgb;
                    lo=min(lo,c); hi=max(hi,c);
                }
                old.rgb=clamp(old.rgb,lo,hi);
                color=mix(fresh,old,clamp(p.amount,0,1)*trust);
            }
        """
    }
}