Zum Inhalt

apple/PhysarumEngine/Sources/EngineCheck/main.swift

import Foundation
import Metal
import simd
import PhysarumEngine

// Headless verification harness (replaces XCTest under the CLT-only toolchain).
// Exits non-zero if any check fails, so it works as a CI/`swift run` gate.

final class Checker {
    var failures = 0
    func check(_ cond: Bool, _ msg: String) {
        if cond { print("  ok: \(msg)") } else { print("  FAIL: \(msg)"); failures += 1 }
    }
    func eq<T: Equatable>(_ a: T, _ b: T, _ msg: String) {
        check(a == b, "\(msg) (got \(a), want \(b))")
    }
}

extension Array where Element == UInt8 {
    func u32(at slot: Int) -> UInt32 {
        let i = slot * 4
        return UInt32(self[i]) | (UInt32(self[i + 1]) << 8)
            | (UInt32(self[i + 2]) << 16) | (UInt32(self[i + 3]) << 24)
    }
    func f32(at slot: Int) -> Float { Float(bitPattern: u32(at: slot)) }
}

let c = Checker()

print("== Params layout ==")
do {
    var u = UniformInput.zeroed(fieldWidth: 256, fieldHeight: 128, agentCount: 1000)
    u.species.0.moveSpeed = 1.5
    u.species.0.turnSpeed = 0.3
    u.species.0.color = RGB(0.9, 0.6, 0.2)
    u.spawnPattern = .ring
    u.matrix.0 = MatrixRow(1, -0.2, 0)
    let b = packParams(u)
    c.eq(b.count, 304, "params byte size")
    c.eq(b.u32(at: 0), 256, "fieldWidth@0")
    c.eq(b.u32(at: 1), 128, "fieldHeight@1")
    c.eq(b.u32(at: 2), 1000, "agentCount@2")
    c.eq(b.u32(at: 15), 3, "spawnPattern .ring@15")
    c.eq(b.f32(at: 16), 1.0, "audio moveSpeedFactor@16")
    c.eq(b.f32(at: 28 + 0), 0.9, "species[0].color.r@28")
    c.eq(b.f32(at: 28 + 6), 0.3, "species[0].turnSpeed@34")
    c.eq(b.f32(at: 28 + 7), 1.5, "species[0].moveSpeed@35")
    c.eq(b.f32(at: 64 + 0), 1.0, "matrix[0][0]@64")
    c.eq(b.f32(at: 64 + 1), -0.2, "matrix[0][1]@65")
}

print("== PostParams layout ==")
do {
    var i = PostUniformInput.zeroed(resX: 256, resY: 128)
    i.blendMode = .paint
    i.kaleidoscope = 4
    i.bloomThreshold = 0.7
    let b = packPostParams(i)
    c.eq(b.count, 80, "postParams byte size")
    c.eq(b.f32(at: 0), 256, "resolutionX@0")
    c.eq(b.f32(at: 5), 0.7, "bloomThreshold@5")
    c.eq(b.u32(at: 10), 4, "kaleidoscope@10")
    c.eq(b.u32(at: 15), 1, "blendMode .paint@15")
}

let testPresetJSON = """
{"id":"t","name":"Test","builtIn":true,"tags":[],"tagSignal":"neural-bleed","params":{
  "agentCount":1000,"spawnPattern":"random",
  "species":[{"color":[0.9,0.6,0.2],"populationShare":1,"sensorAngle":0.5,"sensorDistance":9,"turnSpeed":0.3,"moveSpeed":1.5,"depositAmount":1},
             {"color":[0,0,0],"populationShare":0,"sensorAngle":0,"sensorDistance":1,"turnSpeed":0,"moveSpeed":0,"depositAmount":0},
             {"color":[0,0,0],"populationShare":0,"sensorAngle":0,"sensorDistance":1,"turnSpeed":0,"moveSpeed":0,"depositAmount":0}],
  "interactionMatrix":[[1,0,0],[0,1,0],[0,0,1]],
  "decayRate":0.95,"diffusionFreq":1,"fadeStrength":0,"trailSaturation":100,
  "chaosInterval":0,"chaosStrength":0,"rhythmAmplitude":0,"simSpeed":1,
  "backgroundColor":[0,0,0],"blendMode":"additive","exposure":1,"headIntensity":0,"kaleidoscope":0,
  "feedbackAmount":0,"bloomThreshold":1,"bloomIntensity":1,"vignette":0,"saturation":1,"contrast":1,
  "audioReactive":{"enabled":false,"gain":1,"autoGain":false,"routes":[]}}}
"""

print("== Preset JSON round-trip ==")
var decodedPreset: Preset?
do {
    let preset = try JSONDecoder().decode(Preset.self, from: Data(testPresetJSON.utf8))
    decodedPreset = preset
    c.eq(preset.id, "t", "preset id")
    c.eq(preset.tagSignal, .neuralBleed, "tagSignal neural-bleed")
    c.eq(preset.params.species.count, 3, "species count")
    c.eq(preset.params.species[0].moveSpeed, 1.5, "species[0].moveSpeed")
    c.eq(preset.params.simSpeed, 1, "simSpeed")
} catch {
    c.check(false, "preset decode threw: \(error)")
}

print("== Native sim (agentStep+deposit+diffuse) ==")
if let device = MTLCreateSystemDefaultDevice(), let base = decodedPreset {
    print("  device: \(device.name)")
    var p = base.params                 // public vars are settable cross-module
    p.agentCount = 20000
    p.spawnPattern = .center
    p.fadeStrength = 0.1
    p.rhythmAmplitude = 0.1
    p.species[0].moveSpeed = 1.0
    p.species[0].depositAmount = 5
    // demo post-FX so the bloom chain is visibly exercised
    p.exposure = 1.2
    p.bloomThreshold = 0.15
    p.bloomIntensity = 2.5
    p.vignette = 0.35
    do {
        let lib = ShaderLibrary(device: device)
        let engine = try SimEngine(device: device, library: lib, width: 256, height: 256, preset: p)
        for _ in 0..<120 { engine.step() }
        let s = engine.pheromoneStats()
        print("  after 120 frames: sum=\(String(format: "%.1f", s.sum)) max=\(s.maxValue) frame=\(engine.frame)")
        c.check(!s.hasNaN, "pheromone field has no NaN")
        c.check(s.sum > 0, "agents deposited a trail (sum > 0)")
        c.check(s.maxValue > 0, "pheromone max > 0")

        print("== Render → PNG ==")
        let renderer = try Renderer(device: device, library: lib, width: 256, height: 256)
        let pngPath = "/tmp/physarum-native.png"
        let ok = try renderer.renderPNG(sim: engine, preset: p, to: pngPath)
        c.check(ok, "PNG written to \(pngPath)")
    } catch {
        c.check(false, "sim/render threw: \(error)")
    }
} else {
    print("  (no Metal device — skipping sim check)")
}

print(c.failures == 0 ? "\nALL CHECKS PASSED" : "\n\(c.failures) CHECK(S) FAILED")
exit(c.failures == 0 ? 0 : 1)