FastLED 3.10.6
Loading...
Searching...
No Matches

◆ buildGamutMapRgbwFromSolveQ16()

bool fl::buildGamutMapRgbwFromSolveQ16 ( const colorimetric_response::EmitterProfile & profile,
const EmitterSolveMatrixQ16 & solve,
const i32(&) white_xyz[3],
WhiteAllocationPolicy policy,
GamutMapRgbwQ16 * out )

Bind a four-emitter map from an effective RGB solve and a white column already adapted into the same D65 working coordinates.

Definition at line 434 of file gamut_map.cpp.hpp.

437 {
438 if (out == nullptr) {
439 return false;
440 }
441 if (!buildWhiteAllocationFromSolveQ16(profile, solve, white_xyz, policy,
442 &out->allocation)) {
443 return false;
444 }
445
446 i32 neutral_drives[3];
447 solveRgbDrivesQ16(out->allocation.rgb_solve, kGamutD65Q16, neutral_drives);
448 for (int i = 0; i < 3; ++i) {
449 // As in the three-emitter build: every drive, not just the largest.
450 if (neutral_drives[i] <= 0) {
451 return false;
452 }
453 }
454
455 // The three-emitter bound, reached with the white emitter off. Whatever
456 // else happens this neutral is attainable: at 1 / max(d0) the RGB drives
457 // are exactly in [0, 1] with no white at all.
458 i32 largest_neutral_drive = neutral_drives[0];
459 for (int i = 1; i < 3; ++i) {
460 if (neutral_drives[i] > largest_neutral_drive) {
461 largest_neutral_drive = neutral_drives[i];
462 }
463 }
464 i64 reachable = (static_cast<i64>(kGamutFullDrive) << 16) /
465 static_cast<i64>(largest_neutral_drive);
466
467 // Clamped for the same reason the three-emitter build clamps it, and to
468 // the same place: `buildRgbSolveMatrixQ16` accepts emitter luminances up
469 // to 1e6, so a profile bright enough to reach D65 on one or two raw
470 // units of drive puts `2^32 / largest` at or past i32's range -- exactly
471 // 2^31 at largest == 2. `optimistic` starts at `kOklabQ16MaxMagnitude`,
472 // so for such a profile the bisection below is skipped and the narrowing
473 // is reached directly. 64.0 is where the OKLab transform's domain ends,
474 // so nothing downstream can tell the difference.
475 if (reachable > kOklabQ16MaxMagnitude) {
476 reachable = kOklabQ16MaxMagnitude;
477 }
478
479 // An upper bound on what the white emitter can add. Along the D65 ray
480 // the RGB drives are s * d0 - w * dW, so the *upper* limit on drive i is
481 // loosest at w = 1 when dW_i is positive and at w = 0 when it is
482 // negative -- hence max(dW_i, 0). Dropping the lower limits, and letting
483 // each channel pick its own w, are both relaxations, so this is an upper
484 // bound and never an under-estimate.
485 i64 optimistic = static_cast<i64>(kOklabQ16MaxMagnitude);
486 for (int i = 0; i < 3; ++i) {
487 const i32 per_white = out->allocation.per_white[i];
488 const i64 numerator = static_cast<i64>(kGamutFullDrive) +
489 (per_white > 0 ? static_cast<i64>(per_white) : 0);
490 const i64 candidate =
491 (numerator << 16) / static_cast<i64>(neutral_drives[i]);
492 if (candidate < optimistic) {
493 optimistic = candidate;
494 }
495 }
496
497 // That upper bound is not generally attainable, and treating it as if it
498 // were is a real bug rather than a theoretical one: it enforces only the
499 // upper limits on the RGB drives, and full white can push a *different*
500 // channel negative. With dW = (0.9, 0.05, 0.05) against
501 // d0 = (0.21, 0.72, 0.07) the formula gives 1.468, where the red drive
502 // works out at 1.468 * 0.21 - 0.9 = -0.59. Storing that would leave the
503 // mapper with a zero-chroma candidate its own halving search cannot
504 // satisfy, and the fallback would then return four zero drives for a
505 // colour that is not black.
506 //
507 // So the bound is bisected between the two at bind time. This is not an
508 // iterative solver in the A3/B11 sense: it runs once per profile, never
509 // per pixel, and the per-pixel path sees only the stored result.
510 if (optimistic > reachable) {
511 i64 low = reachable;
512 i64 high = optimistic;
513 for (int step = 0; step < 24; ++step) {
514 const i64 middle = (low + high) / 2;
515 const i32 trial_scale = static_cast<i32>(middle);
516 const i32 trial[3] = {
517 scaleGamutQ16(kGamutD65Q16[0], trial_scale),
518 scaleGamutQ16(kGamutD65Q16[1], trial_scale),
519 scaleGamutQ16(kGamutD65Q16[2], trial_scale),
520 };
521 i32 trial_drives[4];
522 if (allocateEmitterDrivesQ16(out->allocation, trial, trial_drives)) {
523 low = middle;
524 } else {
525 high = middle;
526 }
527 }
528 reachable = low;
529 }
530
531 const i32 scale = static_cast<i32>(reachable);
532 const i32 brightest_neutral[3] = {
533 scaleGamutQ16(kGamutD65Q16[0], scale),
534 scaleGamutQ16(kGamutD65Q16[1], scale),
535 scaleGamutQ16(kGamutD65Q16[2], scale),
536 };
537 i32 lab[3];
538 xyzToOklabQ16(brightest_neutral, lab);
539 out->max_neutral_lightness = lab[0];
540 return true;
541}
fl::UISlider scale("Scale", 4,.1, 4,.1)
i32 scaleGamutQ16(i32 value, i32 factor) FL_NO_EXCEPT
Scale an s16.16 value by an s16.16 factor, rounding to nearest.
constexpr i32 kGamutFullDrive
Full drive, as an s16.16 raw value.
constexpr i32 kOklabQ16MaxMagnitude
Largest magnitude either direction accepts, as an s16.16 raw value (64.0).
Definition oklab_q16.h:40
bool allocateEmitterDrivesQ16(const WhiteAllocationQ16 &allocation, const i32(&xyz)[3], i32(&drives)[4]) FL_NO_EXCEPT
One pixel: XYZ in s16.16 to four drives, in the order red, green, blue, white, at whichever end of th...
void xyzToOklabQ16(const i32(&xyz)[3], i32(&out_lab)[3]) FL_NO_EXCEPT
XYZ (s16.16, D65-relative) -> OKLab (s16.16), L then a then b.
constexpr enable_if< is_fixed_point< T >::value, T >::type step(T edge, T x) FL_NO_EXCEPT
bool buildWhiteAllocationFromSolveQ16(const colorimetric_response::EmitterProfile &profile, const EmitterSolveMatrixQ16 &solve, const i32(&white_xyz)[3], WhiteAllocationPolicy policy, WhiteAllocationQ16 *out) FL_NO_EXCEPT
Bind from an RGB solve already adapted to the D65 working domain.
void solveRgbDrivesQ16(const EmitterSolveMatrixQ16 &matrix, const i32(&xyz)[3], i32(&drives)[3]) FL_NO_EXCEPT
One pixel: XYZ in s16.16 to three emitter drives in s16.16.
WhiteAllocationQ16 allocation
Definition gamut_map.h:70
i32 max_neutral_lightness
OKLab lightness of the brightest D65 neutral this device can reach.
Definition gamut_map.h:85
EmitterSolveMatrixQ16 rgb_solve
Inverse RGB emitter matrix.
fl::i64 i64
Definition stdint.h:221

References allocateEmitterDrivesQ16(), buildWhiteAllocationFromSolveQ16(), FL_NO_EXCEPT, kOklabQ16MaxMagnitude, scale, solveRgbDrivesQ16(), step(), and xyzToOklabQ16().

Referenced by buildGamutMapRgbwQ16(), and fl::anonymous_namespace{pipeline.cpp.hpp}::buildWidePipelineQ16().

+ Here is the call graph for this function:
+ Here is the caller graph for this function: