FastLED 3.10.6
Loading...
Searching...
No Matches
pipeline.cpp.hpp
Go to the documentation of this file.
1// ok no header - implementation for fl/gfx/pipeline.h
2
3#include "fl/gfx/pipeline.h"
4
6#include "fl/gfx/transfer.h"
7
8namespace fl {
9
10namespace {
11
16constexpr Chromaticity kPipelineD65 = Chromaticity(0.3127f, 0.3290f);
17
18bool validResponseLut(const u16* values, u16 size) FL_NO_EXCEPT {
19 if (values == nullptr || size < 2 || values[0] != 0 ||
20 values[size - 1] != 65535) {
21 return false;
22 }
23 for (u16 i = 1; i < size; ++i) {
24 if (values[i] < values[i - 1]) return false;
25 }
26 return true;
27}
28
31i32 inverseResponseQ16(i32 light, const vector<u16>& values) FL_NO_EXCEPT {
32 if (light <= 0) return 0;
33 if (light >= 65536) return 65536;
34 const u32 target =
35 (static_cast<u32>(light) * 65535u + 32768u) >> 16;
36 u32 low = 1;
37 u32 high = static_cast<u32>(values.size() - 1);
38 while (low < high) {
39 const u32 middle = low + (high - low) / 2;
40 if (values[middle] >= target) high = middle;
41 else low = middle + 1;
42 }
43 high = low;
44 const u32 low_value = values[high - 1];
45 const u32 high_value = values[high];
46 // A plateau at the requested light maps to its first attainable code.
47 if (high_value == low_value) {
48 return static_cast<i32>((high - 1) * 65536u /
49 static_cast<u32>(values.size() - 1));
50 }
51 const u32 interval = high_value - low_value;
52 const u32 fraction =
53 ((target - low_value) * 65536u + interval / 2) / interval;
54 const u32 segments = static_cast<u32>(values.size() - 1);
55 return static_cast<i32>(((high - 1) * 65536u + fraction + segments / 2) /
56 segments);
57}
58
59bool wideWhiteColumnQ16(const float (&xy)[2], float luminance,
60 i32 (&out)[3]) FL_NO_EXCEPT {
61 i32 xy_q16[2];
62 i32 luminance_q16 = 0;
63 if (!q16FromFloatBits(xy[0], &xy_q16[0]) ||
64 !q16FromFloatBits(xy[1], &xy_q16[1]) ||
65 !q16FromFloatBits(luminance, &luminance_q16) ||
66 luminance_q16 <= 0) return false;
67 i64 column[3];
68 if (!detail::xyzColumnQ16(xy_q16, luminance_q16, column)) return false;
69 for (int i = 0; i < 3; ++i) {
70 if (column[i] < 0 || column[i] > 2147483647LL) return false;
71 out[i] = static_cast<i32>(column[i]);
72 }
73 return true;
74}
75
78 const Chromaticity* target_white,
79 const EmitterSolveMatrixQ16* effective_solve,
82 if (!wide) return false;
83 wide->topology = device.topology;
84 i32 white1[3];
85 i32 white2[3] = {};
86 if (!wideWhiteColumnQ16(device.xy_white1, device.lum_white1, white1))
87 return false;
88 if (device.topology == colorimetric_response::EmitterTopology::RGBWW &&
89 !wideWhiteColumnQ16(device.xy_white2, device.lum_white2, white2))
90 return false;
91 if (target_white != nullptr) {
92 AdaptationMatrixQ16 to_working;
93 if (!buildBradfordMatrixQ16(*target_white, kPipelineD65,
94 &to_working)) return false;
95 i32 adapted1[3];
96 adaptXyzQ16(to_working, white1, adapted1);
97 for (int i = 0; i < 3; ++i) white1[i] = adapted1[i];
98 if (device.topology == colorimetric_response::EmitterTopology::RGBWW) {
99 i32 adapted2[3];
100 adaptXyzQ16(to_working, white2, adapted2);
101 for (int i = 0; i < 3; ++i) white2[i] = adapted2[i];
102 }
103 }
105 if (device.topology == colorimetric_response::EmitterTopology::RGBW) {
106 const bool ok = effective_solve == nullptr
107 ? buildGamutMapRgbwQ16(device, white1, allocation, &wide->rgbw)
108 : buildGamutMapRgbwFromSolveQ16(device, *effective_solve, white1,
109 allocation, &wide->rgbw);
110 if (!ok) return false;
111 } else if (device.topology == colorimetric_response::EmitterTopology::RGBWW) {
112 const bool ok = effective_solve == nullptr
113 ? buildGamutMapRgbwwQ16(device, white1, white2, allocation,
114 &wide->rgbww)
115 : buildGamutMapRgbwwFromSolveQ16(device, *effective_solve, white1,
116 white2, allocation, &wide->rgbww);
117 if (!ok) return false;
118 } else {
119 return false;
120 }
121 if (device.response_lut_size != 0) {
122 const u16 size = device.response_lut_size;
123 if (!validResponseLut(device.response_lut_white1, size)) return false;
124 wide->white1_response.assign(device.response_lut_white1,
125 device.response_lut_white1 + size);
126 if (wide->white1_response.size() != size) return false;
127 if (device.topology == colorimetric_response::EmitterTopology::RGBWW) {
128 if (!validResponseLut(device.response_lut_white2, size)) return false;
129 wide->white2_response.assign(device.response_lut_white2,
130 device.response_lut_white2 + size);
131 if (wide->white2_response.size() != size) return false;
132 }
133 }
134 *out = wide;
135 return true;
136}
137
138} // namespace
139
142 GamutPolicy policy,
144 const Chromaticity* target_white) FL_NO_EXCEPT {
145 if (out == nullptr) {
146 return false;
147 }
148 out->gamut_policy = policy;
149 if (!buildSourceMatrixQ16(source.primaries, &out->source)) {
150 return false;
151 }
152
153 // Fold the source white's adaptation to D65 into the matrix above, so
154 // the per-pixel path never pays for it. Skipped when the source already
155 // renders to D65, where the transform is the identity and folding it
156 // would only add a round trip's worth of quantization.
157 //
158 // Compared in s16.16 from the float's bits, so this check links no float
159 // either (FastLED#4458). 1e-4 is 6.55 steps; 7 keeps the same window.
160 const Chromaticity source_white = source.primaries.white;
161 i32 white_x = 0;
162 i32 white_y = 0;
163 if (!q16FromFloatBits(source_white.x, &white_x) ||
164 !q16FromFloatBits(source_white.y, &white_y)) {
165 return false;
166 }
167 constexpr i32 kD65XQ16 = 20493; // 0.3127
168 constexpr i32 kD65YQ16 = 21561; // 0.3290
169 constexpr i32 kD65ToleranceQ16 = 7; // ~1e-4
170 const bool already_d65 = white_x > kD65XQ16 - kD65ToleranceQ16 &&
171 white_x < kD65XQ16 + kD65ToleranceQ16 &&
172 white_y > kD65YQ16 - kD65ToleranceQ16 &&
173 white_y < kD65YQ16 + kD65ToleranceQ16;
174 if (!already_d65) {
175 AdaptationMatrixQ16 adaptation;
176 if (!buildBradfordMatrixQ16(source_white, kPipelineD65, &adaptation)) {
177 return false;
178 }
179 foldAdaptationIntoSourceMatrix(adaptation, &out->source);
180 }
181
182 out->wide.reset();
183 const bool is_rgb =
185 if (target_white == nullptr) {
186 if (is_rgb) {
187 if (!buildGamutMapQ16(device, &out->gamut)) return false;
188 } else if (!buildWidePipelineQ16(device, nullptr, nullptr,
189 &out->wide)) {
190 return false;
191 }
192 } else {
193 // The source and OKLab objective remain in D65 coordinates. Map the
194 // physical gamut into that space by composing the inverse emitter
195 // solve with Bradford D65 -> selected rendering white. The neutral
196 // cap must be computed from the composed solve, not the raw profile.
197 AdaptationMatrixQ16 to_target;
198 if (!buildBradfordMatrixQ16(kPipelineD65, *target_white, &to_target))
199 return false;
200 EmitterSolveMatrixQ16 physical_solve;
201 if (!buildRgbSolveMatrixQ16(device, &physical_solve)) return false;
202 EmitterSolveMatrixQ16 effective_solve;
203 for (int row = 0; row < 3; ++row) {
204 for (int col = 0; col < 3; ++col) {
205 i64 sum = 0;
206 constexpr i64 kMax = 9223372036854775807LL;
207 for (int k = 0; k < 3; ++k) {
208 // Each i32 product fits i64, but their sum need not.
209 const i64 term = static_cast<i64>(physical_solve.m[row][k]) *
210 to_target.m[k][col];
211 if ((term > 0 && sum > kMax - term) ||
212 (term < 0 && sum < -kMax - term)) return false;
213 sum += term;
214 }
215 // Keep rounding/negation away from the i64 boundary too.
216 if (sum > 2147483647LL * 65536 + 32768 ||
217 sum < -2147483648LL * 65536 - 32768) return false;
218 const i64 value = sum >= 0 ? (sum + 32768) >> 16
219 : -((-sum + 32768) >> 16);
221 return false;
222 effective_solve.m[row][col] = static_cast<i32>(value);
223 }
224 }
225 if (is_rgb) {
226 if (!buildGamutMapFromSolveQ16(effective_solve, &out->gamut))
227 return false;
228 } else if (!buildWidePipelineQ16(device, target_white,
229 &effective_solve, &out->wide)) {
230 return false;
231 }
232 }
233 out->response.reset();
234 if (device.response_lut_size != 0) {
235 const u16 size = device.response_lut_size;
236 if (!validResponseLut(device.response_lut_r, size) ||
237 !validResponseLut(device.response_lut_g, size) ||
238 !validResponseLut(device.response_lut_b, size)) {
239 return false;
240 }
242 if (!response) return false;
243 response->red.assign(device.response_lut_r, device.response_lut_r + size);
244 response->green.assign(device.response_lut_g, device.response_lut_g + size);
245 response->blue.assign(device.response_lut_b, device.response_lut_b + size);
246 if (response->red.size() != size || response->green.size() != size ||
247 response->blue.size() != size) return false;
248 out->response = response;
249 }
250 out->transfer = source.transfer;
251 out->flux = FluxScalar::unity();
252 return true;
253}
254
257 if (pipeline == nullptr) {
258 return;
259 }
260 pipeline->flux = flux;
261}
262
264 u8 b, i32 (&drives)[3]) FL_NO_EXCEPT {
265 // One semantic conversion per channel: code straight to linear light.
266 // Nothing here writes an RGB8 intermediate, which is what B3 asks and
267 // what ci/tests/test_no_rgb8_intermediate.py enforces structurally.
268 const u16 linear_r = decodeTransferU16(pipeline.transfer, r);
269 const u16 linear_g = decodeTransferU16(pipeline.transfer, g);
270 const u16 linear_b = decodeTransferU16(pipeline.transfer, b);
271
272 i32 xyz[3];
273 linearRgbToXyzQ16(pipeline.source, linear_r, linear_g, linear_b, xyz);
274
275 if (pipeline.gamut_policy == GamutPolicy::Clamp) {
276 // The caller asked for the cheap answer. Solve and clip, which is
277 // `map_clip` from the P7 study -- about 20 dE2000 from the reference
278 // where the mapper is at 0.15, and offered only because the binding
279 // offers it.
280 solveRgbDrivesQ16(pipeline.gamut.solve, xyz, drives);
281 for (int i = 0; i < 3; ++i) {
282 if (drives[i] < 0) {
283 drives[i] = 0;
284 } else if (drives[i] > 65536) {
285 drives[i] = 65536;
286 }
287 }
288 } else {
289 // The mapper owns clamping, so the stages above hand it the honest
290 // target even when that is outside the hull.
291 mapAndSolveDrivesQ16(pipeline.gamut, xyz, drives);
292 }
293
294}
295
296void processPixelQ16(const StreamingPipelineQ16& pipeline, u8 r, u8 g, u8 b,
297 i32 (&drives)[3]) FL_NO_EXCEPT {
298 processPixelLinearQ16(pipeline, r, g, b, drives);
299 // C4's single amplitude stage scales linear light. Physical response
300 // inversion follows it; dimming compensated drive codes would skew hue.
301 applyFluxScalar(pipeline.flux, span<i32>(drives, 3));
302 if (pipeline.response) {
303 drives[0] = inverseResponseQ16(drives[0], pipeline.response->red);
304 drives[1] = inverseResponseQ16(drives[1], pipeline.response->green);
305 drives[2] = inverseResponseQ16(drives[2], pipeline.response->blue);
306 }
307}
308
310 u8 g, u8 b, i32 (&drives)[5]) FL_NO_EXCEPT {
311 for (int i = 0; i < 5; ++i) drives[i] = 0;
312 if (!pipeline.wide) return;
313 const u16 linear_r = decodeTransferU16(pipeline.transfer, r);
314 const u16 linear_g = decodeTransferU16(pipeline.transfer, g);
315 const u16 linear_b = decodeTransferU16(pipeline.transfer, b);
316 i32 xyz[3];
317 linearRgbToXyzQ16(pipeline.source, linear_r, linear_g, linear_b, xyz);
318 const bool rgbww = pipeline.wide->topology ==
320 if (rgbww) {
321 i32 wide_drives[5];
322 if (pipeline.gamut_policy == GamutPolicy::Clamp &&
323 !allocateTwoWhiteDrivesQ16(pipeline.wide->rgbww.allocation,
324 xyz, wide_drives)) {
325 i32 rgb_drives[3];
326 solveRgbDrivesQ16(pipeline.wide->rgbww.allocation.rgb_solve,
327 xyz, rgb_drives);
328 for (int i = 0; i < 3; ++i) {
329 wide_drives[i] = rgb_drives[i] < 0 ? 0 :
330 (rgb_drives[i] > 65536 ? 65536 : rgb_drives[i]);
331 }
332 wide_drives[3] = 0;
333 wide_drives[4] = 0;
334 } else if (pipeline.gamut_policy != GamutPolicy::Clamp) {
335 mapAndAllocateRgbwwQ16(pipeline.wide->rgbww, xyz, wide_drives);
336 }
337 for (int i = 0; i < 5; ++i) drives[i] = wide_drives[i];
338 } else {
339 i32 wide_drives[4];
340 if (pipeline.gamut_policy == GamutPolicy::Clamp &&
341 !allocateEmitterDrivesQ16(pipeline.wide->rgbw.allocation,
342 xyz, wide_drives)) {
343 i32 rgb_drives[3];
344 solveRgbDrivesQ16(pipeline.wide->rgbw.allocation.rgb_solve,
345 xyz, rgb_drives);
346 for (int i = 0; i < 3; ++i) {
347 wide_drives[i] = rgb_drives[i] < 0 ? 0 :
348 (rgb_drives[i] > 65536 ? 65536 : rgb_drives[i]);
349 }
350 wide_drives[3] = 0;
351 } else if (pipeline.gamut_policy != GamutPolicy::Clamp) {
352 mapAndAllocateRgbwQ16(pipeline.wide->rgbw, xyz, wide_drives);
353 }
354 for (int i = 0; i < 4; ++i) drives[i] = wide_drives[i];
355 }
356}
357
359 u8 b, i32 (&drives)[5]) FL_NO_EXCEPT {
360 processPixelWideLinearQ16(pipeline, r, g, b, drives);
361 if (!pipeline.wide) return;
362 const bool rgbww = pipeline.wide && pipeline.wide->topology ==
364 const int count = rgbww ? 5 : 4;
365 applyFluxScalar(pipeline.flux, span<i32>(drives, count));
366 if (pipeline.response) {
367 drives[0] = inverseResponseQ16(drives[0], pipeline.response->red);
368 drives[1] = inverseResponseQ16(drives[1], pipeline.response->green);
369 drives[2] = inverseResponseQ16(drives[2], pipeline.response->blue);
370 drives[3] = inverseResponseQ16(drives[3],
371 pipeline.wide->white1_response);
372 if (rgbww) {
373 drives[4] = inverseResponseQ16(drives[4],
374 pipeline.wide->white2_response);
375 }
376 }
377}
378
379i32 encodeLinearEmitterQ16(const StreamingPipelineQ16& pipeline, u8 emitter,
380 i32 light, FluxScalar flux) FL_NO_EXCEPT {
381 i32 scaled[1] = {light};
382 applyFluxScalar(flux, span<i32>(scaled, 1));
383 if (!pipeline.response) return scaled[0];
384 if (emitter == 0) return inverseResponseQ16(scaled[0], pipeline.response->red);
385 if (emitter == 1) return inverseResponseQ16(scaled[0], pipeline.response->green);
386 if (emitter == 2) return inverseResponseQ16(scaled[0], pipeline.response->blue);
387 if (!pipeline.wide) return 0;
388 if (emitter == 3) return inverseResponseQ16(scaled[0], pipeline.wide->white1_response);
389 if (emitter == 4) return inverseResponseQ16(scaled[0], pipeline.wide->white2_response);
390 return 0;
391}
392
393} // namespace fl
unsigned int xy(unsigned int x, unsigned int y)
static FluxScalar unity() FL_NO_EXCEPT
Identity. Drives pass through unchanged.
A linear flux scalar in s16.16.
Definition flux_scalar.h:21
bool validResponseLut(const u16 *values, u16 size) FL_NO_EXCEPT
bool wideWhiteColumnQ16(const float(&xy)[2], float luminance, i32(&out)[3]) FL_NO_EXCEPT
i32 inverseResponseQ16(i32 light, const vector< u16 > &values) FL_NO_EXCEPT
Invert piecewise-linear code-to-light data.
bool buildWidePipelineQ16(const colorimetric_response::EmitterProfile &device, const Chromaticity *target_white, const EmitterSolveMatrixQ16 *effective_solve, shared_ptr< const WidePipelineQ16 > *out) FL_NO_EXCEPT
constexpr Chromaticity kPipelineD65
The working domain's rendering white.
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...
unsigned char u8
Definition stdint.h:131
bool buildGamutMapRgbwFromSolveQ16(const colorimetric_response::EmitterProfile &profile, const EmitterSolveMatrixQ16 &solve, const i32(&white_xyz)[3], WhiteAllocationPolicy policy, GamutMapRgbwQ16 *out) FL_NO_EXCEPT
Bind a four-emitter map from an effective RGB solve and a white column already adapted into the same ...
constexpr int type_rank< T >::value
bool buildGamutMapQ16(const colorimetric_response::EmitterProfile &profile, GamutMapQ16 *out) FL_NO_EXCEPT
Derive the mapper for a three-emitter profile.
void mapAndAllocateRgbwwQ16(const GamutMapRgbwwQ16 &map, const i32(&xyz)[3], i32(&drives)[5]) FL_NO_EXCEPT
One pixel: XYZ in s16.16 to five in-gamut drives – red, green, blue, white1, white2.
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 ...
void foldAdaptationIntoSourceMatrix(const AdaptationMatrixQ16 &adaptation, SourceMatrixQ16 *source) FL_NO_EXCEPT
Pre-multiply the adaptation into a source matrix, in place.
void mapAndSolveDrivesQ16(const GamutMapQ16 &map, const i32(&xyz)[3], i32(&drives)[3]) FL_NO_EXCEPT
One pixel: XYZ in s16.16 to in-gamut emitter drives in s16.16.
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...
void setPipelineFluxQ16(StreamingPipelineQ16 *pipeline, FluxScalar flux) FL_NO_EXCEPT
Set the composed brightness-and-power scalar for the frames that follow.
bool buildSourceMatrixQ16(const RgbPrimaries &primaries, SourceMatrixQ16 *out) FL_NO_EXCEPT
Quantize the source matrix for primaries.
GamutPolicy
WhiteAllocationPolicy
Which end of the feasible white interval to take (C3).
@ WhitePreferred
As much white as the target allows.
void applyFluxScalar(FluxScalar scalar, span< i32 > drives) FL_NO_EXCEPT
Scale every drive by one scalar, in place.
void processPixelLinearQ16(const StreamingPipelineQ16 &pipeline, u8 r, u8 g, u8 b, i32(&drives)[3]) FL_NO_EXCEPT
Pre-response linear emitter light for a power prepass.
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:414
bool buildGamutMapRgbwwQ16(const colorimetric_response::EmitterProfile &profile, const i32(&white1_xyz)[3], const i32(&white2_xyz)[3], WhiteAllocationPolicy policy, GamutMapRgbwwQ16 *out) FL_NO_EXCEPT
Derive the mapper for a three-primary profile plus two white emitters.
void processPixelWideLinearQ16(const StreamingPipelineQ16 &pipeline, u8 r, u8 g, u8 b, i32(&drives)[5]) FL_NO_EXCEPT
InputGamut g FL_NO_EXCEPT
Definition rgbw.h:121
u16 decodeTransferU16(TransferFunction transfer, u8 code) FL_NO_EXCEPT
Decode one 8-bit source code to u16 linear light under transfer.
void linearRgbToXyzQ16(const SourceMatrixQ16 &matrix, u16 r, u16 g, u16 b, i32(&out_xyz)[3]) FL_NO_EXCEPT
One pixel: u16 linear RGB -> s16.16 XYZ.
bool buildGamutMapFromSolveQ16(const EmitterSolveMatrixQ16 &solve, GamutMapQ16 *out) FL_NO_EXCEPT
Build a mapper from a bind-time solve already transformed into D65 working coordinates.
InputGamut g
Definition rgbw.h:124
void adaptXyzQ16(const AdaptationMatrixQ16 &matrix, const i32(&xyz)[3], i32(&out_xyz)[3]) FL_NO_EXCEPT
Adapt one XYZ triple.
bool buildStreamingPipelineQ16(const SourceProfile &source, const colorimetric_response::EmitterProfile &device, GamutPolicy policy, StreamingPipelineQ16 *out, const Chromaticity *target_white) FL_NO_EXCEPT
Bind a source declaration and a device profile into a pipeline.
bool buildGamutMapRgbwQ16(const colorimetric_response::EmitterProfile &profile, const i32(&white_xyz)[3], WhiteAllocationPolicy policy, GamutMapRgbwQ16 *out) FL_NO_EXCEPT
Derive the mapper for a three-primary profile plus one white emitter.
void processPixelWideQ16(const StreamingPipelineQ16 &pipeline, u8 r, u8 g, u8 b, i32(&drives)[5]) FL_NO_EXCEPT
Like processPixelQ16, but solves the physical 4/5-emitter hull.
i32 encodeLinearEmitterQ16(const StreamingPipelineQ16 &pipeline, u8 emitter, i32 light, FluxScalar flux) FL_NO_EXCEPT
Scale one linear emitter and invert its physical response, using the identical final stage as process...
bool buildRgbSolveMatrixQ16(const colorimetric_response::EmitterProfile &profile, EmitterSolveMatrixQ16 *out) FL_NO_EXCEPT
Invert the emitter matrix for a three-emitter profile.
bool buildBradfordMatrixQ16(Chromaticity source_white, Chromaticity destination_white, AdaptationMatrixQ16 *out) FL_NO_EXCEPT
Build the Bradford transform from source_white to destination_white.
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.
void processPixelQ16(const StreamingPipelineQ16 &pipeline, u8 r, u8 g, u8 b, i32(&drives)[3]) FL_NO_EXCEPT
One pixel: an RGB8 code triple to three physical emitter drives in s16.16.
void mapAndAllocateRgbwQ16(const GamutMapRgbwQ16 &map, const i32(&xyz)[3], i32(&drives)[4]) FL_NO_EXCEPT
One pixel: XYZ in s16.16 to four in-gamut drives, red, green, blue, white.
bool buildGamutMapRgbwwFromSolveQ16(const colorimetric_response::EmitterProfile &profile, const EmitterSolveMatrixQ16 &solve, const i32(&white1_xyz)[3], const i32(&white2_xyz)[3], WhiteAllocationPolicy policy, GamutMapRgbwwQ16 *out) FL_NO_EXCEPT
Bind a five-emitter map from one effective RGB solve and two white columns, all in the same D65 worki...
Base definition for an LED controller.
Definition crgb.hpp:179
TransferFunction transfer
The source's transfer function, applied per code.
Definition pipeline.h:75
GamutPolicy gamut_policy
What to do with a target the device cannot reproduce.
Definition pipeline.h:91
shared_ptr< const ResponseLutsQ16 > response
Absent means linear code-to-light response.
Definition pipeline.h:102
GamutMapQ16 gamut
The device hull, its solve, and the lightness bound.
Definition pipeline.h:81
FluxScalar flux
Brightness times power limiting, as one scalar (C4).
Definition pipeline.h:98
shared_ptr< const WidePipelineQ16 > wide
Definition pipeline.h:104
EmitterSolveMatrixQ16 solve
Definition gamut_map.h:44
SourceMatrixQ16 source
Source primaries to XYZ, with adaptation to D65 already folded in.
Definition pipeline.h:78
Inverse emitter matrix in s16.16, mapping XYZ to three emitter drives.
A collapsed adaptation transform in s16.16.
Everything the per-pixel path needs, derived once when a profile binds.
Definition pipeline.h:73
fl::i64 i64
Definition stdint.h:221
CIE 1931 xy chromaticity, independent of encoded storage and chipset.
A source declaration, not a container or an output-device selection.