Zum Inhalt

apple/PhysarumApp/Sources/Capture/ThumbnailRenderer.swift

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)
    }
}