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.
741 lines
31 KiB
C
741 lines
31 KiB
C
/* 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;
|
|
}
|