Zum Inhalt

apple/PhysarumEngine/Tests/PhysarumEngineTests/PostFXGatingTests.swift

import XCTest
import Metal
@testable import PhysarumEngine

/// Pass gating: bloom/feedback passes that contribute exactly zero to the final
/// image must be skippable without changing output — and re-enabling feedback
/// must not blend in a stale echo from before it was disabled.
final class PostFXGatingTests: XCTestCase {
    private func makeDevice() throws -> MTLDevice {
        guard let device = MTLCreateSystemDefaultDevice() else {
            throw XCTSkip("no Metal device available")
        }
        return device
    }

    private func pixels(_ renderer: Renderer, _ engine: SimEngine, _ p: PresetParams,
                        _ target: MTLTexture) throws -> [UInt8] {
        try renderer.renderOffscreen(sim: engine, preset: p, into: target, cover: true)
        var raw = [UInt8](repeating: 0, count: target.width * target.height * 4)
        raw.withUnsafeMutableBytes { _ = $0 }   // keep alloc out of getBytes timing
        target.getBytes(&raw, bytesPerRow: target.width * 4,
                        from: MTLRegionMake2D(0, 0, target.width, target.height), mipmapLevel: 0)
        return raw
    }

    private func brightness(_ px: [UInt8]) -> Double {
        var sum = 0.0
        for (i, v) in px.enumerated() where i % 4 != 3 { sum += Double(v) }
        return sum / Double(px.count / 4 * 3)
    }

    private func makeTarget(_ device: MTLDevice, size: Int) throws -> MTLTexture {
        let d = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: size, height: size, mipmapped: false)
        d.usage = [.renderTarget, .shaderRead]
        d.storageMode = .shared
        guard let t = device.makeTexture(descriptor: d) else { throw ShaderError.compile("target") }
        return t
    }

    /// With feedback and bloom both zero, repeated renders of the SAME sim state
    /// are pixel-identical — nothing may accumulate or leak across frames.
    func testZeroedPostFXRendersAreIdempotent() throws {
        let device = try makeDevice()
        let lib = ShaderLibrary(device: device)
        var p = try TestFixtures.minimalPreset().params
        p.agentCount = 5000
        p.spawnPattern = .center
        p.species[0].depositAmount = 5
        p.feedbackAmount = 0
        p.bloomIntensity = 0

        let engine = try SimEngine(device: device, library: lib, width: 64, height: 64, preset: p)
        for _ in 0..<30 { engine.step() }
        let renderer = try Renderer(device: device, library: lib, width: 64, height: 64)
        let target = try makeTarget(device, size: 64)

        let a = try pixels(renderer, engine, p, target)
        let b = try pixels(renderer, engine, p, target)
        XCTAssertGreaterThan(brightness(a), 0.5, "scene must be visible")
        XCTAssertEqual(a, b, "feedback=0/bloom=0 renders of the same sim state must be identical")
    }

    /// Disable feedback, render, re-enable: the first re-enabled frame must look
    /// like the disabled frame (echo starts blank), NOT brightened by a stale
    /// echo accumulated before the disable.
    func testReenabledFeedbackStartsFromBlankEcho() throws {
        let device = try makeDevice()
        let lib = ShaderLibrary(device: device)
        var p = try TestFixtures.minimalPreset().params
        p.agentCount = 5000
        p.spawnPattern = .center
        p.species[0].depositAmount = 5

        let engine = try SimEngine(device: device, library: lib, width: 64, height: 64, preset: p)
        for _ in 0..<30 { engine.step() }
        let renderer = try Renderer(device: device, library: lib, width: 64, height: 64)
        let target = try makeTarget(device, size: 64)

        // Build up a bright echo, then disable feedback.
        var pOn = p; pOn.feedbackAmount = 0.85
        for _ in 0..<10 { _ = try pixels(renderer, engine, pOn, target) }
        var pOff = p; pOff.feedbackAmount = 0
        let off = try pixels(renderer, engine, pOff, target)

        // First frame after re-enable: echo contribution must be ~zero.
        let on1 = try pixels(renderer, engine, pOn, target)
        let bOff = brightness(off), bOn1 = brightness(on1)
        XCTAssertLessThan(bOn1, bOff * 1.15,
                          "first re-enabled frame must not blend a stale echo (off \(bOff) vs re-enabled \(bOn1))")
    }
}