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