From 739a5f76bf31ca518e3464d30f4c5d714fcc095f Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 15 Jul 2026 12:16:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(apple):=20PyroWave=20Phase=205=20=E2=80=94?= =?UTF-8?q?=20native=20Metal=20decode=20on=20Mac=20/=20Apple=20TV=20/=20iP?= =?UTF-8?q?ad=20(=C2=A74.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Apple client now decodes PyroWave natively on the presenter's own MTLDevice — no MoltenVK, no upstream C++ in the app. Completes and wires up the decoder whose early working-tree snapshot rode along in 9127c346: - MetalWaveletShaders.swift: wavelet_dequant + idwt hand-ported from the vendored GLSL (STORAGE_MODE 0 only; subgroup scans → 32-wide simdgroups; DCShift spec constant → function constant; precision-1 split: fp16 levels 0-1 / fp32 2-4). - MetalWaveletDecoder.swift: Swift reimplementation of push_packet/decode_packet incl. the Phase-4 chunk-aligned window walk (FRAG chains, zeroed missing shards, the >half-blocks partial rule), init_block_meta's block-index space, and the 42-dequant + 13-idwt dispatch structure with encoder-boundary barriers. SOF-dims changes rebuild the size-dependent resources, which is also the mid-stream resize path. Ring of 4 output plane sets on the presenter's queue. - Presenter: pf_frag_planar (3xR8, the planar_csc.frag twin) + renderPlanar with a shared present tail; ReadyFrame carries an image enum (.video | .planar). - Stage2Pipeline: a dedicated PyroWave pump — no VideoToolbox machinery, no keyframe/re-anchor recovery (all-intra; partials render as localized blur by design), newest-frame-index staleness guard for late partials. - Opt-in: "PyroWave (wired LAN)" codec entry (probe-gated, ≈A13 floor via a real kernel-compile probe), selecting it advertises + prefers the codec and forces the session SDR (HDR/10-bit/4:4:4 caps dropped, plan contract). - Core ABI: punktfunk_connection_shard_payload() — the Welcome's negotiated shard payload, needed by native decoders to walk chunk-aligned AUs. - Validation: golden fixtures generated by the host encoder + upstream's own decoder (pyrowave_dump_golden, RTX 5070 Ti); the Metal decode PSNR-matches at 77-88 dB across all planes for dense AND chunk-aligned AUs, and a hole-punched partial still decodes. Parser unit tests cover the window walk, FRAG chains, broken chains, the half-blocks gate, and the block-index layout. Tests: apple 134 green (mac; iOS/tvOS build), host 312 w/ pyrowave on .21, core 148 w/ quic; clippy/fmt clean. Co-Authored-By: Claude Fable 5 --- clients/apple/Package.swift | 9 +- .../Sources/PunktfunkClient/ContentView.swift | 1 + .../Session/SessionModel.swift | 12 + .../Settings/SettingsOptions.swift | 7 + .../Connection/PunktfunkConnection.swift | 22 +- .../PunktfunkKit/Support/DefaultsKeys.swift | 6 +- .../Sources/PunktfunkKit/Video/AV1.swift | 19 +- .../Sources/PunktfunkKit/Video/AnnexB.swift | 14 +- .../Video/MetalVideoPresenter.swift | 122 +++++++- .../Video/MetalWaveletDecoder.swift | 34 +- .../PunktfunkKit/Video/Stage2Pipeline.swift | 123 +++++++- .../PunktfunkKit/Video/VideoDecoder.swift | 41 ++- .../Tests/PunktfunkKitTests/AV1Tests.swift | 7 +- .../PyroWaveDecoderTests.swift | 292 ++++++++++++++++++ .../PyroWaveFixtures/au-chunked.bin | Bin 0 -> 61952 bytes .../PyroWaveFixtures/au-dense.bin | Bin 0 -> 44296 bytes .../PyroWaveFixtures/ref-chunked-cb.bin | 1 + .../PyroWaveFixtures/ref-chunked-cr.bin | 1 + .../PyroWaveFixtures/ref-chunked-y.bin | 1 + .../PyroWaveFixtures/ref-dense-cb.bin | 1 + .../PyroWaveFixtures/ref-dense-cr.bin | 1 + .../PyroWaveFixtures/ref-dense-y.bin | 1 + .../PunktfunkKitTests/Stage444Tests.swift | 15 +- .../VideoToolboxRoundTripTests.swift | 5 +- crates/punktfunk-core/src/abi.rs | 27 ++ .../src/encode/linux/pyrowave.rs | 117 ++++++- docs-site/content/docs/pyrowave.md | 16 +- include/punktfunk_core.h | 12 + 28 files changed, 826 insertions(+), 81 deletions(-) create mode 100644 clients/apple/Tests/PunktfunkKitTests/PyroWaveDecoderTests.swift create mode 100644 clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/au-chunked.bin create mode 100644 clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/au-dense.bin create mode 100644 clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-cb.bin create mode 100644 clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-cr.bin create mode 100644 clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-y.bin create mode 100644 clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-cb.bin create mode 100644 clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-cr.bin create mode 100644 clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-y.bin diff --git a/clients/apple/Package.swift b/clients/apple/Package.swift index 1afa0f18..fe75d522 100644 --- a/clients/apple/Package.swift +++ b/clients/apple/Package.swift @@ -42,6 +42,13 @@ let package = Package( .executableTarget(name: "PunktfunkClient", dependencies: ["PunktfunkKit"]), // PunktfunkCore is a direct dep too so the wire tests can name the C ABI's // `PunktfunkInputEvent` / `PUNKTFUNK_INPUT_KIND_*` when asserting the gamepad byte layout. - .testTarget(name: "PunktfunkKitTests", dependencies: ["PunktfunkKit", "PunktfunkCore"]), + .testTarget( + name: "PunktfunkKitTests", dependencies: ["PunktfunkKit", "PunktfunkCore"], + resources: [ + // PyroWave golden fixtures: host-encoded AUs + upstream-decoded reference + // planes (regenerate with punktfunk-host's `pyrowave_dump_golden` on a + // Vulkan box — see PyroWaveDecoderTests.swift). + .copy("PyroWaveFixtures") + ]), ] ) diff --git a/clients/apple/Sources/PunktfunkClient/ContentView.swift b/clients/apple/Sources/PunktfunkClient/ContentView.swift index 61020f82..32121f58 100644 --- a/clients/apple/Sources/PunktfunkClient/ContentView.swift +++ b/clients/apple/Sources/PunktfunkClient/ContentView.swift @@ -46,6 +46,7 @@ struct ContentView: View { case "h264": return PunktfunkConnection.codecH264 case "hevc": return PunktfunkConnection.codecHEVC case "av1": return PunktfunkConnection.codecAV1 + case "pyrowave": return PunktfunkConnection.codecPyroWave default: return 0 } } diff --git a/clients/apple/Sources/PunktfunkClient/Session/SessionModel.swift b/clients/apple/Sources/PunktfunkClient/Session/SessionModel.swift index cd951d3a..79c5d56d 100644 --- a/clients/apple/Sources/PunktfunkClient/Session/SessionModel.swift +++ b/clients/apple/Sources/PunktfunkClient/Session/SessionModel.swift @@ -239,6 +239,18 @@ final class SessionModel: ObservableObject { // from these + the soft `preferredCodec`; `resolvedCodec` reflects what it chose. var videoCodecs = PunktfunkConnection.codecH264 | PunktfunkConnection.codecHEVC if AV1.hardwareDecodeSupported { videoCodecs |= PunktfunkConnection.codecAV1 } + // PyroWave (wired LAN) is a pure opt-in: picking it in the codec setting both + // advertises the bit and prefers it — the host never auto-selects it, and the + // picker only offers it when the Metal decode probe passed (simdgroup floor ≈ A13; + // every M-series Mac and the ATV 4K gen 3 pass). The codec is 8-bit 4:2:0 SDR + // BT.709 by contract, so the opt-in also drops the HDR/10-bit/4:4:4 caps for this + // session — HDR sessions stay HEVC/AV1 (plan §4.7). + if preferredCodec == PunktfunkConnection.codecPyroWave, MetalWaveletDecoder.supported { + videoCodecs |= PunktfunkConnection.codecPyroWave + videoCaps &= ~(PunktfunkConnection.videoCap10Bit + | PunktfunkConnection.videoCapHDR + | PunktfunkConnection.videoCap444) + } let result = Result { try PunktfunkConnection( host: host.address, port: host.port, width: width, height: height, refreshHz: hz, diff --git a/clients/apple/Sources/PunktfunkClient/Settings/SettingsOptions.swift b/clients/apple/Sources/PunktfunkClient/Settings/SettingsOptions.swift index 22286142..de0b8657 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/SettingsOptions.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/SettingsOptions.swift @@ -79,6 +79,13 @@ enum SettingsOptions { if AV1.hardwareDecodeSupported { options.insert(("AV1", "av1"), at: 2) } + // PyroWave is the opt-in wired-LAN low-latency codec (100–400 Mbps all-intra wavelet, + // 8-bit SDR): selecting it advertises + prefers it for the session. Offered only when + // the Metal decode probe passes (same gate SessionModel advertises by) — elsewhere the + // host could never emit it. + if MetalWaveletDecoder.supported { + options.append(("PyroWave (wired LAN)", "pyrowave")) + } return options }() diff --git a/clients/apple/Sources/PunktfunkKit/Connection/PunktfunkConnection.swift b/clients/apple/Sources/PunktfunkKit/Connection/PunktfunkConnection.swift index 61e42f8b..8adc5955 100644 --- a/clients/apple/Sources/PunktfunkKit/Connection/PunktfunkConnection.swift +++ b/clients/apple/Sources/PunktfunkKit/Connection/PunktfunkConnection.swift @@ -337,9 +337,15 @@ public final class PunktfunkConnection { public private(set) var resolvedAudioChannels: UInt8 = 2 /// The video codec the host resolved for this session (`Welcome.codec`, `PUNKTFUNK_CODEC_*`): - /// `2` = HEVC (default / older host), `1` = H.264, `4` = AV1. Build the decoder from THIS. The - /// resolved value honors the client's `preferredCodec` when the host could emit it. + /// `2` = HEVC (default / older host), `1` = H.264, `4` = AV1, `8` = PyroWave (only when this + /// client opted in). Build the decoder from THIS. The resolved value honors the client's + /// `preferredCodec` when the host could emit it. public private(set) var resolvedCodec: UInt8 = 2 // PUNKTFUNK_CODEC_HEVC + + /// The session's negotiated wire shard payload (`Welcome.shard_payload`, bytes) — the + /// parse-window size for `USER_FLAG_CHUNK_ALIGNED` PyroWave AUs (plan §4.4). Other codecs + /// never need it. + public private(set) var shardPayload: UInt32 = 1408 /// The resolved codec as a `VideoCodec` (H.264 / HEVC / AV1) — drives the bitstream framing /// (Annex-B NAL parsing vs the AV1 OBU repack). public var videoCodec: VideoCodec { VideoCodec(wire: resolvedCodec) } @@ -452,6 +458,9 @@ public final class PunktfunkConnection { var codec: UInt8 = 2 // PUNKTFUNK_CODEC_HEVC _ = punktfunk_connection_codec(handle, &codec) resolvedCodec = codec + var shard: UInt32 = 1408 + _ = punktfunk_connection_shard_payload(handle, &shard) + shardPayload = shard } /// A bandwidth speed-test measurement (see `startSpeedTest`). Partial until `done`. @@ -790,6 +799,15 @@ public final class PunktfunkConnection { public static let codecH264: UInt8 = UInt8(PUNKTFUNK_CODEC_H264) public static let codecHEVC: UInt8 = UInt8(PUNKTFUNK_CODEC_HEVC) public static let codecAV1: UInt8 = UInt8(PUNKTFUNK_CODEC_AV1) + /// PyroWave (opt-in wired-LAN wavelet codec, 8-bit SDR): the host only ever resolves it + /// when the client both advertises the bit AND names it `preferredCodec` — never + /// auto-selected. Decoded by the Metal wavelet decoder, not VideoToolbox. + public static let codecPyroWave: UInt8 = UInt8(PUNKTFUNK_CODEC_PYROWAVE) + + /// `AccessUnit.flags` bit: the AU is shard-aligned self-delimiting chunks (the wire's + /// `USER_FLAG_CHUNK_ALIGNED`, PyroWave datagram-aligned mode §4.4) — walk it + /// window-by-window at `shardPayload`. (The C `#define` doesn't import into Swift.) + public static let userFlagChunkAligned: UInt32 = 64 /// Static HDR mastering metadata (SMPTE ST.2086 + content light level) the host sent for an HDR /// session. Mirrors the wire/ABI `PunktfunkHdrMeta`; primaries are in ST.2086 **G, B, R** order, diff --git a/clients/apple/Sources/PunktfunkKit/Support/DefaultsKeys.swift b/clients/apple/Sources/PunktfunkKit/Support/DefaultsKeys.swift index adbc5210..5b94cbba 100644 --- a/clients/apple/Sources/PunktfunkKit/Support/DefaultsKeys.swift +++ b/clients/apple/Sources/PunktfunkKit/Support/DefaultsKeys.swift @@ -27,8 +27,10 @@ public enum DefaultsKey { /// Requested audio channel count: 2 (stereo), 6 (5.1) or 8 (7.1). The host clamps to what it /// can capture; the resolved count drives the in-core decode + AVAudioEngine layout. public static let audioChannels = "punktfunk.audioChannels" - /// Preferred video codec: `"auto"` (host decides), `"hevc"`, or `"h264"`. A soft preference — - /// the host emits it when it can, else falls back. Drives the decoder via `Welcome.codec`. + /// Preferred video codec: `"auto"` (host decides), `"hevc"`, `"h264"`, `"av1"`, or + /// `"pyrowave"` (the opt-in wired-LAN wavelet codec — picking it advertises AND prefers it, + /// and forces the session SDR). A soft preference — the host emits it when it can, else + /// falls back. Drives the decoder via `Welcome.codec`. public static let codec = "punktfunk.codec" public static let micEnabled = "punktfunk.micEnabled" public static let speakerUID = "punktfunk.speakerUID" diff --git a/clients/apple/Sources/PunktfunkKit/Video/AV1.swift b/clients/apple/Sources/PunktfunkKit/Video/AV1.swift index cb0cc9c7..32050b36 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/AV1.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/AV1.swift @@ -543,19 +543,24 @@ public enum AV1 { extension VideoCodec { /// Codec-dispatching format-description refresh: the AV1 path keys on an in-band sequence - /// header, the NAL codecs on in-band parameter sets — one call site in each pump. + /// header, the NAL codecs on in-band parameter sets — one call site in each pump. PyroWave + /// has no CoreMedia representation at all (its pump feeds the Metal wavelet decoder raw). public func formatDescription(fromKeyframe au: Data) -> CMVideoFormatDescription? { - self == .av1 - ? AV1.formatDescription(fromKeyframe: au) - : AnnexB.formatDescription(fromIDR: au, codec: self) + switch self { + case .av1: return AV1.formatDescription(fromKeyframe: au) + case .pyrowave: return nil + default: return AnnexB.formatDescription(fromIDR: au, codec: self) + } } /// Codec-dispatching sample wrap (see `formatDescription(fromKeyframe:)`). public func sampleBuffer( au: AccessUnit, format: CMVideoFormatDescription ) -> CMSampleBuffer? { - self == .av1 - ? AV1.sampleBuffer(au: au, format: format) - : AnnexB.sampleBuffer(au: au, format: format, codec: self) + switch self { + case .av1: return AV1.sampleBuffer(au: au, format: format) + case .pyrowave: return nil + default: return AnnexB.sampleBuffer(au: au, format: format, codec: self) + } } } diff --git a/clients/apple/Sources/PunktfunkKit/Video/AnnexB.swift b/clients/apple/Sources/PunktfunkKit/Video/AnnexB.swift index 303e7a8d..0b2821c1 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/AnnexB.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/AnnexB.swift @@ -26,12 +26,18 @@ public enum VideoCodec: Equatable { case h264 case hevc case av1 + /// PyroWave wavelet (opt-in wired-LAN low-latency codec): not a NAL/OBU codec and not + /// VideoToolbox-decoded at all — the Metal wavelet decoder consumes the raw AUs + /// (Stage2Pipeline's PyroWave pump). Only ever resolved when this client both advertised + /// and preferred it. + case pyrowave /// Resolve from the wire `Welcome.codec` byte (`PUNKTFUNK_CODEC_*`; unknown → HEVC). public init(wire: UInt8) { switch wire { case 0x01: self = .h264 // PUNKTFUNK_CODEC_H264 case 0x04: self = .av1 // PUNKTFUNK_CODEC_AV1 + case 0x08: self = .pyrowave // PUNKTFUNK_CODEC_PYROWAVE default: self = .hevc // PUNKTFUNK_CODEC_HEVC — the default / older-host codec } } @@ -147,8 +153,8 @@ public enum AnnexB { sets = [vps, sps, pps] case .h264: sets = [sps, pps] - case .av1: - return nil // OBU stream, no parameter-set NALs — handled in AV1.swift, never here + case .av1, .pyrowave: + return nil // no parameter-set NALs — dispatched in AV1.swift, never reaches here } var format: CMVideoFormatDescription? @@ -184,8 +190,8 @@ public enum AnnexB { parameterSetSizes: sizes, nalUnitHeaderLength: 4, formatDescriptionOut: &format) - case .av1: - break // unreachable — the .av1 arm above already returned + case .av1, .pyrowave: + break // unreachable — the arm above already returned } } return status == noErr ? format : nil diff --git a/clients/apple/Sources/PunktfunkKit/Video/MetalVideoPresenter.swift b/clients/apple/Sources/PunktfunkKit/Video/MetalVideoPresenter.swift index 25a81d4a..dc2a587e 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/MetalVideoPresenter.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/MetalVideoPresenter.swift @@ -149,6 +149,28 @@ fragment float4 pf_frag(VOut in [[stage_in]], return float4(sampleRgb(lumaTex, chromaTex, in.uv, csc), 1.0); } +// PyroWave planar SDR: three separate R8 planes (Y full-res, Cb/Cr half-res 4:2:0) from the +// Metal wavelet decoder — the Metal twin of pf-presenter's planar_csc.frag. Same bicubic luma +// and left-cosited chroma correction as the biplanar path (chromaUV self-disables at 4:4:4). +fragment float4 pf_frag_planar(VOut in [[stage_in]], + texture2d lumaTex [[texture(0)]], + texture2d cbTex [[texture(1)]], + texture2d crTex [[texture(2)]], + constant CscUniform& csc [[buffer(0)]]) { + constexpr sampler s(filter::linear, address::clamp_to_edge); +#ifdef PF_BILINEAR_LUMA + float lumaY = lumaTex.sample(s, in.uv).r; +#else + float lumaY = catmullRomLuma(lumaTex, s, in.uv); +#endif + float2 cuv = chromaUV(lumaTex, cbTex, in.uv); + float3 yuv = float3(lumaY, cbTex.sample(s, cuv).r, crTex.sample(s, cuv).r); + float3 rgb = 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)); + return float4(rgb, 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 @@ -215,8 +237,16 @@ public final class MetalVideoPresenter { /// 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? + /// PyroWave's 3-plane SDR path (pf_frag_planar → bgra8) — see `renderPlanar`. + private let pipelinePlanar: MTLRenderPipelineState private var textureCache: CVMetalTextureCache? + /// The PyroWave Metal decoder records on the presenter's device + queue: one device means + /// decode, CSC and present share textures with zero interop, and one queue means Metal's + /// hazard tracking orders a ring-slot rewrite after the render still sampling it. + var metalDevice: MTLDevice { device } + var metalQueue: MTLCommandQueue { queue } + /// 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). @@ -258,6 +288,7 @@ public final class MetalVideoPresenter { let pipelineSDR: MTLRenderPipelineState let pipelineHDR: MTLRenderPipelineState let pipelineHDRToneMap: MTLRenderPipelineState? + let pipelinePlanar: MTLRenderPipelineState do { // DEBUG A/B lever: PUNKTFUNK_BILINEAR_LUMA=1 compiles the shader with Catmull-Rom OFF // (plain bilinear luma) by prepending a #define ahead of the source. Default (unset) is @@ -292,6 +323,11 @@ public final class MetalVideoPresenter { #else pipelineHDRToneMap = nil #endif + let planar = MTLRenderPipelineDescriptor() + planar.vertexFunction = vtx + planar.fragmentFunction = library.makeFunction(name: "pf_frag_planar") + planar.colorAttachments[0].pixelFormat = .bgra8Unorm // PyroWave is 8-bit SDR + pipelinePlanar = try device.makeRenderPipelineState(descriptor: planar) } catch { return nil } @@ -331,12 +367,14 @@ public final class MetalVideoPresenter { return MetalVideoPresenter( device: device, queue: queue, pipelineSDR: pipelineSDR, pipelineHDR: pipelineHDR, - pipelineHDRToneMap: pipelineHDRToneMap, textureCache: textureCache, layer: layer) + pipelineHDRToneMap: pipelineHDRToneMap, pipelinePlanar: pipelinePlanar, + textureCache: textureCache, layer: layer) } private init( device: MTLDevice, queue: MTLCommandQueue, pipelineSDR: MTLRenderPipelineState, pipelineHDR: MTLRenderPipelineState, pipelineHDRToneMap: MTLRenderPipelineState?, + pipelinePlanar: MTLRenderPipelineState, textureCache: CVMetalTextureCache, layer: CAMetalLayer ) { self.device = device @@ -344,6 +382,7 @@ public final class MetalVideoPresenter { self.pipelineSDR = pipelineSDR self.pipelineHDR = pipelineHDR self.pipelineHDRToneMap = pipelineHDRToneMap + self.pipelinePlanar = pipelinePlanar self.textureCache = textureCache self.layer = layer } @@ -514,6 +553,67 @@ public final class MetalVideoPresenter { pixelBuffer, plane: 1, format: tenBit ? .rg16Unorm : .rg8Unorm, cache: textureCache) 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) + let pipeline = hdrActive ? hdrPipeline : pipelineSDR + #else + let pipeline = hdrActive ? pipelineHDR : pipelineSDR + #endif + let decodedSize = CGSize( + width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer)) + return encodePresent( + decodedSize: decodedSize, targetFromLayout: targetFromLayout, pipeline: pipeline, + presentAtMediaTime: presentAtMediaTime, onPresented: onPresented, + // Hold the CVMetalTextures + source pixel buffer (its IOSurface) alive until the GPU + // finishes sampling — releasing them at scope exit could free the backing mid-read. + keepAlive: [luma, chroma, pixelBuffer] + ) { encoder in + encoder.setFragmentTexture(CVMetalTextureGetTexture(luma), index: 0) + encoder.setFragmentTexture(CVMetalTextureGetTexture(chroma), index: 1) + encoder.setFragmentBytes(&csc, length: MemoryLayout.stride, index: 0) + } + } + + /// Draw one PyroWave planar frame (three R8 planes off the Metal wavelet decoder) and + /// present it. RENDER THREAD, same contract as `render` — PyroWave is 8-bit SDR, so the + /// layer always takes the plain SDR config, and the CSC rows arrive precomputed from the + /// stream's own sequence-header signaling (no CVPixelBuffer to inspect). + @discardableResult + func renderPlanar( + _ planes: WaveletPlanes, + presentAtMediaTime: CFTimeInterval? = nil, + onPresented: ((Int64?) -> Void)? = nil + ) -> Bool { + stagingLock.lock() + let targetFromLayout = drawableTarget + stagingLock.unlock() + configure(hdr: false) + var csc = planes.csc + return encodePresent( + decodedSize: CGSize(width: planes.width, height: planes.height), + targetFromLayout: targetFromLayout, pipeline: pipelinePlanar, + presentAtMediaTime: presentAtMediaTime, onPresented: onPresented, + // The ring textures stay valid by ring depth; retaining them here also pins the + // slot's set until the sample completes (mirrors the biplanar keep-alive). + keepAlive: [planes.y, planes.cb, planes.cr] + ) { encoder in + encoder.setFragmentTexture(planes.y, index: 0) + encoder.setFragmentTexture(planes.cb, index: 1) + encoder.setFragmentTexture(planes.cr, index: 2) + encoder.setFragmentBytes(&csc, length: MemoryLayout.stride, index: 0) + } + } + + /// The shared present tail of `render`/`renderPlanar`: size the drawable, encode one + /// fullscreen triangle with `pipeline` (`bind` supplies the fragment resources), schedule + /// the present and the on-glass callback. + private func encodePresent( + decodedSize: CGSize, targetFromLayout: CGSize, pipeline: MTLRenderPipelineState, + presentAtMediaTime: CFTimeInterval?, onPresented: ((Int64?) -> Void)?, + keepAlive: [Any], bind: (MTLRenderCommandEncoder) -> Void + ) -> Bool { // Size the drawable to the LAYER's pixels (its laid-out frame × contentsScale, pushed here by // SessionPresenter.layout via `setDrawableTarget` — not read off the layer, whose geometry the // main thread owns) so the Catmull-Rom shader performs the decoded→on-screen scale in one pass: @@ -522,8 +622,6 @@ public final class MetalVideoPresenter { // Before the first layout (zero target) fall back to the decoded size. drawableSize does NOT // track bounds (defaults to 0), so set it BEFORE nextDrawable; re-set only on a change // (layout / Reconfigure / HDR flip — and every frame of a live resize, which is fine). - let decodedSize = CGSize( - width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer)) let targetSize = (targetFromLayout.width > 0 && targetFromLayout.height > 0) ? targetFromLayout : decodedSize if layer.drawableSize != targetSize { layer.drawableSize = targetSize } @@ -542,17 +640,8 @@ 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.setRenderPipelineState(pipeline) + bind(encoder) encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) encoder.endEncoding() if let onPresented { @@ -580,9 +669,8 @@ public final class MetalVideoPresenter { } else { commandBuffer.present(drawable) } - // Hold the CVMetalTextures + source pixel buffer (its IOSurface) alive until the GPU finishes - // sampling — releasing them at scope exit could free the backing mid-read. - commandBuffer.addCompletedHandler { _ in _ = (luma, chroma, pixelBuffer) } + // Keep the bound sources alive until the GPU finishes sampling (see the callers). + commandBuffer.addCompletedHandler { _ in _ = keepAlive } commandBuffer.commit() return true } diff --git a/clients/apple/Sources/PunktfunkKit/Video/MetalWaveletDecoder.swift b/clients/apple/Sources/PunktfunkKit/Video/MetalWaveletDecoder.swift index a440cb19..a39d6b58 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/MetalWaveletDecoder.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/MetalWaveletDecoder.swift @@ -183,18 +183,12 @@ enum WaveletBitstream { } return state.pushPackets(UnsafeBufferPointer(start: base, count: count)) } - guard ok, var frame = state.finish() else { return nil } + guard ok, let frame = state.finish() else { return nil } // Upstream decode_is_ready(allow_partial=true): with no SOF the frame is undecodable; // at half the blocks or fewer it is presumed garbage. guard frame.totalBlocks > 0, frame.decodedBlocks > frame.totalBlocks / 2 else { return nil } - // The dequant kernel indexes the offset table by the LAYOUT's block space; the wire's - // total_blocks only counts blocks the encoder emitted. They agree for a full-coverage - // frame, but size the table by the layout. - if frame.offsets.count != frame.layout.blockCount32 { - frame.offsets = Array(frame.offsets.prefix(frame.layout.blockCount32)) - } return frame } @@ -293,17 +287,17 @@ enum WaveletBitstream { /// One decoded frame's output planes, handed to the presenter's planar render path. The /// textures belong to the decoder's ring — ring depth (4) plus same-queue hazard tracking keep -/// them valid while referenced. -struct WaveletPlanes { - let y: MTLTexture - let cb: MTLTexture - let cr: MTLTexture - let csc: CscUniform - var width: Int { y.width } - var height: Int { y.height } +/// them valid while referenced. Public because it rides inside `ReadyImage`. +public struct WaveletPlanes: @unchecked Sendable { + public let y: MTLTexture + public let cb: MTLTexture + public let cr: MTLTexture + public let csc: CscUniform + public var width: Int { y.width } + public var height: Int { y.height } } -final class MetalWaveletDecoder { +public final class MetalWaveletDecoder { /// Matches the Vulkan client's ring: deep enough that a slot is never rewritten while the /// presenter still samples it in practice; same-queue hazard tracking is the hard backstop. private static let ringDepth = 4 @@ -312,7 +306,7 @@ final class MetalWaveletDecoder { /// dequant kernel needs simdgroup prefix sums with its 16 header lanes inside one /// simdgroup, so compile the real kernels once and check the pipeline facts. Apple6 (A13) /// and every Mac2 device pass the family check; the compile probe is authoritative. - static let supported: Bool = { + public static let supported: Bool = { guard let device = MTLCreateSystemDefaultDevice() else { return false } guard device.supportsFamily(.apple6) || device.supportsFamily(.mac2) else { return false } do { @@ -358,6 +352,12 @@ final class MetalWaveletDecoder { private var slots: [Slot] = [] private var nextSlot = 0 + /// The current geometry (from the last SOF that built the resources) — the pump reports + /// decoded-size changes to the resize overlay from this. PUMP THREAD. + var decodedSize: (width: Int, height: Int)? { + layout.map { ($0.width, $0.height) } + } + /// The pump thread owns `decode`; everything mutable is confined to it. init?(device: MTLDevice, queue: MTLCommandQueue) { self.device = device diff --git a/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift b/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift index 3d41de91..f3a09419 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/Stage2Pipeline.swift @@ -37,6 +37,7 @@ #if canImport(Metal) && canImport(QuartzCore) import AVFoundation import Foundation +import Metal import QuartzCore /// PUNKTFUNK_PRESENT_DEBUG=1: the render thread prints a once-per-second line with the decode @@ -257,6 +258,7 @@ public final class Stage2Pipeline { /// the pipeline's lifetime; SessionPresenter resolves it per session (see PresentPacing). private let pacing: PresentPacing private let endToEndMeter: LatencyMeter? + private let decodeMeter: LatencyMeter? private let displayMeter: LatencyMeter? private let recovery = KeyframeRecovery() /// Post-loss freeze-until-reanchor gate (shared core policy via the C ABI). Created here seeded 0; @@ -306,6 +308,7 @@ public final class Stage2Pipeline { self.presenter = presenter self.pacing = pacing self.endToEndMeter = endToEndMeter + self.decodeMeter = decodeMeter self.displayMeter = displayMeter let ring = ring let recovery = recovery @@ -362,7 +365,21 @@ public final class Stage2Pipeline { let presenter = presenter let pumpStopped = pumpStopped let reanchorGate = gate - let thread = Thread { + // PyroWave rides a different decode half: no CMFormatDescription/VideoToolbox machinery + // (a wavelet AU has no parameter sets), no keyframe recovery or re-anchor freeze (the + // stream is all-intra and Phase 4's partial delivery WANTS lossy frames on glass as + // localized blur, not a freeze). The ready ring, render thread, pacing and meters are + // shared unchanged. + let thread: Thread + if connection.videoCodec == .pyrowave { + thread = Self.makePyroWavePump( + connection: connection, token: token, pumpStopped: pumpStopped, + ring: ring, renderSignal: renderSignal, + device: presenter.metalDevice, queue: presenter.metalQueue, + decodeMeter: decodeMeter, + onFrame: onFrame, onSessionEnd: onSessionEnd, onDecodedSize: onDecodedSize) + } else { + thread = Thread { defer { pumpStopped.signal() } // let stop() join the pump (bounded) before decoder.reset() var format: CMVideoFormatDescription? // Report coded dims to the resize overlay only on a CHANGE (new-mode IDR), not per @@ -445,6 +462,7 @@ public final class Stage2Pipeline { } } } + } } thread.name = "punktfunk-stage2-pump" thread.qualityOfService = .userInteractive @@ -504,9 +522,7 @@ public final class Stage2Pipeline { let presentAt = vsyncEnabled ? vsyncClock.nextVsync(after: CACurrentMediaTime()) : nil let renderStarted = CACurrentMediaTime() - let rendered = presenter.render( - frame.pixelBuffer, isHDR: frame.isHDR, presentAtMediaTime: presentAt - ) { presentedNs in + let onGlass: (Int64?) -> Void = { presentedNs in // Stage-3: the flip reached glass (or was dropped) — free the present slot, // then re-signal so the freshest waiting ring frame goes out immediately. if let gate { @@ -525,6 +541,18 @@ public final class Stage2Pipeline { displayMeter?.record(ptsNs: UInt64(frame.decodedNs), atNs: atNs, offsetNs: 0) debugStats?.presented(atNs: presentedNs) } + // One present tail, two decode sources: the VideoToolbox biplanar buffer or the + // PyroWave Metal planes — the ring, pacing and meters are agnostic to which. + let rendered: Bool + switch frame.image { + case .video(let pixelBuffer, let isHDR): + rendered = presenter.render( + pixelBuffer, isHDR: isHDR, presentAtMediaTime: presentAt, + onPresented: onGlass) + case .planar(let planes): + rendered = presenter.renderPlanar( + planes, presentAtMediaTime: presentAt, onPresented: onGlass) + } debugStats?.renderReturned( ok: rendered, tookMs: (CACurrentMediaTime() - renderStarted) * 1000) if !rendered { @@ -592,6 +620,93 @@ public final class Stage2Pipeline { renderSignal.signal() // wake the render thread so it can observe the stop and exit } + /// The PyroWave pump: AUs go straight into the Metal wavelet decoder (no VideoToolbox, no + /// format descriptions), decoded planes ride the same ready ring / render thread. All-intra + /// stream, so none of the VT pump's recovery machinery applies: keyframe/RFI requests are + /// silenced host-side for this codec, and a lossy (partial-delivery) frame is MEANT to + /// present as localized blur — never a freeze. Static + capture-by-parameter for the same + /// reason the VT pump avoids capturing `self` (a missed stop must not leak a live pipeline). + private static func makePyroWavePump( + connection: PunktfunkConnection, token: StopFlag, pumpStopped: DispatchSemaphore, + ring: ReadyRing, renderSignal: DispatchSemaphore, + device: MTLDevice, queue: MTLCommandQueue, + decodeMeter: LatencyMeter?, + onFrame: (@Sendable (AccessUnit) -> Void)?, + onSessionEnd: (@Sendable () -> Void)?, + onDecodedSize: (@Sendable (Int, Int) -> Void)? + ) -> Thread { + // The chunk-aligned parse window = the session's negotiated shard payload (Welcome); + // the 64-byte floor mirrors the Rust client's guard against a nonsense value. + let windowSize = max(64, Int(connection.shardPayload)) + return Thread { + defer { pumpStopped.signal() } + // Compiles the two compute kernels on the session's first frames' thread — ~tens of + // ms, once per session. Failure = this device can't run the negotiated codec (the + // advertisement probe should have prevented this); end the session cleanly. + guard let decoder = MetalWaveletDecoder(device: device, queue: queue) else { + if !token.isStopped { onSessionEnd?() } + return + } + // Newest decoded frame index — a late partial (the reassembler's 30 ms fuse can + // deliver one behind a newer complete frame) must not travel back in time. + var newestIndex: UInt32? + var lastDims: (w: Int, h: Int)? + var alive = true + while alive, !token.isStopped { + alive = autoreleasepool { () -> Bool in + do { + guard let au = try connection.nextAU(timeoutMs: 100) else { return true } + onFrame?(au) + if let newest = newestIndex, + Int32(bitPattern: au.frameIndex &- newest) <= 0 { + return true // stale (or duplicate) frame — skip + } + guard !token.isStopped else { return true } + let chunkAligned = + au.flags & PunktfunkConnection.userFlagChunkAligned != 0 + let ptsNs = au.ptsNs + let receivedNs = au.receivedNs + let flags = au.flags + let submitted = decoder.decode( + au: au.data, chunkAligned: chunkAligned, windowSize: windowSize + ) { planes in + // Metal completed-handler thread — stamp + enqueue, don't block + // (the exact contract of the VT output callback). + guard let planes else { return } + var ts = timespec() + clock_gettime(CLOCK_REALTIME, &ts) + let decodedNs = + Int64(ts.tv_sec) * 1_000_000_000 + Int64(ts.tv_nsec) + decodeMeter?.record( + ptsNs: UInt64(receivedNs), atNs: decodedNs, offsetNs: 0) + ring.submit( + ReadyFrame( + ptsNs: ptsNs, receivedNs: receivedNs, decodedNs: decodedNs, + image: .planar(planes), flags: flags)) + renderSignal.signal() + } + if submitted { + newestIndex = au.frameIndex + // Decoded-size changes come from the SOF dims (this is also how a + // mid-stream Reconfigure lands here) — report like the VT pump. + if let size = decoder.decodedSize, + lastDims?.w != size.width || lastDims?.h != size.height { + lastDims = (size.width, size.height) + onDecodedSize?(size.width, size.height) + } + } + // A dropped AU (malformed / SOF lost / too few blocks) is just skipped: + // every PyroWave frame is independently decodable, the next one heals. + return true + } catch { + if !token.isStopped { onSessionEnd?() } + return false // session closed + } + } + } + } + } + /// Convert a `CADisplayLink.targetTimestamp` (CACurrentMediaTime basis) to a `CLOCK_REALTIME` /// nanosecond instant — the present clock the AU pts + skew offset live in. Projects to the target /// present time (when the frame is actually on glass), not the moment we drew. diff --git a/clients/apple/Sources/PunktfunkKit/Video/VideoDecoder.swift b/clients/apple/Sources/PunktfunkKit/Video/VideoDecoder.swift index 6115f9bf..05e45a36 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/VideoDecoder.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/VideoDecoder.swift @@ -12,7 +12,23 @@ import CoreVideo import Foundation import VideoToolbox -/// One decoded frame waiting to be presented. Owns a retained `CVPixelBuffer` until shown. +/// A decoded frame's pixels — which present path they take. VideoToolbox codecs deliver a +/// biplanar `CVPixelBuffer` (NV12/P010/444v/x444); the PyroWave Metal decoder delivers three +/// separate R8 plane textures straight off its compute pass (there is no CVPixelBuffer — the +/// planes never leave the GPU). +public enum ReadyImage: @unchecked Sendable { + /// 8-bit NV12 / 4:4:4 biplanar (SDR) or 10-bit P010 / x444 (HDR), Metal-compatible. + /// `isHDR` = the stream is BT.2020 PQ and the presenter must configure EDR output. + case video(CVPixelBuffer, isHDR: Bool) + #if canImport(Metal) + /// PyroWave planar output (Y full-res + Cb/Cr half-res, 8-bit SDR) with its precomputed + /// CSC rows — presented by `MetalVideoPresenter.renderPlanar`. + case planar(WaveletPlanes) + #endif +} + +/// One decoded frame waiting to be presented. Owns its image (a retained `CVPixelBuffer`, or +/// the PyroWave ring textures) until shown. public struct ReadyFrame: @unchecked Sendable { /// Host capture clock (the AU's pts), in nanoseconds. public let ptsNs: UInt64 @@ -22,15 +38,26 @@ public struct ReadyFrame: @unchecked Sendable { public let receivedNs: Int64 /// Client `CLOCK_REALTIME` instant decode completed, in nanoseconds. public let decodedNs: Int64 - /// The decoded image — 8-bit NV12 biplanar (SDR) or 10-bit P010 biplanar (HDR), Metal-compatible. - public let pixelBuffer: CVPixelBuffer - /// True when the stream is HDR (BT.2020 PQ): the buffer is 10-bit P010 and the presenter must - /// configure EDR + BT.2020 PQ output. Derived from the decoded buffer's pixel format. - public let isHDR: Bool + /// The decoded image and which present path it takes. + public let image: ReadyImage /// The AU's wire `user_flags` (`AccessUnit.flags`), threaded through the decode via the frame /// context so the re-anchor gate can classify this decoded frame (IDR / RFI anchor / recovery /// mark) at present time — the async decode callback has no other access to it. 0 when unknown. public let flags: UInt32 + + /// The VideoToolbox path's buffer; nil for a PyroWave planar frame. (Kept as the accessor + /// the decode round-trip tests assert against.) + public var pixelBuffer: CVPixelBuffer? { + if case .video(let buffer, _) = image { return buffer } + return nil + } + + /// Whether this frame presents on the HDR path. PyroWave planar frames are 8-bit SDR by + /// contract. + public var isHDR: Bool { + if case .video(_, let hdr) = image { return hdr } + return false + } } /// Per-frame context threaded through the VideoToolbox frame refcon: the AU's receipt instant (for @@ -286,6 +313,6 @@ public final class VideoDecoder: @unchecked Sendable { onDecoded( ReadyFrame( ptsNs: ptsNs, receivedNs: receivedNs, decodedNs: decodedNs, - pixelBuffer: imageBuffer, isHDR: isHDR, flags: flags)) + image: .video(imageBuffer, isHDR: isHDR), flags: flags)) } } diff --git a/clients/apple/Tests/PunktfunkKitTests/AV1Tests.swift b/clients/apple/Tests/PunktfunkKitTests/AV1Tests.swift index 2217603d..2bd574d5 100644 --- a/clients/apple/Tests/PunktfunkKitTests/AV1Tests.swift +++ b/clients/apple/Tests/PunktfunkKitTests/AV1Tests.swift @@ -237,10 +237,11 @@ final class AV1Tests: XCTestCase { let ready = try XCTUnwrap(frame) XCTAssertEqual(ready.ptsNs, 42_000_000) XCTAssertFalse(ready.isHDR) - XCTAssertEqual(CVPixelBufferGetWidth(ready.pixelBuffer), 320) - XCTAssertEqual(CVPixelBufferGetHeight(ready.pixelBuffer), 180) + let buffer = try XCTUnwrap(ready.pixelBuffer, "a VT decode delivers a .video frame") + XCTAssertEqual(CVPixelBufferGetWidth(buffer), 320) + XCTAssertEqual(CVPixelBufferGetHeight(buffer), 180) XCTAssertEqual( - CVPixelBufferGetPixelFormatType(ready.pixelBuffer), + CVPixelBufferGetPixelFormatType(buffer), kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, "SDR AV1 must decode to NV12") decoder.reset() } diff --git a/clients/apple/Tests/PunktfunkKitTests/PyroWaveDecoderTests.swift b/clients/apple/Tests/PunktfunkKitTests/PyroWaveDecoderTests.swift new file mode 100644 index 00000000..8c28cd15 --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/PyroWaveDecoderTests.swift @@ -0,0 +1,292 @@ +// PyroWave Metal decoder tests — two layers: +// +// 1. Bitstream/window-walk parser tests (pure CPU): hand-crafted packet streams assert the +// exact wire semantics of pyrowave_decoder.cpp's push_packet walk + the Phase-4 +// chunk-aligned framing (4-byte window prefix, FRAG chains, zeroed missing shards). +// +// 2. Golden-frame PSNR tests (Metal GPU): host-encoded fixtures (crates/punktfunk-host +// encode/linux/pyrowave.rs `pyrowave_dump_golden`, run on a Vulkan box) decoded by the +// Metal port and PSNR-matched against upstream's own decoder output. Float wavelet math is +// not bit-exact across implementations (upstream ships precision variants), so the gate is +// PSNR, not equality. This is the §4.7 validation oracle for the hand-ported kernels — +// the gather/mirror addressing in idwt is the spot most likely to drift. + +#if canImport(Metal) +import Metal +import XCTest + +@testable import PunktfunkKit + +final class PyroWaveParserTests: XCTestCase { + // 256x144 → aligned 256x160; block space identical to the committed fixtures. + private let width = 256 + private let height = 144 + + /// A BitstreamSequenceHeader (START_OF_FRAME) for `width`x`height`, 4:2:0 BT.709 limited. + private func sof(totalBlocks: Int, sequence: UInt32 = 1) -> [UInt8] { + let word0 = + UInt32(width - 1) | (UInt32(height - 1) << 14) | (sequence << 28) | (1 << 31) + // code=0 (SOF), chroma=0 (420), primaries/trc/matrix=0 (BT.709), range=1 (LIMITED), + // siting=0. + let word1 = UInt32(totalBlocks) | (1 << 30) + return le32(word0) + le32(word1) + } + + /// A minimal coefficient packet: ballot=0 (all 8x8 blocks empty — legal and decodable), + /// payload_words=2 (header only). + private func packet(blockIndex: Int, sequence: UInt32 = 1) -> [UInt8] { + let word0 = UInt32(0) | (2 << 16) | (sequence << 28) + let word1 = UInt32(0) | (UInt32(blockIndex) << 8) + return le32(word0) + le32(word1) + } + + private func le32(_ v: UInt32) -> [UInt8] { + [UInt8(v & 0xff), UInt8((v >> 8) & 0xff), UInt8((v >> 16) & 0xff), UInt8(v >> 24)] + } + + /// Wrap bodies into `windowSize`-sized windows with the 4-byte used/kind prefix. + private func window(_ body: [UInt8], kind: UInt16, size: Int) -> [UInt8] { + precondition(body.count + 4 <= size) + var out = [UInt8(body.count & 0xff), UInt8(body.count >> 8)] + out += [UInt8(kind & 0xff), UInt8(kind >> 8)] + out += body + out += [UInt8](repeating: 0, count: size - out.count) + return out + } + + func testLayoutMatchesUpstreamBlockSpace() { + // init_block_meta's walk for 256x144 (aligned 256x160): level extents halve from + // 128x80; per (comp,level,band) count32 = ceil(ceil(w/8)/4) * ceil(ceil(h/8)/4). + let layout = WaveletLayout(width: width, height: height) + XCTAssertEqual(layout.alignedWidth, 256) + XCTAssertEqual(layout.alignedHeight, 160) + XCTAssertEqual(layout.levelWidth(0), 128) + XCTAssertEqual(layout.levelHeight(0), 80) + XCTAssertEqual(layout.levelWidth(4), 8) + XCTAssertEqual(layout.levelHeight(4), 5) + // Hand-summed: L4 (8x5 → 1 block) × 3 comps × 4 bands = 12; L3 (16x10 → 1) × 9 = 9; + // L2 (32x20 → 1) × 9 = 9; L1 (64x40 → 2x2=4... ) — trust the invariant instead: + // every band's count is ceil(w8/4)*ceil(h8/4) and the total is their sum. + var expected = 0 + for level in stride(from: 4, through: 0, by: -1) { + let w8 = (layout.levelWidth(level) + 7) / 8 + let h8 = (layout.levelHeight(level) + 7) / 8 + let per = ((w8 + 3) / 4) * ((h8 + 3) / 4) + for component in 0..<3 { + if level == 0 && component != 0 { continue } + expected += per * (level == 4 ? 4 : 3) + } + } + XCTAssertEqual(layout.blockCount32, expected) + // The finest luma level's stride is its 32-block row width. + XCTAssertEqual(layout.blockMeta[0][0][1].stride, (128 + 31) / 32) + // Level-0 chroma is not coded in 4:2:0. + XCTAssertEqual(layout.blockMeta[1][0][1].offset, -1) + } + + func testDenseParseFillsOffsetsAndCountsBlocks() throws { + let layout = WaveletLayout(width: width, height: height) + var au = sof(totalBlocks: 4) + au += packet(blockIndex: 0) + au += packet(blockIndex: 3) + au += packet(blockIndex: 3) // duplicate — first wins, not double-counted + au += packet(blockIndex: layout.blockCount32 - 1) + let frame = try XCTUnwrap( + WaveletBitstream.parse(au: Data(au), chunkAligned: false, windowSize: 0)) + XCTAssertEqual(frame.layout.width, width) + XCTAssertEqual(frame.totalBlocks, 4) + XCTAssertEqual(frame.decodedBlocks, 3) + XCTAssertEqual(frame.offsets[0], 0) + XCTAssertEqual(frame.offsets[3], 2) // u32 words: each header-only packet is 2 words + XCTAssertEqual(frame.offsets[1], UInt32.max) + XCTAssertEqual(frame.payload.count, 6) + XCTAssertFalse(frame.bt2020) + XCTAssertFalse(frame.fullRange) // range bit 1 = LIMITED + } + + func testHalfOrFewerBlocksIsDropped() { + var au = sof(totalBlocks: 4) + au += packet(blockIndex: 0) + au += packet(blockIndex: 1) + // 2 of 4 decoded = exactly half — upstream requires MORE than half. + XCTAssertNil(WaveletBitstream.parse(au: Data(au), chunkAligned: false, windowSize: 0)) + } + + func testMissingSOFIsDropped() { + let au = packet(blockIndex: 0) + packet(blockIndex: 1) + XCTAssertNil(WaveletBitstream.parse(au: Data(au), chunkAligned: false, windowSize: 0)) + } + + func testTruncatedPacketIsRejected() { + var au = sof(totalBlocks: 1) + // Claims 4 payload words but only the 8-byte header follows. + let word0 = UInt32(0) | (4 << 16) | (1 << 28) + au += le32(word0) + le32(0) + XCTAssertNil(WaveletBitstream.parse(au: Data(au), chunkAligned: false, windowSize: 0)) + } + + func testWindowWalkPackedFragAndMissingShard() throws { + let size = 64 + // Window 1: SOF + one packet, PACKED. Window 2: a FRAG chain carrying one packet split + // across two windows. Window 3: all zeros (a lost shard of a partial frame). Window 4: + // a PACKED packet — the chain break must not eat it. + let fragPacket = packet(blockIndex: 2) + var au = window(sof(totalBlocks: 3) + packet(blockIndex: 0), kind: 0, size: size) + au += window(Array(fragPacket[0..<5]), kind: 1, size: size) + au += window(Array(fragPacket[5...]), kind: 3, size: size) + au += [UInt8](repeating: 0, count: size) // missing shard + au += window(packet(blockIndex: 1), kind: 0, size: size) + let frame = try XCTUnwrap( + WaveletBitstream.parse(au: Data(au), chunkAligned: true, windowSize: size)) + XCTAssertEqual(frame.decodedBlocks, 3) + XCTAssertEqual(frame.offsets[0], 0) + XCTAssertEqual(frame.offsets[2], 2) + XCTAssertEqual(frame.offsets[1], 4) + } + + func testBrokenFragChainIsDiscarded() throws { + let size = 64 + let fragPacket = packet(blockIndex: 2) + var au = window(sof(totalBlocks: 1) + packet(blockIndex: 0), kind: 0, size: size) + au += window(Array(fragPacket[0..<5]), kind: 1, size: size) + au += [UInt8](repeating: 0, count: size) // the chain's middle shard was lost + au += window(Array(fragPacket[5...]), kind: 3, size: size) // dangling LAST — dropped + let frame = try XCTUnwrap( + WaveletBitstream.parse(au: Data(au), chunkAligned: true, windowSize: size)) + XCTAssertEqual(frame.decodedBlocks, 1) + XCTAssertEqual(frame.offsets[2], UInt32.max) + } +} + +/// Golden-frame decode against the committed host-encoder fixtures. Skipped when the machine +/// has no Metal device (headless CI) — everywhere else this is the hand-ported kernels' guard. +final class PyroWaveGoldenTests: XCTestCase { + private static let fixtureDir = "PyroWaveFixtures" + + private func fixture(_ name: String) throws -> Data { + let url = try XCTUnwrap( + Bundle.module.url( + forResource: name, withExtension: "bin", subdirectory: Self.fixtureDir), + "missing fixture \(name).bin — regenerate with pyrowave_dump_golden") + return try Data(contentsOf: url) + } + + /// Completion box — the decode callback lands on a Metal thread. + private final class ResultBox: @unchecked Sendable { + let lock = NSLock() + var planes: WaveletPlanes? + } + + /// Decode `au` synchronously and read all three planes back to CPU bytes. + private func decode( + au: Data, chunkAligned: Bool, windowSize: Int + ) throws -> (y: [UInt8], cb: [UInt8], cr: [UInt8]) { + let device = try XCTUnwrap(MTLCreateSystemDefaultDevice()) + let queue = try XCTUnwrap(device.makeCommandQueue()) + let decoder = try XCTUnwrap(MetalWaveletDecoder(device: device, queue: queue)) + let done = expectation(description: "decode completes") + let box = ResultBox() + let submitted = decoder.decode( + au: au, chunkAligned: chunkAligned, windowSize: windowSize + ) { planes in + box.lock.lock() + box.planes = planes + box.lock.unlock() + done.fulfill() + } + XCTAssertTrue(submitted, "the fixture AU must parse") + wait(for: [done], timeout: 10) + box.lock.lock() + let result = box.planes + box.lock.unlock() + let planes = try XCTUnwrap(result, "the GPU pass must complete without error") + return ( + try readback(planes.y, device: device, queue: queue), + try readback(planes.cb, device: device, queue: queue), + try readback(planes.cr, device: device, queue: queue) + ) + } + + private func readback( + _ texture: MTLTexture, device: MTLDevice, queue: MTLCommandQueue + ) throws -> [UInt8] { + let bytesPerRow = texture.width + let length = bytesPerRow * texture.height + let buffer = try XCTUnwrap(device.makeBuffer(length: length, options: .storageModeShared)) + let cmd = try XCTUnwrap(queue.makeCommandBuffer()) + let blit = try XCTUnwrap(cmd.makeBlitCommandEncoder()) + blit.copy( + from: texture, sourceSlice: 0, sourceLevel: 0, + sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), + sourceSize: MTLSize(width: texture.width, height: texture.height, depth: 1), + to: buffer, destinationOffset: 0, destinationBytesPerRow: bytesPerRow, + destinationBytesPerImage: length) + blit.endEncoding() + cmd.commit() + cmd.waitUntilCompleted() + return [UInt8](UnsafeRawBufferPointer(start: buffer.contents(), count: length)) + } + + private func psnr(_ a: [UInt8], _ b: [UInt8]) -> Double { + precondition(a.count == b.count) + var sse = 0.0 + for i in 0.. half) and stay recognizably the same picture (holes reconstruct as + /// localized blur, not garbage). + func testPartialFrameStillDecodes() throws { + try XCTSkipIf(!MetalWaveletDecoder.supported, "no capable Metal device") + var au = try fixture("au-chunked") + let windows = au.count / 1408 + try XCTSkipIf(windows < 3, "fixture too small to punch a hole in") + let hole = (windows / 2) * 1408 + au.replaceSubrange(hole..<(hole + 1408), with: [UInt8](repeating: 0, count: 1408)) + let decoded = try decode(au: au, chunkAligned: true, windowSize: 1408) + let ref = try fixture("ref-chunked-y") + let db = psnr(decoded.y, [UInt8](ref)) + XCTAssertGreaterThan(db, 25.0, "lossy frame should still resemble the source (\(db) dB)") + } +} +#endif diff --git a/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/au-chunked.bin b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/au-chunked.bin new file mode 100644 index 0000000000000000000000000000000000000000..ccb33fe5433855a3bc38e87d4012a42a82211a90 GIT binary patch literal 61952 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 z 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≶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≦{v>V{lHQWY!Al9g}S0<(k%g$EfgFv?Gs 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 literal 0 HcmV?d00001 diff --git a/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-cb.bin b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-cb.bin new file mode 100644 index 00000000..00f0e441 --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-cb.bin @@ -0,0 +1 @@ +ssxvvxy{˼vvrtxuzx}ȼrrstuxtz~ʼrsuqtuww|~~~Żqorsqsutz{~źoprqrqrq{{}~|}|ønllpoprpxzxz{|}|ójmnmmnonyz{{z{~±Ͼÿ}}~{|}qtrtuuuvy~}Ʒz{{{|znqrqtpqqzy~}ǵwzzy{z||lpoopqtq|}{z|~ĵпyvzyvz|zkllpopsqyvx||}z}þĶϿtuvyyzyzjlnlomoo}~vwy|yxzöͼrvrxyxvxkliklnmsyxtxxz{{¾˽psrswsx{ijjhllmm|uuvuvv{y˻qosqswutehijgikl|~~sttwuyvwɹ`aaaccdguvwuxwy~kklnprpq~vyyx|zz}ó^]c_b_acvwvxvvyxhjmolmpqxxzwxx{{]]\_`^`ctxuvwuwxhlkkmmkl~~uxtzxyywñ\[]]^ab_qsuttxutihiikjjnssuuvvyy~Y[\\[^^]nrtsstuwcigfkjmm}~~sqqtwtrt~[Z[Z^[`\oqsrspsuedfefhhi{|{{~~pstputsv}~z~WW[VXY]]qqnrppqwddgghgjkzzz}~~}nqprrstu{{~~VYWYXZ[\nklqpqppcaeeceggyxzzzzy|mooooopry{{}~efhghlkh[Z[\[__]rrqtttuuiifihmjj{~}~}rvuvwwwweeegehjjY[X\__\bnnrqtrtsbhghgjik|}|}rsvvrtuu|}~aedcddhfVX\Z[\\\ospqrtttcdeegfki{||{}}ooqpsrsw||~~_bbbffehVWXXZ^Z]nmlmnopqcabgfghky{}z~}}}oorsqrsu~{|{~~^b_`accgVVVZV][Zjpmooqmq``dccfggzxwzzzx~lpponrqty|}zz~~_a`ba`bdUSTXV[UZhlomnlmna`b`bedgvuzzwz~{lkqknpprwwwx}{|~\\^_accbRSSVUVV[igjkkmnp^_a`b`fcvxy{xy{{ikmnmppqwywyz{}}[[]\^a_`RTSRSUWVdhimnkkj``a__``drtvuvx{{lknmmkonuyyyxzxyHKKKMLNO^_dcaeffWXWWWYY[kkllompqaecfdefgwv}x{|}}mmrrsprqw~x|}}HKLLLKOOb_`aecbbSUVVY]ZZknjlnnpr_b`cddcdwvwy}{~~mlppppptww||x}|}FIHLILJN_^`_^bccSRUTWVW[jiinnnmp```dcacbuvvxzyz|mjlkoqpsvzw||z}EHIJGKIL]]^``_`aNQSUXSXWhjkjmmnn___`adbcxuvxuw{zjmllooloyvxvwx{{GFIGIELH[Z]]^_^`TPRRRUTXehhjjmkl]\a]_`bbsrsvxvwxhjklnnmotvyuwx{yDBGDEEHI[\Z^[[\\OLQRRUQVdghiikkk[^^]]]`_sqssxwwxfhjklhnl}tuswwwzzFAECFHIFU[Z[^Z^`OPOQPTRVgffiigijXZ[_Z\\cqtsqttvvehifhimm}|~rsuttuwx?@BDDEDGV[YZX]]ZLNNPOQQSdedfhejfX[[\\Z^_oonspussdgfhilgjy}~~nqrsvrvw}~QOQPUSSWEHFFIFJLZZ]a`]_`RTTRWUWXgkijlkoi___b__aasuvwvszyhklmmmnq}vuvw{w||PORPRRVUBDDFIHGKZZ\\]Z`bRQPTQWUVgffijillX]]^`^carruwuyzugklklmkl~vsywuwzyMMNQQSQUBHEGEHFHZZ[[_[_\OQQTSRTPddfhhfhkY_^]`^`atssuuvuxfhhlikkk}}swsvvvwyLJJPNOOS@BEBCEFEVYZ[[^Z[KNQPOPTSffeegifgZZ\Z^]_^mtqusqrtfehfgjlh||{}}rvvuvwvt~~}MIMLLNMQB>@BFDFDYVVZZZX`KKNPORPOccdefejgX\^\[]\\nnnqqssxbdgffghg{z}|}{pputuuvvLKINHMNO??>>?DDEXUUXXW[[KLLMNPORcabeegfeVWWXZZ]^mlpopotudbggeffhxy|y~{npnssssrz|{}~~~GJJKNMNM==>A??BBRTWXXXXZIJJLKNPP_bdacccdUVVVZWZYlmoqppstbcceecdgvxyw|z{~nnoppsqr{xz}~~|EGIJJJLN;:A@?@?DQUVSWUWXIJIIJMOO^ab`adcdVTUXXZXXmljlmproaabbcdcdvyyx{y|zplmnklprwxyz|~|65577;99NLPNMPPP@A@EFFFHVVXYW[[^LOMOOQPTbeeegegiZZYY[_^_lnprrqsufegehlkiz|}|~rtsstuus|}65668878KILKNMNQB?AAECAHZUYYX\W[KLMOQQPO`cedefdeTW[ZZ[][pnmtqotrdgbhfiifzyz}~~mqrrsrru|}~~34337449IGMJLKNN>?>B@ACETTYUVXVZJKNLKNOO`abcccfeXYWWY[Z\klnoqnrtb`cbffckxyy{|~|qpprntss}z}}}|14145545FHHHJJLJ=?=@@A@@SSUUWWVWEKJJNNMO_a_acaedUUWVVYZ\knmloqop`_eedcghzy}w|zyllonqsprzx{{{~|.04.4423EFHHIHHL<<<>>B>?QRSUTTUWGKJILJMJ]___`_cdUUXTUXXXkimlmonq_^a_dccdwuuyuyzzkompporoyyy}|x}-.112023FDHDFHJL;9;>=?@AQOSRUVWTFFHKGHIK\_]__^c`STQURTVVeikjlokl[`^^adadutvvyxxymklmmnns}xwz{y|y|~/--010/1GCCDEGFH98==;<==PQPQRUQUDEEEHGJK[]^]\_`_QQPQTTTVgifhljim]_^]^ac^utrvuxwxkjiimmmm|wvtvzyvz,--+1021CB@DCGIG5779:<>?NNQQQPQSCEHFEJEHZ\Y^]]_`LQNSOTSUdfghjjjm\\_]]]castuvtstxfffklikpz}uswvuyvy~:=?:===?02015755IFIJFJKN;;>@>?@DSPTTUUXZIJJHHNOM^^aab`dcTWVWXWVYlkjnonpnb_`dfefewxvx{x|zlkopprrqzyw{|~}7;7:=;??1/143345EEFHJJHI:<>=?A@@SQTSSRWVFIFGIGML]^[a`b^cSVUVTYYUikkmlnno^abe`ed`vvvwywzzlkkknmnpvx}z{|y89=9=<<>0..01521FCEBFJIJ:9<=<>>>MPOTQTRSHIGFJIJK]^```\`aNQTTTTWWfgjfkjoo^`]aaadbtwvuwywyhlmklmpptyyzzx{~686;::<;--,/0023AABEEGIJ;8;:<9?>ONNORTUWEFDJJIHJZ\[]^_`bPRSRSVVVgiiijgml]^``_ba_ttvuvxyxgglnjnmo~vxuwyvzy747788<LLJNKOOPEECGGGGJYXZWX\[[NNRUTTTWdeebggeh[\\]a``bnpqopqusilhkmnpo|z{~|~}uuwv|{{y02265356**,0-022>=@C>ABB78:;=;=?GIKMLOONCDGHIHKIVRUXZYV[PPSQUSTUedcbeeef_babc`dbmopppsptjnllllkmwz{|{|}vwwuvuzy''(')*+)5769:::=23336657@ADCDDHHA>CC@BMMOQPRMSGJMMONPOV\Y]Z\]]UYXUX]X\fdffgiijcaeegehgqqrtsruwpqqqotss|}{{|}||~&(&(+*,+346897882256554:@>B@@BDG=?<@DACDMLLPPRPOMIMLKKPPTV]XZ[][XWXV\W[\becgeijkddddeeejmosrssttnsoposvqzz}|~z}{}}~)(&*()*+332575660/563575;=?BA@CB:=?=BAACJJJNMMOPJKMIOOMLSVWX]XY^VXYZ[[Y^baeceegedcbddgffopmqqrprpnrrqrstz}}{||{}{|}~}%&((((()12233556/4454787=@>=>BAA?BGIKMMMJOIKLKLLNLTRUXVXXXUVYXYX[[`ac`aaddbbgeggeioolpoqtsonosssoryz|z~~}{zz{{~~(('')(++-3012247024317:6;8>?@CCDCEFIJINJLMLMLMQLORUWWUXXZYWXYWWZZ]`_`debd_`dfdfgjlklomqnokpqpqottvwyy{{{~x~}||~}&))()((,*1/220231136665:99><<=A?A@BBCHEHHHIJKKLJNMOKORSTRUSYWTVZWX[\Z\\\`cb`bcbbffffigjmkmormlmposrqswwxzyyz}zy{~}~}}")&'*++,0-,./243//446646897>===??=A@BCCFEEIGGIHHHKMLLMLPPQQRUTUUVWVZ[Z\Z]Z\`__acbb`ccfhghilmkjplopqpopqttvvuzx{yzx}}~~~ \ No newline at end of file diff --git a/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-cr.bin b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-cr.bin new file mode 100644 index 00000000..51be3940 --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-cr.bin @@ -0,0 +1 @@ +qqvssuvw{{|}~|{uuprvrwuzywy{|qqqrsvrxy{{{z|}|}rsupstvuwy|y{x}{z~qorsprtrvw{y}||y}~pqsqrqqpwwyzwx{v|~¼ommpoorouwtuwxxw|||koonmnonwwxxvwz{z}qtqsutsttyxyzy}y~~~~nsrqtooovtz}zzwz}~~}}~~mqpppqtpyywvxz{z|}||~z~lmlqoptpvrtyxyux||}~{|}~~lnomompo~ttvyvsv|}|~|}x~x~{}mnjlmontxvqvuwww}y~||}}x{yy~y~lmljmnnntstrsrxv|}|y}{y~yv{xz~{zhkllhkmnrssuswtt|y{||z}}egffighk||}{}|~mmmoqrpqsvvtxuvx}|}dbjehdeg}|~|{~~jlnqmnqrvuxtutxwzy~||{dcceeceh||}~{}~jommonlltvrxvvvs}z||}{~cbccdghdy{}{z{ymjkkmlkorqssstvvxy|~{}}|acdcadecw{|{{{{fljhnloorppruqoqy|y|{~z|dbaafbgbxy|z{wz|igihhjjk~pssntrqsxyt{yxwz~_`c]``dd{{w{xxxiikkkjmn~npprqrrswv|y|~x}_b`aabcdxuuzyyxxhfjjghji~}~~}nooonnoquwvy{y|y~|qstsswvrcaabaedb{yx{{{|{lmikiolkquttuuuty{{~}{}~~rrrtqsuvac_cffbiwvzy}y|zflkkilkmqsuuprrrvxxzzy|znsqoppuq^`dbbdcby}yyz||{ghihjinlooporpquxzwxx{z|~looossqu_`a`bfbdxvuvvwxyhdfkijkn~~ooqsppqt{vwuyx{z~lpmmoppt``^c^fcbt{wxyzuyeehhgjkk}||lqpnnrpsuxyuuz{x}npopomoq_]]b^d]brwywwtvwgegegjgk}{|mkrknopqtsrtyvwx|~}~kkmnpqqp\]\`___dtruuuwxzdegdgekh~}~jmnonqqqtvtvvwyx|}||}{kkmjlpmn]_]\]_`_ostyyuusgfgedfehy{}{|~omponlposwwvuwtuz{y}||}~UYXWZXYZlmrqnrsraa`__a`cututxuxyfjhkhiij|{|mmrrroposzswxw{|}~UYYZYW[\qnnnsqoo\^__bfcbvxsuwwyzdhdhhggg}{}~nmqqppotsrxxsyww~~SWUZVYW[omonkppq][^]`^`dussxxwvyffeihege{||}~oklkoqpsswsyxu{x|~{~TWXYUYVYmlmonmnoX[]_b\a`suuuwwwwfeddfifg|}{}lpnmpplowsusstww{|}z}~VUXUXS[Vljllnnmo`[]\\^]aptsuuxuvdbgcdfhgzyz}|}~klmnoonprtwrtuxuz~z}~TRWSSSVWlmjnkjkkZW\]\_[_pssutvvubfeccaee{x{z~~ijmmnjplssqvttww||{~}{}~WQVSVWXTflklnjnp[\[\[^]`trsuurtu_abf_bbiz||x|{}}iklijkpnqrtrrruvz{yy~{}zOQRTSUSVgmjkinmjY[Z[Z\\^qqqrtquqadbdcadexxv{x}zzhjjlloil}mqqruottyy{{z~zyececifeiTWTTWSXZjimpokmn]_^\a^abswtuwuysfeehddeez|}}|yjmnnnnnqtsrsxsxyx|{{||dcfcfejhQSRTXVUYjjlklioq^\[_Za__srqtutvw^dcdfchfzy|~{zinonnnlluqwurswu{~~z|~|bbcfegeiRXUWTVTVkjlkokok[\]_^]^Yqprttqsv`gedgdfg}{{}|}{~ikkokllmrvrttstv{yz||~ya_^ebccgPRURSUUSgkkkknijWZ]\ZZ_]ssqqstqrbbdaddedv|y}{xy{ihliiloi~quustusqyywy{zy|~c^ba`caeSOPRVTUSkhgkkjhpXXZ\[^[Yppqqsqvsaefcbdcbwwwzy{{ehkiijki~pousttttz{{{y|~y~cb^d]ccdPQONOTTUkggiigklXXYYZ\Z^qopsrtsq_`_`baeewuyxxw}}ifkkhiik}~~npmsrrrpvxvzxyxw}}}^a`bdcdbOOORPOSRegjkjjikWWWYXZ]\mqsnqpoq^`__b_b`vwx{zx|}ghgjifgj{}~|~oooporpqxsvyyyw{z|~}~\^aa``bdNLSRPQOUdiiejghiWYWVWZ\\mppmoqqq`]^aac``xvsvvz{xgfggghgh|}~~rmmnkloqttuuxzwz||}~~}|KJJLJOMLdaecadedQQOUVUVWggijglknX[YZ[\[_osrrsqrtbb``bfeeuvyzyxz|jijgkonk~rtrqssrqvwz{}zyz}LKKJLMJLb_bacbcfTORQUSQXlfkjimgkWYY[]][Zoprprrpp\`cbbcdbzvu}yv|yhkelimlh}~mqrqrpptxxzz{xyy~IJHHMHHM`]d`baccPQOTPQSUgfkggigkXX[YWZZZnooppprqab__acbctvwxzvz|fdgfjifn~~}rpprlsrryuyxxvy{~}GKGJKKIJ]___a`b`PQNRRRPOfeggihghRXWW[ZY[momnqnrp_^a^^acdvxvuxzxxedjihgjk|~}mlpnqsoqwtwvwyzv~}}}}DGLCKJGH]^__`^]bOONPOSOPeefggfghUZXWYWYVlnnmnmqq__b\]```uswuwywzdcgciggh}{{z~~~lqnqpornwvvyxsyz}~~}}~CEHHIFHH^\`[]_abNLNPOPQSebgeiijfUUVZUVVXlnlnnkqm^^[_[]_^ptvuwytuafccfiei|{||}}}olmomootvtwxuxuww}}z~|HDDHHFEG`\[\]_\_MLQPNOOOeecdehdhSTSSWTXXkmmljmom\\ZZ^]]_rupswurvcfdccfhb}{y}{|}nmkjnnnmutqswurwzy|{~~}DFEBIGIG][X\Z_a_IKJLLOQRcceeecdfRUXUTYSVkminmlnoW]Y^Y^\^prstuutwcbecccjf{||~zyz~hhgmmjlr|~tqussvsux}}yy}||VXZUXWVYGIFGKMKJa]`a]``cMMPROPQTfbffggikXWWUU[[Ymlpoomrp^a``a`^awusxywywgdeikjjh}~|}}~mkoqprrpwurwxzxz{~~~~SWSVXVZZIFHKJIJJ]\]_aa^_MNPOQRRQhegeedihTWSTWTZXlmjpnpkp^`^`]ca]tuuxvxwxcggjejhd}|||~|nllknmnosuzwx{xt|z~{UVYTYWWYHEDGHLHG^[]Y^b``NLOONPPP`dbhcgdeWXVTXVWXmmoonjnoX[^^^]a_rrupusyxefbffeif{~}|}|~inollmqpqwwwwtw{|y~{z{}SUSXVVXVFECGHGIJZYZ]]_`bOLOMNKQPdabbfghjUUSZYWVXjlkmnnnq\]]]]``_suuruqxvddggdhfd{{}|}~~jinplonpuvstvswvz{~}}|{UQTSTUXYHGIIHJIIXZZ\\Z[\POPOMOPObcfcbfecSWWWXXZVijhlkmkqZ__b`b`bqoqtstwwbdbehggfyy{wzz{|jmnmoppm|usxuztwt{~||{~|}SRTPTWRVEHGIJKIHYZZXZ]]]OONNTQSPbbeaebdfUXUUYVZ\kgljjilm]]a_a_^aoorsroqufcgghflivu{z|v{|llnlnmnqsvuyyzwu|{}}~~SRSUTTRVFEIHKJKLX[Z\W[]^QPNOOPORbb_c`ccdVVTXWXWZkikghljk[[_b``_cqrrntspscddeiggiwyywxy|znqloqqtrvuwu|{zw|~}~}OQQTSQRTEDFJGJKLYWZ]W[[ZNNPQSPRT]`abaedcUVYY[Y\Zhdgikjfk^^a^b_`asrqorrqqiljklhmjvxyxy{w|ptqpqpnq|wxxuvtzx}}~}~DFEDEFGERTRUWVVYKLLLONLOXZ\[\[`_QVUXUWRWecfdffgfY[\^^```iilnjlkmaddeeekepvrsvsttijlllomn{zx{{z|tvtssyvyxx{{{z~{EFIGHHIHUSPSRSSVNINPOLQPY]X[_\[]PSWSXXUVbadedg`gX[^_`^`_gmimjlllbfeadidhsprqstsuljnnolonzyz|zy|~uvvurwvu{{|}~z{}EGEFJHJIQRTVWTUUMMOQNNMT[W\XX[]_SUQVZVXYcabeehdc`Z_^\\a`egnijkljgeecjcghorotqtvwnnnmnmmrvw|z{{{{sxtuswzt~{~{|}}~IGFJFGHIRRPTVSSSLJQQMORNUWY\ZY\[QTVSXWWY`__cbbcd]^_[aa_^eghinhineggiiifkpnrpqqspomlnmpnnxzuzyzwzvtxwvwxy||}~~|FFIHGGFGQRQQRSSSKPOPORSQX\XV[][]VTUZWWTX^_accc^c]^_^_^`]fcfjgjhideiggfihnnqmmmppmmroqpnryxuywz}{vtuyyytw~~|{|{}KIGHIHJKMSOPQPRVMOQOMSVQWSYZVYZ]VUVX[Z[Z[]``_d`ab`a_ae_behiifijlighifehhkolmqrnpjkpqoqrtvuvyuzwvrxxvwuzz{}~~z}}~HKKIJIHLJQORQOPRNNPTSRQWUTZWWVVZWZWZXZZZ`]_^__```a_bbc^bfggdgdkhefjggjkikjjnqploonmrqqqtqtxtwx|uttxvzywy~}~~~~}|}~~DKHHLMMMQNLNNRTRMMRQSSPSTVSZXXXYYV[Z\\\^]]a^]`^^]`baab`edddehghghhgklkmilhjommoqoolpnqsrrtvxuszuxxzxwxx{{}|z}}}{ \ No newline at end of file diff --git a/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-y.bin b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-y.bin new file mode 100644 index 00000000..a8305210 --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-chunked-y.bin @@ -0,0 +1 @@ +//0.35/400512245:36945;57;;<;69:"9=;<;@C; !%!!'"%& %#!#'$FCFADC@EBBDFDEHF(%***&((&%&&*-*)KHKKHFJIHGHJJJLL+-.//001-,100.1-QKPRQLRRMNNSMPMQ6406008524125234RQUUTRSVYWSVXSRR::9:<=;9;<<8;?89[\\XV[^^^ZYZ[][[>@?=@@>B<@B>B>CA5/02612344704882:794;<<9;:<8:767 !! !?@?B;=<@<;?@?B=CCC=@BC@>?A6457218174333993<79<96>77>;=? " " !#$$$?=?AB=BB>C?DAC@D$#"$$#%%(%$$')$(FBHBGFGDJDFHDGID&+,'-*(+,--/-).0LNLLHLHNJOLKOMKJ/,123-232/./.441SMORTTPPQUORSRUT68277794:556:845YVVSSXYTXVWZW[XY>::;<9:<>?::@=@>\]\^Z\Z\]_`][\]_@?A@@@CCAB?B?D@D7575533586397:86>77:9899:;8<>@>> #! #%$!"&%?A>AAD@E@D@@@CDA)"$'&&%'(**)$(%'GDCCJHEDFDGFJKHF*.(+++-*/.,+/-,0MLJKIIIPNPMQQNKK0././52353313426PNUUQTTRPTVOUTUW7:::646;95759<:8UXUTXUWV\ZXXVYYX>9@=@A<;>=>?A;B<\Y[^Y_Z^[]a\]b__D@?CBA@BFAF@C@BB74954857;69;668<  !><=<<<9>:=?<@A=A!#!! % &!$&$>>?C?D?@ABDCF@@H$%'(('$$*'',''(*FFHIHKJJIHEIGMMJ///-+*+++22/+//0LPJKQMOMLNNKQOQL6.51464775344515TRUOSRSUTPXWSURX867;8<68:7:98=;8V\WWXV[]VY]]ZWW];;@>@?;@@@B<=>@@?<@@?@;>>!&#$#"&#"'!$%'#ABB?BCBDGAFFCFFH*'+)''%+&'',&*')ILGEHJHELLKKIHKM+*-/1++0,-/+/02/ONMNPLKMRKRQNQPT62/1251753237525SUPWPRUWRSVXSYVW86<<:6<:67;8<8;=[\\\XWYY\WX[^_[^BB;>B?@A>>C@@ba\_c\b_]^bc_bbcE@DFABDFFFFEHEFE6;9:<=89<9=<<<;;!!! " $=>>=@AA=AC@=?B=>!' %!('#%&"(#%%@ACFAEECAFICFCIC+(&)-.*&))+,*..-HKMHILKHMGMLOKMJ/+-0..22,-01110.NRPLMLQQQORUMRRU2773677568:456:9VUTVXYVSYTTVVUVW78;<<<;;<<;>=:=9[]Z]__[\]Z^[\`[_<>=?=@A@AC?@@>@?_^bab^_aeb`_de`cGBBGDDIFFGDEGIGF:;<9:=7;7>7<:?== !!!!# #$"@<>C@C>A?@@>CCCC&&%%"%##"*%(##))AAEHBFJDFFGJDJKH-*(*.-(-+).,0)..IHMKIKMJJKKLPPNI/022/41005.01335NQTTNQSQUSSVVPQO68858879::6997:8RUYUYSZXXUTWZTZV99>>=<=;9@=A@@:<]^ZY\`\]`^`\a^^`>B@?>EEC@CEB@A@@d^cdd``ebfdaffb`EEEJFEFFIGGGFKIJ>=:==^`^`]_^][_a_\^``FBFAEC@@ADECCAHCaf`f`ggfggbdfaadFFHFFIIHGJKKJGJK:<;;:=;?;:@?A;;B! $ $# &&%!&@A@@ADF@DEEDDEA@)(*)%&$++*&')(&)JIEGDJHEFFLLFIMM-,---.0/10-,,1.1JKOOOONMQLQNKNSO.501012553340116URPRTRVQPSSSVWSW9;;;69;<7=:8<7<;XXYYV[UXW]Z^ZZZ^>>=;B:A@=@B>A@ABC@>a_\^cc_cba`^bab_EHGBDADDBGHIGGGIchbeifcfcihdiediGKHJJKJJNLJLKIJIB=A?B?@C@B@=>BA?&!"$"#!(&#(("'(&GFAGFHEDBDBFHEDC'(,,+++.,())*/*)KGFJGKJNNOLKMIJL/,/013300440./21QSLOQQQMOSSTONPQ755735458:55:798RVXSYUWYYXXTY[ZZ=7798<>>:@<<=9>:[\]Z]_]][X_ZZ_^]BC>@>=ECBBD@ADD?adcd`adccedcd_g`HHGJCDCEGJHGEGKFfddjegefedgjehekMIIKLLMLOMJKKPNMC@A==>?D>C@@D@BA!##"$)"$$%&*%+%(GCHICCDICDCHFKFI.,()(()//()*,0+/HIILMNLJOOMPPPLN/1.2.01111051204TNNNNSQSQUQRVVVQ853946:39:;4<9:8SWXUWU[WTX\YZV[X=;?<>;C>ACFFBEFCFAAc^babfcd_``ee`ecDFDEHIKIIHEGIFJFjjejeghlklikgffgPOLONPJPMMLMLMQP>C@A@CCAABDCGABB%&*#(''$$)%*+&,,HEFEDIFFKILHHGGG**.)./+-1.1++.2+MNKLMLMKQLQRMLLO/33.635106244578QROVQVPSWUTRXTQV5448959:5667:87=VWW\XUZ\[]VV\XYZ<@>;@=AA>>?@>@C@]\aZ`b\a_b]^\__^BDBADADBADHFFGHEdaad`aagcfgcdifhGGJKLMFGHLJJIHGKfgglmjmjlihhlmjkOQLOPPLQLNLNLPOR?B@D@B?A@C@FDCDB(&)'*)(),)')*,(+IIEKGHEKIHFGMKIJ000,,,,10,+-/+0-JJOJLPPQRRPPLRMN4606661028456684TRRQPVVVUUTUXTSW<4:98988::7698:<=C=@D@?Bc\a]]_^a_^__edacHCGIHGECDDJIKGGJadcgdgfjfhiffddiMLNLNLOOLIIIINMLhmhilmjhmniipmjpMMQSPNORQONSSPOQ)**)(+%)($,,((**IIJIGIGLLLHKHGLH.+.---*.,,..2-.-MKMOJLLMPLNLOQLQ/42/646413211143TPPQSPSTQRSSSWVV66<5:<::<8797:<8XVWX[\U]X[W]YW\W=A:??A<@B?>A>CB>_]]\`_^_c]`ba__dCFGGCDHDBGCBIGDCdecbbfhfghdcgdccEHEKJMGGMGLHIIHIjfhmifhgljhlkihoNKOPROROMRRRORONpmqoqqrprnrqnqrs*)&+++*'**&-)().HGKKHKILKINIMNLJ./2/11203-.--122JKNQQQSOPRMSQTQO2235412713693968URPWWSXTUUVYTSXX786;798978;9?=>=]Z]YW^ZXY[\_XXZ^;=C==CC>CCCE]]c]]]`b]bc_adcbCCDCHGBDCDFCCHDFhdgceejgecdkjdejILIMLILLLIINJNPKkhnnmljommjonlnkTPNQQNTSPPQNQPUSsnsronppssrqsqor,&-,-&(*+,*).*+,FLMKJKLKKMIHMJKH./1,1/1.13144020LLMPRORQOQQNQQOT5883263826887494UVTSVXXUVWTSVUWU89;>9=8<<<@<8<@9][[^^^Z]\^Y`Z]`ZB=AC==DA?ABE?AEAccdc^d__`edd`ac_DIFCFFHICJGDEFKEgbbhffjejgiefhgeMHIOJOJMKMMJOPNQljhmoojlklpnmpnnQPSNQUNOTPPOPSSTrqnrrpnturspqtur'-*(.++,(/*)0,,.LGMILNJJMNMIIIKL/0/0.42..345//00QSNNTRUUUQTTOORR24522:334:4758::URWVVVTXTTWTY\W\:>8?:=>==@@?;>?=X][[YYY\Z_[a`[a]A@>C@BBFD@@@CADEadc`c`ebcbgdfcbgDFHIJFGGIIHHEGEKieifkhkhejfkkfjkPPILMMOLKQLOLNOMjljjolmplnllprrpNSPRSSRVSVUVTRRQrtsstpqqrurvutvu*-0,,,.-01*,,+.+HJMMMPLJMPLLNPNN/5.145030/003362PUSSVOORPVPWQWTQ43:498575<;5<8:9Z[YWWXYV\YZ[Z[XX::?==?;<=@>>=<>B]\_^\[a``\ba`]`^F?EECEDEDDFGFGHIceddbef`egffggihKGEELMGMKKKFKGMHlimjkfghkgmmhkihJPLRQOQOPOOLSSMNnomorpnronrpmoosQTPSSTRWQXXSRXVSssvrtqwuvsvuvsst)/0+./+-+-2,21,2QKOPLNOQKNPQRRPM4400365466757767VVVVVSQXVSXUVXXX<9;7==<<:78=9;<:[\VWV[XVVZ^XY_^X?;@B<=AA=B<====?b^ba_`\_^abc`a__G@HAEEEGDGFDDGEDaahgaiddcgdbdicdFGMHJNLKHMKOKJMKgkhmlljolkjljiojONLSNSOPRNRRTTSUlqlspomrsprrssuoXXUSXUYSVYVWXTXYssswuwyxxvuvyutv,1.,10+,,..//4./LMMKMKRRMNLPSTQS4775033153458947SSRQQQVXVUVXSUVZ799<<<9:;7<<<:;=WZWWZ[ZZW\_\Z[[]?>;8;?:<>:=@;[YY_[]\Y^[___^Z_ACB@?BE@BAB?EEAFdb`]]__b_cda`efaBJDIHHGHHJGKHIJLigcejcgkgjfdfgkeJNMIIOLJJLKJNJNQkopljlkopqljpppoQRRUSVOSTSVQVRTVtsptqpssuuwuqqrqTYXXUUW[[ZV\YYYYyuuuyww{yxyyvvww12-12-4322412306TSRRURRROUNRTSQV24963684;:;8:79;TXWYXXVZUZVXYYZ\:?;;9>;?;=::;?@=__]`]]`]Z\a[\[]_CD?BBC@CFA@DDGDA_e_afac``bdgf`dcIDKGFFEHJJFGKLHJiihjikihjggljfgkJPJJQJLLNQPNOQMLjkqqnoojpmorrqqmRROPQWPWRSUTTQVRsurqttptqrxwwuuyUX\WYWZYZ[VXY\]Zuyx|yzy{|x{wy|}}/125431545375042ONUOQTRQTURVWQTS54:;44::759<7<=8T[UVXY\\ZWVVXX\^>=;<;:=>?B?@<=><>=:>9?9@=;??9:?>Z]ZZX]Y^_ZYY_]^\?@==E>@CEE?EFEEDb^ee_a`abdac``dcJEFCIJHGHJEJEKGFcihhggkffekeligfNILOPINQNKNQOLKQoijmmjpolmkkqkqnPQPTRSOSSRSWWVTXurqvsrotvrpsvvsqWUXUY\XX\[VWVZ\Xtyw{|zxxxv|zv{yx[]Y[^`a\^[\a^\_b~}~{{|~|}}353756477;88;5;;YZUWWWVT\WZYUYX\99@@>;A@?;[`[aa]\]Z_a^ab^`A?EDEADDFFECCEBEefbafebbfa`fbcbgGKIDJKGIIFGGJMKLhfffijlfljhmmihmOLKPKPRPOMMKRSNMkmkmqonlrrponmpoPOVSWPQVWXTWSUUUtwswwvtuvxvywrwyYVX[\X]W[Y\\Y[]Yv|x}{||{x~y}yyyy\`aa`^b__`^]]c`d~}}}}7477<996<6<=797;UV[YWY\YXVXZYWXZA>@=>>?A@==C>@@?`\a\b\]ab]cb_`^bBDFBECCBHHGDBGGEfghafccbfchdceceLFGHHKMNMLGOIKOOgfiilhnfmgkjikooOQLOPLSPSQMOMMROmmnqosnrpssqpopoXUXVSXUWXVXVTVUUusttwuvwsuyywzyzY\\Y[]YXZ___Y`\]}~{{~y~z|y{\^]bbda`adad``d`~9;668998:8;;=:7:\U[W][YZZW[Y^\_[?@;<>?AC>D=B\b]ba]`c__aa`a`aAEDFCHGGFIHCHGFCceaefehihfbejghkFJINGKNMHNIIMOLHmmlhggloliolmkolLMRMLOMOQQOTTNVTlnlpoqtqrtururuoTWWSSWVUTYYTYXUZvxvtxwuuzuxywxzv^[\]ZXX[]Z[_[][[y|{}|}{{|~{|`c_^be_`aee`cebe~~WUYZT[\UY\\YY[\Y<>;<;>==??C]_`]\___c]`bb`b`GABABHBCADIFIBHHaceebghcgfgbcdfcJHLFJJGMHMMHHOJIlnlmilinnoojimolSMOOLSSQQQRNNSNSornpnqoprtossqsuTWTSWUVTVTUZXWXYvruxyzyyxwvxytyv^WXX^\X^Z__^^`a[yyx|~x~x}~zyzzaa_d^_bdbdb_dcfc}~~hffgicfedeeijjjgWX[X^^X[__[Y[YZZBABA@@BCC@B>CBCA^``aabddd]adebafFGDGFGIEJCEDKGHFichcbjdfhedehjfiNKHJHJLOOOPNJJPNhilhjoikpolkpqkjSMRNPONPPURTURPQunspqrptuqsprttuWUWSZXUUYYUVXWYZyut{wzzzvuyyzy|z`_ZY^[\_\_`_```b}zzzy|~}||||`ef_dadfb`afgdacjeiffejhfhkiifjg\^[W]]\[Y\\Y[]]aA?@D@BB@@>@@C??C]`]b`cbcc_a_bdeeHCHCICEHIJHEJKEKihiijkfiieiffijgHMILOINKKLPKJPNOjpjipmnmlpjponkoNUUROTOQPWUQVPTUqsopvurrsutppqxsYVXYXWZY\XW[U[W\uvyzzzyzzyz{yxy{[[_a`aa^[\`\ba`]{~||e`c_gc`eeefcdbhbfjgihjjihhhikijg`\\Y[Z`Z__[^]]`\E>EFFD@FDEBDDDGDd^dbbaf_`fbdabbfEJGFFIGLLELJFKHJghkiifkmhjkimfgfOKKLOMOQQQOMRPNLknkoqlkmqmpmnqrmRSVVVQQUQUVWQTWQtuustpqtsvvuutryXVZWXXZ[]V]ZWY^Yv{zx|y{{zzww}~|{__b_^\bb_]\]\a]]}~{~~geageebdbidgdcgjjlklgkikkjjiklmkZ^_][`]`^\\_a]`cFCEGABEEFBBCEDEC`efgefaggggdgbbcIHEHGFIJKILLLKMJkgikfhmjmhjkjlmhKROLNRKRPSPNOSSUqqplmqqtmsnprortVTWQVQWWRRWUTYYTtrxxqwwxuwsvusuvZZX\XXWY[YZ]\]Y`|wv}y{z}}{y~|zca\]^_ad]decdcac~~didgeccjggfjdkhjmghhhihmlknjnlnk_aa\^]_^^aaddb]^BAAHHHEDDHIEDDCHgbfcghfdigjgeffjLJMJMMGKLJJJKPNLkiminmminmjomppkRNQLQMTOPUONPQORsnrpqpsqonurssotYTXTSYZTWTTUV[VWwuyxtvwyvuuuyx|u_^[X_Z\]]Z`ZZ[ZZ}zyx{zy}|]^`cd_aed^ef`defchfgggegddfkkfflnnokkokojmolmlnn_`b]bd_cd`d`eacfGFDIICHFEEJJGJEDihfffcffcjeegekfLKOMNLKNMNPNQNMPlljiijojilllkojrQTRTQQSOUTTVSTTVnonruttuttssvrqvXVVXUVVVXWVU\XXZts{tzz{vuuvyvyw|\`_]\ZZ^_`[[``[_{~~{}|~~c`abaaddcaghfhehjhhhghlmemmmgjjglolnmoklkoooqopo__]bdaaa^__`cdcbCFIIJFJIEKIJDGKHcfihhkeelkkfliimOIOOPNNLMNJOLQMNmiimljlmmqkkkkmkOOUTRRTSVURUVPUVqrtuusvqtpxtqwtrZXY[Y[\U\VWXV[YVxxxzzv|ww{yzzy{|_Y][\^[^b_a`\`_`z}}~|}cdaabaccggefddcihjehkiijjmgngiglknoknpqpmmomnmon`efcdbfaecfgdcadGFJDKHGHFJGHLKKMegijffekjigllimiMPOKKPMNRRRPMOPPnpnmokpmorslmmlqSUVVUPQUSTUUXTVVstqxrquvuwwvurszVVWYWVXZYY[]Y[\\{y|v|yxw}{x{}y~aaab^c\]\cba]a`^|}}facbgcdbigcdcdjjjgikmghijjohjkilrooqrqmppqsnootpfefaacbbfgcfghfeFIJHILGIMKGJLILJmgiflgihmgkhliokLMKPKPQLSMROPTQNmmkrprnpptnqnmrsTSVRWUUSRVVVXTWTsxvsrsysuvsyvuwtZ]WX][\^]Y\^^Z\[|{y~}}|}{{}}}{_d]cbc^ccb^`aeaa~~hiideefjedjhhfdkiimohpljkinlmqqnrorqsptrqsursstrhdhdgigfgjhcjigdJMJNLOMOHMKJJNKPhmkjjkkkjjjpklljQSTPQQMOOTTVTTRPotsttrnouquqtsqqXTWXVVZXXXXXU[TYvuwxsuwztyvvuuu{]Y^YZ^[Y_\]Za]Z]xzy{{y||z{_dc_eecaff`egcdahieidjfjkleejelljllomnmllmpqprqmqrttuutvpvqrsvrreffdggfcgehhjikjNNNOHLLONKNNMMQPnjihnnmnnnmnooloTPTRQOSORRTTPRVVsppssotrtortvpsuUUTXYVYZUUVU[VWVtttv{{wzw{z{uv|}]_]]\\^Z]^^aaabb}z~~~z{}}}``eeed__ceagefcfefkihglliififfknjnpppnnqqrnqppmosuqqqquvrvvusttrdgdjgkddegefifggKPPKNJOMNKKJLMQRnmppnnkrlppkopssSPPPTPVUVVTSRVXRqptrtpstutstttswY[X[W\X\WZZ[\Z[[z|ywz{}v}|||x}y}a[_\ab`\_\]_^b^b||}~|{~ad`fbedgadehbccbfmlljimhmiiglmmjkrmnrolqsntsnsomwursruttvsswxswwjejgfijihljlklnnOQLKLLPQNPSMMPNOmqpkqnonnmnorrsoUPPQWXXQWSVSRSYTvvswuxusurxrrrtwUZV[W[W]^\_]\[ZZxyy{}y{{|~|y{}z_a_``c]^bcb`]^a^||~hdddbechfgghdfdfhillikknoinmkpmoqoloqrouqutqvtrqwvxwxxxvttvvvuwwglmkinkjlhilhoilQLRNOOQQTPRONUROrmlqnsrsnqprnroqXTXWRSTTUUTTUUUTrqvststxsvstxvxv^^W_^YY]^`^[Z^_Z|x}z{y|}|~}}}}^___`]^ae^a_f_df~~cfefchicijhhjeiikhlohlpplmjnpjlpsopusvstotprprpsvtyytwxuxxuyxwvwLJQRQPLRMKPPOOMOklklmlknmlpolsmpWUQVVUSUSSTXSWTWpvwvvsrtswxuwusx\W[YZY^VZY[XXX\^w{|}y~v||~yzz}yy__\_]a]\]cd_^`ca}`fbcegbeceidijdhinhlnnljjollkjpklqrrmnopsouqvrrsvuwxyytxxwuzyztuQOKMPNRLQSRTSROPrqnqpnmsslsprnnpTRUSQQVWVYVSTXVSxwturrxutrwzsvsuVVV\YXWYXW[^Y^][|w|zz{}}{y~y|a]a^]bc]`d_^_df`}~~biibgigiidfcifiknknljjjoopjikpmlppoqptquppnuuuquuuzys{uv{vyvuy|uPSPQPQRUMPPSUPVTrnmtmnnptutstovtSRUUYXXSYUUWYUUXuryzwyxwuy{{wzu{[X^YZY]]`Z[\]Yaazxx}}y|}z|}{]cdacdcc_abccec_}~~fhecfjdhefgfjghfnkiiojlllmnpklrktorspuvpvtpqtxxtu{{wwwwvyxyxwv|zOQTOOTOSUOQUSVSRrppurqsuurvppswvZTTUUWUZ[YZUZYXWutxuvwtzuwvwyv{y_^]^[`\``^[b\\b`}z{{}adeb_a`ggfaf`hdfiiiefelilhmljjjnmpjkkknpnpqqqrsppqtutvwvtvttxuyy}zu{|{zwxx|x|~}zVOPVRUSPPTQSSXQTpsstustruxswvvsvXZ[Z[YW]Z[Y\\Z\Ywwzw|vwwzyzw}}xxZ`\^[a[\`__`b]baz|{~}~|~}ce`afgehihfgdhdfjgfifghlgnlnilkoqqolmsrtmoottoqtruqrrtxrxttxwyx{zxz}xyz{z~~|{~QPVVQVQTTRRUTYRXpspsvxtwrruwstus[X\]XZW\\WZ^]\[\yzzww{y|y|y{}|~a`__``_ac``]_^db~|~}~~bbfegbbghbcfeiefgfggmmkkimllkoknrqpqqtqpnnrpronuwtsxxyrsxystxytzzxz}x~xx||zz|{QRSRSVTXYXWTVXYZuuryrvvvtvwzvzxwY]\W^YYXXY[[][_[yx|xx{|{||}]c`cabb`c`bdbfc_~diciifeffjjdjikhilohjlonkijppjoonnpoptquprtqrvwpwvtzuuyz{z|yzy{y~{|}~~{|SXWYUXUTUYZYUXYXrvsxxyuvxy{yuvw{Z]]XYX`Y\^[[[\Z^~yy{{{}~~{|}}b_bcdd_afb_e``ae~idhdjifkhegjjefkknpmlopmknpqopqptsqtppqtswpurstqyvwwx{vxy}yyzvwwz{z|}~{}~VWV[VYXYUZW[YV\[xzvwwu{xuv}|x{zv\[`a_]]``Z_`[\`]y|z~}~`f_fac`fadagaahhejjgighimmkgkhkjnonmokoqproqnossquvqwsuussxvywyt{yvyww}}~|}{x}y|~~|~WTYYUUUVYWV[W^[[{u|v|vz}yz}{||}y\\`a`\b]`b``]_b`{}||~~{~}}dfaedafechcdbecehfhilkmgiijmkojjokpqrmnroosnrqoptxrusswxwwwrtxwvzz|~x}~xx}z~z|y}}~W[]\XZZ^W][[X][[{|yxxz~}|yxxx|}[]`cba\`]cb^`ada~}}}~gdcaedfhbehdjiehimhmikjmnlmmjnpnnptroqrrounttuqourwsuszvwzwtttz{{y{{}}{~~{~~~\YZ\^[X_^YYZY_]`w|x|x}y{y{{{^bb]dbdcda^`b__`edhcjjiggfigijhgnkjjplonikpnmknnprsqpttppvrprwrqyvwzyyzxyy||vv}z}}}|}{}\[\Y]]`\\^a``b\]}yz{z{z}~~{c`abdbdfd`gabefefjjjkejlfijflmfkmmjqnqooqklqlqortvpuvrvuwwsuqtvwxyv|vyvv{xwzxzx{}|~~\\\]\]_[_b_`]`bb{|}~|~~~bc```f`acehaehehhklgmllmnhmjilknmpqrrpmlspmnsnusqwuxwxtyrursyvvyvx}xz~{y{{zz~~~~]a^_`^^^^`c_]_b^|||``gafbhdhdbfbedcikgjmhhlmjkjomomsosmrssrontnrtrqustryrvtyszyvsuv{~~~}~{y~xzy|}~~\^\`b^_]`^c`_d_c~}~~dcdegdjijjjdiggeighmmijjkjjpnmomppqtoqtusstrqrrvvvystuyyzwvyzyuyy}}~|z~}~}||{}~~|~|~}}acbb`gchhchdhedcjjfklhjhgmiimjjjmnmpsqqlmqpqponptuqsurtrtvsyuy{t|}~{zz|~zzz~}}}~}|~~adcfiihdbbghkfjjhhgijhijioijkklpnqmntrnortrststuwuvuvuwzuzvyvv{yy}yyz||y|}~{}}}~~~iegfbhegfijeiefejhnljhikkllmpnomnoopquuruvvqqvwtvzyuzvv{yxyy{v|z}z{~~~}~{ijhdekdkllgkjglfpnonjnmnlqolkqosuupqoqrpttpuxvrwyxw{yz|yx|xx|xx{{|}~}~~~giijemifhhgklhmlpnlnknorqssoqomrrvqquqvxwyqxuuwv||yxy}{yw|}x~~y{{~|}jmlfkklkihlnmmliononqmsormrnrnursquyryurzsuywzyyx{z}y{}~}|{}{z~}nkkjlhjonjhkliklmpsmsmqottprspqsvxvwtwwxtttx|wyxy}x~x|~{~~~lhinpklnnkplqjnopsstqvuqouvstusw{zwvzwyyu|{w{z|v|}{}|~}{}jpjklomponmqpmnqosqtprrwpuxsswvuvyzv{v}x{}|v|x{y~~~oonpopplppprqnosptvuvwqywvxywstwx{yx|w}{{}y~||z|}~prqppmosnnnpqpqorsxxsxvwzwwvuvxu~|yz|{{~||ztmtqtosuqtpspqprzruuuzzywtyz|||v~yyy|z{rntusuqtwwqttuxqxy|tz{xx}yyw{z|}~}{}}}}~pprttrswvststyyx{{|xzv|~z{y{y|yz{~}uqywttrxvtzzvywtx}z~yxz}~{y{}{|~~~~wsyuyvzvswvwxyty}z~x|{||~y~|{~~uvrrxqusxvvutsytv{wv|y}}x~|yz}}y}~}~~vrrxuwtsxzzstyx{yy{xw|}zyzy|}|}zuvwzxvxvzxuz{{|~{|z{{{}~~¿uyxvvztzx{zy{|{w{|zzz}z~~|{}~¼¾xvzwwyz|v{zzxy~}z||~}¿x~x{{zz|}}}}~{|}}¼½¿ÿ|~{x{y~}}~{y{~~¼ÿz{}zzz}{{~|ɀz}}~~~|~}}æ}|~}}}|ľÿƿ£ǃ~~}ĬʄýĽɨ¾èʭ̈́½âȫ΁ž¿ū˯υ©̰ѢǭүĦǯӳ½½Ʀ̱г¾¾ŨΰҶ¾žĿſÿŧ˫εָþŤȬӴѹ ṷ̄ϵع¿ý¦ƪɱҶ׼¿ý¿ĦɩΰշۻÿŬ˱׳عĿ¾£˭ұҴۿƥͯӰַ޻¿ĤǫʳϴԻ¿ĿĵÿŮ˯ֳܾþij½ſſ©ǪЯس޾ĿùĿäȯҵֵۻ½¿Ý½¿ǩƫͲշ۽⛚ÿŤΪϵԺ¼½ý䚛»¾¨ȪϰԽݾ皞þģȭβδո¿ÿ硡¿¿ĿǨбԶٹ¿靠¾ÿſǩεѴ׿½襞¾ÿǬгյ׿½¾韢ƥʲϴѸþ뢧ūȰ̵ֻ¾þ饩éȭϵս½ꦦ¬ͰѺռ¾誩ĿªƶҺؾ몧Ĭ˸ӾýĿÿ髮ȯƲͺſ뭨¿DzǴϽ½髫¿ʹӾ½þÿþȹͼþýȵ̿¾¿¾Ľþôʷ½¿½¾ź̼½ľþ·˸þŽݾƿ˾¿ѿ¿ÿ߿¿ƿƻ¿ĿĿ¿Żƽ¾῿ÿ¿¾¾ſ¿ľÿÿĿƽĽ¿ü¿¿ýǼ¼¾¾Ŀſ¿ýĿ¾Ŀ翽¾鿿Ŀſ \ No newline at end of file diff --git a/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-cb.bin b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-cb.bin new file mode 100644 index 00000000..6121415a --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-cb.bin @@ -0,0 +1 @@ +vvvuzyxz~ͽtvwvwxtz}®ɻqouttwvx|{Ǽqrptrrtu}}~ȷoqsooqrs|{|~|~ɷlnqouqrs}|z{}~Ŷnlonrpqqzwx|y|}~Ƴjolpmoqsyu{{z{}~ö¿z~z}}z|osoqrvst~|{źwzzz{}nrqqtrvu{{|~|~ƶuxzwx}}ynlnrqtrry{zz{~|Ͼxuz{yyz{ljnpnnrrz|{z}}}¿ñοwwwxyx||mkmnmlqpwwyy{}{}µ˾xuvswvxxhljlknoo}vwusxx{{}ïʺrpsvwwxvfkjmkjml~wwvtxzw{ʺqrpsrwsveekigikj|orrtvwwyʸ]abbbcefsxvx{{z}inoonnrqtwvzxz|b_aa`beetxxwyuzzlimmkmnpxvvzy{z~IJ_\^bacacsuttuvv{jjikllpntwuxwv|{Z^\^_\absussvuuugihkkljn~qwyuwxxv~X_\___b]rqsptttufhfkigmi~}}~rrttwwxx}~ZZZZ][_^omqptstsfehfghjl}z}|}~~roqtstrt}{~[WYW^[\\moqsppspbebhhfihxyz}}~rpsrqput{{{~~SVZWYYZ^kjpnnoprbdcechhjxzy{|yzorqptotrx{x{}edggjjik][X\^]a`qrrsuuwydghhhink}~rtrtwuvx~}}fgbgehfgXY[]_\a^mmqqrstuejdihfij~|~otsvrwuw~~~cdcfiefhZ\Z^[Y]]qnrpqsrrecfdhigjxz|oprsrutw}}~~}bbbeddhfWWXZ[ZZ\monpprspbechfeeg||zz{}nnmrpstu{z|~`c`aefddXWUVZYYYomlnnnosdcbbfehfxyyy{y|ylnqpoorr}zz~{{}^`^_^bfcTVVXYYYYkllmpnprbabcgchfsxvxwz|xmmkpmqpozw|z}|{{`\_ba_`eTSTTYUZWgkkimkpn]`abbbbcuuvx{wxxinlnloonyvxyz{y~^]Z]]`a^RRTTVTWXfhkjilkm_]a```ddrwvtvy{{fklnmoon}wvzxxzy|HJJMPLMQ_acaecegUWVWXZ[Ykmnloqpo`aeebfghu{zz{|}}pmqqqqts|zz}~HJMJINLO_``^cecfUZUVX\YYllnmpqqr`babgcehxvvy|yy}olmonqrry|{|}FFKJJLJL^a]^baaaTTUVTUVWlkkklkom`b_accegssuwz|zxnlljlpsrvyy{z{~{FGHIHHKH]]___aaaTUSTTUUWkhillmjq``_b^ccdsuwuwz}zjonmlrnqvvwxwxyxEFFHGEIK[Z\\`]__NPRTRSUVjfkjhmll]Z]_^a`esssxxwxxjiknmlpo~twxxxyyzFCFDEHHI\[[[]\_]QPOQQQRXeeijjkoh\[[[_^_arpsutuvvhikikkml~svuuxuzyA?FBFGEFY[Z[\[^_NONNRSQRbefhgjfi\[^^[`_`sqoqututghhijjjj{~~rssrvwwv{CB@AEFFEYXZZXZ[\NNMLPRRNcedhghhjWY[X\`^^oporotuufchhfigk{~}}}ospturuu~}OOPTSTWUHEFKHIII_]]]]__aQTTSTVVVghjllloo^\[^daeduttvyuwziillkonnsvvywwz{OQRORRTUBGGHEHKK]\[Z[]^\OSQPTVWRgfjjlhkmZ[_a^]abrortvyvxijiillnn}tswv{wxz~KOOQNRORCFGGDHGJZ[]Z]^\_NPOPRSUTfhgiggij\_]^[a`_posutuuxdfifljhm}|rvswvtxz|OLNNOONP?BCFDDFE[XWZ]ZY\PNOORRORchfeehigYYY^Z^__qqqpqsstdgfjijkl|{~}~spsrxtvu|~JKKNNNLSB>BCDCBEVX[Y[Y[\KMNMQRROdbdefeghWX\ZZ[]^nqqqpqrreeefiikhxy|~pqsstpts}|KJJJLLPN@?BCCBBDTVZVX[\ZHMNNOMQNabddbcdfU[WYXZ[^ommmnqpqdcegghdhxzx|{}}mppqosut{{~~GJGINJNK?=>>A?CBXVUSVYY\KKJLNPNN__ccdcceUUYTWVZYlnlnporpebcbfdghx|{z|~{lmqporuvxyy|~|DGKGGJJL?;<@AA@BPRVTWWYWLFMKLMMKa_abbecbTTWWXYZ[iilomopoac`bccchrxwyyzz{kkonpsppwzv{{|~96788::;KLIONKQQDBBEDEIGWXYXY\\[NMOOORORbdfegiel[[X]Z\`[oprpttrtfedhgjfl|{~|}~qprstvwv~|}42664989JIKMLMKR=ABBEDBETXZYZ[\XMMNONRPS^a`deeffWXVVZ\[_nlplqqorbccggelhzywz|}}mppqsqrt||{|}~22265596HGLHJOLL?A?ATRTTTXXXHIIKLMPLbc^_a^bbUTVVXY[]kmnkomnn`cadcdefxuwzz|}}kpnopprqzyz{}|~/3/13443EHJHGHML<<=>><==MQOVQSSTDEIGEGKL]\Z^^a_bMQQRRVWWegiijjjj_\_a`a`buqtxvxvvejkkomln}tvwuwuz}*-01-/.0D@CCGFGI:6;<:><;QMQQOQORDEDEFJGI[Y[]^\\^QQSQRSTTdgdhgiik`^]^`_^aqqsuvvvygighijnm|~~~qswyvxtx=<;=<@?@20044486FFGLKKKL;:????@CSSTWZWUVIJJGIKNO\a_`eaccTVVXVYZXljllmnoma`dccedduvzw|z{}lllnoopsvxxzz|~}:9:<<><>11-/2753FDIIIFJL88>=??>ARPSSWXWWGGHKKJLJ^]__``bcUSUVTWWWikmmjnnnab__addfwvuux{~|ilimnopoxv{z|}yz89:::<<<.,./..01EEGGHJGK9:=9@?>=NPQSPTUSHGDIIIKK]\^^`aabPPSVUWXVfhijiknm\_b``bbcuttuwxv{gkkpiqnnvuwzv|yz658:<;;=*+---30/@ACDFIEH;7<:=??@PMNRRSQUFFHJIJKI\][]_Z]^OQOSSSWUjggfljik_a`_```crtvrvuwymilokonnvuuwyzy{}588:8:<9-/-1/422?BECHHCI99=<=>=?MPPPRRRRCDGHEHJKY][_[^^aPOPTQVRTeffhhgml\\]]a^dbprrvtvtwhhikmllo{~~yvuxy{y|~331544:71--1/105?B@CBBEG7;8:==>ABCEAC99;7=>;BIJLMLOPRBEGFHHIHVXYW\W][NTSQUVSScceggchf^`]^b`_cnooortstfhhimmjmz|}~|uwuw|wwz~214455760,/01004?@>>BAAC;=;=:=>AABCDOPORSRTSJMLJMNOO[[[`]_^\VWTXX\ZZcdfjgllkcceghgefrrtstwwvqknqmrsw~~}|{}}~~&)))+),/1585:78820434668>ABEDFEFA?@DCBDCMLMOQQRQLJNMLLPMYYX]]Z[`XWUYWYX_cffigglmeccdiegfppqsrttumqorrpsr}}|~{{{~||&$*&)))*6447539744527498BAACADBEA>?C@EECKNMONMQQGIKNLMNPYYYZ\^]]TVXZW\[^eagggfghbbcfgehhpsqpqtrsknqtqsptzz~~}zz||}(&(('(*+1226588503434968?A>AA@?E=>?ACCD@IKJLKMMRKMIKLNOOWTWXVYW\TXXZ[Z[[ccedeiii_bcgdgegnmomnstrqnmmosssy}{|||~||~|$')**'+.23234766/5444878?@;??@BB=??CB@?DEIIKMMNLHILOOOPPVUUVXXZXWW[XYZZZb`dcdfdgdabecfgfnlnmnootlnmortsqz{{{z}~}z}yy|~'%'&*)+*/.10246512462557;<:>?AC?;DACJIIIKIKLMJJJLMNPWSUXUYXYUWXW[Z\Z^a`cccdd^bcdefegjklolmonlpnrqusnyyzy{{{{{|{{~|}$(%*&'+)-//0364142365876;9=>=;@??=?@?AEEIFHGGKMIIMKMKJOLQQTRVVXWSUWXXXY\\^``^`baadddbfcekilnlloookooqptsuuv{{z{|zx{|{|}%*&+&)(+./,0/004053438678::9;<;<@?A@?@BBFFGEJHLKGGLNLMOOQOSUVSWWUW[YZZZ[\\`^^`bc^feefdehjfjkjlloomnpqqrrutuwyyx|w||{}} \ No newline at end of file diff --git a/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-cr.bin b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-cr.bin new file mode 100644 index 00000000..d05ad51a --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-cr.bin @@ -0,0 +1 @@ +tttrwwtwxz~{|}{stutuvqww~~x||}~¿Ŀpmtsrusvvt|zz|~}pqotporsx|wy|y~}~~~orsnnppqxwxywzx~~mnqoupqqzxuw|xzy}}olonspqpwstxuwyy{~kqmrmoqswrxxvwyzy~{z~~~}osnpquqrzwu{yyy|~{nsqqsqvtwwwyvy|z~|}}olorqtrquwvuvzwz}~}~{~mkopomrqwyxvzyy{~}}~~}~}~}pmnonlrpttvuwyvy|zz|}y}|}~jolmloppturpttwwv|{{}zw{}~}|hmlomlomvutrvwtxyx{z|~|}zzwzy~z|hhnkiklkmqprtutwz~zz}|ybfggghjjy|~joponnrqqsrwtuxz|}~iegffgjj{}znknnlnnpvtswvxvz~}|}}~fbdigifh{}zz|}{lmjmnmqorvsutszx{z}z||aecdeagg|}{z~||{jlkmmnkpovxsuvusxxy}{|}_gcffeib{y{x||{{ilinkhokqqsruuvu~wx~||~|~bbbaebfexuyx|{|zjikijjmn~qnptqroryuzxz{|y~d_a_fbccvxz}xw{wfiflkilk}}~spsqpntswvvyy{z}}}[_d`aaaeuszwwwxzhigiflkn}~|}osqpsnsptvsuxzz~|{}~~~qpssvvsveb_bdcgfzzyz||~fjjkjkplprprussvx|wv|}{|~stosqtqr``bdgchduuzyzz{{ingmkhklnsrupuruyyzx}zx}pqpsvqrscebfb_dd{w{yy|yzjgjgkljl}~opqrptruyxyxw{{{pporqqur`_`bcbacwxxzyz{wgjgliihi~nnlqorrswvxyz}z~{}|~oqnnstqpb`]^cb`ayxuxwww{ihgfjilj~~}|lorpooppzvuzvuwz}~}~lolmkoso^``abaaavwvwzwz{hfgglgljy~{}||onlqmqpnxsyvyxwv}}oknqpmns_]^^c^c`rvvtwuzxbeggggfh|{|~}}|kpnoloonwsuuwwtz||z~~nmilknol]]^^`]`arsvtsvuwfchefeiiy~}z|gnnonqpnutxutwux||~|{~UXWZ]XY]mopnrprt^`^``ac_uwxuwzxveejifjjlz~pmqqqpsrxuvzxyyy~UX[WV[Y\nnokqsps_d^_adbawvxvzzz{egfflfik~|{~}}qlmonprquxw|x{xz}|{TTYWWZWYmpklqoon_^__]^^_wvvuvuxvfhdfghikxy{}|pmmjkpsrruvwvwzv}z|~~|~UTWXVUYUmlnnmoon__]]^_^`wssvvws{ffdgchhiz|}{}lrpomtnqtttustts{}~}}TVTWUSVXkjlloknmYZ]_\]^`vrwurxwvd`cecgej{zz~}~}mlmpnmqpruvvuvuvy{}~}}zVRUSTVVWmkkjmknk]\Z[\[\cqqtvvvzrcbbafddgyw{|{|}|kkmkmmomqussurxv}{x{~}}~QNVQVVSUjljllkmoZ[YY]^[\nqrtsvptcbfeafef|zwx}{|zkkklmlllqqrptuusu}y~{{yz}TRPPUVUSkikkhjjk[ZXW\^]Wpspustsv_ab_cgedxxw{v|}|jflkilimnrortprrywzz{|yz~~bcchggjgWSUZVWVVollllnmo\_^]^___rttwwwyydb`cjfki|{z|{|kkmmlponqsswtsvw{xy{|}|~cegbfehiQWWVSWZZmlkijlmjZ^\Z_`a[srvuwrvxaafgcbfgzvy{}|~lmkknmoosputxtuww{|{{zz}`ddfafbfSVWVRWUYklnimmjnZ\Z[]^_]sususrttdfdeagfdxw{|{|{ghlhokioquqvtqvwzv|y~}|}eadcccadORTVSSUTlihkmiik\Z[Z^]Y]pvrqqstra``e`efe{zzxyzzzgjhmlmmnroqpvqtrwzyyzz~}_aacccahSNRSTSQThjmjlikkXZZY]^]Zroqrrqss_`dbabddwzyzxxzyiihillnj}}pprssnrpyzwz{yy{b```baecQPSTSRQTfhmgilmkUZ[[[Y]Yopqroppr]d_`_bbexvuuwyxyhhilkkfk}|~mpppmrsrwvzzzzx{~{^a]_d`c`QOOOROSRkigegjjmXXVYZ\ZZnmqpqppr_^b\_]b`vxvwyxzwjggfjhjl}mmqporuutuuw{y|v}}}~[_c^]a`aRMNRRRPScdifjhkhZS[XYZYWpmpqpsqo]]`aaabdtsuyvyyxgidghgglx~}~~~llpoqspntvqw{wwy{z}~|NKLMLNMNaa^db_eeUSRVTTYVhjihilkj[Y[ZZ^Z]oqsqsupxdc`e`cgaxxzx|{y{jigljmhnppqrsuusyzvwzy{JGKKHNLM`^abab_gNRSSVTQTfjkjllmhZZ[\Z^[^lnmqrqrr_a]]adafxtytyyvzfgfjjhpk}{~lpooroqrxwvwxx{|HHGLJJNJ^]b^_daaMURQRTPUhfjklgkhWYWYZ\[Xomoqnstr^a^`bdbcytwwyz|{kfggjklj}}mosmrqqrvxxw|u}{||~GGJJFHIK]`_]_^a`MQRPPRPQgeffejiiVVVXXY]Xrrkmoknn_]`_aaceuwwuxvwueifighhi~{|~lpnpppqpwvvwxwzy~}FJEGIIJH]`b_^^cbONPOOMPTheecfgijVXVVVX]Yllpnqqor\\^Z]^`bsstssutycifhigih{z}~lpnnrpppuvwuxtyw|}}|~{GHGJHGJI[]]^_^c`NKRMSRTPecddficeTUVWWV\Tpklmkqqq\Z]]_^a_vsvxwzvxgcbegihh~z|}|~llkppimputyvrtvwx~|zCEFFGFIE]\]Z``[\NONNQNOOaebjdffgSTXVSUYZmlimmpmqX[\\\`a`psusutssebegffeg}xz|~{{flmmpnmnrtuqtqvzzy~}~{BEHIDFEG^X\[_]_aNIOPMQNMfbeebdbeTTSTUYUWlikmnkjl\\^\\]^^psosrtsuhecdgecfxxz|}|}ikhjklonoquwsupu{||z~~YWVXV[YYIGFKJJML^]^cab`aNLQQPPPSfffjlifgWWXSVX[[jonnsopp]`_a^ab_wtvuvwwugfjhhighz||mmlnooosrttvuxywz}}~VUUWVZVXHIDEHMJH^\aa`]abJJPORPORfcffjjiiVUVYYXYVnlnmnnpq`]__]__`tuxxtxwwhhdcfjik~|{{~knjmoppnusxvyztu}z}~}}TUVUUWWWEDEEDCFG^^__`b^bMMPKSRPNaddfcggeXURWWVXXmkmmopoqZZ^a_aa_rstusuwvbehfegfg|z{{}~{inlrisnosrtwryuv|z{}RRUWXWVYBCDDDKGEXY[\^a\_OKPMPRRQdabfefdhUUWYWYYVlmklnhklZ[Z^]]`^vsrpwurufhgeffehy{~x|{}~pknqmponusrtvwvxvzz{~{~|RVVXUWXUFHFJFLJJX\^[aaZaNMQPPQPRbeeefffeSTWXSWXZjnkojmmp\Z\_\a\^qrrssrxwdcddhdkhxzz~{}z}kklnonnpxusvwywyyz}|}~QQMRQPWSKGFJHJHMY[Y][[]`LQLOQQROaadbdbcdUVUYW[XXhfiomhol`\`^`^_`qrsqqpvshedghfgjx{xy|xy}mmkmqttqttwuuvwxz~||~~}QPURTQTUHIHFKHHIZX[[]_Y[OOPKRSOV^_bb`cefTVXWYYYWhjjhmfmj[b`^ac^^pprttosrhiefkhfkwwwwz|z{jllmqqlpuwtv|vvy{|~|}~}QPSRTRTSLGIJKIIM[[XW\ZZ\RTQSPRSQ`be`_fb_VXYXZ[ZXdfihkkkj\^`a^^b_posqrrppkhjghglhxvx{x{w~nknorprt}utuxvxwzz}~~EIECHHEKVSTVUVXYJMJPRNSN[\[[\^^]RUSUVVWXcecfgehf[^\Y]]^^lkkplomjbc`dcgddopqurwvulkmopnkmzy{y{~~|vnqupuvy}{z||}}DHGGIFIMORURWSUSMINLMOOPX[[^]^\]XUVZXWYWb`bdeefe][`^]\`]jihmmijoedbebddkosrurrwynlklqmnlxxyzx{{{qvsvusvt~{zz~{{~~EBIDGGFGURRTROVSOOPLQNRQ][[\Z\Z]XTUYVZZX`dbdbaeeY[]_]^_ajjjkmnmlbcehcigjrnstsqrslkloonppy|yxx{yzpsvyuwsw~~{{||~~}~IEHGEGHIPPPTSVVRLOONNSPRZ\X[[YX^TUVXZYZU_a`a`bbf^a[]^`a`ieiifiglbfghihhgqprqrvvuhllqmqnoxvwuv{|zwtsrsxww~~|{~|~DHIJJFJMRRQQSVTSKROPOSRRZ[UYXZ[[UWV[ZVUZ\`_acbc`[\_bbacbigghiijiffjgghggqnrpqrqtpllomopowvwuvwv|rtsuxzxu~|~zy}IFHFKHKIOMPNQRTSOOQRNPQRVWUXY\^XSTYTV\XZa``_a_aab^^]``abjegjgkijeghfjikglpnqqqqpimnoppopttvxuvxusxtyx{ys~}~}}|~FJFKFGLIMOOORVSOROPSRTSRXUYYXUZYYUWYWY]]b]_^]ac^^b_b^^b^ddgehhjidfhghhhjkmnnkmomnqpomqmpvsvyuuxxwrvwxw{y|{|~~{}~~~~HMHMGKHKOOLPOOOSOSQQPVSTUVVUVWVWZY[YXYZZ___\a_cb]]ad`bccebfhieiifhljjkjjkkollnpqjsrqrppstpuutvvxxuvxyyyy}z{|~}|{~} \ No newline at end of file diff --git a/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-y.bin b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-y.bin new file mode 100644 index 00000000..e0cc919a --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/ref-dense-y.bin @@ -0,0 +1 @@ +/525.51//673344648948;69779:77<;   >@<<>@>>@B=A<=BA !%% #%#"#"'"&&B@CA@BCFFFBFEAGG(+$*(,,&',,--&-(LFLKJIHGFFGLGIJN+..-.+0-.,10/-3-NKQMONMNPNLRPQRQ1176034158481935WRQWQSQURVXURXUR9;78:<>;778><===V[WV]ZY]][WZ]^]ZA;B=?A?@AB@>>??A41/42451663331344778;7:599:78:==!! " !#;=;=:==B;=A??@B=$%# !'&!#"''!##)ABB@EEDHCBDAHHED&,,')+()**.((,.(JIFILHGFLKHLNHNJ,.//0,-.1-30-002MNNNPQLRLMPMMNNT625822622962:599SVUVWYWWUSRSYYXW=:97:7==>??<;<>9[X]X\]ZX]_]][[]Z?>==>>A>?A=A>CBA246836166735629368:;;<6:9<<;=88>""! #  #?%'#!$"#"$(#'*&&*@FFDFDECHDJFDJJC(*(*.',*.().(/**KKHGKIGJNLIJIJKO12,.1--.-002.563PNMQNRUOQPQSNTTO496673899748;57:WSXSZTVVUXWZXTYX98=98=9=9>;?@:=>AAC@CADC@481778685689339: 98=>==<8<;8>>>?>$#!" $$ AA?>?=D@E>E?>D@D#)#)%%&(*'))('&*FBGECJHFHHEEGJHE,)*+,.)00*//+-++MJKNLOJJJNLNKJJM.1/100430210//76OORQVOPTTQSURQPX3:4868;5967<:657YUZWWWYZX\YZZ\YV=@>=<@;:<:=AB=<=[Y_`\^`^`\]\\[]aDD@DBDDBDCCDEEBA3:345677875978<:  ::98:@==9<:A@@<=%"$"#! !$" '"??@>DBAB@CAB@EBG*($(*$(%+%*,)*')HJJDHFHJEJGMGFIJ)-.*+**,.-2+.02.NIINNOQPOMLRMLOS4432242517838354QTUTVSQSTVWSWWTX79:6<7;77;:=7<8:YYWZ\XYZVXZZW][Y>?>>;?>=>>@A?@<>\]_`a\\b`]b`]_bbCAEDDA@AGEGEDBFB4654:;5:67;::66:  !9;::=@=<>AB:89[ZX\XZYX[[\\]\YZB@?A;<<>>>>@D@A>ab_a]a_a^a`c_`^^C@EFCECEBFBFGFGD6:;;76=<7<68:::= !"# <@A>@<>=BA=@CAD@&#!$#%!"'$(""%$'BCA@CAAFEGBHHCCE(-,',+-(-**-')..JFHMGKGNIJKLHHMO0.2-1/0.3-1/3010PRKROOQRQRMOPRQU7656359653387644TWRXRTSTURVUTU[X6;7:9:;??89:??9@XVY^YZ[\_Y\X[_^ZAB=C]_b^_b^a_a_ddcc_EHGEGHHIHEEGIHFI8:799:8<889:=88;"!!!!#!#$@>BB@>C?C@D=BBC?%%')&'')%$%*&()+DCHDCECICFDECJGI-,*(-,-(*,.+*.)-HLKIOOMHOIKMOONP.,02.14./4202052NMTOSPNQURSSOPUR25938375654;49:7RVYSTZZXTTWZ[U\Y>8<:=8?>?@:?A?>>]Z\Y_][^Y_[__a]`A@AAEAD@A?AFDAC@c_b_dcbdbebfafdeFFGGFEIFJIHJHKFJ8:;=?;8:89=9:<>;#!  "$!!#"% "?@?@CC>E@AADDACF"&)($%#))*&&*+*&JCFEGJJKJGJHLFEE.)+),+.)-.0-*0/-MHPMJPPLJQLJNLLO0210635626373736URSPRUSOSPPWVQPR7587;;757:759777UTZ[XZYWY[]U\ZW[><:?;:;=@A=ABZ`\^`_[`_a[a`^`]@@FEC@C@BABFECFCd`ccdafbgedadedgFEHGFHKJHLHHIFGG<=:?>?9?=:<=?:;?!$% !$# &#"CCBA@?@E?B@E@BEG*')*)$&'(*%%+,*'IIDJKDJLHFFJHLIH0++-/-+1+,*,-21.NNIPNMMKPMQKQRQR3206516626536235TVRTQSPSRRUUUTUU48:;98;<=:;=6=6[_a[`\\`^b_^\cabDCFFDAGAAHHFGDFGdddddgdcacgcdbchJIKJLFFGLHGLJJKM;;@:>A<#!$"%"% ''&(&""!FDFDEAGBGHDAEBEB%)+'&*+')('()',*JIHFIHFMKHLMHILK,,++///2.,01,1,2KKJMRQNORRMQROPT5366422862362629QVTSRURXXSWTRUVU8<57;98<87>=9?:8XWXW[WWX[[]_ZY\\@@=@=@>=DBBDBAAB`bababca_aa`b_b_EAEFHGCCGDGIEIHFcbcbdeghbefghdggIHLLKKLLIJNNLMIJ<>==>?=ACBC?>CBC''!$#%"%#('#)"'&FGEBBCCBGFEHIFFG('*(**-)+),)+(+-JIKGMKGNKINHIHJO00/,0.4-20233320RSNSNRMOTNRTUNTT71889842469848:9SUSXXZSVSZXWTXVZ9>:=>:=?>@9>9;=;^][]_^Z[\]^_[^[]AA?D>>C?EDBCF@D@`_]e`e`c`befab`eIFDFFHJFKGHEJFEHhfifjdjjggkkgghfJMMLMMJLKMLLKMOL=C=AC=>?@C?EC>@D#%%##&)$%%&*)(%*HIBEDCGHDIHDCJHG++'.**/,-*)/-+/1HKNHOIJPMNNKPOLL2.001542./542242RSNSNSTSUSSURTOT5874:867477:<<57YYZYTTUT[XY\YYW[>9<<:>?;>?>><;A<[\YZ_Y_[_^]_a`]aDDEADBAC@AFBC@GFa`_bda`ef`dafddeFEGEJIELIFGLFHIMhdjieigjgfjkfhikKNOKLNPNPOOMNMLQE?A>E?AEEE@CCEAB&&*()(&%%,$&+*%&DFHCKDFJEEFKJJHK,--0,-*1,..*0.-.IMMPLNNKQJOPNKNR0020113004246567PPTURQWUUPQUWVTS755<57:6:8:;766;XV[X[YY\V\V\XV][>:<<;;@A?B@@@>B>[`]`[]``a[]`c]^a@EBGD@BBDCGDHEDGadbccbcbhhabhhhgKKGLELHLIGMGGKMNjifjjjhgimkmkjmhKOPPLNRQQNRPNPONA>EDAB@E??B@DFGF&'++%)*)'(,'-,))GJGFKKIJIHJFJKFJ0/-/0/10.+.+2,--MLJOKNPPKKRNPNPR1364661173327687SUTSPWVTQVXSQWTR9:5689:767:<<;7;WXX\\\\]ZZXYWZXX===?@;AB=8;?;?;<]\^W][XY^^ZZ\[\\<@B>B>>?ADE@?C@A\c__^d_d]b`dbaceDFGGCBGHICHGIJGFicdicgghdjgfgikhGGILNOLMHJINJMQMgolioojjopjknnolPRQSRSRSSTPPTPRR%*)+&&+(+'%'%)'-GDJKEGKHFILILIKG*/-00*-0/,011-.2NOKNLNLPQMKQQMRQ2243601351167416UVPQOVQTUTUVVXUU875:775:;<<:68==VV\U\VZZW]\X^]YX;A@<@=ABB?CBA?>B^\\][\_]^bb]]dabDEDDDEEGGICDHDHEgdbaefefegighhgdIKKJMGINMHGMKIJLhlflgmimjmkjokkjRLOSOPQMNPOSRSNNpklpmrmpqmrqqrnp(&+*)&((&+('()+.GJGKIGGGJJKMHGGM,.-,00/-2,-/1421OONPKLOSPNMQMQMQ5036483163557487WRTVSUYQRXUVSSUU<:9;;==>;8<8;=98]]Z]Z]YX\X_^Y^Z_B?C=>A=DA>>>@EDE_cac_^cb_`b``cb_HDHIFEFHHBGDIIKIhfigfhcdggefkhghHLKNILNMJOMIKKIJmmnikkjmklinnokoNPMSPRNNTPOTURUOrqqosrostssstttq)&&,)(+,)(-'-,./FJJLLGLJJJMHNOJM/.1/3./02412-./5MRMQMLPRMPMQRSUU54115539847847;4UXUXVUSRVXSWVVX[6789:7=:99>?@<:?>@AB?@?@DB?a^`da_bca`beeccfIGDHCDGGIFGFEGGJbdiijjhfhjdieddhNMOHPMJPKNMMQNMKjknomkmplkjoqjprTPSTUORUTOUSPPUStsnponpsstrssspr((-*,*(,+,/+,0+0KJNNMLMJOONKNNOO3-.12024.10.5/00QNNRRTRPNSURVPUV8438:567;88879<6WWUZVWZYXZZZWX[X?<=<<9@>=<=@<:<<_]\]^_\`[_]Z[b[]>D@@D@?DFAEFACBC^c_cad_ccgg_c```FJFCKFKHIHGLLKJJedghgigekhhigilmKKLQIOKOPLKRLNPQolljlqpjpmrnrqllUSUOTQSVTSWWQTVQtrpsqrsrttqttsws+(,.)0))***1+0,*MMMHKKNQOMQONPPM..44250146417502PTQTOVPRSPPTPTQT5:998<59<<<<=76=S[W[[VYVVZVX[\V\<=:=?@?;<=^_]_a[_^[^``^_b^@E?CADCCBAGEBDHHbee`ead`babfafceEKGKLGEIFGJGLIKJekfkljkklklgmkghMKLQLNQLPKPPQPMMpkqpnslrrpllmqsnUSUTSWQWTWVRWYTTppprtquvuwxtvuxw)++...0+-/.311,.JPJMMPPMOPORMQPN//66276712858384PTQWSPRSRUXXVSYW958666;8;<=>=<;:XXV[[]ZYW]XXYX\_<>;><\YZ\\ZV[X^[\_]XZ>;@BCC=A>DB>B??Eac^_``acb^]bcc^dBBCDHEBBDFDIIGFGddfcbchfbgfjjhijJLGGHKJIINOIPNOPllkimhjjkkmnioknQQROSTPNTRQPVRPVnprppponoptvuuppUXWWUSYXWZ[ZYYVZvtuwwxxtwvztuwvz.3-02-20.0/10/13MONMRORRTOQNUUNT1716465983756:67XVYXYUSYVYYWXTZX=>=:;>=88:>:=99=]]Y^\Z[\Y[Y[]\]^?C>@ACAD>C?ABEDDa_b^ba^e`affaeacIGDEGJDCHGKHFKKJfcddhiehiedilfklNNLMJNKPLIOJJNOQoiolppjmnjpomkpkQPPONVOTPRTTRUQTnosssutsvvpsprtrSUWUU[YXZ[W[[VWZvzwxwxxvw{zvwyxw20.3//34251//636QUSRSQPTNURPVQQR897444565:758<89UYWXYYVXYYW[YXXW9?:>UXTZZVYWW[\[YY]]@>@;>:B;@?@B<=B=[^\__a[^]ab`^bbbE@DCE@@CEFGDGCHIbaf`cbdaechgfdfcHFKHLFGJLHJIIMNKgklkmigmmllkhighQLLMLNMPNLNOMONNnoppqopsmrmrpsptSVSSRRUWQRXVXYTXtsstssyuwvtwyxzt[^XWYXYZ^X^\Z]_Y|yxyxzxz|zy}z||}7747660317336856PRWWTRXQRTXSUSTW<;:876:97<8;8=8B?>?>=E\[^^^b\`ccb_`b_bEFABGHHGICHDIEDHghcbgcbdccfeicddFHNKHMMIJJNLMLIIigkkhkjhnokopnokNQLORTSRQTRTSQSSoqqpmntpunussrnqSVUWXWYTTSVUWUZVvrwzruvtvxywuzz|ZY_\[\X\^]^^\`\Z{z{y~{|}}||~~}}|4851441584922484VSRXXRVUWUYUYXYW<=:679:8>?<:;<:<[^\ZXWXWY^^Y\\_Y==BBA=C>??@BCAAD\\b`^_]cbb^badeeGGGBHCEHFFIIFIDEbbbfghehjcjikfjjHIMJIMIHMIOIMMJKinjkmlnljlikllmlPRTOMPOOOOQPPTOSsqsqtnttquuvqtruUVSVTWTZUVUXUY[\swsv{xttxzxxx{uv]Y\][Z][ZZ\a_]\`yx}~z~z|~|z{~~932392:998:8::48USUVUWXZT[XUYVVV8>98=:<==;?@^_]_Z]YZY`__]``aA@DCDE?@?B??@ECFc^d^_db`cdcecebdJHCDGGDEJJGELEKKjggeehhfjghfjllfNOLNPLOPMPJQPMMLmonniqnkqpkolpnlRTPQVPURVRWTQVPVrsqrpsvtpwrrpxvtXTWYUYZXUVXY[[Z\ywz{{{xvwx}v{zw|]][^`_]_`\[_b_^_{z}~{}~5537945698955<:8SWY[YVXZUZWX\WV\=;@?@A:=:?A?_\_Z_^\_`_b\^[[bABBEBDFC@E@EBED@_e_`f`e`bdbfbaeeDFEIIHFHLFJGILLGfikgegfgjmijnilgNNNPJMPKPLQMMQSQkoqrlnnlopqmmmrmPSQSRPWSUQUQVSXUruutwrswwwuxwytt[\UWYY[XZYYW^XY^wvwxyy{{xwy~{zy~_^_]__ba`^]_^bc^~~}~6;<55755879:88>7W\YVZ]W\ZXWYXY[Z:?A:==A>;BAAC:98@BBA@CA<@CC>=A[]``\\`a_ad_a`ecG@BFDCGGHGDGCBEDdchhcghgdijgdjdeFHHJIKMHLKLHNPLIhhlmmjjhilikinnlLQRMRPOSNOQQNSVOosnrosqontotvppvTUUYTSYUSSVVUVZXvurvsxwt{sxu{vx{X\\_[[\_Y`^]\Y[`|{|{zzy}{|~~c_d``cb_d`eacaed~~VV[TZWZVUZZZYZ[]::;A;@:>B@>BDA?cb]^``^cb__b^``eEIBCCHFDJIIDIFIJhfbfebjehjjgidjlIKLMOLJKJLLPNMOPonnijklnjpjomjloOOUOOUOPTORQTURUornpqspoutpsptsqWSSYXUWYVTVZXWUUuvwzvvxuyvux|v|w]_[[\[`Za[][[b^[}{{}~{{|`^fd`dbdbdc`e`fdjhifdiiggkggjjhh^^]Y^\X]Y\^[^a`_?ADBDDDCB@?ABBED``b`cce^^``eedccHGFEIEGHKDFEKJFGiejejdfjgekghjjiHNMONLMJPQNMQOKPklmnjpmkojnqrmrkOQSTNROPPQVVUUWRqtqtpqrsqwvuwptsZVS[YZZZVWWX[W]]wtvyuuuxuz{zvy|v[_`^_\[]^]Z`^b^_~y~~{|~|~e_`ccdebedbdddhfhhegghjjkkglgfjj^]`[Z[a`\^_a`\^\D>BE?@@AD?BDCCFEe`ac_aabedf`ffeaGJEGIELKGLFHKFFHekggiehlfggljlkmINPJKQQOJLLLPMPQknoqlrkqqkqomlsrPPRQUVSWQSWXQTUXspqrqrrttvrwvwtvVWU[WWV[[VWZ]X[Y{w||v{w}wy|yz}}`]_]b^b^b^`b^`ba{{{|}~`gbgaaaggfahgieijhgkjkjljmkklmnh`^[Z^a``b`ab`_cbCDFAFBDDFCAGBDFHf`eecdchcdedgghfHEIJHMGKMKLMGMLHhflmhjhlkmggonhnKOPLSPOSORPNQQPRopsrmlqqmoonsrrnQRVTTUVUUSXUTVTYtxwrxqysvwuyrvs{WZZYWY\Y\]_Y]_YYx}||}~z{|yz||{|b^_bd^a^ca`ca_eb}~~gcffidiiheecjdfkgkjlkihlnnljinmn\^b`a\`^b]`^d`bbDFCGFGEFIIFDFIGHddhhfbggiheigfgjIMLGLIIKKNILMLPMjilnnniijmohlmjmLNNOSTQNMNMTRPORlplstmpsqqrpttruWYWVWSYYSTTWVVX[xyvsvtzxvuzz{yuz^\\^_[X^Y_\^`aZ]|xxy{z{{||}{~{`_ddbaae`cdcfd``gfejjdfdeegfehfemninokpmommpknko^b`caacacb^_dccdFIIHGCDFHEDKEGDEehdefgfhdijfdfkhHOJMLMOIPKOMMOOQiiolpoomlqpopmmkSONSTRRPRVVUSPUSqpnrposvtpsuvwsqZZZYVUXTZ[\V[YZXwyxwxvx{zv||v{{|_Y^Z_[[[^Z^a_a`^~}{|}{~{~|_cadccedgg`addgeifllilleljiifihijomnoonlmkoqpnom_^edebdcbcedfccaDHEDFIDGDKELEIGEehckifkkieeelkgfMNKMNNJOQMKJLJQKkijnmpjnnnmmqqrnUNQORVTPUUTSPSVTtvqqoouutvuvvvrrWZV[VV\WW\[Z\\\Wvzwut{||wu|wzyyxZ]Z\]\`_ba`b]]]c{~||}~fffbeggbecefhccijfjjfhlglinkkmmmmpqqppqrlmmrprmsd^ffc_eegfafhgcaIKHLJHGGKJKHGMKLeiikgfijjijghlgmMPLPQRLNOPMSQSLMppnnloopprrqoplrRPRVUWRRUQSVRWVXuvrqwsxvxqttuxxxVWYWW]Y[Y\\\X\[[v{xw{y{~}x~{~zyy]^\__\b\]c^bdad^{}}}~~~egahhdbicdgejegehkmgggjimoiomijlmqqsqoprsprpqsqpdadfabafhfcficgfHLFHJLIILKKNOHJHkiigmjimljklkkloPQONLSQMOSQPSNPOnpollpoqoqooooooXRUWSVXUXWUVVYTUqvsrxvwtwvzvyxxu][\\W]YXXXY][]YZ}wz}z}}{{{z}}_`\bd_dbecabab``}~ibbebeihgehkgggilknjjkollplnopplpsnrqsrsrrrossspfhbggichdifehjhgJGIILIKLLNLILNOKnjkmjohlmkjlnnqkSNMPTORNPPRRPOORspoprpurroqtpossVXSSTZVYUYTT[UVVwyuuxutvwxxvwzuyZ]Z[][^]`\]\][^_x{{z~~zz~^___ba_b`edbbcba~~ijdkjgfjfkjkhflgjppmojoqpqknroqputsptusvvuuqrvqtgibibgcihhejijidLJJONJOOKMKOMJKLjjkolinokpjooqprNRQNQRSPOTPQVRUUtroruqstopprpwtpYVVYSWXVXU[WWV[Xussxttzuv|z{v{xz\^\\_ZZ]Z\a[\b_`}{z~|{}{}~{}`_fd`febceaafbfgdkjhkfklhjhkhnhlppknkokonnmpsrpnrprssvtsvvvvtsrvhihdjifdlljffglgMINJOOQNNMJKPRKNimknjmopnpnolpllNRPRPQUTTTWVUVURsustustpvrpqwttwYZZUZXXV[X][Y[YWzzyw|x{yzwzx}x}|a^_`[`a_b^ba_a^_~}|z}}~|}eeddedcfhbdcecfghlgghhkjjhlnhkolopmplpopotpnropquuvvuursruxwtssveefhlmgifhhjlmniKPMMORKOMKLLMSPOoqoqkomnkmossppsSUQRRXTSVTSQSUXYutuvwxsuuxtsryvy]WY\W\XY[^[]W_XZvzyxwvy{|~{{z}``[``b^da]badbdd~{|}}~}}cfdgfgbedgfecjefkilgngiklijlnjkomtpssqtupototrspvuxxvuvuvywwzuuufhmghnnhmoiniinpNLLMRPPSOTRSPPUQlpormtssrnqutnuvSTYXSSZVRZUTUTY[wtxxrvytyxuvx{utZZ]XW^[__\Y`Y_\Y|w|}zzx{y|~}`adc]daddfaacbf`~~~~hghdccegdikjhedfmlljkjoompjmqpnloptotonuotvuqvttywwzvzyzy{zzzzyyLJLKNKQLQQRNORSQpppkmkopmqmmotrqPSSRQSUQTWXWWYYXqsvspuvqvwusyuxu[[YVV\]Y]XYW[Y\^|vvxwwzwzz~xz||{````^c]]a`cb]db`}~|~}deeaehfbbddfchffjmimgmhnkhmhlilmormststrqsnuppsosusxyuvwtzxtyzuzOQNPOSNPSNPSNORNnrkrllolmsrptomtUWUVXSXSYRSUVVZUqrqvtwxsrsysxyut[Y]Y^WZ^Y^]YZZZ_wz|~yz{||z{|y|b]b\]`cc^ccedd_d}}}}~~dgihieihgcjhfkfeijoiihklnkmpkmknrsqqrtuqqossqsptvswtwvxyxxyyv{vwRNRPMNRQNTQSPPPTsqmrrnrusunsuvprTVURTTTVZUUZYXTXxuyxtsxzvztuytwyX^YXZZ_`_]_^`^\^xz||z~|z}~|d]`^a`aabc_adfad~ghdgijfjifhhkhgfmloiniojpjkknkmouususrwtvtsrxwsvtvuzxy{{vwz||vv}RUSTOQPUPQPUTPPRnnosouqsttpvwsusSTTU[YYXXUYUXYZZsxz{vvzwvxwxyyyy`^_`a^_]^^``\a\`|{z|z|{}cdee`ab`ce`ecgdfjehgkeeiliifilknmnlmnnnqrpqrlnsnotupwpttwvxrsyuv{zvy|{zw|z}}x}y~VQRVRPQVVWVRXVTQvvtuqvwvptsqxsvvY[UW[XZ[WV[YX]W[zvuvy|vyyy|{w}~Z][[abb_b_c_c`]`z~}}~}~``acgfhghfiegcbcmkihgjimhiglnjkpnmplorrppqqmssrtvtrrttvyyrtzytyzz}}zw~{y}~}~~|TPRTUVXTWUURQSVYpuvwutvwtxrsxrut[ZZY[\YW^[[]XZ\[z||yyyxx~z~xx|xy[`^aa`\_cccbc^`e}~~~fchefddgfhceggeclmgjimlmiikhhnljpmmonrmqrqnsporrxrrsvyxstuztxuzt~z|}yy|z||z{{||TWUXWQXWTWZUXVZTtrqusvyxywxxtvvw[]^\Z\Z]\\_Y_\\[{x|}x{{{xy}{yzcbdd__`daacdeab`}}gcehdhjjdjjjifhhihnnjlilppjmoooprsoptsstvvqpuvpptsstxtv{uzvxwyy}}|y|}~|SYWRVVTZXXVVZ\YXxxwvyvsw{tvwt{{v\X\Y^Y^]_[^_\_[\z}}|z}~}{z~}ab^bcb_``cdeefdb~~hejifeegjhhjlggiijimojkqkrnrqpopqtovvqvrrtwruuuwy{vvuzxz|wwv}|z|}}}}|||XTY[YYUZ[V[ZWXVXz{zzuzvwy|wxv}vz__]_^[``_]]_\ab]|}}{|~}bc`fafebahadcgacgejkkifgffhmfmjhqnqnpmlmqrppsnnsttpsspqrvxyrxxsx}v}y|w{xwx{x|{|~~ZZV[UUUY\]\[[^\Zyxtywv|vz{~zxxz{``]__[[^_^\a^]d`~}~|}}~ebbfagccgbebecccjfflihhilhklnklkqqqrqonpnlsomtoqrvtuxutvvtyxutuy}yz~}w~}|yz||{z~~VZY[[YZYYY\[^\]Y|{y}|~x~}}}yyzyz_^]ca`a`bd_^b`ce}~bcbebbfddgbhieddjgjghnnhnlnikppmpomsrnuttprpuqttxywwvvuvsxzuzuvvzyz|}xzz~{]X\[YX[YXX]Y_^[]yy{}|x|y|~c_aab_d^cbafdfbd~echjidffedkggkhekkplnpjkpnqljpqntunqrprpoopurpqtwy{yzutz{vwzvxx|yz}~{~|}[`Y\[^\``]Z]\`b]~|{||zz{}b_cdab`acfeadedgffgffhghikkflhhgmnjjpkokrrplsornspvspwptvwvtsuvy|yyz{x}{||{w~y~~}}}`^\^Zb`b`^]`[`cbz~~{~~~baebcgfagccbhhcekijigljlmlkggihjnolrospqrmmrsrtmssqsxyrrxtxvuuxt}w~|w{yz|}|z}z}}~a_]a[]`\]]`cc`a]{|}~~acbehcafeegfgchfjgjkikjiihninjiolonmosmsmrrortqtttxxqyxzssxtvuwtxwz~|z|~z~z~~~}_aa_^^_a``__b___}hadffidjdcdhgehekglnmlkilnonppoonmmtpssutprtvsutswwttuytuyxwwyx{~}x{z{y}}{z}~~}~||}~~~}~~cdcafgbbieedceeihhklkgigkiolgihnmlmqlrmqtotnossqvsxqtvwuxvuuttwxw}|x~xw~}y}|{{|z}~~|}~dcehcefeciegjhihiljhjokihoinnnijnntrntspnsrsuqqpttxyxtswxuv{w{|x|xz~}{y~zz}|||~cbehfdcdjdhikelhnjnjlhkpjpqolppqsrpqorrsotoqppstuxtuvwuvxzu{uv{x|{|}z|}|{}~ijfhihehfljkfihljpkmqqllqpqkqnontosoopptuwvrrusxyvwxw|xzw|xww{xz~z~{|}~~lejihefkllfnhiijqrrpnmpmmslomrqppqpussusxxxuwtsrvy{yxxx|z~x~z~{|}|jhlljihkljmiioikqmnmrqrmqnsmuqpostrrrwtxvtyzu{xt{}zyw~}y~y}|}~hgnimnmiiikmnjjprsqqrptsunrunovsvszwyxwssv{{|{yv}x{|||y~y~}~opkiiljpnpqkonnkqtoqopqqpvtqrrvvvusvxtyvy{xxy}|y{|}{}~{z}||~mmmlkmomnpklqoqqvsqrwuswptrwuvsy{vyvvw}y}y{z}{y}}~}~~~}omokrmplrpromrmnwurvwtuuutwzvxxszz}xy}}|}{~x|z|z~}~~~~mssonsorotrnnrtpquuvwyysszvy{zuuz~y|~z|y~}|~}tssnrqnrsptpuupptyrxuxyzyzx{|{wvy}yz}{{}}~|{{{ttrspupsvqtvxpuxwuxxwwy|xzz|vwwz}~|}~~|||}sptvuswwwtsquwuwxuv||z|x|w|{yzx}{}}~~wurwwwvurtuysvzv|vw~z|z~|{zzy}~~}~~~wvuvuvxvwvswxxvu{}~}x{~~|~{}~qupstsrxuwyuvwyu}zz~wxwzxw{}|x~{}urtvtuxywuty{ytx}~xz{~{~}{|z~|z~yszuxytvzuxxzw}z~{{~{~|{z{{|~{|~¿yv{wx|yvzyzxy}w|~}z~z|}}}~~|¿þww{vy{{{xy{{zw}x|~}v{|zzz{~yxzyz{}}~~¡~y}{{~|{||{}þ||}||}~{{~|~¾ĽŦ}z|{|}|~þ¿ģ~}||}}}ž¿ũ}~ɥDž~ÿĤǧʃľľȬɁſƿŬ΄ĿèǭуȧͬΩſ¨ͭδ¢¥ˮδ½½ɫϱѵþþƩвֳſƧˮͱ׵ſ¿ĤǯгԹá¿ƿĦˬҰշ¾ľĤȫɬҵڼ¾¾áƪϳҲ־ýýĿ¿ƨȩϲѴ׾¾ίѶټ޺½¬ɲζڵݿ½Ľãʦɰα۹¾ÿĿ³ÿþľħɫαӵ׼²¿ƦȪѲҸܼ¾ŵçɪ̳׹ݻ¾½þĜŪȫҲӹ޾¿嚟¾½Ť̴ͫҶٻ¼ý瞜½ȪȪΰڸý堛¿¤Ȭʰеսݼ¾ľÿ鞡ý½ŧ˲Ժܽſſ韡¿éȫ̯Ҵ޽¿¾ꠠľǭ̵غۼ¼롣̴ͭѻý¿¾頦¿ʰ͸־¾ÿ餦ĭǭвֹپ룧ÿū˳̸ľ몥žƬȰ̷Ҿ¿ꫦıγηؿþ멭ïͲ̽¿þ¿뭯´Ƴκ묩ƿȰʶͺſĽľïǷͽվžľijķ̺¾İǶϻýܽľþÿűȹþžܿĿľÿɷ˿Ŀþÿ½ÿ¹Ϳ߿¾ſ¼¿½¾¿ƷüĽ¾½˼þ¿ÿ὾ſƾĿÿʿÿĿƻÿý徾þľýÿÿĿ¿ſÿÿƿÿÿ \ No newline at end of file diff --git a/clients/apple/Tests/PunktfunkKitTests/Stage444Tests.swift b/clients/apple/Tests/PunktfunkKitTests/Stage444Tests.swift index b13fc09a..93c3402d 100644 --- a/clients/apple/Tests/PunktfunkKitTests/Stage444Tests.swift +++ b/clients/apple/Tests/PunktfunkKitTests/Stage444Tests.swift @@ -47,18 +47,21 @@ final class Stage444Tests: XCTestCase { box.lock.lock(); let frame = box.frame; let error = box.error; box.lock.unlock() XCTAssertNil(error.map { "decode error \($0)" }) let ready = try XCTUnwrap(frame, "a 4:4:4 ReadyFrame must be delivered") - XCTAssertEqual(CVPixelBufferGetWidth(ready.pixelBuffer), 256) - XCTAssertEqual(CVPixelBufferGetHeight(ready.pixelBuffer), 256) - let pf = CVPixelBufferGetPixelFormatType(ready.pixelBuffer) + guard case .video(let buffer, let isHDR) = ready.image else { + return XCTFail("a VideoToolbox decode must deliver a .video frame") + } + XCTAssertEqual(CVPixelBufferGetWidth(buffer), 256) + XCTAssertEqual(CVPixelBufferGetHeight(buffer), 256) + let pf = CVPixelBufferGetPixelFormatType(buffer) XCTAssertTrue( pf == kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange || pf == kCVPixelFormatType_444YpCbCr8BiPlanarFullRange, "expected a biplanar 4:4:4 8-bit buffer, got \(fourCCString(pf))") - XCTAssertFalse(ready.isHDR, "an 8-bit BT.709 4:4:4 stream is SDR") + XCTAssertFalse(isHDR, "an 8-bit BT.709 4:4:4 stream is SDR") // The chroma plane (plane 1) must be FULL resolution for 4:4:4 (vs half for 4:2:0) — this is // what lets the unchanged shader sample chroma at the luma UV. - XCTAssertEqual(CVPixelBufferGetWidthOfPlane(ready.pixelBuffer, 1), 256) - XCTAssertEqual(CVPixelBufferGetHeightOfPlane(ready.pixelBuffer, 1), 256) + XCTAssertEqual(CVPixelBufferGetWidthOfPlane(buffer, 1), 256) + XCTAssertEqual(CVPixelBufferGetHeightOfPlane(buffer, 1), 256) } private func fourCCString(_ t: OSType) -> String { diff --git a/clients/apple/Tests/PunktfunkKitTests/VideoToolboxRoundTripTests.swift b/clients/apple/Tests/PunktfunkKitTests/VideoToolboxRoundTripTests.swift index 3b3a15c4..b6dee5cf 100644 --- a/clients/apple/Tests/PunktfunkKitTests/VideoToolboxRoundTripTests.swift +++ b/clients/apple/Tests/PunktfunkKitTests/VideoToolboxRoundTripTests.swift @@ -99,8 +99,9 @@ final class VideoToolboxRoundTripTests: XCTestCase { box.lock.unlock() XCTAssertNil(error.map { "decode error \($0)" }) let ready = try XCTUnwrap(frame, "the async output callback must deliver a ReadyFrame") - XCTAssertEqual(CVPixelBufferGetWidth(ready.pixelBuffer), width) - XCTAssertEqual(CVPixelBufferGetHeight(ready.pixelBuffer), height) + let buffer = try XCTUnwrap(ready.pixelBuffer, "a VT decode delivers a .video frame") + XCTAssertEqual(CVPixelBufferGetWidth(buffer), width) + XCTAssertEqual(CVPixelBufferGetHeight(buffer), height) XCTAssertEqual(ready.ptsNs, 42_000_000, "pts round-trips through the decoder") XCTAssertEqual( ready.receivedNs, 41_000_000, "receivedNs round-trips through the frame refcon") diff --git a/crates/punktfunk-core/src/abi.rs b/crates/punktfunk-core/src/abi.rs index 1fef7322..a1343825 100644 --- a/crates/punktfunk-core/src/abi.rs +++ b/crates/punktfunk-core/src/abi.rs @@ -2230,6 +2230,33 @@ pub unsafe extern "C" fn punktfunk_connection_codec( }) } +/// Read the session's negotiated wire shard payload (the `Welcome`'s value, bytes). This is the +/// parse-window size of a [`USER_FLAG_CHUNK_ALIGNED`] AU (PyroWave datagram-aligned mode, +/// design/pyrowave-codec-plan.md §4.4): every `shard_payload`-sized window of the frame buffer +/// starts a fresh self-delimiting chunk. Clients that decode PyroWave natively (the Apple Metal +/// port) need it to walk those AUs; other codecs never need this. +/// +/// # Safety +/// `c` is a valid connection handle; `out` is NULL or writable for one `u32`. +#[cfg(feature = "quic")] +#[no_mangle] +pub unsafe extern "C" fn punktfunk_connection_shard_payload( + c: *mut PunktfunkConnection, + out: *mut u32, +) -> PunktfunkStatus { + guard(|| { + let c = match unsafe { c.as_ref() } { + Some(c) => c, + None => return PunktfunkStatus::NullPointer, + }; + if !out.is_null() { + // SAFETY: `out` is non-null and the caller guarantees it is writable for one `u32`. + unsafe { *out = u32::from(c.inner.shard_payload) }; + } + PunktfunkStatus::Ok + }) +} + /// Send one input event to the host as a QUIC datagram (non-blocking enqueue). /// /// # Safety diff --git a/crates/punktfunk-host/src/encode/linux/pyrowave.rs b/crates/punktfunk-host/src/encode/linux/pyrowave.rs index 67749b12..6c620764 100644 --- a/crates/punktfunk-host/src/encode/linux/pyrowave.rs +++ b/crates/punktfunk-host/src/encode/linux/pyrowave.rs @@ -1130,10 +1130,10 @@ mod tests { ) } - /// Decode an AU with a standalone pyrowave CPU decoder and return plane means (Y, Cb, Cr). - /// This is the Phase-1 "golden frames" oracle: the host-encoded bitstream must round-trip - /// through upstream's own decoder to the CSC's expected values. - unsafe fn decode_plane_means(w: u32, h: u32, au: &[u8]) -> (f64, f64, f64) { + /// Decode an AU with a standalone pyrowave decoder and return the full YUV420P planes. + /// This is the golden oracle for both the Phase-1 smoke check (plane means) and the Apple + /// Metal port's committed PSNR fixtures (`pyrowave_dump_golden`). + unsafe fn decode_planes(w: u32, h: u32, au: &[u8]) -> (Vec, Vec, Vec) { let mut dev: pw::pyrowave_device = std::ptr::null_mut(); assert_eq!( pw::pyrowave_create_default_device(&mut dev), @@ -1177,7 +1177,13 @@ mod tests { ); pw::pyrowave_decoder_destroy(dec); pw::pyrowave_device_destroy(dev); + (y, cb, cr) + } + /// Plane means of an upstream-decoded AU — the Phase-1 smoke assertion. + unsafe fn decode_plane_means(w: u32, h: u32, au: &[u8]) -> (f64, f64, f64) { + // SAFETY: forwarded — same contract as the caller. + let (y, cb, cr) = unsafe { decode_planes(w, h, au) }; let mean = |v: &[u8]| v.iter().map(|&x| x as f64).sum::() / v.len() as f64; (mean(&y), mean(&cb), mean(&cr)) } @@ -1304,4 +1310,107 @@ mod tests { .expect("submit after reset"); assert!(enc.poll().expect("poll").is_some()); } + + /// A deterministic busy BGRA test card (gradients + checker + LCG noise) — flat fills + /// exercise almost none of the entropy decoder, this hits every subband. + fn test_card(w: u32, h: u32, seed: u32) -> CapturedFrame { + let mut rng = seed | 1; + let mut buf = vec![0u8; (w * h * 4) as usize]; + for y in 0..h { + for x in 0..w { + rng = rng.wrapping_mul(1664525).wrapping_add(1013904223); + let i = ((y * w + x) * 4) as usize; + let checker = if (x / 16 + y / 16) % 2 == 0 { 48 } else { 0 }; + let noise = (rng >> 24) as u8 / 8; + buf[i] = ((x * 255 / w) as u8).saturating_add(noise); // B + buf[i + 1] = ((y * 255 / h) as u8).saturating_add(checker); // G + buf[i + 2] = (((x + y) * 255 / (w + h)) as u8).saturating_add(noise); // R + buf[i + 3] = 255; + } + } + CapturedFrame { + width: w, + height: h, + pts_ns: seed as u64 * 16_666_667, + format: PixelFormat::Bgrx, + payload: FramePayload::Cpu(buf), + } + } + + /// Dump the Apple Metal port's golden fixtures (plan §4.7): host-encoded AUs (dense AND + /// chunk-aligned) plus upstream's own decode of each as raw YUV420P planes. The Swift test + /// (PyroWaveGoldenTests.swift) PSNR-matches the Metal decode against these — float wavelet + /// math is not bit-exact across implementations, upstream itself ships precision variants. + /// `#[ignore]`d GPU test; regenerate on a Vulkan 1.3 host: + /// cargo test -p punktfunk-host --features pyrowave --no-run + /// PYROWAVE_GOLDEN_DIR=/tmp/golden --ignored --nocapture pyrowave_dump_golden + /// then copy the files into clients/apple/Tests/PunktfunkKitTests/PyroWaveFixtures/. + #[test] + #[ignore = "fixture generator — needs a real Vulkan 1.3 compute device"] + fn pyrowave_dump_golden() { + let dir = match std::env::var("PYROWAVE_GOLDEN_DIR") { + Ok(d) => std::path::PathBuf::from(d), + Err(_) => { + eprintln!("PYROWAVE_GOLDEN_DIR not set — skipping dump"); + return; + } + }; + std::fs::create_dir_all(&dir).expect("create golden dir"); + + // Odd-block geometry on purpose: 256 aligns clean, 144 → aligned 160 exercises the + // block-grid overhang. ~1.6 bpp at 60 fps. + let (w, h) = (256u32, 144u32); + let mut enc = PyroWaveEncoder::open(w, h, 60, 4_000_000).expect("open"); + + let dump = |name: &str, bytes: &[u8]| { + std::fs::write(dir.join(name), bytes).expect("write fixture"); + eprintln!("wrote {name}: {} bytes", bytes.len()); + }; + + // Dense AU + upstream-decoded reference planes. + enc.submit(&test_card(w, h, 7)).expect("submit"); + let au = enc.poll().expect("poll").expect("AU"); + assert!(!au.chunk_aligned); + dump("au-dense.bin", &au.data); + // SAFETY: test-only FFI with locally-owned buffers. + let (y, cb, cr) = unsafe { decode_planes(w, h, &au.data) }; + dump("ref-dense-y.bin", &y); + dump("ref-dense-cb.bin", &cb); + dump("ref-dense-cr.bin", &cr); + + // Chunk-aligned AU of a DIFFERENT frame (its own reference): the Swift window walk + + // FRAG reassembly must reproduce the packet stream. + enc.set_wire_chunking(1408); + enc.submit(&test_card(w, h, 11)).expect("chunked submit"); + let au = enc.poll().expect("poll").expect("chunked AU"); + assert!(au.chunk_aligned); + assert_eq!(au.data.len() % 1408, 0); + dump("au-chunked.bin", &au.data); + // SAFETY: test-only FFI with locally-owned buffers. + let (y, cb, cr) = unsafe { + // Feed upstream through the same framed walk the clients use. + let mut stream = Vec::new(); + let mut frag: Vec = Vec::new(); + for win in au.data.chunks(1408) { + let used = u16::from_le_bytes([win[0], win[1]]) as usize; + let kind = u16::from_le_bytes([win[2], win[3]]); + let body = &win[4..4 + used]; + match kind { + 0 => stream.extend_from_slice(body), + 1 => frag = body.to_vec(), + 2 => frag.extend_from_slice(body), + 3 => { + frag.extend_from_slice(body); + stream.extend_from_slice(&frag); + frag.clear(); + } + k => panic!("unknown window kind {k}"), + } + } + decode_planes(w, h, &stream) + }; + dump("ref-chunked-y.bin", &y); + dump("ref-chunked-cb.bin", &cb); + dump("ref-chunked-cr.bin", &cr); + } } diff --git a/docs-site/content/docs/pyrowave.md b/docs-site/content/docs/pyrowave.md index 72651d47..15a38392 100644 --- a/docs-site/content/docs/pyrowave.md +++ b/docs-site/content/docs/pyrowave.md @@ -47,9 +47,14 @@ setting. 1. **Host** (Linux): build/install a host with the `pyrowave` feature. On an NVIDIA host the capture path additionally needs `PUNKTFUNK_ENCODER=pyrowave` in `host.env` for the codec to be advertised; AMD/Intel hosts advertise it automatically when the feature is present. -2. **Client** (Linux session client with the `pyrowave` feature): set **Settings → Video - codec → PyroWave (wired LAN)** in the gamepad console, or launch with - `PUNKTFUNK_PREFER_PYROWAVE=1`. +2. **Client**: + - Linux session client (with the `pyrowave` feature): set **Settings → Video codec → + PyroWave (wired LAN)** in the gamepad console, or launch with + `PUNKTFUNK_PREFER_PYROWAVE=1`. + - Apple (Mac, Apple TV 4K, iPad — wired networking strongly recommended): set + **Settings → Codec → PyroWave (wired LAN)**. The option appears only on devices whose + GPU passes the decode probe (Apple Silicon and A13-class or newer); picking it forces + the session SDR. 3. Leave the bitrate on Automatic: a PyroWave session pins itself to the ~1.6 bpp rate for your mode (≈200 Mbps at 1080p60). An explicit bitrate is honored if you set one, but the adaptive-bitrate controller stays off either way — this codec has no useful low-rate @@ -61,6 +66,7 @@ The stats overlay shows `pyrowave` as the decode path when the mode is active. ## Current limits -- Linux host + Linux client (including docked Deck) today; the Windows host and the Apple - native port are tracked on the [roadmap](/docs/roadmap). +- Linux host + Linux client (including docked Deck) and Apple clients (native Metal decode + on Mac / Apple TV 4K / iPad) today; the Windows host is tracked on the + [roadmap](/docs/roadmap). - 8-bit SDR, 4:2:0 only. diff --git a/include/punktfunk_core.h b/include/punktfunk_core.h index 147e35aa..91268555 100644 --- a/include/punktfunk_core.h +++ b/include/punktfunk_core.h @@ -1684,6 +1684,18 @@ PunktfunkStatus punktfunk_connection_codec(PunktfunkConnection *c, uint8_t *out); #endif +#if defined(PUNKTFUNK_FEATURE_QUIC) +// Read the session's negotiated wire shard payload (the `Welcome`'s value, bytes). This is the +// parse-window size of a [`USER_FLAG_CHUNK_ALIGNED`] AU (PyroWave datagram-aligned mode, +// design/pyrowave-codec-plan.md §4.4): every `shard_payload`-sized window of the frame buffer +// starts a fresh self-delimiting chunk. Clients that decode PyroWave natively (the Apple Metal +// port) need it to walk those AUs; other codecs never need this. +// +// # Safety +// `c` is a valid connection handle; `out` is NULL or writable for one `u32`. +PunktfunkStatus punktfunk_connection_shard_payload(PunktfunkConnection *c, uint32_t *out); +#endif + #if defined(PUNKTFUNK_FEATURE_QUIC) // Send one input event to the host as a QUIC datagram (non-blocking enqueue). //