import Foundation
import Metal
import PhysarumEngine
enum ThumbnailSource {
case liveSnapshot(LiveSnapshot)
case warmStart(PresetParams)
}
/// Renders a 256² PNG for a preset, reusing the engine's offscreen path. Synchronous +
/// blocking — call off-main. liveSnapshot restores the captured state (one frame, no step);
/// warmStart simulates ~10 s fresh, then snaps a frame.
enum ThumbnailRenderer {
static let side = 256
static let warmupFrames = 1800 // ~60 s @ 30 fps — 10 s left patterns too immature
static func render(_ source: ThumbnailSource) throws -> Data {
guard let device = MTLCreateSystemDefaultDevice() else { throw CaptureError.noDevice }
let lib = ShaderLibrary(device: device)
let renderer: Renderer
let sim: SimEngine
var preset: PresetParams
switch source {
case .liveSnapshot(let snap):
let f = snap.field
preset = snap.preset
preset.agentCount = snap.sim.agentCount
renderer = try Renderer(device: device, library: lib, width: f.width, height: f.height)
sim = try SimEngine(device: device, library: lib, width: f.width, height: f.height, preset: preset)
try sim.restore(snap.sim)
if let fb = snap.feedback { renderer.restoreFeedback(fb) } else { renderer.resetFeedback() }
case .warmStart(let p):
preset = p
preset.agentCount = densityScaledAgentCount(p.agentCount, fieldWidth: side, fieldHeight: side)
renderer = try Renderer(device: device, library: lib, width: side, height: side)
sim = try SimEngine(device: device, library: lib, width: side, height: side, preset: preset)
renderer.resetFeedback()
for _ in 0..<warmupFrames {
sim.step()
try renderer.renderOffscreen(sim: sim, preset: preset)
}
}
let d = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: .bgra8Unorm, width: side, height: side, mipmapped: false)
d.usage = [.renderTarget, .shaderRead]
d.storageMode = .shared
guard let target = device.makeTexture(descriptor: d) else { throw CaptureError.noTexture }
try renderer.renderOffscreen(sim: sim, preset: preset, into: target, cover: true)
guard let img = renderer.cgImage(target) else { throw CaptureError.encode }
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("thumb-\(UUID().uuidString).png")
guard ImageWriter.writePNG(img, to: url) else { throw CaptureError.encode }
defer { try? FileManager.default.removeItem(at: url) }
return try Data(contentsOf: url)
}
}