Zum Inhalt

native/macos/KuroNativeSaver/Core/GlitchScheduler.swift

// GlitchScheduler — the weighted CRT-artifact timeline. One artifact at a time,
// fired on a randomized cadence and auto-reverted after its duration; it writes
// distortion values the composite shader reads each frame. Ported from the web
// crt-sim.ts scheduler (slice set: h-tear, brightness flicker, chroma spike,
// scanline pulse — the full 12-artifact set + crash sequence come later).

import Foundation

final class GlitchScheduler {
    var intensity: Float
    private var rng: LCG

    private enum Kind { case none, hTear, flicker, chroma, scanPulse, vRoll, blackFrame, skew, staticBurst }
    private var kind: Kind = .none
    private var until: Double = 0
    private var nextAt: Double
    private var forced = false

    // current distortion values
    private var hTearAmount: Float = 0
    private var hTearBandY: Float = 0
    private var brightness: Float = 1
    private var scanPulse: Float = 0
    private var chromaBump: Float = 0
    private var vRoll: Float = 0
    private var blackFrame: Float = 0
    private var skewX: Float = 0
    private var staticAmt: Float = 0

    init(intensity: Float, seed: Int32) {
        self.intensity = intensity
        self.rng = LCG(seed: seed == 0 ? 1 : seed)
        self.nextAt = Double(max(200, 3500 - 3000 * intensity)) / 1000.0
    }

    func update(t: Double) {
        if forced { return }
        if t >= until { reset() }
        if t >= nextAt { fire(now: t); scheduleNext(now: t) }
    }

    private func reset() {
        kind = .none; hTearAmount = 0; brightness = 1; scanPulse = 0; chromaBump = 0
        vRoll = 0; blackFrame = 0; skewX = 0; staticAmt = 0
    }

    private func scheduleNext(now: Double) {
        let interval = Double(max(200, 3500 - 3000 * intensity))
        nextAt = now + (interval + rng.next() * 0.6 * interval) / 1000.0
    }

    private func fire(now: Double) {
        let i = intensity
        let r = rng.next() * 103         // weights 28+20+12+14+4+8+10+7
        let dur: Double
        if r < 28 {                      // h-tear
            kind = .hTear
            hTearAmount = (Float(rng.next()) - 0.5) * (0.006 + 0.02 * i)
            hTearBandY = Float(rng.next())
            dur = 0.04 + rng.next() * 0.08
        } else if r < 48 {               // brightness flicker
            kind = .flicker
            if rng.next() < 0.6 { brightness = 1 - 0.3 * i - Float(rng.next()) * 0.2 }
            else { brightness = 1 + 0.3 * i + Float(rng.next()) * 0.4 }
            dur = 0.06 + rng.next() * 0.09
        } else if r < 60 {               // chroma spike
            kind = .chroma
            chromaBump = (0.003 + Float(rng.next()) * 0.006) * i
            dur = 0.08 + rng.next() * 0.12
        } else if r < 74 {               // scanline pulse
            kind = .scanPulse
            scanPulse = 0.4 + Float(rng.next()) * 1.2 * i
            dur = 0.14 + rng.next() * 0.22
        } else if r < 78 {               // v-roll
            kind = .vRoll
            vRoll = (0.04 + Float(rng.next()) * 0.06) * i
            dur = 0.15 + rng.next() * 0.2
        } else if r < 86 {               // black-frame drop
            kind = .blackFrame; blackFrame = 1
            dur = 0.02 + rng.next() * 0.03
        } else if r < 96 {               // wave skew
            kind = .skew
            skewX = (Float(rng.next()) - 0.5) * 0.08 * i
            dur = 0.08 + rng.next() * 0.1
        } else {                         // static burst
            kind = .staticBurst
            staticAmt = (0.35 + 0.55 * i) * Float(rng.next())
            dur = 0.04 + rng.next() * 0.05
        }
        until = now + dur
    }

    func uniforms() -> SIMD4<Float> { SIMD4(hTearAmount, hTearBandY, brightness, scanPulse) }
    func uniforms3() -> SIMD4<Float> { SIMD4(vRoll, blackFrame, skewX, staticAmt) }
    var chromaOffsetBump: Float { chromaBump }

    /// Debug: hold one artifact active indefinitely (for single-frame verification).
    func forceHold(_ name: String) {
        forced = true; reset()
        switch name {
        case "htear": kind = .hTear; hTearAmount = 0.03; hTearBandY = 0.5
        case "flicker": kind = .flicker; brightness = 1.7
        case "chroma": kind = .chroma; chromaBump = 0.012
        case "scanpulse": kind = .scanPulse; scanPulse = 1.6
        case "vroll": kind = .vRoll; vRoll = 0.2
        case "skew": kind = .skew; skewX = 0.1
        case "static": kind = .staticBurst; staticAmt = 0.6
        default: break
        }
    }
}