Feat: Add post-tone-map mesh diagnostics with RGBA overlays

Color antialiased half-edges by ray outcome with configurable Catppuccin colors and default opacity 0.5.

Rasterize premultiplied RGBA8 overlays on the producer and composite in place after writing the clean image. Keep single-frame, movie, and replay output consistent.

Add overlay, CLI, and queue ownership regressions and document the final output architecture.
This commit is contained in:
wyj committed 2026-10-10 01:46:32 -04:00
1 parent 4053d5d0c8
commit 80f9dcb3a3
17 files changed
+1873 -182

No files matched your search

+1 -1
View File
@@ -476,7 +476,7 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-', dir='/tmp/opencode') a
'--observer-position', 0.3 * time, 0, 0,
'--observer-velocity', 0.3, 0, 0,
'--look-ra-deg', 0, '--look-dec-deg', 0,
'--lens-map-output', path, '--output', tmp / 'alcubierre.png')
'--lens-map-output', path, '--output', tmp / f'alcubierre.{ext}')
assert struct.unpack_from('<d', path.read_bytes(),
MAP_FRAME_HEADER_START + 8)[0] == time
vertices, triangles = map_vertices(path)
+22 -4
View File
@@ -1,5 +1,6 @@
#include "frame.h"
#include "lens_map.h"
#include "mesh_overlay.h"
#include "optics.h"
#include <math.h>
@@ -1086,10 +1087,27 @@ int main(void) {
if (!fast_ok)
goto done;
}
frame_draw_mesh(&mesh, hdr, width, height, 0.5, 0.5);
if (hdr[3 * (10 * width + 20)] != 0.25) {
fputs("mesh diagnostic overlay regression failed\n", stderr);
goto done;
/* Diagnostic overlay draws the finalized mesh as an sRGB8 edge map: the
* vertical coarse edge crossing (20, 10) must be painted, while an interior
* pixel away from every edge must stay at the background value. */
{
MeshOverlayLines overlay_lines = {0};
const MeshOverlaySettings overlay_settings = mesh_overlay_default_settings();
unsigned char *overlay_rgb = calloc((size_t)width * height * 3, 1);
const int overlay_ok =
overlay_rgb != NULL &&
mesh_overlay_prepare(&mesh, &overlay_lines) == 0 &&
overlay_lines.count != 0 &&
mesh_overlay_draw_rgb8(&overlay_lines, overlay_rgb, width, height,
&overlay_settings) == 0 &&
overlay_rgb[3 * (10 * width + 20)] != 0 &&
overlay_rgb[3 * (12 * width + 5)] == 0;
mesh_overlay_lines_destroy(&overlay_lines);
free(overlay_rgb);
if (!overlay_ok) {
fputs("mesh diagnostic overlay regression failed\n", stderr);
goto done;
}
}
/* A fixed absolute edge tolerance used to make tiny source triangles claim
* sources far outside their field. */
+740
View File
@@ -0,0 +1,740 @@
/* Standalone regression for the diagnostic mesh overlay (src/mesh_overlay.c).
* It links only the overlay module, so it needs neither a catalog, ray tracing,
* FFTW, nor an output writer. Every assertion targets observable behavior:
* terminal-category colors, half-edge switching, AA coverage, deduplication,
* deterministic ordering, clipping safety and clean failure on bad input. */
#include "mesh_overlay.h"
#include <float.h>
#include <limits.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int fail(const char *message) {
fprintf(stderr, "%s\n", message);
return -1;
}
static LensVertex make_vertex(double x, double y, int traced,
RayOutcome outcome) {
LensVertex vertex;
memset(&vertex, 0, sizeof vertex);
vertex.image_x = x;
vertex.image_y = y;
vertex.traced = traced;
vertex.outcome = outcome;
return vertex;
}
static int channel_at(const unsigned char *rgb, int width, int x, int y,
int channel) {
return rgb[3 * ((size_t)y * (size_t)width + (size_t)x) + (size_t)channel];
}
static int test_default_settings(void) {
const MeshOverlaySettings settings = mesh_overlay_default_settings();
static const unsigned char expected[MESH_OVERLAY_CATEGORY_COUNT][3] = {
{0x7F, 0x84, 0x9C}, {0xCB, 0xA6, 0xF7}, {0xF9, 0xE2, 0xAF},
{0xF3, 0x8B, 0xA8}, {0x89, 0xB4, 0xFA}};
if (settings.opacity != 0.5)
return fail("default opacity is not 0.5");
for (int category = 0; category < MESH_OVERLAY_CATEGORY_COUNT; ++category)
for (int channel = 0; channel < 3; ++channel)
if (settings.colors[category][channel] != expected[category][channel])
return fail("default palette mismatch");
return 0;
}
static int test_parse_color(void) {
unsigned char rgb[3] = {1, 2, 3};
if (mesh_overlay_parse_color("#7F849C", rgb) != 0 || rgb[0] != 0x7F ||
rgb[1] != 0x84 || rgb[2] != 0x9C)
return fail("parse uppercase failed");
if (mesh_overlay_parse_color("#7f849c", rgb) != 0 || rgb[0] != 0x7F ||
rgb[1] != 0x84 || rgb[2] != 0x9C)
return fail("parse lowercase failed");
if (mesh_overlay_parse_color("#000000", rgb) != 0 || rgb[0] || rgb[1] ||
rgb[2])
return fail("parse black failed");
static const char *const bad[] = {"", "#", "7F849C",
"#7F849", "#7F849C0", "#GG849C",
"#7F84 9C", "#7F849c ", " #7F849C",
"#12345g", "#12345G0"};
for (size_t i = 0; i < sizeof bad / sizeof *bad; ++i)
if (mesh_overlay_parse_color(bad[i], rgb) != -1)
return fail("accepted an invalid color string");
if (mesh_overlay_parse_color(NULL, rgb) != -1)
return fail("accepted NULL text");
if (mesh_overlay_parse_color("#7F849C", NULL) != -1)
return fail("accepted NULL output");
return 0;
}
/* Deduplication, canonical ordering and per-vertex category assignment. */
static int test_prepare_dedup_categories(void) {
LensVertex vertices[4] = {
make_vertex(10, 10, 1, RAY_OUTCOME_ESCAPED),
make_vertex(30, 10, 1, RAY_OUTCOME_DARK),
make_vertex(10, 30, 1, RAY_OUTCOME_UNRESOLVED),
make_vertex(30, 30, 1, RAY_OUTCOME_INCOMPLETE)};
LensTriangle triangles[2] = {{{0, 1, 2}, 0, 0, 0}, {{0, 2, 3}, 0, 0, 0}};
FrameLensMesh mesh = {.vertices = vertices,
.vertex_count = 4,
.triangles = triangles,
.triangle_count = 2};
MeshOverlayLines lines = {0};
if (mesh_overlay_prepare(&mesh, &lines) != 0)
return fail("prepare failed on a valid quad");
/* The shared diagonal (0,2) must appear exactly once. */
if (lines.count != 5) {
mesh_overlay_lines_destroy(&lines);
return fail("unique edge count is not 5");
}
static const struct {
unsigned char c0, c1;
} expected[5] = {
{MESH_OVERLAY_CATEGORY_ESCAPE, MESH_OVERLAY_CATEGORY_DARK},
{MESH_OVERLAY_CATEGORY_ESCAPE, MESH_OVERLAY_CATEGORY_UNRESOLVED},
{MESH_OVERLAY_CATEGORY_ESCAPE, MESH_OVERLAY_CATEGORY_INCOMPLETE},
{MESH_OVERLAY_CATEGORY_DARK, MESH_OVERLAY_CATEGORY_UNRESOLVED},
{MESH_OVERLAY_CATEGORY_UNRESOLVED, MESH_OVERLAY_CATEGORY_INCOMPLETE}};
for (size_t i = 0; i < lines.count; ++i)
if (lines.lines[i].category0 != expected[i].c0 ||
lines.lines[i].category1 != expected[i].c1) {
mesh_overlay_lines_destroy(&lines);
return fail("edge category or deterministic order mismatch");
}
if (lines.lines[0].x0 != 10 || lines.lines[0].y0 != 10 ||
lines.lines[0].x1 != 30 || lines.lines[0].y1 != 10) {
mesh_overlay_lines_destroy(&lines);
return fail("edge coordinates mismatch");
}
/* Lines must copy coordinates, never alias the mutable mesh. */
vertices[0].image_x = 999;
if (lines.lines[0].x0 != 10) {
mesh_overlay_lines_destroy(&lines);
return fail("overlay lines alias the live mesh");
}
mesh_overlay_lines_destroy(&lines);
if (lines.lines != NULL || lines.count != 0)
return fail("destroy did not reset the handle");
mesh_overlay_lines_destroy(NULL);
return 0;
}
static int test_untraced_category(void) {
LensVertex vertices[3] = {
make_vertex(10, 10, 1, RAY_OUTCOME_ESCAPED),
/* traced == 0 must win over the stale outcome value. */
make_vertex(30, 10, 0, RAY_OUTCOME_ESCAPED),
make_vertex(10, 30, 1, RAY_OUTCOME_INCOMPLETE)};
LensTriangle triangle = {{0, 1, 2}, 0, 0, 0};
FrameLensMesh mesh = {.vertices = vertices,
.vertex_count = 3,
.triangles = &triangle,
.triangle_count = 1};
MeshOverlayLines lines = {0};
if (mesh_overlay_prepare(&mesh, &lines) != 0)
return fail("prepare failed for untraced mesh");
int saw_untraced = 0;
for (size_t i = 0; i < lines.count; ++i)
if (lines.lines[i].category0 == MESH_OVERLAY_CATEGORY_UNTRACED ||
lines.lines[i].category1 == MESH_OVERLAY_CATEGORY_UNTRACED)
saw_untraced = 1;
mesh_overlay_lines_destroy(&lines);
return saw_untraced ? 0 : fail("untraced vertex category missing");
}
/* Boundary edges are emitted regardless of winding, and reversing the winding
* cannot change the deterministic output. */
static int test_boundary_and_winding(void) {
LensVertex vertices[3] = {
make_vertex(10, 10, 1, RAY_OUTCOME_ESCAPED),
make_vertex(30, 10, 1, RAY_OUTCOME_DARK),
make_vertex(10, 30, 1, RAY_OUTCOME_UNRESOLVED)};
LensTriangle forward[1] = {{{0, 1, 2}, 0, 0, 0}};
LensTriangle reversed[1] = {{{2, 1, 0}, 0, 0, 0}};
FrameLensMesh mesh_a = {.vertices = vertices,
.vertex_count = 3,
.triangles = forward,
.triangle_count = 1};
FrameLensMesh mesh_b = {.vertices = vertices,
.vertex_count = 3,
.triangles = reversed,
.triangle_count = 1};
MeshOverlayLines a = {0}, b = {0};
if (mesh_overlay_prepare(&mesh_a, &a) != 0 ||
mesh_overlay_prepare(&mesh_b, &b) != 0) {
mesh_overlay_lines_destroy(&a);
mesh_overlay_lines_destroy(&b);
return fail("prepare failed for single triangle");
}
int ok = a.count == 3 && b.count == 3 &&
memcmp(a.lines, b.lines, a.count * sizeof *a.lines) == 0;
mesh_overlay_lines_destroy(&a);
mesh_overlay_lines_destroy(&b);
return ok ? 0 : fail("boundary/winding determinism failed");
}
/* Off-mesh probe witnesses have no triangle edge and must never be emitted or
* drawn as a vertex dot. */
static int test_isolated_witness_not_drawn(void) {
LensVertex vertices[4] = {
make_vertex(10, 50, 1, RAY_OUTCOME_ESCAPED),
make_vertex(30, 50, 1, RAY_OUTCOME_ESCAPED),
make_vertex(20, 30, 1, RAY_OUTCOME_ESCAPED),
make_vertex(90, 90, 1, RAY_OUTCOME_ESCAPED)};
vertices[3].diagnostic_probe = 1;
vertices[3].probe_edge[0] = 0;
vertices[3].probe_edge[1] = 1;
LensTriangle triangle = {{0, 1, 2}, 0, 0, 0};
FrameLensMesh mesh = {.vertices = vertices,
.vertex_count = 4,
.triangles = &triangle,
.triangle_count = 1};
const int width = 100, height = 100;
MeshOverlayLines lines = {0};
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
const MeshOverlaySettings settings = mesh_overlay_default_settings();
if (rgb == NULL || mesh_overlay_prepare(&mesh, &lines) != 0) {
free(rgb);
mesh_overlay_lines_destroy(&lines);
return fail("prepare failed for witness mesh");
}
if (lines.count != 3) {
free(rgb);
mesh_overlay_lines_destroy(&lines);
return fail("isolated witness added an edge");
}
if (mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) != 0) {
free(rgb);
mesh_overlay_lines_destroy(&lines);
return fail("draw failed for witness mesh");
}
const int witness_painted =
channel_at(rgb, width, 90, 90, 0) != 0 ||
channel_at(rgb, width, 90, 90, 1) != 0 ||
channel_at(rgb, width, 90, 90, 2) != 0;
free(rgb);
mesh_overlay_lines_destroy(&lines);
return witness_painted ? fail("isolated witness vertex was drawn as a dot")
: 0;
}
static int test_draw_category_colors(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 1.0;
const int width = 80, height = 40;
for (int category = 0; category < MESH_OVERLAY_CATEGORY_COUNT; ++category) {
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
if (rgb == NULL)
return fail("allocation failed");
MeshOverlayLine line = {.x0 = 10,
.y0 = 20,
.x1 = 70,
.y1 = 20,
.category0 = (unsigned char)category,
.category1 = (unsigned char)category};
MeshOverlayLines lines = {.lines = &line, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
for (int channel = 0; ok && channel < 3; ++channel)
if (channel_at(rgb, width, 40, 20, channel) !=
settings.colors[category][channel])
ok = 0;
free(rgb);
if (!ok)
return fail("category color mismatch");
}
return 0;
}
static int test_halves_and_switch(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 1.0;
const int width = 60, height = 40;
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
if (rgb == NULL)
return fail("allocation failed");
MeshOverlayLine line = {.x0 = 10,
.y0 = 20,
.x1 = 50,
.y1 = 20,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_DARK};
MeshOverlayLines lines = {.lines = &line, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
/* The switch sits at the major-axis midpoint x = 30. Interior pixels are
* fully covered, so each must equal exactly one endpoint color: the whole
* edge is rasterized once, never as two blends that would smear the switch. */
for (int channel = 0; ok && channel < 3; ++channel) {
if (channel_at(rgb, width, 11, 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_ESCAPE][channel] ||
channel_at(rgb, width, 29, 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_ESCAPE][channel] ||
channel_at(rgb, width, 30, 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_DARK][channel] ||
channel_at(rgb, width, 49, 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_DARK][channel])
ok = 0;
}
free(rgb);
return ok ? 0 : fail("half-edge color switch failed");
}
static int test_antialiasing(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 1.0;
const int width = 60, height = 60;
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
if (rgb == NULL)
return fail("allocation failed");
MeshOverlayLine line = {.x0 = 10,
.y0 = 10,
.x1 = 50,
.y1 = 20,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines lines = {.lines = &line, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
const int full = settings.colors[MESH_OVERLAY_CATEGORY_ESCAPE][0];
int partial = 0;
for (int y = 0; y < height; ++y)
for (int x = 0; x < width; ++x) {
const int value = channel_at(rgb, width, x, y, 0);
if (value > 0 && value < full)
++partial;
}
free(rgb);
return ok && partial > 0 ? 0 : fail("no antialiased partial coverage");
}
static int test_subpixel_and_zero_length(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 1.0;
unsigned char pixels[8 * 8 * 3] = {0};
MeshOverlayLine line = {.x0 = 2.1, .y0 = 3.0, .x1 = 2.4, .y1 = 3.0,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines lines = {.lines = &line, .count = 1};
if (mesh_overlay_draw_rgb8(&lines, pixels, 8, 8, &settings))
return fail("subpixel draw failed");
for (int channel = 0; channel < 3; ++channel)
if (channel_at(pixels, 8, 2, 3, channel) !=
lround((line.x1 - line.x0) * settings.colors[0][channel]))
return fail("subpixel edge applied overlapping endpoint blends");
memset(pixels, 0, sizeof pixels);
line.x1 = line.x0;
if (mesh_overlay_draw_rgb8(&lines, pixels, 8, 8, &settings))
return fail("zero-length draw failed");
for (size_t i = 0; i < sizeof pixels; ++i)
if (pixels[i])
return fail("zero-length edge became a vertex dot");
return 0;
}
static int test_high_white_background(void) {
const MeshOverlaySettings settings = mesh_overlay_default_settings();
const int width = 80, height = 40;
unsigned char *rgb = malloc((size_t)width * height * 3);
if (rgb == NULL)
return fail("allocation failed");
memset(rgb, 255, (size_t)width * height * 3);
MeshOverlayLine line = {.x0 = 10,
.y0 = 20,
.x1 = 60,
.y1 = 20,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines lines = {.lines = &line, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
for (int channel = 0; ok && channel < 3; ++channel) {
const long expected =
lround(255.0 * (1.0 - settings.opacity) +
settings.colors[MESH_OVERLAY_CATEGORY_ESCAPE][channel] *
settings.opacity);
if (channel_at(rgb, width, 40, 20, channel) != expected)
ok = 0;
}
/* Still clearly visible against white. */
if (ok && channel_at(rgb, width, 40, 20, 0) == 255)
ok = 0;
free(rgb);
return ok ? 0 : fail("overlay not visible on a high-white background");
}
static int test_opacity_extremes(void) {
const int width = 60, height = 40;
MeshOverlayLine line = {.x0 = 10,
.y0 = 20,
.x1 = 50,
.y1 = 20,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines lines = {.lines = &line, .count = 1};
unsigned char *rgb = malloc((size_t)width * height * 3);
if (rgb == NULL)
return fail("allocation failed");
MeshOverlaySettings settings = mesh_overlay_default_settings();
memset(rgb, 0x33, (size_t)width * height * 3);
settings.opacity = 0.0;
if (mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) != 0) {
free(rgb);
return fail("draw failed at opacity 0");
}
for (int i = 0; i < width * height * 3; ++i)
if (rgb[i] != 0x33) {
free(rgb);
return fail("opacity 0 changed the image");
}
memset(rgb, 0x00, (size_t)width * height * 3);
settings.opacity = 1.0;
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
for (int channel = 0; ok && channel < 3; ++channel)
if (channel_at(rgb, width, 30, 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_ESCAPE][channel])
ok = 0;
free(rgb);
return ok ? 0 : fail("opacity 1 did not apply the full color");
}
static int test_clipping_and_huge_coordinates(void) {
const MeshOverlaySettings settings = mesh_overlay_default_settings();
const int width = 64, height = 64;
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
if (rgb == NULL)
return fail("allocation failed");
/* A horizontal line far beyond both image edges must still paint row 30 and
* terminate in bounded time. */
MeshOverlayLine huge = {.x0 = -1e15,
.y0 = 30,
.x1 = 1e15,
.y1 = 30,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_DARK};
MeshOverlayLines lines = {.lines = &huge, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
int painted = 0;
for (int x = 0; x < width; ++x)
if (channel_at(rgb, width, x, 30, 0) != 0)
painted = 1;
if (!ok || !painted) {
free(rgb);
return fail("huge coordinate line was not clipped into view");
}
/* A fully offscreen line leaves the buffer untouched. */
memset(rgb, 0, (size_t)width * height * 3);
MeshOverlayLine offscreen = {.x0 = 1000,
.y0 = 1000,
.x1 = 2000,
.y1 = 1000,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
lines.lines = &offscreen;
if (mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) != 0) {
free(rgb);
return fail("offscreen line returned an error");
}
for (int i = 0; i < width * height * 3; ++i)
if (rgb[i] != 0) {
free(rgb);
return fail("offscreen line painted the image");
}
/* A huge diagonal must not overflow the integer conversions. */
MeshOverlayLine diagonal = {.x0 = -1e12,
.y0 = -1e12,
.x1 = 1e12,
.y1 = 1e12,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_DARK};
lines.lines = &diagonal;
if (mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) != 0) {
free(rgb);
return fail("huge diagonal returned an error");
}
free(rgb);
return 0;
}
static int test_extreme_magnitudes(void) {
const MeshOverlaySettings settings = mesh_overlay_default_settings();
const int width = 64, height = 64;
const size_t bytes = (size_t)width * height * 3;
unsigned char *rgb = malloc(bytes);
if (rgb == NULL)
return fail("allocation failed");
/* -DBL_MAX..+DBL_MAX overflows the endpoint difference to infinity: the whole
* batch must be rejected before painting and the buffer left untouched. */
MeshOverlayLine bad = {.x0 = -DBL_MAX,
.y0 = 10,
.x1 = DBL_MAX,
.y1 = 10,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines lines = {.lines = &bad, .count = 1};
memset(rgb, 0x5A, bytes);
int rejected =
mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == -1;
int unchanged = 1;
for (size_t i = 0; i < bytes; ++i)
if (rgb[i] != 0x5A)
unchanged = 0;
if (!rejected || !unchanged) {
free(rgb);
return fail("+-DBL_MAX x-delta not rejected unchanged");
}
bad.x0 = 10;
bad.x1 = 10;
bad.y0 = -DBL_MAX;
bad.y1 = DBL_MAX;
memset(rgb, 0x5A, bytes);
rejected = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == -1;
unchanged = 1;
for (size_t i = 0; i < bytes; ++i)
if (rgb[i] != 0x5A)
unchanged = 0;
if (!rejected || !unchanged) {
free(rgb);
return fail("+-DBL_MAX y-delta not rejected unchanged");
}
/* Same-sign DBL_MAX endpoints have a finite difference and a finite (non
* overflowing) midpoint; the segment is entirely offscreen, so it is skipped
* safely without painting. */
MeshOverlayLine same = {.x0 = DBL_MAX,
.y0 = 10,
.x1 = DBL_MAX,
.y1 = 30,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_DARK};
lines.lines = &same;
memset(rgb, 0x5A, bytes);
if (mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) != 0) {
free(rgb);
return fail("same-sign DBL_MAX segment was not handled safely");
}
for (size_t i = 0; i < bytes; ++i)
if (rgb[i] != 0x5A) {
free(rgb);
return fail("offscreen same-sign DBL_MAX segment painted the image");
}
free(rgb);
return 0;
}
static int test_clipped_midpoint_uses_original(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 1.0;
const int width = 64, height = 40;
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
if (rgb == NULL)
return fail("allocation failed");
/* The original midpoint is -40, so the entire visible span [0, 19] lies in
* the second half and every visible pixel must use category1. A midpoint
* recomputed from the clipped endpoints would wrongly color the left half. */
MeshOverlayLine line = {.x0 = -100,
.y0 = 20,
.x1 = 20,
.y1 = 20,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_DARK};
MeshOverlayLines lines = {.lines = &line, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
static const int probes[] = {0, 1, 5, 10, 19};
for (size_t p = 0; ok && p < sizeof probes / sizeof *probes; ++p)
for (int channel = 0; channel < 3; ++channel)
if (channel_at(rgb, width, probes[p], 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_DARK][channel])
ok = 0;
free(rgb);
return ok ? 0
: fail("clipped edge did not use the original midpoint category");
}
static int test_invalid_arguments(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
unsigned char pixels[4 * 4 * 3] = {0};
MeshOverlayLines lines = {0};
if (mesh_overlay_prepare(NULL, &lines) != -1)
return fail("prepare accepted NULL mesh");
if (mesh_overlay_prepare(NULL, NULL) != -1)
return fail("prepare accepted NULL lines");
FrameLensMesh empty = {0};
if (mesh_overlay_prepare(&empty, &lines) != 0 || lines.count != 0 ||
lines.lines != NULL)
return fail("empty mesh did not produce an empty edge set");
LensVertex vertices[3] = {
make_vertex(10, 10, 1, RAY_OUTCOME_ESCAPED),
make_vertex(30, 10, 1, RAY_OUTCOME_ESCAPED),
make_vertex(10, 30, 1, RAY_OUTCOME_ESCAPED)};
LensTriangle triangle = {{0, 1, 2}, 0, 0, 0};
FrameLensMesh mesh = {.vertices = vertices,
.vertex_count = 2, /* vertex 2 is out of range */
.triangles = &triangle,
.triangle_count = 1};
if (mesh_overlay_prepare(&mesh, &lines) != -1)
return fail("prepare accepted an out-of-range vertex");
mesh.vertex_count = 3;
vertices[2].image_x = NAN;
if (mesh_overlay_prepare(&mesh, &lines) != -1)
return fail("prepare accepted a nonfinite coordinate");
vertices[2].image_x = 10;
mesh.triangles = NULL;
if (mesh_overlay_prepare(&mesh, &lines) != -1)
return fail("prepare accepted NULL triangles");
FrameLensMesh overflow = {0};
overflow.triangle_count = SIZE_MAX; /* > SIZE_MAX / 3, rejected before use */
if (mesh_overlay_prepare(&overflow, &lines) != -1)
return fail("prepare accepted an overflowing triangle count");
MeshOverlayLine line = {.x0 = 1,
.y0 = 1,
.x1 = 3,
.y1 = 1,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines one = {.lines = &line, .count = 1};
if (mesh_overlay_draw_rgb8(NULL, pixels, 4, 4, &settings) != -1)
return fail("draw accepted NULL lines");
if (mesh_overlay_draw_rgb8(&one, NULL, 4, 4, &settings) != -1)
return fail("draw accepted NULL pixels");
if (mesh_overlay_draw_rgb8(&one, pixels, 0, 4, &settings) != -1)
return fail("draw accepted zero width");
if (mesh_overlay_draw_rgb8(&one, pixels, 4, -1, &settings) != -1)
return fail("draw accepted negative height");
if (mesh_overlay_draw_rgb8(&one, pixels, 4, 4, NULL) != -1)
return fail("draw accepted NULL settings");
MeshOverlayLines null_lines = {.lines = NULL, .count = 1};
if (mesh_overlay_draw_rgb8(&null_lines, pixels, 4, 4, &settings) != -1)
return fail("draw accepted a NULL line array with a nonzero count");
/* Dimensions too near INT_MAX would overflow the raster's y+1/x+1 casts. */
if (mesh_overlay_draw_rgb8(&one, pixels, INT_MAX, 1, &settings) != -1)
return fail("draw accepted a width near INT_MAX");
if (mesh_overlay_draw_rgb8(&one, pixels, 1, INT_MAX, &settings) != -1)
return fail("draw accepted a height near INT_MAX");
/* An unallocatable line count must be rejected before the array dereference. */
MeshOverlayLines overflow_lines = {.lines = &line, .count = SIZE_MAX};
if (mesh_overlay_draw_rgb8(&overflow_lines, pixels, 4, 4, &settings) != -1)
return fail("draw accepted an overflowing line count");
MeshOverlaySettings bad = settings;
bad.opacity = NAN;
if (mesh_overlay_draw_rgb8(&one, pixels, 4, 4, &bad) != -1)
return fail("draw accepted NaN opacity");
bad.opacity = 1.5;
if (mesh_overlay_draw_rgb8(&one, pixels, 4, 4, &bad) != -1)
return fail("draw accepted opacity above 1");
bad.opacity = -0.1;
if (mesh_overlay_draw_rgb8(&one, pixels, 4, 4, &bad) != -1)
return fail("draw accepted negative opacity");
MeshOverlayLine bad_category = {.x0 = 1,
.y0 = 1,
.x1 = 3,
.y1 = 1,
.category0 = 99,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines bad_lines = {.lines = &bad_category, .count = 1};
if (mesh_overlay_draw_rgb8(&bad_lines, pixels, 4, 4, &settings) != -1)
return fail("draw accepted an out-of-range category");
MeshOverlayLine bad_coord = {.x0 = NAN,
.y0 = 1,
.x1 = 3,
.y1 = 1,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
bad_lines.lines = &bad_coord;
if (mesh_overlay_draw_rgb8(&bad_lines, pixels, 4, 4, &settings) != -1)
return fail("draw accepted a nonfinite coordinate");
MeshOverlayLines none = {.lines = NULL, .count = 0};
if (mesh_overlay_draw_rgb8(&none, pixels, 4, 4, &settings) != 0)
return fail("draw failed on an empty edge set");
return 0;
}
static int test_rgba_layer(void) {
LensVertex vertices[3] = {
make_vertex(2, 4, 1, RAY_OUTCOME_ESCAPED),
make_vertex(12, 4, 1, RAY_OUTCOME_ESCAPED),
make_vertex(2, 12, 1, RAY_OUTCOME_ESCAPED)};
LensTriangle triangles[2] = {{{0, 1, 2}, 0, 0, 0},
{{2, 1, 0}, 0, 0, 0}};
FrameLensMesh mesh = {.vertices = vertices, .vertex_count = 3,
.triangles = triangles, .triangle_count = 1};
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 0.5;
MeshOverlayLayer layer = {0}, duplicate = {0};
if (mesh_overlay_build_layer(&mesh, 16, 16, &settings, &layer))
return fail("RGBA layer build failed");
int ok = layer.width == 16 && layer.height == 16;
const unsigned char *on_edge = &layer.rgba[4 * (4 * 16 + 5)];
for (int c = 0; c < 3; ++c)
ok &= on_edge[c] == lround(settings.colors[0][c] * settings.opacity);
ok &= on_edge[3] == 128;
for (size_t p = 0; p < 16 * 16; ++p)
for (int c = 0; c < 3; ++c)
ok &= layer.rgba[4 * p + c] <= layer.rgba[4 * p + 3];
mesh.triangle_count = 2;
ok &= mesh_overlay_build_layer(&mesh, 16, 16, &settings, &duplicate) == 0;
if (duplicate.rgba != NULL)
ok &= memcmp(layer.rgba, duplicate.rgba, 16 * 16 * 4) == 0;
unsigned char image[16 * 16 * 3];
memset(image, 255, sizeof image);
ok &= mesh_overlay_composite_rgb8(&layer, image, 16, 16) == 0;
for (int c = 0; c < 3; ++c)
ok &= image[3 * (4 * 16 + 5) + c] == on_edge[c] + 127;
ok &= image[0] == 255; /* no overlay coverage, not a full-image gray tint */
mesh_overlay_layer_destroy(&layer);
mesh_overlay_layer_destroy(&duplicate);
ok &= layer.rgba == NULL && layer.width == 0 && layer.height == 0;
settings.opacity = 0.0;
ok &= mesh_overlay_build_layer(&mesh, 16, 16, &settings, &layer) == 0;
if (layer.rgba != NULL)
for (size_t i = 0; i < 16 * 16 * 4; ++i)
ok &= layer.rgba[i] == 0;
unsigned char before[sizeof image];
memcpy(before, image, sizeof image);
ok &= mesh_overlay_composite_rgb8(&layer, image, 16, 16) == 0;
ok &= memcmp(before, image, sizeof image) == 0;
ok &= mesh_overlay_composite_rgb8(&layer, image, 15, 16) == -1;
mesh_overlay_layer_destroy(&layer);
ok &= mesh_overlay_build_layer(&mesh, 0, 16, &settings, &layer) == -1;
ok &= mesh_overlay_build_layer(&mesh, INT_MAX, INT_MAX, &settings, &layer) == -1;
vertices[0].image_x = NAN;
ok &= mesh_overlay_build_layer(&mesh, 16, 16, &settings, &layer) == -1;
ok &= layer.rgba == NULL;
return ok ? 0 : fail("RGBA premultiplication/composition/ownership regression");
}
static int test_rgba_composition_extremes(void) {
unsigned char rgba[] = {0, 0, 0, 0, 20, 40, 60, 255, 10, 20, 30, 128};
const MeshOverlayLayer layer = {.rgba = rgba, .width = 3, .height = 1};
unsigned char rgb[] = {100, 110, 120, 100, 100, 100, 100, 100, 100};
const unsigned char expected[] = {100, 110, 120, 20, 40, 60, 60, 70, 80};
return mesh_overlay_composite_rgb8(&layer, rgb, 3, 1) == 0 &&
memcmp(rgb, expected, sizeof rgb) == 0
? 0 : fail("RGBA transparent/opaque/partial-alpha composition");
}
int main(void) {
if (test_default_settings() || test_parse_color() ||
test_prepare_dedup_categories() || test_untraced_category() ||
test_boundary_and_winding() || test_isolated_witness_not_drawn() ||
test_draw_category_colors() || test_halves_and_switch() ||
test_antialiasing() || test_subpixel_and_zero_length() ||
test_high_white_background() ||
test_opacity_extremes() || test_clipping_and_huge_coordinates() ||
test_extreme_magnitudes() || test_clipped_midpoint_uses_original() ||
test_invalid_arguments() || test_rgba_layer() ||
test_rgba_composition_extremes())
return 1;
return 0;
}
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Production CLI regression for post-tone-map mesh settings and replay."""
import os
from pathlib import Path
import subprocess
import sys
import tempfile
BUILD = Path(sys.argv[1] if len(sys.argv) > 1 else 'build/Release').resolve()
BINARY = BUILD / 'minkowski_sky'
ENV = dict(os.environ, OMP_NUM_THREADS='4')
TMP = Path('/tmp/opencode')
TMP.mkdir(parents=True, exist_ok=True)
COLORS = ['escape', 'dark', 'unresolved', 'incomplete', 'untraced']
COMMON = ['--catalog', 'assets/sky_grid_5deg.csv', '--width', '32', '--height',
'24', '--coarse-cell-pixels', '8', '--refine-max-level', '0',
'--exposure', '1e-3', '--psf-relative-tail', '1e-4']
def run(*args, ok=True):
result = subprocess.run([str(BINARY), *map(str, args)], env=ENV,
capture_output=True, text=True)
assert (result.returncode == 0) == ok, (args, result.stdout, result.stderr)
return result
help_text = run('--help').stdout
ext = 'png' if '.png' in help_text else 'ppm'
for category in COLORS:
assert f'--mesh-color-{category}' in help_text
for value in ['red', '123456', '#12345', '#1234567', '#GG1122', '']:
result = run(f'--mesh-color-{category}', value, ok=False)
assert result.stderr and 'Rendered' not in result.stdout
run(f'--mesh-color-{category}', ok=False)
for value in ['nan', 'inf', '-inf', '-0.1', '1.1', 'junk']:
run('--mesh-opacity', value, ok=False)
run('--mesh-opacity', ok=False)
custom = []
for category, color in zip(COLORS, ['#123ABC', '#ABC123', '#AA5533',
'#1122EE', '#112233']):
custom += [f'--mesh-color-{category}', color]
custom += ['--mesh-opacity', '0.85']
with tempfile.TemporaryDirectory(prefix='mesh-cli-', dir=TMP) as directory:
tmp = Path(directory)
baseline = tmp / f'baseline.{ext}'
run(*COMMON, '--output', baseline)
configured = tmp / f'configured.{ext}'
run(*COMMON, *custom, '--output', configured)
assert baseline.read_bytes() == configured.read_bytes()
assert not (tmp / f'configured_mesh.{ext}').exists()
for opacity in [0, 1]:
output = tmp / f'opacity{opacity}.{ext}'
run(*COMMON, *custom, '--mesh-opacity', opacity, '--draw-mesh',
'--output', output)
assert output.read_bytes() == baseline.read_bytes()
mesh = tmp / f'opacity{opacity}_mesh.{ext}'
assert (mesh.read_bytes() == baseline.read_bytes()) == (opacity == 0)
single_map = tmp / 'single.grlens'
single = tmp / f'single.{ext}'
run(*COMMON, *custom, '--draw-mesh', '--lens-map-output', single_map,
'--output', single)
replay = tmp / f'replay.{ext}'
run('--catalog', 'assets/sky_grid_5deg.csv', '--exposure', '1e-3',
'--psf-relative-tail', '1e-4', *custom, '--draw-mesh',
'--lens-map-input', single_map, '--output', replay)
assert single.read_bytes() == replay.read_bytes()
assert (tmp / f'single_mesh.{ext}').read_bytes() == (tmp / f'replay_mesh.{ext}').read_bytes()
track = tmp / 'track.csv'
run('--write-minkowski-accel-track', track, '--duration', 2, '--fps', 4,
'--proper-acceleration', 0.1)
movie_dir, replay_dir = tmp / 'movie', tmp / 'movie_replay'
movie_dir.mkdir()
replay_dir.mkdir()
movie_map = tmp / 'movie.grlens'
run(*COMMON, *custom, '--draw-mesh', '--observer-track', track,
'--duration', 1, '--fps', 1, '--frames-dir', movie_dir,
'--frames-prefix', 'frame', '--lens-map-output', movie_map)
run('--catalog', 'assets/sky_grid_5deg.csv', '--exposure', '1e-3',
'--psf-relative-tail', '1e-4', *custom, '--draw-mesh',
'--lens-map-input', movie_map, '--frames-dir', replay_dir,
'--frames-prefix', 'frame')
expected_names = {f'frame_{i:06d}{suffix}.{ext}'
for i in range(2) for suffix in ['', '_mesh']}
assert {p.name for p in movie_dir.iterdir()} == expected_names
assert {p.name for p in replay_dir.iterdir()} == expected_names
for name in expected_names:
assert (movie_dir / name).read_bytes() == (replay_dir / name).read_bytes(), name
print('mesh-overlay CLI checks passed: colors/opacity, clean fidelity, single/movie replay')
+213 -29
View File
@@ -2,12 +2,14 @@
#include "movie_output.h"
#include <errno.h>
#include <math.h>
#include <omp.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
@@ -24,6 +26,15 @@ static void check(int condition, const char *message) {
}
}
/* Scratch data lives in the pre-approved OpenCode scratch directory rather
* than directly under /tmp. */
#define TEST_TMP_ROOT "/tmp/opencode"
#ifdef ENABLE_PNG
#define TEST_IMAGE_EXT "png"
#else
#define TEST_IMAGE_EXT "ppm"
#endif
#define MAX_JOBS 64
#define JOB_WIDTH 16
#define JOB_HEIGHT 16
@@ -35,6 +46,9 @@ typedef struct {
size_t count;
size_t order[MAX_JOBS];
unsigned char pixels[MAX_JOBS][JOB_BYTES];
int mesh_draw_flags[MAX_JOBS];
size_t mesh_pixel_counts[MAX_JOBS];
unsigned char layers[MAX_JOBS][JOB_WIDTH * JOB_HEIGHT * 4];
} MockWriter;
static int mock_write(void *context, const MovieOutputJob *job,
@@ -44,6 +58,12 @@ static int mock_write(void *context, const MovieOutputJob *job,
if (mock->count < MAX_JOBS) {
mock->order[mock->count] = job->frame_id;
memcpy(mock->pixels[mock->count], job->clean_rgb8, JOB_BYTES);
mock->mesh_draw_flags[mock->count] = job->draw_mesh;
mock->mesh_pixel_counts[mock->count] = job->mesh_layer.rgba == NULL ? 0 :
(size_t)job->mesh_layer.width * job->mesh_layer.height;
if (job->mesh_layer.rgba != NULL)
memcpy(mock->layers[mock->count], job->mesh_layer.rgba,
sizeof mock->layers[mock->count]);
}
++mock->count;
if (mock->delay_ms > 0) {
@@ -76,6 +96,22 @@ static MovieOutputJob make_job(size_t frame_id) {
return job;
}
/* An independent premultiplied RGBA8 fixture with sparse nonzero pixels. */
static int make_layer(MeshOverlayLayer *layer, size_t count) {
*layer = (MeshOverlayLayer){0};
layer->rgba = calloc(JOB_WIDTH * JOB_HEIGHT, 4);
if (layer->rgba == NULL)
return -1;
layer->width = JOB_WIDTH;
layer->height = JOB_HEIGHT;
for (size_t i = 0; i < count; ++i) {
for (int channel = 0; channel < 3; ++channel)
layer->rgba[4 * i + channel] = (unsigned char)((i * 19 + channel * 3) % 129);
layer->rgba[4 * i + 3] = 128;
}
return 0;
}
/* Order and pixel fidelity for capacity 1 and 2. */
static void test_order_and_pixels(size_t capacity) {
MovieOutputQueue queue;
@@ -146,7 +182,9 @@ static void test_backpressure(void) {
}
/* Writer failure at frame N must propagate, unblock the producer, and join
* cleanly without losing the ownership contract. */
* cleanly. RGBA layers are attached to every job so both ownership paths
* are exercised: the queue releases accepted jobs, while the caller releases
* the job rejected after the failure is recorded. */
static void test_writer_failure(void) {
MovieOutputQueue queue;
MockWriter mock;
@@ -161,9 +199,16 @@ static void test_writer_failure(void) {
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. */
job.draw_mesh = 1;
if (make_layer(&job.mesh_layer, 3)) {
free(job.clean_rgb8);
check(0, "failure: layer allocation");
break;
}
if (movie_output_queue_submit(&queue, &job, NULL)) {
/* The queue no longer owns these resources. */
free(job.clean_rgb8);
mesh_overlay_layer_destroy(&job.mesh_layer);
saw_failure = 1;
break;
}
@@ -193,6 +238,76 @@ static void test_empty_paths(void) {
movie_output_queue_destroy(&queue);
}
/* The queued job owns an independent RGBA layer: rasterizing a heap mesh, then
* releasing that source mesh before the writer runs, must not disturb the
* pixels the writer observes. */
static void test_lines_survive_source_release(void) {
LensVertex *vertices = calloc(3, sizeof *vertices);
LensTriangle *triangles = calloc(1, sizeof *triangles);
if (vertices == NULL || triangles == NULL) {
free(vertices);
free(triangles);
check(0, "source release: allocation");
return;
}
vertices[0] = (LensVertex){.image_x = 1.5,
.image_y = 1.5,
.outcome = RAY_OUTCOME_ESCAPED,
.traced = 1};
vertices[1] = (LensVertex){.image_x = JOB_WIDTH - 2.5,
.image_y = 2.5,
.outcome = RAY_OUTCOME_DARK,
.traced = 1};
vertices[2] = (LensVertex){.image_x = 5.5,
.image_y = JOB_HEIGHT - 2.5,
.outcome = RAY_OUTCOME_INCOMPLETE,
.traced = 0};
triangles[0].vertex[0] = 0;
triangles[0].vertex[1] = 1;
triangles[0].vertex[2] = 2;
FrameLensMesh mesh = {.vertices = vertices,
.vertex_count = 3,
.triangles = triangles,
.triangle_count = 1};
MeshOverlayLayer layer = {0};
const MeshOverlaySettings overlay = mesh_overlay_default_settings();
const int prepared = mesh_overlay_build_layer(&mesh, JOB_WIDTH, JOB_HEIGHT,
&overlay, &layer) == 0;
/* Release the source mesh (and its vertex/triangle arrays) before submit. */
free(vertices);
free(triangles);
check(prepared, "source release: overlay extraction");
MovieOutputQueue queue;
MockWriter mock;
memset(&mock, 0, sizeof mock);
mock.fail_at = -1;
const PngWriteSettings settings = {-1};
if (movie_output_queue_init(&queue, 1, &settings)) {
check(0, "source release: queue init");
mesh_overlay_layer_destroy(&layer);
return;
}
movie_output_queue_set_writer(&queue, mock_write, &mock);
MovieOutputJob job = make_job(0);
job.draw_mesh = 1;
job.mesh_layer = layer; /* ownership transferred to the job/queue */
int submitted = job.clean_rgb8 != NULL &&
movie_output_queue_submit(&queue, &job, NULL) == 0;
if (!submitted) {
free(job.clean_rgb8);
mesh_overlay_layer_destroy(&job.mesh_layer);
}
check(submitted && movie_output_queue_finish(&queue) == 0,
"source release: submit/finish");
check(mock.count == 1 && mock.mesh_draw_flags[0] == 1 &&
mock.mesh_pixel_counts[0] == JOB_WIDTH * JOB_HEIGHT &&
memcmp(mock.layers[0], (unsigned char[JOB_WIDTH * JOB_HEIGHT * 4]){0},
sizeof mock.layers[0]) != 0,
"source release: writer saw the intact RGBA overlay");
movie_output_queue_destroy(&queue);
}
#ifdef ENABLE_PNG
static int decode_png_rgb8(const char *path, unsigned char *out, int width,
int height) {
@@ -216,12 +331,12 @@ static int decode_png_rgb8(const char *path, unsigned char *out, int width,
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";
/* The default writer's clean file must decode unchanged and its mesh sibling
* must equal the core overlay drawn in place on that same clean payload. */
static void test_default_writer_mesh(void) {
char directory[] = TEST_TMP_ROOT "/movie_output_XXXXXX";
if (mkdtemp(directory) == NULL) {
check(0, "default success: mkdtemp");
check(0, "default mesh: mkdtemp");
return;
}
char clean_path[PATH_MAX];
@@ -231,7 +346,7 @@ static void test_default_writer_success(void) {
const PngWriteSettings settings = {-1};
MovieOutputQueue queue;
if (movie_output_queue_init(&queue, 1, &settings)) {
check(0, "default success: queue init");
check(0, "default mesh: queue init");
rmdir(directory);
return;
}
@@ -239,29 +354,35 @@ static void test_default_writer_success(void) {
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 &&
int layer_ok = make_layer(&job.mesh_layer, 5) == 0;
unsigned char *clean_expected = make_rgb8(0);
unsigned char mesh_expected[JOB_BYTES];
int expected_ok = layer_ok && clean_expected != NULL;
if (expected_ok) {
memcpy(mesh_expected, clean_expected, JOB_BYTES);
expected_ok = mesh_overlay_composite_rgb8(&job.mesh_layer, mesh_expected,
JOB_WIDTH, JOB_HEIGHT) == 0;
}
int submitted = expected_ok && job.clean_rgb8 != NULL &&
movie_output_queue_submit(&queue, &job, NULL) == 0;
if (!submitted) {
free(job.clean_rgb8);
free(job.mesh_rgb8);
mesh_overlay_layer_destroy(&job.mesh_layer);
}
check(submitted && movie_output_queue_finish(&queue) == 0,
"default success: submit/finish");
"default mesh: 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");
check(decoded_ok && clean_expected != NULL &&
memcmp(clean_decoded, clean_expected, JOB_BYTES) == 0,
"default mesh: clean image unchanged");
check(decoded_ok && expected_ok &&
memcmp(mesh_decoded, mesh_expected, JOB_BYTES) == 0,
"default mesh: mesh matches core overlay result");
free(clean_expected);
free(mesh_expected);
unlink(clean_path);
unlink(mesh_path);
rmdir(directory);
@@ -269,38 +390,101 @@ static void test_default_writer_success(void) {
}
#endif
/* The default writer must fail under a real filesystem error. */
static void test_default_writer_failure(void) {
/* Failure while writing the clean image happens before any overlay work; the
* queue must still release the job's resources. */
static void test_default_writer_failure_before_overlay(void) {
MovieOutputQueue queue;
const PngWriteSettings settings = {-1};
if (movie_output_queue_init(&queue, 1, &settings)) {
check(0, "default failure: queue init");
check(0, "failure before overlay: queue init");
return;
}
MovieOutputJob job = make_job(0);
job.draw_mesh = 1;
if (make_layer(&job.mesh_layer, 4)) {
free(job.clean_rgb8);
check(0, "failure before overlay: layer allocation");
movie_output_queue_destroy(&queue);
return;
}
snprintf(job.output_path, sizeof job.output_path,
"/nonexistent-directory-xyz/frame.png");
"/nonexistent-directory-xyz/frame." TEST_IMAGE_EXT);
snprintf(job.mesh_path, sizeof job.mesh_path,
"/nonexistent-directory-xyz/frame_mesh." TEST_IMAGE_EXT);
if (job.clean_rgb8 == NULL || movie_output_queue_submit(&queue, &job, NULL)) {
free(job.clean_rgb8);
check(0, "default failure: submit");
mesh_overlay_layer_destroy(&job.mesh_layer);
check(0, "failure before overlay: submit");
movie_output_queue_destroy(&queue);
return;
}
check(movie_output_queue_finish(&queue) != 0,
"default failure: finish reports unwritable path");
"failure before overlay: clean write error propagates");
movie_output_queue_destroy(&queue);
}
/* Failure while writing the mesh image happens after the clean file is written
* and the overlay is drawn; that later error must also propagate. */
static void test_default_writer_failure_after_overlay(void) {
char directory[] = TEST_TMP_ROOT "/movie_output_XXXXXX";
if (mkdtemp(directory) == NULL) {
check(0, "failure after overlay: mkdtemp");
return;
}
char clean_path[PATH_MAX];
snprintf(clean_path, sizeof clean_path, "%s/frame_000000." TEST_IMAGE_EXT,
directory);
MovieOutputQueue queue;
const PngWriteSettings settings = {-1};
if (movie_output_queue_init(&queue, 1, &settings)) {
check(0, "failure after overlay: queue init");
rmdir(directory);
return;
}
MovieOutputJob job = make_job(0);
job.draw_mesh = 1;
if (make_layer(&job.mesh_layer, 4)) {
free(job.clean_rgb8);
check(0, "failure after overlay: layer allocation");
rmdir(directory);
movie_output_queue_destroy(&queue);
return;
}
snprintf(job.output_path, sizeof job.output_path, "%s", clean_path);
snprintf(job.mesh_path, sizeof job.mesh_path,
"/nonexistent-directory-xyz/frame_mesh." TEST_IMAGE_EXT);
if (job.clean_rgb8 == NULL || movie_output_queue_submit(&queue, &job, NULL)) {
free(job.clean_rgb8);
mesh_overlay_layer_destroy(&job.mesh_layer);
check(0, "failure after overlay: submit");
rmdir(directory);
movie_output_queue_destroy(&queue);
return;
}
check(movie_output_queue_finish(&queue) != 0,
"failure after overlay: mesh write error propagates");
unlink(clean_path);
rmdir(directory);
movie_output_queue_destroy(&queue);
}
int main(void) {
/* Standalone/CI runs need not have an OpenCode-created scratch directory. */
if (mkdir(TEST_TMP_ROOT, 0700) != 0 && errno != EEXIST) {
perror("create movie-output test scratch directory");
return 1;
}
test_order_and_pixels(1);
test_order_and_pixels(2);
test_backpressure();
test_writer_failure();
test_empty_paths();
test_lines_survive_source_release();
#ifdef ENABLE_PNG
test_default_writer_success();
test_default_writer_mesh();
#endif
test_default_writer_failure();
test_default_writer_failure_before_overlay();
test_default_writer_failure_after_overlay();
if (failures != 0) {
fprintf(stderr, "%d movie-output failure(s)\n", failures);
return 1;