// PyroWave decode compute kernels — the Metal port of the vendored Vulkan shaders // (crates/pyrowave-sys/vendor/pyrowave/shaders/wavelet_dequant.comp + idwt.comp, upstream pin // 509e4f88, MIT © 2025 Hans-Kristian Arntzen). Runtime-compiled Swift strings per client // convention (no metallib build step — see GamepadChrome.swift's rationale); these are the // client's first compute pipelines. // // Port notes (design/pyrowave-codec-plan.md §4.7): // • Only the STORAGE_MODE 0 path exists: MSL device pointers replace the 8/16-bit-storage SSBO // aliases; the texel-buffer (mode 1) and linear-image (mode 2) fallbacks are non-Apple IHV // workarounds and are dropped, as is the fragment-iDWT path (Mali/Adreno only). // • Subgroup ops map 1:1: subgroupInclusiveAdd → simd_prefix_inclusive_sum, and the fixed // 32-wide Apple simdgroups take the GLSL's `SubgroupSize <= 32` scan branch; the shuffle-up // and LDS fallbacks for exotic wave sizes are dead code here. The dequant kernel needs the // 16 header lanes inside ONE simdgroup — MetalWaveletDecoder's probe enforces // threadExecutionWidth >= 16. // • Precision matches upstream's desktop default (PYROWAVE_PRECISION=1): float arithmetic, // half2 threadgroup storage; the coefficient textures are R16Float for DWT levels 0–1 and // R32Float for levels 2–4 (the low-res levels feed long reconstruction chains — upstream // keeps them fp32 for exactly that reason). // • The gather + mirrored-repeat addressing in idwt is the precision-sensitive spot (upstream // fought a Mali compiler bug there); the golden-frame PSNR fixtures are the guard. import Foundation let waveletShaderSource = """ #include using namespace metal; // --------------------------------------------------------------------------------------------- // Shared helpers (dwt_swizzle.h / constants.h / dwt_quant_scale.h) // --------------------------------------------------------------------------------------------- static inline int2 unswizzle8x8(uint index) { uint y = extract_bits(index, 0, 1); uint x = extract_bits(index, 1, 2); y |= extract_bits(index, 3, 2) << 1; x |= extract_bits(index, 5, 1) << 2; return int2(int(x), int(y)); } // GLSL bitfieldExtract(x, 0, n) where n may be 0; MSL extract_bits(bits=0) is not guaranteed // to return 0, so mask explicitly. static inline uint mask_lo(uint x, int n) { return (n <= 0) ? 0u : (x & (0xffffffffu >> (32 - n))); } // pyrowave_common.hpp decode_quant: custom FP formulation, MaxScaleExp = 4. static inline float decode_quant(uint quant_code) { int e = 4 - int(quant_code >> 3); int m = int(quant_code) & 0x7; return (1.0f / (8.0f * 1024.0f * 1024.0f)) * float((8 + m) * (1 << (20 + e))); } // dwt_quant_scale.h: per-8x8 quant scale, min 0.25, max ~2.2. static inline float decode_quant_scale(uint code) { return float(code) / 8.0f + 0.25f; } // constants.h constant int QUANT_SCALE_OFFSET = 20; constant int QUANT_SCALE_BITS = 4; // --------------------------------------------------------------------------------------------- // wavelet_dequant — one 128-thread threadgroup decodes one 32x32 coefficient block // --------------------------------------------------------------------------------------------- struct DequantRegisters { int2 resolution; int output_layer; int block_offset_32x32; int block_stride_32x32; }; struct DecodedPair { float4 col0; float4 col1; }; // GLSL mat2x4: m[j][i] -> colJ[i] // Bit-plane magnitude decode for one thread's 4x2 coefficient group (decode_payload in the // GLSL). `code_word` is the 8x8 block's 16-bit control word (2 bits of extra planes per 4x2 // group), `q_bits` the base plane count, `offset` the block's plane-payload start byte, // `block_index` this thread's group (0..7). Nonzero magnitudes get the +0.5 deadzone // reconstruction bias. static DecodedPair decode_payload(const device uchar *payload_u8, uint code_word, uint q_bits, uint offset, uint block_index) { DecodedPair m; m.col0 = float4(0.0f); m.col1 = float4(0.0f); if (code_word == 0) return m; int bit_offset = 2 * int(block_index); uint lsbs = code_word & 0x5555u; uint msbs = code_word & 0xaaaau; uint msbs_shift = msbs >> 1; msbs |= msbs_shift; uint byte_offset = popcount(mask_lo(lsbs, bit_offset)) + popcount(mask_lo(msbs, bit_offset)) + q_bits * block_index + offset; uint payload = uint(payload_u8[byte_offset]); uint local_control_word = extract_bits(code_word, uint(bit_offset), 2); int decoded_abs[8] = {0, 0, 0, 0, 0, 0, 0, 0}; int plane_iterations = int(q_bits + local_control_word); for (int q = plane_iterations - 1; q >= 0; q--) { for (int b = 0; b < 8; b++) { int decoded = int(extract_bits(payload, uint(b), 1)); decoded_abs[b] = insert_bits(decoded_abs[b], decoded, uint(q), 1); } byte_offset++; payload = uint(payload_u8[byte_offset]); } for (int i = 0; i < 4; i++) { for (int j = 0; j < 2; j++) { float v = float(decoded_abs[i * 2 + j]); if (v != 0.0f) v += 0.5f; if (j == 0) m.col0[i] = v; else m.col1[i] = v; } } return m; } kernel void wavelet_dequant( texture2d_array uDequantImg [[texture(0)]], const device uint *payload_offsets [[buffer(0)]], const device uint *payload_u32 [[buffer(1)]], constant DequantRegisters ®isters [[buffer(2)]], uint3 wg_id [[threadgroup_position_in_grid]], uint local_index [[thread_index_in_threadgroup]], uint simd_lane [[thread_index_in_simdgroup]], uint simd_group [[simdgroup_index_in_threadgroup]], uint simd_size [[threads_per_simdgroup]]) { // STORAGE_MODE 0's three aliased SSBO views over one buffer, as typed pointers. const device ushort *payload_u16 = reinterpret_cast(payload_u32); const device uchar *payload_u8 = reinterpret_cast(payload_u32); threadgroup uint shared_sign_offset; threadgroup uint shared_plane_byte_offsets[16]; threadgroup uint shared_sign_scan[128 / 4]; int block_index_32x32 = int(uint(registers.block_offset_32x32) + wg_id.y * uint(registers.block_stride_32x32) + wg_id.x); uint block_local_index = extract_bits(local_index, 0, 3); uint block_x = extract_bits(local_index, 3, 2); uint block_y = extract_bits(local_index, 5, 2); uint linear_block = block_y * 4 + block_x; // Each thread individually decodes 8 values (a 4x2 group of its 8x8 block). int2 local_coord = unswizzle8x8(block_local_index << 3); int2 coord = int2(wg_id.xy) * 32; coord += 8 * int2(int(block_x), int(block_y)); coord += local_coord; uint offset_u32 = payload_offsets[block_index_32x32]; // Missing / lost block: zero coefficients (this is how a partial frame's holes decode). if (offset_u32 == ~0u) { for (int j = 0; j < 2; j++) for (int i = 0; i < 4; i++) uDequantImg.write(float4(0.0f), uint2(coord + int2(i, j)), uint(registers.output_layer)); return; } uint ballot = payload_u32[offset_u32] & 0xffffu; uint q_code = payload_u32[offset_u32 + 1] & 0xffu; // Threads 0..15 (one per 8x8 block, all inside simdgroup 0) prefix-scan the per-block // plane-payload byte costs into shared_plane_byte_offsets, and lane 15 records where the // sign bitstream starts. if (local_index < 16) { uint control_word = 0; uint q_bits = 0; if (extract_bits(ballot, local_index, 1) != 0) { uint local_code_offset = popcount(mask_lo(ballot, int(local_index))); control_word = uint(payload_u16[offset_u32 * 2 + 4 + local_code_offset]); q_bits = uint(payload_u8[offset_u32 * 4 + 8 + popcount(ballot) * 2 + local_code_offset]) & 0xfu; } uint lsbs = control_word & 0x5555u; uint msbs = control_word & 0xaaaau; uint msbs_shift = msbs >> 1; msbs |= msbs_shift; uint byte_cost = popcount(lsbs) + popcount(msbs) + q_bits * 8; uint byte_scan = offset_u32 * 4 + 8 + 3 * popcount(ballot) + simd_prefix_inclusive_sum(byte_cost); if (local_index == 15) shared_sign_offset = 8 * byte_scan; shared_plane_byte_offsets[local_index] = byte_scan - byte_cost; } threadgroup_barrier(mem_flags::mem_threadgroup); DecodedPair v; int significant_count; if (extract_bits(ballot, linear_block, 1) != 0) { uint local_code_offset = popcount(mask_lo(ballot, int(linear_block))); uint control_word = uint(payload_u16[offset_u32 * 2 + 4 + local_code_offset]); uint control_word2 = uint(payload_u8[offset_u32 * 4 + 8 + popcount(ballot) * 2 + local_code_offset]); v = decode_payload(payload_u8, control_word, control_word2 & 0xfu, shared_plane_byte_offsets[linear_block], block_local_index); significant_count = 0; for (int j = 0; j < 2; j++) for (int i = 0; i < 4; i++) significant_count += int(((j == 0) ? v.col0[i] : v.col1[i]) != 0.0f); float q = decode_quant(q_code); float inv_scale = q * decode_quant_scale(extract_bits(control_word2, uint(QUANT_SCALE_OFFSET - 16), uint(QUANT_SCALE_BITS))); v.col0 *= inv_scale; v.col1 *= inv_scale; } else { v.col0 = float4(0.0f); v.col1 = float4(0.0f); significant_count = 0; } // Cross-threadgroup scan of significant-coefficient counts → each thread's first sign-bit // position. Apple simdgroups are >= 16 wide, so this is the GLSL's `SubgroupSize <= 32` // branch; the shuffle/LDS fallbacks are unnecessary. int significant_scan = int(simd_prefix_inclusive_sum(uint(significant_count))); if (simd_lane == simd_size - 1) shared_sign_scan[simd_group] = uint(significant_scan); threadgroup_barrier(mem_flags::mem_threadgroup); uint num_simdgroups = (128 + simd_size - 1) / simd_size; if (local_index < num_simdgroups) shared_sign_scan[local_index] = simd_prefix_inclusive_sum(shared_sign_scan[local_index]); threadgroup_barrier(mem_flags::mem_threadgroup); uint sign_offset = shared_sign_offset + uint(significant_scan - significant_count); if (simd_group != 0) sign_offset += shared_sign_scan[simd_group - 1]; // Load 64 bits of sign stream and bit-align (may read one word past the payload — the // buffer carries a 16-byte zeroed guard tail for exactly this). uint sign_word = payload_u32[sign_offset / 32 + 0]; uint sign_word_upper = payload_u32[sign_offset / 32 + 1]; uint masked_sign_offset = sign_offset & 31u; if (masked_sign_offset != 0) { sign_word >>= masked_sign_offset; sign_word |= sign_word_upper << (32 - masked_sign_offset); } int sign_counter = 0; for (int i = 0; i < 4; i++) { for (int j = 0; j < 2; j++) { float val = (j == 0) ? v.col0[i] : v.col1[i]; if (val != 0.0f) { val *= 1.0f - 2.0f * float(extract_bits(sign_word, uint(sign_counter), 1)); sign_counter++; if (j == 0) v.col0[i] = val; else v.col1[i] = val; } } } for (int j = 0; j < 2; j++) for (int i = 0; i < 4; i++) uDequantImg.write(float4((j == 0) ? v.col0[i] : v.col1[i]), uint2(coord + int2(i, j)), uint(registers.output_layer)); } // --------------------------------------------------------------------------------------------- // idwt — inverse CDF 9/7; one 64-thread threadgroup reconstructs one 32x32 output tile from the // four half-res band layers (LL/HL/LH/HH), with a 4-sample mirror apron. The caller passes the // band-image resolution TRANSPOSED (the kernel transposes on load and store, so one kernel does // both the horizontal and vertical passes). // --------------------------------------------------------------------------------------------- constant bool DCShift [[function_constant(0)]]; struct IdwtRegisters { int2 resolution; float2 inv_resolution; }; constant int APRON = 4; constant int APRON_HALF = APRON / 2; constant int BLOCK_SIZE = 32; constant int BLOCK_SIZE_HALF = BLOCK_SIZE >> 1; // CDF 9/7 lifting constants (dwt_common.h). constant float ALPHA = -1.586134342059924f; constant float BETA = -0.052980118572961f; constant float GAMMA = 0.882911075530934f; constant float DELTA = 0.443506852043971f; constant float K = 1.230174104914001f; constant float inv_K = 1.0f / 1.230174104914001f; constant int SHARED_ROWS = (BLOCK_SIZE + 2 * APRON) / 2; // 20 constant int SHARED_COLS = (BLOCK_SIZE + 2 * APRON) + 1; // 41 (+1 avoids bank conflicts) static inline float2 load_shared(threadgroup half2 (&blk)[SHARED_ROWS][SHARED_COLS], int y, int x) { return float2(blk[y][x]); } static inline void store_shared(threadgroup half2 (&blk)[SHARED_ROWS][SHARED_COLS], int y, int x, float2 v) { blk[y][x] = half2(v); } // Even/odd-phase coordinate nudge so mirrored-repeat gather reproduces JPEG2000 whole-sample // mirroring at the image borders, then transpose (uv.yx) on load. static inline float2 generate_mirror_uv(int2 coord, bool even_x, bool even_y, int2 resolution, float2 inv_resolution) { coord.x -= int(even_x && coord.x < 0); coord.y -= int(even_y && coord.y < 0); coord += 1; coord.x += int(!even_x && coord.x >= resolution.x); coord.y += int(!even_y && coord.y >= resolution.y); float2 uv = float2(coord) * inv_resolution; return uv.yx; } static inline void write_shared_4x4(threadgroup half2 (&blk)[SHARED_ROWS][SHARED_COLS], int2 coord, float4 t0, float4 t1, float4 t2, float4 t3) { store_shared(blk, coord.y + 0, 2 * coord.x + 0, float2(t0.x, t2.x)); store_shared(blk, coord.y + 0, 2 * coord.x + 1, float2(t1.x, t3.x)); store_shared(blk, coord.y + 0, 2 * coord.x + 2, float2(t0.y, t2.y)); store_shared(blk, coord.y + 0, 2 * coord.x + 3, float2(t1.y, t3.y)); store_shared(blk, coord.y + 1, 2 * coord.x + 0, float2(t0.z, t2.z)); store_shared(blk, coord.y + 1, 2 * coord.x + 1, float2(t1.z, t3.z)); store_shared(blk, coord.y + 1, 2 * coord.x + 2, float2(t0.w, t2.w)); store_shared(blk, coord.y + 1, 2 * coord.x + 3, float2(t1.w, t3.w)); } // textureGather(...).wxzy — Metal's gather returns the same counter-clockwise-from-(i0,j1) // component order as Vulkan, so the reorder is identical. static inline float4 gather_layer(texture2d_array tex, sampler smp, float2 uv, uint layer) { float4 g = tex.gather(smp, uv, layer); return float4(g.w, g.x, g.z, g.y); } static void load_image_with_apron(texture2d_array tex, sampler smp, threadgroup half2 (&blk)[SHARED_ROWS][SHARED_COLS], uint local_index, uint2 wg_id, int2 resolution, float2 inv_resolution) { int2 base_coord = int2(wg_id) * BLOCK_SIZE_HALF - APRON_HALF; int2 local_coord0 = 2 * unswizzle8x8(local_index); int2 coord0 = base_coord + local_coord0; // Band layers gathered in 0/2/1/3 order (LL/LH/HL/HH interleave for the 2x2 scatter). float4 texels0 = gather_layer(tex, smp, generate_mirror_uv(coord0, true, true, resolution, inv_resolution), 0); float4 texels1 = gather_layer(tex, smp, generate_mirror_uv(coord0, false, true, resolution, inv_resolution), 2); float4 texels2 = gather_layer(tex, smp, generate_mirror_uv(coord0, true, false, resolution, inv_resolution), 1); float4 texels3 = gather_layer(tex, smp, generate_mirror_uv(coord0, false, false, resolution, inv_resolution), 3); write_shared_4x4(blk, local_coord0, texels0, texels1, texels2, texels3); int2 local_coord_horiz = int2(BLOCK_SIZE_HALF + 2 * int(local_index % 2u), 2 * int(local_index / 2u)); if (local_coord_horiz.y < BLOCK_SIZE_HALF + 2 * APRON_HALF) { int2 c = base_coord + local_coord_horiz; texels0 = gather_layer(tex, smp, generate_mirror_uv(c, true, true, resolution, inv_resolution), 0); texels1 = gather_layer(tex, smp, generate_mirror_uv(c, false, true, resolution, inv_resolution), 2); texels2 = gather_layer(tex, smp, generate_mirror_uv(c, true, false, resolution, inv_resolution), 1); texels3 = gather_layer(tex, smp, generate_mirror_uv(c, false, false, resolution, inv_resolution), 3); write_shared_4x4(blk, local_coord_horiz, texels0, texels1, texels2, texels3); } int2 local_coord_vert = local_coord_horiz.yx; if (local_coord_vert.x < BLOCK_SIZE_HALF) { int2 c = base_coord + local_coord_vert; texels0 = gather_layer(tex, smp, generate_mirror_uv(c, true, true, resolution, inv_resolution), 0); texels1 = gather_layer(tex, smp, generate_mirror_uv(c, false, true, resolution, inv_resolution), 2); texels2 = gather_layer(tex, smp, generate_mirror_uv(c, true, false, resolution, inv_resolution), 1); texels3 = gather_layer(tex, smp, generate_mirror_uv(c, false, false, resolution, inv_resolution), 3); write_shared_4x4(blk, local_coord_vert, texels0, texels1, texels2, texels3); } threadgroup_barrier(mem_flags::mem_threadgroup); } static void inverse_transform8x2(threadgroup half2 (&blk)[SHARED_ROWS][SHARED_COLS], uint local_index) { const int SIZE = 8; const int PADDED_SIZE = SIZE + 2 * APRON; const int PADDED_SIZE_HALF = PADDED_SIZE / 2; float2 values[PADDED_SIZE]; int2 local_coord = int2(8 * int(local_index % 4u), int(local_index / 4u)); for (int i = 0; i < PADDED_SIZE; i += 2) { float2 v0 = load_shared(blk, local_coord.y, local_coord.x + i + 0); float2 v1 = load_shared(blk, local_coord.y, local_coord.x + i + 1); values[i + 0] = v0 * K; values[i + 1] = v1 * inv_K; } // CDF 9/7 inverse lifting steps. for (int i = 2; i < PADDED_SIZE - 1; i += 2) values[i] -= DELTA * (values[i - 1] + values[i + 1]); for (int i = 3; i < PADDED_SIZE - 2; i += 2) values[i] -= GAMMA * (values[i - 1] + values[i + 1]); for (int i = 4; i < PADDED_SIZE - 3; i += 2) values[i] -= BETA * (values[i - 1] + values[i + 1]); for (int i = 5; i < PADDED_SIZE - 4; i += 2) values[i] -= ALPHA * (values[i - 1] + values[i + 1]); // Avoid WAR hazard. threadgroup_barrier(mem_flags::mem_threadgroup); for (int i = APRON_HALF; i < PADDED_SIZE_HALF - APRON_HALF; i++) { float2 a = values[2 * i + 0]; float2 b = values[2 * i + 1]; // Transpose the 2x2 block, transpose write. float2 t0 = float2(a.x, b.x); float2 t1 = float2(a.y, b.y); int y_coord = (local_coord.x >> 1) + (i - APRON_HALF); store_shared(blk, y_coord, 2 * local_coord.y + 0, t0); store_shared(blk, y_coord, 2 * local_coord.y + 1, t1); } } static void inverse_transform4x2(threadgroup half2 (&blk)[SHARED_ROWS][SHARED_COLS], uint local_index, bool active_lane, int y_offset) { const int SIZE = 4; const int PADDED_SIZE = SIZE + 2 * APRON; const int PADDED_SIZE_HALF = PADDED_SIZE / 2; float2 values[PADDED_SIZE]; int2 local_coord = int2(4 * int(local_index % 8u), int(local_index / 8u) + y_offset); if (active_lane) { for (int i = 0; i < PADDED_SIZE; i += 2) { float2 v0 = load_shared(blk, local_coord.y, local_coord.x + i + 0); float2 v1 = load_shared(blk, local_coord.y, local_coord.x + i + 1); values[i + 0] = v0 * K; values[i + 1] = v1 * inv_K; } for (int i = 2; i < PADDED_SIZE - 1; i += 2) values[i] -= DELTA * (values[i - 1] + values[i + 1]); for (int i = 3; i < PADDED_SIZE - 2; i += 2) values[i] -= GAMMA * (values[i - 1] + values[i + 1]); for (int i = 4; i < PADDED_SIZE - 3; i += 2) values[i] -= BETA * (values[i - 1] + values[i + 1]); for (int i = 5; i < PADDED_SIZE - 4; i += 2) values[i] -= ALPHA * (values[i - 1] + values[i + 1]); } threadgroup_barrier(mem_flags::mem_threadgroup); if (active_lane) { for (int i = APRON_HALF; i < PADDED_SIZE_HALF - APRON_HALF; i++) { float2 a = values[2 * i + 0]; float2 b = values[2 * i + 1]; float2 t0 = float2(a.x, b.x); float2 t1 = float2(a.y, b.y); int y_coord = (local_coord.x >> 1) + (i - APRON_HALF); store_shared(blk, y_coord, 2 * local_coord.y + 0, t0); store_shared(blk, y_coord, 2 * local_coord.y + 1, t1); } } } kernel void idwt( texture2d_array uTexture [[texture(0)]], texture2d uOutput [[texture(1)]], sampler uSampler [[sampler(0)]], constant IdwtRegisters ®isters [[buffer(0)]], uint3 wg_id [[threadgroup_position_in_grid]], uint local_index [[thread_index_in_threadgroup]]) { threadgroup half2 shared_block[SHARED_ROWS][SHARED_COLS]; load_image_with_apron(uTexture, uSampler, shared_block, local_index, wg_id.xy, registers.resolution, registers.inv_resolution); // Horizontal transform. inverse_transform8x2(shared_block, local_index); // Also need to transform the apron. inverse_transform4x2(shared_block, local_index, local_index < 32, BLOCK_SIZE_HALF); threadgroup_barrier(mem_flags::mem_threadgroup); // Vertical transform. inverse_transform8x2(shared_block, local_index); threadgroup_barrier(mem_flags::mem_threadgroup); int2 local_coord = unswizzle8x8(local_index); for (int y = local_coord.y; y < BLOCK_SIZE_HALF; y += 8) { for (int x = local_coord.x; x < BLOCK_SIZE; x += 8) { float2 v = load_shared(shared_block, y, x); if (DCShift) v += 0.5f; // Transposed store (wg_id.yx) — undoes the transpose-on-load; out-of-range writes // at the aligned-size overhang are dropped by Metal (matching the Vulkan behavior). int2 out0 = int2(2 * y + 0, x) + BLOCK_SIZE * int2(int(wg_id.y), int(wg_id.x)); int2 out1 = int2(2 * y + 1, x) + BLOCK_SIZE * int2(int(wg_id.y), int(wg_id.x)); uOutput.write(float4(v.x), uint2(out0)); uOutput.write(float4(v.y), uint2(out1)); } } } """