FastLED 3.10.6
Loading...
Searching...
No Matches
white_allocation.cpp.hpp
Go to the documentation of this file.
1// ok no header - implementation for fl/gfx/white_allocation.h
2
4
5namespace fl {
6
7namespace {
8
10constexpr i32 kWhiteFullDrive = 65536;
11
33constexpr i32 kWhiteSlackAtUnitColumn = 64;
34
43constexpr i32 kWhiteSlack = 64;
44
76i64 divideWhiteQ16(i32 numerator, i32 denominator) FL_NO_EXCEPT {
77 const i64 scaled = static_cast<i64>(numerator) * 65536;
78 return scaled / static_cast<i64>(denominator);
79}
80
82i32 scaleWhiteQ16(i32 value, i32 factor) FL_NO_EXCEPT {
83 const i64 product = static_cast<i64>(value) * static_cast<i64>(factor);
84 return static_cast<i32>((product + 32768) >> 16);
85}
86
87} // namespace
88
91 const EmitterSolveMatrixQ16& solve, const i32 (&white_xyz)[3],
93 if (out == nullptr) {
94 return false;
95 }
96 out->policy = policy;
97 out->rgb_solve = solve;
98 solveRgbDrivesQ16(out->rgb_solve, white_xyz, out->per_white);
99
100 // Per-channel slack, so the allowance costs the same colour everywhere.
101 // Bind time, once per profile -- there is no per-pixel work here.
102 {
103 const float* xy[3] = {profile.xy_r, profile.xy_g, profile.xy_b};
104 const float luminance[3] = {profile.lum_r, profile.lum_g, profile.lum_b};
105 for (int channel = 0; channel < 3; ++channel) {
106 i32 xy_q16[2];
107 i32 luminance_q16;
108 if (!q16FromFloatBits(xy[channel][0], &xy_q16[0]) ||
109 !q16FromFloatBits(xy[channel][1], &xy_q16[1]) ||
110 !q16FromFloatBits(luminance[channel], &luminance_q16)) {
111 return false;
112 }
113 i64 column[3];
114 if (!detail::xyzColumnQ16(xy_q16, luminance_q16, column)) {
115 return false;
116 }
117 // The peak component, because that is what bounds the per-axis
118 // XYZ error a clamp of one drive unit can cause.
119 i64 peak = 0;
120 for (int axis = 0; axis < 3; ++axis) {
121 const i64 magnitude = column[axis] < 0 ? -column[axis]
122 : column[axis];
123 if (magnitude > peak) {
124 peak = magnitude;
125 }
126 }
127 // Both operands are Q16, so the quotient is in raw drive units.
128 // The numerator fits i64 even at the largest supported slack.
129 i64 scaled = (static_cast<i64>(kWhiteSlackAtUnitColumn) * 65536 +
130 peak / 2) / peak;
131 // Never zero: a column so large that the allowance rounds away
132 // would reject the rounding this exists to tolerate.
133 if (scaled < 1) {
134 scaled = 1;
135 }
136 // And never larger than the allowance it is scaling. Dim
137 // emitters have small columns, so the division wants to *grow*
138 // the slack -- at a luminance of 1e-4 it reaches 640,000 raw
139 // units, nearly ten in drive space, which accepts any drive at
140 // all and clamps it. That is the defect this function exists to
141 // remove, reinstated an order of magnitude worse.
142 //
143 // The tolerance is for rounding in the solve, which is a few raw
144 // units whatever the emitter's brightness. So this only ever
145 // narrows: 64 stays the ceiling and the column decides how far
146 // below it each channel sits. For the corpus device nothing is
147 // clipped -- green is already at 64 and the others below it --
148 // so the measurements above are unaffected.
149 if (scaled > kWhiteSlackAtUnitColumn) {
150 scaled = kWhiteSlackAtUnitColumn;
151 }
152 out->slack[channel] = static_cast<i32>(scaled);
153 }
154 }
155
156 // A white emitter the primaries cannot express at all leaves nothing for
157 // the allocation to trade against, and every bound below would divide by
158 // zero or be vacuous.
159 bool expressible = false;
160 for (int i = 0; i < 3; ++i) {
161 if (out->per_white[i] != 0) {
162 expressible = true;
163 }
164 }
165 return expressible;
166}
167
169 const i32 (&white_xyz)[3],
173 return buildRgbSolveMatrixQ16(profile, &solve) &&
174 buildWhiteAllocationFromSolveQ16(profile, solve, white_xyz, policy, out);
175}
176
178 const i32 (&xyz)[3],
179 i32 (&drives)[4]) FL_NO_EXCEPT {
180 i32 at_zero[3];
181 solveRgbDrivesQ16(allocation.rgb_solve, xyz, at_zero);
182
183 // The interval of white levels that keeps every RGB drive in range.
184 // Each bound is one inequality in the white level because d(w) is affine
185 // in w -- that is the whole reason no search is needed here.
186 //
187 // Strict bounds, deliberately. Slack here would be *spent* rather than
188 // merely tolerated: the policy maximizes white, so widening the bounds
189 // by 64 raw units lets it take 64 units out of every RGB drive on every
190 // pixel. That is not a rounding allowance, it is a systematic colour
191 // shift -- measured at white = 886 where the reference says 0. The
192 // slack lives on the drive check below, which is what actually decides
193 // feasibility.
194 i64 low = 0;
195 i64 high = kWhiteFullDrive;
196 for (int i = 0; i < 3; ++i) {
197 const i32 slope = allocation.per_white[i];
198 if (slope != 0) {
199 // Dividing by a negative slope swaps which bound is which.
200 const i64 first = divideWhiteQ16(at_zero[i], slope);
201 const i64 second = divideWhiteQ16(at_zero[i] - kWhiteFullDrive, slope);
202 const i64 upper = slope > 0 ? first : second;
203 const i64 lower = slope > 0 ? second : first;
204 if (upper < high) {
205 high = upper;
206 }
207 if (lower > low) {
208 low = lower;
209 }
210 } else if (at_zero[i] < -allocation.slack[i] ||
211 at_zero[i] > kWhiteFullDrive + allocation.slack[i]) {
212 // White cannot move this drive at all, and it is already out.
213 return false;
214 }
215 }
216 // At the dark floor the solve rounds a drive to -1 raw on a colour the
217 // device reproduces exactly -- corpus target (4, 1, 19) solves to
218 // (0, -1, 1) -- which drags `high` below zero. Clamp rather than refuse;
219 // the drive check below decides whether the target is really reachable.
220 if (high < 0) {
221 high = 0;
222 }
223 if (low > high) {
224 return false;
225 }
226
227 // Either end reproduces the target exactly; which one is policy. Both
228 // ends are inside [0, full] by construction, so the narrowing is safe.
229 const i32 level = static_cast<i32>(
230 allocation.policy == WhiteAllocationPolicy::RgbPreferred ? low : high);
231 i32 rgb[3];
232 for (int i = 0; i < 3; ++i) {
233 const i32 drive = at_zero[i] - scaleWhiteQ16(allocation.per_white[i], level);
234 const i32 channel_slack = allocation.slack[i];
235 if (drive < -channel_slack || drive > kWhiteFullDrive + channel_slack) {
236 return false;
237 }
238 rgb[i] = drive < 0 ? 0 : (drive > kWhiteFullDrive ? kWhiteFullDrive : drive);
239 }
240 drives[0] = rgb[0];
241 drives[1] = rgb[1];
242 drives[2] = rgb[2];
243 drives[3] = level < 0 ? 0
244 : (level > kWhiteFullDrive ? kWhiteFullDrive : level);
245 return true;
246}
247
248namespace {
249
260constexpr i32 kTwoWhiteMaxColumn = 8 * 65536;
261
273
282 i32 slope;
283 i32 denominator; // > 0 always; the sign is folded into the other two.
284};
285
288 const i64 scaled =
289 static_cast<i64>(bound.numerator) * 65536 + static_cast<i64>(bound.slope) * total;
290 return scaled / static_cast<i64>(bound.denominator);
291}
292
296 return bound.numerator >= -kTwoWhiteMaxColumn &&
297 bound.numerator <= kTwoWhiteMaxColumn &&
298 bound.slope >= -kTwoWhiteMaxColumn && bound.slope <= kTwoWhiteMaxColumn &&
299 bound.denominator > 0 && bound.denominator <= kTwoWhiteMaxColumn;
300}
301
304SplitBound normalizedSplitBound(i32 numerator, i32 slope,
305 i32 denominator) FL_NO_EXCEPT {
306 SplitBound bound;
307 if (denominator < 0) {
308 bound.numerator = -numerator;
309 bound.slope = -slope;
310 bound.denominator = -denominator;
311 } else {
312 bound.numerator = numerator;
313 bound.slope = slope;
314 bound.denominator = denominator;
315 }
316 return bound;
317}
318
332bool narrowTotalForPair(const SplitBound& lower, const SplitBound& upper, i64* low,
333 i64* high) FL_NO_EXCEPT {
334 const i64 slope = static_cast<i64>(upper.denominator) * lower.slope -
335 static_cast<i64>(lower.denominator) * upper.slope;
336 const i64 constant = static_cast<i64>(lower.denominator) * upper.numerator -
337 static_cast<i64>(upper.denominator) * lower.numerator;
338 if (slope == 0) {
339 // No s makes this pair better or worse; it either always holds or
340 // never does.
341 return constant >= 0;
342 }
343 const i64 bound = (constant * 65536) / slope;
344 if (slope > 0) {
345 if (bound < *high) {
346 *high = bound;
347 }
348 } else if (bound > *low) {
349 *low = bound;
350 }
351 return true;
352}
353
354} // namespace
355
357 const EmitterSolveMatrixQ16& solve, const i32 (&white1_xyz)[3],
358 const i32 (&white2_xyz)[3], WhiteAllocationPolicy policy,
360 if (out == nullptr) {
361 return false;
362 }
363 out->policy = policy;
364 out->rgb_solve = solve;
365 i32 per_white1[3];
366 solveRgbDrivesQ16(out->rgb_solve, white1_xyz, per_white1);
367 solveRgbDrivesQ16(out->rgb_solve, white2_xyz, out->per_white2);
368
369 for (int i = 0; i < 3; ++i) {
370 // The difference is what the split multiplies, so it is the one that
371 // has to fit; both columns are checked because it is derived from
372 // them and because each is a bound coefficient in its own right.
373 if (per_white1[i] > kTwoWhiteMaxColumn || per_white1[i] < -kTwoWhiteMaxColumn ||
374 out->per_white2[i] > kTwoWhiteMaxColumn ||
375 out->per_white2[i] < -kTwoWhiteMaxColumn) {
376 return false;
377 }
378 out->difference[i] = per_white1[i] - out->per_white2[i];
379 }
380 return true;
381}
382
384 const i32 (&white1_xyz)[3],
385 const i32 (&white2_xyz)[3],
389 return buildRgbSolveMatrixQ16(profile, &solve) &&
390 buildTwoWhiteAllocationFromSolveQ16(solve, white1_xyz, white2_xyz,
391 policy, out);
392}
393
395 const i32 (&xyz)[3],
396 i32 (&drives)[5]) FL_NO_EXCEPT {
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
575 : (drive > kWhiteFullDrive ? kWhiteFullDrive
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
587 : (split > kWhiteFullDrive ? kWhiteFullDrive
588 : static_cast<i32>(split));
589 drives[4] = second < 0 ? 0
590 : (second > kWhiteFullDrive ? kWhiteFullDrive
591 : static_cast<i32>(second));
592 return true;
593}
594
595} // namespace fl
unsigned int xy(unsigned int x, unsigned int y)
i32 scaleWhiteQ16(i32 value, i32 factor) FL_NO_EXCEPT
Scale an s16.16 value by an s16.16 factor, rounding to nearest.
i64 splitBoundAt(const SplitBound &bound, i64 total) FL_NO_EXCEPT
value at total s, in s16.16.
constexpr i32 kWhiteSlackAtUnitColumn
Slack on the drive bounds, for an emitter whose XYZ column peaks at 1.
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 kTwoWhiteMaxColumn
The largest magnitude a per-white column or a solved drive may reach before the pairwise bounds stop ...
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].
constexpr i32 kTwoWhiteBoundSlack
How far past full drive the bounds let an RGB drive reach.
constexpr i32 kWhiteSlack
The same allowance, uniform, for the two-white (RGBWW) path.
i64 divideWhiteQ16(i32 numerator, i32 denominator) FL_NO_EXCEPT
(numerator << 16) / denominator as s16.16, kept in i64.
A bound on the split, as (numerator + slope * s) / denominator with a positive denominator.
bool xyzColumnQ16(const i32(&xy)[2], i32 luminance, i64(&column)[3]) FL_NO_EXCEPT
XYZ of chromaticity xy at luminance luminance, all s16.16; false for a non-positive or out-of-simplex...
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 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 ...
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...
WhiteAllocationPolicy
Which end of the feasible white interval to take (C3).
@ RgbPreferred
As little as the target allows, which for most targets is none.
bool buildWhiteAllocationQ16(const colorimetric_response::EmitterProfile &profile, const i32(&white_xyz)[3], WhiteAllocationPolicy policy, WhiteAllocationQ16 *out) FL_NO_EXCEPT
Derive the allocation for a three-primary profile plus one white emitter.
InputGamut g FL_NO_EXCEPT
Definition rgbw.h:121
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.
bool q16FromFloatBits(float value, i32 *out) FL_NO_EXCEPT
s16.16 of a float, rounded to nearest, computed from its IEEE-754 bits with integer arithmetic only –...
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.
bool buildTwoWhiteAllocationQ16(const colorimetric_response::EmitterProfile &profile, const i32(&white1_xyz)[3], const i32(&white2_xyz)[3], WhiteAllocationPolicy policy, TwoWhiteAllocationQ16 *out) FL_NO_EXCEPT
Derive the allocation for three primaries plus two white emitters.
Base definition for an LED controller.
Definition crgb.hpp:179
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.
EmitterSolveMatrixQ16 rgb_solve
Inverse RGB emitter matrix.
i32 slack[3]
Per-channel drive slack, in s16.16 raw units.
WhiteAllocationPolicy policy
Which end of the interval this profile takes.
Inverse emitter matrix in s16.16, mapping XYZ to three emitter drives.
Everything the per-pixel allocation needs, derived once when a profile binds.
Everything the two-white allocation needs, derived once when a profile binds (C3, #4198).
fl::i64 i64
Definition stdint.h:221