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.
This commit is contained in:
1 parent
85fce1bdeb
commit
9cd933d1f8
12 files changed
+1280
-7
No files matched your search
@@ -0,0 +1,121 @@
|
||||
/* 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;
|
||||
}
|
||||
Reference in new issue
Block a user