diff --git a/README.md b/README.md index 3f1ef07..aee3824 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,11 @@ cached/direct-fallback image counts. Pass `--psf-direct` to use the slower whose required HDR-tail support exceeds the cache radius selects that reference path automatically. +`--psf-relative-tail R` controls the maximum omitted PSF tail fraction per +image; it defaults to `1e-8`. Larger values intentionally shorten the Moffat +support and rebuild the immutable cache at the corresponding radius, which is +useful when a faster, lower-fidelity render is acceptable. + For bounded preview renders, `--max-cache-psf-flux F` (default `1`) allows images with `1 < flux <= F` to use the existing cache instead of the direct evaluator. This does not rebuild or enlarge the cache: the image retains its diff --git a/src/frame.c b/src/frame.c index 0034a1f..393e4f7 100644 --- a/src/frame.c +++ b/src/frame.c @@ -850,6 +850,7 @@ typedef struct { const PointSpreadFunction *psf; const PsfKernelCache *psf_cache; double max_cache_psf_flux; + double psf_relative_tail; size_t images; size_t direct_fallbacks; size_t cached_wing_clipped; @@ -917,7 +918,8 @@ static int splat_catalog_tile(const Star *stars, size_t count, const int direct_fallback = splat_moffat_cached( context->hdr, context->width, context->height, image_x, image_y, color, flux, - context->psf, context->psf_cache, context->max_cache_psf_flux); + context->psf, context->psf_cache, context->max_cache_psf_flux, + context->psf_relative_tail); context->direct_fallbacks += direct_fallback == 1; context->cached_wing_clipped += direct_fallback == 2; #ifdef GR_DEBUG @@ -941,7 +943,8 @@ static CatalogSplatStats splat_catalog_triangles( const FrameLensMesh *mesh, StarCatalog *catalog, double *hdr, int width, int height, double exposure, const PointSpreadFunction *psf, const PsfKernelCache *psf_cache, double max_magnification, - double max_cache_psf_flux, size_t first_triangle, size_t last_triangle) { + double max_cache_psf_flux, double psf_relative_tail, + size_t first_triangle, size_t last_triangle) { CatalogSplatStats stats = {0}; for (size_t t = first_triangle; t < last_triangle; ++t) { const LensVertex *vertex[3]; @@ -974,7 +977,8 @@ static CatalogSplatStats splat_catalog_triangles( .width = width, .height = height, .exposure = exposure, .magnification = magnification, .psf = psf, .psf_cache = psf_cache, - .max_cache_psf_flux = max_cache_psf_flux}; + .max_cache_psf_flux = max_cache_psf_flux, + .psf_relative_tail = psf_relative_tail}; if (catalog_visit_source_triangle(catalog, direction, 0, splat_catalog_tile, &context) == 0) stats.images += context.images; @@ -1011,6 +1015,7 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh, const PsfKernelCache *psf_cache, double max_magnification, double max_cache_psf_flux, + double psf_relative_tail, int limit_workers_by_memory, int catalog_load_workers, CatalogPrefetchStats *prefetch_stats, @@ -1019,7 +1024,9 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh, if (mesh == NULL || catalog == NULL || hdr == NULL || exposure <= 0.0 || psf == NULL || width <= 0 || height <= 0 || catalog_load_workers <= 0 || isnan(max_magnification) || max_magnification <= 0.0 || - !isfinite(max_cache_psf_flux) || max_cache_psf_flux < 1.0) + !isfinite(max_cache_psf_flux) || max_cache_psf_flux < 1.0 || + !isfinite(psf_relative_tail) || psf_relative_tail <= 0.0 || + psf_relative_tail >= 1.0) return 0; /* A bounded parallel read phase completes before splatting. Its serial cache @@ -1043,7 +1050,8 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh, { const CatalogSplatStats stats = splat_catalog_triangles( mesh, catalog, hdr, width, height, exposure, psf, psf_cache, - max_magnification, max_cache_psf_flux, 0, mesh->triangle_count); + max_magnification, max_cache_psf_flux, psf_relative_tail, + 0, mesh->triangle_count); copy_psf_splat_stats(psf_stats, stats); return stats.images; } @@ -1059,7 +1067,8 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh, { const CatalogSplatStats stats = splat_catalog_triangles( mesh, catalog, hdr, width, height, exposure, psf, psf_cache, - max_magnification, max_cache_psf_flux, 0, mesh->triangle_count); + max_magnification, max_cache_psf_flux, psf_relative_tail, + 0, mesh->triangle_count); copy_psf_splat_stats(psf_stats, stats); return stats.images; } @@ -1069,7 +1078,8 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh, { const CatalogSplatStats stats = splat_catalog_triangles( mesh, catalog, hdr, width, height, exposure, psf, psf_cache, - max_magnification, max_cache_psf_flux, 0, mesh->triangle_count); + max_magnification, max_cache_psf_flux, psf_relative_tail, + 0, mesh->triangle_count); copy_psf_splat_stats(psf_stats, stats); return stats.images; } @@ -1084,8 +1094,9 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh, free(private_hdr[--allocated]); free(private_hdr); const CatalogSplatStats stats = splat_catalog_triangles( - mesh, catalog, hdr, width, height, exposure, psf, psf_cache, - max_magnification, max_cache_psf_flux, 0, mesh->triangle_count); + mesh, catalog, hdr, width, height, exposure, psf, psf_cache, + max_magnification, max_cache_psf_flux, psf_relative_tail, + 0, mesh->triangle_count); copy_psf_splat_stats(psf_stats, stats); return stats.images; } @@ -1114,7 +1125,8 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh, for (size_t triangle = 0; triangle < mesh->triangle_count; ++triangle) { const CatalogSplatStats stats = splat_catalog_triangles( mesh, catalog, private_hdr[worker], width, height, exposure, psf, - psf_cache, max_magnification, max_cache_psf_flux, triangle, triangle + 1); + psf_cache, max_magnification, max_cache_psf_flux, psf_relative_tail, + triangle, triangle + 1); images += stats.images; direct_fallbacks += stats.direct_fallbacks; cached_wing_clipped += stats.cached_wing_clipped; @@ -1145,7 +1157,8 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh, for (size_t triangle = 0; triangle < mesh->triangle_count; ++triangle) { const CatalogSplatStats stats = splat_catalog_triangles( mesh, catalog, private_hdr[worker], width, height, exposure, psf, - psf_cache, max_magnification, max_cache_psf_flux, triangle, triangle + 1); + psf_cache, max_magnification, max_cache_psf_flux, psf_relative_tail, + triangle, triangle + 1); images += stats.images; direct_fallbacks += stats.direct_fallbacks; cached_wing_clipped += stats.cached_wing_clipped; diff --git a/src/frame.h b/src/frame.h index fd825e2..5b59cd8 100644 --- a/src/frame.h +++ b/src/frame.h @@ -126,6 +126,7 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh, const PsfKernelCache *psf_cache, double max_magnification, double max_cache_psf_flux, + double psf_relative_tail, int limit_workers_by_memory, int catalog_load_workers, CatalogPrefetchStats *prefetch_stats, diff --git a/src/main.c b/src/main.c index 16f5000..0fb9bea 100644 --- a/src/main.c +++ b/src/main.c @@ -24,6 +24,7 @@ typedef struct { double horizontal_fov_deg, look_ra_deg, look_dec_deg, exposure; double max_magnification; double max_cache_psf_flux; + double psf_relative_tail; double observer_radius; double observer_inward_speed; PointSpreadFunction psf; @@ -186,6 +187,7 @@ static int parse_args(int argc, char **argv, Settings *s, .exposure = 1e-3, .max_magnification = INFINITY, .max_cache_psf_flux = 1.0, + .psf_relative_tail = 1e-8, .observer_radius = 30.0, .psf = {2.7, 4.5}, .catalog_path = "assets/sky_grid_5deg.csv", @@ -255,6 +257,9 @@ static int parse_args(int argc, char **argv, Settings *s, !parse_finite_positive(argv[++i], &s->max_magnification)) { } else if (!strcmp(argv[i], "--max-cache-psf-flux") && i + 1 < argc && !parse_at_least_one(argv[++i], &s->max_cache_psf_flux)) { + } else if (!strcmp(argv[i], "--psf-relative-tail") && i + 1 < argc && + !parse_finite_positive(argv[++i], &s->psf_relative_tail) && + s->psf_relative_tail < 1.0) { } else if (!strcmp(argv[i], "--psf-direct")) { s->psf_direct = 1; } else if (!strcmp(argv[i], "--verbose")) { @@ -463,6 +468,7 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog, size_t images = frame_splat_catalog( &mesh, catalog, hdr, s->width, s->height, s->exposure, &s->psf, &s->psf_cache, s->max_magnification, s->max_cache_psf_flux, + s->psf_relative_tail, spacetime_limits_render_workers_by_memory(spacetime), s->catalog_load_workers, &prefetch, &psf_stats, &(FrameSplatProgress){report_splat_progress, @@ -641,6 +647,7 @@ static int render_movie(const Settings *s, StarCatalog *catalog, const size_t images = frame_splat_catalog( &movie.frames[i].mesh, catalog, hdr, s->width, s->height, s->exposure, &s->psf, &s->psf_cache, s->max_magnification, s->max_cache_psf_flux, + s->psf_relative_tail, spacetime_limits_render_workers_by_memory(spacetime), s->catalog_load_workers, &prefetch, &psf_stats, &(FrameSplatProgress){report_splat_progress, @@ -686,6 +693,7 @@ int main(int argc, char **argv) { "[--exposure E] [--observer-radius R] [--observer-inward-speed V] " "[--psf-fwhm-pixels N] [--psf-moffat-beta N] " "[--max-magnification M] [--max-cache-psf-flux F] " + "[--psf-relative-tail R] " "[--psf-direct] [--verbose] " #ifdef ENABLE_HDR_OUTPUT "[--hdr-output] " @@ -749,7 +757,8 @@ int main(int argc, char **argv) { catalog_destroy(&catalog); return 1; } - if (!settings.psf_direct && psf_kernel_cache_init(&settings.psf_cache, &settings.psf)) + if (!settings.psf_direct && psf_kernel_cache_init(&settings.psf_cache, &settings.psf, + settings.psf_relative_tail)) fputs("PSF cache construction failed; using direct evaluator.\n", stderr); psf_kernel_cache_report_ready(&settings.psf_cache, stderr); int result = settings.frames_dir != NULL diff --git a/src/optics.c b/src/optics.c index 8486936..be17ec7 100644 --- a/src/optics.c +++ b/src/optics.c @@ -92,7 +92,6 @@ enum { * relative flux cut. The historical 1e-8 relative tail remains a floor for * ordinary images; brighter images grow their support or use the reference * fallback rather than acquiring a visible clipped wing. */ -static const double psf_relative_tail_fraction = 1e-8; static const double psf_tail_absolute_hdr_budget = 1e-6; static const double psf_boundary_hdr_budget = 1e-7; static const double pi = 3.14159265358979323846; @@ -125,10 +124,14 @@ static double moffat_alpha(const PointSpreadFunction *psf) } static double moffat_support_radius(double alpha, double beta, - double scaled_flux) + double scaled_flux, + double relative_tail_fraction) { + if (!isfinite(relative_tail_fraction) || relative_tail_fraction <= 0.0 || + relative_tail_fraction >= 1.0) + return NAN; const double normalization = scaled_flux * (beta - 1.0) / (pi * alpha * alpha); - const double relative_tail = fmin(psf_relative_tail_fraction, + const double relative_tail = fmin(relative_tail_fraction, psf_tail_absolute_hdr_budget / scaled_flux); const double tail_radius = alpha * sqrt(pow(relative_tail, 1.0 / (1.0 - beta)) - 1.0); @@ -190,13 +193,17 @@ void psf_kernel_cache_destroy(PsfKernelCache *cache) *cache = (PsfKernelCache){0}; } -int psf_kernel_cache_init(PsfKernelCache *cache, const PointSpreadFunction *psf) +int psf_kernel_cache_init(PsfKernelCache *cache, const PointSpreadFunction *psf, + double relative_tail_fraction) { - if (cache == NULL || !valid_psf(psf)) + if (cache == NULL || !valid_psf(psf) || + !isfinite(relative_tail_fraction) || relative_tail_fraction <= 0.0 || + relative_tail_fraction >= 1.0) return -1; psf_kernel_cache_destroy(cache); const double alpha = moffat_alpha(psf); - const double requested_radius = moffat_support_radius(alpha, psf->moffat_beta, 1.0); + const double requested_radius = moffat_support_radius( + alpha, psf->moffat_beta, 1.0, relative_tail_fraction); if (!isfinite(requested_radius) || requested_radius <= 0.0) return -1; /* The ordinary cache must cover the support implied by this invocation's @@ -221,6 +228,7 @@ int psf_kernel_cache_init(PsfKernelCache *cache, const PointSpreadFunction *psf) PsfKernelCache building = {.fwhm_pixels = psf->fwhm_pixels, .moffat_beta = psf->moffat_beta, .alpha_pixels = alpha, + .relative_tail_fraction = relative_tail_fraction, .max_radius_pixels = radius, .weights = weights, .phase_resolution = PSF_PHASE_RESOLUTION, @@ -274,9 +282,10 @@ void psf_kernel_cache_report_ready(const PsfKernelCache *cache, FILE *stream) { fputs("PSF cache: disabled; using the direct evaluator.\n", stream); return; } - fprintf(stream, "PSF cache ready: 64x64 phases, radius %.0f px, tail abs %.0e, " - "boundary %.0e, build %.3f s\n", - cache->max_radius_pixels, psf_tail_absolute_hdr_budget, + fprintf(stream, "PSF cache ready: 64x64 phases, radius %.0f px, relative tail %.0e, " + "tail abs %.0e, boundary %.0e, build %.3f s\n", + cache->max_radius_pixels, cache->relative_tail_fraction, + psf_tail_absolute_hdr_budget, psf_boundary_hdr_budget, cache->build_seconds); } @@ -300,14 +309,16 @@ void psf_kernel_cache_report(const PsfKernelCache *cache, void splat_moffat_direct(double *hdr, int width, int height, double x, double y, LinearRgb color, double flux, - const PointSpreadFunction *psf) + const PointSpreadFunction *psf, + double relative_tail_fraction) { if (hdr == NULL || width <= 0 || height <= 0 || flux <= 0.0 || !valid_psf(psf)) return; const double alpha = moffat_alpha(psf); if (!isfinite(flux)) return; - const double support_radius = moffat_support_radius(alpha, psf->moffat_beta, flux); + const double support_radius = moffat_support_radius( + alpha, psf->moffat_beta, flux, relative_tail_fraction); const int min_x = fmax(0.0, floor(x - support_radius)); const int max_x = fmin((double)width - 1.0, ceil(x + support_radius)); const int min_y = fmax(0.0, floor(y - support_radius)); @@ -331,17 +342,21 @@ int splat_moffat_cached(double *hdr, int width, int height, double x, double y, LinearRgb color, double flux, const PointSpreadFunction *psf, const PsfKernelCache *cache, - double max_cache_psf_flux) + double max_cache_psf_flux, + double relative_tail_fraction) { if (hdr == NULL || width <= 0 || height <= 0 || flux <= 0.0 || !valid_psf(psf)) return 1; const double alpha = moffat_alpha(psf); - const double support_radius = moffat_support_radius(alpha, psf->moffat_beta, - flux); + const double support_radius = moffat_support_radius( + alpha, psf->moffat_beta, flux, relative_tail_fraction); if (cache == NULL || !cache->ready || cache->fwhm_pixels != psf->fwhm_pixels || - cache->moffat_beta != psf->moffat_beta || !isfinite(support_radius) || + cache->moffat_beta != psf->moffat_beta || + cache->relative_tail_fraction != relative_tail_fraction || + !isfinite(support_radius) || (support_radius > cache->max_radius_pixels && flux > max_cache_psf_flux)) { - splat_moffat_direct(hdr, width, height, x, y, color, flux, psf); + splat_moffat_direct(hdr, width, height, x, y, color, flux, psf, + relative_tail_fraction); return 1; } const double base_x = floor(x), base_y = floor(y); @@ -376,9 +391,11 @@ int splat_moffat_cached(double *hdr, int width, int height, double x, double y, void splat_moffat(double *hdr, int width, int height, double x, double y, LinearRgb color, double flux, - const PointSpreadFunction *psf) + const PointSpreadFunction *psf, + double relative_tail_fraction) { - splat_moffat_direct(hdr, width, height, x, y, color, flux, psf); + splat_moffat_direct(hdr, width, height, x, y, color, flux, psf, + relative_tail_fraction); } static unsigned char tonemap_channel(double hdr_value) diff --git a/src/optics.h b/src/optics.h index 69b4ce9..0c98f58 100644 --- a/src/optics.h +++ b/src/optics.h @@ -10,10 +10,11 @@ typedef struct { double moffat_beta; } PointSpreadFunction; -/* One immutable process-wide kernel for the one PSF parameter pair accepted - * by the current renderer. Its storage remains private to optics.c. */ +/* One immutable process-wide kernel for the one PSF parameter/tail-policy pair + * accepted by the current renderer. Its storage remains private to optics.c. */ typedef struct { double fwhm_pixels, moffat_beta, alpha_pixels; + double relative_tail_fraction; double max_radius_pixels, build_seconds; float *weights; int phase_resolution, radius_pixels; @@ -34,7 +35,8 @@ typedef struct { * (W m^-2 sr^-1), before catalog amplitude and display exposure. */ LinearRgb blackbody_to_linear_rgb(double temperature_K); int psf_kernel_cache_init(PsfKernelCache *cache, - const PointSpreadFunction *psf); + const PointSpreadFunction *psf, + double relative_tail_fraction); void psf_kernel_cache_destroy(PsfKernelCache *cache); void psf_kernel_cache_report_ready(const PsfKernelCache *cache, FILE *stream); void psf_kernel_cache_report(const PsfKernelCache *cache, @@ -43,10 +45,12 @@ void psf_kernel_cache_report(const PsfKernelCache *cache, * budgets as the cache-aware renderer. */ void splat_moffat_direct(double *hdr, int width, int height, double x, double y, LinearRgb color, double flux, - const PointSpreadFunction *psf); + const PointSpreadFunction *psf, + double relative_tail_fraction); void splat_moffat(double *hdr, int width, int height, double x, double y, LinearRgb color, double flux, - const PointSpreadFunction *psf); + const PointSpreadFunction *psf, + double relative_tail_fraction); /* Returns 0 for a complete cached splat, 1 for the direct reference fallback, * and 2 when the cached core was used with its outer wing intentionally * clipped. max_cache_psf_flux == 1 preserves the historical behavior. */ @@ -54,7 +58,8 @@ int splat_moffat_cached(double *hdr, int width, int height, double x, double y, LinearRgb color, double flux, const PointSpreadFunction *psf, const PsfKernelCache *cache, - double max_cache_psf_flux); + double max_cache_psf_flux, + double relative_tail_fraction); /* Writes PNG when built with libpng; non-libpng builds use PPM fallback. */ int write_tonemapped_image(const char *path, const double *hdr, int width, int height); diff --git a/tests/test_frame.c b/tests/test_frame.c index 51e95df..2d6f638 100644 --- a/tests/test_frame.c +++ b/tests/test_frame.c @@ -60,6 +60,7 @@ static int mesh_has_same_winding_shared_edge(const FrameLensMesh *mesh) { 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}; @@ -77,7 +78,8 @@ int main(void) { goto done; const size_t images = frame_splat_catalog(&mesh, &catalog, hdr, width, height, test_exposure, - &psf, NULL, INFINITY, 1.0, 0, 1, NULL, NULL, NULL); + &psf, NULL, INFINITY, 1.0, psf_relative_tail, + 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; @@ -95,11 +97,11 @@ int main(void) { 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, 0, 1, NULL, NULL, NULL); + INFINITY, 1.0, psf_relative_tail, 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, 0, 1, NULL, NULL, NULL); + INFINITY, 1.0, psf_relative_tail, 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]) > @@ -126,7 +128,7 @@ int main(void) { * 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); + (LinearRgb){1.0, 1.0, 1.0}, 1.0, &psf, psf_relative_tail); double moffat_flux = 0.0; for (int pixel = 0; pixel < width * height; ++pixel) moffat_flux += hdr[3 * pixel]; @@ -142,27 +144,37 @@ int main(void) { 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_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); 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) != 0) { + 1.0, &psf, &cache, 1.0, psf_relative_tail) != 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); + (LinearRgb){1.0, 1.0, 1.0}, 1.0, &psf, psf_relative_tail); double peak = 0.0, max_error = 0.0; for (int value = 0; value < width * height * 3; ++value) { peak = fmax(peak, reference_hdr[value]); @@ -177,7 +189,8 @@ int main(void) { /* 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) != 1) { + (LinearRgb){1.0, 1.0, 1.0}, 1000.0, &psf, &cache, 1.0, + psf_relative_tail) != 1) { fputs("bright PSF direct-fallback regression failed\n", stderr); free(cached_hdr); free(reference_hdr); psf_kernel_cache_destroy(&cache); goto done; @@ -186,13 +199,13 @@ int main(void) { 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) != 2) { + 1000.0, psf_relative_tail) != 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); + (LinearRgb){1.0, 1.0, 1.0}, 1000.0, &psf, psf_relative_tail); const size_t center = 3 * (50 * width + 50); if (cached_hdr[center] <= 0.0 || fabs(cached_hdr[center] - reference_hdr[center]) > @@ -223,7 +236,8 @@ int main(void) { 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, 0, 1, NULL, + test_exposure, &psf, NULL, INFINITY, 1.0, + psf_relative_tail, 0, 1, NULL, NULL, NULL) != 1) { fputs("fine source-triangle containment regression failed\n", stderr); frame_lens_mesh_destroy(&fine_mesh);