| 859 | } |
| 860 | |
| 861 | LutTable build_lut(const ProfileCache& cache, int grid_n, |
| 862 | LutInterp interp) FL_NOEXCEPT { |
| 863 | LutTable lut; |
| 864 | // Guard against degenerate grids: N < 2 would divide-by-zero in the |
| 865 | // 1/(N-1) step below, and negative N would overflow the allocation size. |
| 866 | // Return an empty LutTable so callers can detect failure via .N == 0. |
| 867 | if (grid_n < 2) { |
| 868 | return lut; |
| 869 | } |
| 870 | const fl::size_t n = static_cast<fl::size_t>(grid_n); |
| 871 | lut.N = grid_n; |
| 872 | lut.interp = interp; |
| 873 | const fl::size_t stride = |
| 874 | (interp == LutInterp::Hermite) ? kLutStrideHermite : kLutStrideBilinear; |
| 875 | lut.cells = fl::make_unique<i16[]>(n * n * stride); |
| 876 | |
| 877 | const float* R = cache.profile->xy_r; |
| 878 | const float* G = cache.profile->xy_g; |
| 879 | const float* B = cache.profile->xy_b; |
| 880 | const float xmin = fl::min(fl::min(R[0], G[0]), B[0]); |
| 881 | const float xmax = fl::max(fl::max(R[0], G[0]), B[0]); |
| 882 | const float ymin = fl::min(fl::min(R[1], G[1]), B[1]); |
| 883 | const float ymax = fl::max(fl::max(R[1], G[1]), B[1]); |
| 884 | const float xpad = (xmax - xmin) * 0.02f; |
| 885 | const float ypad = (ymax - ymin) * 0.02f; |
| 886 | lut.xy_min[0] = xmin - xpad; |
| 887 | lut.xy_min[1] = ymin - ypad; |
| 888 | lut.xy_max[0] = xmax + xpad; |
| 889 | lut.xy_max[1] = ymax + ypad; |
| 890 | |
| 891 | i16* cells = lut.cells.get(); |
| 892 | const int N = grid_n; |
| 893 | const float inv_Nm1 = 1.0f / static_cast<float>(N - 1); |
| 894 | const float cell_dx = (lut.xy_max[0] - lut.xy_min[0]) * inv_Nm1; |
| 895 | const float cell_dy = (lut.xy_max[1] - lut.xy_min[1]) * inv_Nm1; |
| 896 | |
| 897 | auto sample = [&](float x, float y, float out[4]) FL_NOEXCEPT { |
| 898 | const float xy[2] = {x, y}; |
| 899 | solve_strict_subgamut_xy(cache, xy, 1.0f, out); |
| 900 | }; |
| 901 | |
| 902 | for (int j = 0; j < N; ++j) { |
| 903 | const float y = lut.xy_min[1] + cell_dy * j; |
| 904 | for (int i = 0; i < N; ++i) { |
| 905 | const float x = lut.xy_min[0] + cell_dx * i; |
| 906 | float v[4]; |
| 907 | sample(x, y, v); |
| 908 | i16* cell = &cells[(j * N + i) * stride]; |
| 909 | cell[0] = quantize_lut_cell(v[0]); |
| 910 | cell[1] = quantize_lut_cell(v[1]); |
| 911 | cell[2] = quantize_lut_cell(v[2]); |
| 912 | cell[3] = quantize_lut_cell(v[3]); |
| 913 | |
| 914 | if (interp != LutInterp::Hermite) continue; |
| 915 | |
| 916 | // Numerical partials in cell-parameter (t) units. Sample |
| 917 | // half-a-cell ahead / behind in world coords, clamped to the LUT |
| 918 | // domain so edges fall back to one-sided differences. With |
nothing calls this directly
no test coverage detected