perf(convert): linear→PQ16 kernel is scalar; f32 path is SIMD — vectorize via linear_to_pq_x16
- Dominant language
- Rust
- Stars
- 1
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
In the conversion pipeline, **linear-f32 → PQ-u16 is scalar per-element**, while the **linear-f32 → PQ-f32** path is fully SIMD. The vectorized PQ kernel already exists and is used by the f32 path — the u16 path just doesn't call it.
## Evidence
`zenpixels-convert/src/convert_kernels.rs`:
- `linear_f32_to_pq_u16_inner` — loops `linear_srgb::tf::linear_to_pq(s[i])` **per element** (the `chunks_exact(16)` is only unrolling, not vectorization).
- `linear_f32_to_pq_f32` (a few lines down) — calls `linear_srgb::default::linear_to_pq_slice`, which **is** SIMD: `incant!([v4, v3, neon, wasm128, scalar])` over `f32x16` via `tf::pq::linear_to_pq_x16` (`linear-srgb/src/simd.rs:1640`; dedicated `linear_to_pq_slice_v3` at `tokens/x8.rs:625`).
So every `Linear → RGB16_BT2100_PQ` conversion (the common HDR intermediate) runs the transfer scalar and leaves the existing AVX-512/AVX2/SSE/NEON/WASM PQ kernel unused.
## Fix
Vectorize `linear_f32_to_pq_u16_inner`: apply `linear_to_pq_x16` to `f32x16` chunks (clamp `≥0`), scale to the u16 code range, and pack — i.e. a `linear_to_pq_u16_slice` analog of the existing `linear_to_pq_slice`. Likely belongs in `linear-srgb` (where the SIMD PQ + its tier dispatch live) so the convert kernel stays a thin call, mirroring the f32 path.
Worth checking the same asymmetry on the **inverse** (`PqU16ToLinearF32`) and the **HLG** u16 paths while here.
## Notes
- Non-API, internal-only; no public surface change.
- **Benchmark before/after with zenbench** (don't assume a speedup magnitude) — the win scales with HDR throughput at 16-bit.
- Surfaced during the HDR API audit (PR #41); recorded there in the commit message and CHANGELOG `[Unreleased]` note.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.