Build all per-profile quantities once so the per-pixel runtime path is just a few matrix-vector operations and tiny fixed-size solves. Important scale convention: P_R/P_G/P_B/P_W are measured emitter columns in absolute device XYZ/Y. build_source_matrix() returns a normalized source matrix where white has Y=1. We scale M_src by the same white-fit factor used by the reference math model so source
| 60 | // a Y=1 source target against hundreds/thousands of device-Y emitter units. |
| 61 | // The result is underpowered solves and the wrong active-channel ratios. |
| 62 | void build_profile_cache(const DiodeProfile* p, int cct_override, |
| 63 | ProfileCache* cache) FL_NOEXCEPT { |
| 64 | cache->profile = p; |
| 65 | xyY_to_XYZ(p->xy_r[0], p->xy_r[1], p->lum_r, cache->P_R); |
| 66 | xyY_to_XYZ(p->xy_g[0], p->xy_g[1], p->lum_g, cache->P_G); |
| 67 | xyY_to_XYZ(p->xy_b[0], p->xy_b[1], p->lum_b, cache->P_B); |
| 68 | cache->xy_w[0] = p->xy_w[0]; |
| 69 | cache->xy_w[1] = p->xy_w[1]; |
| 70 | if (cct_override >= 1500 && cct_override <= 15000) { |
| 71 | cct_to_xy(cct_override, cache->xy_w); |
| 72 | } |
| 73 | xyY_to_XYZ(cache->xy_w[0], cache->xy_w[1], p->lum_w, cache->P_W); |
| 74 | |
| 75 | auto pack = [](const float* a, const float* b, const float* c, |
| 76 | float out[3][3]) FL_NOEXCEPT { |
| 77 | out[0][0] = a[0]; out[0][1] = b[0]; out[0][2] = c[0]; |
| 78 | out[1][0] = a[1]; out[1][1] = b[1]; out[1][2] = c[1]; |
| 79 | out[2][0] = a[2]; out[2][1] = b[2]; out[2][2] = c[2]; |
| 80 | }; |
| 81 | |
| 82 | float P_RGB[3][3], P_RGW[3][3], P_RBW[3][3], P_BGW[3][3]; |
| 83 | pack(cache->P_R, cache->P_G, cache->P_B, P_RGB); |
| 84 | pack(cache->P_R, cache->P_G, cache->P_W, P_RGW); |
| 85 | pack(cache->P_R, cache->P_B, cache->P_W, P_RBW); |
| 86 | pack(cache->P_B, cache->P_G, cache->P_W, P_BGW); |
| 87 | |
| 88 | // Inverting a singular matrix leaves the destination with whatever |
| 89 | // invert3x3 wrote — solvers reading it would silently produce garbage. |
| 90 | // Warn once if any inversion fails so the user knows their profile has |
| 91 | // degenerate primaries (colinear chromaticities, near-zero luminance, etc). |
| 92 | const bool ok_rgb = invert3x3(P_RGB, cache->P_RGB_inv); |
| 93 | const bool ok_rgw = invert3x3(P_RGW, cache->P_RGW_inv); |
| 94 | const bool ok_rbw = invert3x3(P_RBW, cache->P_RBW_inv); |
| 95 | const bool ok_bgw = invert3x3(P_BGW, cache->P_BGW_inv); |
| 96 | if (!(ok_rgb && ok_rgw && ok_rbw && ok_bgw)) { |
| 97 | FL_WARN_ONCE("RGBW colorimetric: profile has degenerate primaries — " |
| 98 | "one or more sub-gamut matrix inversions failed. Output " |
| 99 | "colors will be incorrect. Check DiodeProfile xy/lum values."); |
| 100 | // Zero-init any failed inverse so downstream matvec3() output is |
| 101 | // deterministic (zero) rather than UB-valued garbage. |
| 102 | auto zero3x3 = [](float m[3][3]) FL_NOEXCEPT { |
| 103 | for (int i = 0; i < 3; ++i) |
| 104 | for (int j = 0; j < 3; ++j) m[i][j] = 0.0f; |
| 105 | }; |
| 106 | if (!ok_rgb) zero3x3(cache->P_RGB_inv); |
| 107 | if (!ok_rgw) zero3x3(cache->P_RGW_inv); |
| 108 | if (!ok_rbw) zero3x3(cache->P_RBW_inv); |
| 109 | if (!ok_bgw) zero3x3(cache->P_BGW_inv); |
| 110 | } |
| 111 | |
| 112 | matvec3(cache->P_RGB_inv, cache->P_W, cache->d_W); |
| 113 | |
| 114 | // Source-space matrix (#2705). When input_xy_w is degenerate (the |
| 115 | // legacy value-initialized case `DiodeProfile{}`), leave M_src empty |
| 116 | // and signal the solvers to fall back to the device-emitter projection. |
| 117 | cache->has_source_space = (p->input_xy_w[1] > 1e-6f) |
| 118 | && build_source_matrix(p->input_xy_r, p->input_xy_g, |
| 119 | p->input_xy_b, p->input_xy_w, |
no test coverage detected