Zum Inhalt

native/macos/KuroNativeSaver/Core/TrailsChain.swift

// TrailsChain — phosphor persistence. A ping-pong accumulation texture: each
// frame outputs max(scene, prevAccum * decay), so bright moving wireframes leave
// a glowing, decaying trail. The result feeds bloom + composite.

import Metal

final class TrailsChain {
    private let device: MTLDevice
    private let pipeline: MTLRenderPipelineState
    private var texA: MTLTexture?, texB: MTLTexture?
    private var w = 0, h = 0
    private var cur = 0
    var decay: Float = 0.6

    init(device: MTLDevice, library: MTLLibrary) {
        self.device = device
        let pd = MTLRenderPipelineDescriptor()
        pd.vertexFunction = library.makeFunction(name: "fsq_v")
        pd.fragmentFunction = library.makeFunction(name: "trails_f")
        pd.colorAttachments[0].pixelFormat = Renderer.hdrFormat
        pipeline = try! device.makeRenderPipelineState(descriptor: pd)
    }

    private func ensure(_ nw: Int, _ nh: Int, _ queue: MTLCommandQueue) {
        guard nw != w || nh != h || texA == nil else { return }
        w = nw; h = nh
        let d = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: Renderer.hdrFormat, width: nw, height: nh, mipmapped: false)
        d.usage = [.renderTarget, .shaderRead]; d.storageMode = .private
        texA = device.makeTexture(descriptor: d); texB = device.makeTexture(descriptor: d)
        // clear both so the first frame's "previous" is black, not garbage
        let cb = queue.makeCommandBuffer()!
        for t in [texA!, texB!] {
            let rp = MTLRenderPassDescriptor()
            rp.colorAttachments[0].texture = t
            rp.colorAttachments[0].loadAction = .clear; rp.colorAttachments[0].storeAction = .store
            rp.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)
            cb.makeRenderCommandEncoder(descriptor: rp)!.endEncoding()
        }
        cb.commit()
    }

    /// Render max(sceneHDR, prevAccum*decay) into the next ping-pong slot; returns it.
    func generate(_ cb: MTLCommandBuffer, sceneHDR: MTLTexture, queue: MTLCommandQueue) -> MTLTexture {
        ensure(sceneHDR.width, sceneHDR.height, queue)
        guard let texA, let texB else { return sceneHDR }
        let prev = (cur == 0) ? texB : texA
        let dst  = (cur == 0) ? texA : texB
        let rp = MTLRenderPassDescriptor()
        rp.colorAttachments[0].texture = dst
        rp.colorAttachments[0].loadAction = .dontCare; rp.colorAttachments[0].storeAction = .store
        let enc = cb.makeRenderCommandEncoder(descriptor: rp)!
        enc.setRenderPipelineState(pipeline)
        enc.setFragmentTexture(sceneHDR, index: 0)
        enc.setFragmentTexture(prev, index: 1)
        var d = decay; enc.setFragmentBytes(&d, length: MemoryLayout<Float>.stride, index: 0)
        enc.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
        enc.endEncoding()
        cur ^= 1
        return dst
    }
}