‹ 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

Calibration.swift (25691B)


      1 // HP Reverb G1 (Windows Mixed Reality) factory calibration: config-block decoding, JSON parsing, and the
      2 // Polynomial-3K lens distortion model. Pure Swift (Foundation + simd), no hardware access.
      3 //
      4 // Ported from Monado (https://gitlab.freedesktop.org/monado/monado), src/xrt/drivers/wmr/{wmr_hmd.c,
      5 // wmr_config.c, wmr_config.h, wmr_config_key.h, wmr_protocol.h} and src/xrt/auxiliary/util/u_distortion_mesh.c.
      6 // Upstream copyright notices (each file is SPDX-License-Identifier: BSL-1.0):
      7 //   wmr_hmd.c:            Copyright 2018, Philipp Zabel. Copyright 2020-2021, N Madsen. Copyright 2020-2023, Collabora, Ltd.
      8 //   wmr_protocol.h:       Copyright 2018, Philipp Zabel. Copyright 2020-2021, N Madsen. Copyright 2020-2021, Collabora, Ltd.
      9 //   wmr_config.c/.h, wmr_config_key.h: Copyright 2021, Jan Schmidt.
     10 //   u_distortion_mesh.c:  Copyright 2019-2021, Collabora, Ltd.
     11 // Full license text: LICENSES/BSL-1.0.txt
     12 // The XOR key (wmr_config_key.h) was derived by the Monado/OpenHMD authors from config blocks read with
     13 // https://github.com/pH5/wmr-config.
     14 //
     15 // Boost Software License - Version 1.0 - August 17th, 2003
     16 // Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and
     17 // accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute,
     18 // execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to
     19 // whom the Software is furnished to do so, all subject to the following: The copyright notices in the Software and
     20 // this entire statement, including the above license grant, this restriction and the following disclaimer, must be
     21 // included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such
     22 // copies or derivative works are solely in the form of machine-executable object code generated by a source language
     23 // processor. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
     24 // LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO
     25 // EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY,
     26 // WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
     27 // DEALINGS IN THE SOFTWARE.
     28 //
     29 // Self-test (synthetic fixtures only):
     30 //   swiftc -O Sources/VRWorkstation/Calibration.swift Tests/CalibrationChecks.swift -o /tmp/calibration-selftest && /tmp/calibration-selftest
     31 import Foundation
     32 import simd
     33 
     34 // MARK: - HID transport protocol (HoloLens Sensors 045e:0659), Monado wmr_read_config_part / wmr_read_config_raw
     35 
     36 /// Request/reply layout for reading the config store. The caller owns the HID device and its threading:
     37 /// - request: OUTPUT report, 64 bytes, `[0x02, command, 0, 0, ...]`  (report id 0x02)
     38 /// - reply:   INPUT report with id 0x02 (WMR_MS_HOLOLENS_MSG_CONTROL), 33 bytes. Other input reports (IMU id 0x01,
     39 ///            controller traffic) must be ignored while waiting. Monado waits up to 100 ms per read.
     40 /// Sequence to read one "part" of type T (T = 0x06 metadata, then T = 0x04 data):
     41 ///   1. send 0x0b; expect a 33-byte reply with byte0 == 0x02
     42 ///   2. send T;    expect a 33-byte reply with byte0 == 0x02
     43 ///   3. repeat: send 0x08; reply byte1 == 0x01 => chunk: length = byte2, payload = bytes[3 ..< 3+length];
     44 ///                          reply byte1 == 0x02 => end of part; anything else => error.
     45 /// Whole config: part(0x06) -> 84-byte metadata; dataSize = meta[0] | meta[1] << 8; then part(0x04) -> dataSize bytes.
     46 /// Read the config BEFORE enabling the IMU (`[0x02, 0x07]` uses the same report id), or filter replies carefully.
     47 enum WMRConfigProtocol {
     48     static let reportID: UInt8 = 0x02
     49     static let replyLength = 33
     50     static let selectCommand: UInt8 = 0x0b
     51     static let readChunkCommand: UInt8 = 0x08
     52     static let metadataPart: UInt8 = 0x06
     53     static let dataPart: UInt8 = 0x04
     54 
     55     /// 64-byte output report for `command`.
     56     static func request(_ command: UInt8) -> [UInt8] {
     57         var report = [UInt8](repeating: 0, count: 64)
     58         report[0] = reportID; report[1] = command
     59         return report
     60     }
     61 
     62     enum ChunkReply: Equatable { case data([UInt8]), end, invalid }
     63 
     64     /// Interprets the reply to a 0x08 (read chunk) request.
     65     static func parseChunk(_ reply: [UInt8]) -> ChunkReply {
     66         guard reply.count == replyLength, reply[0] == reportID else { return .invalid }
     67         switch reply[1] {
     68         case 0x01:
     69             let length = Int(reply[2])
     70             guard 3 + length <= reply.count else { return .invalid }
     71             return .data(Array(reply[3 ..< 3 + length]))
     72         case 0x02: return .end
     73         default: return .invalid
     74         }
     75     }
     76 
     77     /// True for a valid acknowledgement of a 0x0b or part-select request.
     78     static func isAck(_ reply: [UInt8]) -> Bool { reply.count == replyLength && reply[0] == reportID }
     79 
     80     /// Size of the data part, from the metadata part.
     81     static func dataSize(fromMetadata meta: [UInt8]) -> Int? {
     82         meta.count >= 2 ? Int(meta[0]) | Int(meta[1]) << 8 : nil
     83     }
     84 }
     85 
     86 // MARK: - Config block (header + XOR-obfuscated JSON), Monado wmr_read_config / struct wmr_config_header
     87 
     88 struct WMRConfigBlock {
     89     var manufacturer = "", device = "", serial = "", uid = "", name = "", revision = "", revisionDate = ""
     90     var json: Data
     91 
     92     enum DecodeError: Error, CustomStringConvertible {
     93         case tooShort(Int), badJSONRange(start: Int, size: Int, total: Int)
     94         var description: String {
     95             switch self {
     96             case .tooShort(let n): return "config block too short (\(n) bytes)"
     97             case .badJSONRange(let s, let z, let t): return "invalid JSON range start=\(s) size=\(z) total=\(t)"
     98             }
     99         }
    100     }
    101 
    102     // struct wmr_config_header (packed by construction: u32, u32, then char arrays)
    103     static let headerSize = 8 + 0x40 + 0x40 + 0x40 + 0x26 + 0xd5 + 0x40 + 0x20 + 0x20
    104 
    105     /// Decodes the raw data part read from the headset.
    106     init(raw: [UInt8]) throws {
    107         guard raw.count >= Self.headerSize else { throw DecodeError.tooShort(raw.count) }
    108         func u32(_ o: Int) -> Int { Int(raw[o]) | Int(raw[o + 1]) << 8 | Int(raw[o + 2]) << 16 | Int(raw[o + 3]) << 24 }
    109         func text(_ o: Int, _ n: Int) -> String {
    110             String(decoding: raw[o ..< o + n].prefix { $0 != 0 }, as: UTF8.self)
    111         }
    112         let jsonStart = u32(0), jsonSize = u32(4)
    113         // Monado: json_start < data_size and (data_size - json_start) >= json_size
    114         guard jsonStart < raw.count, raw.count - jsonStart >= jsonSize, jsonSize > 2 else {
    115             throw DecodeError.badJSONRange(start: jsonStart, size: jsonSize, total: raw.count)
    116         }
    117         var o = 8
    118         manufacturer = text(o, 0x40); o += 0x40
    119         device = text(o, 0x40); o += 0x40
    120         serial = text(o, 0x40); o += 0x40
    121         uid = text(o, 0x26); o += 0x26 + 0xd5
    122         name = text(o, 0x40); o += 0x40
    123         revision = text(o, 0x20); o += 0x20
    124         revisionDate = text(o, 0x20)
    125         // The JSON starts 2 bytes after json_start and is XORed with the repeating key.
    126         let start = jsonStart + 2, count = jsonSize - 2
    127         var bytes = [UInt8](repeating: 0, count: count)
    128         let key = wmrConfigKey
    129         for i in 0 ..< count { bytes[i] = raw[start + i] ^ key[i % key.count] }
    130         json = Data(bytes.prefix { $0 != 0 })
    131     }
    132 
    133     /// Inverse of the decoder, for fixtures: XORs `json` into a block whose header points at it.
    134     static func encodeForTesting(json: Data, name: String = "Test Headset") -> [UInt8] {
    135         var raw = [UInt8](repeating: 0, count: headerSize)
    136         let jsonStart = headerSize
    137         let payload = [UInt8](json) + [0]
    138         let jsonSize = payload.count + 2
    139         for (i, b) in [UInt32(jsonStart), UInt32(jsonSize)].enumerated() {
    140             for k in 0 ..< 4 { raw[i * 4 + k] = UInt8((b >> (8 * UInt32(k))) & 0xff) }
    141         }
    142         let nameOffset = 8 + 0x40 * 3 + 0x26 + 0xd5
    143         for (i, c) in name.utf8.prefix(0x3f).enumerated() { raw[nameOffset + i] = c }
    144         raw += [0, 0] // the two bytes skipped before the JSON
    145         for (i, b) in payload.enumerated() { raw.append(b ^ wmrConfigKey[i % wmrConfigKey.count]) }
    146         return raw
    147     }
    148 }
    149 
    150 // MARK: - Parsed calibration, Monado wmr_config_parse / wmr_config_parse_display / wmr_inertial_sensor_config_parse
    151 
    152 /// One color channel of the Polynomial-3K model: radial distortion around `eyeCenter` (full-panel pixels).
    153 struct Poly3KChannel {
    154     var eyeCenter: SIMD2<Double>   // ModelParameters[0], [1]
    155     var k: SIMD3<Double>           // ModelParameters[2...4]
    156     var displaySize: SIMD2<Double> // full panel, e.g. 4320 x 2160
    157 }
    158 
    159 struct EyeCalibration {
    160     var displaySize: SIMD2<Double>
    161     var visibleCenter: SIMD2<Double>
    162     var visibleRadius: Double
    163     var affine: simd_double3x3          // as stored: row-major JSON loaded into rows
    164     var inverseAffine: simd_double3x3
    165     var rotation: simd_double3x3        // Rt.Rotation, row-major (to HT0)
    166     var translation: SIMD3<Double>      // Rt.Translation, metres (to HT0)
    167     var channels: [Poly3KChannel]       // red, green, blue
    168 }
    169 
    170 struct InertialSensorCalibration {
    171     enum Kind: String { case gyro = "CALIBRATION_InertialSensorType_Gyro",
    172                                  accelerometer = "CALIBRATION_InertialSensorType_Accelerometer",
    173                                  magnetometer = "CALIBRATION_InertialSensorType_Magnetometer" }
    174     var kind: Kind
    175     var rotation: simd_double3x3   // row-major (to HT0)
    176     var translation: SIMD3<Double>
    177     var biasOffsets: SIMD3<Double> // constant terms of BiasTemperatureModel
    178     var mixMatrix: simd_double3x3  // constant terms of MixingMatrixTemperatureModel
    179 }
    180 
    181 struct HeadsetCalibration {
    182     var eyes: [EyeCalibration]      // [left, right]
    183     var sensors: [InertialSensorCalibration]
    184 
    185     enum ParseError: Error, CustomStringConvertible {
    186         case missing(String), badValue(String), unsupportedModel(String)
    187         var description: String {
    188             switch self {
    189             case .missing(let k): return "calibration JSON: missing \(k)"
    190             case .badValue(let k): return "calibration JSON: bad value for \(k)"
    191             case .unsupportedModel(let m): return "calibration JSON: unsupported distortion model \(m)"
    192             }
    193         }
    194     }
    195 
    196     init(json: Data) throws {
    197         guard let root = try JSONSerialization.jsonObject(with: json) as? [String: Any],
    198               let info = root["CalibrationInformation"] as? [String: Any] else { throw ParseError.missing("CalibrationInformation") }
    199         guard let displays = info["Displays"] as? [[String: Any]] else { throw ParseError.missing("Displays") }
    200         var left: EyeCalibration?, right: EyeCalibration?
    201         for d in displays {
    202             let eye = try Self.parseDisplay(d)
    203             switch d["AssignedEye"] as? String {
    204             case "CALIBRATION_DisplayEyeLeft": left = eye
    205             case "CALIBRATION_DisplayEyeRight": right = eye
    206             default: throw ParseError.badValue("AssignedEye")
    207             }
    208         }
    209         guard let left, let right else { throw ParseError.missing("left and right displays") }
    210         eyes = [left, right]
    211         sensors = try ((info["InertialSensors"] as? [[String: Any]]) ?? []).compactMap(Self.parseSensor)
    212     }
    213 
    214     // MARK: helpers
    215 
    216     static func numbers(_ value: Any?, _ key: String, count: Int? = nil) throws -> [Double] {
    217         guard let array = value as? [Any] else { throw ParseError.missing(key) }
    218         let values = try array.map { v -> Double in
    219             guard let n = v as? NSNumber else { throw ParseError.badValue(key) }
    220             return n.doubleValue
    221         }
    222         if let count, values.count < count { throw ParseError.badValue(key) }
    223         return values
    224     }
    225 
    226     static func number(_ dict: [String: Any], _ key: String) throws -> Double {
    227         guard let n = dict[key] as? NSNumber else { throw ParseError.missing(key) }
    228         return n.doubleValue
    229     }
    230 
    231     /// Nine row-major values -> matrix whose rows are those values (simd is column-major, so build from rows).
    232     static func rowMajor3x3(_ v: [Double]) -> simd_double3x3 {
    233         simd_double3x3(rows: [SIMD3(v[0], v[1], v[2]), SIMD3(v[3], v[4], v[5]), SIMD3(v[6], v[7], v[8])])
    234     }
    235 
    236     static func parseRt(_ dict: [String: Any]) throws -> (simd_double3x3, SIMD3<Double>) {
    237         guard let rt = dict["Rt"] as? [String: Any] else { throw ParseError.missing("Rt") }
    238         let r = try numbers(rt["Rotation"], "Rt.Rotation", count: 9)
    239         let t = try numbers(rt["Translation"], "Rt.Translation", count: 3)
    240         return (rowMajor3x3(r), SIMD3(t[0], t[1], t[2]))
    241     }
    242 
    243     static func parseDisplay(_ d: [String: Any]) throws -> EyeCalibration {
    244         let affine = rowMajor3x3(try numbers(d["Affine"], "Affine", count: 9))
    245         let size = SIMD2(try number(d, "DisplayWidth"), try number(d, "DisplayHeight"))
    246         guard let vc = d["VisibleAreaCenter"] as? [String: Any] else { throw ParseError.missing("VisibleAreaCenter") }
    247         let (rotation, translation) = try parseRt(d)
    248         var channels: [Poly3KChannel] = []
    249         for name in ["DistortionRed", "DistortionGreen", "DistortionBlue"] {
    250             guard let dist = d[name] as? [String: Any] else { throw ParseError.missing(name) }
    251             let model = dist["ModelType"] as? String ?? "?"
    252             guard model == "CALIBRATION_DisplayDistortionModelPolynomial3K" else { throw ParseError.unsupportedModel(model) }
    253             let p = try numbers(dist["ModelParameters"], "\(name).ModelParameters", count: 5)
    254             channels.append(Poly3KChannel(eyeCenter: SIMD2(p[0], p[1]), k: SIMD3(p[2], p[3], p[4]), displaySize: size))
    255         }
    256         return EyeCalibration(displaySize: size, visibleCenter: SIMD2(try number(vc, "X"), try number(vc, "Y")),
    257                               visibleRadius: try number(d, "VisibleAreaRadius"), affine: affine,
    258                               inverseAffine: affine.inverse, rotation: rotation, translation: translation,
    259                               channels: channels)
    260     }
    261 
    262     static func parseSensor(_ s: [String: Any]) throws -> InertialSensorCalibration? {
    263         guard let kindName = s["SensorType"] as? String, let kind = InertialSensorCalibration.Kind(rawValue: kindName) else { return nil }
    264         let (rotation, translation) = try parseRt(s)
    265         // Monado takes only the constant coefficient of each temperature model (the others are 0 in practice).
    266         // MixingMatrixTemperatureModel: 3x3 entries x 4 coefficients; BiasTemperatureModel: 3 axes x 4 coefficients.
    267         let mix = (try? numbers(s["MixingMatrixTemperatureModel"], "MixingMatrixTemperatureModel", count: 36)) ?? []
    268         let bias = (try? numbers(s["BiasTemperatureModel"], "BiasTemperatureModel", count: 12)) ?? []
    269         let mixMatrix = mix.count >= 36 ? rowMajor3x3((0 ..< 9).map { mix[$0 * 4] }) : matrix_identity_double3x3
    270         let biasOffsets = bias.count >= 12 ? SIMD3(bias[0], bias[4], bias[8]) : .zero
    271         return InertialSensorCalibration(kind: kind, rotation: rotation, translation: translation,
    272                                          biasOffsets: biasOffsets, mixMatrix: mixMatrix)
    273     }
    274 }
    275 
    276 // MARK: - Poly3K distortion, Monado u_compute_distortion_bounds_poly_3k / u_compute_distortion_poly_3k
    277 
    278 /// Render-target bounds for one eye: tangents of the half-angles (Y down, as in Monado's distortion space).
    279 struct Poly3KBounds {
    280     var texXRange: SIMD2<Double> // (tan left, tan right); left is negative
    281     var texYRange: SIMD2<Double> // (tan up, tan down) in Y-down space; up is negative
    282     /// OpenXR-style angles in radians (left/down negative), for building an asymmetric projection.
    283     var angleLeft: Double { atan(texXRange.x) }
    284     var angleRight: Double { atan(texXRange.y) }
    285     var angleUp: Double { -atan(texYRange.x) }
    286     var angleDown: Double { -atan(texYRange.y) }
    287 }
    288 
    289 extension EyeCalibration {
    290     /// `view` 0 = left, 1 = right (the right eye's panel X starts at displaySize.x / 2).
    291     func distortionBounds(view: Int) -> Poly3KBounds {
    292         var left = 0.0, right = 0.0, up = 0.0, down = 0.0
    293         for c in channels {
    294             let half = c.displaySize.x / 2, v = Double(view)
    295             let pixCoords: [SIMD2<Double>] = [
    296                 SIMD2(v * half - c.eyeCenter.x, 0),                  // -eye_center_x, 0
    297                 SIMD2(0, -c.eyeCenter.y),                            // 0, -eye_center_y
    298                 SIMD2((1 + v) * half - c.eyeCenter.x, 0),            // width - eye_center_x, 0
    299                 SIMD2(0, c.displaySize.y - c.eyeCenter.y),           // 0, height - eye_center_y
    300             ]
    301             for pix in pixCoords {
    302                 let vp = viewPlane(pix, c)
    303                 if pix.x < 0 { left = min(left, vp.x) } else { right = max(right, vp.x) }
    304                 if pix.y < 0 { up = min(up, vp.y) } else { down = max(down, vp.y) }
    305             }
    306         }
    307         return Poly3KBounds(texXRange: SIMD2(left, right), texYRange: SIMD2(up, down))
    308     }
    309 
    310     /// For an output position (u, v) in 0...1 across THIS eye's half of the panel (v = 0 at the top), returns the
    311     /// red/green/blue sample coordinates in 0...1 of the eye's undistorted render target (rendered with `bounds`).
    312     func distort(view: Int, u: Double, v: Double, bounds: Poly3KBounds) -> (r: SIMD2<Double>, g: SIMD2<Double>, b: SIMD2<Double>) {
    313         var out: [SIMD2<Double>] = []
    314         for c in channels {
    315             // Scale the 0..1 UV back to pixels relative to the distortion center (right eye starts at width / 2).
    316             let pix = SIMD2((u + Double(view)) * (c.displaySize.x / 2) - c.eyeCenter.x, v * c.displaySize.y - c.eyeCenter.y)
    317             let vp = viewPlane(pix, c)
    318             out.append(SIMD2((vp.x - bounds.texXRange.x) / (bounds.texXRange.y - bounds.texXRange.x),
    319                              (vp.y - bounds.texYRange.x) / (bounds.texYRange.y - bounds.texYRange.x)))
    320         }
    321         return (out[0], out[1], out[2])
    322     }
    323 
    324     /// Distort a pixel offset radially, then map back to view-plane tangents with the inverse affine transform.
    325     private func viewPlane(_ pix: SIMD2<Double>, _ c: Poly3KChannel) -> SIMD2<Double> {
    326         let r2 = simd_dot(pix, pix)
    327         let d = 1 + r2 * (c.k.x + r2 * (c.k.y + r2 * c.k.z))
    328         let p = SIMD3(pix.x * d + c.eyeCenter.x, pix.y * d + c.eyeCenter.y, 1)
    329         let vp = inverseAffine * p // Monado's math_matrix_3x3_transform_vec3 on a row-major matrix = M * p
    330         return SIMD2(vp.x / vp.z, vp.y / vp.z)
    331     }
    332 
    333     /// Interpupillary distance contribution from the per-eye translations (metres).
    334     static func ipd(_ left: EyeCalibration, _ right: EyeCalibration) -> Double { simd_length(right.translation - left.translation) }
    335 }
    336 
    337 // MARK: - XOR key, Monado wmr_config_key.h (1024 bytes)
    338 
    339 let wmrConfigKey: [UInt8] = [
    340         0x2F, 0xC8, 0x0F, 0x38, 0xDD, 0x00, 0xF6, 0x5C, 0xA1, 0x31, 0xEF, 0xF1, 0xEA, 0x6F, 0xA0, 0xF8,
    341         0x26, 0xB5, 0x9B, 0x39, 0xCF, 0x3A, 0x88, 0xC8, 0x2E, 0x17, 0xC0, 0x63, 0x5B, 0x46, 0x27, 0xBB,
    342         0x98, 0x2F, 0x0E, 0x2A, 0x90, 0x4B, 0x28, 0x2D, 0x82, 0x76, 0xE5, 0x28, 0x72, 0x50, 0x8A, 0xF0,
    343         0xBF, 0x84, 0x54, 0x3B, 0xA8, 0x77, 0x91, 0xCE, 0x87, 0x80, 0x53, 0x2F, 0x07, 0xAD, 0x1B, 0x3F,
    344         0x8C, 0x67, 0x33, 0x2E, 0xEB, 0x6A, 0x2A, 0x52, 0x77, 0x7C, 0x1F, 0x02, 0x11, 0x9E, 0x2A, 0x59,
    345         0x5C, 0x94, 0x0E, 0x4F, 0xF5, 0x44, 0x54, 0x01, 0xE7, 0x8F, 0x66, 0xF0, 0xAD, 0x68, 0x71, 0x3C,
    346         0x6D, 0x2E, 0x1C, 0xE3, 0x11, 0x46, 0xF7, 0x7F, 0x02, 0x6C, 0x15, 0xA0, 0x10, 0xEE, 0x3B, 0x14,
    347         0xAE, 0x6C, 0xA7, 0x3F, 0xAF, 0x83, 0x6A, 0xD7, 0x12, 0x88, 0x53, 0xFE, 0xEB, 0x5C, 0x78, 0x85,
    348         0xAF, 0x1F, 0x80, 0x7F, 0xB6, 0xDA, 0x7C, 0x0E, 0x84, 0xB5, 0x02, 0x8E, 0x92, 0xA3, 0x5B, 0x83,
    349         0x56, 0x11, 0x7B, 0xDF, 0x80, 0xB3, 0x4C, 0x13, 0x8E, 0x61, 0x61, 0xE6, 0x82, 0x8C, 0xDA, 0x08,
    350         0x76, 0x88, 0xBF, 0x85, 0x7F, 0xE4, 0x28, 0x26, 0x1F, 0xB5, 0x67, 0x80, 0x63, 0xD9, 0x26, 0xD4,
    351         0x91, 0xD1, 0xC1, 0x51, 0xCE, 0x61, 0x64, 0x2B, 0x56, 0xAE, 0x3D, 0x06, 0x5D, 0xCD, 0xF7, 0x05,
    352         0x9A, 0x6F, 0xEB, 0x2E, 0xC2, 0x69, 0x42, 0x86, 0xBE, 0x78, 0x32, 0xC3, 0xA8, 0x94, 0xA3, 0x97,
    353         0x84, 0x07, 0xF1, 0x6E, 0x3F, 0x10, 0xDE, 0x2B, 0xB1, 0x41, 0x1A, 0x59, 0xE3, 0x7F, 0x23, 0xDE,
    354         0xF3, 0x13, 0x23, 0xD1, 0x60, 0x1C, 0xBB, 0xC5, 0x4A, 0xB1, 0xC6, 0x02, 0x38, 0x7B, 0xFE, 0xEF,
    355         0xB6, 0x50, 0x16, 0x23, 0x4B, 0xD4, 0xEF, 0xEA, 0x67, 0xEC, 0x44, 0x2C, 0xC0, 0xA6, 0x2F, 0x0D,
    356         0x6E, 0x17, 0x52, 0xC8, 0x26, 0xCB, 0x63, 0x85, 0x72, 0x8D, 0xBA, 0xD8, 0x09, 0xA3, 0x89, 0x64,
    357         0x70, 0x12, 0xC6, 0xDF, 0x4C, 0x28, 0xD4, 0xB8, 0x49, 0x18, 0x69, 0x22, 0x36, 0xF1, 0x00, 0xC3,
    358         0x91, 0xB3, 0x7C, 0xA0, 0xAA, 0x7D, 0x9E, 0x27, 0x65, 0xCD, 0x16, 0x3C, 0x71, 0x3C, 0xCC, 0xF5,
    359         0x02, 0xD1, 0xA5, 0x06, 0x95, 0xB2, 0x1E, 0x71, 0x92, 0x6F, 0xC2, 0xD2, 0xEF, 0x58, 0x7B, 0xD0,
    360         0x53, 0x5E, 0xE9, 0xB6, 0xCA, 0x1C, 0x13, 0x96, 0xAC, 0xF1, 0xF5, 0x19, 0xD9, 0x8A, 0x1D, 0xA9,
    361         0x0D, 0xAE, 0xD0, 0xF4, 0xB3, 0xDD, 0x2D, 0x43, 0x6E, 0x41, 0x22, 0xDC, 0x09, 0xA4, 0x92, 0x42,
    362         0xC5, 0x32, 0x7D, 0xD7, 0xA2, 0x57, 0xA5, 0xA5, 0x11, 0xD2, 0x22, 0xD0, 0xD7, 0x75, 0xFF, 0xFC,
    363         0x2F, 0x66, 0xB5, 0xA9, 0xA3, 0x2B, 0xB4, 0x2B, 0x14, 0xEF, 0xC4, 0xB4, 0x18, 0xF0, 0x56, 0x55,
    364         0x93, 0x6A, 0x30, 0xF6, 0xDF, 0x14, 0x23, 0xF7, 0x2A, 0xDC, 0x4C, 0xE7, 0x78, 0x2B, 0x66, 0x22,
    365         0xB4, 0xAC, 0x2E, 0x03, 0xF2, 0xEF, 0xEC, 0x35, 0xAF, 0x22, 0x6D, 0x05, 0x12, 0x6D, 0xC5, 0x2C,
    366         0x12, 0x9B, 0x7D, 0x66, 0x47, 0x8F, 0xC0, 0x81, 0xCD, 0xFE, 0x97, 0x4C, 0x01, 0xC1, 0xD7, 0x24,
    367         0xD8, 0x46, 0xFD, 0x29, 0xF7, 0xE1, 0x61, 0xF3, 0xA0, 0x69, 0xBD, 0x23, 0x35, 0x7E, 0x66, 0xB1,
    368         0xF6, 0x3A, 0xD9, 0xF8, 0x29, 0xA2, 0x99, 0x8E, 0xF7, 0xBC, 0xCC, 0xD6, 0x37, 0x11, 0x09, 0xC8,
    369         0x07, 0x68, 0x5D, 0xF6, 0xC2, 0x73, 0xE8, 0xE5, 0x2D, 0xA4, 0x62, 0x0E, 0x9D, 0xC2, 0x76, 0xA9,
    370         0x94, 0x06, 0x19, 0x39, 0x9F, 0x8F, 0x83, 0x62, 0xE9, 0xDE, 0xED, 0x76, 0xFD, 0xBC, 0x42, 0x77,
    371         0x1E, 0x49, 0x18, 0xD8, 0x15, 0x22, 0x55, 0x5C, 0xD3, 0xF2, 0x7F, 0xD0, 0x8A, 0x27, 0x82, 0x05,
    372         0xD3, 0xBD, 0x27, 0x0C, 0x2F, 0xB9, 0x72, 0xE9, 0x9D, 0x6B, 0xD3, 0xD6, 0xD6, 0x84, 0xA4, 0x1F,
    373         0x6C, 0x26, 0x7C, 0x61, 0xE0, 0x7E, 0x58, 0x05, 0xAD, 0xC5, 0xE1, 0x14, 0x3A, 0xD7, 0x40, 0x6A,
    374         0x52, 0x57, 0x82, 0xAA, 0x9B, 0xF0, 0xCA, 0x60, 0x5D, 0x6C, 0xC0, 0xA4, 0x6B, 0xF3, 0x87, 0x6D,
    375         0x04, 0x80, 0x2C, 0x7B, 0xEB, 0x9F, 0xD4, 0x03, 0x81, 0x91, 0xD0, 0xB9, 0x74, 0xAE, 0x19, 0xBF,
    376         0x48, 0x63, 0x8F, 0x8C, 0xEE, 0xBC, 0xB4, 0xC0, 0x16, 0x4A, 0xF5, 0x5E, 0x1C, 0x7A, 0xDB, 0xD5,
    377         0xA4, 0x16, 0x92, 0xCB, 0x52, 0x86, 0xCB, 0xD1, 0x1E, 0x1D, 0xEE, 0x90, 0x01, 0x90, 0x52, 0x52,
    378         0x52, 0x8C, 0x25, 0x0A, 0xB7, 0xDE, 0x10, 0x51, 0xB8, 0x23, 0x5C, 0xCB, 0x32, 0x6A, 0xB0, 0xB9,
    379         0xA4, 0x58, 0xB6, 0x14, 0x28, 0xF0, 0xFB, 0xC2, 0xCD, 0x6F, 0x5E, 0x10, 0x48, 0xAD, 0x1F, 0xC8,
    380         0xCE, 0x4F, 0x09, 0xDA, 0xF8, 0xD0, 0x84, 0x44, 0x8C, 0x57, 0x4B, 0xE1, 0x87, 0x5B, 0x79, 0xD0,
    381         0x93, 0x38, 0x57, 0x65, 0x31, 0x55, 0xF2, 0xD6, 0x1F, 0x6C, 0xC9, 0xD1, 0x3A, 0x17, 0x3C, 0x4F,
    382         0x97, 0x23, 0x07, 0xB9, 0xB6, 0xB5, 0x32, 0x28, 0x24, 0x0E, 0xCC, 0x1A, 0xA1, 0x74, 0x39, 0x06,
    383         0xD9, 0x52, 0xD6, 0x38, 0xFC, 0x95, 0xBF, 0x84, 0x3A, 0x76, 0xA3, 0xC3, 0x54, 0xF2, 0x71, 0x4D,
    384         0x2D, 0xE8, 0x9F, 0x58, 0x19, 0xE9, 0xD3, 0x5A, 0xCE, 0x30, 0x1E, 0xB5, 0xEE, 0xB5, 0x83, 0xF4,
    385         0xB9, 0x23, 0xF3, 0xA1, 0xFC, 0xEA, 0x68, 0x2F, 0xAF, 0x22, 0x73, 0xF2, 0x21, 0x66, 0x8C, 0x29,
    386         0xF2, 0x34, 0x7A, 0x39, 0xB9, 0x3C, 0x2C, 0x96, 0x54, 0x7A, 0x7E, 0xA5, 0x24, 0x98, 0xF7, 0x06,
    387         0x78, 0x28, 0x70, 0x7A, 0x3C, 0x73, 0x8D, 0x82, 0xB1, 0x9C, 0x1E, 0xD9, 0xDB, 0xBB, 0xEF, 0x3F,
    388         0xC2, 0x0F, 0xAF, 0x73, 0x0E, 0xC0, 0x01, 0x2E, 0x5B, 0x8A, 0xC4, 0x39, 0x5A, 0x71, 0xA9, 0x2B,
    389         0xD9, 0xD3, 0x9A, 0x0D, 0x28, 0x95, 0xFE, 0x7E, 0xD8, 0xC7, 0x73, 0xDD, 0x77, 0x52, 0x56, 0x94,
    390         0x80, 0x93, 0xD1, 0xFF, 0x02, 0x28, 0xE0, 0x18, 0xA1, 0xF2, 0x7E, 0x9A, 0x1C, 0xF2, 0x7B, 0x76,
    391         0x2C, 0xF0, 0xB7, 0x39, 0xF3, 0x10, 0x08, 0x90, 0x8F, 0xA6, 0xEB, 0x5F, 0xF5, 0x1A, 0xB1, 0x72,
    392         0xF0, 0x1B, 0x7A, 0xF4, 0xF7, 0x4D, 0x5C, 0xC0, 0x82, 0x1F, 0x27, 0xCE, 0xA4, 0x52, 0xB2, 0xE8,
    393         0x24, 0xC7, 0xCA, 0x8C, 0xB9, 0xCB, 0x6C, 0xC5, 0xA0, 0x42, 0x18, 0x7F, 0xE5, 0xFA, 0xA9, 0x8E,
    394         0xA0, 0xF4, 0x58, 0x78, 0xB9, 0x30, 0x86, 0x49, 0x01, 0x15, 0x8E, 0xB0, 0x22, 0x8C, 0xF5, 0x12,
    395         0x64, 0xE6, 0x69, 0x90, 0xD6, 0x86, 0x92, 0x9B, 0x83, 0xD4, 0xF7, 0x01, 0x15, 0x9A, 0x7C, 0xF8,
    396         0xB3, 0xCD, 0x0A, 0xA1, 0x3D, 0x49, 0x90, 0x21, 0x69, 0xD7, 0x25, 0xFC, 0x1A, 0x64, 0x22, 0x77,
    397         0x7A, 0xBF, 0x3C, 0x1C, 0x4B, 0x06, 0x6E, 0x83, 0x03, 0x5D, 0x5C, 0x76, 0xEA, 0x84, 0x29, 0xB5,
    398         0x7C, 0xC0, 0x74, 0xBC, 0x4A, 0x21, 0x7B, 0xDC, 0xFE, 0x1B, 0x1F, 0x77, 0x64, 0x20, 0x59, 0x6A,
    399         0x0B, 0x48, 0xC2, 0x0E, 0x2D, 0xFF, 0xCE, 0x4C, 0x06, 0xED, 0x0E, 0x1C, 0xB6, 0x1A, 0x62, 0x79,
    400         0xEC, 0x25, 0xD6, 0x89, 0xBF, 0x4F, 0x16, 0x75, 0x82, 0xD7, 0x98, 0x5C, 0xBA, 0x75, 0xBA, 0xD3,
    401         0x2D, 0xC7, 0x47, 0xF3, 0xB6, 0x31, 0x54, 0xE0, 0x86, 0xFE, 0x29, 0x8E, 0xE2, 0x92, 0x79, 0x89,
    402         0xE4, 0x43, 0xB4, 0x9C, 0xF7, 0xED, 0x1B, 0xA6, 0x0B, 0x0C, 0x69, 0x23, 0xF4, 0x7D, 0x0A, 0xA2,
    403         0x8C, 0xEC, 0xD5, 0x2C, 0x8E, 0xB6, 0x20, 0x8F, 0xA6, 0xB9, 0x86, 0xB8, 0xBA, 0x59, 0xA3, 0xA7,
    404 ]
    405