FastLED 3.10.8
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
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:423
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 ...
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
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.
fl::i64 i64
Definition s16x16x4.h:222
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.
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
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:125
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.
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
CIE 1931 xy chromaticity, independent of encoded storage and chipset.
A source declaration, not a container or an output-device selection.