| 353 | class Gamma8Impl : public Gamma8 { |
| 354 | public: |
| 355 | explicit Gamma8Impl(float gamma) { |
| 356 | // i=0 is mathematically exact regardless of gamma: pow(0, any) = 0. |
| 357 | // The s8x24::pow short-circuit covers exact 0 input, but we set it |
| 358 | // here directly anyway because (a) the loop below avoids the |
| 359 | // round-trip math and (b) `mLut` is otherwise uninitialized. |
| 360 | // i=255 used to need a workaround for the log2(1+t) endpoint |
| 361 | // residual; that snap is now handled inside s8x24::pow itself |
| 362 | // (see #2969). |
| 363 | mLut[0] = 0; |
| 364 | // Compute the 256-entry u16 gamma LUT in fixed-point so we don't |
| 365 | // pull `__ieee754_pow` (libm, ~2.7 KB) into release builds — the |
| 366 | // double-precision pow chain dominates the top-9 bytes attributed |
| 367 | // to libm in the post-#2908 ESP32-S3 NEOPIXEL Blink audit |
| 368 | // (see #2886 / #2910). |
| 369 | // |
| 370 | // `s8x24` is 8-integer + 24-fractional bits (same 32-bit storage as |
| 371 | // s16x16, but 256× the sub-LSB resolution). Both log2_fp/exp2_fp use |
| 372 | // the same 4-term minimax polynomial, but s8x24 carries the full |
| 373 | // 24-bit intermediate precision end-to-end instead of truncating |
| 374 | // back to 16 frac bits at each stage — bringing the runtime output |
| 375 | // within ~1 LSB of true float pow() at the u16 output. Combined |
| 376 | // with the special-case for gamma=2.8 in Gamma8::getOrCreate(), |
| 377 | // this closes the divergence with the precomputed GAMMA_2_8_LUT |
| 378 | // (see #2963 audit + ucs7604 "default gamma 2.8" subcase). |
| 379 | // |
| 380 | // Bit budget check: max intermediate is exp*log2_fp(1/255) = |
| 381 | // 16 * -7.994 ≈ -127.9, fits in s8x24's signed [-128, 128) range. |
| 382 | // (GammaKey caps user gamma at 16.) |
| 383 | // |
| 384 | // libm-free: log2_fp/exp2_fp are pure integer-polynomial impls. |
| 385 | // The only float kept is the one-shot s8x24(gamma) constructor |
| 386 | // call (pulls __mulsf3 / __fixsfsi helpers, both << 100 B). |
| 387 | const fl::s8x24 gamma_fp(gamma); |
| 388 | constexpr fl::s8x24 inv_255_fp(1.0f / 255.0f); |
| 389 | for (int i = 1; i < 256; ++i) { |
| 390 | const fl::s8x24 x = static_cast<i32>(i) * inv_255_fp; // (0, 1] |
| 391 | const fl::s8x24 r = fl::s8x24::pow(x, gamma_fp); // (0, 1] |
| 392 | // r.raw() is the s8x24 raw with FRAC_BITS=24, range [0, 2^24]. |
| 393 | // Scale to u16 [0, 65535] with round-half-up. 24+16 bit |
| 394 | // multiplication needs a u64 intermediate: |
| 395 | // result = ((u64)raw * 65535 + (1<<23)) >> 24 |
| 396 | const fl::u64 scaled = |
| 397 | (static_cast<fl::u64>(static_cast<fl::u32>(r.raw())) * 65535ull |
| 398 | + (1ull << 23)) >> 24; |
| 399 | mLut[i] = static_cast<u16>(scaled > 65535u ? 65535u : scaled); |
| 400 | } |
| 401 | } |
| 402 | |
| 403 | // Construct a Gamma8Impl by copying a precomputed PROGMEM LUT directly |
| 404 | // into mLut. Used by Gamma8::getOrCreate(2.8f) to alias GAMMA_2_8_LUT, |