78 for (
int i = 0; i < 3; ++i) {
92 for (
int i = 0; i < 3; ++i) {
103 const i64 product =
static_cast<i64>(
value) *
static_cast<i64>(factor);
104 return static_cast<i32
>((product + 32768) >> 16);
110 const i32 candidate_lab[3] = {
155template <
typename Feasible>
161 const i32 factor = (low + high) >> 1;
162 i32 candidate_xyz[3];
164 if (feasible(candidate_xyz)) {
186template <
typename Feasible>
188 Feasible feasible, i32* out_low,
193 const i32 factor =
static_cast<i32
>(
195 i32 candidate_xyz[3];
197 if (feasible(candidate_xyz)) {
212 const i32 factor = low + ((high - low) >> 1);
213 i32 candidate_xyz[3];
215 if (feasible(candidate_xyz)) {
227 const i32 factor = low + ((high - low) >> 1);
228 i32 candidate_xyz[3];
230 if (feasible(candidate_xyz)) {
257template <
typename Feasible>
261 i32 high = requested;
263 const i32 middle = low + ((high - low) >> 1);
291 if (out ==
nullptr) {
302 if (out ==
nullptr)
return false;
308 i32 neutral_drives[3];
310 i32 largest = neutral_drives[0];
311 for (
int i = 0; i < 3; ++i) {
318 if (neutral_drives[i] <= 0) {
321 if (neutral_drives[i] > largest) {
322 largest = neutral_drives[i];
338 i64 scale_wide = (
static_cast<i64>(kGamutFullDrive) << 16) /
339 static_cast<i64>(largest);
343 const i32
scale =
static_cast<i32
>(scale_wide);
344 const i32 brightest_neutral[3] = {
345 scaleGamutQ16(kGamutD65Q16[0],
scale),
346 scaleGamutQ16(kGamutD65Q16[1],
scale),
347 scaleGamutQ16(kGamutD65Q16[2],
scale),
351 out->max_neutral_lightness = lab[0];
358 if (gamutDrivesAreInRange(drives, kGamutFeasibilitySlack)) {
361 clampGamutDrives(drives);
368 i32 lightness = lab[0];
378 if (lightness >
map.max_neutral_lightness) {
381 if (feasibleChromaInterval(lab, lightness, RgbFeasible{
map.solve},
382 &low_edge, &high_edge)) {
383 i32 candidate_xyz[3];
384 chromaCandidateXyz(lab, lightness, clampChromaFactor(low_edge, high_edge),
388 if (gamutDrivesAreInRange(candidate, kGamutFeasibilitySlack)) {
389 for (
int i = 0; i < 3; ++i) {
390 drives[i] = candidate[i];
392 clampGamutDrives(drives);
400 const i32 reachable = highestReachableLightness(
401 lab,
map.max_neutral_lightness, lightness, RgbFeasible{map.solve});
404 if (reachable >
map.max_neutral_lightness &&
405 feasibleChromaInterval(lab, reachable, RgbFeasible{
map.solve},
406 &edge_low, &edge_high)) {
407 i32 candidate_xyz[3];
408 chromaCandidateXyz(lab, reachable,
409 clampChromaFactor(edge_low, edge_high),
413 if (gamutDrivesAreInRange(candidate, kGamutFeasibilitySlack)) {
414 for (
int i = 0; i < 3; ++i) {
415 drives[i] = candidate[i];
417 clampGamutDrives(drives);
423 lightness =
map.max_neutral_lightness;
427 const i32 factor = largestFeasibleChroma(lab, lightness, RgbFeasible{
map.solve});
429 chromaCandidateXyz(lab, lightness, factor, mapped_xyz);
431 clampGamutDrives(drives);
438 if (out ==
nullptr) {
446 i32 neutral_drives[3];
448 for (
int i = 0; i < 3; ++i) {
450 if (neutral_drives[i] <= 0) {
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];
464 i64 reachable = (
static_cast<i64>(kGamutFullDrive) << 16) /
465 static_cast<i64>(largest_neutral_drive);
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;
510 if (optimistic > reachable) {
512 i64 high = optimistic;
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),
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),
539 out->max_neutral_lightness = lab[0];
553 i32 lightness = lab[0];
561 if (lightness >
map.max_neutral_lightness) {
564 if (feasibleChromaInterval(lab, lightness, RgbwFeasible{
map.allocation},
565 &low_edge, &high_edge)) {
566 i32 candidate_xyz[3];
567 chromaCandidateXyz(lab, lightness, clampChromaFactor(low_edge, high_edge),
576 const i32 reachable = highestReachableLightness(
577 lab,
map.max_neutral_lightness, lightness,
578 RgbwFeasible{map.allocation});
581 if (reachable >
map.max_neutral_lightness &&
582 feasibleChromaInterval(lab, reachable, RgbwFeasible{
map.allocation},
583 &edge_low, &edge_high)) {
585 chromaCandidateXyz(lab, reachable,
586 clampChromaFactor(edge_low, edge_high), edge_xyz);
591 lightness =
map.max_neutral_lightness;
595 largestFeasibleChroma(lab, lightness, RgbwFeasible{
map.allocation});
597 chromaCandidateXyz(lab, lightness, factor, mapped_xyz);
604 const i32 neutral_lab[3] = {lightness, 0, 0};
616 const i32 (&white_xyz)[3],
630 if (out ==
nullptr) {
634 policy, &out->allocation)) {
638 i32 neutral_drives[3];
640 for (
int i = 0; i < 3; ++i) {
642 if (neutral_drives[i] <= 0) {
649 i32 largest_neutral_drive = neutral_drives[0];
650 for (
int i = 1; i < 3; ++i) {
651 if (neutral_drives[i] > largest_neutral_drive) {
652 largest_neutral_drive = neutral_drives[i];
655 i64 reachable = (
static_cast<i64>(kGamutFullDrive) << 16) /
656 static_cast<i64>(largest_neutral_drive);
681 for (
int i = 0; i < 3; ++i) {
682 const i32 per_white2 = out->allocation.per_white2[i];
683 const i32 per_white1 = out->allocation.difference[i] + per_white2;
684 i64 numerator =
static_cast<i64>(kGamutFullDrive);
685 if (per_white1 > 0) {
686 numerator +=
static_cast<i64>(per_white1);
688 if (per_white2 > 0) {
689 numerator +=
static_cast<i64>(per_white2);
691 const i64 candidate =
692 (numerator << 16) /
static_cast<i64>(neutral_drives[i]);
693 if (candidate < optimistic) {
694 optimistic = candidate;
702 if (optimistic > reachable) {
704 i64 high = optimistic;
706 const i64 middle = (low + high) / 2;
707 const i32 trial_scale =
static_cast<i32
>(middle);
708 const i32 trial[3] = {
709 scaleGamutQ16(kGamutD65Q16[0], trial_scale),
710 scaleGamutQ16(kGamutD65Q16[1], trial_scale),
711 scaleGamutQ16(kGamutD65Q16[2], trial_scale),
723 const i32
scale =
static_cast<i32
>(reachable);
724 const i32 brightest_neutral[3] = {
725 scaleGamutQ16(kGamutD65Q16[0],
scale),
726 scaleGamutQ16(kGamutD65Q16[1],
scale),
727 scaleGamutQ16(kGamutD65Q16[2],
scale),
731 out->max_neutral_lightness = lab[0];
736 const i32 (&white1_xyz)[3],
737 const i32 (&white2_xyz)[3],
756 i32 lightness = lab[0];
762 if (lightness >
map.max_neutral_lightness) {
765 if (feasibleChromaInterval(lab, lightness, RgbwwFeasible{
map.allocation},
766 &low_edge, &high_edge)) {
767 i32 candidate_xyz[3];
768 chromaCandidateXyz(lab, lightness, clampChromaFactor(low_edge, high_edge),
777 const i32 reachable = highestReachableLightness(
778 lab,
map.max_neutral_lightness, lightness,
779 RgbwwFeasible{map.allocation});
782 if (reachable >
map.max_neutral_lightness &&
783 feasibleChromaInterval(lab, reachable, RgbwwFeasible{
map.allocation},
784 &edge_low, &edge_high)) {
786 chromaCandidateXyz(lab, reachable,
787 clampChromaFactor(edge_low, edge_high), edge_xyz);
792 lightness =
map.max_neutral_lightness;
796 largestFeasibleChroma(lab, lightness, RgbwwFeasible{
map.allocation});
798 chromaCandidateXyz(lab, lightness, factor, mapped_xyz);
805 const i32 neutral_lab[3] = {lightness, 0, 0};
809 for (
int i = 0; i < 5; ++i) {
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 kGamutFeasibilitySlack
Slack allowed on the drive bounds, in s16.16 raw units.
bool gamutDrivesAreInRange(const i32(&drives)[3], i32 slack) FL_NO_EXCEPT
True when every drive is inside [0, 1], allowing slack ULP either side.
constexpr int kGamutMapProbes
Probes used to find a seed inside the feasible chroma interval when the target is brighter than the d...
constexpr i32 kGamutFullDrive
Full drive, as an s16.16 raw value.
void clampGamutDrives(i32(&drives)[3]) FL_NO_EXCEPT
Clamp drives into [0, 1].
i32 clampChromaFactor(i32 low, i32 high) FL_NO_EXCEPT
The target's chroma clamped into [low, high], in factor space.
constexpr i32 kGamutD65Q16[3]
D65 in s16.16, the white the working domain is normalized to.
void chromaCandidateXyz(const i32(&lab)[3], i32 lightness, i32 factor, i32(&out_xyz)[3]) FL_NO_EXCEPT
XYZ of the candidate at factor of the target's chroma.
i32 largestFeasibleChroma(const i32(&lab)[3], i32 lightness, Feasible feasible) FL_NO_EXCEPT
Largest chroma factor the hull accepts, by a fixed count of halvings.
i32 highestReachableLightness(const i32(&lab)[3], i32 cap, i32 requested, Feasible feasible) FL_NO_EXCEPT
The highest lightness at or below requested whose feasible chroma interval the probe scan can still f...
bool feasibleChromaInterval(const i32(&lab)[3], i32 lightness, Feasible feasible, i32 *out_low, i32 *out_high) FL_NO_EXCEPT
The feasible chroma interval at the target's own lightness, as factors of the target's chroma.
constexpr i32 kGamutProbeCeilingQ16
How far above the target's own chroma the probes reach, in Q16.
bool buildGamutMapRgbwFromSolveQ16(const colorimetric_response::EmitterProfile &profile, const EmitterSolveMatrixQ16 &solve, const i32(&white_xyz)[3], WhiteAllocationPolicy policy, GamutMapRgbwQ16 *out) FL_NO_EXCEPT
Bind a four-emitter map from an effective RGB solve and a white column already adapted into the same ...
constexpr int type_rank< T >::value
bool buildTwoWhiteAllocationFromSolveQ16(const EmitterSolveMatrixQ16 &solve, const i32(&white1_xyz)[3], const i32(&white2_xyz)[3], WhiteAllocationPolicy policy, TwoWhiteAllocationQ16 *out) FL_NO_EXCEPT
Bind two adapted white columns with an RGB solve in the same XYZ domain.
bool buildGamutMapQ16(const colorimetric_response::EmitterProfile &profile, GamutMapQ16 *out) FL_NO_EXCEPT
Derive the mapper for a three-emitter profile.
void mapAndAllocateRgbwwQ16(const GamutMapRgbwwQ16 &map, const i32(&xyz)[3], i32(&drives)[5]) FL_NO_EXCEPT
One pixel: XYZ in s16.16 to five in-gamut drives – red, green, blue, white1, white2.
MapRedBlackTree< Key, T, Compare, fl::allocator_slab< char > > map
bool allocateTwoWhiteDrivesQ16(const TwoWhiteAllocationQ16 &allocation, const i32(&xyz)[3], i32(&drives)[5]) FL_NO_EXCEPT
One pixel: XYZ in s16.16 to five drives – red, green, blue, white1, white2 – at whichever end of the ...
constexpr i32 kOklabQ16MaxMagnitude
Largest magnitude either direction accepts, as an s16.16 raw value (64.0).
constexpr int kGamutMapHalvings
Number of chroma halvings the mapper runs, fixed at compile time.
void mapAndSolveDrivesQ16(const GamutMapQ16 &map, const i32(&xyz)[3], i32(&drives)[3]) FL_NO_EXCEPT
One pixel: XYZ in s16.16 to in-gamut emitter drives in s16.16.
void oklabToXyzQ16(const i32(&lab)[3], i32(&out_xyz)[3]) FL_NO_EXCEPT
OKLab (s16.16) -> XYZ (s16.16).
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.
WhiteAllocationPolicy
Which end of the feasible white interval to take (C3).
bool buildGamutMapRgbwwQ16(const colorimetric_response::EmitterProfile &profile, const i32(&white1_xyz)[3], const i32(&white2_xyz)[3], WhiteAllocationPolicy policy, GamutMapRgbwwQ16 *out) FL_NO_EXCEPT
Derive the mapper for a three-primary profile plus two white emitters.
constexpr enable_if< is_fixed_point< T >::value, T >::type step(T edge, T x) FL_NO_EXCEPT
InputGamut g FL_NO_EXCEPT
bool buildGamutMapFromSolveQ16(const EmitterSolveMatrixQ16 &solve, GamutMapQ16 *out) FL_NO_EXCEPT
Build a mapper from a bind-time solve already transformed into D65 working coordinates.
bool buildGamutMapRgbwQ16(const colorimetric_response::EmitterProfile &profile, const i32(&white_xyz)[3], WhiteAllocationPolicy policy, GamutMapRgbwQ16 *out) FL_NO_EXCEPT
Derive the mapper for a three-primary profile plus one white emitter.
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.
bool buildRgbSolveMatrixQ16(const colorimetric_response::EmitterProfile &profile, EmitterSolveMatrixQ16 *out) FL_NO_EXCEPT
Invert the emitter matrix for a three-emitter profile.
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.
void mapAndAllocateRgbwQ16(const GamutMapRgbwQ16 &map, const i32(&xyz)[3], i32(&drives)[4]) FL_NO_EXCEPT
One pixel: XYZ in s16.16 to four in-gamut drives, red, green, blue, white.
bool buildGamutMapRgbwwFromSolveQ16(const colorimetric_response::EmitterProfile &profile, const EmitterSolveMatrixQ16 &solve, const i32(&white1_xyz)[3], const i32(&white2_xyz)[3], WhiteAllocationPolicy policy, GamutMapRgbwwQ16 *out) FL_NO_EXCEPT
Bind a five-emitter map from one effective RGB solve and two white columns, all in the same D65 worki...
Base definition for an LED controller.
Inverse emitter matrix in s16.16, mapping XYZ to three emitter drives.
Everything the per-pixel allocation needs, derived once when a profile binds.
The same mapper for a device with a white emitter.
Everything the per-pixel mapper needs, derived once when a profile binds.
Everything the two-white allocation needs, derived once when a profile binds (C3, #4198).
The same mapper for a device with two white emitters.
bool operator()(const i32(&xyz)[3]) const FL_NO_EXCEPT
const EmitterSolveMatrixQ16 & solve
Feasibility against the three-emitter hull.
bool operator()(const i32(&xyz)[3]) const FL_NO_EXCEPT
const WhiteAllocationQ16 & allocation
Feasibility against the device's real hull when it has a white emitter.
bool operator()(const i32(&xyz)[3]) const FL_NO_EXCEPT
const TwoWhiteAllocationQ16 & allocation
Feasibility against the device's real hull when it has two white emitters.