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)