FastLED 3.10.8
Loading...
Searching...
No Matches
rgbw.cpp.hpp
Go to the documentation of this file.
1
3
4#include "fl/stl/stdint.h"
5
6#define FASTLED_INTERNAL
7#include "fl/system/fastled.h"
8
9#include "fl/gfx/rgbw.h"
10#include "fl/log/log.h"
11#include "fl/stl/singleton.h"
12
13#if FASTLED_RGBW_COLORIMETRIC
14// Pulls in the type decls + tiny inline helpers. The heavy solver / LUT /
15// RGBCCT implementations live in rgbw_colorimetric.cpp.hpp, included once
16// by the unity build manifest (fl/gfx/_build.cpp.hpp).
18#include "fl/stl/noexcept.h"
19#endif
20
21
22namespace fl {
23
24// Default diode profile: SK6812 RGBW3535 at ~6000K. Datasheet wavelength
25// midpoints (R 625nm, G 523nm, B 468nm) converted to CIE 1931 xy; W at
26// nominal 6000K (xy = 0.32208, 0.33805). Luminance ratios normalized so
27// W = 1.0; R/G/B set from datasheet typical mcd ratios for a 5050-class
28// part.
29//
30// Source space defaults to **native LED gamut + D65 white** (#2710): the
31// input primaries equal the LED's measured primaries so a saturated input
32// like RGB=(255,0,0) reaches the full saturation of the LED red diode,
33// and RGB=(255,255,255) lands on D65. This is what most FastLED users
34// expect — full use of their hardware's chromatic range. Users who want
35// named-gamut input semantics (Rec709 / sRGB, Rec2020, DCI-P3 D65/D60)
36// should construct an owned profile with
37// `fl::color::make_diode_profile(profile, fl::color::InputGamut::Rec709)`.
38// See #2705 for the source-space transform itself.
40 /* xy_r */ { 0.700606f, 0.299300f },
41 /* xy_g */ { 0.097940f, 0.831593f },
42 /* xy_b */ { 0.129086f, 0.049450f },
43 /* xy_w */ { 0.322080f, 0.338050f },
44 /* lum_r */ 0.10f,
45 /* lum_g */ 0.37f,
46 /* lum_b */ 0.08f,
47 /* lum_w */ 1.00f,
48 /* nominal_cct */ 6000,
49 /* input_xy_r */ { 0.700606f, 0.299300f }, // native LED R
50 /* input_xy_g */ { 0.097940f, 0.831593f }, // native LED G
51 /* input_xy_b */ { 0.129086f, 0.049450f }, // native LED B
52 /* input_xy_w */ { 0.31272f, 0.32903f }, // D65 white
53};
54
55
56namespace {
57// Not named min3: ArduinoCore-zephyr's <zephyr/sys/util.h> defines min3() as
58// a function-like macro, which would expand this declaration (#4416).
59inline u8 rgbw_min3(u8 a, u8 b, u8 c) FL_NO_EXCEPT {
60 if (a < b) {
61 if (a < c) {
62 return a;
63 } else {
64 return c;
65 }
66 } else {
67 if (b < c) {
68 return b;
69 } else {
70 return c;
71 }
72 }
73}
74
76 u16 y = (u16(x) * 85) >> 8;
77 return static_cast<u8>(y);
78}
79} // namespace
80
81// @brief Converts RGB to RGBW using a color transfer method
82// from color channels to 3x white.
83// @author Jonathanese
84void rgb_2_rgbw_exact(u16 w_color_temperature, u8 r, u8 g,
85 u8 b, u8 r_scale, u8 g_scale,
86 u8 b_scale, u8 *out_r, u8 *out_g,
87 u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
88 (void)w_color_temperature;
89 r = scale8(r, r_scale);
90 g = scale8(g, g_scale);
91 b = scale8(b, b_scale);
92 u8 min_component = rgbw_min3(r, g, b);
93 *out_r = r - min_component;
94 *out_g = g - min_component;
95 *out_b = b - min_component;
96 *out_w = min_component;
97}
98
99void rgb_2_rgbw_max_brightness(u16 w_color_temperature, u8 r,
100 u8 g, u8 b, u8 r_scale,
101 u8 g_scale, u8 b_scale, u8 *out_r,
102 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
103 (void)w_color_temperature;
104 *out_r = scale8(r, r_scale);
105 *out_g = scale8(g, g_scale);
106 *out_b = scale8(b, b_scale);
107 *out_w = rgbw_min3(*out_r, *out_g, *out_b);
108}
109
110void rgb_2_rgbw_null_white_pixel(u16 w_color_temperature, u8 r,
111 u8 g, u8 b, u8 r_scale,
112 u8 g_scale, u8 b_scale,
113 u8 *out_r, u8 *out_g, u8 *out_b,
114 u8 *out_w) FL_NO_EXCEPT {
115 (void)w_color_temperature;
116 *out_r = scale8(r, r_scale);
117 *out_g = scale8(g, g_scale);
118 *out_b = scale8(b, b_scale);
119 *out_w = 0;
120}
121
122void rgb_2_rgbw_white_boosted(u16 w_color_temperature, u8 r,
123 u8 g, u8 b, u8 r_scale,
124 u8 g_scale, u8 b_scale, u8 *out_r,
125 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
126 (void)w_color_temperature;
127 r = scale8(r, r_scale);
128 g = scale8(g, g_scale);
129 b = scale8(b, b_scale);
130 u8 min_component = rgbw_min3(r, g, b);
131 u8 w;
132 bool is_min = true;
133 if (min_component <= 84) {
134 w = 3 * min_component;
135 } else {
136 w = 255;
137 is_min = false;
138 }
139 u8 r_prime;
140 u8 g_prime;
141 u8 b_prime;
142 if (is_min) {
143 r_prime = r - min_component;
144 g_prime = g - min_component;
145 b_prime = b - min_component;
146 } else {
147 u8 w3 = divide_by_3(w);
148 r_prime = r - w3;
149 g_prime = g - w3;
150 b_prime = b - w3;
151 }
152
153 *out_r = r_prime;
154 *out_g = g_prime;
155 *out_b = b_prime;
156 *out_w = w;
157}
158
159// User-installable RGB→RGBW function pointer, held behind a lazily-constructed
160// Singleton<T>. Replaces the previous static-init form
161// (`rgb_2_rgbw_function g_user_function = rgb_2_rgbw_exact`), which forced
162// `rgb_2_rgbw_exact` to be ODR-used at static init even when no caller ever
163// reached the kRGBWUserFunction code path.
164//
165// The Singleton's instance is constructed on first call to set_… or
166// rgb_2_rgbw_user_function below — never at static init. The default value of
167// `fn` is `nullptr`; rgb_2_rgbw_user_function uses `rgb_2_rgbw_exact` as the
168// fallback algorithm when nothing has been installed.
169//
170// See issue #2424 for the broader binary-bloat investigation.
171namespace {
175} // namespace
176
180
181void rgb_2_rgbw_user_function(u16 w_color_temperature, u8 r,
182 u8 g, u8 b, u8 r_scale,
183 u8 g_scale, u8 b_scale, u8 *out_r,
184 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
186 if (fn == nullptr) {
187 fn = rgb_2_rgbw_exact;
188 }
189 fn(w_color_temperature, r, g, b, r_scale, g_scale, b_scale,
190 out_r, out_g, out_b, out_w);
191}
192
193// Active colorimetric profile pointer. Defaults to &kRgbwDefaultProfile on
194// first access. Held behind a lazy Singleton<T> (same pattern as the user
195// function above) so neither the profile constant nor the singleton itself
196// is ODR-used at static init when no colorimetric code path is invoked.
197namespace {
199 const DiodeProfile* profile = nullptr;
200};
201} // namespace
202
209
215
216// Standard primary chromaticities for the named input gamuts (#2710). The
217// numbers come straight from each gamut's published spec:
218// Rec.709 / sRGB — ITU-R BT.709, IEC 61966-2-1
219// Rec.2020 — ITU-R BT.2020
220// DCI-P3 D65 — SMPTE EG 432-1 (consumer display variant)
221// DCI-P3 D60 (ACES) — ACES AP1 white = (0.32168, 0.33767)
222// White-point chromaticities likewise: D65 = (0.31272, 0.32903),
223// ACES D60 = (0.32168, 0.33767).
224namespace {
226 float xy_r[2];
227 float xy_g[2];
228 float xy_b[2];
229 float xy_w[2];
230};
231constexpr NamedGamut kRec709 = {{0.6400f, 0.3300f}, {0.3000f, 0.6000f},
232 {0.1500f, 0.0600f}, {0.31272f, 0.32903f}};
233constexpr NamedGamut kRec2020 = {{0.7080f, 0.2920f}, {0.1700f, 0.7970f},
234 {0.1310f, 0.0460f}, {0.31272f, 0.32903f}};
235constexpr NamedGamut kDciP3D65 = {{0.6800f, 0.3200f}, {0.2650f, 0.6900f},
236 {0.1500f, 0.0600f}, {0.31272f, 0.32903f}};
237constexpr NamedGamut kDciP3D60 = {{0.6800f, 0.3200f}, {0.2650f, 0.6900f},
238 {0.1500f, 0.0600f}, {0.32168f, 0.33767f}};
239} // namespace
240
241// Forward declaration: real implementation lives inside the colorimetric
242// branch below (and is a no-op when colorimetric math is compiled out).
243// Called from set_input_gamut to evict per-process caches keyed on a profile
244// whose input_xy_* fields were just mutated in place — without this hook the
245// pointer+CCT cache key stays equal and stale M_src/LUT data is reused
246// after a gamut switch on the currently active profile.
247namespace { void invalidate_colorimetric_caches_for(const DiodeProfile* profile) FL_NO_EXCEPT; }
248
250 const float* white_xy) FL_NO_EXCEPT {
251 DiodeProfile* const profile = &profile_ref;
252 auto apply = [profile](const float r[2], const float gp[2],
253 const float b[2], const float w[2]) {
254 profile->input_xy_r[0] = r[0]; profile->input_xy_r[1] = r[1];
255 profile->input_xy_g[0] = gp[0]; profile->input_xy_g[1] = gp[1];
256 profile->input_xy_b[0] = b[0]; profile->input_xy_b[1] = b[1];
257 profile->input_xy_w[0] = w[0]; profile->input_xy_w[1] = w[1];
258 };
259 switch (g) {
260 case InputGamut::Native: {
261 // For Native, the input gamut tracks this profile's LED primaries —
262 // copy them over rather than picking a fixed sRGB-like fallback.
263 const float d65[2] = {0.31272f, 0.32903f};
264 const float* w = (white_xy != nullptr) ? white_xy : d65;
265 apply(profile->xy_r, profile->xy_g, profile->xy_b, w);
266 invalidate_colorimetric_caches_for(profile);
267 return;
268 }
269 case InputGamut::Rec709: apply(kRec709.xy_r, kRec709.xy_g, kRec709.xy_b,
270 white_xy != nullptr ? white_xy : kRec709.xy_w);
271 invalidate_colorimetric_caches_for(profile); return;
272 case InputGamut::Rec2020: apply(kRec2020.xy_r, kRec2020.xy_g, kRec2020.xy_b,
273 white_xy != nullptr ? white_xy : kRec2020.xy_w);
274 invalidate_colorimetric_caches_for(profile); return;
275 case InputGamut::DciP3D65: apply(kDciP3D65.xy_r, kDciP3D65.xy_g, kDciP3D65.xy_b,
276 white_xy != nullptr ? white_xy : kDciP3D65.xy_w);
277 invalidate_colorimetric_caches_for(profile); return;
278 case InputGamut::DciP3D60: apply(kDciP3D60.xy_r, kDciP3D60.xy_g, kDciP3D60.xy_b,
279 white_xy != nullptr ? white_xy : kDciP3D60.xy_w);
280 invalidate_colorimetric_caches_for(profile); return;
281 }
282 // Default-fallthrough for forward-compat with future enum additions:
283 // leave the profile's input_xy_* untouched — also nothing to invalidate.
284}
285
287 set_input_gamut(profile, g, nullptr);
288}
289
290// Deprecated pointer forwarders (FastLED#3234). The historical contract was a
291// silent no-op on nullptr, so that is preserved here rather than dereferencing
292// -- a sketch that relied on it must not start crashing at the deprecation.
294 const float white_xy[2]) FL_NO_EXCEPT {
295 if (profile == nullptr) return;
296 set_input_gamut(*profile, g, white_xy);
297}
298
300 if (profile == nullptr) return;
301 set_input_gamut(*profile, g, nullptr);
302}
303
309
314
316 const DiodeProfile& profile, InputGamut g,
317 const float white_xy[2]) FL_NO_EXCEPT {
318 DiodeProfile copy = profile;
319 set_input_gamut(copy, g, white_xy);
320 return make_diode_profile(copy);
321}
322
323#if FASTLED_RGBW_COLORIMETRIC
324
325namespace {
326// Per-process cached profile data (XYZ columns + matrix inverses). Rebuilt
327// whenever the active profile pointer OR the requested CCT changes. The CCT
328// part of the key is what makes Rgbw::white_color_temp actually shift the
329// W vertex chromaticity at solve time.
330struct ColorimetricCacheHolder {
331 colorimetric_detail::ProfileCache cache;
332 const DiodeProfile* cached_for = nullptr;
333 int cached_cct = 0;
334};
335
336// Resolve the CCT to use for the W vertex: if the dispatch passed a value in
337// the valid Krystek range AND it differs from the profile's nominal CCT,
338// shift W. Otherwise use the profile's xy_w as-is (cct_override = 0).
339inline int resolve_cct_override(const DiodeProfile* p, int requested_cct) FL_NO_EXCEPT {
340 if (requested_cct < 1500 || requested_cct > 15000) return 0;
341 if (requested_cct == p->nominal_cct) return 0;
342 return requested_cct;
343}
344
345inline const DiodeProfile* resolve_rgbw_profile(
346 const DiodeProfile* profile) FL_NO_EXCEPT {
347 return profile != nullptr ? profile : get_rgbw_colorimetric_profile();
348}
349
350inline const DiodeProfile* resolve_rgbw_profile(
351 const fl::shared_ptr<const DiodeProfile>& profile) FL_NO_EXCEPT {
352 return resolve_rgbw_profile(profile.get());
353}
354
355inline const colorimetric_detail::ProfileCache& get_cache(
356 const DiodeProfile* profile, int cct) FL_NO_EXCEPT {
357 ColorimetricCacheHolder& h =
359 const DiodeProfile* active = resolve_rgbw_profile(profile);
360 const int override_cct = resolve_cct_override(active, cct);
361 if (h.cached_for != active || h.cached_cct != override_cct) {
362 colorimetric_detail::build_profile_cache(active, override_cct, &h.cache);
363 h.cached_for = active;
364 h.cached_cct = override_cct;
365 }
366 return h.cache;
367}
368
369inline const colorimetric_detail::ProfileCache& get_cache(
370 const fl::shared_ptr<const DiodeProfile>& profile, int cct) FL_NO_EXCEPT {
371 return get_cache(resolve_rgbw_profile(profile), cct);
372}
373
374inline const colorimetric_detail::ProfileCache& get_cache(int cct) FL_NO_EXCEPT {
375 return get_cache(static_cast<const DiodeProfile*>(nullptr), cct);
376}
377
378// ===== LUT state (issue #2545 Phase 2) ==================================
379// Always-compiled when the outer FASTLED_RGBW_COLORIMETRIC gate is on. The
380// inner FASTLED_RGBW_COLORIMETRIC_LUT gate that used to wrap this block was
381// redundant — once you've opted into colorimetric math, the LUT state +
382// rebuild_lut_if_stale + build_lut reference are gc-section-droppable for
383// sketches that never call enable_rgbw_colorimetric_lut() (the `enabled`
384// flag stays false, the fl::unique_ptr<LutTable> stays empty, and only the
385// LutStateHolder singleton instance itself is retained — ~32 bytes).
386struct LutStateHolder {
387 fl::unique_ptr<colorimetric_detail::LutTable> table;
388 const DiodeProfile* built_for = nullptr;
389 int built_cct = 0;
390 int requested_grid_n = 0;
391 RgbwLutInterp requested_interp = RgbwLutInterp::Hermite;
392 bool enabled = false;
393};
394
395inline colorimetric_detail::LutInterp to_internal_interp(
397 return (i == RgbwLutInterp::Hermite)
398 ? colorimetric_detail::LutInterp::Hermite
399 : colorimetric_detail::LutInterp::Bilinear;
400}
401
402inline void rebuild_lut_if_stale(LutStateHolder& s, const DiodeProfile* profile,
403 int cct) FL_NO_EXCEPT {
404 if (!s.enabled || s.requested_grid_n <= 0) return;
405 const DiodeProfile* active = resolve_rgbw_profile(profile);
406 const int override_cct = resolve_cct_override(active, cct);
407 const colorimetric_detail::LutInterp interp =
408 to_internal_interp(s.requested_interp);
409 if (s.table && s.built_for == active && s.built_cct == override_cct
410 && s.table->N == s.requested_grid_n
411 && s.table->interp == interp) {
412 return; // up-to-date
413 }
415 colorimetric_detail::build_lut(get_cache(active, cct), s.requested_grid_n,
416 interp));
417 s.built_for = active;
418 s.built_cct = override_cct;
419}
420
421inline void rebuild_lut_if_stale(LutStateHolder& s,
422 const fl::shared_ptr<const DiodeProfile>& profile,
423 int cct) FL_NO_EXCEPT {
424 rebuild_lut_if_stale(s, resolve_rgbw_profile(profile), cct);
425}
426
427inline void rebuild_lut_if_stale(LutStateHolder& s, int cct) FL_NO_EXCEPT {
428 rebuild_lut_if_stale(s, static_cast<const DiodeProfile*>(nullptr), cct);
429}
430
431// Drop both the ProfileCache and the LUT cache when `profile` matches the
432// current cache key. set_input_gamut() mutates input_xy_* in place without
433// touching the profile pointer or CCT, so the (pointer, cct) cache key
434// stays equal and would otherwise serve stale M_src / LUT data.
436 ColorimetricCacheHolder& ch =
438 if (ch.cached_for == profile) {
439 ch.cached_for = nullptr;
440 ch.cached_cct = 0;
441 }
442 LutStateHolder& lh = fl::Singleton<LutStateHolder>::instance();
443 if (lh.built_for == profile) {
444 lh.built_for = nullptr;
445 lh.built_cct = 0;
446 }
447}
448} // namespace
449
451 if (cfg.rgbw_mode != RGBW_MODE::kRGBWColorimetric
452 && cfg.rgbw_mode != RGBW_MODE::kRGBWColorimetricBoosted) {
453 return;
454 }
455 (void)get_cache(cfg.profile, cfg.white_color_temp);
456 LutStateHolder& lut_state = fl::Singleton<LutStateHolder>::instance();
457 rebuild_lut_if_stale(lut_state, cfg.profile, cfg.white_color_temp);
458}
459
460void rgb_2_rgbw_colorimetric(u16 w_color_temperature, u8 r,
461 u8 g, u8 b, u8 r_scale,
462 u8 g_scale, u8 b_scale, u8 *out_r,
463 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
464 Rgbw cfg(w_color_temperature, RGBW_MODE::kRGBWColorimetric);
465 rgb_2_rgbw_colorimetric(cfg, r, g, b, r_scale, g_scale, b_scale,
466 out_r, out_g, out_b, out_w);
467}
468
469void rgb_2_rgbw_colorimetric(const Rgbw& cfg, u8 r,
470 u8 g, u8 b, u8 r_scale,
471 u8 g_scale, u8 b_scale, u8 *out_r,
472 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
473 const int w_color_temperature = cfg.white_color_temp;
474 r = scale8(r, r_scale);
475 g = scale8(g, g_scale);
476 b = scale8(b, b_scale);
477 if ((r | g | b) == 0) {
478 *out_r = *out_g = *out_b = *out_w = 0;
479 return;
480 }
481 const float s_r = r * (1.0f / 255.0f);
482 const float s_g = g * (1.0f / 255.0f);
483 const float s_b = b * (1.0f / 255.0f);
485 get_cache(cfg.profile, w_color_temperature);
486 float rgbw[4];
487
488 // LUT fast path — gc-sections drops the branch + singleton + lookup_lut
489 // for sketches that never call enable_rgbw_colorimetric_lut().
490 LutStateHolder& lut_state = fl::Singleton<LutStateHolder>::instance();
491 if (lut_state.enabled) {
492 // The xy/Y LUT has no source active-channel mask, so it cannot enforce
493 // native single-axis or dual-edge topology on its own. Keep those rows
494 // on the analytical guard path and reserve LUT lookup for interiors.
496 && colorimetric_detail::count_active_channels(s_r, s_g, s_b) < 3) {
498 cache, s_r, s_g, s_b, rgbw);
499 if (!ok) {
500 rgb_2_rgbw_exact(w_color_temperature, r, g, b, 255, 255, 255,
501 out_r, out_g, out_b, out_w);
502 return;
503 }
508 return;
509 }
510 rebuild_lut_if_stale(lut_state, cfg.profile, w_color_temperature);
511 float X_t[3];
512 if (cache.has_source_space) {
513 // #2705: use source-space matrix so the LUT lookup targets the
514 // same chromaticity as the closed-form solver.
515 const float s_vec[3] = { s_r, s_g, s_b };
516 colorimetric_detail::matvec3(cache.M_src, s_vec, X_t);
517 } else {
518 X_t[0] = cache.P_R[0] * s_r + cache.P_G[0] * s_g + cache.P_B[0] * s_b;
519 X_t[1] = cache.P_R[1] * s_r + cache.P_G[1] * s_g + cache.P_B[1] * s_b;
520 X_t[2] = cache.P_R[2] * s_r + cache.P_G[2] * s_g + cache.P_B[2] * s_b;
521 }
522 const float sum = X_t[0] + X_t[1] + X_t[2];
523 if (sum < 1e-9f) {
524 *out_r = *out_g = *out_b = *out_w = 0;
525 return;
526 }
527 const float xy_t[2] = { X_t[0] / sum, X_t[1] / sum };
528 const float source_value = fl::max(fl::max(s_r, s_g), s_b);
529 colorimetric_detail::lookup_lut(*lut_state.table, xy_t, source_value, rgbw);
534 return;
535 }
536
537 const bool ok =
539 if (!ok) {
540 rgb_2_rgbw_exact(w_color_temperature, r, g, b, 255, 255, 255,
541 out_r, out_g, out_b, out_w);
542 return;
543 }
548}
549
550void rgb_2_rgbw_colorimetric_boosted(u16 w_color_temperature, u8 r,
551 u8 g, u8 b, u8 r_scale,
552 u8 g_scale, u8 b_scale, u8 *out_r,
553 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
554 Rgbw cfg(w_color_temperature, RGBW_MODE::kRGBWColorimetricBoosted);
555 rgb_2_rgbw_colorimetric_boosted(cfg, r, g, b, r_scale, g_scale, b_scale,
556 out_r, out_g, out_b, out_w);
557}
558
559void rgb_2_rgbw_colorimetric_boosted(const Rgbw& cfg, u8 r,
560 u8 g, u8 b, u8 r_scale,
561 u8 g_scale, u8 b_scale, u8 *out_r,
562 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
563 r = scale8(r, r_scale);
564 g = scale8(g, g_scale);
565 b = scale8(b, b_scale);
566 if ((r | g | b) == 0) {
567 *out_r = *out_g = *out_b = *out_w = 0;
568 return;
569 }
570 const float s_r = r * (1.0f / 255.0f);
571 const float s_g = g * (1.0f / 255.0f);
572 const float s_b = b * (1.0f / 255.0f);
573 float rgbw[4];
575 get_cache(cfg.profile, cfg.white_color_temp),
576 s_r, s_g, s_b,
578 rgbw);
583}
584
585bool enable_rgbw_colorimetric_lut(int grid_n,
587 if (grid_n < 4) grid_n = 4;
588 if (grid_n > 256) grid_n = 256;
589 LutStateHolder& s = fl::Singleton<LutStateHolder>::instance();
590 s.enabled = true;
591 s.requested_grid_n = grid_n;
592 s.requested_interp = interp;
593 // Force a rebuild on next colorimetric call.
594 s.built_for = nullptr;
595 s.built_cct = -1;
596 return true;
597}
598
601}
602
604 LutStateHolder& s = fl::Singleton<LutStateHolder>::instance();
605 s.enabled = false;
606 s.table.reset(); // unique_ptr destructor frees the cells storage
607 s.requested_grid_n = 0;
608 s.requested_interp = RgbwLutInterp::Hermite;
609 s.built_for = nullptr;
610 s.built_cct = 0;
611}
612
615}
616
617#else // FASTLED_RGBW_COLORIMETRIC
618
619// No-op cache invalidation stub — the colorimetric cache machinery doesn't
620// exist when FASTLED_RGBW_COLORIMETRIC=0, so set_input_gamut has nothing to
621// evict. Keeping the forward-declared symbol available means the dispatch
622// path above doesn't need its own #if gate.
624
625// Stub APIs for the LUT/CCT/RGBCCT path — no-ops when colorimetric is off.
627
629 RgbwLutInterp /*interp*/) FL_NO_EXCEPT {
630 return false;
631}
632bool enable_rgbw_colorimetric_lut(int /*grid_n*/) FL_NO_EXCEPT { return false; }
635
636// Stub path: warn-once + fall back to rgb_2_rgbw_exact. Does not pull in the
637// simplex solver, profile cache, or float math machinery.
638void rgb_2_rgbw_colorimetric(u16 w_color_temperature, u8 r,
639 u8 g, u8 b, u8 r_scale,
640 u8 g_scale, u8 b_scale, u8 *out_r,
641 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
642#ifndef FASTLED_SUPPRESS_COLORIMETRIC_FALLBACK_WARNING
643 FL_WARN_ONCE("RGBW: kRGBWColorimetric requested but FASTLED_RGBW_COLORIMETRIC is not defined — falling back to kRGBWExactColors. Define FASTLED_RGBW_COLORIMETRIC=1 to enable the colorimetric path.");
644#endif
645 rgb_2_rgbw_exact(w_color_temperature, r, g, b, r_scale, g_scale, b_scale,
646 out_r, out_g, out_b, out_w);
647}
648
649void rgb_2_rgbw_colorimetric_boosted(u16 w_color_temperature, u8 r,
650 u8 g, u8 b, u8 r_scale,
651 u8 g_scale, u8 b_scale, u8 *out_r,
652 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
653#ifndef FASTLED_SUPPRESS_COLORIMETRIC_FALLBACK_WARNING
654 FL_WARN_ONCE("RGBW: kRGBWColorimetricBoosted requested but FASTLED_RGBW_COLORIMETRIC is not defined — falling back to kRGBWExactColors. Define FASTLED_RGBW_COLORIMETRIC=1 to enable the colorimetric path.");
655#endif
656 rgb_2_rgbw_exact(w_color_temperature, r, g, b, r_scale, g_scale, b_scale,
657 out_r, out_g, out_b, out_w);
658}
659
660void rgb_2_rgbw_colorimetric(const Rgbw& cfg, u8 r,
661 u8 g, u8 b, u8 r_scale,
662 u8 g_scale, u8 b_scale, u8 *out_r,
663 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
664 rgb_2_rgbw_colorimetric(cfg.white_color_temp, r, g, b, r_scale, g_scale,
665 b_scale, out_r, out_g, out_b, out_w);
666}
667
669 u8 g, u8 b, u8 r_scale,
670 u8 g_scale, u8 b_scale, u8 *out_r,
671 u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT {
672 rgb_2_rgbw_colorimetric_boosted(cfg.white_color_temp, r, g, b, r_scale,
673 g_scale, b_scale, out_r, out_g, out_b,
674 out_w);
675}
676
677#endif // FASTLED_RGBW_COLORIMETRIC
678
679void rgbw_partial_reorder(EOrderW w_placement, u8 b0, u8 b1,
680 u8 b2, u8 w, u8 *out_b0,
681 u8 *out_b1, u8 *out_b2, u8 *out_b3) FL_NO_EXCEPT {
682
683 u8 out[4] = {b0, b1, b2, 0};
684 switch (w_placement) {
685 // unrolled loop for speed.
686 case W3:
687 out[3] = w;
688 break;
689 case W2:
690 out[3] = out[2]; // memmove and copy.
691 out[2] = w;
692 break;
693 case W1:
694 out[3] = out[2];
695 out[2] = out[1];
696 out[1] = w;
697 break;
698 case W0:
699 out[3] = out[2];
700 out[2] = out[1];
701 out[1] = out[0];
702 out[0] = w;
703 break;
704 }
705 *out_b0 = out[0];
706 *out_b1 = out[1];
707 *out_b2 = out[2];
708 *out_b3 = out[3];
709}
710
711namespace detail {
712namespace {
714 static RgbwConvertFn slot = nullptr; // zero-initialised before any ctor
715 return slot;
716}
717
718void rgbw_convert_pixel(const Rgbw& rgbw, const u8 (&raw)[3],
719 const u8 (&premix)[3], const u8 (&order)[3], u8* b0, u8* b1, u8* b2,
720 u8* b3) FL_NO_EXCEPT {
721 // Same steps as PixelController::loadAndScaleRGBW: source-order RGB plus
722 // W from the raw pixel and premixed scale, then the wire reorder.
723 u8 c[3];
724 u8 w = 0;
725 rgb_2_rgbw(rgbw, raw[0], raw[1], raw[2], premix[0], premix[1], premix[2],
726 &c[0], &c[1], &c[2], &w);
727 rgbw_partial_reorder(rgbw.w_placement, c[order[0]], c[order[1]],
728 c[order[2]], w, b0, b1, b2, b3);
729}
730} // namespace
731
733#if !defined(FL_IS_AVR)
734 // AVR's PixelIterator keeps its own thunk and never reads the slot.
735 rgbw_convert_slot() = &rgbw_convert_pixel;
736#endif
737}
738RgbwConvertFn rgbw_conversion() FL_NO_EXCEPT { return rgbw_convert_slot(); }
739} // namespace detail
740
741} // namespace fl
Rgbw rgbw
static T & instance() FL_NO_EXCEPT
Definition singleton.h:41
Functions for red, green, blue, white (RGBW) output.
Internal FastLED header for implementation files.
#define FL_WARN_ONCE(...)
Definition log.h:439
Centralized logging categories for FastLED hardware interfaces and subsystems.
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NO_EXCEPT
Definition unique_ptr.h:263
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:423
u8 rgbw_min3(u8 a, u8 b, u8 c) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:59
void invalidate_colorimetric_caches_for(const DiodeProfile *profile) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:623
u8 divide_by_3(u8 x) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:75
void solve_wx_overdrive(const ProfileCache &cache, float s_r, float s_g, float s_b, float overdrive_ratio, float out_rgbw[4]) FL_NO_EXCEPT
int count_active_channels(float s_r, float s_g, float s_b) FL_NO_EXCEPT
u8 quantize_u8(float v) FL_NO_EXCEPT
bool is_native_input_gamut(const DiodeProfile &p) FL_NO_EXCEPT
void matvec3(const float M[3][3], const float v[3], float out[3]) FL_NO_EXCEPT
LutTable build_lut(const ProfileCache &cache, int grid_n, LutInterp interp) FL_NO_EXCEPT
void lookup_lut(const LutTable &lut, const float xy_t[2], float source_value, float out_rgbw[4]) FL_NO_EXCEPT
constexpr float kDefaultOverdriveRatio
bool solve_strict_subgamut(const ProfileCache &cache, float s_r, float s_g, float s_b, float out_rgbw[4]) FL_NO_EXCEPT
void build_profile_cache(const DiodeProfile *p, int cct_override, ProfileCache *cache) FL_NO_EXCEPT
RgbwConvertFn & rgbw_convert_slot() FL_NO_EXCEPT
Definition rgbw.cpp.hpp:713
void rgbw_convert_pixel(const Rgbw &rgbw, const u8(&raw)[3], const u8(&premix)[3], const u8(&order)[3], u8 *b0, u8 *b1, u8 *b2, u8 *b3) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:718
void(*)(const Rgbw &rgbw, const u8(&raw)[3], const u8(&premix)[3], const u8(&order)[3], u8 *b0, u8 *b1, u8 *b2, u8 *b3) RgbwConvertFn
Per-pixel RGBW conversion: raw source-order RGB + premixed scale -> 4 wire bytes.
Definition rgbw.h:220
void enable_rgbw_conversion() FL_NO_EXCEPT
Installs the RGBW pixel conversion used by PixelIterator.
Definition rgbw.cpp.hpp:732
RgbwConvertFn rgbw_conversion() FL_NO_EXCEPT
Null until enable_rgbw_conversion() has run.
Definition rgbw.cpp.hpp:738
void rgb_2_rgbw_user_function(u16 w_color_temperature, u8 r, u8 g, u8 b, u8 r_scale, u8 g_scale, u8 b_scale, u8 *out_r, u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:181
unsigned char u8
Definition stdint.h:131
constexpr common_type_t< T, U > max(T a, U b) FL_NO_EXCEPT
Definition math.h:75
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
void rgb_2_rgbw_null_white_pixel(u16 w_color_temperature, u8 r, u8 g, u8 b, u8 r_scale, u8 g_scale, u8 b_scale, u8 *out_r, u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT
Converts RGB to RGBW with the W channel set to black, always.
Definition rgbw.cpp.hpp:110
void rgb_2_rgbw_colorimetric(u16 w_color_temperature, u8 r, u8 g, u8 b, u8 r_scale, u8 g_scale, u8 b_scale, u8 *out_r, u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:638
@ kRGBWDefaultColorTemp
Definition rgbw.h:206
const DiodeProfile kRgbwDefaultProfile
Definition rgbw.cpp.hpp:39
void rgbw_partial_reorder(EOrderW w_placement, u8 b0, u8 b1, u8 b2, u8 w, u8 *out_b0, u8 *out_b1, u8 *out_b2, u8 *out_b3) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:679
void prepare_rgbw_colorimetric(const Rgbw &) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:626
FASTLED_FORCE_INLINE void rgb_2_rgbw(const Rgbw &cfg, u8 r, u8 g, u8 b, u8 r_scale, u8 g_scale, u8 b_scale, u8 *out_r, u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT
Converts RGB to RGBW using one of the functions.
Definition rgbw.h:406
InputGamut
Definition rgbw.h:80
fl::shared_ptr< const DiodeProfile > make_diode_profile(const DiodeProfile &profile) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:304
void rgb_2_rgbw_exact(u16 w_color_temperature, u8 r, u8 g, u8 b, u8 r_scale, u8 g_scale, u8 b_scale, u8 *out_r, u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT
Converts RGB to RGBW using a color transfer method from saturated color channels to white.
Definition rgbw.cpp.hpp:84
bool rgbw_colorimetric_lut_enabled() FL_NO_EXCEPT
Definition rgbw.cpp.hpp:634
RgbwLutInterp
Definition rgbw.h:157
void(* rgb_2_rgbw_function)(u16 w_color_temperature, u8 r, u8 g, u8 b, u8 r_scale, u8 g_scale, u8 b_scale, u8 *out_r, u8 *out_g, u8 *out_b, u8 *out_w)
Definition rgbw.h:312
void disable_rgbw_colorimetric_lut() FL_NO_EXCEPT
Definition rgbw.cpp.hpp:633
const DiodeProfile * get_rgbw_colorimetric_profile() FL_NO_EXCEPT
Definition rgbw.cpp.hpp:210
void rgb_2_rgbw_max_brightness(u16 w_color_temperature, u8 r, u8 g, u8 b, u8 r_scale, u8 g_scale, u8 b_scale, u8 *out_r, u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT
The minimum brigthness of the RGB channels is used to set the W channel.
Definition rgbw.cpp.hpp:99
void set_rgbw_colorimetric_profile(const DiodeProfile *profile) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:203
InputGamut g
Definition rgbw.h:125
void rgb_2_rgbw_colorimetric_boosted(u16 w_color_temperature, u8 r, u8 g, u8 b, u8 r_scale, u8 g_scale, u8 b_scale, u8 *out_r, u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:649
@ kRGBWColorimetricBoosted
Definition rgbw.h:27
@ kRGBWColorimetric
Definition rgbw.h:26
shared_ptr< T > static_pointer_cast(const shared_ptr< Y > &other) FL_NO_EXCEPT
Definition shared_ptr.h:556
EOrderW
Definition eorder.h:25
@ W1
White is second.
Definition eorder.h:28
@ W2
White is third.
Definition eorder.h:27
@ W3
White is fourth.
Definition eorder.h:26
@ W0
White is first.
Definition eorder.h:29
void set_input_gamut(DiodeProfile &profile_ref, InputGamut g, const float *white_xy) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:249
bool enable_rgbw_colorimetric_lut(int, RgbwLutInterp) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:628
void set_rgb_2_rgbw_function(rgb_2_rgbw_function func) FL_NO_EXCEPT
Definition rgbw.cpp.hpp:177
void rgb_2_rgbw_white_boosted(u16 w_color_temperature, u8 r, u8 g, u8 b, u8 r_scale, u8 g_scale, u8 b_scale, u8 *out_r, u8 *out_g, u8 *out_b, u8 *out_w) FL_NO_EXCEPT
Converts RGB to RGBW with a boosted white channel.
Definition rgbw.cpp.hpp:122
Base definition for an LED controller.
Definition crgb.hpp:179
float input_xy_b[2]
Definition rgbw.h:71
float xy_g[2]
Definition rgbw.h:59
float input_xy_r[2]
Definition rgbw.h:69
float xy_b[2]
Definition rgbw.h:60
float input_xy_g[2]
Definition rgbw.h:70
float input_xy_w[2]
Definition rgbw.h:72
float xy_r[2]
Definition rgbw.h:58
Chromaticity-aware RGBW solvers — strict sub-gamut + wx_lp_legacy white extraction + boosted overdriv...
#define FL_NO_EXCEPT