Zum Inhalt

apple/PhysarumEngine/Tests/PhysarumEngineTests/BloomSceneAlignmentTests.swift

import XCTest
import Metal
@testable import PhysarumEngine

/// Regression guard for the Metal bloom-vs-scene VERTICAL FLIP (iPad-reported):
/// the bloom chain runs 3 extra fullscreen passes than the scene path, and the
/// shared fullscreen-triangle VS inverts V per Metal render→sample hop, so an odd
/// extra hop count lands the bloom vertically mirrored relative to the scene.
/// `composite.metal` samples bloom at flipped V to compensate (Metal-only — NOT in
/// the shared WGSL). This test renders the same evolved sim scene-only vs with strong
/// bloom and asserts the isolated bloom glow correlates with the scene, NOT the
/// vertically-flipped scene. Robust to the random spawn (it compares orientations,
/// not absolute positions).
final class BloomSceneAlignmentTests: XCTestCase {
    private func lumField(_ tex: MTLTexture) -> ([Double], Int, Int) {
        let w = tex.width, h = tex.height, bpr = w * 4
        var raw = [UInt8](repeating: 0, count: bpr * h)
        tex.getBytes(&raw, bytesPerRow: bpr, from: MTLRegionMake2D(0, 0, w, h), mipmapLevel: 0)
        var lum = [Double](repeating: 0, count: w * h)
        for p in 0..<(w * h) {
            let i = p * 4   // bgra
            lum[p] = 0.114 * Double(raw[i]) + 0.587 * Double(raw[i + 1]) + 0.299 * Double(raw[i + 2])
        }
        return (lum, w, h)
    }

    func testBloomGlowAlignsWithSceneNotItsVerticalMirror() throws {
        guard let device = MTLCreateSystemDefaultDevice() else { throw XCTSkip("no GPU") }
        let lib = ShaderLibrary(device: device)
        var preset = try TestFixtures.minimalPreset().params
        preset.feedbackAmount = 0
        let field = 192
        let r = try Renderer(device: device, library: lib, width: field, height: field)
        let s = try SimEngine(device: device, library: lib, width: field, height: field, preset: preset)
        let d = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: 384, height: 384, mipmapped: false)
        d.usage = [.renderTarget, .shaderRead]; d.storageMode = .shared
        let target = try XCTUnwrap(device.makeTexture(descriptor: d))

        r.resetFeedback()
        for _ in 0..<160 { s.step() }

        preset.bloomIntensity = 0
        try r.renderOffscreen(sim: s, preset: preset, into: target, cover: true)
        let (scene, w, h) = lumField(target)

        preset.bloomIntensity = 3
        preset.bloomThreshold = 0.0
        try r.renderOffscreen(sim: s, preset: preset, into: target, cover: true)
        let (bloomComposite, _, _) = lumField(target)

        // Isolate the added bloom glow and correlate it with the scene vs the
        // vertically-flipped scene.
        var corrScene = 0.0, corrFlipped = 0.0
        for y in 0..<h {
            for x in 0..<w {
                let p = y * w + x
                let glow = max(0, bloomComposite[p] - scene[p])
                corrScene += glow * scene[p]
                corrFlipped += glow * scene[(h - 1 - y) * w + x]
            }
        }
        print("BLOOMALIGN corrScene=\(corrScene) corrFlipped=\(corrFlipped) ratio=\(corrScene / max(1, corrFlipped))")
        XCTAssertGreaterThan(corrScene, corrFlipped,
            "bloom glow must align with the scene, not its vertical mirror (composite.metal bloom-V flip regressed?)")
    }
}