Zum Inhalt

apple/PhysarumEngine/Sources/PhysarumEngine/Model/Params.swift

import simd

/// Byte-exact port of `src/engine/params.ts`. The packed layouts MUST match the
/// WGSL `Params` (304 B / 76 floats) and `PostParams` (80 B / 20 floats) structs
/// exactly — verified by `ParamsLayoutTests` and end-to-end by the keystone spike.

public struct UniformInput {
    public var fieldWidth: UInt32
    public var fieldHeight: UInt32
    public var agentCount: UInt32
    public var frame: UInt32
    public var time: Float
    public var decayRate: Float
    public var fadeStrength: Float
    public var trailSaturation: Float
    public var diffusionFreq: UInt32
    public var chaosInterval: UInt32
    public var chaosStrength: Float
    public var rhythmAmplitude: Float
    public var exposure: Float
    public var spawnPattern: SpawnPattern
    public var headIntensity: Float
    public var backgroundColor: RGB
    public var species: (SpeciesParams, SpeciesParams, SpeciesParams)
    public var matrix: InteractionMatrix

    public struct Audio {
        public var moveSpeedFactor: Float = 1
        public var turnSpeedFactor: Float = 1
        public var depositFactor: Float = 1
        public var sensorDistanceFactor: Float = 1
        public var rhythmAmplitudeFactor: Float = 1
        public var chaosStrengthFactor: Float = 1
        public init() {}
    }
    public var audio = Audio()

    public static func zeroed(fieldWidth: UInt32, fieldHeight: UInt32, agentCount: UInt32) -> UniformInput {
        let z = SpeciesParams(
            color: .zero, populationShare: 0, sensorAngle: 0, sensorDistance: 1,
            turnSpeed: 0, moveSpeed: 0, depositAmount: 0
        )
        return UniformInput(
            fieldWidth: fieldWidth, fieldHeight: fieldHeight, agentCount: agentCount, frame: 0,
            time: 0, decayRate: 0.95, fadeStrength: 0, trailSaturation: 100,
            diffusionFreq: 1, chaosInterval: 0, chaosStrength: 0, rhythmAmplitude: 0, exposure: 1,
            spawnPattern: .random, headIntensity: 0, backgroundColor: .zero,
            species: (z, z, z), matrix: (.zero, .zero, .zero)
        )
    }
}

public func packParams(_ i: UniformInput) -> [UInt8] {
    var f = [Float](repeating: 0, count: 76)
    func su(_ s: Int, _ v: UInt32) { f[s] = Float(bitPattern: v) }
    su(0, i.fieldWidth); su(1, i.fieldHeight); su(2, i.agentCount); su(3, i.frame)
    f[4] = i.time; f[5] = i.decayRate; f[6] = i.fadeStrength; f[7] = i.trailSaturation
    su(8, max(1, i.diffusionFreq)); su(11, i.chaosInterval)
    f[12] = i.chaosStrength; f[13] = i.rhythmAmplitude; f[14] = i.exposure
    su(15, UInt32(SpawnPattern.allCases.firstIndex(of: i.spawnPattern)!))
    f[16] = i.audio.moveSpeedFactor
    f[17] = i.audio.turnSpeedFactor
    f[18] = i.audio.depositFactor
    f[19] = i.audio.sensorDistanceFactor
    f[20] = i.audio.rhythmAmplitudeFactor
    f[21] = i.audio.chaosStrengthFactor
    f[22] = i.headIntensity
    f[24] = i.backgroundColor.x; f[25] = i.backgroundColor.y; f[26] = i.backgroundColor.z
    let sp = [i.species.0, i.species.1, i.species.2]
    for s in 0..<3 {
        let o = 28 + s * 12
        f[o + 0] = sp[s].color.x; f[o + 1] = sp[s].color.y; f[o + 2] = sp[s].color.z
        f[o + 3] = sp[s].populationShare
        f[o + 4] = sp[s].sensorAngle
        f[o + 5] = sp[s].sensorDistance
        f[o + 6] = sp[s].turnSpeed
        f[o + 7] = sp[s].moveSpeed
        f[o + 8] = sp[s].depositAmount
    }
    let rows = [i.matrix.0, i.matrix.1, i.matrix.2]
    for r in 0..<3 {
        let o = 64 + r * 4
        f[o + 0] = rows[r].x; f[o + 1] = rows[r].y; f[o + 2] = rows[r].z
    }
    return f.withUnsafeBytes { Array($0) }
}

public struct PostUniformInput {
    public var resolutionX: Float
    public var resolutionY: Float
    public var exposure: Float
    public var time: Float
    public var feedbackAmount: Float
    public var bloomThreshold: Float
    public var bloomIntensity: Float
    public var vignette: Float
    public var saturation: Float
    public var contrast: Float
    public var backgroundColor: RGB
    public var blendMode: BlendMode
    public var kaleidoscope: UInt32
    public var exposureFactor: Float = 1
    public var bloomIntensityFactor: Float = 1
    public var saturationFactor: Float = 1
    public var contrastFactor: Float = 1

    public static func zeroed(resX: Float, resY: Float) -> PostUniformInput {
        PostUniformInput(
            resolutionX: resX, resolutionY: resY, exposure: 1, time: 0,
            feedbackAmount: 0, bloomThreshold: 1, bloomIntensity: 1, vignette: 0,
            saturation: 1, contrast: 1, backgroundColor: .zero, blendMode: .additive, kaleidoscope: 0
        )
    }
}

public func packPostParams(_ i: PostUniformInput) -> [UInt8] {
    var f = [Float](repeating: 0, count: 20)
    func su(_ s: Int, _ v: UInt32) { f[s] = Float(bitPattern: v) }
    f[0] = i.resolutionX; f[1] = i.resolutionY; f[2] = i.exposure; f[3] = i.time
    f[4] = i.feedbackAmount; f[5] = i.bloomThreshold; f[6] = i.bloomIntensity
    f[7] = i.vignette; f[8] = i.saturation; f[9] = i.contrast
    su(10, min(6, i.kaleidoscope))
    f[12] = i.backgroundColor.x; f[13] = i.backgroundColor.y; f[14] = i.backgroundColor.z
    su(15, UInt32(BlendMode.allCases.firstIndex(of: i.blendMode)!))
    f[16] = i.exposureFactor; f[17] = i.bloomIntensityFactor
    f[18] = i.saturationFactor; f[19] = i.contrastFactor
    return f.withUnsafeBytes { Array($0) }
}