Files
GR-raytracing/tests/test_movie_output.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

311 lines
9.5 KiB
C

#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;
}