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.
This commit is contained in:
1 parent
a510fec23e
commit
04611e3e5a
20 files changed
+2282
-212
No files matched your search
@@ -24,42 +24,83 @@ static int write_tile(const char *directory, int ra_index, int dec_index,
|
||||
return result ? -1 : 0;
|
||||
}
|
||||
|
||||
static int tile_state(const StarCatalog *catalog, int ra, int dec) {
|
||||
return catalog->tiles[(size_t)dec * CATALOG_ALL_SKY_RA_TILES + ra].state;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
char directory[] = "/tmp/catalog_prefetch_XXXXXX";
|
||||
char path[4096];
|
||||
StarCatalog catalog = {0};
|
||||
unsigned char requested[CATALOG_ALL_SKY_TILE_COUNT] = {0};
|
||||
CatalogPrefetchStats first = {0}, second = {0};
|
||||
StarCatalog batched = {0};
|
||||
int result = 1;
|
||||
if (mkdtemp(directory) == NULL || write_tile(directory, 0, 90, 0.0) ||
|
||||
write_tile(directory, 1, 90, 1.0) ||
|
||||
catalog_load_all_sky(&catalog, directory))
|
||||
goto done;
|
||||
requested[90 * CATALOG_ALL_SKY_RA_TILES] = 1;
|
||||
requested[90 * CATALOG_ALL_SKY_RA_TILES + 1] = 1;
|
||||
requested[90 * CATALOG_ALL_SKY_RA_TILES + 2] = 1; /* Missing on purpose. */
|
||||
if (catalog_prefetch_marked_tiles(&catalog, requested, 4, &first) ||
|
||||
first.requested_tiles != 3 || first.newly_loaded_tiles != 2 ||
|
||||
first.unavailable_tiles != 1 || first.newly_loaded_stars != 2 ||
|
||||
catalog.count != 2 ||
|
||||
catalog.tiles[90 * CATALOG_ALL_SKY_RA_TILES].state != 1 ||
|
||||
catalog.tiles[90 * CATALOG_ALL_SKY_RA_TILES + 1].state != 1 ||
|
||||
catalog.tiles[90 * CATALOG_ALL_SKY_RA_TILES + 2].state != -1) {
|
||||
fputs("parallel catalog prefetch regression failed\n", stderr);
|
||||
|
||||
/* Two overlapping sets: A = {0,1}, B = {1,2}; the union is {0,1,2}, and
|
||||
* tile 2 is intentionally missing from disk. */
|
||||
CatalogTileSet set_a;
|
||||
CatalogTileSet set_b;
|
||||
CatalogTileSet union_set;
|
||||
catalog_tile_set_clear(&set_a);
|
||||
catalog_tile_set_clear(&set_b);
|
||||
catalog_tile_set_clear(&union_set);
|
||||
set_a.requested[90 * CATALOG_ALL_SKY_RA_TILES] = 1;
|
||||
set_a.requested[90 * CATALOG_ALL_SKY_RA_TILES + 1] = 1;
|
||||
set_b.requested[90 * CATALOG_ALL_SKY_RA_TILES + 1] = 1;
|
||||
set_b.requested[90 * CATALOG_ALL_SKY_RA_TILES + 2] = 1;
|
||||
for (int ra = 0; ra < 3; ++ra)
|
||||
union_set.requested[90 * CATALOG_ALL_SKY_RA_TILES + ra] =
|
||||
set_a.requested[90 * CATALOG_ALL_SKY_RA_TILES + ra] ||
|
||||
set_b.requested[90 * CATALOG_ALL_SKY_RA_TILES + ra];
|
||||
if (catalog_tile_set_count(&set_a) != 2 ||
|
||||
catalog_tile_set_count(&set_b) != 2 ||
|
||||
catalog_tile_set_count(&union_set) != 3) {
|
||||
fputs("catalog tile-set union regression failed\n", stderr);
|
||||
goto done;
|
||||
}
|
||||
Star *first_tile = catalog.tiles[90 * CATALOG_ALL_SKY_RA_TILES].stars;
|
||||
if (catalog_prefetch_marked_tiles(&catalog, requested, 2, &second) ||
|
||||
|
||||
CatalogPrefetchStats first = {0};
|
||||
if (catalog_prefetch_tile_set(&catalog, &union_set, 4,
|
||||
CATALOG_PREFETCH_DEFAULT_BATCH_TILES, &first) ||
|
||||
first.requested_tiles != 3 || first.newly_loaded_tiles != 2 ||
|
||||
first.unavailable_tiles != 1 || first.newly_loaded_stars != 2 ||
|
||||
catalog.count != 2 || tile_state(&catalog, 0, 90) != 1 ||
|
||||
tile_state(&catalog, 1, 90) != 1 || tile_state(&catalog, 2, 90) != -1) {
|
||||
fputs("parallel catalog union prefetch regression failed\n", stderr);
|
||||
goto done;
|
||||
}
|
||||
Star *first_tile =
|
||||
catalog.tiles[90 * CATALOG_ALL_SKY_RA_TILES].stars;
|
||||
|
||||
/* Re-prefetching the same union must read nothing and must not re-mark the
|
||||
* unavailable tile. */
|
||||
CatalogPrefetchStats second = {0};
|
||||
if (catalog_prefetch_tile_set(&catalog, &union_set, 2,
|
||||
CATALOG_PREFETCH_DEFAULT_BATCH_TILES, &second) ||
|
||||
second.requested_tiles != 3 || second.newly_loaded_tiles != 0 ||
|
||||
second.unavailable_tiles != 0 || second.newly_loaded_stars != 0 ||
|
||||
catalog.count != 2 ||
|
||||
catalog.tiles[90 * CATALOG_ALL_SKY_RA_TILES].stars != first_tile) {
|
||||
fputs("catalog prefetch cache reuse regression failed\n", stderr);
|
||||
fputs("catalog union prefetch cache reuse regression failed\n", stderr);
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* A batch size of one must reach the same final cache as the default. */
|
||||
if (catalog_load_all_sky(&batched, directory) ||
|
||||
catalog_prefetch_tile_set(&batched, &union_set, 4, 1, NULL) ||
|
||||
batched.count != catalog.count ||
|
||||
tile_state(&batched, 0, 90) != 1 || tile_state(&batched, 1, 90) != 1 ||
|
||||
tile_state(&batched, 2, 90) != -1) {
|
||||
fputs("catalog single-tile batching regression failed\n", stderr);
|
||||
goto done;
|
||||
}
|
||||
result = 0;
|
||||
done:
|
||||
catalog_destroy(&catalog);
|
||||
catalog_destroy(&batched);
|
||||
for (int ra = 0; ra < 3; ++ra) {
|
||||
const int written = snprintf(path, sizeof path, "%s/tile_ra%03d_dec090.csv",
|
||||
directory, ra);
|
||||
|
||||
+249
-16
@@ -16,6 +16,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef INT_MAX
|
||||
#define INT_MAX 2147483647
|
||||
@@ -35,18 +36,6 @@ static PointSpreadFunction default_psf(void)
|
||||
return psf;
|
||||
}
|
||||
|
||||
static uint64_t hash_bytes(const double *values, size_t count)
|
||||
{
|
||||
const unsigned char *bytes = (const unsigned char *)values;
|
||||
const size_t nbytes = count * sizeof *values;
|
||||
uint64_t hash = 1469598103934665603ULL;
|
||||
for (size_t i = 0; i < nbytes; ++i) {
|
||||
hash ^= bytes[i];
|
||||
hash *= 1099511628211ULL;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
SCENE_CENTER_WHITE,
|
||||
SCENE_DISTINCT_RGB,
|
||||
@@ -129,31 +118,46 @@ static int run_compare(const char *name, int width, int height,
|
||||
acc.supersampled_height * 3;
|
||||
double *fftw_hdr = malloc(hdr_count * sizeof *fftw_hdr);
|
||||
double *spatial_hdr = malloc(hdr_count * sizeof *spatial_hdr);
|
||||
if (fftw_hdr == NULL || spatial_hdr == NULL) {
|
||||
double *snapshot = malloc(buffer_count * sizeof *snapshot);
|
||||
if (fftw_hdr == NULL || spatial_hdr == NULL || snapshot == NULL) {
|
||||
fprintf(stderr, "FAIL: %s: HDR allocation failed\n", name);
|
||||
++g_failures;
|
||||
free(fftw_hdr);
|
||||
free(spatial_hdr);
|
||||
free(snapshot);
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < hdr_count; ++i)
|
||||
fftw_hdr[i] = spatial_hdr[i] = prefill ? 0.25 : 0.0;
|
||||
fill_scene(&acc, scene, width, height);
|
||||
const uint64_t buffer_before = hash_bytes(acc.buffer, buffer_count);
|
||||
memcpy(snapshot, acc.buffer, buffer_count * sizeof *snapshot);
|
||||
|
||||
if (fast_psf_accumulator_resolve(&acc, fftw_hdr, 4)) {
|
||||
fprintf(stderr, "FAIL: %s: FFTW resolve failed\n", name);
|
||||
++g_failures;
|
||||
goto cleanup;
|
||||
}
|
||||
/* The consuming resolve must leave the shared buffer all-zero so the next
|
||||
* frame starts clean. */
|
||||
for (size_t i = 0; i < buffer_count; ++i)
|
||||
if (acc.buffer[i] != 0.0) {
|
||||
fprintf(stderr,
|
||||
"FAIL: %s: consuming resolve left nonzero residue at %zu\n", name,
|
||||
i);
|
||||
++g_failures;
|
||||
goto cleanup;
|
||||
}
|
||||
memcpy(acc.buffer, snapshot, buffer_count * sizeof *snapshot);
|
||||
if (fast_psf_accumulator_resolve_spatial_reference(&acc, spatial_hdr, 4)) {
|
||||
fprintf(stderr, "FAIL: %s: spatial reference resolve failed\n", name);
|
||||
++g_failures;
|
||||
goto cleanup;
|
||||
}
|
||||
if (hash_bytes(acc.buffer, buffer_count) != buffer_before) {
|
||||
fprintf(stderr, "FAIL: %s: resolve mutated the impulse buffer\n", name);
|
||||
if (memcmp(snapshot, acc.buffer, buffer_count * sizeof *snapshot) != 0) {
|
||||
fprintf(stderr,
|
||||
"FAIL: %s: spatial reference resolve mutated the impulse buffer\n",
|
||||
name);
|
||||
++g_failures;
|
||||
goto cleanup;
|
||||
}
|
||||
@@ -221,6 +225,7 @@ static int run_compare(const char *name, int width, int height,
|
||||
cleanup:
|
||||
free(fftw_hdr);
|
||||
free(spatial_hdr);
|
||||
free(snapshot);
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
return g_failures == 0 ? 0 : -1;
|
||||
}
|
||||
@@ -335,11 +340,239 @@ static void test_channel_isolation(void)
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
}
|
||||
|
||||
/* Reusing one accumulator across frames must never leak frame N-1 deposits
|
||||
* into frame N. Frame 1 deposits an impulse; frame 2 deposits nothing and
|
||||
* must resolve to all zeros. A third frame with a different impulse must not
|
||||
* contain the first impulse. */
|
||||
static void test_cross_frame_no_residue(FastPsfDeposit deposit)
|
||||
{
|
||||
const PointSpreadFunction psf = default_psf();
|
||||
const double relative_tail = 1e-8;
|
||||
const int width = 19, height = 15, supersample = 2;
|
||||
FastPsfAccumulator acc = {0};
|
||||
if (fast_psf_accumulator_init(&acc, width, height, supersample, deposit, &psf,
|
||||
relative_tail, 0.0, 1)) {
|
||||
fail("cross-frame init");
|
||||
return;
|
||||
}
|
||||
const size_t count = (size_t)width * height * 3;
|
||||
double *hdr = calloc(count, sizeof *hdr);
|
||||
double *reference = calloc(count, sizeof *reference);
|
||||
if (hdr == NULL || reference == NULL) {
|
||||
fail("cross-frame allocation");
|
||||
free(hdr);
|
||||
free(reference);
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
return;
|
||||
}
|
||||
const LinearRgb white = {1.0, 1.0, 1.0};
|
||||
/* Frame 1: impulse at a known pixel. */
|
||||
fast_psf_accumulator_deposit(&acc, 4.5, 4.5, white, 1.0);
|
||||
if (fast_psf_accumulator_resolve(&acc, hdr, 2))
|
||||
fail("cross-frame frame 1 resolve");
|
||||
if (!(hdr[3 * (4 * width + 4)] > 0.0))
|
||||
fail("cross-frame frame 1 peak missing");
|
||||
/* Frame 2: no deposit; every output channel must be exactly zero. */
|
||||
memset(hdr, 0, count * sizeof *hdr);
|
||||
if (fast_psf_accumulator_resolve(&acc, hdr, 2))
|
||||
fail("cross-frame frame 2 resolve");
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
if (hdr[i] != 0.0) {
|
||||
fail("cross-frame frame 2 inherited stale deposits");
|
||||
break;
|
||||
}
|
||||
/* Frame 3: a different impulse. Its result must equal a freshly built
|
||||
* accumulator given only that impulse, proving frame 1 left no residue. */
|
||||
FastPsfAccumulator fresh = {0};
|
||||
if (fast_psf_accumulator_init(&fresh, width, height, supersample, deposit,
|
||||
&psf, relative_tail, 0.0, 1)) {
|
||||
fail("cross-frame reference init");
|
||||
free(hdr);
|
||||
free(reference);
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
return;
|
||||
}
|
||||
memset(hdr, 0, count * sizeof *hdr);
|
||||
memset(reference, 0, count * sizeof *reference);
|
||||
fast_psf_accumulator_deposit(&acc, 12.5, 9.5, white, 1.0);
|
||||
fast_psf_accumulator_deposit(&fresh, 12.5, 9.5, white, 1.0);
|
||||
if (fast_psf_accumulator_resolve(&acc, hdr, 2) ||
|
||||
fast_psf_accumulator_resolve(&fresh, reference, 2))
|
||||
fail("cross-frame frame 3 resolve");
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
if (hdr[i] != reference[i]) {
|
||||
fail("cross-frame frame 3 retained the frame 1 impulse");
|
||||
break;
|
||||
}
|
||||
free(hdr);
|
||||
free(reference);
|
||||
fast_psf_accumulator_destroy(&fresh);
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
}
|
||||
|
||||
static int create_tiny_accumulator(FastPsfAccumulator *acc, int width,
|
||||
int height)
|
||||
{
|
||||
PointSpreadFunction psf = default_psf();
|
||||
psf.fwhm_pixels = 0.8;
|
||||
return fast_psf_accumulator_init(acc, width, height, 1,
|
||||
FAST_PSF_DEPOSIT_NEAREST, &psf, 1e-6, 0.0,
|
||||
1);
|
||||
}
|
||||
|
||||
/* Rewrites the sidecar without the line beginning `key=`. */
|
||||
static int meta_drop_line(const char *path, const char *key)
|
||||
{
|
||||
FILE *file = fopen(path, "r");
|
||||
if (file == NULL)
|
||||
return -1;
|
||||
char buffer[2048];
|
||||
const size_t length = fread(buffer, 1, sizeof buffer - 1, file);
|
||||
fclose(file);
|
||||
buffer[length] = '\0';
|
||||
const size_t key_length = strlen(key);
|
||||
char out[2048];
|
||||
size_t written = 0;
|
||||
char *line = buffer;
|
||||
while (line != NULL && *line != '\0' && written + 1 < sizeof out) {
|
||||
char *newline = strchr(line, '\n');
|
||||
const size_t line_length =
|
||||
newline != NULL ? (size_t)(newline - line) : strlen(line);
|
||||
if (!(line_length >= key_length + 1 &&
|
||||
strncmp(line, key, key_length) == 0 && line[key_length] == '=')) {
|
||||
memcpy(out + written, line, line_length);
|
||||
written += line_length;
|
||||
if (newline != NULL)
|
||||
out[written++] = '\n';
|
||||
}
|
||||
line = newline != NULL ? newline + 1 : NULL;
|
||||
}
|
||||
out[written] = '\0';
|
||||
file = fopen(path, "w");
|
||||
if (file == NULL)
|
||||
return -1;
|
||||
const int ok = fputs(out, file) != EOF && fclose(file) == 0;
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Replaces the first character after `key=` with `replacement`. */
|
||||
static int meta_corrupt_value(const char *path, const char *key,
|
||||
char replacement)
|
||||
{
|
||||
FILE *file = fopen(path, "r");
|
||||
if (file == NULL)
|
||||
return -1;
|
||||
char buffer[2048];
|
||||
const size_t length = fread(buffer, 1, sizeof buffer - 1, file);
|
||||
fclose(file);
|
||||
buffer[length] = '\0';
|
||||
char *position = strstr(buffer, key);
|
||||
if (position == NULL || position[strlen(key)] != '=')
|
||||
return -1;
|
||||
char *value = &position[strlen(key) + 1];
|
||||
if (*value == '\0' || *value == '\n')
|
||||
return -1;
|
||||
*value = replacement;
|
||||
file = fopen(path, "w");
|
||||
if (file == NULL)
|
||||
return -1;
|
||||
const int ok = fputs(buffer, file) != EOF && fclose(file) == 0;
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Tiny estimate/measure/wisdom-update/wisdom round trip; no large planning. */
|
||||
static void test_wisdom_modes(void)
|
||||
{
|
||||
const char *path = "/tmp/gr_fast_fftw_wisdom_test";
|
||||
char meta[256];
|
||||
snprintf(meta, sizeof meta, "%s.meta", path);
|
||||
unlink(path);
|
||||
unlink(meta);
|
||||
FastPsfAccumulator acc = {0};
|
||||
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_ESTIMATE, NULL) ||
|
||||
create_tiny_accumulator(&acc, 16, 12)) {
|
||||
fail("plan mode estimate");
|
||||
}
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_MEASURE, NULL) ||
|
||||
create_tiny_accumulator(&acc, 16, 12)) {
|
||||
fail("plan mode measure");
|
||||
}
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_WISDOM_UPDATE, path) ||
|
||||
create_tiny_accumulator(&acc, 16, 12)) {
|
||||
fail("plan mode wisdom-update");
|
||||
}
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
if (access(path, F_OK) != 0 || access(meta, F_OK) != 0)
|
||||
fail("wisdom-update did not write wisdom and meta");
|
||||
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_WISDOM, path) ||
|
||||
create_tiny_accumulator(&acc, 16, 12)) {
|
||||
fail("plan mode wisdom import");
|
||||
}
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
|
||||
/* A different size must be rejected rather than silently replanned. */
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_WISDOM, path) == 0 &&
|
||||
create_tiny_accumulator(&acc, 22, 12) == 0)
|
||||
fail("mismatched wisdom was not rejected");
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
|
||||
/* A wisdom file without its sidecar is a miss, not a silent replan. */
|
||||
unlink(meta);
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_WISDOM, path) == 0 &&
|
||||
create_tiny_accumulator(&acc, 16, 12) == 0)
|
||||
fail("wisdom without meta was accepted");
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
|
||||
/* Regenerate, then corrupt a numeric field: must be rejected. */
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_WISDOM_UPDATE, path) ||
|
||||
create_tiny_accumulator(&acc, 16, 12))
|
||||
fail("wisdom-update regeneration");
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
if (meta_corrupt_value(meta, "fft_width", 'x') != 0)
|
||||
fail("meta corruption setup");
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_WISDOM, path) == 0 &&
|
||||
create_tiny_accumulator(&acc, 16, 12) == 0)
|
||||
fail("malformed wisdom field was accepted");
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
|
||||
/* Regenerate, then drop a required field: must be rejected. */
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_WISDOM_UPDATE, path) ||
|
||||
create_tiny_accumulator(&acc, 16, 12))
|
||||
fail("wisdom-update second regeneration");
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
if (meta_drop_line(meta, "workers") != 0)
|
||||
fail("meta drop setup");
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_WISDOM, path) == 0 &&
|
||||
create_tiny_accumulator(&acc, 16, 12) == 0)
|
||||
fail("wisdom missing a required field was accepted");
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
|
||||
/* wisdom-update into an unwritable directory must fail initialization. */
|
||||
if (fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_WISDOM_UPDATE,
|
||||
"/nonexistent-dir-xyz/wis") == 0 &&
|
||||
create_tiny_accumulator(&acc, 16, 12) == 0)
|
||||
fail("wisdom-update into an unwritable directory succeeded");
|
||||
fast_psf_accumulator_destroy(&acc);
|
||||
|
||||
fast_psf_fftw_configure(FAST_PSF_FFTW_PLAN_ESTIMATE, NULL);
|
||||
unlink(path);
|
||||
unlink(meta);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_next_smooth_size();
|
||||
test_no_wraparound_and_empty();
|
||||
test_channel_isolation();
|
||||
test_cross_frame_no_residue(FAST_PSF_DEPOSIT_NEAREST);
|
||||
test_cross_frame_no_residue(FAST_PSF_DEPOSIT_BILINEAR);
|
||||
test_wisdom_modes();
|
||||
|
||||
const int sizes[][2] = {{17, 13}, {16, 16}, {23, 31}, {33, 17}};
|
||||
const int supersamples[] = {1, 2, 3, 4};
|
||||
|
||||
+11
-10
@@ -82,7 +82,7 @@ int main(void) {
|
||||
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, NULL);
|
||||
0.0, 0, 1, FRAME_CATALOG_PREFETCH_FRAME, NULL, NULL, 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;
|
||||
@@ -104,7 +104,7 @@ int main(void) {
|
||||
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, NULL) != images) {
|
||||
1.0, psf_relative_tail, 0.0, 0, 1, FRAME_CATALOG_PREFETCH_FRAME, NULL, NULL, 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;
|
||||
@@ -140,7 +140,7 @@ int main(void) {
|
||||
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, NULL) != 1 ||
|
||||
1e300, 0, 1, FRAME_CATALOG_PREFETCH_FRAME, NULL, &min_y_stats, NULL, NULL, 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);
|
||||
@@ -159,11 +159,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, psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL, NULL);
|
||||
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, FRAME_CATALOG_PREFETCH_FRAME, NULL, NULL, 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, NULL);
|
||||
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, FRAME_CATALOG_PREFETCH_FRAME, NULL, NULL, 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]) >
|
||||
@@ -397,8 +397,8 @@ int main(void) {
|
||||
PsfSplatStats fast_stats = {0};
|
||||
const size_t fast_images = frame_splat_catalog(
|
||||
&mesh, &catalog, fast_hdr, width, height, test_exposure, &psf, NULL,
|
||||
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, NULL, &fast_stats, NULL,
|
||||
&fast);
|
||||
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1,
|
||||
FRAME_CATALOG_PREFETCH_FRAME, NULL, &fast_stats, NULL, &fast, NULL);
|
||||
if (fast_images != 1 || fast_stats.discarded_below_min_y != 0) {
|
||||
fputs("fast-mode frame splat regression failed\n", stderr);
|
||||
fast_ok = 0;
|
||||
@@ -458,8 +458,9 @@ int main(void) {
|
||||
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, NULL) != 1) {
|
||||
psf_relative_tail, 0.0, 0, 1,
|
||||
FRAME_CATALOG_PREFETCH_FRAME, NULL, NULL, NULL, NULL,
|
||||
NULL) != 1) {
|
||||
fputs("fine source-triangle containment regression failed\n", stderr);
|
||||
frame_lens_mesh_destroy(&fine_mesh);
|
||||
goto done;
|
||||
@@ -503,7 +504,7 @@ int main(void) {
|
||||
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, NULL) != 1 ||
|
||||
psf_relative_tail, 0.0, 0, 1, FRAME_CATALOG_PREFETCH_FRAME, NULL, NULL, NULL, NULL, NULL) != 1 ||
|
||||
hdr[3 * (43 * width + 43)] <= 0.0) {
|
||||
fputs("thin source-triangle inverse-map regression failed\n", stderr);
|
||||
goto done;
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "movie_output.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <omp.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef ENABLE_PNG
|
||||
#include <png.h>
|
||||
#endif
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
static void check(int condition, const char *message) {
|
||||
if (!condition) {
|
||||
fprintf(stderr, "FAIL: %s\n", message);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
#define MAX_JOBS 64
|
||||
#define JOB_WIDTH 16
|
||||
#define JOB_HEIGHT 16
|
||||
#define JOB_BYTES ((size_t)JOB_WIDTH * JOB_HEIGHT * 3)
|
||||
|
||||
typedef struct {
|
||||
int fail_at;
|
||||
int delay_ms;
|
||||
size_t count;
|
||||
size_t order[MAX_JOBS];
|
||||
unsigned char pixels[MAX_JOBS][JOB_BYTES];
|
||||
} MockWriter;
|
||||
|
||||
static int mock_write(void *context, const MovieOutputJob *job,
|
||||
const PngWriteSettings *settings) {
|
||||
(void)settings;
|
||||
MockWriter *mock = context;
|
||||
if (mock->count < MAX_JOBS) {
|
||||
mock->order[mock->count] = job->frame_id;
|
||||
memcpy(mock->pixels[mock->count], job->clean_rgb8, JOB_BYTES);
|
||||
}
|
||||
++mock->count;
|
||||
if (mock->delay_ms > 0) {
|
||||
const struct timespec delay = {.tv_sec = mock->delay_ms / 1000,
|
||||
.tv_nsec =
|
||||
(long)(mock->delay_ms % 1000) * 1000000L};
|
||||
nanosleep(&delay, NULL);
|
||||
}
|
||||
return mock->fail_at >= 0 && (int)job->frame_id == mock->fail_at ? -1 : 0;
|
||||
}
|
||||
|
||||
static unsigned char *make_rgb8(size_t frame_id) {
|
||||
unsigned char *rgb8 = malloc(JOB_BYTES);
|
||||
if (rgb8 == NULL)
|
||||
return NULL;
|
||||
for (size_t i = 0; i < JOB_BYTES; ++i)
|
||||
rgb8[i] = (unsigned char)((frame_id * 31 + i) & 0xff);
|
||||
return rgb8;
|
||||
}
|
||||
|
||||
static MovieOutputJob make_job(size_t frame_id) {
|
||||
MovieOutputJob job;
|
||||
memset(&job, 0, sizeof job);
|
||||
job.frame_id = frame_id;
|
||||
job.width = JOB_WIDTH;
|
||||
job.height = JOB_HEIGHT;
|
||||
job.images = frame_id + 1;
|
||||
job.catalog_stars = 7;
|
||||
job.clean_rgb8 = make_rgb8(frame_id);
|
||||
return job;
|
||||
}
|
||||
|
||||
/* Order and pixel fidelity for capacity 1 and 2. */
|
||||
static void test_order_and_pixels(size_t capacity) {
|
||||
MovieOutputQueue queue;
|
||||
MockWriter mock;
|
||||
memset(&mock, 0, sizeof mock);
|
||||
mock.fail_at = -1;
|
||||
const PngWriteSettings settings = {-1};
|
||||
if (movie_output_queue_init(&queue, capacity, &settings)) {
|
||||
check(0, "order: queue init");
|
||||
return;
|
||||
}
|
||||
movie_output_queue_set_writer(&queue, mock_write, &mock);
|
||||
const size_t frames = 5;
|
||||
int all_ok = 1;
|
||||
for (size_t f = 0; f < frames; ++f) {
|
||||
MovieOutputJob job = make_job(f);
|
||||
if (job.clean_rgb8 == NULL ||
|
||||
movie_output_queue_submit(&queue, &job, NULL)) {
|
||||
all_ok = 0;
|
||||
free(job.clean_rgb8);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const int finish = movie_output_queue_finish(&queue);
|
||||
check(all_ok && finish == 0, "order: submit/finish");
|
||||
check(mock.count == frames, "order: writer saw every job");
|
||||
for (size_t f = 0; f < frames && f < MAX_JOBS; ++f) {
|
||||
check(mock.order[f] == f, "order: frames written in submit order");
|
||||
unsigned char *expected = make_rgb8(f);
|
||||
if (expected != NULL) {
|
||||
check(memcmp(mock.pixels[f], expected, JOB_BYTES) == 0,
|
||||
"order: pixel payload preserved");
|
||||
free(expected);
|
||||
}
|
||||
}
|
||||
movie_output_queue_destroy(&queue);
|
||||
}
|
||||
|
||||
/* A slow writer must push back on the producer at capacity 1. */
|
||||
static void test_backpressure(void) {
|
||||
MovieOutputQueue queue;
|
||||
MockWriter mock;
|
||||
memset(&mock, 0, sizeof mock);
|
||||
mock.fail_at = -1;
|
||||
mock.delay_ms = 40;
|
||||
const PngWriteSettings settings = {-1};
|
||||
if (movie_output_queue_init(&queue, 1, &settings)) {
|
||||
check(0, "backpressure: queue init");
|
||||
return;
|
||||
}
|
||||
movie_output_queue_set_writer(&queue, mock_write, &mock);
|
||||
double total_wait = 0.0;
|
||||
int all_ok = 1;
|
||||
for (size_t f = 0; f < 5; ++f) {
|
||||
MovieOutputJob job = make_job(f);
|
||||
double wait = 0.0;
|
||||
if (movie_output_queue_submit(&queue, &job, &wait)) {
|
||||
all_ok = 0;
|
||||
free(job.clean_rgb8);
|
||||
break;
|
||||
}
|
||||
total_wait += wait;
|
||||
}
|
||||
check(all_ok && movie_output_queue_finish(&queue) == 0,
|
||||
"backpressure: submit/finish");
|
||||
check(total_wait > 0.05, "backpressure: producer waited on a full queue");
|
||||
movie_output_queue_destroy(&queue);
|
||||
}
|
||||
|
||||
/* Writer failure at frame N must propagate, unblock the producer, and join
|
||||
* cleanly without losing the ownership contract. */
|
||||
static void test_writer_failure(void) {
|
||||
MovieOutputQueue queue;
|
||||
MockWriter mock;
|
||||
memset(&mock, 0, sizeof mock);
|
||||
mock.fail_at = 3;
|
||||
const PngWriteSettings settings = {-1};
|
||||
if (movie_output_queue_init(&queue, 2, &settings)) {
|
||||
check(0, "failure: queue init");
|
||||
return;
|
||||
}
|
||||
movie_output_queue_set_writer(&queue, mock_write, &mock);
|
||||
int saw_failure = 0;
|
||||
for (size_t f = 0; f < 8; ++f) {
|
||||
MovieOutputJob job = make_job(f);
|
||||
if (movie_output_queue_submit(&queue, &job, NULL)) {
|
||||
/* The queue no longer owns these buffers. */
|
||||
free(job.clean_rgb8);
|
||||
saw_failure = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const int finish = movie_output_queue_finish(&queue);
|
||||
check(saw_failure, "failure: a later submit reported the writer error");
|
||||
check(finish != 0, "failure: finish reports the recorded error");
|
||||
check(movie_output_queue_failed(&queue), "failure: failed flag persists");
|
||||
movie_output_queue_destroy(&queue);
|
||||
}
|
||||
|
||||
/* Empty finish and never-submitted init must not hang or leak. */
|
||||
static void test_empty_paths(void) {
|
||||
const PngWriteSettings settings = {-1};
|
||||
MovieOutputQueue queue;
|
||||
if (movie_output_queue_init(&queue, 2, &settings)) {
|
||||
check(0, "empty: queue init");
|
||||
return;
|
||||
}
|
||||
check(movie_output_queue_finish(&queue) == 0, "empty: finish with no jobs");
|
||||
movie_output_queue_destroy(&queue);
|
||||
|
||||
if (movie_output_queue_init(&queue, 1, &settings)) {
|
||||
check(0, "empty: second init");
|
||||
return;
|
||||
}
|
||||
movie_output_queue_destroy(&queue);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_PNG
|
||||
static int decode_png_rgb8(const char *path, unsigned char *out, int width,
|
||||
int height) {
|
||||
FILE *file = fopen(path, "rb");
|
||||
if (file == NULL)
|
||||
return -1;
|
||||
png_structp png =
|
||||
png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
|
||||
png_infop info = png != NULL ? png_create_info_struct(png) : NULL;
|
||||
int ok = 0;
|
||||
if (png != NULL && info != NULL && !setjmp(png_jmpbuf(png))) {
|
||||
png_init_io(png, file);
|
||||
png_read_info(png, info);
|
||||
for (int row = 0; row < height; ++row)
|
||||
png_read_row(png, &out[(size_t)row * width * 3], NULL);
|
||||
png_read_end(png, info);
|
||||
ok = 1;
|
||||
}
|
||||
fclose(file);
|
||||
png_destroy_read_struct(&png, &info, NULL);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* The default writer's clean and mesh files must decode to the submitted
|
||||
* payloads. */
|
||||
static void test_default_writer_success(void) {
|
||||
char directory[] = "/tmp/movie_output_XXXXXX";
|
||||
if (mkdtemp(directory) == NULL) {
|
||||
check(0, "default success: mkdtemp");
|
||||
return;
|
||||
}
|
||||
char clean_path[PATH_MAX];
|
||||
char mesh_path[PATH_MAX];
|
||||
snprintf(clean_path, sizeof clean_path, "%s/frame_000000.png", directory);
|
||||
snprintf(mesh_path, sizeof mesh_path, "%s/frame_000000_mesh.png", directory);
|
||||
const PngWriteSettings settings = {-1};
|
||||
MovieOutputQueue queue;
|
||||
if (movie_output_queue_init(&queue, 1, &settings)) {
|
||||
check(0, "default success: queue init");
|
||||
rmdir(directory);
|
||||
return;
|
||||
}
|
||||
MovieOutputJob job = make_job(0);
|
||||
job.draw_mesh = 1;
|
||||
snprintf(job.output_path, sizeof job.output_path, "%s", clean_path);
|
||||
snprintf(job.mesh_path, sizeof job.mesh_path, "%s", mesh_path);
|
||||
job.mesh_rgb8 = make_rgb8(99);
|
||||
int submitted = job.clean_rgb8 != NULL && job.mesh_rgb8 != NULL &&
|
||||
movie_output_queue_submit(&queue, &job, NULL) == 0;
|
||||
if (!submitted) {
|
||||
free(job.clean_rgb8);
|
||||
free(job.mesh_rgb8);
|
||||
}
|
||||
check(submitted && movie_output_queue_finish(&queue) == 0,
|
||||
"default success: submit/finish");
|
||||
unsigned char clean_decoded[JOB_BYTES];
|
||||
unsigned char mesh_decoded[JOB_BYTES];
|
||||
unsigned char *clean_expected = make_rgb8(0);
|
||||
unsigned char *mesh_expected = make_rgb8(99);
|
||||
const int decoded_ok =
|
||||
clean_expected != NULL && mesh_expected != NULL &&
|
||||
decode_png_rgb8(clean_path, clean_decoded, JOB_WIDTH, JOB_HEIGHT) == 0 &&
|
||||
decode_png_rgb8(mesh_path, mesh_decoded, JOB_WIDTH, JOB_HEIGHT) == 0;
|
||||
check(decoded_ok && memcmp(clean_decoded, clean_expected, JOB_BYTES) == 0,
|
||||
"default success: clean pixels");
|
||||
check(decoded_ok && memcmp(mesh_decoded, mesh_expected, JOB_BYTES) == 0,
|
||||
"default success: mesh pixels");
|
||||
free(clean_expected);
|
||||
free(mesh_expected);
|
||||
unlink(clean_path);
|
||||
unlink(mesh_path);
|
||||
rmdir(directory);
|
||||
movie_output_queue_destroy(&queue);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* The default writer must fail under a real filesystem error. */
|
||||
static void test_default_writer_failure(void) {
|
||||
MovieOutputQueue queue;
|
||||
const PngWriteSettings settings = {-1};
|
||||
if (movie_output_queue_init(&queue, 1, &settings)) {
|
||||
check(0, "default failure: queue init");
|
||||
return;
|
||||
}
|
||||
MovieOutputJob job = make_job(0);
|
||||
snprintf(job.output_path, sizeof job.output_path,
|
||||
"/nonexistent-directory-xyz/frame.png");
|
||||
if (job.clean_rgb8 == NULL || movie_output_queue_submit(&queue, &job, NULL)) {
|
||||
free(job.clean_rgb8);
|
||||
check(0, "default failure: submit");
|
||||
movie_output_queue_destroy(&queue);
|
||||
return;
|
||||
}
|
||||
check(movie_output_queue_finish(&queue) != 0,
|
||||
"default failure: finish reports unwritable path");
|
||||
movie_output_queue_destroy(&queue);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_order_and_pixels(1);
|
||||
test_order_and_pixels(2);
|
||||
test_backpressure();
|
||||
test_writer_failure();
|
||||
test_empty_paths();
|
||||
#ifdef ENABLE_PNG
|
||||
test_default_writer_success();
|
||||
#endif
|
||||
test_default_writer_failure();
|
||||
if (failures != 0) {
|
||||
fprintf(stderr, "%d movie-output failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("movie-output tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -4,11 +4,20 @@
|
||||
* implementation; no formula is duplicated here. The test needs neither a
|
||||
* catalog, ray tracing, a GPU, nor image files, and it builds in both the
|
||||
* ENABLE_PNG=1 and ENABLE_PNG=0 configurations. */
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "optics.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef ENABLE_PNG
|
||||
#include <png.h>
|
||||
#endif
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
@@ -116,6 +125,92 @@ int main(void)
|
||||
tone_map_srgb8_channel(0.5, &reinhard),
|
||||
"softclip is brighter than reinhard at x=0.5");
|
||||
|
||||
/* Parallel HDR -> RGB8 conversion must be byte-identical for any worker
|
||||
* count; the display transfer and quantization stay unchanged. */
|
||||
{
|
||||
const int width = 7, height = 5;
|
||||
const size_t count = (size_t)width * height * 3;
|
||||
double *hdr = malloc(count * sizeof *hdr);
|
||||
unsigned char *one = malloc(count);
|
||||
unsigned char *two = malloc(count);
|
||||
unsigned char *four = malloc(count);
|
||||
if (hdr == NULL || one == NULL || two == NULL || four == NULL) {
|
||||
check(0, "parallel tone-map allocation");
|
||||
} else {
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
hdr[i] = (double)(i % 17) * 0.25 - 0.5;
|
||||
check(tone_map_srgb8_image(hdr, one, width, height, &softclip2, 1) == 0,
|
||||
"tone_map_srgb8_image serial");
|
||||
check(tone_map_srgb8_image(hdr, two, width, height, &softclip2, 2) == 0,
|
||||
"tone_map_srgb8_image 2 workers");
|
||||
check(tone_map_srgb8_image(hdr, four, width, height, &softclip2, 4) == 0,
|
||||
"tone_map_srgb8_image 4 workers");
|
||||
check(memcmp(one, two, count) == 0 &&
|
||||
memcmp(one, four, count) == 0,
|
||||
"parallel tone map changes output bytes");
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
if (one[i] != tone_map_srgb8_channel(hdr[i], &softclip2)) {
|
||||
check(0, "parallel tone map disagrees with channel function");
|
||||
break;
|
||||
}
|
||||
}
|
||||
free(hdr);
|
||||
free(one);
|
||||
free(two);
|
||||
free(four);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_PNG
|
||||
/* The wrapper's PNG pixels must decode back to the same RGB8 bytes the
|
||||
* parallel converter produced. */
|
||||
{
|
||||
const int width = 9, height = 4;
|
||||
const size_t count = (size_t)width * height * 3;
|
||||
double *hdr = malloc(count * sizeof *hdr);
|
||||
unsigned char *expected = malloc(count);
|
||||
unsigned char *decoded = calloc(count, 1);
|
||||
char template_path[] = "/tmp/tone_map_XXXXXX";
|
||||
char path[64];
|
||||
const int fd = mkstemp(template_path);
|
||||
snprintf(path, sizeof path, "%s.png", template_path);
|
||||
if (hdr == NULL || expected == NULL || decoded == NULL || fd < 0) {
|
||||
check(0, "PNG round-trip allocation");
|
||||
} else {
|
||||
close(fd);
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
hdr[i] = (double)(i % 23) * 0.11;
|
||||
tone_map_srgb8_image(hdr, expected, width, height, &softclip2, 2);
|
||||
check(write_tonemapped_image(path, hdr, width, height, &softclip2) == 0,
|
||||
"write_tonemapped_image wrapper writes PNG");
|
||||
FILE *file = fopen(path, "rb");
|
||||
png_structp png = file ? png_create_read_struct(
|
||||
PNG_LIBPNG_VER_STRING, NULL, NULL, NULL)
|
||||
: NULL;
|
||||
png_infop info = png ? png_create_info_struct(png) : NULL;
|
||||
volatile int decode_ok = 0;
|
||||
if (png == NULL || info == NULL || setjmp(png_jmpbuf(png))) {
|
||||
check(0, "PNG decode setup");
|
||||
} else {
|
||||
png_init_io(png, file);
|
||||
png_read_info(png, info);
|
||||
for (int row = 0; row < height; ++row)
|
||||
png_read_row(png, &decoded[(size_t)row * width * 3], NULL);
|
||||
png_read_end(png, info);
|
||||
decode_ok = 1;
|
||||
}
|
||||
if (file) fclose(file);
|
||||
png_destroy_read_struct(&png, &info, NULL);
|
||||
check(decode_ok && memcmp(decoded, expected, count) == 0,
|
||||
"wrapper PNG pixels differ from parallel RGB8");
|
||||
unlink(path);
|
||||
unlink(template_path);
|
||||
}
|
||||
free(hdr);
|
||||
free(expected);
|
||||
free(decoded);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (failures != 0) {
|
||||
fprintf(stderr, "%d tone-map assertion(s) failed\n", failures);
|
||||
return EXIT_FAILURE;
|
||||
|
||||
Reference in new issue
Block a user