/* Regression tests for the display tone mapping in src/optics.c. * * These call the production public functions so the test cannot drift from the * implementation; no formula is duplicated here. The test needs neither a * catalog, ray tracing, a GPU, nor image files, and it builds in both the * ENABLE_PNG=1 and ENABLE_PNG=0 configurations. */ #include "optics.h" #include #include #include static int failures = 0; static void check(int condition, const char *message) { if (!condition) { fprintf(stderr, "FAIL: %s\n", message); ++failures; } } static int close_to(double value, double expected, double tolerance) { return fabs(value - expected) <= tolerance; } int main(void) { const double grid[] = {0.0, 1e-12, 1e-9, 1e-6, 1e-3, 0.01, 0.1, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 4.0, 10.0, 1e6}; const size_t grid_count = sizeof grid / sizeof grid[0]; const double hardness[] = {1.0, 2.0, 4.0}; const double small[] = {1e-12, 1e-9, 1e-6}; const double negative[] = {-1e-12, -0.5, -1.0, -1e6}; const ToneMapSettings softclip2 = {TONE_MAP_SOFTCLIP, 2.0}; const ToneMapSettings reinhard = {TONE_MAP_REINHARD, 2.0}; /* Basic properties for every supported softclip hardness. */ for (size_t h = 0; h < 3; ++h) { const ToneMapSettings settings = {TONE_MAP_SOFTCLIP, hardness[h]}; check(tone_map_linear_channel(0.0, &settings) == 0.0, "softclip T(0) == 0"); check(tone_map_linear_channel(INFINITY, &settings) == 1.0, "softclip T(+infinity) == 1"); check(tone_map_linear_channel(1e300, &settings) == 1.0, "softclip overflow saturates at 1"); for (size_t n = 0; n < sizeof negative / sizeof negative[0]; ++n) check(tone_map_linear_channel(negative[n], &settings) == 0.0, "softclip negative input maps to 0"); double previous = tone_map_linear_channel(grid[0], &settings); for (size_t i = 1; i < grid_count; ++i) { const double current = tone_map_linear_channel(grid[i], &settings); check(isfinite(current) && current >= 0.0 && current <= 1.0, "softclip output is finite and bounded"); check(current >= previous, "softclip is monotonically non-decreasing"); previous = current; } for (size_t i = 0; i < sizeof small / sizeof small[0]; ++i) { const double ratio = tone_map_linear_channel(small[i], &settings) / small[i]; check(close_to(ratio, 1.0, 1e-9), "softclip T(x)/x tends to 1 for small x"); } } /* Known double-precision values of T_2(x) = sqrt(tanh(x^2)). */ check(close_to(tone_map_linear_channel(0.5, &softclip2), 0.49489257663023107, 1e-12), "T2(0.5) value"); check(close_to(tone_map_linear_channel(1.0, &softclip2), 0.8726936208978296, 1e-12), "T2(1.0) value"); check(close_to(tone_map_linear_channel(2.0, &softclip2), 0.9996645936208139, 1e-12), "T2(2.0) value"); /* NULL settings select the production default softclip, p = 2. */ check(tone_map_linear_channel(0.5, NULL) == tone_map_linear_channel(0.5, &softclip2), "NULL settings default to softclip p=2"); /* Both operators must sanitize non-finite and nonpositive inputs: the * public contract is a finite result in [0, 1] and lround() must never see * NaN. This includes Reinhard, whose raw formula would map x < -1 to white * and produce NaN for +Infinity or NaN. */ const ToneMapSettings operators[] = {softclip2, reinhard}; for (size_t i = 0; i < sizeof operators / sizeof operators[0]; ++i) { check(tone_map_linear_channel(NAN, &operators[i]) == 0.0, "NaN maps to 0 for every operator"); check(tone_map_linear_channel(INFINITY, &operators[i]) == 1.0, "+infinity maps to 1 for every operator"); check(tone_map_linear_channel(-2.0, &operators[i]) == 0.0, "negative input maps to 0 for every operator"); check(tone_map_linear_channel(-0.5, &operators[i]) == 0.0, "small negative input maps to 0 for every operator"); check(tone_map_srgb8_channel(NAN, &operators[i]) == 0, "NaN 8-bit output is black"); check(tone_map_srgb8_channel(INFINITY, &operators[i]) == 255, "+infinity 8-bit output is white"); } /* Reinhard must reproduce the pre-soft-clip formula bit for bit. */ check(tone_map_linear_channel(0.5, &reinhard) == 0.5 / (1.0 + 0.5), "reinhard x=0.5 -> 1/3"); check(tone_map_linear_channel(1.0, &reinhard) == 1.0 / (1.0 + 1.0), "reinhard x=1.0 -> 1/2"); check(tone_map_linear_channel(2.0, &reinhard) == 2.0 / (1.0 + 2.0), "reinhard x=2.0 -> 2/3"); /* The 8-bit path is what the PNG and PPM writers actually emit. */ check(tone_map_srgb8_channel(0.0, &softclip2) == 0, "softclip p=2 x=0 is black"); check(tone_map_srgb8_channel(2.0, &softclip2) == 255, "softclip p=2 x=2 saturates the white channel"); check(tone_map_srgb8_channel(2.0, &reinhard) != 255, "reinhard x=2 is not fully saturated"); check(tone_map_srgb8_channel(0.5, &softclip2) > tone_map_srgb8_channel(0.5, &reinhard), "softclip is brighter than reinhard at x=0.5"); if (failures != 0) { fprintf(stderr, "%d tone-map assertion(s) failed\n", failures); return EXIT_FAILURE; } puts("tone-map tests passed"); return EXIT_SUCCESS; }