FastLED 3.10.6
Loading...
Searching...
No Matches
gamut_map.cpp.hpp
Go to the documentation of this file.
1// ok no header - implementation for fl/gfx/gamut_map.h
2
3#include "fl/gfx/gamut_map.h"
4
5#include "fl/gfx/oklab_q16.h"
6
7namespace fl {
8
9namespace {
10
12constexpr i32 kGamutFullDrive = 65536;
13
28constexpr int kGamutMapProbes = 8;
29
38constexpr i32 kGamutProbeCeilingQ16 = 4 << 16;
39
41constexpr i32 kGamutD65Q16[3] = {62289, 65536, 71372};
42
56constexpr i32 kGamutFeasibilitySlack = 64;
57
77bool gamutDrivesAreInRange(const i32 (&drives)[3], i32 slack) FL_NO_EXCEPT {
78 for (int i = 0; i < 3; ++i) {
79 if (drives[i] < -slack || drives[i] > kGamutFullDrive + slack) {
80 return false;
81 }
82 }
83 return true;
84}
85
91void clampGamutDrives(i32 (&drives)[3]) FL_NO_EXCEPT {
92 for (int i = 0; i < 3; ++i) {
93 if (drives[i] < 0) {
94 drives[i] = 0;
95 } else if (drives[i] > kGamutFullDrive) {
96 drives[i] = kGamutFullDrive;
97 }
98 }
99}
100
102i32 scaleGamutQ16(i32 value, i32 factor) FL_NO_EXCEPT {
103 const i64 product = static_cast<i64>(value) * static_cast<i64>(factor);
104 return static_cast<i32>((product + 32768) >> 16);
105}
106
108void chromaCandidateXyz(const i32 (&lab)[3], i32 lightness, i32 factor,
109 i32 (&out_xyz)[3]) FL_NO_EXCEPT {
110 const i32 candidate_lab[3] = {
111 lightness,
112 scaleGamutQ16(lab[1], factor),
113 scaleGamutQ16(lab[2], factor),
114 };
115 oklabToXyzQ16(candidate_lab, out_xyz);
116}
117
121
122 bool operator()(const i32 (&xyz)[3]) const FL_NO_EXCEPT {
123 i32 drives[3];
124 solveRgbDrivesQ16(solve, xyz, drives);
125 return gamutDrivesAreInRange(drives, 0);
126 }
127};
128
132
133 bool operator()(const i32 (&xyz)[3]) const FL_NO_EXCEPT {
134 i32 drives[4];
135 return allocateEmitterDrivesQ16(allocation, xyz, drives);
136 }
137};
138
143
144 bool operator()(const i32 (&xyz)[3]) const FL_NO_EXCEPT {
145 i32 drives[5];
146 return allocateTwoWhiteDrivesQ16(allocation, xyz, drives);
147 }
148};
149
155template <typename Feasible>
156i32 largestFeasibleChroma(const i32 (&lab)[3], i32 lightness,
157 Feasible feasible) FL_NO_EXCEPT {
158 i32 low = 0;
159 i32 high = kGamutFullDrive;
160 for (int step = 0; step < kGamutMapHalvings; ++step) {
161 const i32 factor = (low + high) >> 1;
162 i32 candidate_xyz[3];
163 chromaCandidateXyz(lab, lightness, factor, candidate_xyz);
164 if (feasible(candidate_xyz)) {
165 low = factor;
166 } else {
167 high = factor;
168 }
169 }
170 return low;
171}
172
186template <typename Feasible>
187bool feasibleChromaInterval(const i32 (&lab)[3], i32 lightness,
188 Feasible feasible, i32* out_low,
189 i32* out_high) FL_NO_EXCEPT {
190 i32 seed = 0;
191 bool found = false;
192 for (int probe = 1; probe <= kGamutMapProbes; ++probe) {
193 const i32 factor = static_cast<i32>(
194 (static_cast<i64>(kGamutProbeCeilingQ16) * probe) / kGamutMapProbes);
195 i32 candidate_xyz[3];
196 chromaCandidateXyz(lab, lightness, factor, candidate_xyz);
197 if (feasible(candidate_xyz)) {
198 seed = factor;
199 found = true;
200 break;
201 }
202 }
203 if (!found) {
204 return false;
205 }
206
207 // Lower edge: the smallest feasible factor. `high` is feasible throughout,
208 // `low` is not, which is the mirror of the usual invariant.
209 i32 low = 0;
210 i32 high = seed;
211 for (int step = 0; step < kGamutMapHalvings; ++step) {
212 const i32 factor = low + ((high - low) >> 1);
213 i32 candidate_xyz[3];
214 chromaCandidateXyz(lab, lightness, factor, candidate_xyz);
215 if (feasible(candidate_xyz)) {
216 high = factor;
217 } else {
218 low = factor;
219 }
220 }
221 *out_low = high;
222
223 // Upper edge: the largest feasible factor, the usual invariant again.
224 low = seed;
226 for (int step = 0; step < kGamutMapHalvings; ++step) {
227 const i32 factor = low + ((high - low) >> 1);
228 i32 candidate_xyz[3];
229 chromaCandidateXyz(lab, lightness, factor, candidate_xyz);
230 if (feasible(candidate_xyz)) {
231 low = factor;
232 } else {
233 high = factor;
234 }
235 }
236 *out_high = low;
237 return true;
238}
239
257template <typename Feasible>
258i32 highestReachableLightness(const i32 (&lab)[3], i32 cap, i32 requested,
259 Feasible feasible) FL_NO_EXCEPT {
260 i32 low = cap; // feasible: chroma zero is reachable at the cap
261 i32 high = requested; // not feasible, or the caller would not be here
262 for (int step = 0; step < kGamutMapHalvings; ++step) {
263 const i32 middle = low + ((high - low) >> 1);
264 i32 unused_low = 0;
265 i32 unused_high = 0;
266 if (feasibleChromaInterval(lab, middle, feasible, &unused_low,
267 &unused_high)) {
268 low = middle;
269 } else {
270 high = middle;
271 }
272 }
273 return low;
274}
275
277i32 clampChromaFactor(i32 low, i32 high) FL_NO_EXCEPT {
278 i32 factor = kGamutFullDrive;
279 if (factor < low) {
280 factor = low;
281 }
282 if (factor > high) {
283 factor = high;
284 }
285 return factor;
286}
287
288} // namespace
289
291 if (out == nullptr) {
292 return false;
293 }
294 if (!buildRgbSolveMatrixQ16(profile, &out->solve)) {
295 return false;
296 }
297 return buildGamutMapFromSolveQ16(out->solve, out);
298}
299
302 if (out == nullptr) return false;
303 out->solve = solve;
304
305 // The drives this device needs to reproduce D65 at unit luminance. The
306 // largest of them is what saturates first as the neutral is scaled up,
307 // so it sets the brightest neutral the device can reach.
308 i32 neutral_drives[3];
309 solveRgbDrivesQ16(out->solve, kGamutD65Q16, neutral_drives);
310 i32 largest = neutral_drives[0];
311 for (int i = 0; i < 3; ++i) {
312 // Every drive, not just the largest. A solve like {-x, y, z} has a
313 // positive largest but describes a device whose primaries do not
314 // enclose D65, so it cannot make a neutral at any brightness. Taking
315 // the largest alone would scale that infeasible point up and hand
316 // back the lightness of a colour the device cannot produce, leaving
317 // the mapper's lightness bound too permissive.
318 if (neutral_drives[i] <= 0) {
319 return false;
320 }
321 if (neutral_drives[i] > largest) {
322 largest = neutral_drives[i];
323 }
324 }
325
326 // s_max = 1 / largest, in s16.16. This is the one division in the whole
327 // module, and it runs once per bind rather than once per pixel.
328 //
329 // Computed and clamped in i64. `buildRgbSolveMatrixQ16` accepts emitter
330 // luminances up to 1e6, and a profile bright enough to reach D65 on a
331 // drive of one or two raw units puts 2^32 / largest at or past i32's
332 // range -- 2^31 exactly, at largest == 2. Narrowing that is
333 // implementation-defined, and the bound it produced would be nonsense.
334 //
335 // The clamp is at 64.0 because that is where the OKLab transform's
336 // domain ends: a neutral scaled past it would be clamped there anyway,
337 // so nothing downstream can tell the difference.
338 i64 scale_wide = (static_cast<i64>(kGamutFullDrive) << 16) /
339 static_cast<i64>(largest);
340 if (scale_wide > kOklabQ16MaxMagnitude) {
341 scale_wide = kOklabQ16MaxMagnitude;
342 }
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),
348 };
349 i32 lab[3];
350 xyzToOklabQ16(brightest_neutral, lab);
351 out->max_neutral_lightness = lab[0];
352 return true;
353}
354
355void mapAndSolveDrivesQ16(const GamutMapQ16& map, const i32 (&xyz)[3],
356 i32 (&drives)[3]) FL_NO_EXCEPT {
357 solveRgbDrivesQ16(map.solve, xyz, drives);
358 if (gamutDrivesAreInRange(drives, kGamutFeasibilitySlack)) {
359 // Already inside the hull. An in-gamut image pays one solve, this
360 // comparison and the clamp, and never touches OKLab at all.
361 clampGamutDrives(drives);
362 return;
363 }
364
365 i32 lab[3];
366 xyzToOklabQ16(xyz, lab);
367
368 i32 lightness = lab[0];
369 if (lightness < 0) {
370 lightness = 0;
371 }
372
373 // Above the brightest neutral, try keeping the lightness (#4245). The
374 // feasible chroma there is an interval that does not contain zero, so it
375 // has to be located before either edge can be bisected; when it is found,
376 // clamping the target's chroma into it holds the requested lightness
377 // instead of discarding up to 29.7% of what the hull reaches.
378 if (lightness > map.max_neutral_lightness) {
379 i32 low_edge = 0;
380 i32 high_edge = 0;
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),
385 candidate_xyz);
386 i32 candidate[3];
387 solveRgbDrivesQ16(map.solve, candidate_xyz, candidate);
388 if (gamutDrivesAreInRange(candidate, kGamutFeasibilitySlack)) {
389 for (int i = 0; i < 3; ++i) {
390 drives[i] = candidate[i];
391 }
392 clampGamutDrives(drives);
393 return;
394 }
395 }
396 // No seed at this lightness. Walking down to the highest lightness
397 // that still has one keeps the answer continuous; dropping straight
398 // to the neutral cap does not, and that step measured 234 eight-bit
399 // codes before this search was added.
400 const i32 reachable = highestReachableLightness(
401 lab, map.max_neutral_lightness, lightness, RgbFeasible{map.solve});
402 i32 edge_low = 0;
403 i32 edge_high = 0;
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),
410 candidate_xyz);
411 i32 candidate[3];
412 solveRgbDrivesQ16(map.solve, candidate_xyz, candidate);
413 if (gamutDrivesAreInRange(candidate, kGamutFeasibilitySlack)) {
414 for (int i = 0; i < 3; ++i) {
415 drives[i] = candidate[i];
416 }
417 clampGamutDrives(drives);
418 return;
419 }
420 }
421 // Nothing above the cap works for this hue at all. Fall through to
422 // the clamp, which is what shipped before #4245.
423 lightness = map.max_neutral_lightness;
424 }
425
426 // Then chroma, by scaling (a, b) toward zero.
427 const i32 factor = largestFeasibleChroma(lab, lightness, RgbFeasible{map.solve});
428 i32 mapped_xyz[3];
429 chromaCandidateXyz(lab, lightness, factor, mapped_xyz);
430 solveRgbDrivesQ16(map.solve, mapped_xyz, drives);
431 clampGamutDrives(drives);
432}
433
436 const EmitterSolveMatrixQ16& solve, const i32 (&white_xyz)[3],
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}
542
543void mapAndAllocateRgbwQ16(const GamutMapRgbwQ16& map, const i32 (&xyz)[3],
544 i32 (&drives)[4]) FL_NO_EXCEPT {
545 if (allocateEmitterDrivesQ16(map.allocation, xyz, drives)) {
546 // Already inside the device's hull -- which, with a white emitter,
547 // is a good deal larger than the RGB one.
548 return;
549 }
550
551 i32 lab[3];
552 xyzToOklabQ16(xyz, lab);
553 i32 lightness = lab[0];
554 if (lightness < 0) {
555 lightness = 0;
556 }
557
558 // Same above-cap interval clamp as the RGB path (#4245). The white
559 // emitter moves the cap but does not change the shape: above it the
560 // feasible chroma still misses zero.
561 if (lightness > map.max_neutral_lightness) {
562 i32 low_edge = 0;
563 i32 high_edge = 0;
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),
568 candidate_xyz);
569 if (allocateEmitterDrivesQ16(map.allocation, candidate_xyz, drives)) {
570 return;
571 }
572 }
573
574 // Same walk-down as the RGB path: dropping straight to the cap when
575 // the probes stop landing is a step, not a mapping.
576 const i32 reachable = highestReachableLightness(
577 lab, map.max_neutral_lightness, lightness,
578 RgbwFeasible{map.allocation});
579 i32 edge_low = 0;
580 i32 edge_high = 0;
581 if (reachable > map.max_neutral_lightness &&
582 feasibleChromaInterval(lab, reachable, RgbwFeasible{map.allocation},
583 &edge_low, &edge_high)) {
584 i32 edge_xyz[3];
585 chromaCandidateXyz(lab, reachable,
586 clampChromaFactor(edge_low, edge_high), edge_xyz);
587 if (allocateEmitterDrivesQ16(map.allocation, edge_xyz, drives)) {
588 return;
589 }
590 }
591 lightness = map.max_neutral_lightness;
592 }
593
594 const i32 factor =
595 largestFeasibleChroma(lab, lightness, RgbwFeasible{map.allocation});
596 i32 mapped_xyz[3];
597 chromaCandidateXyz(lab, lightness, factor, mapped_xyz);
598 if (allocateEmitterDrivesQ16(map.allocation, mapped_xyz, drives)) {
599 return;
600 }
601 // The accepted candidate can fall a few ULP outside on the final
602 // re-solve. Fall back to the neutral at this lightness, which the
603 // lightness bound guarantees is reachable.
604 const i32 neutral_lab[3] = {lightness, 0, 0};
605 i32 neutral_xyz[3];
606 oklabToXyzQ16(neutral_lab, neutral_xyz);
607 if (!allocateEmitterDrivesQ16(map.allocation, neutral_xyz, drives)) {
608 drives[0] = 0;
609 drives[1] = 0;
610 drives[2] = 0;
611 drives[3] = 0;
612 }
613}
614
616 const i32 (&white_xyz)[3],
620 return buildRgbSolveMatrixQ16(profile, &solve) &&
621 buildGamutMapRgbwFromSolveQ16(profile, solve, white_xyz, policy, out);
622}
623
624
627 const EmitterSolveMatrixQ16& solve, const i32 (&white1_xyz)[3],
628 const i32 (&white2_xyz)[3], WhiteAllocationPolicy policy,
630 if (out == nullptr) {
631 return false;
632 }
633 if (!buildTwoWhiteAllocationFromSolveQ16(solve, white1_xyz, white2_xyz,
634 policy, &out->allocation)) {
635 return false;
636 }
637
638 i32 neutral_drives[3];
639 solveRgbDrivesQ16(out->allocation.rgb_solve, kGamutD65Q16, neutral_drives);
640 for (int i = 0; i < 3; ++i) {
641 // As in the other two builds: every drive, not just the largest.
642 if (neutral_drives[i] <= 0) {
643 return false;
644 }
645 }
646
647 // The three-emitter bound, reached with both whites off, and always
648 // attainable.
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];
653 }
654 }
655 i64 reachable = (static_cast<i64>(kGamutFullDrive) << 16) /
656 static_cast<i64>(largest_neutral_drive);
657
658 // Clamped for the same reason the three-emitter build clamps it, and to
659 // the same place: `buildRgbSolveMatrixQ16` accepts emitter luminances up
660 // to 1e6, so a profile bright enough to reach D65 on one or two raw
661 // units of drive puts `2^32 / largest` at or past i32's range -- exactly
662 // 2^31 at largest == 2. `optimistic` starts at `kOklabQ16MaxMagnitude`,
663 // so for such a profile the bisection below is skipped and the narrowing
664 // is reached directly. 64.0 is where the OKLab transform's domain ends,
665 // so nothing downstream can tell the difference.
666 if (reachable > kOklabQ16MaxMagnitude) {
667 reachable = kOklabQ16MaxMagnitude;
668 }
669
670 // The one-white relaxation with a second white added. Along the D65 ray
671 // the RGB drives are `s*d0 - w1*dW1 - w2*dW2`, so the upper limit on
672 // drive i is loosest with each white at full when its column is positive
673 // and off when it is not -- hence a `max(.., 0)` term per white.
674 // Dropping the lower limits, and letting each channel choose its own
675 // whites, are both relaxations, so this is an upper bound.
676 //
677 // `per_white1` is not stored: the allocation keeps the second column and
678 // the difference, because that is all the per-pixel path needs. It is
679 // recovered here rather than widening the struct for a bind-time sum.
680 i64 optimistic = static_cast<i64>(kOklabQ16MaxMagnitude);
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);
687 }
688 if (per_white2 > 0) {
689 numerator += static_cast<i64>(per_white2);
690 }
691 const i64 candidate =
692 (numerator << 16) / static_cast<i64>(neutral_drives[i]);
693 if (candidate < optimistic) {
694 optimistic = candidate;
695 }
696 }
697
698 // Bisected against the attainable bound rather than trusted, for exactly
699 // the reason spelled out on the one-white path: the relaxation enforces
700 // only the upper limits, and full white can push a different channel
701 // negative. Once per profile, never per pixel (A3/B11).
702 if (optimistic > reachable) {
703 i64 low = reachable;
704 i64 high = optimistic;
705 for (int step = 0; step < 24; ++step) {
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),
712 };
713 i32 trial_drives[5];
714 if (allocateTwoWhiteDrivesQ16(out->allocation, trial, trial_drives)) {
715 low = middle;
716 } else {
717 high = middle;
718 }
719 }
720 reachable = low;
721 }
722
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),
728 };
729 i32 lab[3];
730 xyzToOklabQ16(brightest_neutral, lab);
731 out->max_neutral_lightness = lab[0];
732 return true;
733}
734
736 const i32 (&white1_xyz)[3],
737 const i32 (&white2_xyz)[3],
741 return buildRgbSolveMatrixQ16(profile, &solve) &&
742 buildGamutMapRgbwwFromSolveQ16(profile, solve, white1_xyz, white2_xyz,
743 policy, out);
744}
745
746void mapAndAllocateRgbwwQ16(const GamutMapRgbwwQ16& map, const i32 (&xyz)[3],
747 i32 (&drives)[5]) FL_NO_EXCEPT {
748 if (allocateTwoWhiteDrivesQ16(map.allocation, xyz, drives)) {
749 // Already inside the device's hull, which with two whites is larger
750 // again than the one-white one.
751 return;
752 }
753
754 i32 lab[3];
755 xyzToOklabQ16(xyz, lab);
756 i32 lightness = lab[0];
757 if (lightness < 0) {
758 lightness = 0;
759 }
760
761 // And again for two whites (#4245).
762 if (lightness > map.max_neutral_lightness) {
763 i32 low_edge = 0;
764 i32 high_edge = 0;
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),
769 candidate_xyz);
770 if (allocateTwoWhiteDrivesQ16(map.allocation, candidate_xyz, drives)) {
771 return;
772 }
773 }
774
775 // Same walk-down as the RGB path: dropping straight to the cap when
776 // the probes stop landing is a step, not a mapping.
777 const i32 reachable = highestReachableLightness(
778 lab, map.max_neutral_lightness, lightness,
779 RgbwwFeasible{map.allocation});
780 i32 edge_low = 0;
781 i32 edge_high = 0;
782 if (reachable > map.max_neutral_lightness &&
783 feasibleChromaInterval(lab, reachable, RgbwwFeasible{map.allocation},
784 &edge_low, &edge_high)) {
785 i32 edge_xyz[3];
786 chromaCandidateXyz(lab, reachable,
787 clampChromaFactor(edge_low, edge_high), edge_xyz);
788 if (allocateTwoWhiteDrivesQ16(map.allocation, edge_xyz, drives)) {
789 return;
790 }
791 }
792 lightness = map.max_neutral_lightness;
793 }
794
795 const i32 factor =
796 largestFeasibleChroma(lab, lightness, RgbwwFeasible{map.allocation});
797 i32 mapped_xyz[3];
798 chromaCandidateXyz(lab, lightness, factor, mapped_xyz);
799 if (allocateTwoWhiteDrivesQ16(map.allocation, mapped_xyz, drives)) {
800 return;
801 }
802 // Same last resort as the other paths: the accepted candidate can fall a
803 // few ULP outside on the final re-solve, and the neutral at this
804 // lightness is guaranteed reachable by the bound above.
805 const i32 neutral_lab[3] = {lightness, 0, 0};
806 i32 neutral_xyz[3];
807 oklabToXyzQ16(neutral_lab, neutral_xyz);
808 if (!allocateTwoWhiteDrivesQ16(map.allocation, neutral_xyz, drives)) {
809 for (int i = 0; i < 5; ++i) {
810 drives[i] = 0;
811 }
812 }
813}
814
815} // namespace fl
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
Definition map.h:283
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).
Definition oklab_q16.h:40
constexpr int kGamutMapHalvings
Number of chroma halvings the mapper runs, fixed at compile time.
Definition gamut_map.h:40
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
Definition rgbw.h:121
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.
Definition crgb.hpp:179
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.
Definition gamut_map.h:69
Everything the per-pixel mapper needs, derived once when a profile binds.
Definition gamut_map.h:43
Everything the two-white allocation needs, derived once when a profile binds (C3, #4198).
The same mapper for a device with two white emitters.
Definition gamut_map.h:122
fl::i64 i64
Definition stdint.h:221
bool operator()(const i32(&xyz)[3]) const FL_NO_EXCEPT
Feasibility against the three-emitter hull.
bool operator()(const i32(&xyz)[3]) const FL_NO_EXCEPT
Feasibility against the device's real hull when it has a white emitter.
bool operator()(const i32(&xyz)[3]) const FL_NO_EXCEPT
Feasibility against the device's real hull when it has two white emitters.