Files
GR-raytracing/src/fast_psf_fftw.c
T
wyj 04611e3e5a Feat: Overlap movie output and cut per-frame fast-mode work
- Gather the union of every movie frame's all-sky tiles once and read them in bounded batches, replacing the per-frame prefetch scan and log.
- Fuse the fast supersampled-buffer clear into the FFTW pack pass so a resolved frame starts clean with no serial memset.
- Split parallel HDR->RGB8 tone mapping from PNG encoding.
- Add a bounded single-producer/single-writer movie output queue used by both observer movies and multi-frame imported lens maps, with writer timing and error propagation.
- Add --png-compression-level and --movie-output-workers shared|reserve-one.
- Add --fast-fftw-plan estimate|measure|wisdom|wisdom-update with strict wisdom identity sidecars.
- Add staged movie timing, regression tests, and docs.
2026-10-03 19:32:35 -04:00

675 lines
24 KiB
C

#include "fast_psf_fftw.h"
#include <fftw3.h>
#include <errno.h>
#include <limits.h>
#include <math.h>
#include <omp.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Process-global FFTW threading lifecycle. Plans are created only from the
* serial control thread, and global cleanup happens only after the last live
* state is gone. This keeps the individual states from racing on FFTW's
* process-wide planner state. */
static int g_threading_initialized = 0;
static int g_live_states = 0;
static FastPsfFftwPlanMode g_plan_mode = FAST_PSF_FFTW_PLAN_ESTIMATE;
static const char *g_wisdom_path = NULL;
static int g_wisdom_imported = 0;
static int g_wisdom_available = 0;
void fast_psf_fftw_set_plan_mode(int measure)
{
g_plan_mode = measure ? FAST_PSF_FFTW_PLAN_MEASURE
: FAST_PSF_FFTW_PLAN_ESTIMATE;
g_wisdom_path = NULL;
g_wisdom_imported = 0;
g_wisdom_available = 0;
}
int fast_psf_fftw_configure(FastPsfFftwPlanMode mode, const char *wisdom_path)
{
if ((mode == FAST_PSF_FFTW_PLAN_WISDOM ||
mode == FAST_PSF_FFTW_PLAN_WISDOM_UPDATE) &&
(wisdom_path == NULL || wisdom_path[0] == '\0')) {
fprintf(stderr,
"Fast FFTW: --fast-fftw-plan wisdom modes require "
"--fast-fftw-wisdom FILE.\n");
return -1;
}
g_plan_mode = mode;
g_wisdom_path = wisdom_path;
g_wisdom_imported = 0;
g_wisdom_available = 0;
return 0;
}
static const char *plan_mode_name(void)
{
switch (g_plan_mode) {
case FAST_PSF_FFTW_PLAN_MEASURE:
return "measure";
case FAST_PSF_FFTW_PLAN_WISDOM:
return "wisdom";
case FAST_PSF_FFTW_PLAN_WISDOM_UPDATE:
return "wisdom-update";
case FAST_PSF_FFTW_PLAN_ESTIMATE:
default:
return "estimate";
}
}
static unsigned plan_flags(void)
{
switch (g_plan_mode) {
case FAST_PSF_FFTW_PLAN_MEASURE:
case FAST_PSF_FFTW_PLAN_WISDOM_UPDATE:
return FFTW_MEASURE;
case FAST_PSF_FFTW_PLAN_WISDOM:
return FFTW_WISDOM_ONLY;
case FAST_PSF_FFTW_PLAN_ESTIMATE:
default:
return FFTW_ESTIMATE;
}
}
/* Wisdom identity sidecar. FFTW's own wisdom already encodes the transform
* size and precision, but not this build's supersampling or worker count, so a
* validated <wisdom>.meta accompanies every exported file. */
static int wisdom_meta_path(char *out, size_t cap, const char *wisdom_path)
{
const int written = snprintf(out, cap, "%s.meta", wisdom_path);
return written < 0 || (size_t)written >= cap ? -1 : 0;
}
struct FastPsfFftwState {
int fft_width, fft_height;
int ss_width, ss_height;
int final_width, final_height;
int supersample;
int kernel_radius;
int workers;
int registered;
int plan_measure;
size_t scratch_bytes;
double setup_seconds;
double kernel_seconds;
double fft_scale;
double box_scale;
double *real_rgb; /* 3 planes of fft_height * fft_width */
fftw_complex *freq_rgb; /* 3 planes of fft_height * (fft_width/2 + 1) */
fftw_complex *kernel_freq; /* one plane of fft_height * (fft_width/2 + 1) */
fftw_plan forward_plan;
fftw_plan inverse_plan;
};
/* Required identity fields. Every one must appear exactly once and match; a
* missing or bad field is a miss, not an implicit default. */
enum {
WISDOM_META_VERSION = 1u << 0,
WISDOM_META_PRECISION = 1u << 1,
WISDOM_META_SS_WIDTH = 1u << 2,
WISDOM_META_SS_HEIGHT = 1u << 3,
WISDOM_META_FFT_WIDTH = 1u << 4,
WISDOM_META_FFT_HEIGHT = 1u << 5,
WISDOM_META_SUPERSAMPLE = 1u << 6,
WISDOM_META_RADIUS = 1u << 7,
WISDOM_META_WORKERS = 1u << 8,
WISDOM_META_ALL = (1u << 9) - 1
};
static int strict_long(const char *text, long *out)
{
char *end;
errno = 0;
const long value = strtol(text, &end, 10);
if (errno != 0 || end == text || *end != '\0')
return -1;
*out = value;
return 0;
}
static int wisdom_meta_matches(const FastPsfFftwState *state)
{
if (g_wisdom_path == NULL)
return 0;
char path[PATH_MAX];
if (wisdom_meta_path(path, sizeof path, g_wisdom_path))
return 0;
FILE *file = fopen(path, "r");
if (file == NULL)
return 0;
char line[256];
unsigned seen = 0;
int ok = 1;
while (ok && fgets(line, sizeof line, file) != NULL) {
char key[64], value[192];
if (sscanf(line, "%63[^=]=%191s", key, value) != 2)
continue;
long expected = 0;
unsigned bit = 0;
int numeric = 0;
if (!strcmp(key, "fftw_version")) {
ok = strcmp(value, fftw_version) == 0;
bit = WISDOM_META_VERSION;
} else if (!strcmp(key, "precision")) {
ok = strcmp(value, "double") == 0;
bit = WISDOM_META_PRECISION;
} else if (!strcmp(key, "ss_width")) {
expected = state->ss_width; numeric = 1; bit = WISDOM_META_SS_WIDTH;
} else if (!strcmp(key, "ss_height")) {
expected = state->ss_height; numeric = 1; bit = WISDOM_META_SS_HEIGHT;
} else if (!strcmp(key, "fft_width")) {
expected = state->fft_width; numeric = 1; bit = WISDOM_META_FFT_WIDTH;
} else if (!strcmp(key, "fft_height")) {
expected = state->fft_height; numeric = 1; bit = WISDOM_META_FFT_HEIGHT;
} else if (!strcmp(key, "supersample")) {
expected = state->supersample; numeric = 1; bit = WISDOM_META_SUPERSAMPLE;
} else if (!strcmp(key, "kernel_radius")) {
expected = state->kernel_radius; numeric = 1; bit = WISDOM_META_RADIUS;
} else if (!strcmp(key, "workers")) {
expected = state->workers; numeric = 1; bit = WISDOM_META_WORKERS;
} else {
continue;
}
if (ok && numeric) {
long parsed = 0;
ok = strict_long(value, &parsed) == 0 && parsed == expected;
}
if (ok && (seen & bit) != 0)
ok = 0; /* duplicate field: reject rather than accept twice */
if (ok)
seen |= bit;
}
fclose(file);
return ok && (seen & WISDOM_META_ALL) == WISDOM_META_ALL;
}
static int wisdom_write_meta(const FastPsfFftwState *state, const char *path)
{
FILE *file = fopen(path, "w");
if (file == NULL)
return -1;
const int written = fprintf(
file,
"fftw_version=%s\nprecision=double\nss_width=%d\nss_height=%d\n"
"fft_width=%d\nfft_height=%d\nsupersample=%d\nkernel_radius=%d\n"
"workers=%d\n",
fftw_version, state->ss_width, state->ss_height, state->fft_width,
state->fft_height, state->supersample, state->kernel_radius,
state->workers);
const int closed = fclose(file);
return written < 0 || closed != 0 ? -1 : 0;
}
static int wisdom_import_and_verify(const FastPsfFftwState *state)
{
if (!g_wisdom_imported) {
g_wisdom_imported = 1;
if (g_wisdom_path != NULL &&
fftw_import_wisdom_from_filename(g_wisdom_path) != 0)
g_wisdom_available = 1;
}
if (!g_wisdom_available) {
fprintf(stderr,
"Fast FFTW: --fast-fftw-plan wisdom could not import wisdom from "
"%s.\n",
g_wisdom_path != NULL ? g_wisdom_path : "(unset)");
return 0;
}
if (!wisdom_meta_matches(state)) {
fprintf(stderr,
"Fast FFTW: wisdom in %s does not match this render "
"(size, supersample, worker count, version, or precision).\n",
g_wisdom_path);
return 0;
}
return 1;
}
/* Writes the wisdom and its identity sidecar next to the requested path.
* Each file is written to a unique temporary and renamed into place, so each
* rename is individually atomic. The wisdom file and its sidecar are two
* separate files and are not transactionally updated: the sidecar is committed
* last, so a loader that sees a wisdom without a matching sidecar treats it as
* a miss. Any failure returns -1 and makes wisdom-update initialization fail. */
static int wisdom_export(const FastPsfFftwState *state)
{
if (g_plan_mode != FAST_PSF_FFTW_PLAN_WISDOM_UPDATE || g_wisdom_path == NULL)
return 0;
char meta_path[PATH_MAX];
char wisdom_temp[PATH_MAX];
char meta_temp[PATH_MAX];
if (wisdom_meta_path(meta_path, sizeof meta_path, g_wisdom_path))
return -1;
const long pid = (long)getpid();
if (snprintf(wisdom_temp, sizeof wisdom_temp, "%s.tmp.%ld", g_wisdom_path,
pid) < 0 ||
snprintf(meta_temp, sizeof meta_temp, "%s.tmp.%ld", meta_path, pid) < 0)
return -1;
if (wisdom_write_meta(state, meta_temp) != 0) {
fprintf(stderr, "Fast FFTW: failed to write wisdom metadata %s.\n",
meta_temp);
unlink(meta_temp);
return -1;
}
if (fftw_export_wisdom_to_filename(wisdom_temp) == 0) {
fputs("Fast FFTW: failed to export FFTW wisdom.\n", stderr);
unlink(wisdom_temp);
unlink(meta_temp);
return -1;
}
if (rename(wisdom_temp, g_wisdom_path) != 0) {
fprintf(stderr, "Fast FFTW: failed to commit wisdom to %s.\n",
g_wisdom_path);
unlink(wisdom_temp);
unlink(meta_temp);
return -1;
}
if (rename(meta_temp, meta_path) != 0) {
fprintf(stderr, "Fast FFTW: failed to commit wisdom metadata to %s.\n",
meta_path);
unlink(meta_temp);
return -1;
}
return 0;
}
static int checked_mul_size(size_t a, size_t b, size_t *out)
{
if (a != 0 && b > SIZE_MAX / a)
return -1;
*out = a * b;
return 0;
}
int fast_psf_fftw_next_smooth_size(size_t min_extent, size_t *out)
{
if (out == NULL || min_extent == 0)
return -1;
if (min_extent > (size_t)INT_MAX)
return -1;
static const unsigned factors[] = {2, 3, 5, 7};
for (size_t candidate = min_extent; candidate <= (size_t)INT_MAX;
++candidate) {
size_t remaining = candidate;
for (size_t i = 0; i < sizeof factors / sizeof factors[0]; ++i)
while (remaining % factors[i] == 0)
remaining /= factors[i];
if (remaining == 1) {
*out = candidate;
return 0;
}
}
return -1;
}
void fast_psf_fftw_destroy(FastPsfFftwState *state)
{
if (state == NULL)
return;
if (state->forward_plan != NULL)
fftw_destroy_plan(state->forward_plan);
if (state->inverse_plan != NULL)
fftw_destroy_plan(state->inverse_plan);
if (state->real_rgb != NULL)
fftw_free(state->real_rgb);
if (state->freq_rgb != NULL)
fftw_free(state->freq_rgb);
if (state->kernel_freq != NULL)
fftw_free(state->kernel_freq);
const int registered = state->registered;
free(state);
if (!registered)
return;
g_live_states--;
if (g_live_states <= 0) {
g_live_states = 0;
if (g_threading_initialized) {
fftw_cleanup_threads();
g_threading_initialized = 0;
}
}
}
FastPsfFftwState *fast_psf_fftw_create(int ss_width, int ss_height,
int final_width, int final_height,
int supersample, int kernel_radius,
const float *weights,
const int *row_span, int fft_workers,
double *setup_seconds,
double *kernel_seconds)
{
if (setup_seconds != NULL)
*setup_seconds = 0.0;
if (kernel_seconds != NULL)
*kernel_seconds = 0.0;
if (ss_width <= 0 || ss_height <= 0 || final_width <= 0 ||
final_height <= 0 || supersample < 1 || kernel_radius < 0 ||
weights == NULL || row_span == NULL || fft_workers < 1) {
fprintf(stderr,
"Fast FFTW init failed (arguments): ss=%dx%d final=%dx%d "
"supersample=%d kernel_radius=%d workers=%d\n",
ss_width, ss_height, final_width, final_height, supersample,
kernel_radius, fft_workers);
return NULL;
}
if ((size_t)final_width * (size_t)supersample != (size_t)ss_width ||
(size_t)final_height * (size_t)supersample != (size_t)ss_height) {
fprintf(stderr,
"Fast FFTW init failed (dimensions): ss=%dx%d is not "
"final=%dx%d times supersample=%d\n",
ss_width, ss_height, final_width, final_height, supersample);
return NULL;
}
const char *stage = "state allocation";
FastPsfFftwState *state = calloc(1, sizeof *state);
if (state == NULL) {
fprintf(stderr, "Fast FFTW init failed (state allocation): %zu bytes\n",
sizeof *state);
return NULL;
}
state->ss_width = ss_width;
state->ss_height = ss_height;
state->final_width = final_width;
state->final_height = final_height;
state->supersample = supersample;
state->kernel_radius = kernel_radius;
state->workers = fft_workers;
state->plan_measure =
g_plan_mode == FAST_PSF_FFTW_PLAN_MEASURE ||
g_plan_mode == FAST_PSF_FFTW_PLAN_WISDOM_UPDATE;
size_t real_bytes = 0, freq_bytes = 0, kernel_bytes = 0;
size_t fft_width = 0, fft_height = 0;
const size_t radius = (size_t)kernel_radius;
size_t width_min, height_min;
stage = "kernel extent";
if (checked_mul_size(radius, 2, &width_min) ||
checked_mul_size(radius, 2, &height_min))
goto fail;
if ((size_t)ss_width > SIZE_MAX - width_min ||
(size_t)ss_height > SIZE_MAX - height_min)
goto fail;
width_min += (size_t)ss_width;
height_min += (size_t)ss_height;
stage = "smooth FFT size";
if (fast_psf_fftw_next_smooth_size(width_min, &fft_width) ||
fast_psf_fftw_next_smooth_size(height_min, &fft_height))
goto fail;
state->fft_width = (int)fft_width;
state->fft_height = (int)fft_height;
const size_t complex_width = fft_width / 2 + 1;
size_t real_plane, real_count;
size_t freq_plane, freq_count;
stage = "allocation size";
if (checked_mul_size(fft_height, fft_width, &real_plane) ||
checked_mul_size(real_plane, 3, &real_count) ||
checked_mul_size(real_count, sizeof(double), &real_bytes) ||
checked_mul_size(fft_height, complex_width, &freq_plane) ||
checked_mul_size(freq_plane, 3, &freq_count) ||
checked_mul_size(freq_count, sizeof(fftw_complex), &freq_bytes) ||
checked_mul_size(freq_plane, sizeof(fftw_complex), &kernel_bytes) ||
real_bytes > SIZE_MAX - freq_bytes - kernel_bytes)
goto fail;
/* FFTW's plan_many interface takes idist/odist as int, so the per-plane
* element counts must fit before the cast below even when each axis is a
* valid int. */
stage = "plan_many stride";
if (real_plane > (size_t)INT_MAX || freq_plane > (size_t)INT_MAX)
goto fail;
state->scratch_bytes = real_bytes + freq_bytes + kernel_bytes;
stage = "FFTW threading initialization";
if (g_live_states == 0) {
if (!fftw_init_threads())
goto fail;
g_threading_initialized = 1;
}
g_live_states++;
state->registered = 1;
fftw_plan_with_nthreads(fft_workers);
if (g_plan_mode == FAST_PSF_FFTW_PLAN_WISDOM &&
!wisdom_import_and_verify(state))
goto fail;
const double plan_start = omp_get_wtime();
stage = "FFTW scratch allocation";
state->real_rgb = fftw_alloc_real(real_count);
state->freq_rgb = fftw_alloc_complex(freq_count);
state->kernel_freq = fftw_alloc_complex(freq_plane);
if (state->real_rgb == NULL || state->freq_rgb == NULL ||
state->kernel_freq == NULL)
goto fail;
int n[2] = {state->fft_height, state->fft_width};
int real_embed[2] = {state->fft_height, state->fft_width};
int complex_embed[2] = {state->fft_height, (int)complex_width};
const int real_dist = (int)real_plane;
const int complex_dist = (int)freq_plane;
stage = "FFTW plan creation";
state->forward_plan = fftw_plan_many_dft_r2c(
2, n, 3, state->real_rgb, real_embed, 1, real_dist, state->freq_rgb,
complex_embed, 1, complex_dist, plan_flags());
state->inverse_plan = fftw_plan_many_dft_c2r(
2, n, 3, state->freq_rgb, complex_embed, 1, complex_dist,
state->real_rgb, real_embed, 1, real_dist, plan_flags());
if (state->forward_plan == NULL || state->inverse_plan == NULL)
goto fail;
state->setup_seconds = omp_get_wtime() - plan_start;
if (setup_seconds != NULL)
*setup_seconds = state->setup_seconds;
/* One-time kernel transform. The kernel is stored unshifted at padded origin
* indices (dy + R, dx + R), retaining only the circular mask the spatial
* reference traverses; square corners and FFT padding stay zero. The plan is
* created before the kernel is written because FFTW_MEASURE overwrites its
* input during planning. */
stage = "kernel scratch allocation";
double *kernel_real = fftw_alloc_real(real_plane);
fftw_plan kernel_plan = NULL;
if (kernel_real == NULL)
goto fail;
const double kernel_start = omp_get_wtime();
stage = "kernel plan creation";
kernel_plan = fftw_plan_dft_r2c_2d(state->fft_height, state->fft_width,
kernel_real, state->kernel_freq,
plan_flags());
if (kernel_plan == NULL) {
fftw_free(kernel_real);
goto fail;
}
memset(kernel_real, 0, real_plane * sizeof *kernel_real);
const size_t side = 2 * radius + 1;
for (int dy = -kernel_radius; dy <= kernel_radius; ++dy) {
const int span = row_span[dy + kernel_radius];
for (int dx = -span; dx <= span; ++dx)
kernel_real[(size_t)(dy + kernel_radius) * fft_width +
(size_t)(dx + kernel_radius)] =
(double)weights[(size_t)(dy + kernel_radius) * side +
(size_t)(dx + kernel_radius)];
}
fftw_execute(kernel_plan);
state->kernel_seconds = omp_get_wtime() - kernel_start;
if (kernel_seconds != NULL)
*kernel_seconds = state->kernel_seconds;
fftw_destroy_plan(kernel_plan);
fftw_free(kernel_real);
state->fft_scale = 1.0 / ((double)fft_width * (double)fft_height);
state->box_scale = 1.0 / ((double)supersample * (double)supersample);
stage = "wisdom export";
if (wisdom_export(state) != 0)
goto fail;
return state;
fail:
fprintf(stderr,
"Fast FFTW init failed at '%s': ss=%dx%d, linear min=%zux%zu, "
"fft=%zux%zu, plan=%s, workers=%d, bytes real=%zu freq=%zu "
"kernel=%zu total=%zu\n",
stage, ss_width, ss_height,
(size_t)ss_width + 2 * radius,
(size_t)ss_height + 2 * radius, fft_width, fft_height,
plan_mode_name(), fft_workers,
real_bytes, freq_bytes, kernel_bytes,
real_bytes + freq_bytes + kernel_bytes);
fast_psf_fftw_destroy(state);
return NULL;
}
static int fast_psf_fftw_resolve_impl(FastPsfFftwState *state,
const double *interleaved_ss_rgb,
int clear_source,
double *interleaved_hdr_rgb,
FastPsfFftwFrameTiming *timing)
{
if (state == NULL || interleaved_ss_rgb == NULL ||
interleaved_hdr_rgb == NULL)
return -1;
FastPsfFftwFrameTiming local = {0};
const double total_start = omp_get_wtime();
const size_t plane = (size_t)state->fft_height * state->fft_width;
const size_t complex_width = (size_t)state->fft_width / 2 + 1;
const size_t complex_plane = (size_t)state->fft_height * complex_width;
const size_t real_count = 3 * plane;
const size_t freq_count = 3 * complex_plane;
/* The pack pass writes every FFT cell and, when clear_source is set, both
* consumes and zeroes its own source triplet. Each supersampled triplet is
* touched by exactly one worker, so zeroing needs no atomic and no second
* serial memset. Clearing is deliberately unconditional in the consuming
* variant: even a later failure must not leave stale deposits behind. */
double *source_mut = (double *)(uintptr_t)interleaved_ss_rgb;
double start = omp_get_wtime();
/* The padded border is never overwritten by packing, so it must be zeroed
* every frame regardless of the consuming variant. */
#pragma omp parallel for schedule(static) num_threads(state->workers) \
if (state->workers > 1)
for (size_t i = 0; i < real_count; ++i)
state->real_rgb[i] = 0.0;
#pragma omp parallel for schedule(static) num_threads(state->workers) \
if (state->workers > 1)
for (int y = 0; y < state->ss_height; ++y) {
for (int x = 0; x < state->ss_width; ++x) {
const size_t source =
3 * ((size_t)y * state->ss_width + (size_t)x);
const size_t target = (size_t)y * state->fft_width + (size_t)x;
state->real_rgb[target] = source_mut[source];
state->real_rgb[plane + target] = source_mut[source + 1];
state->real_rgb[2 * plane + target] = source_mut[source + 2];
if (clear_source) {
source_mut[source] = 0.0;
source_mut[source + 1] = 0.0;
source_mut[source + 2] = 0.0;
}
}
}
local.zero_pack_seconds = omp_get_wtime() - start;
start = omp_get_wtime();
fftw_execute(state->forward_plan);
local.forward_seconds = omp_get_wtime() - start;
start = omp_get_wtime();
#pragma omp parallel for schedule(static) num_threads(state->workers) \
if (state->workers > 1)
for (size_t i = 0; i < freq_count; ++i) {
const double ar = state->freq_rgb[i][0];
const double ai = state->freq_rgb[i][1];
const double br = state->kernel_freq[i % complex_plane][0];
const double bi = state->kernel_freq[i % complex_plane][1];
state->freq_rgb[i][0] = ar * br - ai * bi;
state->freq_rgb[i][1] = ar * bi + ai * br;
}
local.multiply_seconds = omp_get_wtime() - start;
start = omp_get_wtime();
fftw_execute(state->inverse_plan);
local.inverse_seconds = omp_get_wtime() - start;
start = omp_get_wtime();
const int radius = state->kernel_radius;
const int supersample = state->supersample;
const double scale = state->fft_scale * state->box_scale;
#pragma omp parallel for schedule(static) num_threads(state->workers) \
if (state->workers > 1)
for (int row = 0; row < state->final_height; ++row) {
for (int column = 0; column < state->final_width; ++column) {
double sum[3] = {0.0, 0.0, 0.0};
const int base_y = radius + supersample * row;
const int base_x = radius + supersample * column;
for (int j = 0; j < supersample; ++j)
for (int i = 0; i < supersample; ++i) {
const size_t offset =
(size_t)(base_y + j) * state->fft_width +
(size_t)(base_x + i);
sum[0] += state->real_rgb[offset];
sum[1] += state->real_rgb[plane + offset];
sum[2] += state->real_rgb[2 * plane + offset];
}
const size_t out =
3 * ((size_t)row * state->final_width + (size_t)column);
interleaved_hdr_rgb[out] += sum[0] * scale;
interleaved_hdr_rgb[out + 1] += sum[1] * scale;
interleaved_hdr_rgb[out + 2] += sum[2] * scale;
}
}
local.crop_downsample_seconds = omp_get_wtime() - start;
local.total_seconds = omp_get_wtime() - total_start;
if (timing != NULL)
*timing = local;
return 0;
}
int fast_psf_fftw_resolve(FastPsfFftwState *state,
const double *interleaved_ss_rgb,
double *interleaved_hdr_rgb,
FastPsfFftwFrameTiming *timing)
{
return fast_psf_fftw_resolve_impl(state, interleaved_ss_rgb, 0,
interleaved_hdr_rgb, timing);
}
int fast_psf_fftw_resolve_and_clear(FastPsfFftwState *state,
double *interleaved_ss_rgb,
double *interleaved_hdr_rgb,
FastPsfFftwFrameTiming *timing)
{
if (state == NULL || interleaved_ss_rgb == NULL)
return -1;
const int result = fast_psf_fftw_resolve_impl(state, interleaved_ss_rgb, 1,
interleaved_hdr_rgb, timing);
if (result != 0) {
/* The pack pass may not have run, so guarantee a clean source even on the
* error path rather than letting a later frame inherit stale deposits. */
const size_t count = (size_t)state->ss_width * state->ss_height * 3;
memset(interleaved_ss_rgb, 0, count * sizeof *interleaved_ss_rgb);
}
return result;
}
void fast_psf_fftw_report(const FastPsfFftwState *state, FILE *stream)
{
if (state == NULL || stream == NULL)
return;
fprintf(stream,
"Fast FFTW: linear min=%dx%d, fft=%dx%d, workers=%d, "
"plan=%s, plan=%.6f s, kernel_fft=%.6f s, scratch=%.1f MiB\n",
state->ss_height + 2 * state->kernel_radius,
state->ss_width + 2 * state->kernel_radius, state->fft_height,
state->fft_width, state->workers,
plan_mode_name(), state->setup_seconds,
state->kernel_seconds,
(double)state->scratch_bytes / (1024.0 * 1024.0));
}