‹ projects

reverb-m1

Native wired VR on Apple silicon: Reverb G1 display bring-up and stereo demo
git clone https://umhau.org/code/reverb-m1.git
Log | Files | Refs

LensWarpChecks.swift (7113B)


      1 // Compare the production Metal warp against the independent Double-precision CPU model.
      2 // Takes a saved factory JSON path; opens no hardware devices.
      3 import Foundation
      4 import Metal
      5 import simd
      6 func option(_ name: String) -> String? { nil }
      7 
      8 @main
      9 struct LensWarpChecks {
     10     static func main() throws {
     11         let optics = try HeadsetOptics(json: Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[1])))
     12         let device = MTLCreateSystemDefaultDevice()!, queue = device.makeCommandQueue()!
     13         let warp = try LensWarp(device: device, optics: optics)
     14         var coordinateWorst = 0.0
     15         var points: [SIMD2<Float>] = []
     16         for y in stride(from: 0, through: 2160, by: 90) {
     17             for x in stride(from: 0, through: 2160, by: 90) {
     18                 points.append(SIMD2(Float(x) / 2160, Float(y) / 2160))
     19             }
     20         }
     21         for eye in 0..<2 {
     22             let actual = try warp.coordinateSamples(device: device, points: points, eye: eye)
     23             for (i, p) in points.enumerated() {
     24                 let cpu = optics.calibration.eyes[eye].distort(view: eye, u: Double(p.x), v: Double(p.y), bounds: optics.bounds[eye])
     25                 for (c, expected) in [cpu.r, cpu.g, cpu.b].enumerated() {
     26                     let error = simd_abs(SIMD2<Double>(actual[i * 3 + c]) - expected)
     27                     coordinateWorst = max(coordinateWorst, error.x, error.y)
     28                 }
     29             }
     30         }
     31         for gain: Float in [-1, 0, 0.5, 2, 4] {
     32             for eye in 0..<2 {
     33                 let actual = try warp.coordinateSamples(device: device, points: points, eye: eye, redGain: gain, blueHorizontalGain: gain, blueVerticalGain: gain)
     34                 for (i, p) in points.enumerated() {
     35                     let cpu = optics.calibration.eyes[eye].distort(view: eye, u: Double(p.x), v: Double(p.y), bounds: optics.bounds[eye])
     36                     let expected = [cpu.g + (cpu.r - cpu.g) * Double(gain), cpu.g, cpu.g + (cpu.b - cpu.g) * Double(gain)]
     37                     for c in 0..<3 {
     38                         let error = simd_abs(SIMD2<Double>(actual[i * 3 + c]) - expected[c])
     39                         coordinateWorst = max(coordinateWorst, error.x, error.y)
     40                     }
     41                 }
     42             }
     43         }
     44         for eye in 0..<2 {
     45             let actual = try warp.coordinateSamples(device: device, points: points, eye: eye, redGain: 1.75, blueHorizontalGain: 2.25, blueVerticalGain: 1.5)
     46             for (i, p) in points.enumerated() {
     47                 let cpu = optics.calibration.eyes[eye].distort(view: eye, u: Double(p.x), v: Double(p.y), bounds: optics.bounds[eye])
     48                 let expected = [cpu.g + (cpu.r - cpu.g) * Double(Float(1.75)), cpu.g,
     49                                 cpu.g + (cpu.b - cpu.g) * SIMD2(Double(Float(2.25)), Double(Float(1.5)))]
     50                 for c in 0..<3 {
     51                     let error = simd_abs(SIMD2<Double>(actual[i * 3 + c]) - expected[c])
     52                     coordinateWorst = max(coordinateWorst, error.x, error.y)
     53                 }
     54             }
     55         }
     56         precondition(coordinateWorst < 1e-5, "Float shader coordinates differ from Double model: \(coordinateWorst)")
     57         print("PASS: \(points.count * 2 * 3 * 7) RGB float-coordinate pairs (six strengths and independent blue axes), max GPU/CPU error \(coordinateWorst)")
     58         let w = warp.color.width, h = warp.color.height, eyeW = w / 2
     59         let bytes = UnsafeMutablePointer<UInt8>.allocate(capacity: w * h * 4)
     60         defer { bytes.deallocate() }
     61         for y in 0..<h {
     62             for x in 0..<w {
     63                 let p = (y * w + x) * 4
     64                 bytes[p] = 128
     65                 bytes[p + 1] = UInt8(Double(y) / Double(h - 1) * 255)
     66                 bytes[p + 2] = UInt8(Double(x % eyeW) / Double(eyeW - 1) * 255)
     67                 bytes[p + 3] = 255
     68             }
     69         }
     70         func texture(_ width: Int, _ height: Int) -> MTLTexture {
     71             let d = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: width, height: height, mipmapped: false)
     72             d.storageMode = .shared; d.usage = .renderTarget
     73             return device.makeTexture(descriptor: d)!
     74         }
     75         let source = texture(w, h), target = texture(4320, 2160)
     76         source.replace(region: MTLRegionMake2D(0, 0, w, h), mipmapLevel: 0, withBytes: bytes, bytesPerRow: w * 4)
     77         let command = queue.makeCommandBuffer()!, blit = command.makeBlitCommandEncoder()!
     78         blit.copy(from: source, sourceSlice: 0, sourceLevel: 0, sourceOrigin: .init(x: 0, y: 0, z: 0), sourceSize: .init(width: w, height: h, depth: 1), to: warp.color, destinationSlice: 0, destinationLevel: 0, destinationOrigin: .init(x: 0, y: 0, z: 0))
     79         blit.endEncoding()
     80         warp.encode(command, target: target)
     81         command.commit(); command.waitUntilCompleted()
     82         precondition(command.status == .completed, "Metal command failed")
     83         let outW = target.width, outH = target.height
     84         let output = UnsafeMutablePointer<UInt8>.allocate(capacity: outW * outH * 4)
     85         defer { output.deallocate() }
     86         target.getBytes(output, bytesPerRow: outW * 4, from: MTLRegionMake2D(0, 0, outW, outH), mipmapLevel: 0)
     87         var worst = 0.0, count = 0
     88         for eye in 0..<2 {
     89             let model = optics.calibration.eyes[eye], bounds = optics.bounds[eye]
     90             for y in stride(from: 53, to: 2160, by: 137) {
     91                 for x in stride(from: 29, to: 2160, by: 131) {
     92                     let tc = model.distort(view: eye, u: (Double(x) + 0.5) / 2160, v: (Double(y) + 0.5) / 2160, bounds: bounds)
     93                     func expected(_ uv: SIMD2<Double>, _ value: Double) -> Double {
     94                         uv.x < 0 || uv.x > 1 || uv.y < 0 || uv.y > 1 ? 0 : value
     95                     }
     96                     let p = (y * outW + eye * 2160 + x) * 4
     97                     worst = max(worst, abs(Double(output[p + 2]) / 255 - expected(tc.r, tc.r.x)),
     98                                 abs(Double(output[p + 1]) / 255 - expected(tc.g, tc.g.y)),
     99                                 abs(Double(output[p]) / 255 - expected(tc.b, 128.0 / 255)))
    100                     count += 3
    101                 }
    102             }
    103             let projection = perspective(bounds: bounds, near: 0.05, far: 100)
    104             for (tangent, expectedX) in [(Float(bounds.texXRange.x), Float(-1)), (Float(bounds.texXRange.y), Float(1))] {
    105                 let clip = projection * SIMD4(tangent * 0.05, 0, -0.05, 1)
    106                 precondition(abs(clip.x / clip.w - expectedX) < 1e-5, "projection X edge incorrect")
    107             }
    108             for (tangent, expectedY) in [(-Float(bounds.texYRange.x), Float(1)), (-Float(bounds.texYRange.y), Float(-1))] {
    109                 let clip = projection * SIMD4(0, tangent * 0.05, -0.05, 1)
    110                 precondition(abs(clip.y / clip.w - expectedY) < 1e-5, "projection Y edge incorrect")
    111             }
    112         }
    113         precondition(worst < 0.012, "GPU warp differs from CPU model: \(worst)")
    114         print("PASS: \(count) RGB channel samples, max GPU/CPU error \(worst); asymmetric projection edges correct")
    115     }
    116 }