Project an out-of-hull target XYZ onto the achievable LED gamut (#2708, math-model gist §3). Returns true if a projection was applied, in which case `X_t` and `xy_t` are updated to the projected point at unit Y. When the target is already inside the full RGB triangle, returns false and leaves `X_t` / `xy_t` untouched. Algorithm matches the reference's `_strict_project_target_xyz_to_led_hull`: run
| 181 | // 1.0 per sub-gamut, then pick the sub-gamut with smallest residual. Use |
| 182 | // the achieved chromaticity as the new target xy. |
| 183 | static bool project_to_hull(const ProfileCache& cache, |
| 184 | float X_t[3], float xy_t[2]) FL_NOEXCEPT { |
| 185 | const float sum = X_t[0] + X_t[1] + X_t[2]; |
| 186 | if (sum < 1e-12f) return false; |
| 187 | xy_t[0] = X_t[0] / sum; |
| 188 | xy_t[1] = X_t[1] / sum; |
| 189 | |
| 190 | const DiodeProfile& p = *cache.profile; |
| 191 | float bary[3]; |
| 192 | // Test the full RGB triangle (not the sub-gamut triangles) — a target |
| 193 | // can be outside a single sub-gamut yet still inside the full hull. |
| 194 | if (barycentric_xy(xy_t, p.xy_r, p.xy_g, p.xy_b, bary) |
| 195 | && bary[0] >= -1e-9f && bary[1] >= -1e-9f && bary[2] >= -1e-9f) { |
| 196 | return false; |
| 197 | } |
| 198 | |
| 199 | struct Tri { const float* a; const float* b; const float* c; }; |
| 200 | const Tri tris[3] = { |
| 201 | { cache.P_R, cache.P_G, cache.P_W }, |
| 202 | { cache.P_R, cache.P_B, cache.P_W }, |
| 203 | { cache.P_B, cache.P_G, cache.P_W }, |
| 204 | }; |
| 205 | float best_xyz[3] = {0, 0, 0}; |
| 206 | float best_residual = 1e30f; |
| 207 | for (int k = 0; k < 3; ++k) { |
| 208 | const Tri& tri = tris[k]; |
| 209 | const float M[3][3] = { |
| 210 | { tri.a[0], tri.b[0], tri.c[0] }, |
| 211 | { tri.a[1], tri.b[1], tri.c[1] }, |
| 212 | { tri.a[2], tri.b[2], tri.c[2] }, |
| 213 | }; |
| 214 | float t[3], residual; |
| 215 | nnls3(M, X_t, t, &residual); |
| 216 | // Cap drive at full-scale per sub-gamut, matching the reference. |
| 217 | float mt = t[0]; |
| 218 | if (t[1] > mt) mt = t[1]; |
| 219 | if (t[2] > mt) mt = t[2]; |
| 220 | if (mt > 1.0f) { const float inv = 1.0f / mt; t[0] *= inv; t[1] *= inv; t[2] *= inv; } |
| 221 | float xyz[3]; |
| 222 | xyz[0] = M[0][0]*t[0] + M[0][1]*t[1] + M[0][2]*t[2]; |
| 223 | xyz[1] = M[1][0]*t[0] + M[1][1]*t[1] + M[1][2]*t[2]; |
| 224 | xyz[2] = M[2][0]*t[0] + M[2][1]*t[1] + M[2][2]*t[2]; |
| 225 | if (xyz[1] <= 1e-12f) continue; |
| 226 | if (residual < best_residual) { |
| 227 | best_residual = residual; |
| 228 | best_xyz[0] = xyz[0]; best_xyz[1] = xyz[1]; best_xyz[2] = xyz[2]; |
| 229 | } |
| 230 | } |
| 231 | if (best_xyz[1] <= 1e-12f) { |
| 232 | // All sub-gamut projections collapsed to zero — pathological |
| 233 | // profile or extreme target. Leave the original X_t alone; the |
| 234 | // strict solver will return false and the dispatch falls back. |
| 235 | return false; |
| 236 | } |
| 237 | const float s2 = best_xyz[0] + best_xyz[1] + best_xyz[2]; |
| 238 | xy_t[0] = best_xyz[0] / s2; |
| 239 | xy_t[1] = best_xyz[1] / s2; |
| 240 | // Per reference, normalize Y to 1.0 for downstream routing. The full-chroma |
no test coverage detected