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

◆ allocateTwoWhiteDrivesQ16()

bool fl::allocateTwoWhiteDrivesQ16 ( const TwoWhiteAllocationQ16 & allocation,
const i32(&) xyz[3],
i32(&) drives[5] )

One pixel: XYZ in s16.16 to five drives – red, green, blue, white1, white2 – at whichever end of the feasible total the profile's policy names.

The method, and why it is not a search. Writing the total s = w1 + w2 and substituting w2 = s - w1 leaves the RGB drives as

(d0 - s * per_white2) - w1 * difference

which is the one-white shape with a shifted target. So at any fixed total the feasible w1 is again an interval, bounded by five lower and five upper bounds: three from the RGB drives, and two more because w1 and w2 = s - w1 are each a drive in their own right. Every one of those bounds is affine in s, so "some `w1` exists at this total" is exactly the conjunction of the pairwise inequalities lower_k(s) <= upper_j(s), each linear in s and each solvable for one s bound. Intersecting them gives the feasible totals in closed form – no bisection, no vertex enumeration, no per-pixel iteration (A3/B11).

That the interval has to be found rather than assumed is the whole point. Achievable totals form [s_lo, s_hi], which need not start at zero: a bright target is unreachable with the primaries alone, so a bisection seeded at zero has no feasible starting point and reports such targets unreachable. The first attempt at this made exactly that assumption (#4198).

At the chosen total the split is not free, which is measurement rather than assumption. The reference settles a free split by minimizing the sum of squares of the RGB drives, and an earlier revision did the same here; the feasible splits at the extreme total turn out to be a single point, so there was nothing for the rule to choose. Width measured 0.0 over 4000 random targets on the corpus's cool/warm device and 951 on a device built to make one drive's constraint parallel to w1 + w2 – the shape that could have produced an optimal edge. Two whites of the same colour are the exception, and there the reference's own tie-break is the end this takes.

False when no total keeps every drive in range, which means the target is outside the device hull – the gamut mapper's job, not this one's. drives is not written in that case.

Cost note, recorded rather than optimized away on a guess: up to 25 s16.16 divisions per pixel against the one-white path's six. Whether cross-multiplying the pairwise comparisons – which would leave one division and a great many i64 multiplies – wins on an 8-bit target is unmeasured, so the straightforward version is what ships.

Definition at line 394 of file white_allocation.cpp.hpp.

396 {
397 i32 at_zero[3];
398 solveRgbDrivesQ16(allocation.rgb_solve, xyz, at_zero);
399 for (int i = 0; i < 3; ++i) {
400 // A target this far out is not a rounding case, it is outside the
401 // hull by a wide margin, and letting it through would be the only
402 // way the bounds below could overflow.
403 if (at_zero[i] > kTwoWhiteMaxColumn || at_zero[i] < -kTwoWhiteMaxColumn) {
404 return false;
405 }
406 }
407
408 // `w1 >= 0`, `w1 >= s - 1` (from `w2 <= 1`), `w1 <= 1`, `w1 <= s` (from
409 // `w2 >= 0`). Every part of a bound is s16.16, the denominator included,
410 // so a denominator of "one" is `kWhiteFullDrive` and not 1 -- writing a
411 // raw 1 there scales those four bounds by 65536 and silently rejects
412 // every target whose total runs past the primaries.
413 SplitBound lowers[5];
414 SplitBound uppers[5];
415 int lower_count = 0;
416 int upper_count = 0;
417 lowers[lower_count++] = normalizedSplitBound(0, 0, kWhiteFullDrive);
418 lowers[lower_count++] =
419 normalizedSplitBound(-kWhiteFullDrive, kWhiteFullDrive, kWhiteFullDrive);
420 uppers[upper_count++] = normalizedSplitBound(kWhiteFullDrive, 0, kWhiteFullDrive);
421 uppers[upper_count++] = normalizedSplitBound(0, kWhiteFullDrive, kWhiteFullDrive);
422
423 i64 low_total = 0;
424 i64 high_total = 2 * static_cast<i64>(kWhiteFullDrive);
425
426 for (int i = 0; i < 3; ++i) {
427 // drive_i(w1, s) = (at_zero_i - s*per_white2_i) - w1*difference_i,
428 // which must stay in [0, 1].
429 const i32 slope = allocation.difference[i];
430 if (slope == 0) {
431 // The split cannot move this drive: the constraint is on the
432 // total alone. `at_zero_i - s*per_white2_i` in [0, 1].
433 const i32 per = allocation.per_white2[i];
434 if (per == 0) {
435 if (at_zero[i] < -kWhiteSlack ||
436 at_zero[i] > kWhiteFullDrive + kWhiteSlack) {
437 return false;
438 }
439 continue;
440 }
441 const i64 first = divideWhiteQ16(at_zero[i], per);
442 const i64 second =
443 divideWhiteQ16(at_zero[i] - kWhiteFullDrive - kTwoWhiteBoundSlack, per);
444 const i64 upper = per > 0 ? first : second;
445 const i64 lower = per > 0 ? second : first;
446 if (upper < high_total) {
447 high_total = upper;
448 }
449 if (lower > low_total) {
450 low_total = lower;
451 }
452 continue;
453 }
454 // Dividing by a negative slope swaps which bound is which; that is
455 // handled once, in normalizedSplitBound, by folding the sign.
456 // The slack sits on the `drive <= 1` side and nowhere else, which
457 // is the same asymmetry the one-white path argues for above. That
458 // side can only *narrow* the white the policy may take, so tolerating
459 // a drive 64 raw units past full -- exactly what the final range
460 // check tolerates -- costs nothing. Putting slack on the `drive >= 0`
461 // side would be different: the policy maximizes white, so it would
462 // spend the slack on every pixel.
463 //
464 // Without it the exact hull corner is refused. Five emitters at full
465 // drive solve, through a quantized matrix, to a target needing 57 raw
466 // units more than a total of 2.0 can supply, so the feasible interval
467 // comes out empty by less than a thousandth of a drive.
468 const SplitBound at_full =
469 normalizedSplitBound(at_zero[i] - kWhiteFullDrive - kTwoWhiteBoundSlack,
470 -allocation.per_white2[i], slope);
471 const SplitBound at_dark =
472 normalizedSplitBound(at_zero[i], -allocation.per_white2[i], slope);
473 if (!splitBoundInRange(at_full) || !splitBoundInRange(at_dark)) {
474 return false;
475 }
476 if (slope > 0) {
477 uppers[upper_count++] = at_dark;
478 lowers[lower_count++] = at_full;
479 } else {
480 uppers[upper_count++] = at_full;
481 lowers[lower_count++] = at_dark;
482 }
483 }
484
485 // Some total admits a split exactly when every lower bound sits under
486 // every upper bound there. Each pair is linear in the total, so this is
487 // the closed form the header describes.
488 for (int k = 0; k < lower_count; ++k) {
489 for (int j = 0; j < upper_count; ++j) {
490 if (!narrowTotalForPair(lowers[k], uppers[j], &low_total, &high_total)) {
491 return false;
492 }
493 }
494 }
495 if (high_total < 0) {
496 high_total = 0;
497 }
498 if (low_total > high_total) {
499 return false;
500 }
501
502 const i64 total = allocation.policy == WhiteAllocationPolicy::RgbPreferred
503 ? low_total
504 : high_total;
505
506 // At the chosen total the split is free, and the reference minimizes the
507 // sum of squares of the RGB drives -- a quadratic in w1, so its minimum
508 // is one division.
509 i64 split_low = 0;
510 i64 split_high = kWhiteFullDrive;
511 for (int k = 0; k < lower_count; ++k) {
512 const i64 value = splitBoundAt(lowers[k], total);
513 if (value > split_low) {
514 split_low = value;
515 }
516 }
517 for (int j = 0; j < upper_count; ++j) {
518 const i64 value = splitBoundAt(uppers[j], total);
519 if (value < split_high) {
520 split_high = value;
521 }
522 }
523 if (split_low > split_high) {
524 // Collapsed rather than empty. At either end of the total the split
525 // bounds meet exactly, and they were derived through a quantized
526 // matrix, so integer truncation can cross them by a few raw units --
527 // measured at 21 on the RGB-preferred end of a target needing both
528 // whites. Same answer as the one-white path gives its own boundary
529 // case: take the point they collapse to and let the drive check
530 // below decide whether the target is really reachable.
531 if (split_low - split_high > kWhiteSlack) {
532 return false;
533 }
534 const i64 middle = (split_low + split_high) / 2;
535 split_low = middle;
536 split_high = middle;
537 }
538
539 i64 at_total[3];
540 for (int i = 0; i < 3; ++i) {
541 at_total[i] = static_cast<i64>(at_zero[i]) -
542 ((static_cast<i64>(allocation.per_white2[i]) * total + 32768) >> 16);
543 }
544
545 // The low end, and there is nothing to choose. The reference settles the
546 // split by minimizing the sum of squares of the RGB drives, and an
547 // earlier revision here did the same -- it is a quadratic in the split,
548 // so one division. Measurement removed it: at the *extreme* total the
549 // feasible split is a single point, so there is no freedom for any rule
550 // to exercise. Widest interval measured 0.0 over 4000 random targets on
551 // the corpus's cool/warm device and 951 on a device built specifically
552 // to make one drive's constraint parallel to `w1 + w2`, which is the
553 // shape that could have produced an optimal edge.
554 //
555 // Where the split really is free -- two whites of the same colour, so
556 // the difference column is zero and no RGB drive moves with the split --
557 // the reference's own tie-break falls through to the lexicographically
558 // smallest drives, which is this end. So the low end is not a
559 // simplification away from the reference; it is what the reference does.
560 //
561 // See `ci/tests/test_color_rgbw_study.py`, which gates the whole
562 // allocation against the reference and would fail if this stopped
563 // holding.
564 const i64 split = split_low;
565
566 i32 rgb[3];
567 for (int i = 0; i < 3; ++i) {
568 const i64 drive =
569 at_total[i] -
570 ((static_cast<i64>(allocation.difference[i]) * split + 32768) >> 16);
571 if (drive < -kWhiteSlack || drive > kWhiteFullDrive + kWhiteSlack) {
572 return false;
573 }
574 rgb[i] = drive < 0 ? 0
576 : static_cast<i32>(drive));
577 }
578 const i64 second = total - split;
579 if (second < -kWhiteSlack || second > kWhiteFullDrive + kWhiteSlack ||
580 split < -kWhiteSlack || split > kWhiteFullDrive + kWhiteSlack) {
581 return false;
582 }
583 drives[0] = rgb[0];
584 drives[1] = rgb[1];
585 drives[2] = rgb[2];
586 drives[3] = split < 0 ? 0
588 : static_cast<i32>(split));
589 drives[4] = second < 0 ? 0
590 : (second > kWhiteFullDrive ? kWhiteFullDrive
591 : static_cast<i32>(second));
592 return true;
593}
i64 splitBoundAt(const SplitBound &bound, i64 total) FL_NO_EXCEPT
value at total s, in s16.16.
SplitBound normalizedSplitBound(i32 numerator, i32 slope, i32 denominator) FL_NO_EXCEPT
Fold a negative denominator into the numerator and slope, so every bound compares the same way round.
bool splitBoundInRange(const SplitBound &bound) FL_NO_EXCEPT
Whether every part of a bound stays inside the range the accumulators were sized for.
constexpr i32 kWhiteFullDrive
Full drive, as an s16.16 raw value.
bool narrowTotalForPair(const SplitBound &lower, const SplitBound &upper, i64 *low, i64 *high) FL_NO_EXCEPT
The totals at which lower <= upper holds, narrowed into [*low, *high].
i64 divideWhiteQ16(i32 numerator, i32 denominator) FL_NO_EXCEPT
(numerator << 16) / denominator as s16.16, kept in i64.
constexpr int type_rank< T >::value
@ RgbPreferred
As little as the target allows, which for most targets is none.
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.
i32 per_white2[3]
M^-1 . w2: the RGB drives one unit of the second white replaces.
WhiteAllocationPolicy policy
Which end of the feasible total this profile takes.
EmitterSolveMatrixQ16 rgb_solve
Inverse RGB emitter matrix.
fl::i64 i64
Definition stdint.h:221

References fl::TwoWhiteAllocationQ16::difference, FL_NO_EXCEPT, fl::TwoWhiteAllocationQ16::per_white2, fl::TwoWhiteAllocationQ16::policy, fl::TwoWhiteAllocationQ16::rgb_solve, RgbPreferred, solveRgbDrivesQ16(), and type_rank< T >::value.

Referenced by buildGamutMapRgbwwFromSolveQ16(), mapAndAllocateRgbwwQ16(), fl::anonymous_namespace{gamut_map.cpp.hpp}::RgbwwFeasible::operator()(), and processPixelWideLinearQ16().

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