Files
GR-raytracing/tests/test_frame.c
T
wyj d2a901cab1 Feat: Add CSV CIE 1931 blackbody LUT backend
Replace the in-tree Wyman analytic CIE fit with a repository CIE 1931
2-degree LUT derived from the 360-830 nm, 1 nm-linear CSV reference. The
GRBBLUT3 table stores 1024 uniform log(T) XYZ nodes over
[670.146556, 101408.88] K with four-point cubic Lagrange interpolation, and
the same CSV reference supplies a three-term Rayleigh-Jeans form above the
table and an inverse-temperature/log-XYZ crossover with channel-specific
endpoint forms below it. The loader validates the header and payload
checksum and rejects legacy formats and runtime generation.

Initialize the backend in the renderer, frame test, and PSF capture tool so
the new default is active wherever colors are produced.
2026-09-12 15:29:33 -04:00

515 lines
24 KiB
C

#include "frame.h"
#include "lens_map.h"
#include "optics.h"
#include <math.h>
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int mesh_has_hanging_vertex(const FrameLensMesh *mesh) {
for (size_t triangle = 0; triangle < mesh->triangle_count; ++triangle)
for (size_t side = 0; side < 3; ++side) {
const LensVertex *a = &mesh->vertices[mesh->triangles[triangle].vertex[side]];
const LensVertex *b =
&mesh->vertices[mesh->triangles[triangle].vertex[(side + 1) % 3]];
const double dx = b->image_x - a->image_x;
const double dy = b->image_y - a->image_y;
const double length_squared = dx * dx + dy * dy;
for (size_t vertex = 0; vertex < mesh->vertex_count; ++vertex) {
if (vertex == mesh->triangles[triangle].vertex[side] ||
vertex == mesh->triangles[triangle].vertex[(side + 1) % 3])
continue;
const LensVertex *p = &mesh->vertices[vertex];
const double px = p->image_x - a->image_x;
const double py = p->image_y - a->image_y;
const double cross = px * dy - py * dx;
const double position = (px * dx + py * dy) / length_squared;
if (fabs(cross) <= 1e-12 * length_squared && position > 1e-12 &&
position < 1.0 - 1e-12)
return 1;
}
}
return 0;
}
static int mesh_has_same_winding_shared_edge(const FrameLensMesh *mesh) {
for (size_t left_triangle = 0; left_triangle < mesh->triangle_count;
++left_triangle)
for (size_t left_side = 0; left_side < 3; ++left_side) {
const size_t from = mesh->triangles[left_triangle].vertex[left_side];
const size_t to =
mesh->triangles[left_triangle].vertex[(left_side + 1) % 3];
for (size_t right_triangle = left_triangle + 1;
right_triangle < mesh->triangle_count; ++right_triangle)
for (size_t right_side = 0; right_side < 3; ++right_side) {
const size_t other_from =
mesh->triangles[right_triangle].vertex[right_side];
const size_t other_to =
mesh->triangles[right_triangle].vertex[(right_side + 1) % 3];
if ((from == other_from && to == other_to) ||
(from == other_to && to == other_from)) {
if (from == other_from && to == other_to)
return 1;
}
}
}
return 0;
}
int main(void) {
const int width = 100, height = 100;
const double test_exposure = 1e-3;
const double psf_relative_tail = 1e-8;
const PointSpreadFunction psf = {.fwhm_pixels = 2.7, .moffat_beta = 4.5};
const GeodesicTraceConfig trace = {.coordinate_time_step = 0.25,
.max_steps = 100};
const ObserverState observer = observer_fixed_at_origin();
Star star = {
.direction = {0.0, 0.0, -1.0}, .temperature_K = 7000.0, .amplitude = 1.0};
StarCatalog catalog = {.stars = &star, .count = 1};
SpacetimeSource spacetime = {0};
FrameLensMesh mesh = {0};
double *hdr = calloc((size_t)width * height * 3, sizeof *hdr);
int result = 1;
if (blackbody_backend_init(NULL, 0, NAN, NAN, NULL, stderr) ||
hdr == NULL || spacetime_create_minkowski(&spacetime, 10.0) ||
frame_lens_mesh_build_coarse(&mesh, width, height, 20, 30.0) ||
frame_lens_mesh_trace(&mesh, &spacetime, &observer, &trace))
goto done;
const size_t images =
frame_splat_catalog(&mesh, &catalog, hdr, width, height, test_exposure,
&psf, NULL, INFINITY, 1.0, psf_relative_tail,
0.0, 0, 1, NULL, NULL, NULL);
if (images != 1 || hdr[3 * (50 * width + 50)] <= 0.0) {
fputs("flat-space inverse lens-map regression failed\n", stderr);
goto done;
}
/* A finalized mesh can be persisted independently of spacetime and then
* drive the exact same catalog inverse-map and PSF pass. */
const char *lens_map_path = "/tmp/gr_lens_map_test.grlens";
const LensMapFrame saved_frame = {.frame_id = 7,
.coordinate_time = 3.0,
.proper_time = 2.0,
.mesh = mesh};
LensMap loaded_map = {0};
double *roundtrip_hdr = calloc((size_t)width * height * 3, sizeof *roundtrip_hdr);
if (roundtrip_hdr == NULL ||
lens_map_write(lens_map_path, width, height, 30.0, &saved_frame, 1) ||
lens_map_read(lens_map_path, &loaded_map) || loaded_map.frame_count != 1 ||
loaded_map.frames[0].frame_id != 7 || loaded_map.width != width ||
loaded_map.height != height ||
loaded_map.frames[0].mesh.vertex_count != mesh.vertex_count ||
frame_splat_catalog(&loaded_map.frames[0].mesh, &catalog, roundtrip_hdr,
width, height, test_exposure, &psf, NULL, INFINITY,
1.0, psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL) != images) {
fputs("lens-map round-trip regression failed\n", stderr);
free(roundtrip_hdr); lens_map_destroy(&loaded_map); unlink(lens_map_path);
goto done;
}
for (int value = 0; value < width * height * 3; ++value)
if (hdr[value] != roundtrip_hdr[value]) {
fputs("lens-map round-trip HDR regression failed\n", stderr);
free(roundtrip_hdr); lens_map_destroy(&loaded_map); unlink(lens_map_path);
goto done;
}
free(roundtrip_hdr);
lens_map_destroy(&loaded_map);
/* A damaged payload must not be mistaken for a reusable physical map. */
FILE *damaged = fopen(lens_map_path, "r+b");
int damage_failed = damaged == NULL;
if (!damage_failed) {
if (fseek(damaged, -5L, SEEK_END))
damage_failed = 1;
const int original = damage_failed ? EOF : fgetc(damaged);
if (damage_failed || fseek(damaged, -5L, SEEK_END) || original == EOF ||
fputc(original ^ 0xff, damaged) == EOF)
damage_failed = 1;
}
if (damaged != NULL && fclose(damaged))
damage_failed = 1;
if (damage_failed || !lens_map_read(lens_map_path, &loaded_map)) {
fputs("lens-map corruption rejection regression failed\n", stderr);
lens_map_destroy(&loaded_map); unlink(lens_map_path);
goto done;
}
unlink(lens_map_path);
memset(hdr, 0, (size_t)width * height * 3 * sizeof *hdr);
PsfSplatStats min_y_stats = {0};
if (frame_splat_catalog(&mesh, &catalog, hdr, width, height, test_exposure,
&psf, NULL, INFINITY, 1.0, psf_relative_tail,
1e300, 0, 1, NULL, &min_y_stats, NULL) != 1 ||
min_y_stats.discarded_below_min_y != 1 ||
hdr[3 * (50 * width + 50)] != 0.0) {
fputs("PSF minimum-Y discard regression failed\n", stderr);
goto done;
}
/* Private HDR accumulation must preserve the serial splat result. */
double *serial_hdr = calloc((size_t)width * height * 3, sizeof *serial_hdr);
double *parallel_hdr = calloc((size_t)width * height * 3, sizeof *parallel_hdr);
if (serial_hdr == NULL || parallel_hdr == NULL) {
free(serial_hdr);
free(parallel_hdr);
goto done;
}
const int original_threads = omp_get_max_threads();
omp_set_dynamic(0);
omp_set_num_threads(1);
const size_t serial_images = frame_splat_catalog(
&mesh, &catalog, serial_hdr, width, height, test_exposure, &psf, NULL,
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL);
omp_set_num_threads(4);
const size_t parallel_images = frame_splat_catalog(
&mesh, &catalog, parallel_hdr, width, height, test_exposure, &psf, NULL,
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL);
omp_set_num_threads(original_threads);
for (int value = 0; value < width * height * 3; ++value)
if (fabs(serial_hdr[value] - parallel_hdr[value]) >
1e-12 * fmax(1.0, fabs(serial_hdr[value]))) {
fputs("parallel catalog splat regression failed\n", stderr);
free(serial_hdr);
free(parallel_hdr);
goto done;
}
free(serial_hdr);
free(parallel_hdr);
if (serial_images != 1 || parallel_images != serial_images) {
fputs("parallel catalog image-count regression failed\n", stderr);
goto done;
}
const LinearRgb cool = blackbody_to_linear_rgb(3000.0);
const LinearRgb hot = blackbody_to_linear_rgb(10000.0);
if (!(cool.r > cool.b && hot.b > hot.r &&
hot.r + hot.g + hot.b > cool.r + cool.g + cool.b)) {
fputs("blackbody spectral-color regression failed\n", stderr);
goto done;
}
/* The Moffat is flux-normalized and retains a measurable, continuous wing
* beyond the former Gaussian's 3-sigma raster box. */
memset(hdr, 0, (size_t)width * height * 3 * sizeof *hdr);
splat_moffat(hdr, width, height, 50.5, 50.5,
(LinearRgb){1.0, 1.0, 1.0}, 1.0, &psf, psf_relative_tail, 0.0);
double moffat_flux = 0.0;
for (int pixel = 0; pixel < width * height; ++pixel)
moffat_flux += hdr[3 * pixel];
if (fabs(moffat_flux - 1.0) > 0.01 ||
hdr[3 * (50 * width + 62)] <= 0.0) {
fputs("Moffat normalization or wing regression failed\n", stderr);
goto done;
}
/* The cache stores 4-point pixel-area integrals over a 64x64 sub-pixel
* lattice. Compare its bilinear interpolation with the independent 8-point
* direct reference at phases on both sides of a pixel boundary. */
PsfKernelCache cache = {0};
double *cached_hdr = calloc((size_t)width * height * 3, sizeof *cached_hdr);
double *reference_hdr = calloc((size_t)width * height * 3, sizeof *reference_hdr);
if (cached_hdr == NULL || reference_hdr == NULL ||
psf_kernel_cache_init(&cache, &psf, psf_relative_tail)) {
free(cached_hdr);
free(reference_hdr);
psf_kernel_cache_destroy(&cache);
fputs("PSF cache construction regression failed\n", stderr);
goto done;
}
PsfKernelCache loose_tail_cache = {0};
if (psf_kernel_cache_init(&loose_tail_cache, &psf, 1e-5) ||
loose_tail_cache.relative_tail_fraction != 1e-5 ||
loose_tail_cache.radius_pixels >= cache.radius_pixels) {
fputs("PSF relative-tail cache-radius regression failed\n", stderr);
psf_kernel_cache_destroy(&loose_tail_cache);
free(cached_hdr); free(reference_hdr); psf_kernel_cache_destroy(&cache);
goto done;
}
psf_kernel_cache_destroy(&loose_tail_cache);
/* This is the exact eligibility split that a future event sink exposes to
* HIP: cache event, CPU direct fallback, or min-Y discard. */
PsfCachedEvent prepared = {0};
if (psf_prepare_cached_event(&prepared, 12.25, 14.75,
(LinearRgb){1.0, 1.0, 1.0}, 1.0, &psf, &cache,
1.0, psf_relative_tail, 0.0) != 0 ||
prepared.x != 12.25 || prepared.y != 14.75 ||
!(prepared.support_radius > 0.0) ||
psf_prepare_cached_event(&prepared, 12.25, 14.75,
(LinearRgb){1.0, 1.0, 1.0}, 1000.0, &psf, &cache,
1.0, psf_relative_tail, 0.0) != 1 ||
psf_prepare_cached_event(&prepared, 12.25, 14.75,
(LinearRgb){1.0, 1.0, 1.0}, 1.0, &psf, &cache,
1.0, psf_relative_tail, 1.0) != 3) {
fputs("PSF event eligibility regression failed\n", stderr);
free(cached_hdr); free(reference_hdr); psf_kernel_cache_destroy(&cache);
goto done;
}
const double phases[][2] = {{0.01, 0.99}, {0.499, 0.501}, {0.999, 0.001}};
for (size_t phase = 0; phase < sizeof phases / sizeof *phases; ++phase) {
memset(cached_hdr, 0, (size_t)width * height * 3 * sizeof *cached_hdr);
memset(reference_hdr, 0, (size_t)width * height * 3 * sizeof *reference_hdr);
if (splat_moffat_cached(cached_hdr, width, height, 50.0 + phases[phase][0],
50.0 + phases[phase][1], (LinearRgb){1.0, 1.0, 1.0},
1.0, &psf, &cache, 1.0, psf_relative_tail, 0.0) != 0) {
fputs("ordinary PSF cache unexpectedly fell back\n", stderr);
free(cached_hdr); free(reference_hdr); psf_kernel_cache_destroy(&cache);
goto done;
}
splat_moffat_direct(reference_hdr, width, height,
50.0 + phases[phase][0], 50.0 + phases[phase][1],
(LinearRgb){1.0, 1.0, 1.0}, 1.0, &psf, psf_relative_tail, 0.0);
double peak = 0.0, max_error = 0.0;
for (int value = 0; value < width * height * 3; ++value) {
peak = fmax(peak, reference_hdr[value]);
max_error = fmax(max_error, fabs(cached_hdr[value] - reference_hdr[value]));
}
if (peak <= 0.0 || max_error > 4e-5 * peak) {
fputs("PSF cache interpolation accuracy regression failed\n", stderr);
free(cached_hdr); free(reference_hdr); psf_kernel_cache_destroy(&cache);
goto done;
}
}
if (splat_moffat_cached(cached_hdr, width, height, 50.5, 50.5,
(LinearRgb){1.0, 1.0, 1.0}, 1.0, &psf, &cache,
1.0, psf_relative_tail, 1.0) != 3) {
fputs("PSF minimum-Y cached discard regression failed\n", stderr);
free(cached_hdr); free(reference_hdr); psf_kernel_cache_destroy(&cache);
goto done;
}
/* A bright event must avoid a cached hard cutoff by selecting the direct
* reference path when the requested support exceeds the cache. */
if (splat_moffat_cached(cached_hdr, width, height, 50.5, 50.5,
(LinearRgb){1.0, 1.0, 1.0}, 1000.0, &psf, &cache, 1.0,
psf_relative_tail, 0.0) != 1) {
fputs("bright PSF direct-fallback regression failed\n", stderr);
free(cached_hdr); free(reference_hdr); psf_kernel_cache_destroy(&cache);
goto done;
}
memset(cached_hdr, 0, (size_t)width * height * 3 * sizeof *cached_hdr);
memset(reference_hdr, 0, (size_t)width * height * 3 * sizeof *reference_hdr);
if (splat_moffat_cached(cached_hdr, width, height, 50.5, 50.5,
(LinearRgb){1.0, 1.0, 1.0}, 1000.0, &psf, &cache,
1000.0, psf_relative_tail, 0.0) != 2) {
fputs("bright PSF cached-wing-clipping regression failed\n", stderr);
free(cached_hdr); free(reference_hdr); psf_kernel_cache_destroy(&cache);
goto done;
}
splat_moffat_direct(reference_hdr, width, height, 50.5, 50.5,
(LinearRgb){1.0, 1.0, 1.0}, 1000.0, &psf, psf_relative_tail, 0.0);
const size_t center = 3 * (50 * width + 50);
if (cached_hdr[center] <= 0.0 ||
fabs(cached_hdr[center] - reference_hdr[center]) >
4e-5 * reference_hdr[center]) {
fputs("cached-wing clipping changed the bright PSF core\n", stderr);
free(cached_hdr); free(reference_hdr); psf_kernel_cache_destroy(&cache);
goto done;
}
free(cached_hdr);
free(reference_hdr);
psf_kernel_cache_destroy(&cache);
frame_draw_mesh(&mesh, hdr, width, height, 0.5, 0.5);
if (hdr[3 * (10 * width + 20)] != 0.25) {
fputs("mesh diagnostic overlay regression failed\n", stderr);
goto done;
}
/* A fixed absolute edge tolerance used to make tiny source triangles claim
* sources far outside their field. */
FrameLensMesh fine_mesh = {0};
Star fine_stars[2] = {{.direction = {0.0, 0.0, -1.0},
.temperature_K = 7000.0,
.amplitude = 1.0},
{.direction = {0.01, 0.0, -0.9999499987499375},
.temperature_K = 7000.0,
.amplitude = 1.0}};
StarCatalog fine_catalog = {.stars = fine_stars, .count = 2};
memset(hdr, 0, (size_t)width * height * 3 * sizeof *hdr);
if (frame_lens_mesh_build_coarse(&fine_mesh, width, height, 1, 0.1) ||
frame_lens_mesh_trace(&fine_mesh, &spacetime, &observer, &trace) ||
frame_splat_catalog(&fine_mesh, &fine_catalog, hdr, width, height,
test_exposure, &psf, NULL, INFINITY, 1.0,
psf_relative_tail, 0.0, 0, 1, NULL,
NULL, NULL) != 1) {
fputs("fine source-triangle containment regression failed\n", stderr);
frame_lens_mesh_destroy(&fine_mesh);
goto done;
}
frame_lens_mesh_destroy(&fine_mesh);
/* Schwarzschild level-4/J=0.2 ring triangle: the old edge tolerance accepts
* (1,0,0) although it is outside, producing unsigned weights summing to
* 1.09608. Keep a genuine interior source after it to check that rejecting
* one source does not discard subsequent stars. Exercise both parities. */
const double thin_directions[3][3] = {
{0.99999998891116071, 0.00013431364716360775, 6.4323579270168807e-05},
{0.99999997228355786, -0.00021216644782556991, -0.00010206998544149922},
{0.9999927016945267, -0.0034455730513416835, -0.001650631403158936}};
LensVertex thin_vertices[3] = {
{.image_x = 40, .image_y = 40, .status = RAY_ENDPOINT_ESCAPED},
{.image_x = 48, .image_y = 40, .status = RAY_ENDPOINT_ESCAPED},
{.image_x = 40, .image_y = 48, .status = RAY_ENDPOINT_ESCAPED}};
LensTriangle thin_triangle = {.vertex = {0, 1, 2}};
FrameLensMesh thin_mesh = {.vertices = thin_vertices, .vertex_count = 3,
.triangles = &thin_triangle, .triangle_count = 1};
Star thin_stars[2] = {
{.direction = {1, 0, 0}, .temperature_K = 7000, .amplitude = 1},
{.temperature_K = 7000, .amplitude = 1}};
for (int i = 0; i < 3; ++i) {
memcpy(thin_vertices[i].n_infinity, thin_directions[i],
sizeof thin_directions[i]);
memcpy(thin_vertices[i].camera_direction, thin_directions[i],
sizeof thin_directions[i]);
for (int axis = 0; axis < 3; ++axis)
thin_stars[1].direction[axis] += thin_directions[i][axis];
}
const double thin_norm = hypot(hypot(thin_stars[1].direction[0],
thin_stars[1].direction[1]),
thin_stars[1].direction[2]);
for (int axis = 0; axis < 3; ++axis)
thin_stars[1].direction[axis] /= thin_norm;
StarCatalog thin_catalog = {.stars = thin_stars, .count = 2};
for (int parity = 0; parity < 2; ++parity) {
thin_triangle.vertex[1] = parity ? 2 : 1;
thin_triangle.vertex[2] = parity ? 1 : 2;
memset(hdr, 0, (size_t)width * height * 3 * sizeof *hdr);
if (frame_splat_catalog(&thin_mesh, &thin_catalog, hdr, width, height,
test_exposure, &psf, NULL, 1.0, 1.0,
psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL) != 1 ||
hdr[3 * (43 * width + 43)] <= 0.0) {
fputs("thin source-triangle inverse-map regression failed\n", stderr);
goto done;
}
}
/* Refinement probes are temporary until their generation is complete. A
* shared diagonal probe must produce one stable midpoint and conforming
* children only after its endpoint has been installed. */
FrameLensMesh adaptive_mesh = {0};
RefinementConfig refine = {.max_level = 1,
.angle_absolute_rad = 1e-4,
.angle_relative = 1e-4,
.jacobian_minimum = 1e-3,
.min_edge_pixels = 1.0,
.min_area_pixels2 = 1.0};
if (frame_lens_mesh_build_coarse(&adaptive_mesh, width, height, 100, 30.0))
goto done;
for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) {
adaptive_mesh.vertices[i].traced = 1;
adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED;
adaptive_mesh.vertices[i].n_infinity[0] = 1.0;
}
if (frame_lens_mesh_prepare_generation(&adaptive_mesh, &refine) != 1) {
fputs("adaptive shared-edge probe setup regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
const RayEndpoint bent_probe = {.n_infinity = {0.0, 1.0, 0.0},
.frequency_ratio = 1.0,
.status = RAY_ENDPOINT_ESCAPED};
if (frame_lens_mesh_install_sample(&adaptive_mesh, 0, &bent_probe) ||
frame_lens_mesh_finish_generation(&adaptive_mesh, &refine) != 1 ||
adaptive_mesh.vertex_count != 5 || adaptive_mesh.triangle_count != 4) {
fputs("adaptive shared-edge split regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
frame_lens_mesh_destroy(&adaptive_mesh);
/* A capture/escape discontinuity is a shadow boundary, not a smooth map
* error: request all three midpoint rays and red-refine in one generation. */
if (frame_lens_mesh_build_coarse(&adaptive_mesh, width, height, 100, 30.0))
goto done;
adaptive_mesh.triangle_count = 1;
for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) {
adaptive_mesh.vertices[i].traced = 1;
adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED;
adaptive_mesh.vertices[i].n_infinity[0] = 1.0;
}
adaptive_mesh.vertices[0].status = RAY_ENDPOINT_CAPTURED;
refine.max_level = 1;
refine.angle_absolute_rad = 3.14159265358979323846;
refine.angle_relative = 1e6;
refine.jacobian_minimum = 1e-12;
if (frame_lens_mesh_prepare_generation(&adaptive_mesh, &refine) != 3) {
fputs("shadow-boundary red-probe setup regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
for (size_t i = 0; i < adaptive_mesh.sample_count; ++i)
if (frame_lens_mesh_install_sample(&adaptive_mesh, i, &bent_probe)) {
fputs("shadow-boundary red-probe installation regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
if (frame_lens_mesh_finish_generation(&adaptive_mesh, &refine) != 3 ||
adaptive_mesh.vertex_count != 7 || adaptive_mesh.triangle_count != 4) {
fputs("shadow-boundary red-refinement regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
frame_lens_mesh_destroy(&adaptive_mesh);
/* Two shadow leaves can force two edges of an escaped neighbour. That
* neighbour must use a local three-child blue split, not create a third
* requested edge that spreads red refinement farther outward. */
if (frame_lens_mesh_build_coarse(&adaptive_mesh, 200, 100, 100, 30.0))
goto done;
adaptive_mesh.triangle_count = 3;
for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) {
adaptive_mesh.vertices[i].traced = 1;
adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED;
adaptive_mesh.vertices[i].n_infinity[0] = 1.0;
}
adaptive_mesh.vertices[3].status = RAY_ENDPOINT_CAPTURED;
adaptive_mesh.vertices[5].status = RAY_ENDPOINT_CAPTURED;
if (frame_lens_mesh_prepare_generation(&adaptive_mesh, &refine) != 6) {
fputs("shadow-boundary blue-neighbour probe setup regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
for (size_t i = 0; i < adaptive_mesh.sample_count; ++i)
if (frame_lens_mesh_install_sample(&adaptive_mesh, i, &bent_probe)) {
fputs("shadow-boundary blue-neighbour probe installation regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
if (frame_lens_mesh_finish_generation(&adaptive_mesh, &refine) != 6 ||
adaptive_mesh.vertex_count != 12 || adaptive_mesh.triangle_count != 11 ||
mesh_has_hanging_vertex(&adaptive_mesh) ||
mesh_has_same_winding_shared_edge(&adaptive_mesh)) {
fputs("shadow-boundary blue-neighbour refinement regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
frame_lens_mesh_destroy(&adaptive_mesh);
const RayEndpoint flat_probe = {.n_infinity = {1.0, 0.0, 0.0},
.frequency_ratio = 1.0,
.status = RAY_ENDPOINT_ESCAPED};
/* Opposite nonzero discrete-Jacobian signs on the two sides of the shared
* diagonal require a sufficiently small magnitude before requesting it. */
refine.jacobian_minimum = 10.0;
refine.angle_absolute_rad = 3.14159265358979323846;
refine.angle_relative = 1e6;
if (frame_lens_mesh_build_coarse(&adaptive_mesh, width, height, 100, 30.0))
goto done;
const double source_directions[4][3] = {
{1.0, 0.0, 0.0},
{sqrt(0.99), 0.0, 0.1},
{sqrt(0.99), 0.0, 0.1},
{sqrt(0.98), 0.1, 0.1}};
for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) {
adaptive_mesh.vertices[i].traced = 1;
adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED;
memcpy(adaptive_mesh.vertices[i].n_infinity, source_directions[i],
sizeof source_directions[i]);
}
if (frame_lens_mesh_prepare_generation(&adaptive_mesh, &refine) != 1 ||
frame_lens_mesh_install_sample(&adaptive_mesh, 0, &flat_probe) ||
frame_lens_mesh_finish_generation(&adaptive_mesh, &refine) != 1 ||
adaptive_mesh.vertex_count != 5 || adaptive_mesh.triangle_count != 4) {
fputs("adaptive fold-parity split regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
frame_lens_mesh_destroy(&adaptive_mesh);
result = 0;
done:
frame_lens_mesh_destroy(&mesh);
spacetime_destroy(&spacetime);
blackbody_backend_destroy();
free(hdr);
return result;
}