‹ 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

CalibrationChecks.swift (5410B)


      1 import Foundation
      2 import simd
      3 func check(_ ok: Bool, _ what: String) { print(ok ? "PASS" : "FAIL", what); if !ok { exit(1) } }
      4 func near(_ a: Double, _ b: Double, _ eps: Double = 1e-9) -> Bool { abs(a - b) <= eps }
      5 
      6 /// 3x3 identity mixing matrix, 4 temperature coefficients per entry (constant term first).
      7 func identityMixModel() -> [Double] {
      8     var v: [Double] = []
      9     for i in 0 ..< 9 { v += [i % 4 == 0 ? 1 : 0, 0, 0, 0] }
     10     return v
     11 }
     12 
     13 /// Synthetic G1-like panel: 4320x2160, eye centers in each half, focal ~1000 px, mild positive k1.
     14 func fixtureJSON(k1: Double, chroma: Double = 0) -> Data {
     15     func display(_ eye: String, _ cx: Double, _ tx: Double) -> [String: Any] {
     16         let f = 1000.0
     17         let affine = [f, 0, cx, 0, f, 1080, 0, 0, 1] // tangent-plane -> pixels; inverse maps pixels -> tangents
     18         func dist(_ k: Double) -> [String: Any] {
     19             ["ModelType": "CALIBRATION_DisplayDistortionModelPolynomial3K", "ModelParameterCount": 5,
     20              "ModelParameters": [cx, 1080, k, 0, 0]]
     21         }
     22         return ["AssignedEye": eye, "Affine": affine, "DisplayWidth": 4320, "DisplayHeight": 2160,
     23                 "VisibleAreaCenter": ["X": cx, "Y": 1080], "VisibleAreaRadius": 1050,
     24                 "Rt": ["Rotation": [1, 0, 0, 0, 1, 0, 0, 0, 1], "Translation": [tx, 0, 0]],
     25                 "DistortionRed": dist(k1 * (1 + chroma)), "DistortionGreen": dist(k1), "DistortionBlue": dist(k1 * (1 - chroma))]
     26     }
     27     let gyro: [String: Any] = ["SensorType": "CALIBRATION_InertialSensorType_Gyro",
     28                                "Rt": ["Rotation": [0, 1, 0, -1, 0, 0, 0, 0, 1], "Translation": [0, 0, 0]],
     29                                "BiasTemperatureModel": [0.01, 0, 0, 0, 0.02, 0, 0, 0, 0.03, 0, 0, 0],
     30                                "MixingMatrixTemperatureModel": identityMixModel()]
     31     let root: [String: Any] = ["CalibrationInformation": [
     32         "Displays": [display("CALIBRATION_DisplayEyeLeft", 1080, -0.0315), display("CALIBRATION_DisplayEyeRight", 3240, 0.0315)],
     33         "InertialSensors": [gyro], "Cameras": [] as [Any]]]
     34     return try! JSONSerialization.data(withJSONObject: root)
     35 }
     36 
     37 @main
     38 struct CalibrationChecks {
     39 static func main() {
     40 
     41 do {
     42     check(wmrConfigKey.count == 1024 && wmrConfigKey.prefix(4) == [0x2F, 0xC8, 0x0F, 0x38], "key: 1024 bytes, upstream prefix")
     43     check(WMRConfigProtocol.request(0x0b) == [0x02, 0x0b] + [UInt8](repeating: 0, count: 62), "request layout")
     44     var chunk = [UInt8](repeating: 0, count: 33); chunk[0] = 2; chunk[1] = 1; chunk[2] = 3; chunk[3] = 7; chunk[4] = 8; chunk[5] = 9
     45     check(WMRConfigProtocol.parseChunk(chunk) == .data([7, 8, 9]), "chunk reply parse")
     46     chunk[1] = 2; check(WMRConfigProtocol.parseChunk(chunk) == .end, "end reply parse")
     47     check(WMRConfigProtocol.dataSize(fromMetadata: [0x34, 0x12]) == 0x1234, "metadata size LE")
     48 
     49     // 1. Block round trip: encode synthetic JSON like the headset store, decode it back.
     50     let json = fixtureJSON(k1: 0)
     51     let block = try WMRConfigBlock(raw: WMRConfigBlock.encodeForTesting(json: json))
     52     check(block.json == json && block.name == "Test Headset", "config block XOR/header round trip")
     53 
     54     // 2. Parse and identity model: k = 0 and a pure scale affine => sample coords equal output coords.
     55     let cal = try HeadsetCalibration(json: block.json)
     56     check(cal.eyes.count == 2 && cal.sensors.count == 1 && cal.sensors[0].kind == .gyro, "parse displays + gyro")
     57     check(near(EyeCalibration.ipd(cal.eyes[0], cal.eyes[1]), 0.063), "IPD from Rt translations")
     58     check(near(cal.sensors[0].biasOffsets.y, 0.02), "gyro bias constant terms")
     59     for view in 0 ..< 2 {
     60         let eye = cal.eyes[view], b = eye.distortionBounds(view: view)
     61         check(near(b.texXRange.x, -1.08) && near(b.texXRange.y, 1.08) && near(b.texYRange.x, -1.08) && near(b.texYRange.y, 1.08),
     62               "view \(view): identity bounds = +-1080/1000")
     63         for (u, v) in [(0.0, 0.0), (0.5, 0.5), (0.25, 0.8), (1.0, 1.0)] {
     64             let s = eye.distort(view: view, u: u, v: v, bounds: b)
     65             check(near(s.g.x, u, 1e-9) && near(s.g.y, v, 1e-9) && s.r == s.g && s.b == s.g, "view \(view) identity (\(u), \(v))")
     66         }
     67     }
     68 
     69     // 3. Barrel/pincushion behaviour: k1 > 0 pushes edge samples outward, centre unchanged, symmetric.
     70     let k1 = 1e-7
     71     let cal2 = try HeadsetCalibration(json: fixtureJSON(k1: k1, chroma: 0.05))
     72     let eye = cal2.eyes[0], b = eye.distortionBounds(view: 0)
     73     let centre = eye.distort(view: 0, u: 0.5, v: 0.5, bounds: b)
     74     check(near(centre.g.x, 0.5) && near(centre.g.y, 0.5), "k1>0: centre maps to centre")
     75     let edge = eye.distort(view: 0, u: 0.9, v: 0.5, bounds: b), mirror = eye.distort(view: 0, u: 0.1, v: 0.5, bounds: b)
     76     check(near(edge.g.x + mirror.g.x, 1, 1e-9), "k1>0: left/right symmetric")
     77     // Hand-computed expectation for u = 0.9: pix.x = 864, d = 1 + k1 * 864^2, tangent = 864 * d / 1000.
     78     let d = 1 + k1 * 864 * 864, tan = 864 * d / 1000
     79     check(near(edge.g.x, (tan - b.texXRange.x) / (b.texXRange.y - b.texXRange.x), 1e-12), "k1>0: matches hand calculation")
     80     check(edge.r.x > edge.g.x && edge.b.x < edge.g.x, "chromatic: red samples farther out than green, blue nearer")
     81     check(b.angleLeft < 0 && b.angleRight > 0 && b.angleUp > 0 && b.angleDown < 0, "bounds -> asymmetric FOV angles")
     82     print("all calibration self-tests passed")
     83 } catch {
     84     print("FAIL", error); exit(1)
     85 }
     86 
     87 }
     88 }