FastLED 3.9.15
Loading...
Searching...
No Matches
rgbww.cpp.hpp
Go to the documentation of this file.
1
16
17#include "fl/stl/stdint.h"
18
19#define FASTLED_INTERNAL
20#include "fl/system/fastled.h"
21
22#include "fl/gfx/rgbww.h"
23// rgbw_colorimetric.h carries the RgbcctProfile type definition that
24// kRgbwwDefaultProfile needs, plus the inline math primitives Phase D uses.
25// The non-inline solve_rgbcct symbol itself only exists when
26// FASTLED_RGBW_COLORIMETRIC is defined at library build time — see the gated
27// dispatch bodies further down.
29#include "fl/log/log.h"
30#include "fl/stl/singleton.h"
31
32
33namespace fl {
34
35namespace {
36inline void zero_out(u8 *r, u8 *g, u8 *b, u8 *ww, u8 *wc) FL_NOEXCEPT {
37 *r = 0; *g = 0; *b = 0; *ww = 0; *wc = 0;
38}
39} // namespace
40
41// ===== Default profile + active-profile state ==============================
42//
43// kRgbwwDefaultProfile: two DiodeProfile entries derived from SK6812-RGBWW-
44// style datasheets. R/G/B chromaticities and luminances mirror
45// kRgbwDefaultProfile (the colorimetric 4-channel default); W vertex is
46// hardcoded to the Planckian xy at 2700K (warm) and 6500K (cool) so static
47// initialization stays trivial (no CCT conversion at init time).
48// 2700K Planckian xy ≈ (0.4600, 0.4107) (matches cct_to_xy(2700) ± 0.002)
49// 6500K Planckian xy ≈ (0.3135, 0.3237) (matches cct_to_xy(6500) ± 0.002)
50// Default profile: native LED gamut + D65 source white on both warm and cool
51// paths (#2710). Users wanting Rec709 / Rec2020 / DCI-P3 input semantics
52// should call `fl::set_input_gamut()` on their per-strip profile copy.
54 /* warm_path */ {
55 /* xy_r */ { 0.700606f, 0.299300f },
56 /* xy_g */ { 0.097940f, 0.831593f },
57 /* xy_b */ { 0.129086f, 0.049450f },
58 /* xy_w (2700) */ { 0.460000f, 0.410700f },
59 /* lum_r */ 0.10f,
60 /* lum_g */ 0.37f,
61 /* lum_b */ 0.08f,
62 /* lum_w */ 1.00f,
63 /* nominal_cct */ 2700,
64 /* input_xy_r */ { 0.700606f, 0.299300f }, // native LED R
65 /* input_xy_g */ { 0.097940f, 0.831593f }, // native LED G
66 /* input_xy_b */ { 0.129086f, 0.049450f }, // native LED B
67 /* input_xy_w */ { 0.31272f, 0.32903f }, // D65
68 },
69 /* cool_path */ {
70 /* xy_r */ { 0.700606f, 0.299300f },
71 /* xy_g */ { 0.097940f, 0.831593f },
72 /* xy_b */ { 0.129086f, 0.049450f },
73 /* xy_w (6500) */ { 0.313500f, 0.323700f },
74 /* lum_r */ 0.10f,
75 /* lum_g */ 0.37f,
76 /* lum_b */ 0.08f,
77 /* lum_w */ 1.00f,
78 /* nominal_cct */ 6500,
79 /* input_xy_r */ { 0.700606f, 0.299300f }, // native LED R
80 /* input_xy_g */ { 0.097940f, 0.831593f }, // native LED G
81 /* input_xy_b */ { 0.129086f, 0.049450f }, // native LED B
82 /* input_xy_w */ { 0.31272f, 0.32903f }, // D65
83 },
84};
85
86namespace {
87// Owns a copy of the active profile (see #2580 finding A4, originally
88// CodeRabbit on #2560). The earlier implementation stashed the caller's raw
89// pointer, which became a dangling reference the moment the caller's
90// RgbcctProfile (often a stack local in setup()) went out of scope. We now
91// copy by value; has_profile == false reverts to kRgbwwDefaultProfile.
96} // namespace
97
100 if (profile == nullptr) {
101 state.has_profile = false;
102 return;
103 }
104 state.profile = *profile;
105 state.has_profile = true;
106}
107
112
113// User-installable RGB->RGBWW function pointer, held behind a lazy
114// Singleton<T> (same pattern as the rgb_2_rgbw user function — see #2424 for
115// the binary-bloat rationale). Default is nullptr; the user function path
116// emits zero output when nothing is installed.
117namespace {
121} // namespace
122
126
128 u8 b0, u8 b1, u8 b2,
129 u8 ww, u8 wc,
130 u8 *out_b0, u8 *out_b1, u8 *out_b2,
131 u8 *out_b3, u8 *out_b4) FL_NOEXCEPT {
132 // Five output slots: out[0..4].
133 u8 out[5];
134 const u8 enc = static_cast<u8>(ww_placement);
135 const u8 ww_idx = (enc >> 4) & 0x07;
136 const u8 wc_idx = enc & 0x07;
137 out[ww_idx] = ww;
138 out[wc_idx] = wc;
139 // Fill the three remaining slots with b0, b1, b2 in ascending index order.
140 u8 b_idx = 0;
141 const u8 rgb_bytes[3] = { b0, b1, b2 };
142 for (u8 k = 0; k < 5; ++k) {
143 if (k != ww_idx && k != wc_idx) {
144 out[k] = rgb_bytes[b_idx++];
145 }
146 }
147 *out_b0 = out[0];
148 *out_b1 = out[1];
149 *out_b2 = out[2];
150 *out_b3 = out[3];
151 *out_b4 = out[4];
152}
153
155 u8 r, u8 g, u8 b,
156 u8 r_scale, u8 g_scale, u8 b_scale,
157 u8 *out_r, u8 *out_g, u8 *out_b,
158 u8 *out_ww, u8 *out_wc) FL_NOEXCEPT {
160 if (fn == nullptr) {
161 // No user function installed — produce safe zero output.
162 zero_out(out_r, out_g, out_b, out_ww, out_wc);
163 return;
164 }
165 fn(cfg, r, g, b, r_scale, g_scale, b_scale,
166 out_r, out_g, out_b, out_ww, out_wc);
167}
168
169
170#if FASTLED_RGBW_COLORIMETRIC
171
172namespace {
173// Compute the warm/cool blend factor from the input chromaticity. Simple
174// x-based heuristic: warmer inputs (higher x, closer to warm white) → lower
175// eta (more warm-W); cooler inputs → higher eta. Mathematically not optimal
176// — a chromaticity-aware solver could place eta to minimize dE — but cheap,
177// monotonic, and good enough for the common ambilight / neutral-pastel case.
178// Per-process cache for the input chromaticity matrix used by
179// compute_eta_from_input. `build_source_matrix` runs a 3x3 invert, which is
180// far too expensive to repeat per pixel (CodeRabbit #2707). The keyed input
181// fields are profile-level data — the matrix is invariant until the active
182// profile's input_xy_* changes — so we cache it and invalidate only when one
183// of those eight floats differs from the cached snapshot.
184struct EtaSourceMatrixCache {
185 float xy_r[2] = {0.0f, 0.0f};
186 float xy_g[2] = {0.0f, 0.0f};
187 float xy_b[2] = {0.0f, 0.0f};
188 float xy_w[2] = {0.0f, 0.0f};
189 float M_src[3][3] = {{0}};
190 bool has_M_src = false;
191 bool initialized = false;
192};
193
194inline bool xy_equal(const float a[2], const float b[2]) FL_NOEXCEPT {
195 return a[0] == b[0] && a[1] == b[1];
196}
197
198inline float compute_eta_from_input(const colorimetric_detail::RgbcctProfile& profile,
199 float s_r, float s_g, float s_b) FL_NOEXCEPT {
200 // Compute the input chromaticity in the *source* color space (#2705) when
201 // the warm path carries populated input_xy_* fields. Falling back to the
202 // emitter-space projection used previously would bias eta toward the LED
203 // gamut, so a neutral source white wouldn't blend symmetrically across
204 // warm/cool when the source primaries (e.g. sRGB) differ from the LED
205 // primaries — exactly the regression flagged on this PR.
206 EtaSourceMatrixCache& cache =
208 const fl::DiodeProfile& wp = profile.warm_path;
209 const bool key_changed =
210 !cache.initialized ||
211 !xy_equal(cache.xy_r, wp.input_xy_r) ||
212 !xy_equal(cache.xy_g, wp.input_xy_g) ||
213 !xy_equal(cache.xy_b, wp.input_xy_b) ||
214 !xy_equal(cache.xy_w, wp.input_xy_w);
215 if (key_changed) {
216 cache.xy_r[0] = wp.input_xy_r[0]; cache.xy_r[1] = wp.input_xy_r[1];
217 cache.xy_g[0] = wp.input_xy_g[0]; cache.xy_g[1] = wp.input_xy_g[1];
218 cache.xy_b[0] = wp.input_xy_b[0]; cache.xy_b[1] = wp.input_xy_b[1];
219 cache.xy_w[0] = wp.input_xy_w[0]; cache.xy_w[1] = wp.input_xy_w[1];
220 cache.has_M_src = wp.input_xy_w[1] > 1e-6f &&
222 wp.input_xy_r, wp.input_xy_g,
223 wp.input_xy_b, wp.input_xy_w, cache.M_src);
224 cache.initialized = true;
225 }
226
227 float X = 0.0f, Y = 0.0f, Z = 0.0f;
228 if (cache.has_M_src) {
229 const float s[3] = { s_r, s_g, s_b };
230 float xyz[3];
231 colorimetric_detail::matvec3(cache.M_src, s, xyz);
232 X = xyz[0]; Y = xyz[1]; Z = xyz[2];
233 } else {
234 // Legacy emitter-space fallback for profiles without populated source.
235 // Three xyY_to_XYZ calls per pixel — degraded but still cheap, and
236 // only reached when the user explicitly opts out of source space.
237 float P_R[3], P_G[3], P_B[3];
238 colorimetric_detail::xyY_to_XYZ(wp.xy_r[0], wp.xy_r[1], wp.lum_r, P_R);
239 colorimetric_detail::xyY_to_XYZ(wp.xy_g[0], wp.xy_g[1], wp.lum_g, P_G);
240 colorimetric_detail::xyY_to_XYZ(wp.xy_b[0], wp.xy_b[1], wp.lum_b, P_B);
241 X = P_R[0]*s_r + P_G[0]*s_g + P_B[0]*s_b;
242 Y = P_R[1]*s_r + P_G[1]*s_g + P_B[1]*s_b;
243 Z = P_R[2]*s_r + P_G[2]*s_g + P_B[2]*s_b;
244 }
245 const float sum = X + Y + Z;
246 if (sum < 1e-9f) return 0.5f;
247 const float input_x = X / sum;
248 const float warm_x = profile.warm_path.xy_w[0];
249 const float cool_x = profile.cool_path.xy_w[0];
250 if (cool_x == warm_x) return 0.5f;
251 const float eta = (input_x - warm_x) / (cool_x - warm_x);
252 if (eta < 0.0f) return 0.0f;
253 if (eta > 1.0f) return 1.0f;
254 return eta;
255}
256// Resolve the per-call RgbcctProfile, honoring cfg.warm_cct / cool_cct.
257//
258// (#2558) CodeRabbit caught that the previous implementation ignored the
259// per-strip CCT fields whenever cfg.profile == nullptr — every Rgbww config
260// produced the same warm=2700/cool=6500 default. Now we shift the W vertex
261// of a per-call profile to whatever CCTs the user requested. When the
262// requested CCTs match the default profile's nominal_cct values exactly, we
263// short-circuit to the global default to avoid the per-pixel xy recompute.
265resolve_active_rgbcct_profile(const Rgbww& cfg,
267 if (cfg.profile != nullptr) {
268 return *cfg.profile;
269 }
271 if (static_cast<int>(cfg.warm_cct) == base->warm_path.nominal_cct
272 && static_cast<int>(cfg.cool_cct) == base->cool_path.nominal_cct) {
273 return *base;
274 }
275 // Build a temp profile with W vertices at the requested CCTs.
276 scratch = *base;
277 if (static_cast<int>(cfg.warm_cct) != base->warm_path.nominal_cct) {
278 float xy[2];
279 colorimetric_detail::cct_to_xy(cfg.warm_cct, xy);
280 scratch.warm_path.xy_w[0] = xy[0];
281 scratch.warm_path.xy_w[1] = xy[1];
282 scratch.warm_path.nominal_cct = cfg.warm_cct;
283 }
284 if (static_cast<int>(cfg.cool_cct) != base->cool_path.nominal_cct) {
285 float xy[2];
286 colorimetric_detail::cct_to_xy(cfg.cool_cct, xy);
287 scratch.cool_path.xy_w[0] = xy[0];
288 scratch.cool_path.xy_w[1] = xy[1];
289 scratch.cool_path.nominal_cct = cfg.cool_cct;
290 }
291 return scratch;
292}
293
294} // namespace
295
296void rgb_2_rgbww_colorimetric(const Rgbww& cfg,
297 u8 r, u8 g, u8 b,
298 u8 r_scale, u8 g_scale, u8 b_scale,
299 u8 *out_r, u8 *out_g, u8 *out_b,
300 u8 *out_ww, u8 *out_wc) FL_NOEXCEPT {
301 r = scale8(r, r_scale);
302 g = scale8(g, g_scale);
303 b = scale8(b, b_scale);
304 if ((r | g | b) == 0) { zero_out(out_r, out_g, out_b, out_ww, out_wc); return; }
305
308 resolve_active_rgbcct_profile(cfg, scratch);
309
310 const float s_r = r * (1.0f / 255.0f);
311 const float s_g = g * (1.0f / 255.0f);
312 const float s_b = b * (1.0f / 255.0f);
313 const float eta = compute_eta_from_input(profile, s_r, s_g, s_b);
314
315 float rgbww[5];
316 colorimetric_detail::solve_rgbcct(profile, s_r, s_g, s_b, eta, rgbww);
317
318 *out_r = colorimetric_detail::quantize_u8(rgbww[0]);
319 *out_g = colorimetric_detail::quantize_u8(rgbww[1]);
320 *out_b = colorimetric_detail::quantize_u8(rgbww[2]);
321 *out_ww = colorimetric_detail::quantize_u8(rgbww[3]);
322 *out_wc = colorimetric_detail::quantize_u8(rgbww[4]);
323}
324
326 u8 r, u8 g, u8 b,
327 u8 r_scale, u8 g_scale, u8 b_scale,
328 u8 *out_r, u8 *out_g, u8 *out_b,
329 u8 *out_ww, u8 *out_wc) FL_NOEXCEPT {
330 // Phase D MVP: the boosted variant uses the same RGBCCT line-blend as the
331 // strict path but skews eta toward 0.5 (equal warm+cool) so the combined
332 // W contribution is larger when the input chromaticity sits near neutral.
333 // A future revision can use a wx_lp-style maximization here.
334 r = scale8(r, r_scale);
335 g = scale8(g, g_scale);
336 b = scale8(b, b_scale);
337 if ((r | g | b) == 0) { zero_out(out_r, out_g, out_b, out_ww, out_wc); return; }
338
341 resolve_active_rgbcct_profile(cfg, scratch);
342
343 const float s_r = r * (1.0f / 255.0f);
344 const float s_g = g * (1.0f / 255.0f);
345 const float s_b = b * (1.0f / 255.0f);
346 const float eta_chroma = compute_eta_from_input(profile, s_r, s_g, s_b);
347 // Push eta halfway toward 0.5 (equal blend) for more total W participation.
348 const float eta = 0.5f * eta_chroma + 0.5f * 0.5f;
349
350 float rgbww[5];
351 colorimetric_detail::solve_rgbcct(profile, s_r, s_g, s_b, eta, rgbww);
352
353 *out_r = colorimetric_detail::quantize_u8(rgbww[0]);
354 *out_g = colorimetric_detail::quantize_u8(rgbww[1]);
355 *out_b = colorimetric_detail::quantize_u8(rgbww[2]);
356 *out_ww = colorimetric_detail::quantize_u8(rgbww[3]);
357 *out_wc = colorimetric_detail::quantize_u8(rgbww[4]);
358}
359
360#else // FASTLED_RGBW_COLORIMETRIC
361
362// Stub path when the colorimetric math library is not compiled in. Emits
363// FL_WARN_ONCE (suppressible) and five zero bytes. Does not pull in the
364// solver, profile cache, or float math machinery — same gc-section behavior
365// as the rest of the colorimetric Phase 1 dispatch in PR #2552.
367 u8 r, u8 g, u8 b,
368 u8 r_scale, u8 g_scale, u8 b_scale,
369 u8 *out_r, u8 *out_g, u8 *out_b,
370 u8 *out_ww, u8 *out_wc) FL_NOEXCEPT {
371 (void)cfg; (void)r; (void)g; (void)b;
372 (void)r_scale; (void)g_scale; (void)b_scale;
373#ifndef FASTLED_SUPPRESS_RGBWW_FALLBACK_WARNING
374 FL_WARN_ONCE("RGBWW: kRGBWWColorimetric requires FASTLED_RGBW_COLORIMETRIC=1 "
375 "(the math library that provides solve_rgbcct). Outputting zeros.");
376#endif
377 zero_out(out_r, out_g, out_b, out_ww, out_wc);
378}
379
381 u8 r, u8 g, u8 b,
382 u8 r_scale, u8 g_scale, u8 b_scale,
383 u8 *out_r, u8 *out_g, u8 *out_b,
384 u8 *out_ww, u8 *out_wc) FL_NOEXCEPT {
385 (void)cfg; (void)r; (void)g; (void)b;
386 (void)r_scale; (void)g_scale; (void)b_scale;
387#ifndef FASTLED_SUPPRESS_RGBWW_FALLBACK_WARNING
388 FL_WARN_ONCE("RGBWW: kRGBWWColorimetricBoosted requires FASTLED_RGBW_COLORIMETRIC=1. "
389 "Outputting zeros.");
390#endif
391 zero_out(out_r, out_g, out_b, out_ww, out_wc);
392}
393
394#endif // FASTLED_RGBW_COLORIMETRIC
395
396} // namespace fl
TestState state
unsigned int xy(unsigned int x, unsigned int y)
static T & instance() FL_NOEXCEPT
Definition singleton.h:41
Internal FastLED header for implementation files.
#define FL_WARN_ONCE(X)
Definition log.h:278
Centralized logging categories for FastLED hardware interfaces and subsystems.
void zero_out(u8 *r, u8 *g, u8 *b, u8 *ww, u8 *wc) FL_NOEXCEPT
Definition rgbww.cpp.hpp:36
void xyY_to_XYZ(float x, float y, float Y, float out[3]) FL_NOEXCEPT
void cct_to_xy(int cct, float out[2]) FL_NOEXCEPT
bool build_source_matrix(const float xy_r[2], const float xy_g[2], const float xy_b[2], const float xy_w[2], float M_out[3][3]) FL_NOEXCEPT
void matvec3(const float M[3][3], const float v[3], float out[3]) FL_NOEXCEPT
u8 quantize_u8(float v) FL_NOEXCEPT
void solve_rgbcct(const RgbcctProfile &profile, float s_r, float s_g, float s_b, float eta, float out[5]) FL_NOEXCEPT
unsigned char u8
Definition stdint.h:131
void rgb_2_rgbww_colorimetric_boosted(const Rgbww &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_ww, u8 *out_wc) FL_NOEXCEPT
Colorimetric white-overdrive solver for RGBWW (wx_lp_legacy + RGBCCT layered blend).
const colorimetric_detail::RgbcctProfile kRgbwwDefaultProfile
Definition rgbww.cpp.hpp:53
EOrderWW
White-byte ordering for 5-channel RGBWW output.
Definition rgbww.h:27
void set_rgb_2_rgbww_function(rgb_2_rgbww_function func) FL_NOEXCEPT
void rgb_2_rgbww_user_function(const Rgbww &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_ww, u8 *out_wc) FL_NOEXCEPT
User-installable RGB->RGBWW function.
void rgb_2_rgbww_colorimetric(const Rgbww &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_ww, u8 *out_wc) FL_NOEXCEPT
Colorimetric strict sub-gamut solver for RGBWW (gist sec 5 + sec 11-12, using solve_rgbcct from rgbw_...
void(* rgb_2_rgbww_function)(const Rgbww &cfg, fl::u8 r, fl::u8 g, fl::u8 b, fl::u8 r_scale, fl::u8 g_scale, fl::u8 b_scale, fl::u8 *out_r, fl::u8 *out_g, fl::u8 *out_b, fl::u8 *out_ww, fl::u8 *out_wc)
Definition rgbww.h:110
void set_rgbww_colorimetric_profile(const colorimetric_detail::RgbcctProfile *profile) FL_NOEXCEPT
Definition rgbww.cpp.hpp:98
const colorimetric_detail::RgbcctProfile * get_rgbww_colorimetric_profile() FL_NOEXCEPT
void rgbww_partial_reorder(EOrderWW ww_placement, u8 b0, u8 b1, u8 b2, u8 ww, u8 wc, u8 *out_b0, u8 *out_b1, u8 *out_b2, u8 *out_b3, u8 *out_b4) FL_NOEXCEPT
Dispatch RGB->RGBWW for a given mode.
Base definition for an LED controller.
Definition crgb.hpp:179
float input_xy_b[2]
Definition rgbw.h:68
float xy_g[2]
Definition rgbw.h:56
float input_xy_r[2]
Definition rgbw.h:66
float xy_b[2]
Definition rgbw.h:57
float input_xy_g[2]
Definition rgbw.h:67
float input_xy_w[2]
Definition rgbw.h:69
float lum_r
Definition rgbw.h:59
float xy_r[2]
Definition rgbw.h:55
float lum_b
Definition rgbw.h:61
float lum_g
Definition rgbw.h:60
Chromaticity-aware RGBW solvers — strict sub-gamut + wx_lp_legacy white extraction + boosted overdriv...
5-channel RGB + warm-W + cool-W (RGBWW / RGBCCT) configuration types (issue #2558,...
#define FL_NOEXCEPT
Per-strip RGBWW configuration.
Definition rgbww.h:60