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