Files
GR-raytracing/tests/benchmark_sensor_bloom.c
T
wyj 9cd933d1f8 Feat: Add optional three-channel sensor bloom model
Add an opt-in, post-processing limited-response model applied to the
finished linear HDR before tone mapping. Each RGB channel is processed
independently and isotropically: overflow above E spreads to the eight
neighbours with a fixed 9-point stencil, while the rest is absorbed or
lost at the image boundary. The synchronous ping-pong update uses a
monotonic bounding box and a row-parallel, deterministic reduction; the
conservative round bound reserves fp guard rounds inside a 4096 hard
limit and fails before touching HDR when exceeded.

Expose --sensor-bloom-limit E and --sensor-bloom-transfer e (both
required together, default disabled), validate them before expensive
initialization, and route every output path through the same hook in
write_frame_outputs: raw FITS first, bloom, tone-mapped PNG/PPM, then the
mesh overlay. The raw --hdr-output FITS therefore stays pre-bloom.

Add a standalone unit test (stencil, boundary loss, cascade reference,
symmetry, thread determinism, convergence limits, validation, allocation
failure), CLI integration and regression coverage, an isolated
sensor-bloom-bench target, and document the model in the design, usage,
README, and build docs.
2026-09-27 04:39:38 -04:00

122 lines
4.0 KiB
C

/* Isolated benchmark for the sensor-bloom model in src/sensor_bloom.c.
*
* This performs no ray tracing, catalog lookup, or PSF splatting; it fills a
* synthetic HDR framebuffer and times the post-processing model alone. The
* default 512x288 size keeps a full sweep bounded, and the optional size
* arguments are clamped so the benchmark cannot be turned into a production
* render. It is a non-default target and is not part of `make test`. */
#include "sensor_bloom.h"
#include <math.h>
#include <omp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DEFAULT_WIDTH 512
#define DEFAULT_HEIGHT 288
#define MAX_DIMENSION 1024
#define RESPONSE_LIMIT 1.0
static uint32_t next_random(uint32_t *state) {
*state = *state * 1664525u + 1013904223u;
return *state;
}
static uint64_t checksum(const double *hdr, size_t count) {
const unsigned char *bytes = (const unsigned char *)hdr;
uint64_t hash = 1469598103934665603ull;
for (size_t i = 0; i < count * sizeof(double); ++i) {
hash ^= bytes[i];
hash *= 1099511628211ull;
}
return hash;
}
static void fill_central(double *hdr, int width, int height) {
const size_t index = ((size_t)(height / 2) * width + width / 2) * 3;
hdr[index + 0] = 20.0;
hdr[index + 1] = 20.0;
hdr[index + 2] = 20.0;
}
static void fill_scattered(double *hdr, int width, int height) {
int placed = 0;
for (int y = 0; y < height && placed < 64; y += height > 8 ? height / 8 : 1) {
for (int x = 0; x < width && placed < 64; x += width > 8 ? width / 8 : 1) {
const size_t index = ((size_t)y * width + x) * 3;
hdr[index + 0] = 20.0;
hdr[index + 1] = 16.0;
hdr[index + 2] = 12.0;
++placed;
}
}
}
static void fill_random_fraction(double *hdr, int width, int height) {
const size_t count = (size_t)width * height * 3;
uint32_t state = 20260927u;
for (size_t i = 0; i < count; ++i)
hdr[i] = 1.5 + (double)(next_random(&state) % 2500u) / 1000.0;
for (size_t i = 0; i < count; ++i)
if (next_random(&state) % 100u >= 5u)
hdr[i] = 0.0;
}
typedef void (*FillFunction)(double *, int, int);
static void run_case(const char *name, FillFunction fill, int width, int height,
double transfer) {
const size_t count = (size_t)width * height * 3;
double *hdr = calloc(count, sizeof *hdr);
if (hdr == NULL) {
fprintf(stderr, "allocation failed for %s\n", name);
exit(EXIT_FAILURE);
}
fill(hdr, width, height);
const SensorBloomSettings settings = {RESPONSE_LIMIT, transfer};
SensorBloomStats stats;
if (sensor_bloom_apply(hdr, width, height, &settings, &stats)) {
fprintf(stderr, "%s e=%.2f failed\n", name, transfer);
free(hdr);
exit(EXIT_FAILURE);
}
/* Report the model's own guard-inclusive conservative cap, not a duplicate
* of the bound formula. */
printf("%-10s e=%.2f size=%dx%d saturated=%zu predicted=%zu actual=%zu "
"bbox=%.4f elapsed=%.6fs checksum=%016llx\n",
name, transfer, width, height, stats.initially_saturated_channels,
stats.predicted_iterations, stats.iterations, stats.bbox_coverage,
stats.elapsed_seconds, (unsigned long long)checksum(hdr, count));
free(hdr);
}
int main(int argc, char **argv) {
int width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT;
if (argc >= 2)
width = atoi(argv[1]);
if (argc >= 3)
height = atoi(argv[2]);
if (width < 1)
width = 1;
if (height < 1)
height = 1;
if (width > MAX_DIMENSION)
width = MAX_DIMENSION;
if (height > MAX_DIMENSION)
height = MAX_DIMENSION;
const int workers = omp_get_max_threads();
printf("sensor-bloom benchmark: size=%dx%d threads=%d limit=%.3f\n",
width, height, workers, RESPONSE_LIMIT);
const double transfers[] = {0.0, 0.5, 0.9};
for (size_t e = 0; e < sizeof transfers / sizeof transfers[0]; ++e) {
run_case("central", fill_central, width, height, transfers[e]);
run_case("scattered", fill_scattered, width, height, transfers[e]);
run_case("random5pct", fill_random_fraction, width, height, transfers[e]);
}
return EXIT_SUCCESS;
}