FastLED 3.10.6
Loading...
Searching...
No Matches
device_solve.cpp.hpp
Go to the documentation of this file.
1// ok no header - implementation for fl/gfx/device_solve.h
2
4
5#include "fl/stl/bit_cast.h"
6
7namespace fl {
8
9namespace {
10
11// Helpers carry a `solve` qualifier: .cpp.hpp files share one translation
12// unit under the unity build, so an anonymous namespace does not isolate them
13// from same-named helpers in sibling files.
14
22constexpr i32 kMaxCoefficient = 21845; // floor(2^32 / 3) >> 16
23
24constexpr i64 kI64Max = 9223372036854775807LL;
25
27constexpr i64 kQ16One = 65536;
28
36 if (a == 0 || b == 0) {
37 return true;
38 }
39 const i64 lhs = a < 0 ? -a : a;
40 const i64 rhs = b < 0 ? -b : b;
41 return rhs <= kI64Max / lhs;
42}
43
57 if (b > 0) {
58 return a <= kI64Max - b;
59 }
60 if (b < 0) {
61 return a >= -kI64Max - b;
62 }
63 return true;
64}
65
67bool cofactorQ32(i32 e, i32 i_, i32 f, i32 h, i64* out) FL_NO_EXCEPT {
68 const i64 left = static_cast<i64>(e) * static_cast<i64>(i_);
69 const i64 right = static_cast<i64>(f) * static_cast<i64>(h);
70 // Two i32 products always fit; their difference need not.
71 if (!sumFitsI64(left, -right)) {
72 return false;
73 }
74 *out = left - right;
75 return true;
76}
77
81i64 roundedDivI64(i64 numerator, i64 denominator) FL_NO_EXCEPT {
82 if (denominator < 0) {
83 numerator = -numerator;
84 denominator = -denominator;
85 }
86 const i64 half = denominator / 2;
87 if (numerator >= 0) {
88 return (numerator + half) / denominator;
89 }
90 return -((-numerator + half) / denominator);
91}
92
112i64 divideCoefficientQ16(i64 cofactor_q32, i64 determinant_q48) FL_NO_EXCEPT {
113 i64 numerator = cofactor_q32;
114 i64 denominator = determinant_q48;
115 // Safe to negate: `sumFitsI64` refuses a determinant of i64's minimum, so
116 // the caller cannot pass the one value this would be undefined for.
117 if (denominator < 0) {
118 numerator = -numerator;
119 denominator = -denominator;
120 }
121 constexpr i64 kHalfMax = kI64Max / 2;
122 for (int spent = 0; spent < 32; ++spent) {
123 const i64 magnitude = numerator < 0 ? -numerator : numerator;
124 if (magnitude <= kHalfMax) {
125 numerator *= 2;
126 } else {
127 denominator = (denominator + 1) / 2;
128 }
129 }
130 return roundedDivI64(numerator, denominator);
131}
132
137i64 roundedDivideQ16(i64 numerator, i64 denominator) FL_NO_EXCEPT {
138 if (denominator < 0) {
139 numerator = -numerator;
140 denominator = -denominator;
141 }
142 const i64 half = denominator / 2;
143 if (numerator >= 0) {
144 return (numerator + half) / denominator;
145 }
146 return -((-numerator + half) / denominator);
147}
148
158bool emitterColumnQ16(const i32 (&xy)[2], i32 luminance,
159 i64 (&column)[3]) FL_NO_EXCEPT {
160 const i32 x = xy[0];
161 const i32 y = xy[1];
162 // The float guard rejects a `y` at or below 1e-6; in Q16 anything under
163 // half a step quantises to zero, and dividing by it is the failure the
164 // float path cannot have.
165 if (y <= 0 || luminance <= 0 || x <= 0) {
166 return false;
167 }
168 if (static_cast<i64>(x) + static_cast<i64>(y) > kQ16One) {
169 // Outside the CIE simplex, so z would be negative for a physical
170 // emitter.
171 return false;
172 }
173 const i64 z = kQ16One - static_cast<i64>(x) - static_cast<i64>(y);
174 const i64 x_over_y = roundedDivideQ16(static_cast<i64>(x) * kQ16One, y);
175 const i64 z_over_y = roundedDivideQ16(z * kQ16One, y);
176 column[0] = roundedDivideQ16(x_over_y * luminance, kQ16One);
177 column[1] = luminance;
178 column[2] = roundedDivideQ16(z_over_y * luminance, kQ16One);
179 for (int i = 0; i < 3; ++i) {
180 // The inverse works in i32, so a column that does not fit is refused
181 // here rather than wrapped on the way in.
182 if (column[i] > 2147483647LL || column[i] < -2147483648LL) {
183 return false;
184 }
185 }
186 return true;
187}
188
189i32 dotSolveRowQ16(const i32 (&row)[3], const i32 (&v)[3]) FL_NO_EXCEPT {
190 const i64 acc = static_cast<i64>(row[0]) * static_cast<i64>(v[0])
191 + static_cast<i64>(row[1]) * static_cast<i64>(v[1])
192 + static_cast<i64>(row[2]) * static_cast<i64>(v[2]);
193 const i64 rounded = acc >= 0 ? (acc + 32768) >> 16
194 : -((-acc + 32768) >> 16);
195 // Saturate rather than wrap. An extreme target can still land outside
196 // s16.16 after the shift, and wrapping would turn an out-of-gamut
197 // overshoot into a wildly wrong colour of the opposite sign, which the
198 // gamut mapper downstream would then treat as legitimate.
199 constexpr i64 kMax = 2147483647;
200 constexpr i64 kMin = -2147483647 - 1;
201 if (rounded > kMax) {
202 return static_cast<i32>(kMax);
203 }
204 if (rounded < kMin) {
205 return static_cast<i32>(kMin);
206 }
207 return static_cast<i32>(rounded);
208}
209
210} // namespace
211
212bool invert3x3Q16(const i32 (&in)[3][3], i32 (&out)[3][3]) FL_NO_EXCEPT {
213 const i32 a = in[0][0], b = in[0][1], c = in[0][2];
214 const i32 d = in[1][0], e = in[1][1], f = in[1][2];
215 const i32 g = in[2][0], h = in[2][1], i_ = in[2][2];
216
217 // Cofactors are differences of two Q16 products, so Q32.
218 i64 cof[3][3];
219 if (!cofactorQ32(e, i_, f, h, &cof[0][0]) ||
220 !cofactorQ32(c, h, b, i_, &cof[0][1]) ||
221 !cofactorQ32(b, f, c, e, &cof[0][2]) ||
222 !cofactorQ32(f, g, d, i_, &cof[1][0]) ||
223 !cofactorQ32(a, i_, c, g, &cof[1][1]) ||
224 !cofactorQ32(c, d, a, f, &cof[1][2]) ||
225 !cofactorQ32(d, h, e, g, &cof[2][0]) ||
226 !cofactorQ32(b, g, a, h, &cof[2][1]) ||
227 !cofactorQ32(a, e, b, d, &cof[2][2])) {
228 return false;
229 }
230
231 // Determinant is a Q16 times a Q32, so Q48.
232 const i32 first_row[3] = {a, b, c};
233 i64 determinant = 0;
234 for (int col = 0; col < 3; ++col) {
235 const i64 term_lhs = static_cast<i64>(first_row[col]);
236 if (!productFitsI64(term_lhs, cof[col][0])) {
237 return false;
238 }
239 const i64 term = term_lhs * cof[col][0];
240 if (!sumFitsI64(determinant, term)) {
241 return false;
242 }
243 determinant += term;
244 }
245 if (determinant == 0) {
246 return false;
247 }
248
249 for (int row = 0; row < 3; ++row) {
250 for (int col = 0; col < 3; ++col) {
251 const i64 value = divideCoefficientQ16(cof[row][col], determinant);
252 // The same bound the float path applies to its own result, and
253 // for the same reason: past it the solve's accumulator can
254 // overflow for an in-range XYZ input.
255 const i64 limit = static_cast<i64>(kMaxCoefficient) << 16;
256 if (value > limit || value < -limit) {
257 return false;
258 }
259 out[row][col] = static_cast<i32>(value);
260 }
261 }
262 return true;
263}
264
267 if (out == nullptr) {
268 return false;
269 }
270 i64 red[3];
271 i64 green[3];
272 i64 blue[3];
273 if (!emitterColumnQ16(profile.xy_r, profile.lum_r, red) ||
274 !emitterColumnQ16(profile.xy_g, profile.lum_g, green) ||
275 !emitterColumnQ16(profile.xy_b, profile.lum_b, blue)) {
276 return false;
277 }
278
279 // Columns are the emitters' XYZ contributions at full drive, the same
280 // arrangement `buildRgbSolveMatrixQ16` builds in float.
281 const i32 emitter[3][3] = {
282 {static_cast<i32>(red[0]), static_cast<i32>(green[0]),
283 static_cast<i32>(blue[0])},
284 {static_cast<i32>(red[1]), static_cast<i32>(green[1]),
285 static_cast<i32>(blue[1])},
286 {static_cast<i32>(red[2]), static_cast<i32>(green[2]),
287 static_cast<i32>(blue[2])},
288 };
289 return invert3x3Q16(emitter, out->m);
290}
291
292bool q16FromFloatBits(float value, i32* out) FL_NO_EXCEPT {
293 if (out == nullptr) {
294 return false;
295 }
296 // Integer arithmetic on the IEEE-754 single-precision bits: sign, 8-bit
297 // biased exponent, 23-bit mantissa. A float->int cast would be a call to
298 // a soft-float helper (__aeabi_f2iz) on a part without an FPU, which is
299 // exactly what the bind path must not reach (FastLED#4458).
300 const u32 bits = fl::bit_cast<u32>(value);
301 const bool negative = (bits >> 31) != 0;
302 const i32 exponent = static_cast<i32>((bits >> 23) & 0xFFu);
303 const u32 fraction = bits & 0x7FFFFFu;
304 if (exponent == 0xFF) {
305 return false; // infinity or NaN
306 }
307 if (exponent == 0) {
308 *out = 0; // zero or subnormal: far below one s16.16 step
309 return true;
310 }
311 // value = mantissa * 2^(exponent - 150); in s16.16 that is
312 // mantissa * 2^(exponent - 134).
313 const u64 mantissa = static_cast<u64>(fraction | 0x800000u);
314 const i32 shift = exponent - 134;
315 u64 magnitude = 0;
316 if (shift >= 0) {
317 if (shift > 7) {
318 return false; // |value| >= 32768: outside s16.16
319 }
320 magnitude = mantissa << shift;
321 } else {
322 const i32 right = -shift;
323 if (right > 40) {
324 magnitude = 0;
325 } else {
326 // Round to nearest, halves away from zero -- the same rounding
327 // the float quantizers this replaces used.
328 magnitude = (mantissa + (static_cast<u64>(1) << (right - 1))) >> right;
329 }
330 }
331 if (magnitude > 0x7FFFFFFFull) {
332 return false;
333 }
334 *out = negative ? -static_cast<i32>(magnitude) : static_cast<i32>(magnitude);
335 return true;
336}
337
338namespace detail {
339
340i64 roundedDivideQ16(i64 numerator, i64 denominator) FL_NO_EXCEPT {
341 return ::fl::roundedDivideQ16(numerator, denominator);
342}
343
344bool xyzColumnQ16(const i32 (&xy)[2], i32 luminance,
345 i64 (&column)[3]) FL_NO_EXCEPT {
346 return emitterColumnQ16(xy, luminance, column);
347}
348
349i32 dotRowQ16(const i32 (&row)[3], const i32 (&v)[3]) FL_NO_EXCEPT {
350 return dotSolveRowQ16(row, v);
351}
352
353} // namespace detail
354
357 if (out == nullptr) {
358 return false;
359 }
360 // The profile stores float; its fields come across by their bits, and
361 // the whole derivation runs in s16.16 (FastLED#4458). The Q16 build
362 // refuses what the float one did: a non-positive or out-of-simplex
363 // chromaticity, a non-positive luminance, a singular matrix -- and NaN,
364 // infinity or an out-of-range value never gets past the conversion.
366 if (!q16FromFloatBits(profile.xy_r[0], &q16.xy_r[0]) ||
367 !q16FromFloatBits(profile.xy_r[1], &q16.xy_r[1]) ||
368 !q16FromFloatBits(profile.xy_g[0], &q16.xy_g[0]) ||
369 !q16FromFloatBits(profile.xy_g[1], &q16.xy_g[1]) ||
370 !q16FromFloatBits(profile.xy_b[0], &q16.xy_b[0]) ||
371 !q16FromFloatBits(profile.xy_b[1], &q16.xy_b[1]) ||
372 !q16FromFloatBits(profile.lum_r, &q16.lum_r) ||
373 !q16FromFloatBits(profile.lum_g, &q16.lum_g) ||
374 !q16FromFloatBits(profile.lum_b, &q16.lum_b)) {
375 return false;
376 }
377 return buildRgbSolveMatrixFromQ16(q16, out);
378}
379
381 const i32 (&xyz)[3], i32 (&drives)[3]) FL_NO_EXCEPT {
382 const i32 in[3] = {xyz[0], xyz[1], xyz[2]};
383 drives[0] = dotSolveRowQ16(matrix.m[0], in);
384 drives[1] = dotSolveRowQ16(matrix.m[1], in);
385 drives[2] = dotSolveRowQ16(matrix.m[2], in);
386}
387
388} // namespace fl
uint32_t z[NUM_LAYERS]
Definition Fire2023.h:93
unsigned int xy(unsigned int x, unsigned int y)
bool cofactorQ32(i32 e, i32 i_, i32 f, i32 h, i64 *out) FL_NO_EXCEPT
e * i - f * h, or false if any part of it leaves i64.
bool emitterColumnQ16(const i32(&xy)[2], i32 luminance, i64(&column)[3]) FL_NO_EXCEPT
One emitter's XYZ column at its own luminance, all in s16.16.
i64 roundedDivideQ16(i64 numerator, i64 denominator) FL_NO_EXCEPT
Nearest integer of numerator / denominator, halves away from zero.
bool sumFitsI64(i64 a, i64 b) FL_NO_EXCEPT
Does a + b fit an i64, for operands already known to be in range?
bool productFitsI64(i64 a, i64 b) FL_NO_EXCEPT
Does a * b fit an i64?
i64 roundedDivI64(i64 numerator, i64 denominator) FL_NO_EXCEPT
Nearest integer, halves away from zero.
i32 dotSolveRowQ16(const i32(&row)[3], const i32(&v)[3]) FL_NO_EXCEPT
i64 divideCoefficientQ16(i64 cofactor_q32, i64 determinant_q48) FL_NO_EXCEPT
round(cofactor_q32 * 2^32 / determinant_q48), in s16.16, without a 128-bit intermediate.
constexpr i32 kMaxCoefficient
Largest coefficient magnitude the build accepts, in whole units.
i32 dotRowQ16(const i32(&row)[3], const i32(&v)[3]) FL_NO_EXCEPT
One s16.16 matrix row against a vector, rounded and saturated.
i64 roundedDivideQ16(i64 numerator, i64 denominator) FL_NO_EXCEPT
The s16.16 building blocks the bind-time builders share.
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...
bool buildRgbSolveMatrixFromQ16(const EmitterChromaticitiesQ16 &profile, EmitterSolveMatrixQ16 *out) FL_NO_EXCEPT
Build the inverse emitter matrix from a Q16 profile, without floats.
constexpr int type_rank< T >::value
InputGamut g FL_NO_EXCEPT
Definition rgbw.h:121
To bit_cast(const From &from) FL_NO_EXCEPT
Definition bit_cast.h:48
InputGamut g
Definition rgbw.h:124
bool invert3x3Q16(const i32(&in)[3][3], i32(&out)[3][3]) FL_NO_EXCEPT
Inverse of an s16.16 3x3 matrix, in s16.16, computed without floats.
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.
Base definition for an LED controller.
Definition crgb.hpp:179
Inverse emitter matrix in s16.16, mapping XYZ to three emitter drives.
A three-emitter profile with every field in s16.16.
fl::u64 u64
Definition stdint.h:220
fl::i64 i64
Definition stdint.h:221