diff --git a/clients/apple/Sources/PunktfunkKit/Video/CscRows.swift b/clients/apple/Sources/PunktfunkKit/Video/CscRows.swift new file mode 100644 index 00000000..87195785 --- /dev/null +++ b/clients/apple/Sources/PunktfunkKit/Video/CscRows.swift @@ -0,0 +1,126 @@ +// The Y′CbCr→RGB conversion as three shader rows, ported from pf-client-core's `csc_rows` +// (crates/pf-client-core/src/video.rs) — the ONE coefficient implementation every punktfunk +// presenter derives its CSC from. Keep the two in LOCKSTEP: both carry the same unit tests +// (CscRowsTests.swift ↔ the Rust `csc_rows` tests), and a coefficient change lands in both or +// neither. +// +// Why this exists: the stage-2 Metal shaders used to hardcode BT.709 (SDR) / BT.2020 (HDR) +// matrices, silently ignoring the stream's signaled matrix. A Linux host's RGB-input NVENC paths +// signal BT.601 limited (NVENC's fixed internal RGB→YUV conversion; ffmpeg force-writes that +// VUI), so those streams rendered with the wrong coefficients — a constant hue error. The rows +// are now computed per frame from the decoded buffer's actual signaling (VideoToolbox propagates +// the HEVC VUI / AV1 colour config onto the CVPixelBuffer's attachments) and handed to the +// fragment shaders as bytes. + +import CoreVideo +import simd + +/// The fragment shaders' CSC constant block: `rgb[i] = dot(r[i].xyz, yuv) + r[i].w`. +/// Layout matches the Metal-side `struct CscUniform { float4 r0; float4 r1; float4 r2; }` +/// (three 16-byte-aligned float4s, stride 48) — passed via `setFragmentBytes`. +public struct CscUniform: Equatable, Sendable { + public var r0: SIMD4 + public var r1: SIMD4 + public var r2: SIMD4 +} + +public enum CscRows { + /// A decoded frame's Y′CbCr signaling: the H.273 matrix code (1 = BT.709, 5/6 = BT.601, + /// 9/10 = BT.2020; 2 = unspecified → the BT.709 SDR default, mirroring `ColorDesc`) and + /// whether the samples are full range. + public struct Signal: Equatable, Sendable { + public var matrix: UInt8 + public var fullRange: Bool + + public init(matrix: UInt8, fullRange: Bool) { + self.matrix = matrix + self.fullRange = fullRange + } + } + + /// Read a decoded buffer's signaling: the matrix from the `CVImageBuffer` attachment + /// (VideoToolbox propagates the bitstream's colour description there), the range from the + /// pixel format itself (the video- vs full-range biplanar siblings), so a full-range stream + /// expands correctly no matter which sibling VideoToolbox delivered. + public static func signal(of buffer: CVPixelBuffer) -> Signal { + var matrix: UInt8 = 2 // unspecified → BT.709 default in rows() + if let att = CVBufferCopyAttachment(buffer, kCVImageBufferYCbCrMatrixKey, nil), + CFGetTypeID(att) == CFStringGetTypeID() { + let s = unsafeDowncast(att, to: CFString.self) + if CFEqual(s, kCVImageBufferYCbCrMatrix_ITU_R_709_2) { + matrix = 1 + } else if CFEqual(s, kCVImageBufferYCbCrMatrix_ITU_R_601_4) { + matrix = 5 + } else if CFEqual(s, kCVImageBufferYCbCrMatrix_SMPTE_240M_1995) { + matrix = 7 + } else if CFEqual(s, kCVImageBufferYCbCrMatrix_ITU_R_2020) { + matrix = 9 + } else { + // CICP codes CoreMedia has no named constant for arrive as the literal string + // "YCbCrMatrix#" — the suffix IS the H.273 code. BT.470BG (5) takes this + // form (proven by the 601 golden fixture), and BT.470BG is exactly what a Linux + // host's RGB-input NVENC signals, so missing it re-creates the hue bug the + // per-frame signaling exists to fix. + let str = s as String + if str.hasPrefix("YCbCrMatrix#"), let code = UInt8(str.dropFirst(12)) { + matrix = code + } + } + } + let pf = CVPixelBufferGetPixelFormatType(buffer) + let fullRange = pf == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + || pf == kCVPixelFormatType_420YpCbCr10BiPlanarFullRange + || pf == kCVPixelFormatType_444YpCbCr8BiPlanarFullRange + || pf == kCVPixelFormatType_444YpCbCr10BiPlanarFullRange + return Signal(matrix: matrix, fullRange: fullRange) + } + + /// Compute the three rows — bit-depth exact. `depth` picks the limited-range code points + /// (8-bit: 16/235/240 over 255; 10-bit: 64/940/960 over 1023 — NOT the same normalized + /// values, the difference is ~half a code). `msbPacked` folds in the P010/x444 packing + /// factor: 10 significant bits live in the MSBs of 16, so an `.r16Unorm` sample reads + /// `code·64/65535` — multiplying by `65535/65472` recovers exact `code/1023` (this replaces + /// the shaders' old documented ~0.1% approximation). + public static func rows(_ signal: Signal, depth: Int, msbPacked: Bool) -> CscUniform { + // BT.601 (5/6), BT.2020 (9/10); everything else — incl. unspecified — is the host's + // BT.709 SDR default (mirrors the Rust side's dispatch). + let (kr, kb): (Double, Double) + switch signal.matrix { + case 5, 6: (kr, kb) = (0.299, 0.114) + case 9, 10: (kr, kb) = (0.2627, 0.0593) + default: (kr, kb) = (0.2126, 0.0722) + } + let kg = 1.0 - kr - kb + let max = Double((1 << depth) - 1) // 255 / 1023 + let step = Double(1 << (depth - 8)) // code points per 8-bit step: 1 / 4 + let pack = msbPacked ? 65535.0 / 65472.0 : 1.0 + let (sy, oy, sc): (Double, Double, Double) + if signal.fullRange { + (sy, oy, sc) = (pack, 0.0, pack) + } else { + (sy, oy, sc) = ( + pack * max / (219.0 * step), + -(16.0 * step) / max, + pack * max / (224.0 * step) + ) + } + // rgb = M * (yuv + off) = M*yuv + M*off — rows of M with the offset dot folded into + // w. `yuv` is the SAMPLED (packed) value, so the offsets divide by the packing + // factor to land on the same scale. + let off = [oy / pack, -0.5 / pack, -0.5 / pack] + let m: [[Double]] = [ + [sy, 0.0, 2.0 * (1.0 - kr) * sc], + [ + sy, + -2.0 * (1.0 - kb) * kb / kg * sc, + -2.0 * (1.0 - kr) * kr / kg * sc, + ], + [sy, 2.0 * (1.0 - kb) * sc, 0.0], + ] + func row(_ r: Int) -> SIMD4 { + let w = (0..<3).reduce(0.0) { $0 + m[r][$1] * off[$1] } + return SIMD4(Float(m[r][0]), Float(m[r][1]), Float(m[r][2]), Float(w)) + } + return CscUniform(r0: row(0), r1: row(1), r2: row(2)) + } +} diff --git a/clients/apple/Sources/PunktfunkKit/Video/MetalVideoPresenter.swift b/clients/apple/Sources/PunktfunkKit/Video/MetalVideoPresenter.swift index e94600b1..063c6080 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/MetalVideoPresenter.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/MetalVideoPresenter.swift @@ -28,17 +28,39 @@ private let presenterLog = Logger(subsystem: "io.unom.punktfunk", category: "pre /// dimmer. Matches the host's standard PQ reference white. private let hdrReferenceWhiteNits: Float = 203.0 -/// Runtime-compiled (no metallib build step needed in SwiftPM): a fullscreen triangle and BT.709 SDR -/// and BT.2020-PQ HDR Y′CbCr→RGB fragment shaders. uv.y is flipped (1 - p.y) so the top-left-origin -/// texture presents upright (NDC y is up). The HDR shader outputs PQ-encoded R′G′B′ as-is — the -/// CAMetalLayer's `itur_2100_PQ` colour space + `edrMetadata` tell the system compositor the samples -/// are PQ and how to tone-map them (no EOTF here, matching the host's BT.2020 PQ emission). +/// PUNKTFUNK_SDR_COLORSPACE=srgb — A/B hatch for the SDR layer's colour tag. Today the SDR layer +/// ships with `colorspace = nil`, which on macOS means NO colour matching: the BT.709/sRGB-encoded +/// stream is displayed with the panel's native primaries — mild oversaturation on every P3 Mac. +/// `srgb` tags the layer so CoreAnimation colour-matches it into the panel's gamut (the strictly +/// correct rendering). Kept OFF by default until the on-glass A/B confirms it (the nil path is the +/// long-proven look, and some users may prefer the vivid rendition); flip the default once verified. +private let sdrColorspaceOverride: CGColorSpace? = { + guard ProcessInfo.processInfo.environment["PUNKTFUNK_SDR_COLORSPACE"] == "srgb" else { + return nil + } + return CGColorSpace(name: CGColorSpace.sRGB) +}() + +/// Runtime-compiled (no metallib build step needed in SwiftPM): a fullscreen triangle and Y′CbCr→RGB +/// fragment shaders whose conversion arrives as three constant rows computed per frame on the CPU +/// (`CscRows` — the Swift port of pf-client-core's `csc_rows`, from the decoded buffer's actual +/// signaling). One set of coefficients honors BT.601/709/2020 × full/limited × 8/10-bit instead of +/// the old hardcoded BT.709/BT.2020 matrices — a BT.601-signaled stream (a Linux host's RGB-input +/// NVENC) used to render with BT.709 coefficients, a constant hue error. uv.y is flipped (1 - p.y) +/// so the top-left-origin texture presents upright (NDC y is up). The HDR shader outputs PQ-encoded +/// R′G′B′ as-is — the CAMetalLayer's `itur_2100_PQ` colour space + `edrMetadata` tell the system +/// compositor the samples are PQ and how to tone-map them (no EOTF here, matching the host's +/// BT.2020 PQ emission). private let shaderSource = """ #include using namespace metal; struct VOut { float4 pos [[position]]; float2 uv; }; +// The CPU-computed CSC rows (CscRows.swift, layout-matched): rgb[i] = dot(ri.xyz, yuv) + ri.w. +// Range expansion, the matrix, and the 10-bit MSB-packing factor are all folded in. +struct CscUniform { float4 r0; float4 r1; float4 r2; }; + vertex VOut pf_vtx(uint vid [[vertex_id]]) { float2 p = float2(float((vid << 1) & 2), float(vid & 2)); VOut o; @@ -94,43 +116,80 @@ float2 chromaUV(texture2d lumaTex, texture2d chromaTex, float2 uv) return uv; } -// SDR: 8-bit NV12 / 4:4:4 (BT.709, limited/video range) → full-range RGB. Chroma is sampled at the -// (siting-corrected) luma UV, so a full-size 4:4:4 chroma plane needs no shader change vs 4:2:0. -fragment float4 pf_frag(VOut in [[stage_in]], - texture2d lumaTex [[texture(0)]], - texture2d chromaTex [[texture(1)]]) { +// The shared sample + row-multiply: Y′CbCr (bicubic luma, siting-corrected bilinear chroma) → +// RGB via the per-frame rows. A full-size 4:4:4 chroma plane needs no change vs 4:2:0 (the siting +// offset self-disables). What the result MEANS depends on the stream: an SDR frame's rows yield +// gamma-encoded RGB, an HDR frame's rows yield PQ-encoded R′G′B′ — the fragment variants below +// differ only in what they do next. +float3 sampleRgb(texture2d lumaTex, texture2d chromaTex, float2 uv, + constant CscUniform& csc) { constexpr sampler s(filter::linear, address::clamp_to_edge); - float y = catmullRomLuma(lumaTex, s, in.uv); - float2 c = chromaTex.sample(s, chromaUV(lumaTex, chromaTex, in.uv)).rg; - // BT.709, 8-bit limited (video) range → full-range RGB. - y = (y - 16.0/255.0) * (255.0/219.0); - float u = (c.x - 128.0/255.0) * (255.0/224.0); - float v = (c.y - 128.0/255.0) * (255.0/224.0); - float r = y + 1.5748 * v; - float g = y - 0.1873 * u - 0.4681 * v; - float b = y + 1.8556 * u; - return float4(saturate(float3(r, g, b)), 1.0); + float3 yuv = float3(catmullRomLuma(lumaTex, s, uv), + chromaTex.sample(s, chromaUV(lumaTex, chromaTex, uv)).rg); + return saturate(float3(dot(csc.r0.xyz, yuv) + csc.r0.w, + dot(csc.r1.xyz, yuv) + csc.r1.w, + dot(csc.r2.xyz, yuv) + csc.r2.w)); } -// HDR: 10-bit P010 / 4:4:4 (BT.2020, limited range), Y′CbCr that is PQ-encoded. We apply the BT.2020 -// matrix to get PQ-encoded R′G′B′ and output it as-is — the CAMetalLayer's itur_2100_PQ colour space -// + edrMetadata tell the compositor the samples are PQ, so it does the PQ→display tone-map. No EOTF -// here. P010/x444 store the 10-bit code in the high bits of each 16-bit sample, so an .r16Unorm sample -// reads ~code/1023 (the /1024 vs /1023 error is < 0.1%). +// SDR: 8-bit NV12 / 4:4:4 → full-range RGB, transfer left baked (shown as-is, the proven SDR +// layer config). +fragment float4 pf_frag(VOut in [[stage_in]], + texture2d lumaTex [[texture(0)]], + texture2d chromaTex [[texture(1)]], + constant CscUniform& csc [[buffer(0)]]) { + return float4(sampleRgb(lumaTex, chromaTex, in.uv, csc), 1.0); +} + +// HDR: 10-bit P010 / 4:4:4 (BT.2020, PQ-encoded Y′CbCr) → full-range PQ R′G′B′, output as-is — +// the CAMetalLayer's itur_2100_PQ colour space + edrMetadata tell the compositor the samples are +// PQ, so it does the PQ→display tone-map. No EOTF here. The rows fold in the exact 10-bit +// MSB-packing factor (the old hardcoded shader carried a documented ~0.1% /1024-vs-/1023 error). fragment float4 pf_frag_hdr(VOut in [[stage_in]], texture2d lumaTex [[texture(0)]], - texture2d chromaTex [[texture(1)]]) { - constexpr sampler s(filter::linear, address::clamp_to_edge); - float y = catmullRomLuma(lumaTex, s, in.uv); - float2 c = chromaTex.sample(s, chromaUV(lumaTex, chromaTex, in.uv)).rg; - // BT.2020 10-bit limited (video) range → full-range PQ R′G′B′. - y = (y - 64.0/1023.0) * (1023.0/876.0); - float u = (c.x - 512.0/1023.0) * (1023.0/896.0); - float v = (c.y - 512.0/1023.0) * (1023.0/896.0); - float r = y + 1.4746 * v; - float g = y - 0.16455 * u - 0.57135 * v; - float b = y + 1.8814 * u; - return float4(saturate(float3(r, g, b)), 1.0); + texture2d chromaTex [[texture(1)]], + constant CscUniform& csc [[buffer(0)]]) { + return float4(sampleRgb(lumaTex, chromaTex, in.uv, csc), 1.0); +} + +// HDR on tvOS when the display is composited WITHOUT HDR headroom (SDR output mode, or the user +// disabled Match Dynamic Range): no Metal EDR API exists there (CAEDRMetadata / +// wantsExtendedDynamicRangeContent are API_UNAVAILABLE(tvos)), and a bare PQ colour-space tag +// composites UNtone-mapped — the CAMetalLayer header says so outright — which showed as a badly +// overblown picture on Apple TV. So this variant finishes the job in-shader: PQ EOTF → linear +// light, 203-nit reference white (BT.2408) anchored at display white, extended-Reinhard highlight +// rolloff with a 1000-nit knee, BT.2020→BT.709 primaries, BT.709 OETF — into the proven SDR layer +// config. The 10-bit BT.2020 stream keeps its full decode depth; only the final presentation is +// display-referred SDR. (When the display IS in an HDR mode — requested per session via +// AVDisplayManager, see StreamViewIOS — tvOS presents pf_frag_hdr's PQ passthrough instead: +// in a genuine HDR10 output, PQ passthrough is the correct emission and the TV tone-maps.) +fragment float4 pf_frag_hdr_tv(VOut in [[stage_in]], + texture2d lumaTex [[texture(0)]], + texture2d chromaTex [[texture(1)]], + constant CscUniform& csc [[buffer(0)]]) { + // Y′CbCr → full-range PQ R′G′B′ via the per-frame rows (as pf_frag_hdr). + float3 pq = sampleRgb(lumaTex, chromaTex, in.uv, csc); + // ST 2084 EOTF: PQ code value → linear light, 1.0 = 10,000 nits. + const float m1 = 2610.0/16384.0; + const float m2 = 78.84375; + const float c1 = 3424.0/4096.0; + const float c2 = 18.8515625; + const float c3 = 18.6875; + float3 p = pow(pq, 1.0/m2); + float3 lin = pow(max(p - c1, 0.0) / (c2 - c3 * p), 1.0/m1); + // Scene-referred with diffuse white at 1.0 (the same 203-nit anchor the EDR path uses). + float3 t = lin * (10000.0/203.0); + // BT.2020 → BT.709 primaries while still linear; negatives are out-of-gamut, floor them. + float3 t709 = float3( + dot(t, float3( 1.6605, -0.5876, -0.0728)), + dot(t, float3(-0.1246, 1.1329, -0.0083)), + dot(t, float3(-0.0182, -0.1006, 1.1187))); + t709 = max(t709, 0.0); + // Extended Reinhard: 1.0 stays put, the 1000-nit knee lands at display white, above rolls off. + const float w = 1000.0/203.0; + float3 mapped = saturate(t709 * (1.0 + t709 / (w * w)) / (1.0 + t709)); + // BT.709 OETF — the same encoding the SDR stream arrives in, so both paths present alike. + float3 e = select(1.099 * pow(mapped, 0.45) - 0.099, 4.5 * mapped, mapped < 0.018); + return float4(e, 1.0); } """ @@ -144,12 +203,19 @@ public final class MetalVideoPresenter { /// frame in `render`; the layer is reconfigured to match when the session flips (HDR toggle). private let pipelineSDR: MTLRenderPipelineState private let pipelineHDR: MTLRenderPipelineState + /// tvOS only: the in-shader PQ→SDR tone-map fallback (pf_frag_hdr_tv → bgra8), used whenever + /// the display is composited without HDR headroom — see `setDisplayHeadroom`. nil elsewhere. + private let pipelineHDRToneMap: MTLRenderPipelineState? private var textureCache: CVMetalTextureCache? /// Current layer configuration — switched in `configure(hdr:)` when a frame's HDR-ness differs. /// Render-thread confined once the pipeline runs (Stage2Pipeline.start's one pre-thread /// `configure` call is ordered before the thread starts, so it doesn't race). private var hdrActive = false + /// tvOS only: whether HDR frames currently present as PQ PASSTHROUGH (display has HDR headroom + /// — its own tone-map applies) vs the in-shader tone-map fallback. Render-thread confined; + /// derived from the staged display headroom at the top of every `render`. + private var hdrPassthroughActive = false /// Last HDR mastering grade received via `setHdrMeta` (the host's 0xCE). Cached so a mid-session /// SDR→HDR flip's `configureColor` re-applies the real grade instead of clobbering it back to the /// bare reference-white anchor (an out-of-order race otherwise: `setHdrMeta` and the flip both write @@ -163,6 +229,11 @@ public final class MetalVideoPresenter { private let stagingLock = NSLock() private var pendingHdrMeta: PunktfunkConnection.HdrMeta? private var drawableTarget: CGSize = .zero + /// tvOS: the display's current EDR headroom (UIScreen.currentEDRHeadroom), pushed from the + /// main thread (SessionPresenter.layout + the mode-switch observers). > 1 ⇒ the display is + /// composited with HDR headroom, so HDR frames present as PQ passthrough; otherwise the + /// in-shader tone-map keeps the picture from blowing out. 1 (the default) is the safe start. + private var stagedDisplayHeadroom: CGFloat = 1.0 #if DEBUG /// Last logged "decoded→drawable" signature, so the diagnostic logs only on a size/HDR change. @@ -177,6 +248,7 @@ public final class MetalVideoPresenter { else { return nil } let pipelineSDR: MTLRenderPipelineState let pipelineHDR: MTLRenderPipelineState + let pipelineHDRToneMap: MTLRenderPipelineState? do { let library = try device.makeLibrary(source: shaderSource, options: nil) let vtx = library.makeFunction(name: "pf_vtx") @@ -188,8 +260,20 @@ public final class MetalVideoPresenter { let hdr = MTLRenderPipelineDescriptor() hdr.vertexFunction = vtx hdr.fragmentFunction = library.makeFunction(name: "pf_frag_hdr") - hdr.colorAttachments[0].pixelFormat = .rgba16Float // EDR-capable + hdr.colorAttachments[0].pixelFormat = .rgba16Float // PQ passthrough pipelineHDR = try device.makeRenderPipelineState(descriptor: hdr) + #if os(tvOS) + // tvOS carries BOTH HDR pipelines: PQ passthrough when the display is composited + // with HDR headroom, the in-shader tone-map (→ the 8-bit SDR config) when it isn't. + // See setDisplayHeadroom / configureColor. + let tm = MTLRenderPipelineDescriptor() + tm.vertexFunction = vtx + tm.fragmentFunction = library.makeFunction(name: "pf_frag_hdr_tv") + tm.colorAttachments[0].pixelFormat = .bgra8Unorm + pipelineHDRToneMap = try device.makeRenderPipelineState(descriptor: tm) + #else + pipelineHDRToneMap = nil + #endif } catch { return nil } @@ -229,17 +313,19 @@ public final class MetalVideoPresenter { return MetalVideoPresenter( device: device, queue: queue, pipelineSDR: pipelineSDR, pipelineHDR: pipelineHDR, - textureCache: textureCache, layer: layer) + pipelineHDRToneMap: pipelineHDRToneMap, textureCache: textureCache, layer: layer) } private init( device: MTLDevice, queue: MTLCommandQueue, pipelineSDR: MTLRenderPipelineState, - pipelineHDR: MTLRenderPipelineState, textureCache: CVMetalTextureCache, layer: CAMetalLayer + pipelineHDR: MTLRenderPipelineState, pipelineHDRToneMap: MTLRenderPipelineState?, + textureCache: CVMetalTextureCache, layer: CAMetalLayer ) { self.device = device self.queue = queue self.pipelineSDR = pipelineSDR self.pipelineHDR = pipelineHDR + self.pipelineHDRToneMap = pipelineHDRToneMap self.textureCache = textureCache self.layer = layer } @@ -251,30 +337,68 @@ public final class MetalVideoPresenter { /// an rgba16Float drawable + BT.2020 PQ colour space + EDR with a 203-nit reference-white anchor; /// SDR uses the plain 8-bit sRGB path. public func configure(hdr: Bool) { + #if os(tvOS) + // Reconfigure on an HDR flip AND on a passthrough↔tone-map flip: the display's headroom + // changes when the AVDisplayManager mode switch (requested at session start) completes — + // typically a second or two into the session. + stagingLock.lock() + let passthrough = stagedDisplayHeadroom > 1.0 + stagingLock.unlock() + guard hdr != hdrActive || (hdr && passthrough != hdrPassthroughActive) else { return } + hdrActive = hdr + hdrPassthroughActive = passthrough + #else guard hdr != hdrActive else { return } hdrActive = hdr + #endif configureColor(hdr: hdr) } + /// tvOS: park the display's current EDR headroom (a MAIN-thread `UIScreen` read — pushed by + /// SessionPresenter.layout and the stream view's mode-switch observers). > 1 flips HDR frames + /// to PQ passthrough (the display's own tone-map applies); ≤ 1 keeps the in-shader tone-map. + /// Applied by the render thread on the next frame, like every other staged value here. + public func setDisplayHeadroom(_ headroom: CGFloat) { + stagingLock.lock() + stagedDisplayHeadroom = headroom + stagingLock.unlock() + } + /// Set the layer's pixel format + colour config for SDR or HDR. MAIN THREAD ONLY. EDR is requested /// on macOS + iOS (the old `#if os(macOS)` guard left iOS EDR half-engaged). tvOS has NO EDR API - /// (`wantsExtendedDynamicRangeContent`/`edrMetadata`/`CAEDRMetadata` are all unavailable there), so - /// it gets the PQ pixel format + colour space only — the tvOS compositor tone-maps from those. + /// (`wantsExtendedDynamicRangeContent`/`edrMetadata`/`CAEDRMetadata` are all unavailable there) — + /// and a bare PQ colour-space tag composites UNtone-mapped (the "overblown HDR" Apple TV report), + /// so tvOS instead tone-maps PQ→SDR in the shader (pf_frag_hdr_tv) and keeps the SDR layer config. private func configureColor(hdr: Bool) { if hdr { + #if os(tvOS) + if hdrPassthroughActive { + // Display composited WITH HDR headroom (the session's AVDisplayManager request + // landed): emit PQ passthrough — in a real HDR10 output that's the correct + // emission, and the TV applies its own tone-map. + layer.pixelFormat = .rgba16Float + layer.colorspace = CGColorSpace(name: CGColorSpace.itur_2100_PQ) + } else { + // SDR-composited display: PQ would render untone-mapped (blown out) — the + // pf_frag_hdr_tv shader tone-maps to SDR instead. + layer.pixelFormat = .bgra8Unorm + layer.colorspace = nil + } + #else layer.pixelFormat = .rgba16Float layer.colorspace = CGColorSpace(name: CGColorSpace.itur_2100_PQ) - #if !os(tvOS) layer.wantsExtendedDynamicRangeContent = true // Anchor reference white. Re-apply the real grade if one already arrived (0xCE before the // flip); otherwise the bare 203-nit anchor. Without this anchor the PQ signal is too bright. layer.edrMetadata = makeEDR(lastHdrMeta) #endif } else { - // SDR: gamma-encoded BT.709 [0,1] in an 8-bit drawable; a nil colorspace tags it device/sRGB - // (the proven SDR path — never showed the "too bright" issue, which was HDR-only). + // SDR: gamma-encoded BT.709 [0,1] in an 8-bit drawable. Default: nil colorspace = NO + // colour matching on macOS (the panel's native primaries — the long-proven look, + // slightly oversaturated on P3 panels); PUNKTFUNK_SDR_COLORSPACE=srgb tags the layer + // for correct colour matching instead (A/B pending — see sdrColorspaceOverride). layer.pixelFormat = .bgra8Unorm - layer.colorspace = nil + layer.colorspace = sdrColorspaceOverride #if !os(tvOS) layer.wantsExtendedDynamicRangeContent = false layer.edrMetadata = nil @@ -360,6 +484,11 @@ public final class MetalVideoPresenter { || pf == kCVPixelFormatType_420YpCbCr10BiPlanarFullRange || pf == kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange || pf == kCVPixelFormatType_444YpCbCr10BiPlanarFullRange + // The frame's Y′CbCr→RGB rows, from its ACTUAL signaling (buffer attachments + pixel + // format) — a BT.601-signaled stream gets 601 coefficients, full-range gets full-range + // expansion; recomputed per frame because the host can flip colour in-band (SDR↔HDR). + var csc = CscRows.rows( + CscRows.signal(of: pixelBuffer), depth: tenBit ? 10 : 8, msbPacked: tenBit) guard let textureCache, let luma = makeTexture( pixelBuffer, plane: 0, format: tenBit ? .r16Unorm : .r8Unorm, cache: textureCache), @@ -395,9 +524,17 @@ public final class MetalVideoPresenter { guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: pass) else { return false } + #if os(tvOS) + // HDR splits by the display's headroom (kept in step with the layer by `configure` above): + // PQ passthrough into an HDR-composited display, the tone-map shader otherwise. + let hdrPipeline = hdrPassthroughActive ? pipelineHDR : (pipelineHDRToneMap ?? pipelineHDR) + encoder.setRenderPipelineState(hdrActive ? hdrPipeline : pipelineSDR) + #else encoder.setRenderPipelineState(hdrActive ? pipelineHDR : pipelineSDR) + #endif encoder.setFragmentTexture(CVMetalTextureGetTexture(luma), index: 0) encoder.setFragmentTexture(CVMetalTextureGetTexture(chroma), index: 1) + encoder.setFragmentBytes(&csc, length: MemoryLayout.stride, index: 0) encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) encoder.endEncoding() if let onPresented { diff --git a/clients/apple/Tests/PunktfunkKitTests/ColorBarDecodeTests.swift b/clients/apple/Tests/PunktfunkKitTests/ColorBarDecodeTests.swift new file mode 100644 index 00000000..8f25dea7 --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/ColorBarDecodeTests.swift @@ -0,0 +1,112 @@ +import CoreMedia +import CoreVideo +import VideoToolbox +import XCTest +import simd + +@testable import PunktfunkKit + +/// Golden end-to-end colour tests: decode the known-signaling bar fixtures through a real +/// `VTDecompressionSession`, read the buffer's propagated signaling via `CscRows.signal(of:)`, +/// convert sampled Y′CbCr through `CscRows.rows` — the exact math the Metal shaders run — and +/// require the ORIGINAL RGB bars back. This is the proof of the two assumptions the stage-2 +/// colour fix rests on: (1) VideoToolbox propagates the bitstream's matrix onto the decoded +/// CVPixelBuffer's attachments, and (2) signal+rows renders it correctly for BT.601/709 × +/// limited/full. A hardcoded-709 regression fails the 601 fixture by tens of code points. +final class ColorBarDecodeTests: XCTestCase { + private static let bars: [(r: Float, g: Float, b: Float)] = [ + (255, 255, 255), (255, 255, 0), (0, 255, 255), (0, 255, 0), + (255, 0, 255), (255, 0, 0), (0, 0, 255), (0, 0, 0), + ] + + /// Decode one fixture AU to a biplanar 4:2:0 buffer of the given range sibling. + private func decode(_ au: [UInt8], pixelFormat: OSType) throws -> CVPixelBuffer { + let data = Data(au) + guard let format = AnnexB.formatDescription(fromIDR: data, codec: .hevc) else { + throw XCTSkip("could not build a format description from the fixture") + } + let attrs: [CFString: Any] = [kCVPixelBufferPixelFormatTypeKey: pixelFormat] + var session: VTDecompressionSession? + let created = VTDecompressionSessionCreate( + allocator: kCFAllocatorDefault, formatDescription: format, + decoderSpecification: nil, imageBufferAttributes: attrs as CFDictionary, + outputCallback: nil, decompressionSessionOut: &session) + guard created == noErr, let session else { + throw XCTSkip("VTDecompressionSessionCreate failed (\(created))") + } + defer { VTDecompressionSessionInvalidate(session) } + let unit = AccessUnit(data: data, ptsNs: 0, frameIndex: 0, flags: 0, receivedNs: 0) + guard let sample = AnnexB.sampleBuffer(au: unit, format: format, codec: .hevc) else { + throw XCTSkip("could not build a sample buffer") + } + var produced: CVPixelBuffer? + let status = VTDecompressionSessionDecodeFrame( + session, sampleBuffer: sample, flags: [], infoFlagsOut: nil + ) { status, _, imageBuffer, _, _ in + if status == noErr { produced = imageBuffer } + } + XCTAssertEqual(status, noErr, "decode submit") + VTDecompressionSessionWaitForAsynchronousFrames(session) + return try XCTUnwrap(produced, "no decoded frame") + } + + private func assertBars( + _ name: String, au: [UInt8], pixelFormat: OSType, + expected: CscRows.Signal + ) throws { + let buffer = try decode(au, pixelFormat: pixelFormat) + let signal = CscRows.signal(of: buffer) + XCTAssertEqual(signal, expected, "\(name): VT must propagate the bitstream signaling") + + let rows = CscRows.rows(signal, depth: 8, msbPacked: false) + CVPixelBufferLockBaseAddress(buffer, .readOnly) + defer { CVPixelBufferUnlockBaseAddress(buffer, .readOnly) } + let yBase = try XCTUnwrap(CVPixelBufferGetBaseAddressOfPlane(buffer, 0)) + .assumingMemoryBound(to: UInt8.self) + let yStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 0) + let cBase = try XCTUnwrap(CVPixelBufferGetBaseAddressOfPlane(buffer, 1)) + .assumingMemoryBound(to: UInt8.self) + let cStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 1) + + for (i, bar) in Self.bars.enumerated() { + let (cx, cy) = (i * 32 + 16, 32) + let y = Float(yBase[cy * yStride + cx]) / 255.0 + let cb = Float(cBase[(cy / 2) * cStride + (cx / 2) * 2]) / 255.0 + let cr = Float(cBase[(cy / 2) * cStride + (cx / 2) * 2 + 1]) / 255.0 + let yuv = SIMD3(y, cb, cr) + let rgb = SIMD3( + simd_dot(SIMD3(rows.r0.x, rows.r0.y, rows.r0.z), yuv) + rows.r0.w, + simd_dot(SIMD3(rows.r1.x, rows.r1.y, rows.r1.z), yuv) + rows.r1.w, + simd_dot(SIMD3(rows.r2.x, rows.r2.y, rows.r2.z), yuv) + rows.r2.w) + XCTAssertEqual(rgb.x * 255, bar.r, accuracy: 3, "\(name) bar \(i) R") + XCTAssertEqual(rgb.y * 255, bar.g, accuracy: 3, "\(name) bar \(i) G") + XCTAssertEqual(rgb.z * 255, bar.b, accuracy: 3, "\(name) bar \(i) B") + } + } + + /// BT.601 (BT.470BG) limited — what a Linux host's RGB-input NVENC signals. The fixture that + /// catches a hardcoded-BT.709 shader. + func testGolden601LimitedBars() throws { + try assertBars( + "601-limited", au: ColorBarFixtures.bars601Limited, + pixelFormat: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + expected: .init(matrix: 5, fullRange: false)) + } + + /// BT.709 limited — the hosts' explicit SDR signaling. + func testGolden709LimitedBars() throws { + try assertBars( + "709-limited", au: ColorBarFixtures.bars709Limited, + pixelFormat: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + expected: .init(matrix: 1, fullRange: false)) + } + + /// BT.709 full range — the PUNKTFUNK_444_FULLRANGE experiment's signaling (requesting the + /// full-range sibling keeps VT from range-converting, so the full-range rows are exercised). + func testGolden709FullBars() throws { + try assertBars( + "709-full", au: ColorBarFixtures.bars709Full, + pixelFormat: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + expected: .init(matrix: 1, fullRange: true)) + } +} diff --git a/clients/apple/Tests/PunktfunkKitTests/ColorBarFixtures.swift b/clients/apple/Tests/PunktfunkKitTests/ColorBarFixtures.swift new file mode 100644 index 00000000..2ff647d6 --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/ColorBarFixtures.swift @@ -0,0 +1,864 @@ +// Golden colour-bar fixtures — the SAME bytes as crates/pf-client-core/tests/bars-*.h265 +// (one 256×64 LOSSLESS x265 IDR of 8 saturated bars per signaling variant; generated +// offline with ffmpeg/libx265, RGB→YUV matched to the declared VUI so the original RGB +// is recoverable ±1 code). Regenerate both together — the Rust and Swift golden tests +// must chew identical streams. Test-target only; nothing here ships. + +enum ColorBarFixtures { + static let bars601Limited: [UInt8] = [ + 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0xff, 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, + 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, + 0x00, 0xff, 0xa0, 0x08, 0x08, 0x10, 0x59, 0x65, 0x66, 0x92, 0x4c, 0xae, 0x6a, 0x02, 0x02, 0x0a, + 0x08, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x00, 0xc8, 0x40, 0x00, 0x00, 0x00, 0x01, + 0x44, 0x01, 0xc1, 0x71, 0xa9, 0x12, 0x00, 0x00, 0x01, 0x4e, 0x01, 0x05, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xd1, 0x2c, 0xa2, 0xde, 0x09, 0xb5, 0x17, 0x47, 0xdb, 0xbb, 0x55, 0xa4, + 0xfe, 0x7f, 0xc2, 0xfc, 0x4e, 0x78, 0x32, 0x36, 0x35, 0x20, 0x28, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x20, 0x32, 0x31, 0x36, 0x29, 0x20, 0x2d, 0x20, 0x34, 0x2e, 0x32, 0x2b, 0x31, 0x2d, 0x65, 0x34, + 0x34, 0x34, 0x37, 0x34, 0x34, 0x3a, 0x5b, 0x4d, 0x61, 0x63, 0x20, 0x4f, 0x53, 0x20, 0x58, 0x5d, + 0x5b, 0x63, 0x6c, 0x61, 0x6e, 0x67, 0x20, 0x32, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x5d, 0x5b, 0x36, + 0x34, 0x20, 0x62, 0x69, 0x74, 0x5d, 0x20, 0x38, 0x62, 0x69, 0x74, 0x2b, 0x31, 0x30, 0x62, 0x69, + 0x74, 0x2b, 0x31, 0x32, 0x62, 0x69, 0x74, 0x20, 0x2d, 0x20, 0x48, 0x2e, 0x32, 0x36, 0x35, 0x2f, + 0x48, 0x45, 0x56, 0x43, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x63, 0x20, 0x2d, 0x20, 0x43, 0x6f, 0x70, + 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x32, 0x30, 0x31, 0x33, 0x2d, 0x32, 0x30, 0x31, 0x38, + 0x20, 0x28, 0x63, 0x29, 0x20, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x63, 0x6f, 0x72, 0x65, 0x77, 0x61, + 0x72, 0x65, 0x2c, 0x20, 0x49, 0x6e, 0x63, 0x20, 0x2d, 0x20, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, + 0x2f, 0x78, 0x32, 0x36, 0x35, 0x2e, 0x6f, 0x72, 0x67, 0x20, 0x2d, 0x20, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x3a, 0x20, 0x63, 0x70, 0x75, 0x69, 0x64, 0x3d, 0x39, 0x38, 0x20, 0x66, 0x72, + 0x61, 0x6d, 0x65, 0x2d, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x3d, 0x31, 0x20, 0x6e, 0x6f, + 0x2d, 0x77, 0x70, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x70, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x6e, 0x6f, + 0x2d, 0x70, 0x6d, 0x65, 0x20, 0x6e, 0x6f, 0x2d, 0x70, 0x73, 0x6e, 0x72, 0x20, 0x6e, 0x6f, 0x2d, + 0x73, 0x73, 0x69, 0x6d, 0x20, 0x6c, 0x6f, 0x67, 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, + 0x20, 0x62, 0x69, 0x74, 0x64, 0x65, 0x70, 0x74, 0x68, 0x3d, 0x38, 0x20, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x2d, 0x63, 0x73, 0x70, 0x3d, 0x31, 0x20, 0x66, 0x70, 0x73, 0x3d, 0x32, 0x35, 0x2f, 0x31, + 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2d, 0x72, 0x65, 0x73, 0x3d, 0x32, 0x35, 0x36, 0x78, 0x36, + 0x34, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6c, 0x61, 0x63, 0x65, 0x3d, 0x30, 0x20, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x2d, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x3d, 0x30, 0x20, 0x6c, 0x65, 0x76, + 0x65, 0x6c, 0x2d, 0x69, 0x64, 0x63, 0x3d, 0x30, 0x20, 0x68, 0x69, 0x67, 0x68, 0x2d, 0x74, 0x69, + 0x65, 0x72, 0x3d, 0x31, 0x20, 0x75, 0x68, 0x64, 0x2d, 0x62, 0x64, 0x3d, 0x30, 0x20, 0x72, 0x65, + 0x66, 0x3d, 0x33, 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x2d, 0x6e, 0x6f, 0x6e, + 0x2d, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x20, 0x72, 0x65, 0x70, + 0x65, 0x61, 0x74, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x20, 0x61, 0x6e, 0x6e, 0x65, + 0x78, 0x62, 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x75, 0x64, 0x20, 0x6e, 0x6f, 0x2d, 0x65, 0x6f, 0x62, + 0x20, 0x6e, 0x6f, 0x2d, 0x65, 0x6f, 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x72, 0x64, 0x20, 0x69, + 0x6e, 0x66, 0x6f, 0x20, 0x68, 0x61, 0x73, 0x68, 0x3d, 0x30, 0x20, 0x74, 0x65, 0x6d, 0x70, 0x6f, + 0x72, 0x61, 0x6c, 0x2d, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x3d, 0x30, 0x20, 0x6f, 0x70, 0x65, + 0x6e, 0x2d, 0x67, 0x6f, 0x70, 0x20, 0x6d, 0x69, 0x6e, 0x2d, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, + 0x3d, 0x32, 0x35, 0x20, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, 0x3d, 0x32, 0x35, 0x30, 0x20, 0x67, + 0x6f, 0x70, 0x2d, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x3d, 0x30, 0x20, 0x62, + 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x3d, 0x34, 0x20, 0x62, 0x2d, 0x61, 0x64, 0x61, 0x70, 0x74, + 0x3d, 0x32, 0x20, 0x62, 0x2d, 0x70, 0x79, 0x72, 0x61, 0x6d, 0x69, 0x64, 0x20, 0x62, 0x66, 0x72, + 0x61, 0x6d, 0x65, 0x2d, 0x62, 0x69, 0x61, 0x73, 0x3d, 0x30, 0x20, 0x72, 0x63, 0x2d, 0x6c, 0x6f, + 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x3d, 0x32, 0x30, 0x20, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, + 0x68, 0x65, 0x61, 0x64, 0x2d, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x73, 0x3d, 0x30, 0x20, 0x73, 0x63, + 0x65, 0x6e, 0x65, 0x63, 0x75, 0x74, 0x3d, 0x34, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x69, 0x73, + 0x74, 0x2d, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x63, 0x75, 0x74, 0x20, 0x72, 0x61, 0x64, 0x6c, 0x3d, + 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x70, 0x6c, 0x69, 0x63, 0x65, 0x20, 0x6e, 0x6f, 0x2d, 0x69, + 0x6e, 0x74, 0x72, 0x61, 0x2d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x20, 0x63, 0x74, 0x75, + 0x3d, 0x36, 0x34, 0x20, 0x6d, 0x69, 0x6e, 0x2d, 0x63, 0x75, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3d, + 0x38, 0x20, 0x6e, 0x6f, 0x2d, 0x72, 0x65, 0x63, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x6d, 0x70, + 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x74, 0x75, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3d, 0x33, 0x32, 0x20, + 0x74, 0x75, 0x2d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x2d, 0x64, 0x65, 0x70, 0x74, 0x68, 0x3d, 0x31, + 0x20, 0x74, 0x75, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x2d, 0x64, 0x65, 0x70, 0x74, 0x68, 0x3d, + 0x31, 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x74, 0x75, 0x3d, 0x30, 0x20, 0x72, 0x64, 0x6f, + 0x71, 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x2d, 0x72, 0x64, 0x3d, 0x30, 0x2e, 0x30, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x73, 0x69, + 0x6d, 0x2d, 0x72, 0x64, 0x20, 0x73, 0x69, 0x67, 0x6e, 0x68, 0x69, 0x64, 0x65, 0x20, 0x6e, 0x6f, + 0x2d, 0x74, 0x73, 0x6b, 0x69, 0x70, 0x20, 0x6e, 0x72, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x3d, + 0x30, 0x20, 0x6e, 0x72, 0x2d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, + 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x64, 0x2d, 0x69, 0x6e, 0x74, 0x72, + 0x61, 0x20, 0x73, 0x74, 0x72, 0x6f, 0x6e, 0x67, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x2d, 0x73, + 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x6d, 0x65, 0x72, + 0x67, 0x65, 0x3d, 0x33, 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x72, 0x65, 0x66, 0x73, 0x3d, + 0x31, 0x20, 0x6e, 0x6f, 0x2d, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x6d, 0x6f, 0x64, 0x65, 0x73, + 0x20, 0x6d, 0x65, 0x3d, 0x31, 0x20, 0x73, 0x75, 0x62, 0x6d, 0x65, 0x3d, 0x32, 0x20, 0x6d, 0x65, + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x3d, 0x35, 0x37, 0x20, 0x74, 0x65, 0x6d, 0x70, 0x6f, 0x72, 0x61, + 0x6c, 0x2d, 0x6d, 0x76, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x2d, 0x64, + 0x75, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x6d, 0x65, 0x20, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, + 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x62, 0x20, 0x6e, 0x6f, 0x2d, + 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x7a, 0x65, 0x2d, 0x73, 0x72, 0x63, 0x2d, 0x70, 0x69, 0x63, 0x73, + 0x20, 0x64, 0x65, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x3d, 0x30, 0x3a, 0x30, 0x20, 0x73, 0x61, 0x6f, + 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x61, 0x6f, 0x2d, 0x6e, 0x6f, 0x6e, 0x2d, 0x64, 0x65, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x20, 0x72, 0x64, 0x3d, 0x33, 0x20, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x2d, 0x73, 0x61, 0x6f, 0x3d, 0x34, 0x20, 0x65, 0x61, 0x72, 0x6c, 0x79, 0x2d, 0x73, + 0x6b, 0x69, 0x70, 0x20, 0x72, 0x73, 0x6b, 0x69, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x61, 0x73, + 0x74, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x20, 0x6e, 0x6f, 0x2d, 0x74, 0x73, 0x6b, 0x69, 0x70, + 0x2d, 0x66, 0x61, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x63, 0x75, 0x2d, 0x6c, 0x6f, 0x73, 0x73, + 0x6c, 0x65, 0x73, 0x73, 0x20, 0x62, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x20, 0x6e, 0x6f, 0x2d, + 0x73, 0x70, 0x6c, 0x69, 0x74, 0x72, 0x64, 0x2d, 0x73, 0x6b, 0x69, 0x70, 0x20, 0x72, 0x64, 0x70, + 0x65, 0x6e, 0x61, 0x6c, 0x74, 0x79, 0x3d, 0x30, 0x20, 0x70, 0x73, 0x79, 0x2d, 0x72, 0x64, 0x3d, + 0x32, 0x2e, 0x30, 0x30, 0x20, 0x70, 0x73, 0x79, 0x2d, 0x72, 0x64, 0x6f, 0x71, 0x3d, 0x30, 0x2e, + 0x30, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x72, 0x64, 0x2d, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x20, + 0x6c, 0x6f, 0x73, 0x73, 0x6c, 0x65, 0x73, 0x73, 0x20, 0x63, 0x62, 0x71, 0x70, 0x6f, 0x66, 0x66, + 0x73, 0x3d, 0x30, 0x20, 0x63, 0x72, 0x71, 0x70, 0x6f, 0x66, 0x66, 0x73, 0x3d, 0x30, 0x20, 0x72, + 0x63, 0x3d, 0x63, 0x71, 0x70, 0x20, 0x71, 0x70, 0x3d, 0x34, 0x20, 0x69, 0x70, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x3d, 0x31, 0x2e, 0x34, 0x30, 0x20, 0x70, 0x62, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x3d, + 0x31, 0x2e, 0x33, 0x30, 0x20, 0x61, 0x71, 0x2d, 0x6d, 0x6f, 0x64, 0x65, 0x3d, 0x30, 0x20, 0x61, + 0x71, 0x2d, 0x73, 0x74, 0x72, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x3d, 0x30, 0x2e, 0x30, 0x30, 0x20, + 0x6e, 0x6f, 0x2d, 0x63, 0x75, 0x74, 0x72, 0x65, 0x65, 0x20, 0x7a, 0x6f, 0x6e, 0x65, 0x2d, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, + 0x2d, 0x63, 0x62, 0x72, 0x20, 0x71, 0x67, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3d, 0x36, 0x34, 0x20, + 0x6e, 0x6f, 0x2d, 0x72, 0x63, 0x2d, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x20, 0x71, 0x70, 0x6d, 0x61, + 0x78, 0x3d, 0x36, 0x39, 0x20, 0x71, 0x70, 0x6d, 0x69, 0x6e, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, + 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x2d, 0x76, 0x62, 0x76, 0x20, 0x73, 0x61, 0x72, 0x3d, 0x30, 0x20, + 0x6f, 0x76, 0x65, 0x72, 0x73, 0x63, 0x61, 0x6e, 0x3d, 0x30, 0x20, 0x76, 0x69, 0x64, 0x65, 0x6f, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x3d, 0x35, 0x20, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x3d, 0x30, + 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x70, 0x72, 0x69, 0x6d, 0x3d, 0x31, 0x20, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x66, 0x65, 0x72, 0x3d, 0x31, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x72, 0x69, 0x78, 0x3d, 0x35, 0x20, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x61, 0x6c, 0x6f, 0x63, 0x3d, + 0x30, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x2d, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, + 0x3d, 0x30, 0x20, 0x63, 0x6c, 0x6c, 0x3d, 0x30, 0x2c, 0x30, 0x20, 0x6d, 0x69, 0x6e, 0x2d, 0x6c, + 0x75, 0x6d, 0x61, 0x3d, 0x30, 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x6c, 0x75, 0x6d, 0x61, 0x3d, 0x32, + 0x35, 0x35, 0x20, 0x6c, 0x6f, 0x67, 0x32, 0x2d, 0x6d, 0x61, 0x78, 0x2d, 0x70, 0x6f, 0x63, 0x2d, + 0x6c, 0x73, 0x62, 0x3d, 0x38, 0x20, 0x76, 0x75, 0x69, 0x2d, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, + 0x2d, 0x69, 0x6e, 0x66, 0x6f, 0x20, 0x76, 0x75, 0x69, 0x2d, 0x68, 0x72, 0x64, 0x2d, 0x69, 0x6e, + 0x66, 0x6f, 0x20, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x73, 0x3d, 0x31, 0x20, 0x6e, 0x6f, 0x2d, 0x6f, + 0x70, 0x74, 0x2d, 0x71, 0x70, 0x2d, 0x70, 0x70, 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x6f, 0x70, 0x74, + 0x2d, 0x72, 0x65, 0x66, 0x2d, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, + 0x2d, 0x70, 0x70, 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x2d, 0x70, 0x61, + 0x73, 0x73, 0x2d, 0x6f, 0x70, 0x74, 0x2d, 0x72, 0x70, 0x73, 0x20, 0x73, 0x63, 0x65, 0x6e, 0x65, + 0x63, 0x75, 0x74, 0x2d, 0x62, 0x69, 0x61, 0x73, 0x3d, 0x30, 0x2e, 0x30, 0x35, 0x20, 0x6e, 0x6f, + 0x2d, 0x6f, 0x70, 0x74, 0x2d, 0x63, 0x75, 0x2d, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x2d, 0x71, 0x70, + 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x71, 0x2d, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x6f, + 0x2d, 0x68, 0x64, 0x72, 0x31, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x64, 0x72, 0x31, 0x30, 0x2d, + 0x6f, 0x70, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x64, 0x68, 0x64, 0x72, 0x31, 0x30, 0x2d, 0x6f, 0x70, + 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x69, 0x64, 0x72, 0x2d, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, + 0x79, 0x2d, 0x73, 0x65, 0x69, 0x20, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2d, 0x72, + 0x65, 0x75, 0x73, 0x65, 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x61, 0x6e, 0x61, + 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2d, 0x73, 0x61, 0x76, 0x65, 0x2d, 0x72, 0x65, 0x75, 0x73, 0x65, + 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, + 0x73, 0x2d, 0x6c, 0x6f, 0x61, 0x64, 0x2d, 0x72, 0x65, 0x75, 0x73, 0x65, 0x2d, 0x6c, 0x65, 0x76, + 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2d, 0x66, 0x61, 0x63, 0x74, 0x6f, + 0x72, 0x3d, 0x30, 0x20, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, + 0x3d, 0x30, 0x20, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3d, + 0x30, 0x20, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x6d, 0x76, 0x3d, 0x31, 0x20, 0x72, 0x65, + 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x63, 0x74, 0x75, 0x2d, 0x64, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x74, + 0x69, 0x6f, 0x6e, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x73, + 0x61, 0x6f, 0x20, 0x63, 0x74, 0x75, 0x2d, 0x69, 0x6e, 0x66, 0x6f, 0x3d, 0x30, 0x20, 0x6e, 0x6f, + 0x2d, 0x6c, 0x6f, 0x77, 0x70, 0x61, 0x73, 0x73, 0x2d, 0x64, 0x63, 0x74, 0x20, 0x72, 0x65, 0x66, + 0x69, 0x6e, 0x65, 0x2d, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2d, 0x74, 0x79, 0x70, + 0x65, 0x3d, 0x30, 0x20, 0x63, 0x6f, 0x70, 0x79, 0x2d, 0x70, 0x69, 0x63, 0x3d, 0x31, 0x20, 0x6d, + 0x61, 0x78, 0x2d, 0x61, 0x75, 0x73, 0x69, 0x7a, 0x65, 0x2d, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x3d, 0x31, 0x2e, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x2d, + 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, + 0x2d, 0x73, 0x65, 0x69, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x65, 0x76, 0x63, 0x2d, 0x61, 0x71, 0x20, + 0x6e, 0x6f, 0x2d, 0x73, 0x76, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, + 0x71, 0x70, 0x2d, 0x61, 0x64, 0x61, 0x70, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x72, 0x61, + 0x6e, 0x67, 0x65, 0x3d, 0x31, 0x2e, 0x30, 0x30, 0x20, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x63, 0x75, + 0x74, 0x2d, 0x61, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x71, 0x70, 0x3d, 0x30, 0x63, 0x6f, 0x6e, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2d, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2d, 0x6f, + 0x66, 0x66, 0x73, 0x65, 0x74, 0x73, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x3d, 0x30, 0x20, 0x62, + 0x6f, 0x74, 0x74, 0x6f, 0x6d, 0x3d, 0x30, 0x20, 0x64, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2d, + 0x6d, 0x61, 0x78, 0x2d, 0x72, 0x61, 0x74, 0x65, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x76, 0x62, + 0x76, 0x2d, 0x6c, 0x69, 0x76, 0x65, 0x2d, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x2d, 0x70, 0x61, 0x73, + 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x6d, 0x63, 0x73, 0x74, 0x66, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x62, + 0x72, 0x63, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x2d, 0x72, 0x63, 0x80, 0x00, + 0x00, 0x01, 0x28, 0x01, 0xaf, 0x05, 0xb8, 0x10, 0x0c, 0x7f, 0x80, 0xd7, 0x00, 0xc4, 0xbb, 0x82, + 0x75, 0x79, 0xbd, 0x3c, 0x5f, 0x14, 0xb1, 0x4b, 0x14, 0xb1, 0x4b, 0x17, 0xc5, 0x7c, 0x57, 0xc5, + 0x7c, 0x57, 0xc5, 0x7c, 0x57, 0xc5, 0x7d, 0xb4, 0xcb, 0x51, 0xc8, 0xd6, 0xa0, 0x35, 0xd5, 0x55, + 0xa4, 0x40, 0xd9, 0x57, 0x3f, 0xff, 0xb5, 0xb6, 0x5d, 0x34, 0x79, 0xe7, 0x9e, 0x7f, 0x7d, 0xf7, + 0xdf, 0x7d, 0xf8, 0x18, 0x53, 0xff, 0xfe, 0xc3, 0xa1, 0x01, 0xf1, 0xbc, 0xa9, 0x03, 0x52, 0x4f, + 0xc2, 0x91, 0xef, 0xff, 0xf9, 0x13, 0xb5, 0xff, 0xff, 0xe0, 0xd3, 0xc0, 0xe8, 0xf7, 0xbe, 0xff, + 0x98, 0x39, 0x83, 0x98, 0x39, 0x83, 0x99, 0xb5, 0x9b, 0x59, 0xb5, 0x9b, 0x59, 0xb5, 0x9b, 0x59, + 0xb5, 0x88, 0xe7, 0xff, 0xb1, 0xee, 0x80, 0x1f, 0xaf, 0xd3, 0xc3, 0x0c, 0x30, 0xc3, 0x0c, 0x71, + 0xc7, 0x1c, 0x71, 0xc7, 0x16, 0x7a, 0x50, 0x01, 0xc9, 0xff, 0xf9, 0x43, 0x06, 0x41, 0x75, 0x47, + 0xb1, 0xe7, 0x71, 0x8e, 0x4e, 0x9d, 0x1f, 0xff, 0xce, 0x91, 0x28, 0x4d, 0xc5, 0x7a, 0x68, 0x84, + 0xdc, 0x76, 0x6c, 0xb4, 0xb3, 0x45, 0xc9, 0xef, 0xff, 0xcb, 0x9f, 0x2b, 0x6b, 0x50, 0x63, 0xb0, + 0x1f, 0xe0, 0x49, 0xe5, 0x74, 0x7f, 0xff, 0x3a, 0x44, 0xa1, 0x37, 0x15, 0xe9, 0xa2, 0x13, 0x71, + 0xd9, 0xaa, 0x79, 0xa3, 0xe3, 0xff, 0xf5, 0x23, 0x05, 0x46, 0x2f, 0x61, 0x97, 0x0a, 0x0e, 0x16, + 0x36, 0x94, 0x4f, 0xff, 0xb3, 0x7f, 0x1f, 0xe6, 0x13, 0x25, 0x0f, 0x78, 0x49, 0x00, 0x63, 0xf9, + 0x58, 0x64, 0x83, 0x85, 0x00, 0x07, 0x98, 0xce, 0xcb, 0x42, 0xb2, 0x35, 0x1d, 0xfa, 0xbf, 0xc6, + 0x85, 0xe6, 0x00, 0x48, 0xff, 0xfe, 0xed, 0x0e, 0xf8, 0x8b, 0xfc, 0x55, 0xed, 0x8d, 0xa0, 0xbd, + 0xf6, 0xdf, 0xff, 0xc7, 0xf9, 0xd8, 0x58, 0x24, 0x13, 0x43, 0x41, 0x3e, 0x1f, 0xc8, 0x40, 0x1c, + 0x5f, 0xff, 0x95, 0x21, 0x6f, 0x83, 0x90, 0x1c, 0x23, 0xc9, 0xe6, 0x5e, 0xdb, 0xd7, 0xff, 0xde, + 0xb1, 0x0b, 0xeb, 0x42, 0x9f, 0xe3, 0xae, 0xdb, 0x41, 0x26, 0x3b, 0x04, 0x56, 0x7f, 0xfe, 0xc9, + 0xf5, 0x58, 0x41, 0xd2, 0xa9, 0xeb, 0x7f, 0xd7, 0x3b, 0xa2, 0xef, 0xff, 0xde, 0x56, 0xa7, 0xa2, + 0xac, 0x43, 0x81, 0xaf, 0xce, 0x33, 0x5c, 0x41, 0xbf, 0x1f, 0xff, 0x5d, 0xee, 0xcb, 0x39, 0xae, + 0x1e, 0xa3, 0xd4, 0x47, 0xc7, 0xe9, 0x47, 0xff, 0xeb, 0x2a, 0xe3, 0x45, 0x58, 0xb5, 0x6a, 0x37, + 0xfd, 0xd2, 0xcf, 0xfa, 0x80, 0xa2, 0x8c, 0xff, 0xff, 0xf0, 0x0f, 0x7e, 0x56, 0xf1, 0x4e, 0x9d, + 0x3a, 0x74, 0xf2, 0x64, 0xc9, 0x93, 0x26, 0x4c, 0x98, 0x43, 0xff, 0xff, 0x77, 0x88, 0xb7, 0xf6, + 0x16, 0xba, 0x3e, 0xa1, 0xc4, 0xf8, 0x98, 0xc5, 0xc5, 0x67, 0xf6, 0xa6, 0x20, 0x0b, 0x3d, 0x62, + 0x9e, 0xaf, 0xe6, 0xaa, 0x45, 0x3e, 0x02, 0xd0, 0x2d, 0x02, 0xd0, 0x2d, 0x02, 0xfd, 0x2f, 0xd2, + 0xfd, 0x2f, 0xd2, 0xfd, 0x2f, 0xd2, 0xfd, 0x2d, 0x3d, 0xfe, 0x6b, 0xf7, 0xff, 0xff, 0x04, 0x5e, + 0x01, 0x17, 0x8a, 0x34, 0xae, 0x56, 0xe5, 0x6e, 0x56, 0xe5, 0x6e, 0x66, 0x26, 0x62, 0x66, 0x26, + 0x62, 0x66, 0x26, 0x62, 0x66, 0x25, 0xe8, 0x81, 0xb7, 0xff, 0xfc, 0x8c, 0xfc, 0x2d, 0xdd, 0xfc, + 0x78, 0xe0, 0x8f, 0x36, 0xc1, 0x09, 0xb5, 0x10, 0xff, 0xff, 0x05, 0x24, 0xbb, 0x93, 0x6d, 0x8b, + 0x11, 0x3f, 0xf5, 0x5f, 0x91, 0x4d, 0xe8, 0x34, 0x32, 0x13, 0xff, 0xf2, 0xd0, 0x8e, 0xa4, 0x10, + 0x3b, 0x17, 0x3d, 0x0d, 0xf0, 0x64, 0x88, 0x64, 0xd4, 0xff, 0xfd, 0xe4, 0x28, 0x23, 0x05, 0xf8, + 0x06, 0x68, 0xb4, 0xa5, 0x8c, 0x20, 0x1e, 0x54, 0x22, 0xf3, 0xff, 0xf6, 0x32, 0x77, 0x6d, 0x34, + 0xce, 0xe5, 0x7e, 0xdf, 0xcc, 0x43, 0x72, 0x2c, 0xdf, 0x7f, 0xfe, 0x7f, 0xb0, 0x15, 0x20, 0x52, + 0xd4, 0x06, 0xf8, 0x83, 0x2d, 0x08, 0x97, 0x2f, 0x08, 0x3d, 0x3d, 0x5f, 0xff, 0xe6, 0x0a, 0xa2, + 0x05, 0xf9, 0x24, 0x92, 0x4b, 0x18, 0x61, 0x86, 0x18, 0x61, 0x88, 0x6f, 0xff, 0xd4, 0xec, 0x35, + 0x68, 0x57, 0xf9, 0x4c, 0x73, 0xc7, 0x6e, 0x6d, 0x63, 0x89, 0x93, 0xff, 0xff, 0x6e, 0x4e, 0xd9, + 0x23, 0x2c, 0xf5, 0xed, 0xef, 0xde, 0xfd, 0xef, 0xde, 0xfd, 0xf6, 0xcf, 0x6c, 0xf6, 0xcf, 0x6c, + 0xf6, 0xcf, 0x6c, 0xf6, 0xcf, 0x01, 0x1b, 0x12, 0xc0, 0x18, 0x22, 0xee, 0x4d, 0x92, 0x49, 0x24, + 0x92, 0x7a, 0xaa, 0xaa, 0xaa, 0xaa, 0xa7, 0xd0, 0x1a, 0x5f, 0xff, 0x9c, 0xa9, 0xca, 0xe5, 0x6d, + 0x24, 0x4e, 0x60, 0x52, 0x2f, 0xa6, 0x7f, 0xff, 0xb0, 0xe8, 0x40, 0x7c, 0x6f, 0x2a, 0x40, 0xd4, + 0x93, 0xf0, 0xa6, 0xa6, 0x14, 0x55, 0xff, 0xf3, 0x76, 0x99, 0x75, 0xaa, 0xeb, 0x16, 0xc8, 0x7d, + 0x93, 0x68, 0x60, 0xff, 0xfd, 0x65, 0x5c, 0x68, 0xab, 0x16, 0xad, 0x46, 0xff, 0xba, 0x5a, 0x12, + 0x2c, 0x9f, 0xff, 0xe5, 0x39, 0x42, 0xf7, 0xb8, 0x97, 0xfb, 0xe6, 0xf7, 0xd3, 0x1e, 0x27, 0xff, + 0xf9, 0xc3, 0x14, 0x52, 0xf4, 0xf5, 0x3c, 0xbb, 0x6e, 0xb7, 0x2b, 0xbb, 0x80, 0x5e, 0xbf, 0xff, + 0x41, 0xd9, 0xcb, 0x9f, 0x5d, 0xc9, 0x28, 0x36, 0x1c, 0xcf, 0x18, 0xa2, 0xd9, 0xdf, 0xff, 0xa7, + 0xf3, 0x09, 0x84, 0xe6, 0x31, 0x94, 0xa5, 0x8d, 0xad, 0x93, 0x83, 0x8c, 0x01, 0x58, 0xff, 0xfd, + 0x4b, 0xa2, 0xd2, 0x82, 0x6b, 0xc7, 0x81, 0x87, 0x90, 0xc0, 0x16, 0x28, 0x4c, 0x7f, 0xfe, 0x95, + 0xc3, 0x60, 0xc7, 0x92, 0xea, 0x9a, 0xf4, 0xee, 0x9b, 0x68, 0x59, 0x04, 0x74, 0xec, 0x80, 0x0f, + 0x41, 0x5c, 0xee, 0x30, 0xf1, 0xc6, 0xe8, 0x40, 0x12, 0xed, 0xb5, 0x55, 0xff, 0xfc, 0x11, 0xc8, + 0x03, 0x4d, 0x33, 0x0f, 0x22, 0xec, 0x22, 0xdf, 0x56, 0x44, 0xb6, 0x7f, 0xff, 0x12, 0x30, 0x22, + 0x76, 0x41, 0xf5, 0x7f, 0xf8, 0x06, 0x4f, 0x55, 0x86, 0x31, 0x58, 0xa3, 0xff, 0xf4, 0xf1, 0xc2, + 0x65, 0x14, 0x4c, 0x4d, 0x40, 0x92, 0x12, 0x88, 0x96, 0xdb, 0xc4, 0xff, 0xfd, 0x33, 0x8d, 0xc5, + 0xcb, 0xf7, 0x51, 0xc8, 0xd1, 0x4a, 0x19, 0x22, 0x0f, 0x81, 0xee, 0x95, 0xff, 0xff, 0x9c, 0x31, + 0x45, 0x2f, 0x4f, 0x53, 0xcb, 0xb6, 0xeb, 0x72, 0xca, 0xaf, 0xff, 0x9b, 0x7c, 0x7c, 0x9e, 0x22, + 0x9b, 0x07, 0xb8, 0xd1, 0x57, 0x20, 0x17, 0x9d, 0xff, 0xf8, 0x24, 0xc8, 0xb0, 0xee, 0x78, 0x7b, + 0x95, 0x1d, 0x8d, 0x56, 0x89, 0xff, 0xf8, 0x15, 0xb7, 0xfa, 0x2a, 0xbe, 0x6b, 0x3f, 0xf0, 0xb5, + 0xb3, 0x63, 0x74, 0x55, 0xff, 0xfc, 0xcc, 0xbd, 0x0e, 0x72, 0xf9, 0x76, 0x87, 0x1d, 0x0e, 0xd9, + 0x12, 0x9f, 0xff, 0x32, 0x43, 0xc0, 0x1f, 0x1b, 0x10, 0xf4, 0xd3, 0x9f, 0x4b, 0xd2, 0x6b, 0x2f, + 0xff, 0xef, 0xcb, 0x14, 0x52, 0x91, 0x69, 0x76, 0xc8, 0x6d, 0x94, 0x00, 0x34, 0xff, 0xfb, 0xec, + 0xba, 0x8f, 0x8d, 0xeb, 0x14, 0xf5, 0x9e, 0xab, 0x96, 0xf0, 0xd9, 0x4f, 0x39, 0x0f, 0xff, 0xf8, + 0x50, 0x81, 0x7f, 0x73, 0xb2, 0xe5, 0xcb, 0x97, 0x32, 0x6c, 0xd9, 0xb3, 0x66, 0xcd, 0x9b, 0x61, + 0x7f, 0xff, 0x74, 0x86, 0x0d, 0xaf, 0x45, 0xff, 0x85, 0x5a, 0xa4, 0x21, 0x8d, 0xfd, 0x18, 0x3a, + 0x85, 0x00, 0x5d, 0x6a, 0x15, 0xf4, 0xff, 0x44, 0xd0, 0x78, 0xe5, 0xaf, 0x7a, 0xf7, 0xaf, 0x7a, + 0xf7, 0xb0, 0xf1, 0x0f, 0x10, 0xf1, 0x0f, 0x10, 0xf1, 0x0f, 0x10, 0xf0, 0xf8, 0x37, 0xae, 0xc0, + 0x38, 0xb4, 0xea, 0x7a, 0x6e, 0x22, 0x51, 0xbc, 0x31, 0xdd, 0x3b, 0x74, 0xed, 0xd3, 0xb7, 0x4e, + 0xdd, 0x74, 0x35, 0xd0, 0xd7, 0x43, 0x5d, 0x0d, 0x74, 0x35, 0xd0, 0xd7, 0x43, 0x56, 0xc8, 0x91, + 0x7f, 0xff, 0xa1, 0xad, 0x88, 0x55, 0x61, 0x10, 0x40, 0x65, 0xd0, 0x9b, 0x4b, 0xc3, 0x5b, 0x8f, + 0xff, 0xd0, 0xb6, 0xa8, 0x17, 0xbd, 0x41, 0xad, 0xe6, 0xc9, 0x9a, 0x1c, 0x1c, 0x1b, 0x5d, 0xa6, + 0x97, 0xff, 0xf4, 0x29, 0x51, 0x3f, 0xff, 0xfa, 0xae, 0x19, 0xa1, 0x7e, 0x73, 0xcd, 0xc7, 0xfa, + 0xff, 0xfd, 0x07, 0x66, 0xff, 0x1d, 0x6b, 0x4c, 0x94, 0xe8, 0xc3, 0x30, 0x89, 0x10, 0xd4, 0x81, + 0x17, 0xff, 0xe4, 0x00, 0x31, 0xf0, 0xc2, 0xff, 0x9b, 0xc2, 0xb6, 0xd5, 0xbc, 0x8d, 0x4a, 0xbf, + 0xff, 0xf8, 0xf9, 0xdb, 0xda, 0xc8, 0x96, 0xff, 0x59, 0x48, 0xb9, 0xdc, 0xca, 0x74, 0x56, 0x89, + 0x79, 0x8c, 0xff, 0xff, 0x11, 0xf1, 0x1f, 0x0b, 0x12, 0x49, 0x24, 0xa4, 0x30, 0xc3, 0x0c, 0x30, + 0xc4, 0x34, 0xff, 0xfd, 0xec, 0xf2, 0xd5, 0x8c, 0xa1, 0x40, 0x45, 0x93, 0x4f, 0x05, 0x8a, 0xdb, + 0xbf, 0xff, 0xfc, 0x63, 0xf8, 0xb1, 0xe1, 0x66, 0xf5, 0x2c, 0x92, 0xc9, 0x2c, 0x92, 0xc9, 0x2c, + 0xbe, 0xcb, 0xec, 0xbe, 0xcb, 0xec, 0xbe, 0xcb, 0xec, 0xbe, 0xc9, 0x46, 0xf9, 0x00, 0x24, 0xe2, + 0xac, 0x81, 0x0c, 0x30, 0xc3, 0x0c, 0x7c, 0x71, 0xc7, 0x1c, 0x71, 0xc3, 0x33, 0xbe, 0x7f, 0xff, + 0x09, 0x4e, 0x4f, 0x3a, 0xf9, 0x34, 0x8d, 0x41, 0xb5, 0x0d, 0x42, 0xbf, 0xfe, 0xf5, 0x88, 0x5f, + 0x5a, 0x14, 0xff, 0x1d, 0x76, 0xda, 0x09, 0x36, 0xa4, 0x5d, 0xaf, 0xff, 0xbd, 0x87, 0x69, 0x9c, + 0xd9, 0xf7, 0x0c, 0x48, 0x98, 0x9e, 0x17, 0x4b, 0xff, 0xef, 0x58, 0x85, 0xf5, 0xa1, 0x4f, 0xf1, + 0xd7, 0x6d, 0xa0, 0x93, 0x60, 0x15, 0xbf, 0xfe, 0xf6, 0x1d, 0xa6, 0x73, 0x67, 0xdc, 0x31, 0x22, + 0x62, 0x78, 0x5d, 0x2f, 0xff, 0xbd, 0x62, 0x17, 0xd6, 0x85, 0x3f, 0xc7, 0x5d, 0xb6, 0x82, 0x4d, + 0xcc, 0x83, 0xff, 0xff, 0xf1, 0xd3, 0x9b, 0xa2, 0x9d, 0xe7, 0x8c, 0x66, 0x72, 0xc0, 0x2f, 0xcf, + 0x65, 0x9b, 0xff, 0xf7, 0xc0, 0xca, 0x22, 0x04, 0xa8, 0x24, 0xf3, 0x1d, 0x63, 0x5d, 0x84, 0x8f, + 0xff, 0x44, 0x5f, 0xff, 0x9f, 0xeb, 0xff, 0x4c, 0x13, 0x36, 0x00, 0x3c, 0x22, 0xc9, 0xc2, 0xf5, + 0xf9, 0xef, 0xff, 0xcf, 0xf5, 0xff, 0xa6, 0x09, 0x9b, 0x00, 0x1e, 0x11, 0x64, 0xe1, 0x7a, 0xf0, + 0x2a, 0x80, 0xe0, 0x04, 0x5a, 0x21, 0x7a, 0xa9, 0x51, 0x5c, 0x9d, 0x9c, 0x8a, 0x61, 0xc3, 0xd2, + 0xff, 0xfd, 0xc8, 0xbf, 0xcc, 0xd3, 0x4c, 0x35, 0xeb, 0x66, 0x85, 0xe3, 0xe5, 0xaa, 0x5e, 0xbf, + 0xff, 0x72, 0x03, 0xdc, 0x34, 0x44, 0x7e, 0x97, 0x68, 0x3e, 0x1a, 0xca, 0x8d, 0xf6, 0xb9, 0xc9, + 0xff, 0xf9, 0x98, 0x62, 0x79, 0xa6, 0x97, 0x88, 0xdb, 0x12, 0xa0, 0xdb, 0x18, 0x4a, 0x52, 0x7f, + 0xfe, 0x65, 0x99, 0x2d, 0xca, 0xa3, 0x9b, 0x23, 0x17, 0x99, 0x47, 0x1b, 0x57, 0x6b, 0x24, 0xda, + 0x4f, 0xff, 0x9f, 0xb9, 0x30, 0x05, 0x2e, 0xef, 0x9f, 0xad, 0x6d, 0xfc, 0x3e, 0xcf, 0xff, 0x9f, + 0xb9, 0x30, 0x05, 0x2e, 0xef, 0x9f, 0xad, 0x6d, 0xfb, 0xed, 0x2b, 0xed, 0xff, 0xf5, 0xca, 0x2c, + 0x22, 0x59, 0x65, 0xee, 0x46, 0x1b, 0xdc, 0x14, 0xc5, 0xff, 0xf5, 0xca, 0x2c, 0x22, 0x59, 0x65, + 0xee, 0x46, 0x1b, 0xdc, 0x0c, 0x2d, 0x3c, 0xe4, 0xff, 0xf9, 0xfe, 0xf1, 0xf6, 0xb4, 0x7a, 0xc8, + 0x10, 0x48, 0x21, 0xb8, 0xfb, 0x3f, 0xfe, 0x7e, 0xe4, 0xc0, 0x14, 0xbb, 0xbe, 0x7e, 0xb5, 0xb7, + 0xef, 0xb4, 0xfa, 0x17, 0xff, 0xd7, 0x3f, 0x91, 0xb5, 0xa5, 0x4a, 0x1d, 0xb1, 0x7b, 0x6a, 0xd4, + 0xc5, 0xff, 0xf5, 0xca, 0x2c, 0x22, 0x59, 0x65, 0xee, 0x46, 0x1b, 0xdc, 0x0c, 0x28, 0xe4, 0xd2, + 0xe3, 0x5f, 0xff, 0xde, 0xd4, 0xf2, 0x31, 0x27, 0x12, 0xa5, 0x4a, 0x95, 0x3c, 0x28, 0x50, 0xa1, + 0x42, 0x85, 0x06, 0xda, 0xff, 0xfd, 0xc8, 0x0f, 0x94, 0x31, 0x1a, 0xd2, 0x66, 0x8a, 0xa8, 0x74, + 0x03, 0x52, 0xcf, 0x12, 0x00, 0x52, 0x6c, 0xd3, 0x36, 0x5f, 0x46, 0x35, 0xfb, 0xc6, 0xbf, 0x4b, + 0xf4, 0xbf, 0x4b, 0xf4, 0xc0, 0x96, 0x09, 0x60, 0x96, 0x09, 0x60, 0x96, 0x09, 0x60, 0x95, 0xf5, + 0xab, 0x2f, 0xff, 0xfe, 0xd2, 0xdd, 0x9c, 0x95, 0xe0, 0x25, 0xdd, 0x39, 0xd3, 0x9d, 0x39, 0xd3, + 0x9d, 0x4c, 0x14, 0xc1, 0x4c, 0x14, 0xc1, 0x4c, 0x14, 0xc1, 0x4c, 0x13, 0xab, 0xb0, 0x0b, 0xff, + 0xf7, 0x20, 0x3e, 0x50, 0xc4, 0x6b, 0x49, 0x9a, 0x2a, 0xa1, 0xd0, 0x0d, 0x52, 0xf5, 0xff, 0xfb, + 0x90, 0x1f, 0x28, 0x62, 0x35, 0xa4, 0xcd, 0x15, 0x50, 0xe8, 0x06, 0xa5, 0xb2, 0xce, 0xbf, 0x7f, + 0xff, 0x14, 0x7b, 0x0e, 0x08, 0x20, 0x18, 0x32, 0x3e, 0xeb, 0x9a, 0xc6, 0xee, 0x81, 0xdf, 0xff, + 0xc5, 0x0c, 0x78, 0xe1, 0x5d, 0x95, 0x29, 0x2c, 0x78, 0x61, 0xfa, 0x77, 0x12, 0x37, 0x16, 0xff, + 0xfd, 0xc8, 0xbf, 0xcc, 0xd3, 0x4c, 0x35, 0xeb, 0x66, 0x85, 0xe3, 0xe5, 0xaa, 0x5e, 0xbf, 0xff, + 0x72, 0x03, 0xe5, 0x0c, 0x46, 0xb4, 0x99, 0xa2, 0xaa, 0x1d, 0x00, 0xd4, 0xb6, 0xe7, 0x39, 0xf1, + 0xaf, 0xff, 0xe2, 0x23, 0x20, 0x60, 0x5e, 0x18, 0x61, 0x88, 0x57, 0x5d, 0x75, 0xd7, 0x5d, 0x5b, + 0x2f, 0xff, 0xbd, 0x62, 0x17, 0xd6, 0x85, 0x3f, 0xc7, 0x5d, 0xb6, 0x81, 0xde, 0x79, 0xff, 0xff, + 0xf5, 0x01, 0xe9, 0xc0, 0xa2, 0xd0, 0xd3, 0x64, 0xd6, 0x4d, 0x64, 0xd6, 0x4d, 0x65, 0x5c, 0x55, + 0xc5, 0x5c, 0x55, 0xc5, 0x5c, 0x55, 0xc5, 0x5c, 0x4d, 0xaa, 0x10, 0x05, 0xd9, 0x33, 0xd5, 0xc0, + 0x00, 0x00, 0x03, 0x00, 0x0b, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xc6, 0x42, 0xf7, 0xff, 0xd7, 0x28, + 0xb0, 0x89, 0x65, 0x97, 0xb9, 0x18, 0x6f, 0x70, 0x53, 0x17, 0xff, 0xd7, 0x28, 0xb0, 0x89, 0x65, + 0x97, 0xb9, 0x18, 0x6f, 0x70, 0x30, 0x56, 0xfd, 0x5b, 0xff, 0xf2, 0x33, 0x67, 0x90, 0x81, 0x60, + 0x4a, 0x3d, 0x34, 0x7d, 0x12, 0xe6, 0xff, 0xfc, 0x8a, 0xea, 0x0e, 0x95, 0x0c, 0xc0, 0x73, 0xf5, + 0x83, 0xaf, 0xfa, 0x2f, 0x2f, 0xff, 0xbd, 0x87, 0x69, 0x9c, 0xd9, 0xf7, 0x0c, 0x48, 0x98, 0x9e, + 0x07, 0x4b, 0xff, 0xef, 0x58, 0x85, 0xf5, 0xa1, 0x4f, 0xf1, 0xd7, 0x6d, 0xa0, 0x78, 0x35, 0x4a, + 0x6d, 0xff, 0xfc, 0x50, 0xc7, 0x8e, 0x15, 0xd9, 0x52, 0x92, 0xc7, 0x86, 0x1f, 0xa7, 0x74, 0x0e, + 0xff, 0xfe, 0x28, 0x63, 0xc7, 0x0a, 0xec, 0xa9, 0x49, 0x63, 0xc3, 0x0f, 0xd3, 0xb8, 0x92, 0x9f, + 0x57, 0xff, 0xee, 0x40, 0x7c, 0xa1, 0x88, 0xd6, 0x93, 0x34, 0x55, 0x43, 0xa0, 0x1a, 0xa5, 0xeb, + 0xff, 0xf7, 0x20, 0x3e, 0x50, 0xc4, 0x6b, 0x49, 0x9a, 0x2a, 0xa1, 0xd0, 0x0d, 0x4b, 0x13, 0x9a, + 0xfc, 0x01, 0x0a, 0xb8, 0xdc, 0xe9, 0xaa, 0x51, 0xa6, 0x2f, 0x33, 0x38, 0x70, 0x9f, 0x3f, 0xff, + 0x81, 0x38, 0x6f, 0x96, 0x59, 0x2c, 0x9d, 0xc5, 0x46, 0x2d, 0xbb, 0xb2, 0x86, 0x2f, 0xff, 0xde, + 0x02, 0x4a, 0x0e, 0x78, 0xf4, 0x81, 0xf4, 0x0e, 0xf1, 0xaf, 0x76, 0xc4, 0x68, 0x3a, 0x7f, 0xfe, + 0x7a, 0x23, 0xee, 0xeb, 0xae, 0x0b, 0xba, 0xa9, 0x83, 0xd2, 0x73, 0xc7, 0xd0, 0x9f, 0xff, 0x9e, + 0x6a, 0xb4, 0x7c, 0xad, 0x6d, 0xa6, 0x32, 0xbc, 0x60, 0xd2, 0xe3, 0x9c, 0x90, 0x95, 0xe3, 0xff, + 0xe9, 0x1b, 0x9a, 0x48, 0x96, 0x45, 0x2e, 0x92, 0xdc, 0x57, 0xac, 0xb3, 0xff, 0xe9, 0x1b, 0x9a, + 0x48, 0x96, 0x45, 0x2e, 0x92, 0xdc, 0x57, 0x99, 0x5e, 0x95, 0x7f, 0xfe, 0x01, 0xa9, 0xd0, 0xd9, + 0xc1, 0x16, 0xba, 0xb0, 0xf7, 0x82, 0xfd, 0x7f, 0xfe, 0x01, 0xa9, 0xd0, 0xd9, 0xc1, 0x16, 0xba, + 0xb0, 0xf7, 0x81, 0x46, 0x41, 0x3d, 0xbf, 0xff, 0x01, 0x63, 0xdb, 0x18, 0x93, 0x66, 0x4d, 0xce, + 0x9b, 0xce, 0x5f, 0xaf, 0xff, 0xc0, 0x35, 0x3a, 0x1b, 0x38, 0x22, 0xd7, 0x56, 0x1e, 0xf0, 0x28, + 0x50, 0x74, 0xff, 0xfa, 0x4a, 0x1b, 0x55, 0xac, 0x47, 0xb6, 0x24, 0xc4, 0x4a, 0xa2, 0xcb, 0x3f, + 0xfe, 0x91, 0xb9, 0xa4, 0x89, 0x64, 0x52, 0xe9, 0x2d, 0xc5, 0x79, 0x98, 0x82, 0x80, + ] + + static let bars709Limited: [UInt8] = [ + 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0xff, 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, + 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, + 0x00, 0xff, 0xa0, 0x08, 0x08, 0x10, 0x59, 0x65, 0x66, 0x92, 0x4c, 0xae, 0x6a, 0x02, 0x02, 0x02, + 0x08, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x00, 0xc8, 0x40, 0x00, 0x00, 0x00, 0x01, + 0x44, 0x01, 0xc1, 0x71, 0xa9, 0x12, 0x00, 0x00, 0x01, 0x4e, 0x01, 0x05, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xd1, 0x2c, 0xa2, 0xde, 0x09, 0xb5, 0x17, 0x47, 0xdb, 0xbb, 0x55, 0xa4, + 0xfe, 0x7f, 0xc2, 0xfc, 0x4e, 0x78, 0x32, 0x36, 0x35, 0x20, 0x28, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x20, 0x32, 0x31, 0x36, 0x29, 0x20, 0x2d, 0x20, 0x34, 0x2e, 0x32, 0x2b, 0x31, 0x2d, 0x65, 0x34, + 0x34, 0x34, 0x37, 0x34, 0x34, 0x3a, 0x5b, 0x4d, 0x61, 0x63, 0x20, 0x4f, 0x53, 0x20, 0x58, 0x5d, + 0x5b, 0x63, 0x6c, 0x61, 0x6e, 0x67, 0x20, 0x32, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x5d, 0x5b, 0x36, + 0x34, 0x20, 0x62, 0x69, 0x74, 0x5d, 0x20, 0x38, 0x62, 0x69, 0x74, 0x2b, 0x31, 0x30, 0x62, 0x69, + 0x74, 0x2b, 0x31, 0x32, 0x62, 0x69, 0x74, 0x20, 0x2d, 0x20, 0x48, 0x2e, 0x32, 0x36, 0x35, 0x2f, + 0x48, 0x45, 0x56, 0x43, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x63, 0x20, 0x2d, 0x20, 0x43, 0x6f, 0x70, + 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x32, 0x30, 0x31, 0x33, 0x2d, 0x32, 0x30, 0x31, 0x38, + 0x20, 0x28, 0x63, 0x29, 0x20, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x63, 0x6f, 0x72, 0x65, 0x77, 0x61, + 0x72, 0x65, 0x2c, 0x20, 0x49, 0x6e, 0x63, 0x20, 0x2d, 0x20, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, + 0x2f, 0x78, 0x32, 0x36, 0x35, 0x2e, 0x6f, 0x72, 0x67, 0x20, 0x2d, 0x20, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x3a, 0x20, 0x63, 0x70, 0x75, 0x69, 0x64, 0x3d, 0x39, 0x38, 0x20, 0x66, 0x72, + 0x61, 0x6d, 0x65, 0x2d, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x3d, 0x31, 0x20, 0x6e, 0x6f, + 0x2d, 0x77, 0x70, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x70, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x6e, 0x6f, + 0x2d, 0x70, 0x6d, 0x65, 0x20, 0x6e, 0x6f, 0x2d, 0x70, 0x73, 0x6e, 0x72, 0x20, 0x6e, 0x6f, 0x2d, + 0x73, 0x73, 0x69, 0x6d, 0x20, 0x6c, 0x6f, 0x67, 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, + 0x20, 0x62, 0x69, 0x74, 0x64, 0x65, 0x70, 0x74, 0x68, 0x3d, 0x38, 0x20, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x2d, 0x63, 0x73, 0x70, 0x3d, 0x31, 0x20, 0x66, 0x70, 0x73, 0x3d, 0x32, 0x35, 0x2f, 0x31, + 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2d, 0x72, 0x65, 0x73, 0x3d, 0x32, 0x35, 0x36, 0x78, 0x36, + 0x34, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6c, 0x61, 0x63, 0x65, 0x3d, 0x30, 0x20, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x2d, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x3d, 0x30, 0x20, 0x6c, 0x65, 0x76, + 0x65, 0x6c, 0x2d, 0x69, 0x64, 0x63, 0x3d, 0x30, 0x20, 0x68, 0x69, 0x67, 0x68, 0x2d, 0x74, 0x69, + 0x65, 0x72, 0x3d, 0x31, 0x20, 0x75, 0x68, 0x64, 0x2d, 0x62, 0x64, 0x3d, 0x30, 0x20, 0x72, 0x65, + 0x66, 0x3d, 0x33, 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x2d, 0x6e, 0x6f, 0x6e, + 0x2d, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x20, 0x72, 0x65, 0x70, + 0x65, 0x61, 0x74, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x20, 0x61, 0x6e, 0x6e, 0x65, + 0x78, 0x62, 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x75, 0x64, 0x20, 0x6e, 0x6f, 0x2d, 0x65, 0x6f, 0x62, + 0x20, 0x6e, 0x6f, 0x2d, 0x65, 0x6f, 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x72, 0x64, 0x20, 0x69, + 0x6e, 0x66, 0x6f, 0x20, 0x68, 0x61, 0x73, 0x68, 0x3d, 0x30, 0x20, 0x74, 0x65, 0x6d, 0x70, 0x6f, + 0x72, 0x61, 0x6c, 0x2d, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x3d, 0x30, 0x20, 0x6f, 0x70, 0x65, + 0x6e, 0x2d, 0x67, 0x6f, 0x70, 0x20, 0x6d, 0x69, 0x6e, 0x2d, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, + 0x3d, 0x32, 0x35, 0x20, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, 0x3d, 0x32, 0x35, 0x30, 0x20, 0x67, + 0x6f, 0x70, 0x2d, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x3d, 0x30, 0x20, 0x62, + 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x3d, 0x34, 0x20, 0x62, 0x2d, 0x61, 0x64, 0x61, 0x70, 0x74, + 0x3d, 0x32, 0x20, 0x62, 0x2d, 0x70, 0x79, 0x72, 0x61, 0x6d, 0x69, 0x64, 0x20, 0x62, 0x66, 0x72, + 0x61, 0x6d, 0x65, 0x2d, 0x62, 0x69, 0x61, 0x73, 0x3d, 0x30, 0x20, 0x72, 0x63, 0x2d, 0x6c, 0x6f, + 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x3d, 0x32, 0x30, 0x20, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, + 0x68, 0x65, 0x61, 0x64, 0x2d, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x73, 0x3d, 0x30, 0x20, 0x73, 0x63, + 0x65, 0x6e, 0x65, 0x63, 0x75, 0x74, 0x3d, 0x34, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x69, 0x73, + 0x74, 0x2d, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x63, 0x75, 0x74, 0x20, 0x72, 0x61, 0x64, 0x6c, 0x3d, + 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x70, 0x6c, 0x69, 0x63, 0x65, 0x20, 0x6e, 0x6f, 0x2d, 0x69, + 0x6e, 0x74, 0x72, 0x61, 0x2d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x20, 0x63, 0x74, 0x75, + 0x3d, 0x36, 0x34, 0x20, 0x6d, 0x69, 0x6e, 0x2d, 0x63, 0x75, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3d, + 0x38, 0x20, 0x6e, 0x6f, 0x2d, 0x72, 0x65, 0x63, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x6d, 0x70, + 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x74, 0x75, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3d, 0x33, 0x32, 0x20, + 0x74, 0x75, 0x2d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x2d, 0x64, 0x65, 0x70, 0x74, 0x68, 0x3d, 0x31, + 0x20, 0x74, 0x75, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x2d, 0x64, 0x65, 0x70, 0x74, 0x68, 0x3d, + 0x31, 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x74, 0x75, 0x3d, 0x30, 0x20, 0x72, 0x64, 0x6f, + 0x71, 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x2d, 0x72, 0x64, 0x3d, 0x30, 0x2e, 0x30, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x73, 0x69, + 0x6d, 0x2d, 0x72, 0x64, 0x20, 0x73, 0x69, 0x67, 0x6e, 0x68, 0x69, 0x64, 0x65, 0x20, 0x6e, 0x6f, + 0x2d, 0x74, 0x73, 0x6b, 0x69, 0x70, 0x20, 0x6e, 0x72, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x3d, + 0x30, 0x20, 0x6e, 0x72, 0x2d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, + 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x64, 0x2d, 0x69, 0x6e, 0x74, 0x72, + 0x61, 0x20, 0x73, 0x74, 0x72, 0x6f, 0x6e, 0x67, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x2d, 0x73, + 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x6d, 0x65, 0x72, + 0x67, 0x65, 0x3d, 0x33, 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x72, 0x65, 0x66, 0x73, 0x3d, + 0x31, 0x20, 0x6e, 0x6f, 0x2d, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x6d, 0x6f, 0x64, 0x65, 0x73, + 0x20, 0x6d, 0x65, 0x3d, 0x31, 0x20, 0x73, 0x75, 0x62, 0x6d, 0x65, 0x3d, 0x32, 0x20, 0x6d, 0x65, + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x3d, 0x35, 0x37, 0x20, 0x74, 0x65, 0x6d, 0x70, 0x6f, 0x72, 0x61, + 0x6c, 0x2d, 0x6d, 0x76, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x2d, 0x64, + 0x75, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x6d, 0x65, 0x20, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, + 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x62, 0x20, 0x6e, 0x6f, 0x2d, + 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x7a, 0x65, 0x2d, 0x73, 0x72, 0x63, 0x2d, 0x70, 0x69, 0x63, 0x73, + 0x20, 0x64, 0x65, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x3d, 0x30, 0x3a, 0x30, 0x20, 0x73, 0x61, 0x6f, + 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x61, 0x6f, 0x2d, 0x6e, 0x6f, 0x6e, 0x2d, 0x64, 0x65, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x20, 0x72, 0x64, 0x3d, 0x33, 0x20, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x2d, 0x73, 0x61, 0x6f, 0x3d, 0x34, 0x20, 0x65, 0x61, 0x72, 0x6c, 0x79, 0x2d, 0x73, + 0x6b, 0x69, 0x70, 0x20, 0x72, 0x73, 0x6b, 0x69, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x61, 0x73, + 0x74, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x20, 0x6e, 0x6f, 0x2d, 0x74, 0x73, 0x6b, 0x69, 0x70, + 0x2d, 0x66, 0x61, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x63, 0x75, 0x2d, 0x6c, 0x6f, 0x73, 0x73, + 0x6c, 0x65, 0x73, 0x73, 0x20, 0x62, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x20, 0x6e, 0x6f, 0x2d, + 0x73, 0x70, 0x6c, 0x69, 0x74, 0x72, 0x64, 0x2d, 0x73, 0x6b, 0x69, 0x70, 0x20, 0x72, 0x64, 0x70, + 0x65, 0x6e, 0x61, 0x6c, 0x74, 0x79, 0x3d, 0x30, 0x20, 0x70, 0x73, 0x79, 0x2d, 0x72, 0x64, 0x3d, + 0x32, 0x2e, 0x30, 0x30, 0x20, 0x70, 0x73, 0x79, 0x2d, 0x72, 0x64, 0x6f, 0x71, 0x3d, 0x30, 0x2e, + 0x30, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x72, 0x64, 0x2d, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x20, + 0x6c, 0x6f, 0x73, 0x73, 0x6c, 0x65, 0x73, 0x73, 0x20, 0x63, 0x62, 0x71, 0x70, 0x6f, 0x66, 0x66, + 0x73, 0x3d, 0x30, 0x20, 0x63, 0x72, 0x71, 0x70, 0x6f, 0x66, 0x66, 0x73, 0x3d, 0x30, 0x20, 0x72, + 0x63, 0x3d, 0x63, 0x71, 0x70, 0x20, 0x71, 0x70, 0x3d, 0x34, 0x20, 0x69, 0x70, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x3d, 0x31, 0x2e, 0x34, 0x30, 0x20, 0x70, 0x62, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x3d, + 0x31, 0x2e, 0x33, 0x30, 0x20, 0x61, 0x71, 0x2d, 0x6d, 0x6f, 0x64, 0x65, 0x3d, 0x30, 0x20, 0x61, + 0x71, 0x2d, 0x73, 0x74, 0x72, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x3d, 0x30, 0x2e, 0x30, 0x30, 0x20, + 0x6e, 0x6f, 0x2d, 0x63, 0x75, 0x74, 0x72, 0x65, 0x65, 0x20, 0x7a, 0x6f, 0x6e, 0x65, 0x2d, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, + 0x2d, 0x63, 0x62, 0x72, 0x20, 0x71, 0x67, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3d, 0x36, 0x34, 0x20, + 0x6e, 0x6f, 0x2d, 0x72, 0x63, 0x2d, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x20, 0x71, 0x70, 0x6d, 0x61, + 0x78, 0x3d, 0x36, 0x39, 0x20, 0x71, 0x70, 0x6d, 0x69, 0x6e, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, + 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x2d, 0x76, 0x62, 0x76, 0x20, 0x73, 0x61, 0x72, 0x3d, 0x30, 0x20, + 0x6f, 0x76, 0x65, 0x72, 0x73, 0x63, 0x61, 0x6e, 0x3d, 0x30, 0x20, 0x76, 0x69, 0x64, 0x65, 0x6f, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x3d, 0x35, 0x20, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x3d, 0x30, + 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x70, 0x72, 0x69, 0x6d, 0x3d, 0x31, 0x20, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x66, 0x65, 0x72, 0x3d, 0x31, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x72, 0x69, 0x78, 0x3d, 0x31, 0x20, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x61, 0x6c, 0x6f, 0x63, 0x3d, + 0x30, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x2d, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, + 0x3d, 0x30, 0x20, 0x63, 0x6c, 0x6c, 0x3d, 0x30, 0x2c, 0x30, 0x20, 0x6d, 0x69, 0x6e, 0x2d, 0x6c, + 0x75, 0x6d, 0x61, 0x3d, 0x30, 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x6c, 0x75, 0x6d, 0x61, 0x3d, 0x32, + 0x35, 0x35, 0x20, 0x6c, 0x6f, 0x67, 0x32, 0x2d, 0x6d, 0x61, 0x78, 0x2d, 0x70, 0x6f, 0x63, 0x2d, + 0x6c, 0x73, 0x62, 0x3d, 0x38, 0x20, 0x76, 0x75, 0x69, 0x2d, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, + 0x2d, 0x69, 0x6e, 0x66, 0x6f, 0x20, 0x76, 0x75, 0x69, 0x2d, 0x68, 0x72, 0x64, 0x2d, 0x69, 0x6e, + 0x66, 0x6f, 0x20, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x73, 0x3d, 0x31, 0x20, 0x6e, 0x6f, 0x2d, 0x6f, + 0x70, 0x74, 0x2d, 0x71, 0x70, 0x2d, 0x70, 0x70, 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x6f, 0x70, 0x74, + 0x2d, 0x72, 0x65, 0x66, 0x2d, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, + 0x2d, 0x70, 0x70, 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x2d, 0x70, 0x61, + 0x73, 0x73, 0x2d, 0x6f, 0x70, 0x74, 0x2d, 0x72, 0x70, 0x73, 0x20, 0x73, 0x63, 0x65, 0x6e, 0x65, + 0x63, 0x75, 0x74, 0x2d, 0x62, 0x69, 0x61, 0x73, 0x3d, 0x30, 0x2e, 0x30, 0x35, 0x20, 0x6e, 0x6f, + 0x2d, 0x6f, 0x70, 0x74, 0x2d, 0x63, 0x75, 0x2d, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x2d, 0x71, 0x70, + 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x71, 0x2d, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x6f, + 0x2d, 0x68, 0x64, 0x72, 0x31, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x64, 0x72, 0x31, 0x30, 0x2d, + 0x6f, 0x70, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x64, 0x68, 0x64, 0x72, 0x31, 0x30, 0x2d, 0x6f, 0x70, + 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x69, 0x64, 0x72, 0x2d, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, + 0x79, 0x2d, 0x73, 0x65, 0x69, 0x20, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2d, 0x72, + 0x65, 0x75, 0x73, 0x65, 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x61, 0x6e, 0x61, + 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2d, 0x73, 0x61, 0x76, 0x65, 0x2d, 0x72, 0x65, 0x75, 0x73, 0x65, + 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, + 0x73, 0x2d, 0x6c, 0x6f, 0x61, 0x64, 0x2d, 0x72, 0x65, 0x75, 0x73, 0x65, 0x2d, 0x6c, 0x65, 0x76, + 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2d, 0x66, 0x61, 0x63, 0x74, 0x6f, + 0x72, 0x3d, 0x30, 0x20, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, + 0x3d, 0x30, 0x20, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3d, + 0x30, 0x20, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x6d, 0x76, 0x3d, 0x31, 0x20, 0x72, 0x65, + 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x63, 0x74, 0x75, 0x2d, 0x64, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x74, + 0x69, 0x6f, 0x6e, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x73, + 0x61, 0x6f, 0x20, 0x63, 0x74, 0x75, 0x2d, 0x69, 0x6e, 0x66, 0x6f, 0x3d, 0x30, 0x20, 0x6e, 0x6f, + 0x2d, 0x6c, 0x6f, 0x77, 0x70, 0x61, 0x73, 0x73, 0x2d, 0x64, 0x63, 0x74, 0x20, 0x72, 0x65, 0x66, + 0x69, 0x6e, 0x65, 0x2d, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2d, 0x74, 0x79, 0x70, + 0x65, 0x3d, 0x30, 0x20, 0x63, 0x6f, 0x70, 0x79, 0x2d, 0x70, 0x69, 0x63, 0x3d, 0x31, 0x20, 0x6d, + 0x61, 0x78, 0x2d, 0x61, 0x75, 0x73, 0x69, 0x7a, 0x65, 0x2d, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x3d, 0x31, 0x2e, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x2d, + 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, + 0x2d, 0x73, 0x65, 0x69, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x65, 0x76, 0x63, 0x2d, 0x61, 0x71, 0x20, + 0x6e, 0x6f, 0x2d, 0x73, 0x76, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, + 0x71, 0x70, 0x2d, 0x61, 0x64, 0x61, 0x70, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x72, 0x61, + 0x6e, 0x67, 0x65, 0x3d, 0x31, 0x2e, 0x30, 0x30, 0x20, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x63, 0x75, + 0x74, 0x2d, 0x61, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x71, 0x70, 0x3d, 0x30, 0x63, 0x6f, 0x6e, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2d, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2d, 0x6f, + 0x66, 0x66, 0x73, 0x65, 0x74, 0x73, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x3d, 0x30, 0x20, 0x62, + 0x6f, 0x74, 0x74, 0x6f, 0x6d, 0x3d, 0x30, 0x20, 0x64, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2d, + 0x6d, 0x61, 0x78, 0x2d, 0x72, 0x61, 0x74, 0x65, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x76, 0x62, + 0x76, 0x2d, 0x6c, 0x69, 0x76, 0x65, 0x2d, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x2d, 0x70, 0x61, 0x73, + 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x6d, 0x63, 0x73, 0x74, 0x66, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x62, + 0x72, 0x63, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x2d, 0x72, 0x63, 0x80, 0x00, + 0x00, 0x01, 0x28, 0x01, 0xaf, 0x05, 0xb8, 0x10, 0x0c, 0x7f, 0x80, 0xd7, 0x00, 0xc4, 0xbb, 0x82, + 0x75, 0x79, 0xbd, 0x3c, 0x5f, 0x14, 0xb1, 0x4b, 0x14, 0xb1, 0x4b, 0x17, 0xc5, 0x7c, 0x57, 0xc5, + 0x7c, 0x57, 0xc5, 0x7c, 0x57, 0xc5, 0x7d, 0xb4, 0xcb, 0x51, 0xc8, 0xd6, 0xa0, 0x35, 0xd5, 0x55, + 0xa4, 0x40, 0xd9, 0x57, 0x3f, 0xff, 0x7b, 0xb8, 0x0b, 0x9c, 0xe7, 0x3a, 0x29, 0x4a, 0x52, 0x94, + 0xa5, 0xc2, 0x9f, 0xff, 0xe8, 0xfd, 0xf0, 0x63, 0x71, 0xfc, 0x63, 0xec, 0x8f, 0x38, 0xf7, 0xff, + 0xfc, 0x89, 0xda, 0xff, 0xff, 0xf0, 0x69, 0xe0, 0x74, 0x7b, 0xdf, 0x7f, 0xcc, 0x1c, 0xc1, 0xcc, + 0x1c, 0xc1, 0xcc, 0xda, 0xcd, 0xac, 0xda, 0xcd, 0xac, 0xda, 0xcd, 0xac, 0xda, 0xc4, 0x73, 0xff, + 0xd8, 0xf7, 0x40, 0x0f, 0x9f, 0x18, 0xc6, 0x31, 0x8c, 0x67, 0x39, 0xce, 0x73, 0x9c, 0xb3, 0xd2, + 0x80, 0x0e, 0x4f, 0xff, 0x82, 0x8d, 0x24, 0xc8, 0xe1, 0x4d, 0xd7, 0xae, 0x61, 0xc6, 0x8f, 0xff, + 0xc6, 0x7a, 0xd8, 0xf8, 0x9c, 0xf6, 0xf9, 0xcf, 0x66, 0x3d, 0xa5, 0x9a, 0x2e, 0x4f, 0x7f, 0xfc, + 0x55, 0x8a, 0xa8, 0xed, 0xa7, 0x75, 0xbb, 0xe9, 0x49, 0x47, 0xff, 0xe3, 0x3d, 0x6c, 0x7c, 0x4e, + 0x7b, 0x7c, 0xe7, 0xb3, 0x16, 0x79, 0xa3, 0xe3, 0xff, 0xe7, 0xab, 0xaa, 0x9f, 0x21, 0x38, 0x65, + 0xc4, 0xb8, 0x18, 0x9f, 0xfe, 0x9b, 0xec, 0x4b, 0x58, 0x5d, 0xf8, 0xe5, 0xdf, 0x53, 0x6f, 0x2b, + 0x0c, 0x90, 0x70, 0xa0, 0x00, 0xee, 0xdd, 0x34, 0xd4, 0x1e, 0xb0, 0x10, 0x19, 0xe6, 0x85, 0xe6, + 0x00, 0x48, 0xff, 0xfd, 0x80, 0x28, 0x35, 0x78, 0x31, 0x21, 0xb6, 0x44, 0xdc, 0xed, 0xff, 0xf7, + 0xd9, 0xef, 0xcd, 0xe3, 0x09, 0x63, 0x63, 0xc8, 0x0d, 0x40, 0x1c, 0x5f, 0xff, 0x07, 0x4b, 0x3e, + 0xdd, 0x9f, 0x5f, 0x13, 0x8f, 0x5e, 0x4d, 0x7f, 0xfb, 0x27, 0xc1, 0xaa, 0x45, 0xe8, 0x00, 0x78, + 0x18, 0xd2, 0x7b, 0x04, 0x56, 0x7f, 0xfd, 0x49, 0x9f, 0xfe, 0xb6, 0xbb, 0x0b, 0x58, 0x86, 0x8b, + 0xdf, 0xff, 0x63, 0x53, 0x7d, 0x4f, 0xd7, 0x6d, 0x83, 0xdd, 0x24, 0x58, 0x37, 0xe3, 0xff, 0xd2, + 0xa0, 0xff, 0xea, 0x7c, 0x38, 0xbd, 0xc8, 0xcb, 0x41, 0xff, 0xf3, 0xe1, 0x33, 0x34, 0x62, 0xe7, + 0x3d, 0xc8, 0x0b, 0x72, 0xea, 0x02, 0x8a, 0x33, 0xff, 0xff, 0xa6, 0xb4, 0x4c, 0x89, 0x44, 0x92, + 0x49, 0x25, 0xa6, 0x9a, 0x69, 0xa6, 0x9a, 0x60, 0x3f, 0xff, 0xf3, 0x35, 0x41, 0x04, 0x41, 0xef, + 0xf0, 0x95, 0xfa, 0x4c, 0x21, 0x48, 0x56, 0x7f, 0x6a, 0x62, 0x00, 0xb3, 0xd4, 0x0d, 0xe9, 0xf0, + 0x69, 0x96, 0x4e, 0x10, 0x2d, 0x02, 0xd0, 0x2d, 0x02, 0xd0, 0x38, 0x43, 0x84, 0x38, 0x43, 0x84, + 0x38, 0x43, 0x84, 0x38, 0x42, 0xdf, 0x7f, 0x9a, 0xfd, 0xff, 0xff, 0xb9, 0x77, 0x71, 0x05, 0xa5, + 0x8b, 0x43, 0x77, 0x37, 0x73, 0x77, 0x37, 0x73, 0x7b, 0x07, 0xb0, 0x7b, 0x07, 0xb0, 0x7b, 0x07, + 0xb0, 0x7b, 0x07, 0x92, 0x20, 0x6d, 0xff, 0xfe, 0xb4, 0xe9, 0xde, 0x13, 0xd7, 0xc9, 0xbc, 0x56, + 0xf3, 0xee, 0xa8, 0xe4, 0x3f, 0xff, 0xa1, 0xf0, 0xfc, 0x2e, 0x5d, 0x4d, 0xac, 0xc7, 0x3f, 0x7b, + 0x12, 0xf6, 0x0d, 0x0c, 0x84, 0xff, 0xfb, 0x0e, 0x4f, 0x86, 0x62, 0xe4, 0x29, 0xcd, 0x16, 0x0d, + 0x1c, 0xd5, 0xf4, 0xff, 0xfc, 0xd6, 0x57, 0xd6, 0xef, 0x5c, 0x7f, 0xf4, 0x23, 0xc4, 0x3b, 0xff, + 0x44, 0x22, 0xf3, 0xff, 0xf1, 0x4c, 0x00, 0x90, 0x41, 0x74, 0x0e, 0xdb, 0x83, 0xdb, 0x98, 0x09, + 0xfd, 0xff, 0xf6, 0xfe, 0x6f, 0x2f, 0xb4, 0x11, 0xde, 0x8f, 0x3c, 0xed, 0x20, 0xdb, 0x5c, 0x20, + 0xf4, 0xf5, 0x7f, 0xff, 0x37, 0xb3, 0x9d, 0x94, 0xa5, 0x29, 0xc9, 0x4a, 0x52, 0x94, 0xa4, 0x4d, + 0xff, 0xf3, 0x75, 0xe0, 0xaa, 0xe3, 0x22, 0xfb, 0x9e, 0x89, 0xbf, 0x1c, 0x4c, 0x9f, 0xff, 0xfb, + 0x72, 0x76, 0xc9, 0x19, 0x67, 0xaf, 0x6f, 0x7e, 0xf7, 0xef, 0x7e, 0xf7, 0xef, 0xb6, 0x7b, 0x67, + 0xb6, 0x7b, 0x67, 0xb6, 0x7b, 0x67, 0xb6, 0x78, 0x08, 0xd8, 0x96, 0x00, 0xbe, 0x62, 0xfd, 0xef, + 0x7b, 0xdf, 0x2a, 0x10, 0x84, 0x21, 0x06, 0xa8, 0x0d, 0x2f, 0xff, 0x8c, 0x55, 0xb0, 0xd2, 0xf0, + 0x2a, 0xe6, 0xfa, 0xe9, 0xef, 0xff, 0xe8, 0xfa, 0x01, 0x39, 0xc2, 0x9c, 0x8f, 0x09, 0xe8, 0xf5, + 0x30, 0xa2, 0xaf, 0xff, 0x1f, 0x61, 0x54, 0xea, 0xd1, 0xd6, 0xde, 0xc5, 0x29, 0x4f, 0xff, 0x9e, + 0xf9, 0x05, 0x2a, 0x46, 0x6f, 0x36, 0x3c, 0x0c, 0xa8, 0xb2, 0x7f, 0xff, 0x10, 0x1f, 0xff, 0x8e, + 0x40, 0x9d, 0x84, 0xfb, 0x4f, 0x3f, 0xff, 0x8b, 0xc9, 0x5b, 0x85, 0x13, 0x47, 0x02, 0xc4, 0x4e, + 0xf7, 0x00, 0xbd, 0x7f, 0xfd, 0xc5, 0xa0, 0xda, 0x81, 0x9a, 0x24, 0xd8, 0x3d, 0x5c, 0x15, 0x53, + 0xfb, 0xbf, 0xfe, 0xf7, 0xe2, 0x20, 0x48, 0xd8, 0xf0, 0xa3, 0xa0, 0x20, 0xd8, 0x91, 0x6c, 0x02, + 0xb1, 0xff, 0xf7, 0xe3, 0x2f, 0x59, 0xc2, 0x49, 0x94, 0x57, 0x95, 0xcb, 0x93, 0x3a, 0xf8, 0xff, + 0xfb, 0xc1, 0x6f, 0x38, 0x3c, 0x77, 0x60, 0x88, 0x59, 0xdf, 0x60, 0xb2, 0x38, 0xe9, 0xd9, 0x00, + 0x1d, 0x78, 0xe9, 0xc8, 0x3a, 0x32, 0x69, 0x9c, 0x9d, 0xb6, 0xaa, 0xbf, 0xff, 0x43, 0x59, 0x62, + 0x04, 0x19, 0x30, 0xcb, 0xb7, 0xbb, 0xb8, 0xbf, 0x13, 0x3f, 0xff, 0x4d, 0xaa, 0x30, 0x0e, 0x2d, + 0xd4, 0xce, 0xc1, 0x74, 0x43, 0x75, 0xd4, 0xac, 0x51, 0xff, 0xf7, 0xb5, 0x7d, 0x74, 0x83, 0xc8, + 0xce, 0x4e, 0xcc, 0xce, 0xd8, 0x34, 0xc9, 0xff, 0xf7, 0x9a, 0xf2, 0xaa, 0xcb, 0x45, 0x75, 0xe2, + 0x6c, 0x41, 0xf5, 0xd9, 0xe3, 0xdd, 0x2b, 0xff, 0xfe, 0x2f, 0x25, 0x6e, 0x14, 0x4d, 0x1c, 0x0b, + 0x11, 0x43, 0x57, 0xff, 0x8a, 0xf6, 0xdb, 0x90, 0x47, 0xf1, 0x2c, 0x72, 0x5f, 0x12, 0xf3, 0xbf, + 0xfd, 0xb6, 0x8c, 0x1f, 0x2a, 0x79, 0x43, 0xec, 0x20, 0x9c, 0xcf, 0xff, 0x6c, 0x8a, 0x5d, 0x2e, + 0xe4, 0x89, 0x32, 0x9a, 0xe5, 0xa6, 0xe8, 0xab, 0xff, 0xf1, 0xaa, 0x55, 0x4e, 0x89, 0x31, 0xd5, + 0x8f, 0x95, 0xfa, 0x7f, 0xf8, 0x83, 0x93, 0x10, 0xaf, 0x77, 0xdb, 0x76, 0x14, 0x41, 0x6b, 0x2f, + 0xff, 0xdb, 0xb3, 0xff, 0xee, 0xc9, 0xc6, 0xfe, 0x3a, 0x41, 0x29, 0xff, 0xed, 0x04, 0xf6, 0x57, + 0xff, 0xad, 0x42, 0x1c, 0xbd, 0xb4, 0x6c, 0xa7, 0x9c, 0x87, 0xff, 0xfe, 0x68, 0xd4, 0xc9, 0x92, + 0xa5, 0x0d, 0x13, 0x6c, 0xdb, 0x36, 0xcd, 0xb3, 0xad, 0x6b, 0x5a, 0xd6, 0xb5, 0xad, 0x6b, 0x5a, + 0xd6, 0xa0, 0xbf, 0xff, 0xe3, 0xc2, 0xd8, 0x6d, 0xe4, 0xc2, 0xf4, 0x25, 0xcb, 0x59, 0x2d, 0x72, + 0x69, 0x07, 0x74, 0x08, 0x72, 0x03, 0xa8, 0x50, 0x05, 0xd6, 0xc4, 0x6f, 0x61, 0x7c, 0x5e, 0x8f, + 0xb1, 0x6c, 0x68, 0x31, 0xa0, 0xc6, 0x83, 0x1a, 0x0c, 0x6e, 0x09, 0xb8, 0x26, 0xe0, 0x9b, 0x82, + 0x6e, 0x09, 0xb8, 0x26, 0xe0, 0x9a, 0x13, 0x7a, 0xec, 0x03, 0x8b, 0x5c, 0xd7, 0xad, 0xfa, 0x2c, + 0x33, 0xd1, 0x4e, 0x0c, 0x78, 0x31, 0xe0, 0xc7, 0x83, 0x1e, 0x10, 0x04, 0x40, 0x11, 0x00, 0x44, + 0x01, 0x10, 0x04, 0x40, 0x11, 0x00, 0x43, 0x9c, 0x89, 0x17, 0xff, 0xfd, 0x9c, 0xe7, 0x03, 0xc3, + 0xae, 0xde, 0x4e, 0xb4, 0xa4, 0x8b, 0xab, 0x48, 0x6a, 0xfd, 0x60, 0x7c, 0x57, 0x1f, 0xff, 0xd9, + 0xb4, 0x6f, 0x2c, 0x0c, 0xc9, 0xc2, 0xcd, 0xf5, 0x60, 0x12, 0x59, 0xc5, 0xae, 0xf1, 0x27, 0x19, + 0x97, 0x69, 0xa5, 0xff, 0xfe, 0xcc, 0xd6, 0x37, 0x17, 0x92, 0xb3, 0x66, 0x3f, 0xdb, 0x68, 0x44, + 0x9e, 0x21, 0x12, 0x8a, 0x48, 0x6e, 0xbf, 0xff, 0xb3, 0x00, 0xda, 0x17, 0x61, 0x02, 0xfd, 0x26, + 0x97, 0x1d, 0x83, 0x48, 0x87, 0x5a, 0x4e, 0xcb, 0x8f, 0x24, 0x08, 0xbf, 0xff, 0xa5, 0x31, 0x1b, + 0x76, 0x35, 0x05, 0x4a, 0x6b, 0x42, 0x63, 0x65, 0x8e, 0x3d, 0x96, 0x3e, 0x36, 0xef, 0xff, 0xff, + 0xe9, 0x3f, 0x11, 0xd7, 0xa0, 0x62, 0x26, 0x55, 0xb4, 0x6c, 0xb0, 0x97, 0x1f, 0x37, 0xbb, 0x6c, + 0x1c, 0x56, 0x89, 0x79, 0x8c, 0xff, 0xfe, 0x34, 0xc7, 0x82, 0x42, 0x10, 0x85, 0x44, 0x21, 0x08, + 0x42, 0x17, 0x27, 0xff, 0xd9, 0x5a, 0x82, 0x0e, 0xab, 0x3d, 0xa6, 0xf5, 0x32, 0x85, 0xb7, 0x7f, + 0xff, 0xf8, 0xc7, 0xf1, 0x63, 0xc2, 0xcd, 0xea, 0x59, 0x25, 0x92, 0x59, 0x25, 0x92, 0x59, 0x7d, + 0x97, 0xd9, 0x7d, 0x97, 0xd9, 0x7d, 0x97, 0xd9, 0x7d, 0x92, 0x8d, 0xf2, 0x00, 0x48, 0xbc, 0xcb, + 0x5a, 0xd6, 0xb5, 0xc0, 0xa5, 0x29, 0x4a, 0x52, 0x06, 0x77, 0xcf, 0xff, 0xb8, 0x30, 0x59, 0x27, + 0xf7, 0x62, 0x38, 0x0c, 0xa1, 0x35, 0xff, 0xec, 0x9b, 0xb6, 0x5d, 0x9a, 0x02, 0x56, 0xf3, 0xc6, + 0x71, 0x48, 0xbb, 0x5f, 0xfe, 0xd6, 0x42, 0xaa, 0x1d, 0x7a, 0xde, 0x77, 0x06, 0xab, 0xaf, 0xff, + 0x64, 0xdd, 0xb2, 0xec, 0xd0, 0x12, 0xb7, 0x9e, 0x33, 0x80, 0x15, 0xbf, 0xfd, 0xac, 0x85, 0x54, + 0x3a, 0xf5, 0xbc, 0xee, 0x0d, 0x57, 0x5f, 0xfe, 0xc9, 0xbb, 0x65, 0xd9, 0xa0, 0x25, 0x6f, 0x3c, + 0x67, 0x26, 0x41, 0xff, 0xff, 0xfd, 0x21, 0x61, 0xf6, 0xe8, 0x83, 0x3c, 0x43, 0x61, 0x53, 0x6b, + 0xfc, 0x33, 0xa6, 0xbe, 0x35, 0x59, 0x89, 0xbf, 0xff, 0xca, 0x91, 0xd0, 0xc1, 0x31, 0xc1, 0xe7, + 0xbb, 0x89, 0xf6, 0x15, 0x87, 0x70, 0x29, 0xba, 0x64, 0xba, 0x7e, 0x88, 0xbf, 0xff, 0xb2, 0x30, + 0xd1, 0x95, 0xef, 0xe1, 0xec, 0x3b, 0xef, 0xd8, 0x40, 0x72, 0x7f, 0x53, 0x27, 0xc6, 0x4c, 0xfb, + 0xff, 0xfb, 0x23, 0x0d, 0x19, 0x5e, 0xfe, 0x1e, 0xc3, 0xbe, 0xfd, 0x84, 0x07, 0x27, 0xf5, 0x32, + 0x7c, 0x64, 0x69, 0x54, 0x07, 0x00, 0x21, 0xa1, 0xb8, 0x31, 0xe7, 0x92, 0x4d, 0x21, 0x98, 0xe1, + 0xe9, 0x7f, 0xff, 0x8d, 0x04, 0x56, 0x50, 0xf3, 0x87, 0x8e, 0xe9, 0x2c, 0x8b, 0x9a, 0x5d, 0x0e, + 0x69, 0x7a, 0xdf, 0xb3, 0x5f, 0xff, 0xe3, 0x40, 0xd3, 0x1c, 0xfe, 0x0e, 0x49, 0x93, 0x22, 0xcd, + 0xe3, 0xac, 0xa4, 0x02, 0xfb, 0xa5, 0x1b, 0xab, 0x9c, 0x9f, 0xff, 0xd6, 0x74, 0xad, 0x35, 0x38, + 0x54, 0x07, 0xfc, 0xf4, 0x47, 0x3e, 0xc8, 0x84, 0xfb, 0x24, 0x7a, 0xe0, 0x27, 0xff, 0xf5, 0x9d, + 0x13, 0x49, 0x92, 0x11, 0x5f, 0xc8, 0xd6, 0x12, 0xc1, 0xc0, 0x69, 0x64, 0x95, 0x44, 0x79, 0x86, + 0x49, 0xb4, 0x9f, 0xfe, 0x3f, 0x97, 0x6d, 0x34, 0x2f, 0xd0, 0xc9, 0x77, 0xb6, 0x27, 0xff, 0x8f, + 0xe5, 0xdb, 0x4d, 0x0b, 0xf4, 0x32, 0x5d, 0xe3, 0x55, 0x7d, 0xbf, 0xfd, 0x07, 0x21, 0x7d, 0xd6, + 0x5c, 0x66, 0xa7, 0xd9, 0x86, 0xaf, 0xff, 0x41, 0xc8, 0x5f, 0x75, 0x97, 0x19, 0xa9, 0xf6, 0x50, + 0x7a, 0x79, 0xc9, 0xff, 0xe5, 0x1b, 0x2a, 0x9d, 0xe9, 0xe9, 0x3d, 0x4b, 0x9a, 0x64, 0xff, 0xf1, + 0xfc, 0xbb, 0x69, 0xa1, 0x7e, 0x86, 0x4b, 0xbc, 0x6a, 0xfa, 0x17, 0xff, 0xa4, 0xbb, 0x55, 0x2a, + 0x0d, 0xeb, 0x17, 0x5e, 0x75, 0x2b, 0xff, 0xd0, 0x72, 0x17, 0xdd, 0x65, 0xc6, 0x6a, 0x7d, 0x94, + 0x1c, 0x72, 0x69, 0x71, 0xaf, 0xff, 0xe8, 0xc5, 0xf4, 0xd9, 0x5a, 0x12, 0x49, 0x24, 0xda, 0x28, + 0xa2, 0x8a, 0x28, 0x9d, 0x85, 0xff, 0xf9, 0x58, 0x68, 0xe1, 0xbe, 0x89, 0x87, 0x0a, 0x41, 0xbd, + 0x40, 0x5a, 0xbe, 0x24, 0x00, 0xa4, 0xd7, 0xb7, 0x6b, 0xc7, 0x0b, 0x74, 0x72, 0x39, 0x7e, 0x97, + 0xe9, 0x7e, 0x97, 0xe9, 0x88, 0xe8, 0x8e, 0x88, 0xe8, 0x8e, 0x88, 0xe8, 0x8e, 0x88, 0xe7, 0xf1, + 0x59, 0x7f, 0xff, 0xf5, 0x72, 0xea, 0x9c, 0xa5, 0xe0, 0xe5, 0xe5, 0x3e, 0x53, 0xe5, 0x3e, 0x53, + 0xe5, 0xd0, 0x5d, 0x05, 0xd0, 0x5d, 0x05, 0xd0, 0x5d, 0x05, 0xd0, 0x54, 0x5d, 0x80, 0x5f, 0xff, + 0x95, 0x86, 0x8e, 0x1b, 0xe8, 0x98, 0x70, 0xa4, 0x1b, 0xd4, 0x05, 0xe9, 0xaf, 0xff, 0xca, 0xc3, + 0x47, 0x0d, 0xf4, 0x4c, 0x38, 0x52, 0x0d, 0xea, 0x02, 0xd6, 0x96, 0x75, 0xfb, 0xff, 0xf4, 0xf5, + 0xfe, 0xc9, 0x8b, 0xed, 0xa0, 0xd4, 0x3f, 0xd4, 0x4e, 0xf3, 0xbb, 0xff, 0xf4, 0xf2, 0xb7, 0x3b, + 0x35, 0x34, 0x01, 0xbd, 0xcf, 0xc7, 0x6b, 0xc8, 0x86, 0xe2, 0xdf, 0xff, 0x95, 0xa6, 0x26, 0x6d, + 0x66, 0xef, 0x24, 0x4f, 0x9c, 0x50, 0x2e, 0x0b, 0x5f, 0xff, 0x95, 0x86, 0x8e, 0x1b, 0xe8, 0x98, + 0x70, 0xa4, 0x1b, 0xd4, 0x05, 0xad, 0x73, 0x9c, 0xf8, 0xd7, 0xff, 0xe5, 0x69, 0x89, 0x9b, 0x5a, + 0xd6, 0xc7, 0xef, 0x7b, 0xde, 0xf7, 0x16, 0xbf, 0xfd, 0x93, 0xe0, 0xd5, 0x22, 0xf4, 0x00, 0x3c, + 0x0c, 0x4d, 0xbe, 0x7f, 0xff, 0xfd, 0x40, 0x7a, 0x70, 0x28, 0xb4, 0x34, 0xd9, 0x35, 0x93, 0x59, + 0x35, 0x93, 0x59, 0x57, 0x15, 0x71, 0x57, 0x15, 0x71, 0x57, 0x15, 0x71, 0x57, 0x13, 0x6a, 0x84, + 0x01, 0x71, 0x0e, 0xa9, 0x4a, 0x52, 0x95, 0x06, 0xb5, 0xad, 0x6b, 0x51, 0x99, 0x0b, 0xdf, 0xfe, + 0x83, 0xd1, 0xc1, 0x15, 0xe1, 0x93, 0x97, 0xb2, 0x8b, 0xd5, 0xff, 0xe8, 0x3d, 0x1c, 0x11, 0x5e, + 0x19, 0x39, 0x7b, 0x28, 0x32, 0x5b, 0xf5, 0x6f, 0xff, 0x84, 0x61, 0x55, 0x1a, 0xb6, 0xa2, 0x05, + 0x17, 0xea, 0x77, 0xff, 0xbf, 0xa1, 0xda, 0x6b, 0x8b, 0x14, 0x60, 0xb1, 0x38, 0x81, 0x17, 0x97, + 0xff, 0xb5, 0x90, 0xaa, 0x87, 0x5e, 0xb7, 0x9d, 0xc1, 0x9a, 0xeb, 0xff, 0xd9, 0x3e, 0x0d, 0x52, + 0x2f, 0x40, 0x03, 0xc0, 0xc4, 0xe5, 0x54, 0xa6, 0xdf, 0xff, 0xa7, 0x95, 0xb9, 0xd9, 0xa9, 0xa0, + 0x0d, 0xee, 0x7e, 0x3b, 0x5e, 0x72, 0xef, 0xff, 0xd3, 0xca, 0xdc, 0xec, 0xd4, 0xd0, 0x06, 0xf7, + 0x3f, 0x1d, 0xaf, 0x22, 0x29, 0xf5, 0x7f, 0xfe, 0x56, 0x1a, 0x38, 0x6f, 0xa2, 0x61, 0xc2, 0x90, + 0x6f, 0x50, 0x17, 0xa6, 0xbf, 0xff, 0x2b, 0x0d, 0x1c, 0x37, 0xd1, 0x30, 0xe1, 0x48, 0x37, 0xa8, + 0x0b, 0x55, 0x39, 0xaf, 0xc0, 0x10, 0x61, 0x2d, 0x46, 0x41, 0x11, 0x62, 0x85, 0x8e, 0x38, 0x4f, + 0x9f, 0xff, 0xa0, 0xec, 0x48, 0x58, 0xc2, 0x3d, 0xd1, 0xc3, 0x19, 0xc3, 0x9c, 0x10, 0x5f, 0xff, + 0x9a, 0x0a, 0x59, 0x0b, 0x58, 0xda, 0x57, 0xf6, 0xb4, 0x6d, 0xdb, 0xd2, 0xd0, 0x74, 0xff, 0xfb, + 0x6e, 0x83, 0xdb, 0x08, 0x11, 0x73, 0xd9, 0x78, 0x19, 0x7e, 0x5b, 0xa4, 0xff, 0xfb, 0x6d, 0x28, + 0x6c, 0x1c, 0x47, 0xe7, 0xaa, 0x54, 0x14, 0x29, 0x10, 0x54, 0x84, 0xaf, 0x1f, 0xfe, 0x55, 0xcd, + 0xa1, 0xdc, 0xc5, 0x8c, 0x5f, 0x8b, 0xa9, 0x49, 0xff, 0xe5, 0x5c, 0xda, 0x1d, 0xcc, 0x58, 0xc5, + 0xf8, 0xb9, 0xf9, 0xf4, 0xab, 0xff, 0xda, 0xe2, 0xc5, 0x4b, 0x14, 0xd2, 0x8e, 0xe6, 0xbb, 0x98, + 0xbf, 0xfd, 0xae, 0x2c, 0x54, 0xb1, 0x4d, 0x28, 0xee, 0x6b, 0xab, 0xd2, 0x09, 0xed, 0xff, 0xee, + 0x2e, 0x2a, 0xa2, 0x37, 0x77, 0x7d, 0xbd, 0x0c, 0x42, 0xff, 0xf6, 0xb8, 0xb1, 0x52, 0xc5, 0x34, + 0xa3, 0xb9, 0xae, 0xae, 0xd0, 0x74, 0xff, 0xf3, 0x35, 0xea, 0xa4, 0x9a, 0x1c, 0x9b, 0xe5, 0xac, + 0xa2, 0x7f, 0xf9, 0x57, 0x36, 0x87, 0x73, 0x16, 0x31, 0x7e, 0x2e, 0x7e, 0xd0, 0x50, + ] + + static let bars709Full: [UInt8] = [ + 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0xff, 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, + 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, + 0x00, 0xff, 0xa0, 0x08, 0x08, 0x10, 0x59, 0x65, 0x66, 0x92, 0x4c, 0xae, 0x6e, 0x02, 0x02, 0x02, + 0x08, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x00, 0xc8, 0x40, 0x00, 0x00, 0x00, 0x01, + 0x44, 0x01, 0xc1, 0x71, 0xa9, 0x12, 0x00, 0x00, 0x01, 0x4e, 0x01, 0x05, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xd1, 0x2c, 0xa2, 0xde, 0x09, 0xb5, 0x17, 0x47, 0xdb, 0xbb, 0x55, 0xa4, + 0xfe, 0x7f, 0xc2, 0xfc, 0x4e, 0x78, 0x32, 0x36, 0x35, 0x20, 0x28, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x20, 0x32, 0x31, 0x36, 0x29, 0x20, 0x2d, 0x20, 0x34, 0x2e, 0x32, 0x2b, 0x31, 0x2d, 0x65, 0x34, + 0x34, 0x34, 0x37, 0x34, 0x34, 0x3a, 0x5b, 0x4d, 0x61, 0x63, 0x20, 0x4f, 0x53, 0x20, 0x58, 0x5d, + 0x5b, 0x63, 0x6c, 0x61, 0x6e, 0x67, 0x20, 0x32, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x5d, 0x5b, 0x36, + 0x34, 0x20, 0x62, 0x69, 0x74, 0x5d, 0x20, 0x38, 0x62, 0x69, 0x74, 0x2b, 0x31, 0x30, 0x62, 0x69, + 0x74, 0x2b, 0x31, 0x32, 0x62, 0x69, 0x74, 0x20, 0x2d, 0x20, 0x48, 0x2e, 0x32, 0x36, 0x35, 0x2f, + 0x48, 0x45, 0x56, 0x43, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x63, 0x20, 0x2d, 0x20, 0x43, 0x6f, 0x70, + 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x32, 0x30, 0x31, 0x33, 0x2d, 0x32, 0x30, 0x31, 0x38, + 0x20, 0x28, 0x63, 0x29, 0x20, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x63, 0x6f, 0x72, 0x65, 0x77, 0x61, + 0x72, 0x65, 0x2c, 0x20, 0x49, 0x6e, 0x63, 0x20, 0x2d, 0x20, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, + 0x2f, 0x78, 0x32, 0x36, 0x35, 0x2e, 0x6f, 0x72, 0x67, 0x20, 0x2d, 0x20, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x3a, 0x20, 0x63, 0x70, 0x75, 0x69, 0x64, 0x3d, 0x39, 0x38, 0x20, 0x66, 0x72, + 0x61, 0x6d, 0x65, 0x2d, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x3d, 0x31, 0x20, 0x6e, 0x6f, + 0x2d, 0x77, 0x70, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x70, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x6e, 0x6f, + 0x2d, 0x70, 0x6d, 0x65, 0x20, 0x6e, 0x6f, 0x2d, 0x70, 0x73, 0x6e, 0x72, 0x20, 0x6e, 0x6f, 0x2d, + 0x73, 0x73, 0x69, 0x6d, 0x20, 0x6c, 0x6f, 0x67, 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, + 0x20, 0x62, 0x69, 0x74, 0x64, 0x65, 0x70, 0x74, 0x68, 0x3d, 0x38, 0x20, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x2d, 0x63, 0x73, 0x70, 0x3d, 0x31, 0x20, 0x66, 0x70, 0x73, 0x3d, 0x32, 0x35, 0x2f, 0x31, + 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2d, 0x72, 0x65, 0x73, 0x3d, 0x32, 0x35, 0x36, 0x78, 0x36, + 0x34, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6c, 0x61, 0x63, 0x65, 0x3d, 0x30, 0x20, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x2d, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x3d, 0x30, 0x20, 0x6c, 0x65, 0x76, + 0x65, 0x6c, 0x2d, 0x69, 0x64, 0x63, 0x3d, 0x30, 0x20, 0x68, 0x69, 0x67, 0x68, 0x2d, 0x74, 0x69, + 0x65, 0x72, 0x3d, 0x31, 0x20, 0x75, 0x68, 0x64, 0x2d, 0x62, 0x64, 0x3d, 0x30, 0x20, 0x72, 0x65, + 0x66, 0x3d, 0x33, 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x2d, 0x6e, 0x6f, 0x6e, + 0x2d, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x20, 0x72, 0x65, 0x70, + 0x65, 0x61, 0x74, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x20, 0x61, 0x6e, 0x6e, 0x65, + 0x78, 0x62, 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x75, 0x64, 0x20, 0x6e, 0x6f, 0x2d, 0x65, 0x6f, 0x62, + 0x20, 0x6e, 0x6f, 0x2d, 0x65, 0x6f, 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x72, 0x64, 0x20, 0x69, + 0x6e, 0x66, 0x6f, 0x20, 0x68, 0x61, 0x73, 0x68, 0x3d, 0x30, 0x20, 0x74, 0x65, 0x6d, 0x70, 0x6f, + 0x72, 0x61, 0x6c, 0x2d, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x3d, 0x30, 0x20, 0x6f, 0x70, 0x65, + 0x6e, 0x2d, 0x67, 0x6f, 0x70, 0x20, 0x6d, 0x69, 0x6e, 0x2d, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, + 0x3d, 0x32, 0x35, 0x20, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, 0x3d, 0x32, 0x35, 0x30, 0x20, 0x67, + 0x6f, 0x70, 0x2d, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x3d, 0x30, 0x20, 0x62, + 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x3d, 0x34, 0x20, 0x62, 0x2d, 0x61, 0x64, 0x61, 0x70, 0x74, + 0x3d, 0x32, 0x20, 0x62, 0x2d, 0x70, 0x79, 0x72, 0x61, 0x6d, 0x69, 0x64, 0x20, 0x62, 0x66, 0x72, + 0x61, 0x6d, 0x65, 0x2d, 0x62, 0x69, 0x61, 0x73, 0x3d, 0x30, 0x20, 0x72, 0x63, 0x2d, 0x6c, 0x6f, + 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x3d, 0x32, 0x30, 0x20, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, + 0x68, 0x65, 0x61, 0x64, 0x2d, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x73, 0x3d, 0x30, 0x20, 0x73, 0x63, + 0x65, 0x6e, 0x65, 0x63, 0x75, 0x74, 0x3d, 0x34, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x69, 0x73, + 0x74, 0x2d, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x63, 0x75, 0x74, 0x20, 0x72, 0x61, 0x64, 0x6c, 0x3d, + 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x70, 0x6c, 0x69, 0x63, 0x65, 0x20, 0x6e, 0x6f, 0x2d, 0x69, + 0x6e, 0x74, 0x72, 0x61, 0x2d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x20, 0x63, 0x74, 0x75, + 0x3d, 0x36, 0x34, 0x20, 0x6d, 0x69, 0x6e, 0x2d, 0x63, 0x75, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3d, + 0x38, 0x20, 0x6e, 0x6f, 0x2d, 0x72, 0x65, 0x63, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x6d, 0x70, + 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x74, 0x75, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3d, 0x33, 0x32, 0x20, + 0x74, 0x75, 0x2d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x2d, 0x64, 0x65, 0x70, 0x74, 0x68, 0x3d, 0x31, + 0x20, 0x74, 0x75, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x2d, 0x64, 0x65, 0x70, 0x74, 0x68, 0x3d, + 0x31, 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x74, 0x75, 0x3d, 0x30, 0x20, 0x72, 0x64, 0x6f, + 0x71, 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x2d, 0x72, 0x64, 0x3d, 0x30, 0x2e, 0x30, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x73, 0x69, + 0x6d, 0x2d, 0x72, 0x64, 0x20, 0x73, 0x69, 0x67, 0x6e, 0x68, 0x69, 0x64, 0x65, 0x20, 0x6e, 0x6f, + 0x2d, 0x74, 0x73, 0x6b, 0x69, 0x70, 0x20, 0x6e, 0x72, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x3d, + 0x30, 0x20, 0x6e, 0x72, 0x2d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, + 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x64, 0x2d, 0x69, 0x6e, 0x74, 0x72, + 0x61, 0x20, 0x73, 0x74, 0x72, 0x6f, 0x6e, 0x67, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x2d, 0x73, + 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x6d, 0x65, 0x72, + 0x67, 0x65, 0x3d, 0x33, 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x72, 0x65, 0x66, 0x73, 0x3d, + 0x31, 0x20, 0x6e, 0x6f, 0x2d, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x6d, 0x6f, 0x64, 0x65, 0x73, + 0x20, 0x6d, 0x65, 0x3d, 0x31, 0x20, 0x73, 0x75, 0x62, 0x6d, 0x65, 0x3d, 0x32, 0x20, 0x6d, 0x65, + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x3d, 0x35, 0x37, 0x20, 0x74, 0x65, 0x6d, 0x70, 0x6f, 0x72, 0x61, + 0x6c, 0x2d, 0x6d, 0x76, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x2d, 0x64, + 0x75, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x6d, 0x65, 0x20, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, + 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x62, 0x20, 0x6e, 0x6f, 0x2d, + 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x7a, 0x65, 0x2d, 0x73, 0x72, 0x63, 0x2d, 0x70, 0x69, 0x63, 0x73, + 0x20, 0x64, 0x65, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x3d, 0x30, 0x3a, 0x30, 0x20, 0x73, 0x61, 0x6f, + 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x61, 0x6f, 0x2d, 0x6e, 0x6f, 0x6e, 0x2d, 0x64, 0x65, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x20, 0x72, 0x64, 0x3d, 0x33, 0x20, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x2d, 0x73, 0x61, 0x6f, 0x3d, 0x34, 0x20, 0x65, 0x61, 0x72, 0x6c, 0x79, 0x2d, 0x73, + 0x6b, 0x69, 0x70, 0x20, 0x72, 0x73, 0x6b, 0x69, 0x70, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x61, 0x73, + 0x74, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x20, 0x6e, 0x6f, 0x2d, 0x74, 0x73, 0x6b, 0x69, 0x70, + 0x2d, 0x66, 0x61, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x63, 0x75, 0x2d, 0x6c, 0x6f, 0x73, 0x73, + 0x6c, 0x65, 0x73, 0x73, 0x20, 0x62, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x20, 0x6e, 0x6f, 0x2d, + 0x73, 0x70, 0x6c, 0x69, 0x74, 0x72, 0x64, 0x2d, 0x73, 0x6b, 0x69, 0x70, 0x20, 0x72, 0x64, 0x70, + 0x65, 0x6e, 0x61, 0x6c, 0x74, 0x79, 0x3d, 0x30, 0x20, 0x70, 0x73, 0x79, 0x2d, 0x72, 0x64, 0x3d, + 0x32, 0x2e, 0x30, 0x30, 0x20, 0x70, 0x73, 0x79, 0x2d, 0x72, 0x64, 0x6f, 0x71, 0x3d, 0x30, 0x2e, + 0x30, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x72, 0x64, 0x2d, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x20, + 0x6c, 0x6f, 0x73, 0x73, 0x6c, 0x65, 0x73, 0x73, 0x20, 0x63, 0x62, 0x71, 0x70, 0x6f, 0x66, 0x66, + 0x73, 0x3d, 0x30, 0x20, 0x63, 0x72, 0x71, 0x70, 0x6f, 0x66, 0x66, 0x73, 0x3d, 0x30, 0x20, 0x72, + 0x63, 0x3d, 0x63, 0x71, 0x70, 0x20, 0x71, 0x70, 0x3d, 0x34, 0x20, 0x69, 0x70, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x3d, 0x31, 0x2e, 0x34, 0x30, 0x20, 0x70, 0x62, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x3d, + 0x31, 0x2e, 0x33, 0x30, 0x20, 0x61, 0x71, 0x2d, 0x6d, 0x6f, 0x64, 0x65, 0x3d, 0x30, 0x20, 0x61, + 0x71, 0x2d, 0x73, 0x74, 0x72, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x3d, 0x30, 0x2e, 0x30, 0x30, 0x20, + 0x6e, 0x6f, 0x2d, 0x63, 0x75, 0x74, 0x72, 0x65, 0x65, 0x20, 0x7a, 0x6f, 0x6e, 0x65, 0x2d, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, + 0x2d, 0x63, 0x62, 0x72, 0x20, 0x71, 0x67, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3d, 0x36, 0x34, 0x20, + 0x6e, 0x6f, 0x2d, 0x72, 0x63, 0x2d, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x20, 0x71, 0x70, 0x6d, 0x61, + 0x78, 0x3d, 0x36, 0x39, 0x20, 0x71, 0x70, 0x6d, 0x69, 0x6e, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, + 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x2d, 0x76, 0x62, 0x76, 0x20, 0x73, 0x61, 0x72, 0x3d, 0x30, 0x20, + 0x6f, 0x76, 0x65, 0x72, 0x73, 0x63, 0x61, 0x6e, 0x3d, 0x30, 0x20, 0x76, 0x69, 0x64, 0x65, 0x6f, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x3d, 0x35, 0x20, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x3d, 0x31, + 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x70, 0x72, 0x69, 0x6d, 0x3d, 0x31, 0x20, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x66, 0x65, 0x72, 0x3d, 0x31, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x72, 0x69, 0x78, 0x3d, 0x31, 0x20, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x61, 0x6c, 0x6f, 0x63, 0x3d, + 0x30, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x2d, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, + 0x3d, 0x30, 0x20, 0x63, 0x6c, 0x6c, 0x3d, 0x30, 0x2c, 0x30, 0x20, 0x6d, 0x69, 0x6e, 0x2d, 0x6c, + 0x75, 0x6d, 0x61, 0x3d, 0x30, 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x6c, 0x75, 0x6d, 0x61, 0x3d, 0x32, + 0x35, 0x35, 0x20, 0x6c, 0x6f, 0x67, 0x32, 0x2d, 0x6d, 0x61, 0x78, 0x2d, 0x70, 0x6f, 0x63, 0x2d, + 0x6c, 0x73, 0x62, 0x3d, 0x38, 0x20, 0x76, 0x75, 0x69, 0x2d, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, + 0x2d, 0x69, 0x6e, 0x66, 0x6f, 0x20, 0x76, 0x75, 0x69, 0x2d, 0x68, 0x72, 0x64, 0x2d, 0x69, 0x6e, + 0x66, 0x6f, 0x20, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x73, 0x3d, 0x31, 0x20, 0x6e, 0x6f, 0x2d, 0x6f, + 0x70, 0x74, 0x2d, 0x71, 0x70, 0x2d, 0x70, 0x70, 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x6f, 0x70, 0x74, + 0x2d, 0x72, 0x65, 0x66, 0x2d, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, + 0x2d, 0x70, 0x70, 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x2d, 0x70, 0x61, + 0x73, 0x73, 0x2d, 0x6f, 0x70, 0x74, 0x2d, 0x72, 0x70, 0x73, 0x20, 0x73, 0x63, 0x65, 0x6e, 0x65, + 0x63, 0x75, 0x74, 0x2d, 0x62, 0x69, 0x61, 0x73, 0x3d, 0x30, 0x2e, 0x30, 0x35, 0x20, 0x6e, 0x6f, + 0x2d, 0x6f, 0x70, 0x74, 0x2d, 0x63, 0x75, 0x2d, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x2d, 0x71, 0x70, + 0x20, 0x6e, 0x6f, 0x2d, 0x61, 0x71, 0x2d, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x6f, + 0x2d, 0x68, 0x64, 0x72, 0x31, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x64, 0x72, 0x31, 0x30, 0x2d, + 0x6f, 0x70, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x64, 0x68, 0x64, 0x72, 0x31, 0x30, 0x2d, 0x6f, 0x70, + 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x69, 0x64, 0x72, 0x2d, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, + 0x79, 0x2d, 0x73, 0x65, 0x69, 0x20, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2d, 0x72, + 0x65, 0x75, 0x73, 0x65, 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x61, 0x6e, 0x61, + 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2d, 0x73, 0x61, 0x76, 0x65, 0x2d, 0x72, 0x65, 0x75, 0x73, 0x65, + 0x2d, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, + 0x73, 0x2d, 0x6c, 0x6f, 0x61, 0x64, 0x2d, 0x72, 0x65, 0x75, 0x73, 0x65, 0x2d, 0x6c, 0x65, 0x76, + 0x65, 0x6c, 0x3d, 0x30, 0x20, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2d, 0x66, 0x61, 0x63, 0x74, 0x6f, + 0x72, 0x3d, 0x30, 0x20, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x69, 0x6e, 0x74, 0x72, 0x61, + 0x3d, 0x30, 0x20, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3d, + 0x30, 0x20, 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x6d, 0x76, 0x3d, 0x31, 0x20, 0x72, 0x65, + 0x66, 0x69, 0x6e, 0x65, 0x2d, 0x63, 0x74, 0x75, 0x2d, 0x64, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x74, + 0x69, 0x6f, 0x6e, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2d, 0x73, + 0x61, 0x6f, 0x20, 0x63, 0x74, 0x75, 0x2d, 0x69, 0x6e, 0x66, 0x6f, 0x3d, 0x30, 0x20, 0x6e, 0x6f, + 0x2d, 0x6c, 0x6f, 0x77, 0x70, 0x61, 0x73, 0x73, 0x2d, 0x64, 0x63, 0x74, 0x20, 0x72, 0x65, 0x66, + 0x69, 0x6e, 0x65, 0x2d, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2d, 0x74, 0x79, 0x70, + 0x65, 0x3d, 0x30, 0x20, 0x63, 0x6f, 0x70, 0x79, 0x2d, 0x70, 0x69, 0x63, 0x3d, 0x31, 0x20, 0x6d, + 0x61, 0x78, 0x2d, 0x61, 0x75, 0x73, 0x69, 0x7a, 0x65, 0x2d, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x3d, 0x31, 0x2e, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x2d, + 0x72, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, + 0x2d, 0x73, 0x65, 0x69, 0x20, 0x6e, 0x6f, 0x2d, 0x68, 0x65, 0x76, 0x63, 0x2d, 0x61, 0x71, 0x20, + 0x6e, 0x6f, 0x2d, 0x73, 0x76, 0x74, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, + 0x71, 0x70, 0x2d, 0x61, 0x64, 0x61, 0x70, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x72, 0x61, + 0x6e, 0x67, 0x65, 0x3d, 0x31, 0x2e, 0x30, 0x30, 0x20, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x63, 0x75, + 0x74, 0x2d, 0x61, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x71, 0x70, 0x3d, 0x30, 0x63, 0x6f, 0x6e, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2d, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2d, 0x6f, + 0x66, 0x66, 0x73, 0x65, 0x74, 0x73, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x3d, 0x30, 0x20, 0x62, + 0x6f, 0x74, 0x74, 0x6f, 0x6d, 0x3d, 0x30, 0x20, 0x64, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2d, + 0x6d, 0x61, 0x78, 0x2d, 0x72, 0x61, 0x74, 0x65, 0x3d, 0x30, 0x20, 0x6e, 0x6f, 0x2d, 0x76, 0x62, + 0x76, 0x2d, 0x6c, 0x69, 0x76, 0x65, 0x2d, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x2d, 0x70, 0x61, 0x73, + 0x73, 0x20, 0x6e, 0x6f, 0x2d, 0x6d, 0x63, 0x73, 0x74, 0x66, 0x20, 0x6e, 0x6f, 0x2d, 0x73, 0x62, + 0x72, 0x63, 0x20, 0x6e, 0x6f, 0x2d, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x2d, 0x72, 0x63, 0x80, 0x00, + 0x00, 0x01, 0x28, 0x01, 0xaf, 0x05, 0xb8, 0x10, 0x0c, 0x7f, 0x80, 0xd7, 0x00, 0xc4, 0xca, 0xe6, + 0x94, 0x42, 0x38, 0x60, 0x38, 0x52, 0x45, 0x24, 0x52, 0x45, 0x24, 0x55, 0x59, 0x55, 0x95, 0x59, + 0x55, 0x95, 0x59, 0x55, 0x95, 0x59, 0x56, 0xb4, 0xcb, 0x51, 0xc8, 0xd6, 0xa0, 0x35, 0xd5, 0x55, + 0xa4, 0x40, 0xd9, 0x57, 0x3f, 0xff, 0x91, 0xc3, 0x2c, 0x30, 0xc3, 0x0c, 0x31, 0x1c, 0x71, 0xc7, + 0x1c, 0x71, 0xc7, 0x85, 0x3f, 0xff, 0xda, 0x86, 0xda, 0x58, 0x75, 0x4a, 0x3b, 0xc1, 0xa2, 0x82, + 0x3d, 0xff, 0xff, 0x22, 0x76, 0xbf, 0xff, 0xfc, 0xd8, 0x79, 0x99, 0x24, 0xe8, 0x0f, 0x75, 0xff, + 0x5f, 0xf5, 0xff, 0x5f, 0xf6, 0x2e, 0xe2, 0xee, 0x2e, 0xe2, 0xee, 0x2e, 0xe2, 0xee, 0x2e, 0xe0, + 0x9c, 0xff, 0xf6, 0x3d, 0xd0, 0x03, 0xeb, 0xce, 0x73, 0x9c, 0xe7, 0x3a, 0xd6, 0xb5, 0xad, 0x6b, + 0x4c, 0xf4, 0xa0, 0x03, 0x93, 0xff, 0xe6, 0x75, 0xd0, 0x3d, 0xcf, 0xf0, 0x4e, 0xd2, 0x72, 0x8d, + 0x74, 0x7f, 0xfe, 0x89, 0x44, 0x87, 0x74, 0x94, 0xe6, 0x55, 0x90, 0x59, 0x45, 0xa5, 0x9a, 0x2e, + 0x4f, 0x7f, 0xfc, 0xeb, 0x9a, 0x21, 0xb2, 0xc8, 0x3c, 0x30, 0xf4, 0x24, 0x7a, 0x3f, 0xff, 0x44, + 0xa2, 0x43, 0xba, 0x4a, 0x73, 0x2a, 0xc8, 0x2c, 0x9a, 0x79, 0xa3, 0xe3, 0xff, 0xeb, 0x8f, 0x95, + 0x52, 0x72, 0x52, 0x34, 0xc8, 0xe9, 0x8b, 0x44, 0xff, 0xf6, 0xf0, 0x3a, 0x83, 0x7c, 0x5c, 0xce, + 0xd4, 0xba, 0x89, 0x3f, 0x2b, 0x0c, 0x90, 0x70, 0xa0, 0x00, 0xf0, 0x2c, 0x88, 0xe9, 0x8e, 0x1f, + 0xe0, 0x06, 0x79, 0xa1, 0x79, 0x80, 0x12, 0x3f, 0xff, 0x7d, 0xbc, 0x64, 0xb9, 0x97, 0xb8, 0xe8, + 0x1b, 0x03, 0xeb, 0x6f, 0xff, 0xca, 0xe9, 0xb5, 0x52, 0xa6, 0x29, 0x29, 0xfc, 0xfb, 0x8c, 0x40, + 0x1c, 0x5f, 0xff, 0x35, 0x77, 0x52, 0xf6, 0x79, 0xec, 0xa4, 0x7e, 0x2a, 0xdf, 0xaf, 0xff, 0x81, + 0xc0, 0x3b, 0x0b, 0x8b, 0x27, 0xcf, 0x6a, 0xd7, 0x4c, 0xec, 0x11, 0x59, 0xff, 0xf6, 0xe2, 0x9f, + 0xfe, 0xb6, 0x70, 0xc4, 0x83, 0x1c, 0x05, 0xbb, 0xff, 0xf0, 0x0d, 0x33, 0xc8, 0xb1, 0xeb, 0x05, + 0x9f, 0x5c, 0xc7, 0x20, 0xdf, 0x8f, 0xff, 0x67, 0x88, 0xff, 0xea, 0x77, 0x5e, 0x4b, 0x79, 0xd4, + 0xd2, 0x3f, 0xfe, 0xc4, 0x27, 0x79, 0x7b, 0xdc, 0x96, 0x40, 0xcd, 0xc7, 0x6f, 0xa8, 0x0a, 0x28, + 0xcf, 0xff, 0xfe, 0xef, 0xdb, 0xd4, 0xd0, 0x3c, 0x78, 0xf1, 0xe3, 0xd0, 0x20, 0x40, 0x81, 0x02, + 0x04, 0x07, 0x43, 0xff, 0xff, 0x55, 0x80, 0x26, 0x9f, 0xe0, 0xb3, 0x64, 0xa3, 0x87, 0x88, 0x9c, + 0xee, 0x95, 0x9f, 0xda, 0x98, 0x80, 0x2c, 0xf5, 0xd9, 0x3a, 0xe6, 0xfa, 0xd0, 0x75, 0x46, 0xd0, + 0x2d, 0x02, 0xd0, 0x2d, 0x02, 0xd0, 0xe1, 0x0e, 0x10, 0xe1, 0x0e, 0x10, 0xe1, 0x0e, 0x10, 0xe1, + 0x03, 0xb7, 0xf9, 0xaf, 0xdf, 0xff, 0xfc, 0x4a, 0xae, 0x23, 0x6e, 0x1b, 0xcb, 0xd4, 0x2e, 0x02, + 0xe0, 0x2e, 0x02, 0xe0, 0x2e, 0xf6, 0xef, 0x6e, 0xf6, 0xef, 0x6e, 0xf6, 0xef, 0x6e, 0xf6, 0xe7, + 0xc8, 0x1b, 0x7f, 0xff, 0xbb, 0x0c, 0x4b, 0xdb, 0x02, 0x66, 0xaa, 0x94, 0xf4, 0xed, 0xd6, 0x0d, + 0xd4, 0x3f, 0xff, 0xb1, 0xa5, 0x3e, 0xed, 0x3c, 0xe9, 0x07, 0xa9, 0x44, 0xdf, 0xb9, 0x10, 0x00, + 0x34, 0x32, 0x13, 0xff, 0xef, 0x84, 0xe7, 0xf8, 0x61, 0x80, 0x24, 0xdf, 0xcb, 0x21, 0xe5, 0xb1, + 0x52, 0x09, 0xff, 0xfa, 0xbb, 0x0c, 0x7c, 0xe1, 0xdb, 0x30, 0xf6, 0x95, 0x55, 0xf5, 0x15, 0xdf, + 0x61, 0x17, 0x9f, 0xff, 0x9d, 0xf9, 0xe9, 0x79, 0xe7, 0x7b, 0x14, 0x7a, 0x36, 0x0d, 0x1b, 0xc7, + 0x1c, 0xf7, 0xff, 0xe1, 0xfd, 0x7f, 0x44, 0xd8, 0x9b, 0xcf, 0x7a, 0x6e, 0x17, 0x38, 0xcc, 0x8c, + 0xc2, 0x0f, 0x4f, 0x57, 0xff, 0xf7, 0x55, 0x2b, 0x2a, 0xaa, 0xaa, 0xaa, 0xb2, 0x79, 0xe7, 0x9e, + 0x79, 0xe7, 0x59, 0xbf, 0xfe, 0xe7, 0x49, 0x8d, 0x06, 0x01, 0xfc, 0x41, 0x0b, 0xa7, 0x35, 0xc7, + 0x13, 0x27, 0xff, 0xff, 0x14, 0x1e, 0x21, 0x48, 0x15, 0xf9, 0xbc, 0xbd, 0xcb, 0xdc, 0xbd, 0xcb, + 0xdc, 0xcb, 0xac, 0xba, 0xcb, 0xac, 0xba, 0xcb, 0xac, 0xba, 0xcb, 0xab, 0xe2, 0x36, 0x25, 0x80, + 0x2f, 0xfb, 0x08, 0xc6, 0x31, 0x8c, 0x6f, 0xad, 0x6b, 0x5a, 0xd6, 0x36, 0x03, 0x4b, 0xff, 0xeb, + 0xc5, 0x63, 0x41, 0x65, 0x0a, 0x0c, 0x08, 0x18, 0xba, 0xd7, 0xff, 0xf7, 0xf2, 0x5f, 0x53, 0x0e, + 0xbe, 0xd7, 0xe6, 0x1a, 0xd5, 0x75, 0x30, 0xa2, 0xaf, 0xff, 0x5e, 0x5e, 0x52, 0x94, 0x4f, 0x21, + 0x29, 0x08, 0xa5, 0xbf, 0xbc, 0xff, 0xfb, 0xc1, 0xf6, 0xc9, 0x77, 0xc4, 0x88, 0x96, 0xee, 0x22, + 0xbd, 0x16, 0x4f, 0xff, 0xea, 0x6c, 0x84, 0x20, 0xfc, 0xc6, 0x13, 0x44, 0x4d, 0x48, 0x56, 0x7f, + 0xff, 0x5d, 0x66, 0xc1, 0x99, 0x29, 0xb7, 0xe2, 0x87, 0x73, 0x1f, 0xdc, 0x02, 0xf5, 0xff, 0xf8, + 0x93, 0x6e, 0xdb, 0xb9, 0xee, 0x66, 0x4b, 0x3c, 0xfd, 0x63, 0xc6, 0x4c, 0x7b, 0xff, 0xf2, 0x3f, + 0xb0, 0x4a, 0xa8, 0xc5, 0x0e, 0x87, 0x4e, 0x84, 0xf5, 0x99, 0x36, 0x80, 0xac, 0x7f, 0xfe, 0x4f, + 0x7b, 0xad, 0x46, 0x83, 0x6c, 0xb9, 0xa6, 0x5d, 0x76, 0xca, 0x57, 0x08, 0xff, 0xfc, 0x76, 0xd9, + 0x45, 0x85, 0x77, 0xf4, 0x9a, 0x09, 0xcb, 0xc2, 0x6e, 0x53, 0x03, 0xa7, 0x64, 0x00, 0x78, 0x92, + 0xba, 0xea, 0xdd, 0xb9, 0xae, 0xec, 0xc9, 0xdb, 0x6a, 0xab, 0xff, 0xf6, 0x2c, 0x94, 0x07, 0x9e, + 0x76, 0x5b, 0x4e, 0x17, 0x35, 0x0b, 0xad, 0xd6, 0x79, 0xff, 0xfb, 0x5b, 0x84, 0xd1, 0x00, 0x16, + 0xc4, 0x5b, 0xa1, 0x12, 0xa1, 0xaf, 0x57, 0xd5, 0x8a, 0x3f, 0xff, 0x22, 0xe5, 0xff, 0xe7, 0x9e, + 0x2a, 0x10, 0xbe, 0x98, 0xff, 0x4e, 0x32, 0x46, 0x9f, 0xff, 0x90, 0x1c, 0x19, 0x58, 0xeb, 0x75, + 0xba, 0x0b, 0x50, 0xf1, 0xa6, 0xfd, 0x2c, 0xf7, 0x4a, 0xff, 0xff, 0xae, 0xb3, 0x60, 0xcc, 0x94, + 0xdb, 0xf1, 0x43, 0xb9, 0x93, 0xab, 0xff, 0xd7, 0x10, 0x0f, 0x3b, 0x8a, 0xf4, 0xc9, 0x7c, 0x5d, + 0xbb, 0xf2, 0xf3, 0xbf, 0xfe, 0x66, 0x6f, 0x82, 0x36, 0xed, 0xe9, 0xbf, 0x6d, 0x54, 0xc7, 0x9f, + 0xff, 0x31, 0xaf, 0x10, 0x37, 0x79, 0xc3, 0xe4, 0x44, 0x04, 0xc7, 0x9b, 0xa2, 0xaf, 0xff, 0xd6, + 0xbd, 0x8c, 0x63, 0x02, 0xd0, 0x1b, 0xdc, 0x6f, 0xdb, 0x96, 0x9f, 0xfe, 0xb0, 0xd5, 0x05, 0x68, + 0x65, 0xb1, 0x62, 0xa5, 0xb2, 0x19, 0xb5, 0x97, 0xff, 0xf2, 0xf0, 0xd6, 0xb5, 0xa6, 0x77, 0xe3, + 0x3d, 0x8d, 0x17, 0xb5, 0x4f, 0xff, 0x95, 0xc6, 0x26, 0x53, 0xc2, 0x7f, 0xfb, 0x3c, 0xb7, 0x47, + 0xc6, 0xca, 0x79, 0xc8, 0x7f, 0xff, 0xea, 0x14, 0x53, 0xa7, 0x46, 0x89, 0xb3, 0x0b, 0x10, 0xb1, + 0x0b, 0x10, 0xb1, 0x0c, 0x12, 0xc1, 0x2c, 0x12, 0xc1, 0x2c, 0x12, 0xc1, 0x2c, 0x12, 0xbf, 0xcb, + 0xff, 0xfe, 0x78, 0xff, 0xb5, 0x5e, 0xcd, 0x7d, 0xb8, 0x9e, 0x9b, 0xf6, 0x77, 0x9c, 0xb6, 0x6b, + 0xb3, 0xd6, 0xbc, 0x5a, 0x3a, 0x85, 0x00, 0x5d, 0x6e, 0x8f, 0x57, 0x3b, 0xf7, 0x0d, 0x2d, 0x5f, + 0x2f, 0xa4, 0xbe, 0x92, 0xfa, 0x4b, 0xe9, 0x30, 0x02, 0x40, 0x09, 0x00, 0x24, 0x00, 0x90, 0x02, + 0x40, 0x09, 0x00, 0x23, 0xe9, 0x97, 0xae, 0xc0, 0x38, 0xb7, 0x4c, 0x8b, 0x9f, 0x2b, 0x82, 0xc6, + 0x93, 0xf6, 0xc3, 0xdb, 0x0f, 0x6c, 0x3d, 0xb0, 0xf6, 0xfc, 0x9b, 0xf2, 0x6f, 0xc9, 0xbf, 0x26, + 0xfc, 0x9b, 0xf2, 0x6f, 0xc9, 0xb8, 0xd8, 0x91, 0x7f, 0xff, 0xde, 0xf2, 0x9f, 0x72, 0x5f, 0xa4, + 0xdf, 0x42, 0xd1, 0x53, 0xe9, 0xe1, 0x81, 0x9b, 0x85, 0x88, 0x7b, 0xb1, 0xf1, 0xff, 0xfd, 0xed, + 0xc1, 0xe8, 0x25, 0x65, 0xdd, 0x23, 0xcd, 0xe2, 0x3c, 0xbd, 0xd5, 0x70, 0xef, 0xfc, 0x2b, 0x7e, + 0xd8, 0x76, 0x9a, 0x5f, 0xff, 0xef, 0x62, 0xf8, 0x1e, 0xc4, 0x28, 0xd5, 0xd5, 0x53, 0x4b, 0xb8, + 0x01, 0x3b, 0x94, 0x00, 0x9d, 0xe8, 0x5e, 0x76, 0xaf, 0xff, 0xef, 0x57, 0x8e, 0x51, 0x21, 0xe7, + 0xdc, 0x18, 0x7b, 0xe1, 0xc7, 0xea, 0x80, 0xfa, 0xfa, 0x1b, 0x98, 0x35, 0x31, 0x02, 0x2f, 0xff, + 0xec, 0x5a, 0x9f, 0x51, 0x14, 0xaa, 0x31, 0xb1, 0x17, 0x53, 0x71, 0xf9, 0x54, 0xac, 0xfc, 0xaa, + 0x7a, 0x67, 0x94, 0x7f, 0xff, 0xec, 0x4f, 0x2d, 0xf7, 0xe1, 0xd4, 0x9a, 0xcb, 0x35, 0xe5, 0xbc, + 0x5a, 0xc5, 0x0a, 0xf2, 0xf4, 0xb6, 0x9e, 0x56, 0x89, 0x79, 0x8c, 0xff, 0xfe, 0xc2, 0x0e, 0xc5, + 0x55, 0x55, 0x55, 0x56, 0x73, 0xcf, 0x3c, 0xf3, 0xcf, 0x44, 0x4f, 0xff, 0xc9, 0xed, 0xa4, 0x9b, + 0x63, 0x01, 0x60, 0x76, 0xb4, 0x22, 0x96, 0xdd, 0xff, 0xff, 0xe8, 0x9f, 0xd0, 0x8f, 0x37, 0x39, + 0x09, 0x7a, 0x97, 0xa9, 0x7a, 0x97, 0xa9, 0x7b, 0xf7, 0xbf, 0x7b, 0xf7, 0xbf, 0x7b, 0xf7, 0xbf, + 0x7b, 0xf7, 0xaa, 0x37, 0xc8, 0x01, 0x25, 0x4b, 0xfb, 0xde, 0xf7, 0xbe, 0x3c, 0x63, 0x18, 0xc6, + 0x2e, 0xb9, 0xdf, 0x3f, 0xff, 0x37, 0x0d, 0x7b, 0x55, 0x6f, 0xef, 0x54, 0x55, 0x48, 0x47, 0xaf, + 0xff, 0x93, 0x79, 0x1c, 0xfd, 0xc6, 0xb6, 0x82, 0x10, 0xbe, 0xf2, 0x94, 0x8b, 0xb5, 0xff, 0xf2, + 0x9c, 0x0c, 0x63, 0x11, 0xa5, 0x98, 0x4e, 0xe1, 0x5d, 0xe0, 0xaf, 0xff, 0x93, 0x79, 0x1c, 0xfd, + 0xc6, 0xb6, 0x82, 0x10, 0xbe, 0xf2, 0x80, 0x2b, 0x7f, 0xfc, 0xa7, 0x03, 0x18, 0xc4, 0x69, 0x66, + 0x13, 0xb8, 0x57, 0x78, 0x2b, 0xff, 0xe4, 0xde, 0x47, 0x3f, 0x71, 0xad, 0xa0, 0x84, 0x2f, 0xbc, + 0xa9, 0x90, 0x7f, 0xff, 0xff, 0x61, 0x11, 0x60, 0xbe, 0x7a, 0x34, 0x05, 0xfa, 0x7c, 0x2c, 0x02, + 0x93, 0x7f, 0x8f, 0x3b, 0x49, 0x78, 0xe9, 0x6f, 0xff, 0xf4, 0x70, 0xc4, 0xb3, 0x3c, 0x00, 0xcf, + 0x71, 0x94, 0xc8, 0x94, 0xdc, 0x24, 0xd1, 0x48, 0xe9, 0xf2, 0x10, 0xa7, 0xa2, 0x2f, 0xff, 0xef, + 0x2a, 0x8c, 0x71, 0x0f, 0x59, 0xc2, 0x0c, 0x95, 0x81, 0x8b, 0xe2, 0x2b, 0xe1, 0xee, 0x90, 0x10, + 0xb3, 0x86, 0xff, 0xfe, 0xf2, 0xa8, 0xc7, 0x10, 0xf5, 0x9c, 0x20, 0xc9, 0x58, 0x18, 0xbe, 0x22, + 0xbe, 0x1e, 0xe9, 0x01, 0x0b, 0x1e, 0xd5, 0x01, 0xc0, 0x08, 0x99, 0x6e, 0xc0, 0x1b, 0x0a, 0x14, + 0xed, 0x5f, 0x98, 0xe1, 0xe9, 0x7f, 0xff, 0x9c, 0x7e, 0xd2, 0x28, 0xb1, 0xf8, 0xa4, 0xa1, 0x96, + 0xc9, 0x93, 0x29, 0x3e, 0xe9, 0x94, 0xa0, 0x2a, 0xab, 0x01, 0x7f, 0xff, 0x9c, 0x7d, 0xda, 0x96, + 0xf9, 0xd2, 0x69, 0xb0, 0xa7, 0x3b, 0x45, 0x93, 0xda, 0xa0, 0x79, 0x82, 0x63, 0xd1, 0xee, 0x72, + 0x7f, 0xff, 0x70, 0x31, 0x67, 0xd0, 0x12, 0x65, 0x89, 0x85, 0x1f, 0x93, 0x7d, 0xd7, 0x33, 0x1e, + 0xeb, 0x9a, 0x96, 0x50, 0x7e, 0x7f, 0xff, 0x70, 0x30, 0x02, 0x04, 0xb5, 0xf0, 0xc5, 0xf9, 0xe0, + 0x80, 0x2d, 0x60, 0x15, 0xf9, 0x1c, 0x28, 0xfe, 0xa0, 0x24, 0x9b, 0x49, 0xff, 0xec, 0x64, 0xeb, + 0x1e, 0x03, 0xe5, 0xf2, 0x2e, 0xab, 0xf9, 0x94, 0xff, 0xf6, 0x32, 0x75, 0x8f, 0x01, 0xf2, 0xf9, + 0x17, 0x55, 0xfa, 0x3d, 0x5f, 0x6f, 0xff, 0x7a, 0xee, 0x0e, 0xbd, 0xf3, 0xa6, 0xca, 0xe6, 0x65, + 0xee, 0xd7, 0xff, 0xbd, 0x77, 0x07, 0x5e, 0xf9, 0xd3, 0x65, 0x73, 0x32, 0xe6, 0x3a, 0x79, 0xc9, + 0xff, 0xec, 0xa5, 0xd2, 0x94, 0x9a, 0xdc, 0x53, 0x86, 0xce, 0x4c, 0x7d, 0x4f, 0xff, 0x63, 0x27, + 0x58, 0xf0, 0x1f, 0x2f, 0x91, 0x75, 0x5f, 0xa3, 0xdf, 0x42, 0xff, 0xf7, 0xca, 0x69, 0x4a, 0x4e, + 0x36, 0x07, 0xe4, 0x5f, 0xa6, 0x64, 0xd7, 0xff, 0xbd, 0x77, 0x07, 0x5e, 0xf9, 0xd3, 0x65, 0x73, + 0x32, 0xe6, 0x31, 0xc9, 0xa5, 0xc6, 0xbf, 0xff, 0xb9, 0x36, 0x97, 0x97, 0x1b, 0x06, 0x0c, 0x18, + 0x32, 0x9b, 0x36, 0x6c, 0xd9, 0xb3, 0x66, 0x6f, 0x5f, 0xff, 0xa7, 0x4d, 0x7d, 0x81, 0x4a, 0x12, + 0x07, 0xa6, 0x03, 0x36, 0x99, 0x29, 0xfb, 0x89, 0x00, 0x29, 0x36, 0xb1, 0xcb, 0x53, 0xbb, 0x3f, + 0x16, 0x2b, 0x94, 0x29, 0x42, 0x94, 0x29, 0x42, 0x94, 0xce, 0x4c, 0xe4, 0xce, 0x4c, 0xe4, 0xce, + 0x4c, 0xe4, 0xce, 0x43, 0x05, 0x97, 0xff, 0xff, 0x71, 0xfd, 0xb8, 0xb5, 0xb7, 0x91, 0x60, 0x43, + 0xbc, 0x3b, 0xc3, 0xbc, 0x3b, 0xc3, 0xe0, 0xbe, 0x0b, 0xe0, 0xbe, 0x0b, 0xe0, 0xbe, 0x0b, 0xe0, + 0xbb, 0xdf, 0x60, 0x17, 0xff, 0xe9, 0xd3, 0x5f, 0x60, 0x52, 0x84, 0x81, 0xe9, 0x80, 0xcd, 0xa6, + 0x4a, 0xbc, 0xaf, 0xff, 0xd3, 0xa6, 0xbe, 0xc0, 0xa5, 0x09, 0x03, 0xd3, 0x01, 0x9b, 0x4c, 0x95, + 0x00, 0x59, 0xd7, 0xef, 0xff, 0xdb, 0x33, 0xb2, 0x06, 0x18, 0x54, 0x3c, 0x70, 0xf1, 0xf0, 0x79, + 0x40, 0x76, 0xf7, 0xff, 0xed, 0x96, 0x31, 0x59, 0x07, 0xe9, 0xf6, 0x47, 0xc7, 0x39, 0xb1, 0xe3, + 0x67, 0x37, 0x16, 0xff, 0xfd, 0x3a, 0xf8, 0xc4, 0xf3, 0xce, 0x3c, 0xec, 0x73, 0x29, 0xb9, 0x9a, + 0x4c, 0xa5, 0x7f, 0xfe, 0x9d, 0x35, 0xf6, 0x05, 0x28, 0x48, 0x1e, 0x98, 0x0c, 0xda, 0x64, 0xa8, + 0x07, 0x39, 0xcf, 0x8d, 0x7f, 0xfe, 0x9d, 0x7c, 0x62, 0x79, 0xe7, 0x9e, 0x7b, 0x07, 0x1c, 0x71, + 0xc7, 0x1c, 0x5a, 0x57, 0xff, 0xc0, 0xe0, 0x1d, 0x85, 0xc5, 0x93, 0xe7, 0xb5, 0x6b, 0x38, 0x79, + 0xff, 0xff, 0xf6, 0xf8, 0x6d, 0xad, 0xb2, 0x85, 0x51, 0x0c, 0xb0, 0xcb, 0x0c, 0xb0, 0xcb, 0x0d, + 0x36, 0xd3, 0x6d, 0x36, 0xd3, 0x6d, 0x36, 0xd3, 0x6d, 0x36, 0xcb, 0x4a, 0x10, 0x05, 0xd0, 0x23, + 0x08, 0x42, 0x10, 0x85, 0xa7, 0x39, 0xce, 0x73, 0x74, 0xe4, 0x2f, 0x7f, 0xfb, 0x29, 0x99, 0x54, + 0x5f, 0x24, 0x27, 0x86, 0x0b, 0x58, 0x02, 0xff, 0xf6, 0x53, 0x32, 0xa8, 0xbe, 0x48, 0x4f, 0x0c, + 0x16, 0xa7, 0x55, 0xbf, 0x56, 0xff, 0xf9, 0x82, 0x5a, 0xa9, 0xbf, 0xfa, 0xf8, 0x8b, 0xe9, 0x45, + 0x1b, 0xff, 0xe5, 0xca, 0x81, 0xda, 0xd2, 0x08, 0x7d, 0x9a, 0x79, 0xdc, 0xd1, 0x79, 0x7f, 0xfc, + 0x17, 0x7a, 0x21, 0x94, 0xd2, 0x7c, 0x99, 0xf6, 0xac, 0x65, 0xff, 0xf0, 0x38, 0x07, 0x61, 0x71, + 0x64, 0xf9, 0xed, 0x5a, 0xce, 0xb5, 0x4a, 0x6d, 0xff, 0xfb, 0x65, 0x8c, 0x56, 0x41, 0xfa, 0x7d, + 0x91, 0xf1, 0xce, 0x6c, 0x78, 0xe5, 0x7b, 0xff, 0xf6, 0xcb, 0x18, 0xac, 0x83, 0xf4, 0xfb, 0x23, + 0xe3, 0x9c, 0xd8, 0xf1, 0xb3, 0xa9, 0xf5, 0x7f, 0xfe, 0x9d, 0x35, 0xf6, 0x05, 0x28, 0x48, 0x1e, + 0x98, 0x0c, 0xda, 0x64, 0xab, 0xca, 0xff, 0xfd, 0x3a, 0x6b, 0xec, 0x0a, 0x50, 0x90, 0x3d, 0x30, + 0x19, 0xb4, 0xc9, 0x4f, 0xb3, 0x9a, 0xfc, 0x01, 0x07, 0x82, 0xbd, 0x99, 0xa4, 0x39, 0xc6, 0x16, + 0x38, 0xe1, 0x3e, 0x7f, 0xfe, 0xc3, 0x11, 0x1f, 0x0c, 0x30, 0x50, 0x9a, 0x8a, 0x51, 0x85, 0x2b, + 0x30, 0xc4, 0xbf, 0xff, 0x56, 0x20, 0x9e, 0xf3, 0xfe, 0xee, 0xf8, 0x08, 0x93, 0x45, 0x49, 0x85, + 0xce, 0x83, 0xa7, 0xff, 0xe1, 0x8b, 0x31, 0xd8, 0x61, 0x7b, 0x18, 0xf8, 0x2b, 0x60, 0x15, 0xeb, + 0xd2, 0xd3, 0xff, 0xf0, 0xc2, 0x92, 0x82, 0xe0, 0xf7, 0x90, 0x77, 0x52, 0xa4, 0xdb, 0xac, 0x6b, + 0xc8, 0x4a, 0xf1, 0xff, 0xea, 0x4e, 0xd2, 0xf7, 0x03, 0xe5, 0x41, 0xd4, 0x47, 0x86, 0x33, 0xff, + 0xd4, 0x9d, 0xa5, 0xee, 0x07, 0xca, 0x83, 0xa8, 0x8e, 0xe5, 0xbd, 0x2a, 0xff, 0xf8, 0x71, 0xaa, + 0xe2, 0x37, 0xa6, 0x65, 0x92, 0xa9, 0xb2, 0x35, 0xff, 0xf0, 0xe3, 0x55, 0xc4, 0x6f, 0x4c, 0xcb, + 0x25, 0x53, 0x56, 0xb2, 0x09, 0xed, 0xff, 0xf1, 0x07, 0x9b, 0xb9, 0x9e, 0x74, 0xf2, 0x13, 0xd8, + 0xa6, 0xd7, 0xff, 0xc3, 0x8d, 0x57, 0x11, 0xbd, 0x33, 0x2c, 0x95, 0x4d, 0x5a, 0x50, 0x74, 0xff, + 0xf5, 0x41, 0x72, 0x20, 0x9d, 0xa7, 0x0f, 0xfc, 0x4b, 0xab, 0x33, 0xff, 0xd4, 0x9d, 0xa5, 0xee, + 0x07, 0xca, 0x83, 0xa8, 0x8e, 0xe6, 0x10, 0x50, + ] + +} diff --git a/clients/apple/Tests/PunktfunkKitTests/CscRowsTests.swift b/clients/apple/Tests/PunktfunkKitTests/CscRowsTests.swift new file mode 100644 index 00000000..4a718849 --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/CscRowsTests.swift @@ -0,0 +1,96 @@ +import CoreVideo +import XCTest +import simd + +@testable import PunktfunkKit + +/// Mirrors pf-client-core's `csc_rows` tests (crates/pf-client-core/src/video.rs) — the Swift port +/// must stay in LOCKSTEP with the Rust implementation, so these are the same fixtures with the +/// same tolerances. A divergence here means the two sides would render the same stream +/// differently. +final class CscRowsTests: XCTestCase { + private func apply(_ u: CscUniform, _ yuv: SIMD3) -> SIMD3 { + SIMD3( + simd_dot(SIMD3(u.r0.x, u.r0.y, u.r0.z), yuv) + u.r0.w, + simd_dot(SIMD3(u.r1.x, u.r1.y, u.r1.z), yuv) + u.r1.w, + simd_dot(SIMD3(u.r2.x, u.r2.y, u.r2.z), yuv) + u.r2.w) + } + + /// 10-bit limited MSB-packed (P010/x444): reference white Y=940, black Y=64, neutral + /// chroma 512 — sampled as UNORM16 of `code << 6`. + func testBt2020TenBitLimitedWhiteBlack() { + let rows = CscRows.rows(.init(matrix: 9, fullRange: false), depth: 10, msbPacked: true) + func s(_ code: UInt32) -> Float { Float(code << 6) / 65535.0 } + let white = apply(rows, SIMD3(s(940), s(512), s(512))) + let black = apply(rows, SIMD3(s(64), s(512), s(512))) + for i in 0..<3 { + XCTAssertEqual(white[i], 1.0, accuracy: 0.002, "white \(white)") + XCTAssertEqual(black[i], 0.0, accuracy: 0.002, "black \(black)") + } + } + + /// Reference white (Y=235, U=V=128 limited) → RGB 1.0; reference black (Y=16) → 0.0. + func testBt709LimitedWhiteBlack() { + let rows = CscRows.rows(.init(matrix: 1, fullRange: false), depth: 8, msbPacked: false) + let white = apply(rows, SIMD3(235.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0)) + let black = apply(rows, SIMD3(16.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0)) + for i in 0..<3 { + XCTAssertEqual(white[i], 1.0, accuracy: 0.005, "white \(white)") + XCTAssertEqual(black[i], 0.0, accuracy: 0.005, "black \(black)") + } + } + + /// Full-range identity points + the 601-vs-709 red excursion (guards the matrix-code + /// dispatch — the two matrices MUST differ measurably, that difference is the whole bug + /// class this port fixes). + func testFullRangeAndRedExcursion() { + let rows601 = CscRows.rows(.init(matrix: 5, fullRange: true), depth: 8, msbPacked: false) + let white = apply(rows601, SIMD3(1.0, 0.5, 0.5)) + for i in 0..<3 { + XCTAssertEqual(white[i], 1.0, accuracy: 1e-5, "\(white)") + } + let red601 = apply(rows601, SIMD3(0.0, 0.5, 1.0)) + XCTAssertEqual(red601[0], 2.0 * (1.0 - 0.299) * 0.5, accuracy: 1e-4, "\(red601)") + let rows709 = CscRows.rows(.init(matrix: 1, fullRange: true), depth: 8, msbPacked: false) + let red709 = apply(rows709, SIMD3(0.0, 0.5, 1.0)) + XCTAssertEqual(red709[0], 2.0 * (1.0 - 0.2126) * 0.5, accuracy: 1e-4, "\(red709)") + XCTAssertGreaterThan(abs(red601[0] - red709[0]), 0.05) + } + + /// Unspecified (2) and unknown matrix codes fall back to BT.709 — the same default as the + /// Rust side and every punktfunk host's implicit SDR baseline. + func testUnspecifiedFallsBackTo709() { + let unspec = CscRows.rows(.init(matrix: 2, fullRange: false), depth: 8, msbPacked: false) + let bt709 = CscRows.rows(.init(matrix: 1, fullRange: false), depth: 8, msbPacked: false) + XCTAssertEqual(unspec, bt709) + } + + /// `signal(of:)` reads the matrix off the buffer's attachment (what VideoToolbox propagates + /// from the VUI) and the range off the pixel format — a 601-tagged buffer must come back as + /// matrix 5, an untagged one as unspecified (2), and a full-range sibling as fullRange. + func testSignalReadsAttachmentAndRange() throws { + func makeBuffer(_ format: OSType) throws -> CVPixelBuffer { + var pb: CVPixelBuffer? + let status = CVPixelBufferCreate(kCFAllocatorDefault, 64, 64, format, nil, &pb) + guard status == kCVReturnSuccess, let pb else { + throw XCTSkip("could not allocate a \(format) pixel buffer") + } + return pb + } + + let tagged = try makeBuffer(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) + CVBufferSetAttachment( + tagged, kCVImageBufferYCbCrMatrixKey, kCVImageBufferYCbCrMatrix_ITU_R_601_4, + .shouldPropagate) + XCTAssertEqual(CscRows.signal(of: tagged), CscRows.Signal(matrix: 5, fullRange: false)) + + let untagged = try makeBuffer(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) + XCTAssertEqual(CscRows.signal(of: untagged), CscRows.Signal(matrix: 2, fullRange: false)) + + let full = try makeBuffer(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) + CVBufferSetAttachment( + full, kCVImageBufferYCbCrMatrixKey, kCVImageBufferYCbCrMatrix_ITU_R_2020, + .shouldPropagate) + XCTAssertEqual(CscRows.signal(of: full), CscRows.Signal(matrix: 9, fullRange: true)) + } +} diff --git a/clients/windows/src/present.rs b/clients/windows/src/present.rs index 11dbf665..e843df50 100644 --- a/clients/windows/src/present.rs +++ b/clients/windows/src/present.rs @@ -4,7 +4,9 @@ //! the dedicated render thread ([`crate::render`]) — presenting never touches (or is stalled by) //! the XAML thread. //! -//! Two frame sources, one pair of YUV shaders (identical colour math for both): +//! Two frame sources, ONE Y′CbCr→RGB shader whose conversion rows arrive per frame in a constant +//! buffer (`pf_client_core::video::csc_rows` from the frame's CICP signaling — identical colour +//! math for both sources, and the stream's signaled matrix/range is honored, not assumed): //! //! * **GPU (D3D11VA)** — [`crate::video::GpuFrame`] is a slice of the decoder-only NV12/P010 //! texture array. One `CopySubresourceRegion` with a display-size box moves the slice — **both @@ -46,10 +48,14 @@ use windows::Win32::Graphics::Dxgi::Common::*; use windows::Win32::Graphics::Dxgi::*; use windows::Win32::System::Threading::WaitForSingleObject; -// One vertex shader (fullscreen triangle) + two pixel shaders, selected per frame colour space. -// tex0 is the luma plane, tex1 the chroma plane. The YUV→RGB matrices fold the limited→full range -// scale into the coefficients; for P010 the R16 sample is rescaled (×65535/65472) to undo the -// 10-bits-in-the-high-bits packing, then converted with BT.2020 NCL, PQ preserved. +// One vertex shader (fullscreen triangle) + ONE pixel shader for every colour combination: +// tex0 is the luma plane, tex1 the chroma plane, and the Y′CbCr→RGB conversion arrives as three +// constant-buffer rows precomputed on the CPU per frame (`pf_client_core::video::csc_rows` — +// bit-depth exact, range expansion + the P010 ×65535/65472 high-bit repack folded in). One shader +// honors whatever the stream signals (BT.601/709/2020, full/limited, 8/10-bit) instead of the old +// two hardcoded matrices — a BT.601-signaled stream (a Linux host's RGB-input NVENC) used to +// render with BT.709 coefficients, a constant hue error. A PQ stream's rows yield PQ-encoded +// R′G′B′ passed through as-is to the HDR10 swapchain, exactly as before. const SHADER_HLSL: &str = r#" struct VSOut { float4 pos : SV_Position; float2 uv : TEXCOORD0; }; VSOut vs_main(uint vid : SV_VertexID) { @@ -62,47 +68,47 @@ VSOut vs_main(uint vid : SV_VertexID) { Texture2D tex0 : register(t0); Texture2D tex1 : register(t1); SamplerState smp : register(s0); +cbuffer Csc : register(b0) { + float4 r0; // rgb[i] = dot(ri.xyz, yuv) + ri.w + float4 r1; + float4 r2; +}; -float4 ps_nv12(VSOut i) : SV_Target { - float y = tex0.Sample(smp, i.uv).r; - float2 uv = tex1.Sample(smp, i.uv).rg; - float yy = (y - 0.0627451) * 1.164384; // (Y-16/255)*255/219 - float u = uv.x - 0.5; - float v = uv.y - 0.5; // BT.709 limited, chroma scale folded - float r = yy + 1.792741 * v; - float g = yy - 0.213249 * u - 0.532909 * v; - float b = yy + 2.112402 * u; - return float4(saturate(float3(r, g, b)), 1.0); -} - -float4 ps_p010(VSOut i) : SV_Target { - const float S = 65535.0 / 65472.0; // undo P010 high-bit packing → exact 10-bit / 1023 - float y = tex0.Sample(smp, i.uv).r * S; - float2 uv = tex1.Sample(smp, i.uv).rg * S; - float yy = (y - 0.0625611) * 1.167808; // (Y-64/1023)*1023/876 - float u = uv.x - 0.5; - float v = uv.y - 0.5; // BT.2020 NCL limited, chroma scale folded; PQ kept - float r = yy + 1.683611 * v; - float g = yy - 0.187877 * u - 0.652337 * v; - float b = yy + 2.148072 * u; - return float4(saturate(float3(r, g, b)), 1.0); +float4 ps_yuv(VSOut i) : SV_Target { + // 4:2:0 chroma is left-cosited (H.273 type 0 — the default inference when unsignaled, and + // what the hosts produce), but sampling the half-res plane at the luma UV assumes CENTER + // siting — a ~0.5-luma-px rightward chroma shift on hard colored edges. Offset +0.25 chroma + // texels to re-align (the same correction the Apple client applies). Self-disables when the + // plane widths match (a full-size 4:4:4 chroma plane has no subsampling to correct). + float lw, lh, cw, ch; + tex0.GetDimensions(lw, lh); + tex1.GetDimensions(cw, ch); + float2 cuv = i.uv; + if (cw < lw) { cuv.x += 0.25 / cw; } + float3 yuv = float3(tex0.Sample(smp, i.uv).r, tex1.Sample(smp, cuv).rg); + float3 rgb = float3(dot(r0.xyz, yuv) + r0.w, + dot(r1.xyz, yuv) + r1.w, + dot(r2.xyz, yuv) + r2.w); + return float4(saturate(rgb), 1.0); } "#; /// The currently bound frame: per-plane SRVs (over the GPU sample texture or the CPU plane -/// textures) + the colour space that picks the shader. Redraws (resize, letterbox) re-present it. +/// textures). Redraws (resize, letterbox) re-present it — the CSC constant buffer still holds +/// this frame's rows, and the swapchain mode was latched by `set_hdr` when the frame arrived. struct Bound { y: ID3D11ShaderResourceView, c: ID3D11ShaderResourceView, - hdr: bool, } pub struct Presenter { device: ID3D11Device, context: ID3D11DeviceContext, vs: ID3D11VertexShader, - ps_nv12: ID3D11PixelShader, - ps_p010: ID3D11PixelShader, + ps_yuv: ID3D11PixelShader, + /// Dynamic constant buffer holding the bound frame's three CSC rows (`csc_rows`), rewritten + /// on every bind (colour signaling can flip in-band, e.g. the host's SDR→HDR re-init). + csc_buf: ID3D11Buffer, sampler: ID3D11SamplerState, swap: IDXGISwapChain1, /// Creation flags — MUST be re-passed to every `ResizeBuffers` or it fails. @@ -157,7 +163,22 @@ impl Presenter { let shared = crate::gpu::shared().ok_or_else(|| anyhow!("no shared D3D11 device"))?; let device = shared.device.clone(); let context = shared.context.clone(); - let (vs, ps_nv12, ps_p010, sampler) = build_pipeline(&device)?; + let (vs, ps_yuv, sampler) = build_pipeline(&device)?; + // The per-frame CSC rows (three float4s). Dynamic: rewritten with Map-discard on bind. + let csc_desc = D3D11_BUFFER_DESC { + ByteWidth: 48, + Usage: D3D11_USAGE_DYNAMIC, + BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + ..Default::default() + }; + let csc_buf = unsafe { + let mut b = None; + device + .CreateBuffer(&csc_desc, None, Some(&mut b)) + .context("CreateBuffer (CSC rows)")?; + b.ok_or_else(|| anyhow!("null CSC constant buffer"))? + }; let (swap, swap_flags) = create_composition_swapchain(&device, width.max(1), height.max(1))?; // ≤1 queued present: the render thread blocks on the waitable, so a frame is only drawn @@ -175,8 +196,8 @@ impl Presenter { device, context, vs, - ps_nv12, - ps_p010, + ps_yuv, + csc_buf, sampler, swap, swap_flags, @@ -327,12 +348,10 @@ impl Presenter { let (fy, fc) = plane_formats(g.ten_bit); let y = self.plane_srv(&dst, fy)?; let c = self.plane_srv(&dst, fc)?; - if g.ten_bit != g.hdr { - warn_bitdepth_mismatch_once(g.ten_bit, g.hdr); - } + self.write_csc_rows(g.color, g.ten_bit)?; self.src_w = g.width; self.src_h = g.height; - self.bound = Some(Bound { y, c, hdr: g.hdr }); + self.bound = Some(Bound { y, c }); // Hold the frame until the next bind: its decode surface stays out of the reuse pool // until this copy is queued ahead of any later decoder write (previous frame drops here). self.gpu_frame = Some(g); @@ -428,12 +447,13 @@ impl Presenter { w.div_ceil(2) as usize * 2 * bytes, h.div_ceil(2) as usize, )?; + let (y_srv, uv_srv) = (y_srv.clone(), uv_srv.clone()); + self.write_csc_rows(frame.color, frame.ten_bit)?; self.src_w = w; self.src_h = h; self.bound = Some(Bound { - y: y_srv.clone(), - c: uv_srv.clone(), - hdr: frame.hdr, + y: y_srv, + c: uv_srv, }); self.gpu_frame = None; // drop any held GPU frame Ok(()) @@ -464,6 +484,26 @@ impl Presenter { } } + /// Recompute the bound frame's Y′CbCr→RGB rows from its CICP signaling and Map-discard them + /// into the CSC constant buffer. `ten_bit` selects the 10-bit code points AND the P010 + /// high-bit repack (the plane SRVs are R16/R16G16 UNORM for 10-bit). + fn write_csc_rows(&self, color: pf_client_core::video::ColorDesc, ten_bit: bool) -> Result<()> { + let rows = pf_client_core::video::csc_rows(color, if ten_bit { 10 } else { 8 }, ten_bit); + unsafe { + let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); + self.context + .Map(&self.csc_buf, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut mapped)) + .context("Map CSC constant buffer")?; + std::ptr::copy_nonoverlapping( + rows.as_ptr() as *const u8, + mapped.pData as *mut u8, + 48, // [[f32; 4]; 3] + ); + self.context.Unmap(&self.csc_buf, 0); + } + Ok(()) + } + /// Map-discard `tex` and copy `rows` rows of `row_bytes` from `src` (stride `src_pitch`). fn map_rows( &self, @@ -525,14 +565,8 @@ impl Presenter { c.IASetInputLayout(None); c.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); c.VSSetShader(&self.vs, None); - c.PSSetShader( - if bound.hdr { - &self.ps_p010 - } else { - &self.ps_nv12 - }, - None, - ); + c.PSSetShader(&self.ps_yuv, None); + c.PSSetConstantBuffers(0, Some(&[Some(self.csc_buf.clone())])); c.PSSetShaderResources(0, Some(&[Some(bound.y.clone()), Some(bound.c.clone())])); c.PSSetSamplers(0, Some(&[Some(self.sampler.clone())])); c.Draw(3, 0); @@ -645,20 +679,6 @@ fn plane_formats(ten_bit: bool) -> (DXGI_FORMAT, DXGI_FORMAT) { } } -/// The host couples 10-bit ⟺ HDR today; a mismatch means the shader's transfer/matrix assumption -/// is off for this stream (rendered anyway — approximate colour beats no picture). -fn warn_bitdepth_mismatch_once(ten_bit: bool, hdr: bool) { - use std::sync::atomic::{AtomicBool, Ordering}; - static ONCE: AtomicBool = AtomicBool::new(true); - if ONCE.swap(false, Ordering::Relaxed) { - tracing::warn!( - ten_bit, - hdr, - "bit depth / HDR mismatch — colour may be approximate" - ); - } -} - /// A composition flip-model swapchain (no HWND) for binding to a XAML `SwapChainPanel`, with the /// frame-latency waitable when the driver allows it. Returns the swapchain + the flags it was /// created with (every `ResizeBuffers` must re-pass them). @@ -708,28 +728,18 @@ fn create_composition_swapchain( fn build_pipeline( device: &ID3D11Device, -) -> Result<( - ID3D11VertexShader, - ID3D11PixelShader, - ID3D11PixelShader, - ID3D11SamplerState, -)> { +) -> Result<(ID3D11VertexShader, ID3D11PixelShader, ID3D11SamplerState)> { let vs_blob = compile(SHADER_HLSL, "vs_main", "vs_5_0")?; - let nv12_blob = compile(SHADER_HLSL, "ps_nv12", "ps_5_0")?; - let p010_blob = compile(SHADER_HLSL, "ps_p010", "ps_5_0")?; + let yuv_blob = compile(SHADER_HLSL, "ps_yuv", "ps_5_0")?; unsafe { let mut vs = None; device .CreateVertexShader(blob_bytes(&vs_blob), None, Some(&mut vs)) .context("CreateVertexShader")?; - let mut ps_nv12 = None; + let mut ps_yuv = None; device - .CreatePixelShader(blob_bytes(&nv12_blob), None, Some(&mut ps_nv12)) - .context("CreatePixelShader (nv12)")?; - let mut ps_p010 = None; - device - .CreatePixelShader(blob_bytes(&p010_blob), None, Some(&mut ps_p010)) - .context("CreatePixelShader (p010)")?; + .CreatePixelShader(blob_bytes(&yuv_blob), None, Some(&mut ps_yuv)) + .context("CreatePixelShader (yuv)")?; let sdesc = D3D11_SAMPLER_DESC { Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR, AddressU: D3D11_TEXTURE_ADDRESS_CLAMP, @@ -742,12 +752,7 @@ fn build_pipeline( device .CreateSamplerState(&sdesc, Some(&mut sampler)) .context("CreateSamplerState")?; - Ok(( - vs.unwrap(), - ps_nv12.unwrap(), - ps_p010.unwrap(), - sampler.unwrap(), - )) + Ok((vs.unwrap(), ps_yuv.unwrap(), sampler.unwrap())) } } diff --git a/clients/windows/src/video.rs b/clients/windows/src/video.rs index 20a24347..d0501ba6 100644 --- a/clients/windows/src/video.rs +++ b/clients/windows/src/video.rs @@ -32,6 +32,7 @@ use ffmpeg::format::Pixel; use ffmpeg::software::scaling; use ffmpeg::util::frame::Video as AvFrame; use ffmpeg_next as ffmpeg; +use pf_client_core::video::ColorDesc; use std::ffi::c_void; use std::ptr; use windows::core::{Interface, GUID}; @@ -95,8 +96,12 @@ pub struct CpuFrame { pub uv_stride: usize, /// P010 sample layout (10 bits in the high bits of 16) vs NV12. Selects texture/SRV formats. pub ten_bit: bool, - /// BT.2020 PQ HDR10 vs ordinary BT.709 SDR. Selects shader + swapchain colour space. + /// BT.2020 PQ HDR10 vs ordinary BT.709 SDR. Selects the swapchain colour space. pub hdr: bool, + /// The frame's CICP signaling (HEVC VUI → `AVFrame`), read per-frame — the presenter derives + /// its Y′CbCr→RGB constant buffer from it (`csc_rows`), so a BT.601-signaled stream (a Linux + /// host's RGB-input NVENC) no longer renders with BT.709 coefficients. + pub color: ColorDesc, } /// A decoded frame still on the GPU: a D3D11 texture **array** plus the slice index the decoder @@ -112,9 +117,11 @@ pub struct GpuFrame { /// `sw_format`. The presenter keys its copy-texture/SRV formats off this: they must match the /// source array exactly for `CopySubresourceRegion`. pub ten_bit: bool, - /// BT.2020 PQ HDR10 (ST.2084 transfer) vs ordinary BT.709 SDR. Selects shader + swapchain - /// colour space only (the host couples 10-bit ⟺ HDR today, but formats key off `ten_bit`). + /// BT.2020 PQ HDR10 (ST.2084 transfer) vs ordinary BT.709 SDR. Selects the swapchain colour + /// space only (the host couples 10-bit ⟺ HDR today, but formats key off `ten_bit`). pub hdr: bool, + /// Per-frame CICP signaling — see [`CpuFrame::color`]. + pub color: ColorDesc, guard: D3d11FrameGuard, } @@ -329,9 +336,10 @@ impl SoftwareDecoder { /// matrix/range/transfer handling all lives in the presenter's shaders, shared with the /// D3D11VA path, so software frames are bit-comparable with hardware ones. fn convert(&mut self, frame: &AvFrame) -> Result { - use ffmpeg::color::TransferCharacteristic; let (fmt, w, h) = (frame.format(), frame.width(), frame.height()); - let hdr = frame.color_transfer_characteristic() == TransferCharacteristic::SMPTE2084; + // SAFETY: `frame` wraps a live decoded AVFrame for the duration of this call. + let color = unsafe { ColorDesc::from_raw(frame.as_ptr()) }; + let hdr = color.is_pq(); // Source bit depth from the pix-fmt descriptor (stable FFmpeg public API). let ten_bit = unsafe { let desc = ffmpeg::ffi::av_pix_fmt_desc_get(fmt.into()); @@ -356,6 +364,7 @@ impl SoftwareDecoder { uv_stride: conv.stride(1), ten_bit, hdr, + color, }) } } @@ -586,8 +595,9 @@ impl D3d11vaDecoder { if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_D3D11 as i32 { bail!("decoder returned a software frame (no D3D11 surface)"); } - let hdr = - (*self.frame).color_trc == ffi::AVColorTransferCharacteristic::AVCOL_TRC_SMPTE2084; + // SAFETY: `self.frame` is the live decoded AVFrame for the duration of this call. + let color = ColorDesc::from_raw(self.frame); + let hdr = color.is_pq(); let ten_bit = { let hwfc = (*self.frame).hw_frames_ctx; !hwfc.is_null() @@ -604,6 +614,7 @@ impl D3d11vaDecoder { index: (*self.frame).data[1] as usize as u32, ten_bit, hdr, + color, guard: D3d11FrameGuard(cloned), }; log_layout_once(frame.width, frame.height, frame.index, hdr, ten_bit); diff --git a/crates/pf-client-core/src/video.rs b/crates/pf-client-core/src/video.rs index 4a05dd27..968761ba 100644 --- a/crates/pf-client-core/src/video.rs +++ b/crates/pf-client-core/src/video.rs @@ -119,11 +119,13 @@ pub struct ColorDesc { } impl ColorDesc { - /// Read the CICP fields off a raw decoded frame. + /// Read the CICP fields off a raw decoded frame. Public: the Windows client's raw-FFI + /// D3D11VA/software decoders build their per-frame `ColorDesc` with it too (same + /// `ffmpeg-next` major, so the `AVFrame` type unifies across the workspace). /// /// # Safety /// `frame` must point to a valid `AVFrame` (alive for the duration of the call). - pub(crate) unsafe fn from_raw(frame: *const ffmpeg::ffi::AVFrame) -> ColorDesc { + pub unsafe fn from_raw(frame: *const ffmpeg::ffi::AVFrame) -> ColorDesc { // SAFETY: caller guarantees a live AVFrame; these are plain enum field reads. unsafe { ColorDesc { @@ -141,6 +143,57 @@ impl ColorDesc { } } +/// The Y′CbCr→RGB conversion as three vec4 rows for a shader constant buffer / push-constant +/// block: `rgb[i] = dot(r[i].xyz, yuv) + r[i].w` — bit-depth exact. The ONE coefficient +/// implementation every presenter derives its CSC from (Vulkan push constants, the Windows +/// client's D3D11 constant buffer), so a stream's signaled matrix/range is honored identically +/// everywhere; the Apple client ports this function (and its tests) to Swift. +/// +/// `depth` picks the limited-range code points (8-bit: 16/235/240 over 255; 10-bit: +/// 64/940/960 over 1023 — NOT the same normalized values, the difference is ~half a +/// code). `msb_packed` folds in the P010/X6 packing factor: 10 significant bits live in +/// the MSBs of 16, so a UNORM16 sample reads `code·64/65535` — multiplying by +/// `65535/65472` recovers exact `code/1023`. +pub fn csc_rows(desc: ColorDesc, depth: u8, msb_packed: bool) -> [[f32; 4]; 3] { + // BT.601 (5/6), BT.2020 (9/10); everything else — incl. unspecified — is the host's + // BT.709 SDR default (mirrors the software path's swscale coefficient choice). + let (kr, kb) = match desc.matrix { + 5 | 6 => (0.299, 0.114), + 9 | 10 => (0.2627, 0.0593), + _ => (0.2126, 0.0722), + }; + let kg = 1.0 - kr - kb; + let max = f64::from((1u32 << depth) - 1); // 255 / 1023 + let step = f64::from(1u32 << (depth - 8)); // code points per 8-bit step: 1 / 4 + let pack = if msb_packed { 65535.0 / 65472.0 } else { 1.0 }; + let (sy, oy, sc) = if desc.full_range { + (pack, 0.0f64, pack) + } else { + ( + pack * max / (219.0 * step), + -(16.0 * step) / max, + pack * max / (224.0 * step), + ) + }; + // rgb = M * (yuv + off) = M*yuv + M*off — rows of M with the offset dot folded into + // w. `yuv` is the SAMPLED (packed) value, so the offsets divide by the packing + // factor to land on the same scale. + let off = [oy / pack, -0.5 / pack, -0.5 / pack]; + let m = [ + [sy, 0.0, 2.0 * (1.0 - kr) * sc], + [ + sy, + -2.0 * (1.0 - kb) * kb / kg * sc, + -2.0 * (1.0 - kr) * kr / kg * sc, + ], + [sy, 2.0 * (1.0 - kb) * sc, 0.0], + ]; + core::array::from_fn(|r| { + let w: f64 = (0..3).map(|c| m[r][c] * off[c]).sum(); + [m[r][0] as f32, m[r][1] as f32, m[r][2] as f32, w as f32] + }) +} + /// RGBA pixels for `GdkMemoryTexture` (which takes a stride). pub struct CpuFrame { pub width: u32, @@ -1387,6 +1440,117 @@ unsafe extern "C" fn pick_vulkan( mod tests { use super::*; + fn desc(matrix: u8, full_range: bool) -> ColorDesc { + ColorDesc { + primaries: 1, + transfer: 1, + matrix, + full_range, + } + } + + fn apply(rows: &[[f32; 4]; 3], yuv: [f32; 3]) -> [f32; 3] { + core::array::from_fn(|r| { + rows[r][0] * yuv[0] + rows[r][1] * yuv[1] + rows[r][2] * yuv[2] + rows[r][3] + }) + } + + /// 10-bit limited MSB-packed (P010/X6): reference white Y=940, black Y=64, neutral + /// chroma 512 — sampled as UNORM16 of `code << 6`. + #[test] + fn bt2020_10bit_limited_white_black() { + let rows = csc_rows(desc(9, false), 10, true); + let s = |code: u32| ((code << 6) as f32) / 65535.0; + let white = apply(&rows, [s(940), s(512), s(512)]); + let black = apply(&rows, [s(64), s(512), s(512)]); + for (w, b) in white.iter().zip(black) { + assert!((w - 1.0).abs() < 0.002, "white {white:?}"); + assert!(b.abs() < 0.002, "black {black:?}"); + } + } + + /// Reference white (Y=235, U=V=128 limited) → RGB 1.0; reference black (Y=16) → 0.0 + /// — the GL presenter's test, in row form. + #[test] + fn bt709_limited_white_black() { + let rows = csc_rows(desc(1, false), 8, false); + let white = apply(&rows, [235.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0]); + let black = apply(&rows, [16.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0]); + for (w, b) in white.iter().zip(black) { + assert!((w - 1.0).abs() < 0.005, "white {white:?}"); + assert!(b.abs() < 0.005, "black {black:?}"); + } + } + + /// Full-range identity points + the 601-vs-709 red excursion (guards the + /// matrix-code dispatch), same as the GL presenter's test. + #[test] + fn full_range_and_red_excursion() { + let rows = csc_rows(desc(5, true), 8, false); + let white = apply(&rows, [1.0, 0.5, 0.5]); + assert!(white.iter().all(|v| (v - 1.0).abs() < 1e-5), "{white:?}"); + let red = apply(&rows, [0.0, 0.5, 1.0]); + assert!((red[0] - 2.0 * (1.0 - 0.299) * 0.5).abs() < 1e-4, "{red:?}"); + let rows709 = csc_rows(desc(1, true), 8, false); + let red709 = apply(&rows709, [0.0, 0.5, 1.0]); + assert!( + (red709[0] - 2.0 * (1.0 - 0.2126) * 0.5).abs() < 1e-4, + "{red709:?}" + ); + assert!((red[0] - red709[0]).abs() > 0.05); + } + + /// The row form must agree with the GL presenter's column-major `yuv_to_rgb` on a + /// grid of inputs — same math, different packing. + #[test] + fn rows_match_the_gl_matrix_form() { + for (matrix, full) in [(1u8, false), (1, true), (5, false), (9, false), (9, true)] { + let d = desc(matrix, full); + let rows = csc_rows(d, 8, false); + // Reimplementation of video_gl::yuv_to_rgb's application for comparison. + let (kr, kb) = match matrix { + 5 | 6 => (0.299f32, 0.114f32), + 9 | 10 => (0.2627, 0.0593), + _ => (0.2126, 0.0722), + }; + let kg = 1.0 - kr - kb; + let (sy, oy, sc) = if full { + (1.0f32, 0.0f32, 1.0f32) + } else { + (255.0 / 219.0, -16.0 / 255.0, 255.0 / 224.0) + }; + let mat = [ + sy, + sy, + sy, + 0.0, + -2.0 * (1.0 - kb) * kb / kg * sc, + 2.0 * (1.0 - kb) * sc, + 2.0 * (1.0 - kr) * sc, + -2.0 * (1.0 - kr) * kr / kg * sc, + 0.0, + ]; + let off = [oy, -0.5, -0.5]; + for yuv in [ + [0.1f32, 0.3, 0.7], + [0.9, 0.5, 0.5], + [0.5, 0.2, 0.8], + [16.0 / 255.0, 0.5, 0.5], + ] { + let v = [yuv[0] + off[0], yuv[1] + off[1], yuv[2] + off[2]]; + let gl: [f32; 3] = + core::array::from_fn(|r| (0..3).map(|c| mat[c * 3 + r] * v[c]).sum()); + let ours = apply(&rows, yuv); + for (a, b) in gl.iter().zip(ours) { + assert!( + (a - b).abs() < 1e-5, + "{matrix}/{full}: gl {gl:?} rows {ours:?}" + ); + } + } + } + } + /// Lock the DRM FourCC magic numbers against typos — these are the exact values /// `` defines, and a wrong one is what painted the Steam Deck green. #[test] @@ -1434,4 +1598,82 @@ mod tests { assert!(f.color.is_pq()); assert_eq!((f.width, f.height), (64, 64)); } + + /// Golden colour fixtures: one 256×64 LOSSLESS x265 IDR of 8 fully-saturated colour bars per + /// signaling variant (generated offline with ffmpeg/libx265; the RGB→YUV conversion matched + /// to the VUI each fixture declares, so the original RGB is recoverable ±1 code). Decoding + /// through the real CPU path (`SoftwareDecoder` → per-frame `ColorDesc` → swscale with the + /// signaled matrix/range) must reproduce the bars — the end-to-end guard for the + /// signaling-driven CSC across BT.601/709 × limited/full. A hardcoded-709 regression fails + /// the 601 fixture by tens of code points; a range mix-up fails the full-range one. + #[test] + fn software_decode_reproduces_golden_bars() { + const BARS: [(u8, u8, u8); 8] = [ + (255, 255, 255), + (255, 255, 0), + (0, 255, 255), + (0, 255, 0), + (255, 0, 255), + (255, 0, 0), + (0, 0, 255), + (0, 0, 0), + ]; + let fixtures: [(&str, &[u8], ColorDesc); 3] = [ + ( + "601-limited", + include_bytes!("../tests/bars-601-limited.h265"), + ColorDesc { + primaries: 1, + transfer: 1, + matrix: 5, // BT.470BG — what a Linux host's RGB-input NVENC signals + full_range: false, + }, + ), + ( + "709-limited", + include_bytes!("../tests/bars-709-limited.h265"), + ColorDesc { + primaries: 1, + transfer: 1, + matrix: 1, + full_range: false, + }, + ), + ( + "709-full", + include_bytes!("../tests/bars-709-full.h265"), + ColorDesc { + primaries: 1, + transfer: 1, + matrix: 1, + full_range: true, // the PUNKTFUNK_444_FULLRANGE experiment's signaling + }, + ), + ]; + for (name, au, want_color) in fixtures { + let mut dec = SoftwareDecoder::new(ffmpeg::codec::Id::HEVC).expect("hevc decoder"); + let mut got = dec.decode(au).expect("decode"); + if got.is_none() { + dec.decoder.send_eof().ok(); + let mut frame = AvFrame::empty(); + if dec.decoder.receive_frame(&mut frame).is_ok() { + got = Some(dec.convert_rgba(&frame).expect("convert")); + } + } + let f = got.unwrap_or_else(|| panic!("{name}: no frame decoded")); + assert_eq!(f.color, want_color, "{name}: signaling"); + assert_eq!((f.width, f.height), (256, 64), "{name}: dims"); + for (i, (r, g, b)) in BARS.iter().enumerate() { + let (cx, cy) = (i * 32 + 16, 32usize); + let o = cy * f.stride + cx * 4; + let px = &f.rgba[o..o + 3]; + for (got, want) in px.iter().zip([r, g, b]) { + assert!( + got.abs_diff(*want) <= 3, + "{name} bar {i}: got {px:?}, want ({r},{g},{b})" + ); + } + } + } + } } diff --git a/crates/pf-client-core/src/video_d3d11.rs b/crates/pf-client-core/src/video_d3d11.rs index bc919802..9e5694ee 100644 --- a/crates/pf-client-core/src/video_d3d11.rs +++ b/crates/pf-client-core/src/video_d3d11.rs @@ -52,10 +52,12 @@ use windows::Win32::Graphics::Direct3D11::{ D3D11_VPOV_DIMENSION_TEXTURE2D, }; use windows::Win32::Graphics::Dxgi::Common::{ - DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709, DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709, + DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709, DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P2020, + DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601, DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709, DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020, DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020, - DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, - DXGI_FORMAT_NV12, DXGI_FORMAT_P010, DXGI_RATIONAL, DXGI_SAMPLE_DESC, + DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601, DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, + DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_NV12, DXGI_FORMAT_P010, DXGI_RATIONAL, + DXGI_SAMPLE_DESC, }; use windows::Win32::Graphics::Dxgi::{ CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIKeyedMutex, IDXGIResource1, @@ -629,9 +631,16 @@ impl D3d11vaDecoder { // Colour spaces per frame (the host flips PQ in-band): YCbCr in, sRGB out — a PQ // stream is tone-mapped to SDR by the processor (module docs). CICP → DXGI enums. + // BT.601 (5/6) matters in practice: a Linux host's RGB-input NVENC paths signal + // BT470BG limited (NVENC's fixed internal RGB→YUV is BT.601 — ffmpeg force-writes + // that VUI), and mapping it to P709 here was a constant hue error on those streams. + // DXGI has no full-range G2084 YCbCr enum, so PQ is studio regardless of range. let in_cs = match (color.transfer, color.matrix, color.full_range) { (16, _, _) => DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020, - (_, 9, _) => DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020, + (_, 9 | 10, false) => DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020, + (_, 9 | 10, true) => DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P2020, + (_, 5 | 6, false) => DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601, + (_, 5 | 6, true) => DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601, (_, _, true) => DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709, _ => DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, }; diff --git a/crates/pf-client-core/tests/bars-601-limited.h265 b/crates/pf-client-core/tests/bars-601-limited.h265 new file mode 100644 index 00000000..48654683 Binary files /dev/null and b/crates/pf-client-core/tests/bars-601-limited.h265 differ diff --git a/crates/pf-client-core/tests/bars-709-full.h265 b/crates/pf-client-core/tests/bars-709-full.h265 new file mode 100644 index 00000000..cc0da18d Binary files /dev/null and b/crates/pf-client-core/tests/bars-709-full.h265 differ diff --git a/crates/pf-client-core/tests/bars-709-limited.h265 b/crates/pf-client-core/tests/bars-709-limited.h265 new file mode 100644 index 00000000..9a1ad414 Binary files /dev/null and b/crates/pf-client-core/tests/bars-709-limited.h265 differ diff --git a/crates/pf-presenter/shaders/nv12_csc.frag b/crates/pf-presenter/shaders/nv12_csc.frag index 862445b7..8444aaaa 100644 --- a/crates/pf-presenter/shaders/nv12_csc.frag +++ b/crates/pf-presenter/shaders/nv12_csc.frag @@ -62,7 +62,17 @@ vec3 srgb_oetf(vec3 c) { } void main() { - vec3 yuv = vec3(texture(u_y, v_uv).r, texture(u_c, v_uv).rg); + // 4:2:0 chroma is left-cosited (H.273 type 0 — the default inference when unsignaled, and + // what the hosts produce), but sampling the half-res plane at the luma UV assumes CENTER + // siting — a ~0.5-luma-px rightward chroma shift on hard colored edges. Offset +0.25 chroma + // texels to re-align (the same correction the Apple/Windows clients apply). Self-disables + // when the plane widths match (a full-size 4:4:4 chroma plane needs no correction). + vec2 cuv = v_uv; + int cw = textureSize(u_c, 0).x; + if (cw < textureSize(u_y, 0).x) { + cuv.x += 0.25 / float(cw); + } + vec3 yuv = vec3(texture(u_y, v_uv).r, texture(u_c, cuv).rg); vec3 rgb = vec3( dot(pc.r0.xyz, yuv) + pc.r0.w, dot(pc.r1.xyz, yuv) + pc.r1.w, diff --git a/crates/pf-presenter/shaders/nv12_csc.frag.spv b/crates/pf-presenter/shaders/nv12_csc.frag.spv index dd710b02..4579428a 100644 Binary files a/crates/pf-presenter/shaders/nv12_csc.frag.spv and b/crates/pf-presenter/shaders/nv12_csc.frag.spv differ diff --git a/crates/pf-presenter/src/csc.rs b/crates/pf-presenter/src/csc.rs index 8832814f..8c6f2de9 100644 --- a/crates/pf-presenter/src/csc.rs +++ b/crates/pf-presenter/src/csc.rs @@ -9,55 +9,11 @@ use anyhow::{Context as _, Result}; use ash::vk; -use pf_client_core::video::ColorDesc; -/// The push-constant block's matrix half: three vec4 rows, -/// `rgb[i] = dot(r[i].xyz, yuv) + r[i].w` — bit-depth exact. -/// -/// `depth` picks the limited-range code points (8-bit: 16/235/240 over 255; 10-bit: -/// 64/940/960 over 1023 — NOT the same normalized values, the difference is ~half a -/// code). `msb_packed` folds in the P010/X6 packing factor: 10 significant bits live in -/// the MSBs of 16, so a UNORM16 sample reads `code·64/65535` — multiplying by -/// `65535/65472` recovers exact `code/1023`. -pub fn csc_rows(desc: ColorDesc, depth: u8, msb_packed: bool) -> [[f32; 4]; 3] { - // BT.601 (5/6), BT.2020 (9/10); everything else — incl. unspecified — is the host's - // BT.709 SDR default (mirrors the software path's swscale coefficient choice). - let (kr, kb) = match desc.matrix { - 5 | 6 => (0.299, 0.114), - 9 | 10 => (0.2627, 0.0593), - _ => (0.2126, 0.0722), - }; - let kg = 1.0 - kr - kb; - let max = f64::from((1u32 << depth) - 1); // 255 / 1023 - let step = f64::from(1u32 << (depth - 8)); // code points per 8-bit step: 1 / 4 - let pack = if msb_packed { 65535.0 / 65472.0 } else { 1.0 }; - let (sy, oy, sc) = if desc.full_range { - (pack, 0.0f64, pack) - } else { - ( - pack * max / (219.0 * step), - -(16.0 * step) / max, - pack * max / (224.0 * step), - ) - }; - // rgb = M * (yuv + off) = M*yuv + M*off — rows of M with the offset dot folded into - // w. `yuv` is the SAMPLED (packed) value, so the offsets divide by the packing - // factor to land on the same scale. - let off = [oy / pack, -0.5 / pack, -0.5 / pack]; - let m = [ - [sy, 0.0, 2.0 * (1.0 - kr) * sc], - [ - sy, - -2.0 * (1.0 - kb) * kb / kg * sc, - -2.0 * (1.0 - kr) * kr / kg * sc, - ], - [sy, 2.0 * (1.0 - kb) * sc, 0.0], - ]; - core::array::from_fn(|r| { - let w: f64 = (0..3).map(|c| m[r][c] * off[c]).sum(); - [m[r][0] as f32, m[r][1] as f32, m[r][2] as f32, w as f32] - }) -} +// The coefficient math lives in pf-client-core next to `ColorDesc` (one tested +// implementation shared with the Windows client's D3D11 constant buffer and mirrored by the +// Apple client's Swift port); re-exported here so presenter callers keep their import path. +pub use pf_client_core::video::csc_rows; /// The pass objects (everything except the per-video-size framebuffer, which lives with /// the video image). Destroyed explicitly via [`CscPass::destroy`] from the presenter's @@ -337,118 +293,3 @@ pub(crate) fn build_fullscreen_pipeline( Ok(pipeline?[0]) } -#[cfg(test)] -mod tests { - use super::*; - - fn desc(matrix: u8, full_range: bool) -> ColorDesc { - ColorDesc { - primaries: 1, - transfer: 1, - matrix, - full_range, - } - } - - fn apply(rows: &[[f32; 4]; 3], yuv: [f32; 3]) -> [f32; 3] { - core::array::from_fn(|r| { - rows[r][0] * yuv[0] + rows[r][1] * yuv[1] + rows[r][2] * yuv[2] + rows[r][3] - }) - } - - /// 10-bit limited MSB-packed (P010/X6): reference white Y=940, black Y=64, neutral - /// chroma 512 — sampled as UNORM16 of `code << 6`. - #[test] - fn bt2020_10bit_limited_white_black() { - let rows = csc_rows(desc(9, false), 10, true); - let s = |code: u32| ((code << 6) as f32) / 65535.0; - let white = apply(&rows, [s(940), s(512), s(512)]); - let black = apply(&rows, [s(64), s(512), s(512)]); - for (w, b) in white.iter().zip(black) { - assert!((w - 1.0).abs() < 0.002, "white {white:?}"); - assert!(b.abs() < 0.002, "black {black:?}"); - } - } - - /// Reference white (Y=235, U=V=128 limited) → RGB 1.0; reference black (Y=16) → 0.0 - /// — the GL presenter's test, in row form. - #[test] - fn bt709_limited_white_black() { - let rows = csc_rows(desc(1, false), 8, false); - let white = apply(&rows, [235.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0]); - let black = apply(&rows, [16.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0]); - for (w, b) in white.iter().zip(black) { - assert!((w - 1.0).abs() < 0.005, "white {white:?}"); - assert!(b.abs() < 0.005, "black {black:?}"); - } - } - - /// Full-range identity points + the 601-vs-709 red excursion (guards the - /// matrix-code dispatch), same as the GL presenter's test. - #[test] - fn full_range_and_red_excursion() { - let rows = csc_rows(desc(5, true), 8, false); - let white = apply(&rows, [1.0, 0.5, 0.5]); - assert!(white.iter().all(|v| (v - 1.0).abs() < 1e-5), "{white:?}"); - let red = apply(&rows, [0.0, 0.5, 1.0]); - assert!((red[0] - 2.0 * (1.0 - 0.299) * 0.5).abs() < 1e-4, "{red:?}"); - let rows709 = csc_rows(desc(1, true), 8, false); - let red709 = apply(&rows709, [0.0, 0.5, 1.0]); - assert!( - (red709[0] - 2.0 * (1.0 - 0.2126) * 0.5).abs() < 1e-4, - "{red709:?}" - ); - assert!((red[0] - red709[0]).abs() > 0.05); - } - - /// The row form must agree with the GL presenter's column-major `yuv_to_rgb` on a - /// grid of inputs — same math, different packing. - #[test] - fn rows_match_the_gl_matrix_form() { - for (matrix, full) in [(1u8, false), (1, true), (5, false), (9, false), (9, true)] { - let d = desc(matrix, full); - let rows = csc_rows(d, 8, false); - // Reimplementation of video_gl::yuv_to_rgb's application for comparison. - let (kr, kb) = match matrix { - 5 | 6 => (0.299f32, 0.114f32), - 9 | 10 => (0.2627, 0.0593), - _ => (0.2126, 0.0722), - }; - let kg = 1.0 - kr - kb; - let (sy, oy, sc) = if full { - (1.0f32, 0.0f32, 1.0f32) - } else { - (255.0 / 219.0, -16.0 / 255.0, 255.0 / 224.0) - }; - let mat = [ - sy, - sy, - sy, - 0.0, - -2.0 * (1.0 - kb) * kb / kg * sc, - 2.0 * (1.0 - kb) * sc, - 2.0 * (1.0 - kr) * sc, - -2.0 * (1.0 - kr) * kr / kg * sc, - 0.0, - ]; - let off = [oy, -0.5, -0.5]; - for yuv in [ - [0.1f32, 0.3, 0.7], - [0.9, 0.5, 0.5], - [0.5, 0.2, 0.8], - [16.0 / 255.0, 0.5, 0.5], - ] { - let v = [yuv[0] + off[0], yuv[1] + off[1], yuv[2] + off[2]]; - let gl: [f32; 3] = - core::array::from_fn(|r| (0..3).map(|c| mat[c * 3 + r] * v[c]).sum()); - let ours = apply(&rows, yuv); - for (a, b) in gl.iter().zip(ours) { - assert!( - (a - b).abs() < 1e-5, - "{matrix}/{full}: gl {gl:?} rows {ours:?}" - ); - } - } - } - } -} diff --git a/crates/punktfunk-host/src/capture.rs b/crates/punktfunk-host/src/capture.rs index 346108bc..a92b6d07 100644 --- a/crates/punktfunk-host/src/capture.rs +++ b/crates/punktfunk-host/src/capture.rs @@ -62,10 +62,13 @@ pub struct OutputFormat { /// HDR: the capturer converts to 10-bit (IDD-push FP16 → `P010`, or `Rgb10a2` for a 4:4:4 source). /// `false` = 8-bit SDR. pub hdr: bool, - /// Full-chroma 4:4:4 session: the capturer must keep full chroma — deliver packed **RGB** - /// (`Bgra` / `Rgb10a2`), NOT the subsampled `Nv12`/`P010` the Windows video-engine path produces by - /// default — because 4:4:4 can only be recovered from a full-chroma source. NVENC then does the - /// RGB→YUV444 CSC at encode (chroma_format_idc=3). `false` on every 4:2:0 session. + /// Full-chroma 4:4:4 session: the capturer must keep full chroma. On Windows the IDD-push + /// capturer hands the **BGRA** slot through (skipping the subsampling BGRA→NV12 + /// VideoConverter) so NVENC ingests full-chroma RGB and CSCs to 4:4:4 itself — measured + /// on-glass (RTX 5070 Ti): ARGB + `chromaFormatIDC=3` yields TRUE 4:4:4 and the conversion + /// follows the configured VUI matrix (BT.709 limited since the VUI is always written). On + /// Linux it forces the CPU RGB path the encoder swscales to `YUV444P`. `false` on every + /// 4:2:0 session. pub chroma_444: bool, } @@ -404,10 +407,11 @@ pub fn capture_virtual_output( // Duplication, no WGC helper). A FRESH monitor + ring is created per session: a REUSED monitor's // swap-chain dies after ~2 sessions and can't be revived. The ring is always FP16 when the display // is HDR (the driver composes the IDD in FP16); `want.hdr` proactively enables advanced color and - // selects the per-frame conversion (FP16 → P010 vs BGRA → NV12). `IddPushCapturer` takes the - // keepalive (it owns the virtual display). There is NO fallback (DDA + the WGC relay were removed): - // if it can't open or the driver doesn't attach, the session fails cleanly and the client reconnects. - idd_push::IddPushCapturer::open(target, pref, want.hdr, keep) + // selects the per-frame conversion (FP16 → P010 vs BGRA → NV12, or BGRA → AYUV for a + // `want.chroma_444` SDR session). `IddPushCapturer` takes the keepalive (it owns the virtual + // display). There is NO fallback (DDA + the WGC relay were removed): if it can't open or the + // driver doesn't attach, the session fails cleanly and the client reconnects. + idd_push::IddPushCapturer::open(target, pref, want.hdr, want.chroma_444, keep) .map(|c| Box::new(c) as Box) .map_err(|(e, _keep)| e.context("IDD-push capture open (no fallback)")) } @@ -422,9 +426,14 @@ pub(crate) fn capturer_supports_444() -> bool { } #[cfg(target_os = "windows")] pub(crate) fn capturer_supports_444() -> bool { - // IDD-push 4:4:4 (full-chroma RGB from the FP16 ring) is the next step; until then the sole Windows - // capturer delivers subsampled NV12/P010 only, so the host honestly negotiates 4:2:0. - false + // IDD-push delivers full-chroma BGRA for an SDR 4:4:4 session (skipping the NV12 + // VideoConverter) — but only the direct-NVENC backend ingests RGB and CSCs it to 4:4:4 + // (measured on-glass: true full chroma, matrix follows the configured VUI), so gate on it + // (AMF can't 4:4:4 at all; the QSV/ffmpeg path has no RGB-input 4:4:4 wiring). An HDR + // display can't be known here (the virtual display's mode settles after the Welcome); that + // combination downgrades at capture time — the capturer emits P010 and the encoder's caps + // cross-check reports the 4:2:0 truth (the in-band SPS keeps the client correct either way). + crate::encode::windows_resolved_backend() == crate::encode::WindowsBackend::Nvenc } #[cfg(not(any(target_os = "linux", target_os = "windows")))] pub(crate) fn capturer_supports_444() -> bool { diff --git a/crates/punktfunk-host/src/capture/windows/dxgi.rs b/crates/punktfunk-host/src/capture/windows/dxgi.rs index 12c0bd75..e6899711 100644 --- a/crates/punktfunk-host/src/capture/windows/dxgi.rs +++ b/crates/punktfunk-host/src/capture/windows/dxgi.rs @@ -464,21 +464,25 @@ float main(float4 pos : SV_POSITION, float2 uv : TEXCOORD0) : SV_TARGET { } "; -/// P010 CHROMA pass PS — half-res, writes interleaved (Cb,Cr) to plane 1 (R16G16_UNORM RTV). Averages -/// the 2x2 scRGB source footprint of this chroma sample (box filter) IN scRGB-linear space before the -/// PQ encode, then forms Cb/Cr from the averaged-then-PQ-encoded RGB. `inv_src` = (1/srcW, 1/srcH). +/// P010 CHROMA pass PS — half-res, writes interleaved (Cb,Cr) to plane 1 (R16G16_UNORM RTV). +/// **Left-cosited** (H.273 chroma_loc type 0 — the default every decoder infers when +/// chroma_loc_info is unsignaled, and what the clients' sampling corrections assume): the chroma +/// sample sits ON the even luma column, vertically centered between its two rows — so the filter +/// is the 2-row average of that ONE column, IN scRGB-linear space before the PQ encode, then +/// Cb/Cr from the averaged-then-PQ-encoded RGB. (The old 2×2 box was CENTER-sited — a +/// half-luma-pixel chroma shift against what decoders reconstruct; the narrow column decimation +/// also keeps desktop text/edge chroma crisp, and block-uniform inputs stay exact for +/// `hdr_p010_selftest`.) `inv_src` = (1/srcW, 1/srcH). const HDR_P010_UV_PS: &str = r" #include_common cbuffer C : register(b0) { float2 inv_src; float2 pad; }; float2 main(float4 pos : SV_POSITION, float2 uv : TEXCOORD0) : SV_TARGET { - // `uv` is the chroma-sample centre in [0,1]; the 4 co-sited luma texels sit at uv ± half a luma - // texel in each axis. Average their scRGB (linear) values, then run the SAME PQ/CSC as the Y pass. + // `uv` is the chroma RT texel centre = the middle of the 2x2 luma block; the left-cosited + // target is the block's LEFT column, whose two texel centres sit at uv + (-h.x, ±h.y). float2 h = inv_src * 0.5; float3 a = max(tx.Sample(sm, uv + float2(-h.x, -h.y)).rgb, 0.0); - float3 b = max(tx.Sample(sm, uv + float2( h.x, -h.y)).rgb, 0.0); - float3 c = max(tx.Sample(sm, uv + float2(-h.x, h.y)).rgb, 0.0); - float3 d = max(tx.Sample(sm, uv + float2( h.x, h.y)).rgb, 0.0); - float3 scrgb = (a + b + c + d) * 0.25; + float3 b = max(tx.Sample(sm, uv + float2(-h.x, h.y)).rgb, 0.0); + float3 scrgb = (a + b) * 0.5; float3 nits = scrgb * 80.0; float3 lin2020 = mul(BT709_TO_BT2020, nits); float3 pq = pq_oetf(lin2020 / 10000.0); diff --git a/crates/punktfunk-host/src/capture/windows/idd_push.rs b/crates/punktfunk-host/src/capture/windows/idd_push.rs index 633c55bf..c0a479d3 100644 --- a/crates/punktfunk-host/src/capture/windows/idd_push.rs +++ b/crates/punktfunk-host/src/capture/windows/idd_push.rs @@ -669,6 +669,13 @@ pub struct IddPushCapturer { /// Windows mid-session. Drives the ring format (HDR → FP16 surfaces, SDR → BGRA) and the conversion. /// Polled in the capture loop; a change recreates the ring (see [`Self::recreate_ring`]). display_hdr: bool, + /// The session negotiated full-chroma 4:4:4: while the display is SDR the BGRA slot passes + /// THROUGH (a plain copy into the out ring, no NV12 VideoConverter) so NVENC gets full-chroma + /// RGB and CSCs to 4:4:4 itself — measured on-glass: `chromaFormatIDC=3` + ARGB input yields + /// TRUE 4:4:4 and the conversion follows the VUI matrix (BT.709 limited, always written). + /// While the display is HDR this is overridden to the P010 path (no 10-bit 4:4:4 source): + /// the stream honestly downgrades to 4:2:0 — the encoder's caps cross-check reports it. + want_444: bool, /// Off-thread display-descriptor sampler (see [`DescriptorPoller`]) — the capture loop reads /// its snapshot instead of running CCD queries inline on the frame path. desc_poller: DescriptorPoller, @@ -824,9 +831,10 @@ impl IddPushCapturer { target: WinCaptureTarget, preferred: Option<(u32, u32, u32)>, client_10bit: bool, + want_444: bool, keepalive: Box, ) -> std::result::Result)> { - match Self::open_inner(target, preferred, client_10bit) { + match Self::open_inner(target, preferred, client_10bit, want_444) { Ok(mut me) => { me._keepalive = keepalive; Ok(me) @@ -839,6 +847,7 @@ impl IddPushCapturer { target: WinCaptureTarget, preferred: Option<(u32, u32, u32)>, client_10bit: bool, + want_444: bool, ) -> Result { // The ring MUST live on the adapter the driver's swap-chain renders on. Primary: the // selected render GPU — the same pick SET_RENDER_ADAPTER pinned the driver to at monitor @@ -853,7 +862,7 @@ impl IddPushCapturer { LowPart: (target.adapter_luid & 0xffff_ffff) as u32, HighPart: (target.adapter_luid >> 32) as i32, }); - match Self::open_on(target.clone(), preferred, client_10bit, luid) { + match Self::open_on(target.clone(), preferred, client_10bit, want_444, luid) { Ok(me) => Ok(me), Err(e) => { // Self-heal a render-adapter mismatch ONCE: on TEX_FAIL the driver has reported the @@ -878,7 +887,7 @@ impl IddPushCapturer { "IDD push: ring/driver render-adapter mismatch — rebinding the ring to the \ driver's reported adapter" ); - Self::open_on(target, preferred, client_10bit, drv) + Self::open_on(target, preferred, client_10bit, want_444, drv) .context("IDD-push rebind to the driver's reported render adapter") } } @@ -888,6 +897,7 @@ impl IddPushCapturer { target: WinCaptureTarget, preferred: Option<(u32, u32, u32)>, client_10bit: bool, + want_444: bool, luid: LUID, ) -> Result { let (pw, ph, _hz) = preferred @@ -1042,6 +1052,7 @@ impl IddPushCapturer { mode = format!("{w}x{h}"), display_hdr, client_10bit, + want_444, ring_fp16 = display_hdr, "IDD push(host): created sealed ring + delivered the channel; waiting for the driver \ to attach + publish" @@ -1060,6 +1071,7 @@ impl IddPushCapturer { generation, client_10bit, display_hdr, + want_444, desc_poller: DescriptorPoller::spawn( target.target_id, DisplayDescriptor { @@ -1219,15 +1231,24 @@ impl IddPushCapturer { } } - /// The output texture format + the [`PixelFormat`] NVENC encodes, driven SOLELY by the DISPLAY's HDR - /// state (like the WGC path): HDR → `P010` (BT.2020 PQ 10-bit limited) → NVENC Main10, and the client - /// auto-detects PQ from the HEVC VUI; SDR → `Nv12` (BT.709 8-bit limited). Both are native YUV so - /// NVENC skips its internal RGB→YUV CSC on the contended SM (plan §5.A). We do NOT gate HDR on the - /// client's advertised `VIDEO_CAP_10BIT` — clients under-report it (e.g. the Mac advertises 10-bit - /// only when its OWN display is HDR), yet all decode Main10 + auto-switch, exactly as on the WGC path. + /// The output texture format + the [`PixelFormat`] NVENC encodes, driven by the DISPLAY's HDR + /// state (like the WGC path) plus the session's 4:4:4 negotiation: HDR → `P010` (BT.2020 PQ + /// 10-bit limited) → NVENC Main10, and the client auto-detects PQ from the HEVC VUI; SDR → + /// `Nv12` (BT.709 8-bit limited), or full-chroma `Bgra` passthrough on a 4:4:4 session (NVENC + /// CSCs RGB→YUV444 itself, following the BT.709 VUI — the one path that deliberately pays the + /// SM-side CSC, because the video processor can only produce subsampled output). We do NOT + /// gate HDR on the client's advertised `VIDEO_CAP_10BIT` — clients under-report it (e.g. the + /// Mac advertises 10-bit only when its OWN display is HDR), yet all decode Main10 + + /// auto-switch, exactly as on the WGC path. HDR wins over 4:4:4 (there is no 10-bit + /// full-chroma source): the stream downgrades to 4:2:0 with a warning. fn out_format(&self) -> (DXGI_FORMAT, PixelFormat) { if self.display_hdr { + if self.want_444 { + warn_444_hdr_downgrade_once(); + } (DXGI_FORMAT_P010, PixelFormat::P010) + } else if self.want_444 { + (DXGI_FORMAT_B8G8R8A8_UNORM, PixelFormat::Bgra) } else { (DXGI_FORMAT_NV12, PixelFormat::Nv12) } @@ -1397,6 +1418,7 @@ impl IddPushCapturer { /// Build the per-mode YUV converter if not already built: a VIDEO-engine BGRA→NV12 processor on an /// SDR display, or the FP16→P010 shader on an HDR display. Both keep NVENC's RGB→YUV CSC off the SM. + /// An SDR 4:4:4 session needs NO converter — the BGRA slot passes through (see `out_format`). fn ensure_converter(&mut self) -> Result<()> { if self.display_hdr { if self.hdr_p010_conv.is_none() { @@ -1405,6 +1427,8 @@ impl IddPushCapturer { // belong to, and `?` propagates any failure before the converter is stored. self.hdr_p010_conv = Some(unsafe { HdrP010Converter::new(&self.device)? }); } + } else if self.want_444 { + // Full-chroma passthrough — no conversion resources to build. } else if self.video_conv.is_none() { // SAFETY: `VideoConverter::new` is `unsafe` (it sets up the D3D11 VIDEO processor); we pass live // borrows of `self.device` + its immediate `self.context` (single-threaded, this thread) plus @@ -1509,6 +1533,11 @@ impl IddPushCapturer { self.height, )?; } + } else if self.want_444 { + // SDR 4:4:4: pass the BGRA slot through untouched — NVENC ingests full-chroma + // RGB and CSCs to YUV 4:4:4 itself (per the always-written BT.709 VUI). Plain + // copy-engine move; the slot releases back to the driver immediately. + self.context.CopyResource(&out, &s.tex); } else { // SDR: BGRA slot → NV12 on the VIDEO engine; NVENC takes native NV12, no SM-side CSC. if let Some(conv) = self.video_conv.as_ref() { @@ -1672,6 +1701,21 @@ impl Capturer for IddPushCapturer { } } +/// A 4:4:4 session while the display is HDR: there is no 10-bit full-chroma source (the FP16 +/// desktop needs the PQ tone curve, which the P010 shader provides at 4:2:0), so the stream +/// honestly downgrades — the encoder's `chroma_444` caps cross-check reports it and the in-band +/// SPS keeps the client decoding correctly. Once per process: the state can flap mid-session. +fn warn_444_hdr_downgrade_once() { + use std::sync::atomic::{AtomicBool, Ordering}; + static ONCE: AtomicBool = AtomicBool::new(true); + if ONCE.swap(false, Ordering::Relaxed) { + tracing::warn!( + "4:4:4 negotiated but the display is HDR — no 10-bit full-chroma source exists; \ + encoding HDR 4:2:0 (P010) instead (disable HDR on the virtual display for 4:4:4)" + ); + } +} + impl Drop for IddPushCapturer { fn drop(&mut self) { self.slots.clear(); diff --git a/crates/punktfunk-host/src/encode/linux/mod.rs b/crates/punktfunk-host/src/encode/linux/mod.rs index 7f832023..34a7c960 100644 --- a/crates/punktfunk-host/src/encode/linux/mod.rs +++ b/crates/punktfunk-host/src/encode/linux/mod.rs @@ -326,11 +326,19 @@ impl NvencEncoder { }; } - // NV12 / 4:4:4 paths: we do the RGB→YUV conversion ourselves as BT.709 *limited* range - // (swscale), so signal that in the bitstream VUI (colorspace/range/primaries/transfer) — - // otherwise the client decoder assumes a default and the picture comes out washed-out / - // wrong-contrast. The RGB-input 4:2:0 path leaves these unset (NVENC's internal CSC writes - // its own VUI). Matches the Windows NV12 path's BT.709 limited-range signalling. + // NV12 / 4:4:4 paths: we do the RGB→YUV conversion ourselves as BT.709 (swscale), so + // signal that in the bitstream VUI (colorspace/range/primaries/transfer) — otherwise the + // client decoder assumes a default and the picture comes out washed-out / wrong-contrast. + // The RGB-input 4:2:0 path leaves these unset (NVENC's internal CSC writes its own VUI). + // Matches the Windows NV12 path's BT.709 limited-range signalling. + // + // PUNKTFUNK_444_FULLRANGE=1 (experimental, 4:4:4-only): convert AND signal FULL range — + // recovers the ~12% of code space limited-range quantization gives up, for the exact + // text/UI chroma 4:4:4 exists for. Every punktfunk client honors the signaled range + // (csc_rows / the Apple rows port); ship as default only if the on-glass A/B shows a + // visible win. Linux-only: the Windows path's NVENC-internal CSC range is unmeasured. + let full_range_444 = want_444 + && std::env::var("PUNKTFUNK_444_FULLRANGE").is_ok_and(|v| v.trim() == "1"); if matches!(format, PixelFormat::Nv12) || want_444 { // SAFETY: same `video` builder — `raw = video.as_mut_ptr()` is the non-null, properly- // aligned, sole-owned, not-yet-opened `AVCodecContext`. We set its four VUI colour enum @@ -339,7 +347,11 @@ impl NvencEncoder { unsafe { let raw = video.as_mut_ptr(); (*raw).colorspace = ffi::AVColorSpace::AVCOL_SPC_BT709; - (*raw).color_range = ffi::AVColorRange::AVCOL_RANGE_MPEG; // limited/studio + (*raw).color_range = if full_range_444 { + ffi::AVColorRange::AVCOL_RANGE_JPEG // full + } else { + ffi::AVColorRange::AVCOL_RANGE_MPEG // limited/studio + }; (*raw).color_primaries = ffi::AVColorPrimaries::AVCOL_PRI_BT709; (*raw).color_trc = ffi::AVColorTransferCharacteristic::AVCOL_TRC_BT709; } @@ -401,10 +413,12 @@ impl NvencEncoder { // SAFETY: `sws` is the non-null context from the call above (null-checked). The ITU-709 // coefficient table from `sws_getCoefficients` is a process-lifetime libswscale static, // reused for src+dst matrices; `sws_setColorspaceDetails` only reads it and writes scalar - // CSC settings into `sws` (limited-range dst: dstRange = 0). No Rust memory is passed. + // CSC settings into `sws` (dstRange matches the VUI: 0 = limited, 1 = the + // PUNKTFUNK_444_FULLRANGE experiment). No Rust memory is passed. unsafe { let cs709 = ffi::sws_getCoefficients(SWS_CS_ITU709); - ffi::sws_setColorspaceDetails(sws, cs709, 1, cs709, 0, 0, 1 << 16, 1 << 16); + let dst_range = i32::from(full_range_444); + ffi::sws_setColorspaceDetails(sws, cs709, 1, cs709, dst_range, 0, 1 << 16, 1 << 16); } Some(sws) } else { diff --git a/crates/punktfunk-host/src/encode/linux/vaapi.rs b/crates/punktfunk-host/src/encode/linux/vaapi.rs index 577c4b5f..e17c984f 100644 --- a/crates/punktfunk-host/src/encode/linux/vaapi.rs +++ b/crates/punktfunk-host/src/encode/linux/vaapi.rs @@ -204,8 +204,9 @@ unsafe fn open_vaapi_encoder_mode( let raw = video.as_mut_ptr(); (*raw).rc_buffer_size = vbv_bits as i32; (*raw).gop_size = i32::MAX; // no periodic IDR (forced-IDR via pict_type=I on RFI) - // We hand the encoder BT.709 *limited* NV12 (swscale CSC, or scale_vaapi which preserves the - // input range we tag), so signal that VUI — else the client decoder washes the picture out. + // We hand the encoder BT.709 *limited* NV12 (swscale CSC on the CPU path; scale_vaapi pinned + // to `out_color_matrix=bt709:out_range=limited` on the zero-copy path, with the full-range + // RGB input tagged), so signal that VUI — else the client decoder washes the picture out. (*raw).colorspace = ffi::AVColorSpace::AVCOL_SPC_BT709; (*raw).color_range = ffi::AVColorRange::AVCOL_RANGE_MPEG; (*raw).color_primaries = ffi::AVColorPrimaries::AVCOL_PRI_BT709; @@ -718,6 +719,11 @@ impl DmabufInner { (*par).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as c_int; (*par).width = width as c_int; (*par).height = height as c_int; + // Declare the link's colour up front (full-range RGB — the compositor's desktop) so + // the per-frame tags in `submit` match the negotiated link instead of reading as a + // mid-stream property change. + (*par).color_space = ffi::AVColorSpace::AVCOL_SPC_RGB; + (*par).color_range = ffi::AVColorRange::AVCOL_RANGE_JPEG; (*par).time_base = ffi::AVRational { num: 1, den: fps as c_int, @@ -751,7 +757,14 @@ impl DmabufInner { } init!(src, ptr::null(), "buffer"); init!(hwmap, c"mode=read".as_ptr(), "hwmap"); - init!(scale, c"format=nv12".as_ptr(), "scale_vaapi"); + // Pin the VPP's output colour to what the encoder's VUI signals (BT.709 limited). + // Without the explicit options the conversion matrix is whatever the driver defaults + // to for an unspecified output (Mesa: BT.601) — a hue shift against the signaled VUI. + init!( + scale, + c"format=nv12:out_color_matrix=bt709:out_range=limited".as_ptr(), + "scale_vaapi" + ); init!(sink, ptr::null(), "buffersink"); let link = |a: *mut ffi::AVFilterContext, b: *mut ffi::AVFilterContext| -> c_int { @@ -879,6 +892,12 @@ impl DmabufInner { (*drm).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as c_int; (*drm).width = self.width as c_int; (*drm).height = self.height as c_int; + // The dmabuf is the compositor's rendered desktop: full-range RGB. Tag the frame so + // the VPP's colour negotiation sees the real input instead of "unspecified" (an + // untagged input lets the driver pick its own default for the RGB→NV12 conversion — + // Mesa's is BT.601, contradicting the BT.709-limited VUI the encoder signals). + (*drm).color_range = ffi::AVColorRange::AVCOL_RANGE_JPEG; + (*drm).colorspace = ffi::AVColorSpace::AVCOL_SPC_RGB; (*drm).hw_frames_ctx = ffi::av_buffer_ref(self.drm_frames); (*drm).data[0] = Box::into_raw(desc) as *mut u8; // Own the descriptor so it frees with the frame (the fd is owned by the DmabufFrame, diff --git a/crates/punktfunk-host/src/encode/sw.rs b/crates/punktfunk-host/src/encode/sw.rs index 992ea9c2..23daa1d8 100644 --- a/crates/punktfunk-host/src/encode/sw.rs +++ b/crates/punktfunk-host/src/encode/sw.rs @@ -1,7 +1,13 @@ //! Software H.264 encoder (openh264) — the GPU-less encode path for the Windows host (and a //! fallback when NVENC is unavailable). Low-latency screen-content config: single-reference, -//! no B-frames (Baseline), bitrate rate-control, in-band SPS/PPS each IDR, BT.709 limited range. +//! no B-frames (Baseline), bitrate rate-control, in-band SPS/PPS each IDR. //! Synchronous: `submit` encodes immediately and stashes the AU for `poll` (no internal queue). +//! +//! The RGB→YUV conversion is OURS, BT.709 limited range: openh264 writes no colour description +//! into the VUI (unspecified), so decoders fall back to their default — BT.709 limited on every +//! punktfunk client — and the pixels must match that default. The crate's own `YUVBuffer` +//! converter is BT.601 (0.2578/0.5039/0.0977 + 16), which decoded-as-709 is a constant hue +//! error; that's why it is NOT used here. // Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). #![deny(clippy::undocumented_unsafe_blocks)] @@ -12,19 +18,20 @@ use openh264::encoder::{ BitRate, Complexity, Encoder as Oh264, EncoderConfig, FrameRate, FrameType, IntraFramePeriod, Profile, RateControlMode, SpsPpsStrategy, UsageType, }; -use openh264::formats::{BgraSliceU8, RgbSliceU8, YUVBuffer}; +use openh264::formats::YUVSlices; use openh264::OpenH264API; pub struct OpenH264Encoder { enc: Oh264, - yuv: YUVBuffer, width: u32, height: u32, fps: u32, src_format: PixelFormat, - /// BGRA scratch for the 3-bpp (Bgr) and R/B-swapped (Rgba/Rgbx) formats openh264 can't wrap - /// directly. Reused across frames. - scratch: Vec, + /// The converted I420 planes (our BT.709-limited CSC — see the module doc), reused across + /// frames: full-res luma + quarter-res Cb/Cr, tightly packed (stride = width, width/2). + y_plane: Vec, + u_plane: Vec, + v_plane: Vec, frame_idx: i64, force_kf: bool, /// At most one AU per submit (no lookahead), handed back by the next `poll`. @@ -33,7 +40,7 @@ pub struct OpenH264Encoder { // openh264's Encoder holds a raw C handle (not auto-Send); it lives on the single encode thread. // SAFETY: `OpenH264Encoder` wraps `Oh264` (openh264's `Encoder`), which holds a raw C handle to the -// openh264 `ISVCEncoder` and is not auto-`Send`; the other fields (`YUVBuffer`, `Vec`, scalars, +// openh264 `ISVCEncoder` and is not auto-`Send`; the other fields (the plane `Vec`s, scalars, // `Option`) are plain owned data. The session creates the encoder, calls // `submit`/`poll`/`flush`, and drops it all on one dedicated encode thread, never sharing it by // reference across threads, so the C handle is only ever touched from a single thread. Moving the @@ -62,51 +69,75 @@ impl OpenH264Encoder { .scene_change_detect(false) // no surprise IDRs (bitrate spikes / freeze) .adaptive_quantization(true) .complexity(Complexity::Low) // latency over BD-rate - .profile(Profile::Baseline); // no B-frames; BT.709 limited is the crate default VUI + .profile(Profile::Baseline); // no B-frames; the VUI carries no colour description let api = OpenH264API::from_source(); // statically-bundled build (default `source` feature) let enc = Oh264::with_api_config(api, cfg).context("openh264 Encoder::with_api_config")?; - let yuv = YUVBuffer::new(width as usize, height as usize); + let (w, h) = (width as usize, height as usize); tracing::info!( "openh264 software encoder: {width}x{height}@{fps} {} Mbps (Baseline, screen-content)", bps / 1_000_000 ); Ok(Self { enc, - yuv, width, height, fps, src_format: format, - scratch: Vec::new(), + y_plane: vec![0; w * h], + u_plane: vec![0; (w / 2) * (h / 2)], + v_plane: vec![0; (w / 2) * (h / 2)], frame_idx: 0, force_kf: false, pending: None, }) } - /// Normalize a packed source buffer into the reused BGRA `scratch` ([B,G,R,A]). `rgb_order` - /// = source is R,G,B (swap into B,G,R); otherwise source is already B,G,R. - fn normalize_to_bgra(&mut self, src: &[u8], src_bpp: usize, rgb_order: bool) { + /// Convert one packed full-range RGB frame into the I420 planes, BT.709 limited range. + /// `bpp` is the source pixel stride; `ri`/`gi`/`bi` the channel byte offsets within a pixel. + /// Luma per pixel; Cb/Cr from the 2×2 block's averaged RGB (the same box filter the crate's + /// converter used, so only the matrix changed). + fn convert_bt709(&mut self, src: &[u8], bpp: usize, ri: usize, gi: usize, bi: usize) { let w = self.width as usize; let h = self.height as usize; - self.scratch.resize(w * h * 4, 0); - for px in 0..(w * h) { - let s = &src[px * src_bpp..px * src_bpp + 3]; - let d = &mut self.scratch[px * 4..px * 4 + 4]; - if rgb_order { - d[0] = s[2]; - d[1] = s[1]; - d[2] = s[0]; - } else { - d[0] = s[0]; - d[1] = s[1]; - d[2] = s[2]; + let cw = w / 2; + for by in 0..h / 2 { + for bx in 0..cw { + let mut sum = (0f32, 0f32, 0f32); + for (dy, dx) in [(0, 0), (0, 1), (1, 0), (1, 1)] { + let (px, py) = (bx * 2 + dx, by * 2 + dy); + let s = &src[(py * w + px) * bpp..]; + let (r, g, b) = (f32::from(s[ri]), f32::from(s[gi]), f32::from(s[bi])); + self.y_plane[py * w + px] = luma709(r, g, b); + sum = (sum.0 + r, sum.1 + g, sum.2 + b); + } + let (cb, cr) = chroma709(sum.0 / 4.0, sum.1 / 4.0, sum.2 / 4.0); + self.u_plane[by * cw + bx] = cb; + self.v_plane[by * cw + bx] = cr; } - d[3] = 0xff; } } } +/// BT.709 luma coefficients (Kg = 1 − Kr − Kb). +const KR: f32 = 0.2126; +const KB: f32 = 0.0722; +const KG: f32 = 1.0 - KR - KB; + +/// One full-range RGB pixel (0..=255 channels) → the BT.709 limited-range 8-bit luma code +/// (16..=235). Kept in lockstep with the client-side inverse (`pf-client-core::video::csc_rows`). +fn luma709(r: f32, g: f32, b: f32) -> u8 { + let y = KR * r + KG * g + KB * b; // full-scale luma, 0..=255 + (16.0 + y * (219.0 / 255.0) + 0.5) as u8 // `as` saturates — no manual clamp needed +} + +/// (Averaged) full-range RGB → the BT.709 limited-range Cb/Cr codes (16..=240, neutral 128). +fn chroma709(r: f32, g: f32, b: f32) -> (u8, u8) { + let y = KR * r + KG * g + KB * b; + let cb = 128.0 + (b - y) * (224.0 / 255.0) / (2.0 * (1.0 - KB)); + let cr = 128.0 + (r - y) * (224.0 / 255.0) / (2.0 * (1.0 - KR)); + ((cb + 0.5) as u8, (cr + 0.5) as u8) +} + impl Encoder for OpenH264Encoder { fn submit(&mut self, captured: &CapturedFrame) -> Result<()> { ensure!( @@ -139,21 +170,13 @@ impl Encoder for OpenH264Encoder { self.src_format ); - match self.src_format { - PixelFormat::Rgb => self - .yuv - .read_rgb(RgbSliceU8::new(&bytes[..w * h * 3], (w, h))), - PixelFormat::Bgra | PixelFormat::Bgrx => self - .yuv - .read_rgb(BgraSliceU8::new(&bytes[..w * h * 4], (w, h))), - PixelFormat::Rgba | PixelFormat::Rgbx => { - self.normalize_to_bgra(bytes, 4, true); - self.yuv.read_rgb(BgraSliceU8::new(&self.scratch, (w, h))); - } - PixelFormat::Bgr => { - self.normalize_to_bgra(bytes, 3, false); - self.yuv.read_rgb(BgraSliceU8::new(&self.scratch, (w, h))); - } + // Source pixel stride + R/G/B byte offsets within a pixel — one converter for every + // packed-RGB layout the capturers emit (no BGRA normalization pass needed). + let (bpp, ri, gi, bi) = match self.src_format { + PixelFormat::Rgb => (3, 0, 1, 2), + PixelFormat::Bgr => (3, 2, 1, 0), + PixelFormat::Rgba | PixelFormat::Rgbx => (4, 0, 1, 2), + PixelFormat::Bgra | PixelFormat::Bgrx => (4, 2, 1, 0), // 10-bit HDR comes only from the GPU NVENC path; the software 8-bit H.264 encoder // can't represent it (and never receives it — the capturer pairs Rgb10a2 with NVENC). PixelFormat::Rgb10a2 => { @@ -166,13 +189,19 @@ impl Encoder for OpenH264Encoder { "software encoder cannot encode YUV GPU textures (NV12/P010 → NVENC only)" ) } - } + }; + self.convert_bt709(bytes, bpp, ri, gi, bi); if self.force_kf { self.enc.force_intra_frame(); self.force_kf = false; } - let bs = self.enc.encode(&self.yuv).context("openh264 encode")?; + let slices = YUVSlices::new( + (&self.y_plane, &self.u_plane, &self.v_plane), + (w, h), + (w, w / 2, w / 2), + ); + let bs = self.enc.encode(&slices).context("openh264 encode")?; let mut data = Vec::new(); bs.write_vec(&mut data); // AnnexB start codes; SPS/PPS prepended on IDR if !data.is_empty() { @@ -225,6 +254,47 @@ mod tests { use super::*; use crate::capture::{CapturedFrame, FramePayload, PixelFormat}; + /// The BT.709 limited-range anchor points: reference white → (235,128,128), black → + /// (16,128,128), pure red's Cr must hit the positive extreme 240 (it does exactly: + /// 255(1−Kr)·(224/255)/(2(1−Kr)) = 112). ±1 code for float rounding. + #[test] + fn bt709_conversion_anchor_points() { + assert_eq!(luma709(255.0, 255.0, 255.0), 235); + assert_eq!(luma709(0.0, 0.0, 0.0), 16); + assert_eq!(chroma709(255.0, 255.0, 255.0), (128, 128)); + assert_eq!(chroma709(0.0, 0.0, 0.0), (128, 128)); + let (cb, cr) = chroma709(255.0, 0.0, 0.0); + assert_eq!(cr, 240, "pure red must reach the Cr extreme"); + assert!((101..=103).contains(&cb), "red Cb ~102, got {cb}"); + let (cb, _) = chroma709(0.0, 0.0, 255.0); + assert_eq!(cb, 240, "pure blue must reach the Cb extreme"); + } + + /// The 601-vs-709 luma split on pure green (Kg 0.587 vs 0.7152) — guards against anyone + /// "simplifying" the coefficients back to the crate's BT.601 converter (the hue-shift bug + /// this module's own conversion exists to prevent). + #[test] + fn bt709_is_not_bt601() { + // BT.601 green luma: 16 + 219·0.587 = 144.5; BT.709: 16 + 219·0.7152 = 172.6. + let y = luma709(0.0, 255.0, 0.0); + assert!((172..=174).contains(&y), "709 green luma ~173, got {y}"); + } + + /// A flat gray frame converts to neutral chroma and mid luma across every plane byte + /// (exercises the block loop + plane sizing, not just the per-pixel math). + #[test] + fn converts_flat_gray_to_neutral_planes() { + let (w, h) = (16u32, 8u32); + let mut enc = + OpenH264Encoder::open(PixelFormat::Bgrx, w, h, 60, 1_000_000).expect("open openh264"); + let bytes = vec![0x80u8; (w * h * 4) as usize]; + enc.convert_bt709(&bytes, 4, 2, 1, 0); + // 16 + 128·(219/255) = 125.9 → 126. + assert!(enc.y_plane.iter().all(|&y| y == 126), "{:?}", &enc.y_plane[..4]); + assert!(enc.u_plane.iter().all(|&u| u == 128)); + assert!(enc.v_plane.iter().all(|&v| v == 128)); + } + #[test] fn encodes_synthetic_frame_to_annexb_idr() { let (w, h, fps) = (1280u32, 720u32, 60u32); diff --git a/crates/punktfunk-host/src/encode/windows/nvenc.rs b/crates/punktfunk-host/src/encode/windows/nvenc.rs index 91006c3d..6f2343bb 100644 --- a/crates/punktfunk-host/src/encode/windows/nvenc.rs +++ b/crates/punktfunk-host/src/encode/windows/nvenc.rs @@ -708,11 +708,13 @@ impl NvencD3d11Encoder { // input — a subsampled NV12/P010 source can't reconstruct full chroma (so the capturer is // forced to RGB for a 4:4:4 session, and we guard on the input format here too). // - // ON-GLASS TODO (RTX box): confirm ARGB + chromaFormatIDC=3 + FREXT yields a *true* 4:4:4 - // stream. NVENC's RGB→YUV CSC is documented to honor chromaFormatIDC (unlike libavcodec's - // wrapper, which always subsamples RGB to 4:2:0 — hence the Linux path feeds planar YUV444 - // instead). If on-glass shows 4:2:0, the follow-up is a BGRA→AYUV shader feeding the native - // `NV_ENC_BUFFER_FORMAT_AYUV` 4:4:4 input format. + // ON-GLASS MEASURED (RTX 5070 Ti, driver 610.43, 2026-07-10 — `nvenc_444_on_glass_probe` + // below + colour-bar analysis): ARGB + chromaFormatIDC=3 + FREXT yields a TRUE 4:4:4 + // stream (1-px chroma stripes survive, adjacent-column |dU| ≈ 138), and NVENC's internal + // RGB→YUV conversion FOLLOWS THE CONFIGURED VUI MATRIX (bars match BT.709 within ±1 code + // with our 709 VUI; the same driver produces exact BT.601 when libavcodec's nvenc wrapper + // sets its BT470BG VUI on Linux). The always-written SDR VUI above therefore makes the + // pixels and the signaling agree by construction — no AYUV shader needed. let rgb_input = matches!( self.buffer_fmt, nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB @@ -752,21 +754,33 @@ impl NvencD3d11Encoder { } } - // HDR colour signaling: BT.2020 primaries + SMPTE ST.2084 (PQ) transfer + BT.2020-NCL - // matrix, limited (studio) range — NVENC's RGB→YUV default. HEVC/H.264 carry it in the VUI; - // AV1 has NO VUI, so the SAME CICP code points go in the sequence-header colour config - // (`colorPrimaries`/`transferCharacteristics`/`matrixCoefficients`/`colorRange`). Without - // this a non-HEVC decoder assumes BT.709 SDR → washed-out / colour-shifted HDR. + // Colour signaling, written UNCONDITIONALLY (was HDR-only): the capturer hands NVENC + // pre-converted NV12 (BT.709 limited, the IDD VideoConverter) or P010 (BT.2020 PQ + // limited, the FP16→P010 shader), so the stream must SAY so — an SDR stream with no + // colour description decodes correctly only on clients whose "unspecified" default + // happens to be BT.709 limited (ours are, but Moonlight/third-party/Android-vendor + // decoders default 601 at sub-HD resolutions). HEVC/H.264 carry it in the VUI; AV1 has + // NO VUI, so the SAME CICP code points go in the sequence-header colour config + // (`colorPrimaries`/`transferCharacteristics`/`matrixCoefficients`/`colorRange`). // // This is the per-stream colour *description* only. The static mastering-display (ST.2086) // and content-light (MaxCLL/MaxFALL) metadata — HEVC SEI / AV1 METADATA OBUs — is a // separate follow-up, as is wiring AV1/H.264 to a true 10-bit (Main10) encode (only HEVC // sets Main10 above today). - if self.hdr { - let prim = nv::NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT2020; - let trc = - nv::NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084; - let mat = nv::NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL; + { + let (prim, trc, mat) = if self.hdr { + ( + nv::NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT2020, + nv::NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084, + nv::NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL, + ) + } else { + ( + nv::NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709, + nv::NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709, + nv::NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709, + ) + }; match self.codec { Codec::H265 => { let vui = &mut cfg.encodeCodecConfig.hevcConfig.hevcVUIParameters; @@ -1160,6 +1174,24 @@ impl Encoder for NvencD3d11Encoder { } _ => nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB, }; + // 4:4:4 honesty: the FREXT/chromaFormatIDC=3 config engages only on an RGB input (a + // subsampled NV12/P010 source can't reconstruct full chroma). If the capturer handed + // native YUV despite a 4:4:4 negotiation, this session encodes 4:2:0 — clear the flag + // NOW so `caps().chroma_444` (and punktfunk1's post-open cross-check) reports what + // the stream really carries instead of silently claiming full chroma. + if self.chroma_444 + && !matches!( + self.buffer_fmt, + nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ARGB + | nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_ABGR10 + ) + { + tracing::warn!( + format = ?captured.format, + "4:4:4 negotiated but the capturer delivered subsampled YUV — encoding 4:2:0" + ); + self.chroma_444 = false; + } let device = frame.device.clone(); self.init_session(&device)?; self.init_device = dev_raw; @@ -1573,3 +1605,162 @@ pub fn probe_can_encode_444(codec: Codec) -> bool { ok } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::capture::{dxgi::D3d11Frame, CapturedFrame, FramePayload}; + use windows::Win32::Graphics::Direct3D11::{ + D3D11_BIND_RENDER_TARGET, D3D11_SUBRESOURCE_DATA, D3D11_TEXTURE2D_DESC, + D3D11_USAGE_DEFAULT, + }; + use windows::Win32::Graphics::Dxgi::Common::{ + DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC, + }; + use windows::Win32::Graphics::Dxgi::{ + CreateDXGIFactory1, IDXGIFactory1, DXGI_ADAPTER_FLAG_SOFTWARE, + }; + + /// The 8 fully-saturated colour bars the matrix analysis samples (RGB). Saturated primaries + /// separate BT.601 from BT.709 by tens of code points (e.g. pure-green luma 145 vs 173). + const BARS: [(u8, u8, u8); 8] = [ + (255, 255, 255), // white + (255, 255, 0), // yellow + (0, 255, 255), // cyan + (0, 255, 0), // green + (255, 0, 255), // magenta + (255, 0, 0), // red + (0, 0, 255), // blue + (0, 0, 0), // black + ]; + + /// BGRA probe pattern: left half = the 8 colour bars (flat patches → matrix measurement), + /// right half = alternating 1-px red/blue columns (the chroma-resolution litmus: true 4:4:4 + /// keeps adjacent columns' chroma distinct; an internally-subsampled encode blends them). + fn probe_pattern(w: usize, h: usize) -> Vec { + let mut px = vec![0u8; w * h * 4]; + let bar_w = (w / 2) / BARS.len(); + for y in 0..h { + for x in 0..w { + let (r, g, b) = if x < w / 2 { + BARS[(x / bar_w).min(BARS.len() - 1)] + } else if x % 2 == 0 { + (255, 0, 0) // red column + } else { + (0, 0, 255) // blue column + }; + let o = (y * w + x) * 4; + px[o] = b; + px[o + 1] = g; + px[o + 2] = r; + px[o + 3] = 255; + } + } + px + } + + /// Encode 30 static pattern frames through the real NVENC session (ARGB input, the exact + /// production configuration) at the given chroma and write the Annex-B stream to `path`. + fn encode_pattern(chroma: ChromaFormat, path: &str) { + const W: u32 = 1280; + const H: u32 = 720; + // SAFETY (test-only): straight-line D3D11/DXGI COM calls on one thread; every out-pointer + // is checked before use; the texture/device outlive the encoder (dropped at scope end). + unsafe { + let factory: IDXGIFactory1 = CreateDXGIFactory1().expect("DXGI factory"); + let mut adapter = None; + for i in 0.. { + let Ok(a) = factory.EnumAdapters1(i) else { break }; + let desc = a.GetDesc1().expect("adapter desc"); + if desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0 as u32 == 0 { + adapter = Some(a); + break; + } + } + let adapter = adapter.expect("no hardware DXGI adapter"); + let (device, _ctx) = + crate::capture::dxgi::make_device(&adapter).expect("make_device"); + + let bytes = probe_pattern(W as usize, H as usize); + let init = D3D11_SUBRESOURCE_DATA { + pSysMem: bytes.as_ptr() as *const _, + SysMemPitch: W * 4, + SysMemSlicePitch: 0, + }; + let desc = D3D11_TEXTURE2D_DESC { + Width: W, + Height: H, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + // NVENC registration requires RENDER_TARGET on D3D11 input textures. + BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + let mut tex = None; + device + .CreateTexture2D(&desc, Some(&init), Some(&mut tex)) + .expect("pattern texture"); + let tex = tex.expect("null pattern texture"); + + let mut enc = NvencD3d11Encoder::open( + Codec::H265, + PixelFormat::Bgra, + W, + H, + 60, + 100_000_000, // high rate: the 1-px stripes must survive quantization + 8, + chroma, + ) + .expect("NVENC open"); + let mut out = Vec::new(); + for i in 0..30u64 { + let frame = CapturedFrame { + width: W, + height: H, + pts_ns: i * 16_666_667, + format: PixelFormat::Bgra, + payload: FramePayload::D3d11(D3d11Frame { + texture: tex.clone(), + device: device.clone(), + }), + }; + enc.submit(&frame).expect("submit"); + while let Some(au) = enc.poll().expect("poll") { + out.extend_from_slice(&au.data); + } + } + enc.flush().ok(); + while let Ok(Some(au)) = enc.poll() { + out.extend_from_slice(&au.data); + } + assert!(!out.is_empty(), "no AUs produced"); + let caps444 = enc.caps().chroma_444; + std::fs::write(path, &out).expect("write bitstream"); + println!( + "wrote {path}: {} bytes, requested {chroma:?}, caps.chroma_444={caps444}", + out.len() + ); + } + } + + /// ON-GLASS (RTX box): the measurement gating the AYUV 4:4:4 work — encodes the probe + /// pattern through the REAL ARGB-input NVENC session once with `chromaFormatIDC=3`/FREXT + /// and once as plain 4:2:0, so offline analysis of the two bitstreams answers (1) whether + /// the FREXT stream is truly full-chroma and (2) which matrix NVENC's internal RGB→YUV CSC + /// used (BT.601 vs BT.709 — saturated bars differ by tens of code points). Run with: + /// cargo test -p punktfunk-host --features nvenc -- --ignored nvenc_444_on_glass --nocapture + #[test] + #[ignore = "requires an NVIDIA GPU + driver — run manually on the RTX box"] + fn nvenc_444_on_glass_probe() { + encode_pattern(ChromaFormat::Yuv444, "C:\\Users\\Public\\nvenc444_probe.h265"); + encode_pattern(ChromaFormat::Yuv420, "C:\\Users\\Public\\nvenc420_probe.h265"); + } +} diff --git a/crates/punktfunk-host/src/gamestream/mod.rs b/crates/punktfunk-host/src/gamestream/mod.rs index f4f00300..1b40952f 100644 --- a/crates/punktfunk-host/src/gamestream/mod.rs +++ b/crates/punktfunk-host/src/gamestream/mod.rs @@ -53,8 +53,8 @@ pub const SCM_AV1_MAIN10: u32 = 0x0002_0000; /// host can actually deliver it ([`host_hdr_capable`]); it is never a static claim, because a non-HDR /// host (Linux, or a Windows host without the `PUNKTFUNK_10BIT` opt-in) must not invite a client into /// an HDR mode it can't produce. (The previous placeholder 3843 = 0xF03 wrongly claimed HEVC Main10 + -/// 4:4:4 and *no* AV1.) 4:4:4 stays off entirely: stock Moonlight is 4:2:0 and the Windows IDD-push -/// capturer can't yet deliver full-chroma frames (`crate::capture::capturer_supports_444`). +/// 4:4:4 and *no* AV1.) 4:4:4 stays off entirely on GameStream: stock Moonlight is 4:2:0 — +/// full-chroma is a punktfunk/1-native negotiation only (`crate::capture::capturer_supports_444`). pub const SERVER_CODEC_MODE_SUPPORT: u32 = SCM_H264 | SCM_HEVC | SCM_AV1_MAIN8; /// Whether this host can deliver an **HDR** (HEVC Main10 / BT.2020 PQ) GameStream — the single gate diff --git a/crates/punktfunk-host/src/gamestream/rtsp.rs b/crates/punktfunk-host/src/gamestream/rtsp.rs index e910c4d5..490c3df8 100644 --- a/crates/punktfunk-host/src/gamestream/rtsp.rs +++ b/crates/punktfunk-host/src/gamestream/rtsp.rs @@ -380,6 +380,38 @@ fn stream_config(map: &HashMap) -> Option { "client requested HDR (dynamicRangeMode != 0) but host is not HDR-capable — streaming 8-bit SDR" ); } + // The client's requested CSC (moonlight-common-c SdpGenerator.c: `encoderCscMode = + // (colorspace << 1) | fullRange` — colorspace 0=Rec601, 1=Rec709, 2=Rec2020). Moonlight + // renderers configure their YUV→RGB from this REQUESTED value (not the bitstream VUI), so a + // host that encodes something else shifts the client's colours. INSTRUMENTATION ONLY for + // now: we always encode BT.709 limited for SDR (the IDD VideoConverter / VUI-driven NVENC) + // and BT.2020 PQ for HDR — log what clients actually ask for so honoring `encoderCscMode` + // can be scoped from field data rather than guessed. (Absent on very old clients.) + if let Some(csc) = parse_u("x-nv-video[0].encoderCscMode") { + let (space, range) = ( + match csc >> 1 { + 0 => "Rec601", + 1 => "Rec709", + 2 => "Rec2020", + _ => "unknown", + }, + if csc & 1 != 0 { "full" } else { "limited" }, + ); + let ours = if hdr { "Rec2020 limited (PQ)" } else { "Rec709 limited" }; + let matches_ours = (hdr && csc >> 1 == 2 || !hdr && csc >> 1 == 1) && csc & 1 == 0; + if matches_ours { + tracing::info!(csc, space, range, "GameStream client requested CSC — matches ours"); + } else { + tracing::warn!( + csc, + requested = format!("{space} {range}"), + encoding = ours, + "GameStream client requested a CSC we don't encode — Moonlight renders by its \ + REQUEST, so its colours will be shifted (honoring encoderCscMode is a known \ + follow-up; report this log line)" + ); + } + } // Parity floor the client asks for (protects small frames); clamp to a sane max. let min_fec = parse_u("x-nv-vqos[0].fec.minRequiredFecPackets") .unwrap_or(2) diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index 42bcc8de..defbe0ee 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -979,6 +979,19 @@ async fn serve_session( "encode chroma" ); + // Linux 4:4:4 rides the CPU swscale → 8-bit `YUV444P` path (see `encode/linux`) — there + // is no 10-bit 4:4:4 input there, so a 10-bit-negotiated session would silently encode + // 8-bit. Resolve the depth DOWN before the Welcome so the wire never overstates what the + // stream carries. (Windows NVENC composes Main 4:4:4 10 from an RGB input, so it keeps + // the resolved depth — this clamp is Linux-only.) + #[cfg(target_os = "linux")] + let bit_depth: u8 = if chroma.is_444() && bit_depth == 10 { + tracing::info!("4:4:4 on the Linux path encodes 8-bit YUV444P — resolving bit depth 8"); + 8 + } else { + bit_depth + }; + // Reserve the data-plane UDP socket up front and HOLD it through streaming (no // bind→read→drop→rebind window a concurrent session could race for a fixed port). A fixed // `--data-port` yields `direct = true` (stream straight to the client's reported address, diff --git a/crates/punktfunk-host/src/session_plan.rs b/crates/punktfunk-host/src/session_plan.rs index 9dc93c1d..775d04b9 100644 --- a/crates/punktfunk-host/src/session_plan.rs +++ b/crates/punktfunk-host/src/session_plan.rs @@ -132,6 +132,16 @@ impl SessionPlan { let gpu = { let force_cpu_for_nvenc_444 = self.chroma.is_444() && !crate::encode::linux_zero_copy_is_vaapi(); + if gpu && force_cpu_for_nvenc_444 { + // Surface the trade loudly: this is the single biggest per-frame cost a 4:4:4 + // session adds (full-res CPU readback + swscale RGB→YUV444P every frame), and + // it looks like an unexplained fps ceiling if you don't know it happened. + tracing::warn!( + "4:4:4 session on the NVENC path: zero-copy GPU capture DISABLED — every \ + frame is CPU RGB + swscale RGB→YUV444P; expect a lower fps ceiling than \ + 4:2:0 at this mode" + ); + } gpu && !force_cpu_for_nvenc_444 }; crate::capture::OutputFormat {