import Metal
import Foundation
// ── 1. Load naga-generated MSL and patch the placeholder bindings ──────────
// naga's CLI emits `[[user(fake0)]]` for every resource because it doesn't
// know the Metal slot map. The kernel args are, in order:
// params(0) pheromone(1) agentsIn(2) agentsOut(3) _buffer_sizes(4)
// (matching the WGSL @binding numbers; the sizes buffer goes after.)
let mslPath = "/tmp/physarum-spike/agentStep.metal"
var src = try! String(contentsOfFile: mslPath, encoding: .utf8)
for i in 0..<5 {
guard let r = src.range(of: "[[user(fake0)]]") else {
fatalError("expected 5 placeholder bindings, ran out at \(i)")
}
src.replaceSubrange(r, with: "[[buffer(\(i))]]")
}
guard let device = MTLCreateSystemDefaultDevice() else { fatalError("no Metal device") }
print("device: \(device.name)")
let lib: MTLLibrary
do {
lib = try device.makeLibrary(source: src, options: nil)
} catch {
fatalError("❌ runtime MSL compile FAILED: \(error)")
}
guard let fn = lib.makeFunction(name: "main_") else { fatalError("no main_ function") }
let pso = try! device.makeComputePipelineState(function: fn)
print("✅ runtime-compiled transpiled MSL; pipeline maxThreads=\(pso.maxTotalThreadsPerThreadgroup)")
// ── 2. Build the params uniform (304 bytes / 76 slots), layout per params.ts ─
let W: UInt32 = 256, H: UInt32 = 256
let N = 1024
var p = [Float](repeating: 0, count: 76)
func setU(_ i: Int, _ v: UInt32) { p[i] = Float(bitPattern: v) }
setU(0, W); setU(1, H); setU(2, UInt32(N)); setU(3, 0) // field, agentCount, frame
p[5] = 0.95 // decayRate
setU(8, 1) // diffusionFreq
setU(11, 0) // chaosInterval (off)
p[14] = 1.0 // exposure
setU(15, 0) // spawnPattern
for k in 16...21 { p[k] = 1.0 } // audio *Factor = 1 (no mod)
// species[0] at float 28, stride 12
let o = 28
p[o+0] = 0.9; p[o+1] = 0.6; p[o+2] = 0.2 // color
p[o+3] = 1.0 // populationShare
p[o+4] = 0.5 // sensorAngle
p[o+5] = 9.0 // sensorDistance
p[o+6] = 0.3 // turnSpeed
p[o+7] = 1.5 // moveSpeed
p[o+8] = 1.0 // depositAmount
// interaction matrix row 0 at float 64
p[64] = 1.0
let paramsBuf = device.makeBuffer(bytes: &p, length: 304, options: .storageModeShared)!
// ── 3. Pheromone field (zeroed), agents (initialised), out buffer, sizes ────
let phBytes = Int(W * H) * 16
let phBuf = device.makeBuffer(length: phBytes, options: .storageModeShared)!
memset(phBuf.contents(), 0, phBytes)
let stride = 24 // Agent: pos(8) angle(4) species(4) rhythmPhase(4) pad(4)
let agentsInBuf = device.makeBuffer(length: N * stride, options: .storageModeShared)!
let agentsOutBuf = device.makeBuffer(length: N * stride, options: .storageModeShared)!
do {
let base = agentsInBuf.contents()
for k in 0..<N {
let q = base + k * stride
q.storeBytes(of: Float(Int(W) / 2 + (k % 17)), toByteOffset: 0, as: Float.self)
q.storeBytes(of: Float(Int(H) / 2 + (k % 13)), toByteOffset: 4, as: Float.self)
q.storeBytes(of: Float(0), toByteOffset: 8, as: Float.self) // angle
q.storeBytes(of: UInt32(0), toByteOffset: 12, as: UInt32.self) // species
q.storeBytes(of: Float(0), toByteOffset: 16, as: Float.self) // rhythmPhase
}
}
var sizes = [UInt32](repeating: 0, count: 4)
sizes[0] = UInt32(phBytes); sizes[1] = UInt32(N * stride); sizes[2] = UInt32(N * stride)
let sizesBuf = device.makeBuffer(bytes: &sizes, length: 16, options: .storageModeShared)!
// ── 4. Dispatch one sim step ────────────────────────────────────────────────
let queue = device.makeCommandQueue()!
let cb = queue.makeCommandBuffer()!
let enc = cb.makeComputeCommandEncoder()!
enc.setComputePipelineState(pso)
enc.setBuffer(paramsBuf, offset: 0, index: 0)
enc.setBuffer(phBuf, offset: 0, index: 1)
enc.setBuffer(agentsInBuf, offset: 0, index: 2)
enc.setBuffer(agentsOutBuf, offset: 0, index: 3)
enc.setBuffer(sizesBuf, offset: 0, index: 4)
enc.dispatchThreadgroups(MTLSize(width: (N + 63) / 64, height: 1, depth: 1),
threadsPerThreadgroup: MTLSize(width: 64, height: 1, depth: 1))
enc.endEncoding()
cb.commit()
cb.waitUntilCompleted()
if let e = cb.error { fatalError("❌ GPU execution error: \(e)") }
// ── 5. Verify agents stepped ────────────────────────────────────────────────
let outP = agentsOutBuf.contents(), inP = agentsInBuf.contents()
var moved = 0
var sample = ""
for k in 0..<N {
let ix = inP.load(fromByteOffset: k * stride, as: Float.self)
let iy = inP.load(fromByteOffset: k * stride + 4, as: Float.self)
let ox = outP.load(fromByteOffset: k * stride, as: Float.self)
let oy = outP.load(fromByteOffset: k * stride + 4, as: Float.self)
if abs(ox - ix) > 1e-6 || abs(oy - iy) > 1e-6 { moved += 1 }
if k < 2 { sample += " agent[\(k)]: in(\(ix),\(iy)) -> out(\(ox),\(oy))\n" }
}
print("agents moved: \(moved)/\(N)")
print(sample, terminator: "")
print(moved == N
? "✅ SPIKE PASS — naga-transpiled WGSL compute shader runtime-compiled AND ran on Metal; all agents stepped."
: "⚠️ ran but only \(moved)/\(N) moved — investigate params layout.")