Feat: Rework ray termination into escaped/dark/unresolved/incomplete

Replace the position capture cutoff with a camera-relative dark threshold
shared by every backend, and carry explicit outcome/reason provenance
through the ray, RayPool, adaptive mesh, lens-map and replay paths.

- eval/eval_slab return SpacetimePointStatus; remove SPACETIME_RAY_CAPTURED
  and the Schwarzschild capture radius; decouple observer construction from
  ray position.
- RayEndpoint stores RayOutcome/RayReason plus the last trusted state;
  budget exhaustion is retryable UNRESOLVED, data/integration failures are
  INCOMPLETE.
- Normal dark terminal is L - L0 >= --dark-threshold (default 8), with L0
  taken at the camera event and kept distinct from the worldtube entry
  energy; photon energy and frequency ratio are never reset.
- Implement E/D/U triangle decisions with merged budget retries, persistent
  probe witnesses promoted in place by vertex identity, conformity settling,
  and approximate-black boundary provenance with achieved-scale statistics.
- Add RayPool continuation state and per-ray step budgets.
- Bump lens-map to v2 with explicit end/outcome/reason, approx_black,
  threshold/retry/geometry provenance and per-frame retry counts; reject v1.
- Gate production output on incomplete/error results, overridable with
  --allow-incomplete.
- Update AGENTS.md, the design document and usage docs; add the termination
  oracle and regression coverage.

make -B -j4 BUILD_TYPE=Debug test passes with bit-identical reference HDRs.
This commit is contained in:
wyj committed 2026-10-05 06:22:47 -04:00
1 parent 7fde49308b
commit f7380cbf75
31 files changed
+2349 -402

No files matched your search

+540 -69
View File
@@ -89,9 +89,9 @@ int frame_lens_mesh_build_coarse(FrameLensMesh *mesh, int width, int height,
const size_t bottom_left = vertex_index(column, row + 1, columns);
const size_t bottom_right = vertex_index(column + 1, row + 1, columns);
triangles[next_triangle++] =
(LensTriangle){{top_left, bottom_left, bottom_right}, 0, 0};
(LensTriangle){{top_left, bottom_left, bottom_right}, 0, 0, 0};
triangles[next_triangle++] =
(LensTriangle){{top_left, bottom_right, top_right}, 0, 0};
(LensTriangle){{top_left, bottom_right, top_right}, 0, 0, 0};
}
*mesh = (FrameLensMesh){.vertices = vertices,
.triangles = triangles,
@@ -102,6 +102,38 @@ int frame_lens_mesh_build_coarse(FrameLensMesh *mesh, int width, int height,
return 0;
}
/* Store an endpoint into a lens vertex. For an UNRESOLVED result the last
* accepted continuous state is kept so the ray can be resumed; `granted_limit`
* is the total accepted-step budget that produced this result (0 when it is
* the base trace config). */
static void store_endpoint(LensVertex *vertex, const RayEndpoint *endpoint,
unsigned int granted_limit) {
vertex->outcome = endpoint->outcome;
vertex->reason = endpoint->reason;
vertex->end_id = endpoint->end_id;
vertex->traced = 1;
if (endpoint->outcome == RAY_OUTCOME_ESCAPED) {
for (int axis = 0; axis < 3; ++axis)
vertex->n_infinity[axis] = endpoint->n_infinity[axis];
vertex->log_frequency_ratio = log(endpoint->frequency_ratio);
return;
}
if (endpoint->outcome == RAY_OUTCOME_UNRESOLVED) {
vertex->continuation_t = endpoint->stop_coordinate_time;
for (int axis = 0; axis < 3; ++axis) {
vertex->continuation_x[axis] = endpoint->final_x[axis];
vertex->continuation_Pi[axis] = endpoint->final_Pi[axis];
}
vertex->continuation_log_alpha_p0 = endpoint->final_log_alpha_p0;
vertex->continuation_log_alpha_p0_0 = endpoint->final_log_alpha_p0_0;
vertex->continuation_steps = endpoint->accepted_steps;
const unsigned int used =
granted_limit != 0 ? granted_limit : endpoint->accepted_steps;
if (used > vertex->continuation_limit)
vertex->continuation_limit = used;
}
}
int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime,
const ObserverState *observer,
const GeodesicTraceConfig *trace) {
@@ -115,14 +147,7 @@ int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime,
LensVertex *vertex = &mesh->vertices[i];
RayEndpoint endpoint = geodesic_trace_past(spacetime, observer,
vertex->camera_direction, trace);
vertex->status = endpoint.status;
vertex->end_id = endpoint.end_id;
vertex->traced = 1;
if (endpoint.status == RAY_ENDPOINT_ESCAPED) {
for (int axis = 0; axis < 3; ++axis)
vertex->n_infinity[axis] = endpoint.n_infinity[axis];
vertex->log_frequency_ratio = log(endpoint.frequency_ratio);
}
store_endpoint(vertex, &endpoint, 0);
}
return 0;
}
@@ -436,24 +461,54 @@ static int all_vertices_traced(const FrameLensMesh *mesh) {
return 1;
}
typedef struct {
int e, d, u, bad;
} VertexMix;
static VertexMix triangle_mix(const FrameLensMesh *mesh,
const LensTriangle *triangle) {
VertexMix mix = {0, 0, 0, 0};
for (int i = 0; i < 3; ++i) {
switch (mesh->vertices[triangle->vertex[i]].outcome) {
case RAY_OUTCOME_ESCAPED: ++mix.e; break;
case RAY_OUTCOME_DARK: ++mix.d; break;
case RAY_OUTCOME_UNRESOLVED: ++mix.u; break;
default: ++mix.bad; break;
}
}
return mix;
}
/* A discontinuous boundary that must be red-refined. Escape/dark and
* unresolved/dark (with no escape vertex) are boundaries; errors are not. */
static int terminal_mismatch(const LensVertex *a, const LensVertex *b,
const LensVertex *c) {
int escaped = 0, captured = 0, have_end = 0;
int escaped = 0, dark = 0, unresolved = 0, bad = 0, have_end = 0;
SpacetimeEndId end = SPACETIME_END_NONE;
const LensVertex *vertices[] = {a, b, c};
for (size_t i = 0; i < 3; ++i) {
escaped |= vertices[i]->status == RAY_ENDPOINT_ESCAPED;
captured |= vertices[i]->status == RAY_ENDPOINT_CAPTURED;
if (vertices[i]->status == RAY_ENDPOINT_ESCAPED) {
switch (vertices[i]->outcome) {
case RAY_OUTCOME_ESCAPED:
++escaped;
if (!have_end) {
end = vertices[i]->end_id;
have_end = 1;
} else if (vertices[i]->end_id != end) {
return 1; /* two different infinity ends must not be interpolated */
}
break;
case RAY_OUTCOME_DARK: ++dark; break;
case RAY_OUTCOME_UNRESOLVED: ++unresolved; break;
default: ++bad; break;
}
}
return escaped && captured;
if (bad > 0)
return 0;
if (escaped > 0 && dark > 0)
return 1;
if (unresolved > 0 && dark > 0 && escaped == 0)
return 1;
return 0;
}
static int add_sample(FrameLensMesh *mesh, const FrameSample *sample) {
@@ -512,6 +567,94 @@ static void index_probe(FrameLensMesh *mesh, size_t sample_id) {
mesh->probe_slots[slot] = sample_id + 1;
}
/* Rebuild the persistent witness index from the mesh vertices: a witness is a
* vertex flagged diagnostic_probe that no triangle references. Consumed
* witnesses (now formal midpoints) are un-flagged here. */
static int witness_rebuild(FrameLensMesh *mesh) {
unsigned char *used = calloc(mesh->vertex_count ? mesh->vertex_count : 1, 1);
if (used == NULL)
return -1;
for (size_t t = 0; t < mesh->triangle_count; ++t)
for (int j = 0; j < 3; ++j)
if (mesh->triangles[t].vertex[j] < mesh->vertex_count)
used[mesh->triangles[t].vertex[j]] = 1;
size_t count = 0;
for (size_t v = 0; v < mesh->vertex_count; ++v) {
if (!mesh->vertices[v].diagnostic_probe)
continue;
if (used[v]) {
mesh->vertices[v].diagnostic_probe = 0; /* promoted to a midpoint */
continue;
}
if (count == mesh->witness_capacity) {
size_t cap = mesh->witness_capacity ? mesh->witness_capacity * 2 : 8;
size_t *list = realloc(mesh->witness_vertices, cap * sizeof *list);
if (list == NULL) {
free(used);
return -1;
}
mesh->witness_vertices = list;
mesh->witness_capacity = cap;
}
mesh->witness_vertices[count++] = v;
}
mesh->witness_count = count;
mesh->diagnostic_probe_count = count;
size_t cap = 16;
while (cap < count * 2)
cap *= 2;
if (cap > mesh->witness_slot_capacity) {
size_t *slots = realloc(mesh->witness_slots, cap * sizeof *slots);
if (slots == NULL) {
free(used);
return -1;
}
mesh->witness_slots = slots;
mesh->witness_slot_capacity = cap;
}
if (mesh->witness_slot_capacity != 0)
memset(mesh->witness_slots, 0,
mesh->witness_slot_capacity * sizeof *mesh->witness_slots);
for (size_t i = 0; i < count; ++i) {
const size_t id = mesh->witness_vertices[i];
const size_t a = mesh->vertices[id].probe_edge[0];
const size_t b = mesh->vertices[id].probe_edge[1];
size_t slot = probe_hash(a, b) & (mesh->witness_slot_capacity - 1);
while (mesh->witness_slots[slot] != 0)
slot = (slot + 1) & (mesh->witness_slot_capacity - 1);
mesh->witness_slots[slot] = i + 1;
}
free(used);
return 0;
}
static size_t witness_find(const FrameLensMesh *mesh, size_t a, size_t b) {
if (a > b) { const size_t swap = a; a = b; b = swap; }
if (mesh->witness_slot_capacity == 0)
return SIZE_MAX;
size_t slot = probe_hash(a, b) & (mesh->witness_slot_capacity - 1);
while (mesh->witness_slots[slot] != 0) {
const size_t id = mesh->witness_vertices[mesh->witness_slots[slot] - 1];
if (mesh->vertices[id].probe_edge[0] == a &&
mesh->vertices[id].probe_edge[1] == b)
return id;
slot = (slot + 1) & (mesh->witness_slot_capacity - 1);
}
return SIZE_MAX;
}
static unsigned int retry_limit(unsigned int current,
const RefinementConfig *config) {
const unsigned int remaining = config->max_total_steps - current;
return current + (config->retry_step_increment < remaining
? config->retry_step_increment
: remaining);
}
static int triangle_allows_children(const FrameLensMesh *mesh,
const LensTriangle *triangle,
const RefinementConfig *config);
int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
const RefinementConfig *config) {
if (mesh == NULL || config == NULL || mesh->sample_count != 0)
@@ -528,16 +671,96 @@ int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
.vertex = mesh->vertices[i]}))
return -1;
}
int emitted_probes = 0;
/* Retry pass: merge one request per physical sample id. A triangle vertex
* that is UNRESOLVED with an escape side (or an all-U triangle) is retried,
* as is an off-mesh unresolved witness. A witness retry carries its edge so
* the refinement decision sees the updated state; both passes share
* retry_seen so a promoted witness is never requested twice. */
if (config->retry_step_increment > 0 && mesh->vertex_count > 0) {
unsigned char *retry_seen = calloc(mesh->vertex_count, 1);
if (retry_seen == NULL)
return -1;
for (size_t i = 0; i < mesh->triangle_count; ++i) {
const LensTriangle *triangle = &mesh->triangles[i];
const VertexMix mix = triangle_mix(mesh, triangle);
if (mix.bad > 0)
continue;
if (!((mix.u > 0 && mix.e > 0) || mix.u == 3))
continue;
for (int corner = 0; corner < 3; ++corner) {
const size_t v = triangle->vertex[corner];
LensVertex *vertex = &mesh->vertices[v];
if (vertex->outcome != RAY_OUTCOME_UNRESOLVED || retry_seen[v])
continue;
retry_seen[v] = 1;
if (vertex->continuation_limit >= config->max_total_steps)
continue; /* capped: reported as budget-incomplete, not retried */
const unsigned int limit = retry_limit(vertex->continuation_limit, config);
if (limit <= vertex->continuation_limit)
continue;
FrameSample retry = {.kind = FRAME_SAMPLE_RETRY,
.vertex_id = v,
.step_limit = limit,
.vertex = *vertex};
if (add_sample(mesh, &retry)) {
free(retry_seen);
return -1;
}
++mesh->retry_requests;
}
}
for (size_t i = 0; i < mesh->witness_count; ++i) {
const size_t v = mesh->witness_vertices[i];
LensVertex *vertex = &mesh->vertices[v];
if (retry_seen[v] || vertex->outcome != RAY_OUTCOME_UNRESOLVED)
continue;
retry_seen[v] = 1;
if (vertex->continuation_limit >= config->max_total_steps)
continue;
const unsigned int limit = retry_limit(vertex->continuation_limit, config);
if (limit <= vertex->continuation_limit)
continue;
FrameSample retry = {.kind = FRAME_SAMPLE_RETRY, .vertex_id = v,
.edge_vertex = {vertex->probe_edge[0],
vertex->probe_edge[1]},
.step_limit = limit, .vertex = *vertex};
if (add_sample(mesh, &retry)) {
free(retry_seen);
return -1;
}
if (mesh->probe_slot_capacity != 0)
index_probe(mesh, mesh->sample_count - 1);
emitted_probes = 1;
++mesh->retry_requests;
}
free(retry_seen);
}
if (config->max_level == 0)
/* Coarse vertices still need tracing when refinement is disabled. */
return (int)mesh->sample_count;
/* Every generation may batch newly inserted vertices with probes for its
* new leaves: probe positions depend only on image-plane geometry. Their
* endpoints are considered only after this complete generation finishes. */
* endpoints are considered only after this complete generation finishes.
* Unresolved/error triangles are handled by the retry pass or the boundary
* accounting, so they request no probes here. */
for (size_t i = 0; i < mesh->triangle_count; ++i) {
const LensTriangle *triangle = &mesh->triangles[i];
if (triangle->level >= config->max_level || triangle->evaluated)
continue;
const VertexMix mix = triangle_mix(mesh, triangle);
if (mix.bad > 0)
continue;
if (mix.u > 0) {
/* UUD/UDD may still be red-refined to locate the boundary, but only
* while the geometry can support children; otherwise it is an
* approximate-black boundary decision. U+escape and UUU are handled by
* the retry pass instead. */
const int dark_side_only = mix.e == 0 && mix.d > 0;
if (!dark_side_only ||
!triangle_allows_children(mesh, triangle, config))
continue;
}
const unsigned int first_side = longest_side(mesh, triangle);
const unsigned int side_count = terminal_mismatch(
&mesh->vertices[triangle->vertex[0]],
@@ -551,6 +774,21 @@ int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
const size_t b = triangle->vertex[(side + 1) % 3];
if (find_probe(mesh, a, b) != SIZE_MAX)
continue;
/* A persistent witness (terminal or capped unresolved) is reused in
* place: no retrace and, if its edge is later split, no duplicate. */
const size_t wid = witness_find(mesh, a, b);
if (wid != SIZE_MAX) {
FrameSample cached = {.kind = FRAME_SAMPLE_PROBE,
.vertex_id = wid,
.edge_vertex = {a, b},
.cached = 1,
.vertex = mesh->vertices[wid]};
if (add_sample(mesh, &cached))
return -1;
index_probe(mesh, mesh->sample_count - 1);
emitted_probes = 1;
continue;
}
FrameSample probe = {.kind = FRAME_SAMPLE_PROBE, .edge_vertex = {a, b}};
const LensVertex *left = &mesh->vertices[a];
const LensVertex *right = &mesh->vertices[b];
@@ -564,9 +802,10 @@ int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
if (add_sample(mesh, &probe))
return -1;
index_probe(mesh, mesh->sample_count - 1);
emitted_probes = 1;
}
}
mesh->samples_include_probes = mesh->sample_count != 0;
mesh->samples_include_probes = emitted_probes;
return (int)mesh->sample_count;
}
@@ -582,17 +821,12 @@ int frame_lens_mesh_install_sample(FrameLensMesh *mesh, size_t sample_id,
if (mesh == NULL || endpoint == NULL || sample_id >= mesh->sample_count)
return -1;
FrameSample *sample = &mesh->samples[sample_id];
LensVertex *vertex = sample->kind == FRAME_SAMPLE_VERTEX
? &mesh->vertices[sample->vertex_id]
: &sample->vertex;
vertex->status = endpoint->status;
vertex->end_id = endpoint->end_id;
vertex->traced = 1;
if (endpoint->status == RAY_ENDPOINT_ESCAPED) {
for (int axis = 0; axis < 3; ++axis)
vertex->n_infinity[axis] = endpoint->n_infinity[axis];
vertex->log_frequency_ratio = log(endpoint->frequency_ratio);
}
LensVertex *vertex = sample->kind == FRAME_SAMPLE_PROBE
? &sample->vertex
: &mesh->vertices[sample->vertex_id];
store_endpoint(vertex, endpoint, sample->step_limit);
if (sample->kind == FRAME_SAMPLE_RETRY)
sample->vertex = *vertex; /* witness retries are read back by find_probe */
return 0;
}
@@ -618,8 +852,8 @@ static int discrete_jacobian(const FrameLensMesh *mesh,
const LensVertex *a = &mesh->vertices[triangle->vertex[0]];
const LensVertex *b = &mesh->vertices[triangle->vertex[1]];
const LensVertex *c = &mesh->vertices[triangle->vertex[2]];
if (a->status != RAY_ENDPOINT_ESCAPED || b->status != RAY_ENDPOINT_ESCAPED ||
c->status != RAY_ENDPOINT_ESCAPED)
if (a->outcome != RAY_OUTCOME_ESCAPED || b->outcome != RAY_OUTCOME_ESCAPED ||
c->outcome != RAY_OUTCOME_ESCAPED)
return 0;
if (a->end_id != b->end_id || a->end_id != c->end_id)
return 0;
@@ -651,16 +885,22 @@ static int probe_requires_split(const FrameLensMesh *mesh,
if (probe_id == SIZE_MAX)
return 0;
const LensVertex *probe = &mesh->samples[probe_id].vertex;
if ((probe->status == RAY_ENDPOINT_ESCAPED) !=
(a->status == RAY_ENDPOINT_ESCAPED) ||
(probe->status == RAY_ENDPOINT_ESCAPED) !=
(b->status == RAY_ENDPOINT_ESCAPED))
return (probe->status == RAY_ENDPOINT_ESCAPED ||
a->status == RAY_ENDPOINT_ESCAPED || b->status == RAY_ENDPOINT_ESCAPED) &&
(probe->status == RAY_ENDPOINT_CAPTURED ||
a->status == RAY_ENDPOINT_CAPTURED || b->status == RAY_ENDPOINT_CAPTURED);
if (a->status != RAY_ENDPOINT_ESCAPED || b->status != RAY_ENDPOINT_ESCAPED ||
probe->status != RAY_ENDPOINT_ESCAPED)
if (probe->outcome == RAY_OUTCOME_INCOMPLETE ||
probe->outcome == RAY_OUTCOME_UNRESOLVED)
return 0; /* retained as a witness and retried, not a mapping estimate */
if ((probe->outcome == RAY_OUTCOME_ESCAPED) !=
(a->outcome == RAY_OUTCOME_ESCAPED) ||
(probe->outcome == RAY_OUTCOME_ESCAPED) !=
(b->outcome == RAY_OUTCOME_ESCAPED))
return (probe->outcome == RAY_OUTCOME_ESCAPED ||
a->outcome == RAY_OUTCOME_ESCAPED ||
b->outcome == RAY_OUTCOME_ESCAPED) &&
(probe->outcome == RAY_OUTCOME_DARK ||
a->outcome == RAY_OUTCOME_DARK ||
b->outcome == RAY_OUTCOME_DARK);
if (a->outcome != RAY_OUTCOME_ESCAPED ||
b->outcome != RAY_OUTCOME_ESCAPED ||
probe->outcome != RAY_OUTCOME_ESCAPED)
return 0;
double predicted[3] = {a->n_infinity[0] + b->n_infinity[0],
a->n_infinity[1] + b->n_infinity[1],
@@ -686,7 +926,72 @@ static int triangle_allows_children(const FrameLensMesh *mesh,
const double edge = fmax(image_edge_length(a, b),
fmax(image_edge_length(b, c), image_edge_length(c, a)));
const double area = image_triangle_area(a, b, c);
return edge > config->min_edge_pixels && area > config->min_area_pixels2;
return triangle->level < config->max_level &&
edge > config->min_edge_pixels && area > config->min_area_pixels2;
}
void frame_lens_mesh_boundary_stats(FrameLensMesh *mesh,
const RefinementConfig *config,
FrameBoundaryStats *stats) {
if (stats == NULL)
return;
*stats = (FrameBoundaryStats){0};
if (mesh == NULL || config == NULL)
return;
/* Off-mesh probes are samples too. Their failures cannot disappear just
* because no inverse patch uses them. Infer orphanhood for replay as well. */
unsigned char *used = calloc(mesh->vertex_count, 1);
if (used == NULL) { ++stats->error; return; }
for (size_t i = 0; i < mesh->triangle_count; ++i)
for (int j = 0; j < 3; ++j) used[mesh->triangles[i].vertex[j]] = 1;
for (size_t i = 0; i < mesh->vertex_count; ++i) {
if (!used[i] && mesh->vertices[i].outcome == RAY_OUTCOME_INCOMPLETE)
++stats->error;
if (!used[i] && mesh->vertices[i].outcome == RAY_OUTCOME_UNRESOLVED)
++stats->budget_incomplete_triangles;
}
free(used);
for (size_t i = 0; i < mesh->triangle_count; ++i) {
LensTriangle *triangle = &mesh->triangles[i];
triangle->approx_black = 0;
const VertexMix mix = triangle_mix(mesh, triangle);
if (mix.bad > 0) {
++stats->error;
continue;
}
const int unresolved_with_escape = mix.u > 0 && mix.e > 0;
const int all_unresolved = mix.u == 3;
if (mix.u == 0) {
if (mix.d == 0)
++stats->escaped_only;
else if (mix.e == 0)
++stats->dark_only;
else
++stats->eed_edd;
} else if (unresolved_with_escape) {
++stats->u_with_escape;
} else if (all_unresolved) {
++stats->uuu;
} else {
++stats->uud_udd; /* UUD / UDD */
if (!triangle_allows_children(mesh, triangle, config)) {
triangle->approx_black = 1;
++stats->approx_black_triangles;
const LensVertex *a = &mesh->vertices[triangle->vertex[0]];
const LensVertex *b = &mesh->vertices[triangle->vertex[1]];
const LensVertex *c = &mesh->vertices[triangle->vertex[2]];
stats->approx_black_area_pixels2 += image_triangle_area(a, b, c);
stats->approx_black_max_edge_pixels = fmax(stats->approx_black_max_edge_pixels,
fmax(image_edge_length(a,b), fmax(image_edge_length(b,c), image_edge_length(c,a))));
stats->approx_black_max_area_pixels2 = fmax(stats->approx_black_max_area_pixels2,
image_triangle_area(a,b,c));
if (triangle->level >= config->max_level) ++stats->approx_black_level_stops;
} else ++stats->budget_incomplete_triangles;
}
if (unresolved_with_escape || all_unresolved)
++stats->budget_incomplete_triangles;
}
stats->retry_requests = mesh->retry_requests;
}
static LensVertex midpoint_vertex(const LensVertex *a, const LensVertex *b) {
@@ -727,7 +1032,8 @@ static int append_triangle(LensTriangle *triangles, size_t *count,
unsigned int level, int evaluated) {
if (*count >= capacity)
return -1;
triangles[(*count)++] = (LensTriangle){{a, b, c}, level, evaluated};
triangles[(*count)++] =
(LensTriangle){{a, b, c}, level, evaluated, 0};
return 0;
}
@@ -756,10 +1062,75 @@ static int append_triangle_with_parent_winding(
evaluated);
}
/* Append off-mesh failed/unresolved probes as persistent witnesses and
* rebuild the witness index. `consumed[i]` marks a probe sample that became
* a formal midpoint this generation. */
static int promote_witnesses(FrameLensMesh *mesh,
const unsigned char *consumed) {
int added = 0;
for (size_t i = 0; i < mesh->sample_count; ++i) {
const FrameSample *s = &mesh->samples[i];
if (s->cached || s->kind != FRAME_SAMPLE_PROBE)
continue;
if (consumed != NULL && consumed[i])
continue;
if (s->vertex.outcome != RAY_OUTCOME_INCOMPLETE &&
s->vertex.outcome != RAY_OUTCOME_UNRESOLVED)
continue;
if (ensure_vertices(mesh, mesh->vertex_count + 1))
return -1;
LensVertex witness = s->vertex;
witness.diagnostic_probe = 1;
size_t a = s->edge_vertex[0], b = s->edge_vertex[1];
if (a > b) { const size_t swap = a; a = b; b = swap; }
witness.probe_edge[0] = a;
witness.probe_edge[1] = b;
mesh->vertices[mesh->vertex_count++] = witness;
++added;
}
/* Rebuild only when the witness set can have changed; the common no-witness
* refinement path stays O(T) without an O(V) scan. */
if (added == 0 && mesh->witness_count == 0)
return 0;
return witness_rebuild(mesh);
}
int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
const RefinementConfig *config) {
if (mesh == NULL || config == NULL || mesh->sample_count == 0)
return -1;
/* Serial decision invalidation: installation is an OpenMP bulk loop and
* must not write shared triangle flags from individual ray workers. Mark
* retried vertex ids once, then make a single pass over the mesh; witness
* edges are resolved through the O(1) witness index rather than scanning all
* retries for every triangle. */
unsigned char *vertex_retried =
calloc(mesh->vertex_count ? mesh->vertex_count : 1, 1);
if (vertex_retried == NULL)
return -1;
for (size_t j = 0; j < mesh->sample_count; ++j) {
const FrameSample *s = &mesh->samples[j];
if (s->kind == FRAME_SAMPLE_RETRY && s->vertex_id < mesh->vertex_count)
vertex_retried[s->vertex_id] = 1;
}
for (size_t i = 0; i < mesh->triangle_count; ++i) {
LensTriangle *t = &mesh->triangles[i];
int touches = vertex_retried[t->vertex[0]] ||
vertex_retried[t->vertex[1]] ||
vertex_retried[t->vertex[2]];
for (unsigned int side = 0; side < 3 && !touches; ++side) {
const size_t wid = witness_find(mesh, t->vertex[side],
t->vertex[(side + 1) % 3]);
if (wid != SIZE_MAX && wid < mesh->vertex_count &&
vertex_retried[wid])
touches = 1;
}
if (touches) {
t->evaluated = 0;
t->approx_black = 0;
}
}
free(vertex_retried);
for (size_t i = 0; i < mesh->sample_count; ++i)
if (!mesh->samples[i].vertex.traced &&
mesh->samples[i].kind == FRAME_SAMPLE_PROBE)
@@ -769,12 +1140,31 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
mesh->samples_include_probes = 0;
return 0;
}
for (size_t i = 0; i < mesh->triangle_count; ++i)
if (mesh->triangles[i].level < config->max_level)
mesh->triangles[i].evaluated = 1;
/* A triangle with a completed (or capped) probe can settle; one whose probe
* is still being retried must stay pending so the next generation sees the
* updated state. */
for (size_t i = 0; i < mesh->triangle_count; ++i) {
LensTriangle *t = &mesh->triangles[i];
const VertexMix mix = triangle_mix(mesh, t);
if (mix.u || mix.bad)
continue;
const unsigned side = longest_side(mesh, t);
const size_t probe =
find_probe(mesh, t->vertex[side], t->vertex[(side + 1) % 3]);
if (probe == SIZE_MAX)
continue;
const FrameSample *ps = &mesh->samples[probe];
if (ps->vertex.outcome == RAY_OUTCOME_UNRESOLVED && !ps->cached)
continue; /* retry in flight */
t->evaluated = 1;
}
const size_t edge_count = mesh->triangle_count * 3;
MeshEdge *edges = calloc(edge_count, sizeof *edges);
unsigned char *requested = calloc(edge_count, sizeof *requested);
/* `wanted` records that a triangle asked to split before conformity may
* cancel its edges, so a fully blocked triangle can settle instead of
* re-requesting the same probes forever. */
unsigned char *wanted = calloc(mesh->triangle_count, sizeof *wanted);
unsigned char *allowed = calloc(mesh->triangle_count, sizeof *allowed);
signed char *parity = calloc(mesh->triangle_count, sizeof *parity);
double *jacobians = calloc(mesh->triangle_count, sizeof *jacobians);
@@ -782,10 +1172,12 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
* Keep that relation in the original triangle-side order so child emission
* stays O(T), rather than scanning every sorted edge for every child side. */
size_t *side_midpoints = malloc(edge_count * sizeof *side_midpoints);
if (edges == NULL || requested == NULL || allowed == NULL || parity == NULL ||
jacobians == NULL || side_midpoints == NULL) {
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
free(side_midpoints);
unsigned char *consumed = calloc(mesh->sample_count ? mesh->sample_count : 1, 1);
if (edges == NULL || requested == NULL || wanted == NULL || allowed == NULL ||
parity == NULL || jacobians == NULL || side_midpoints == NULL ||
consumed == NULL) {
free(edges); free(requested); free(wanted); free(allowed); free(parity);
free(jacobians); free(side_midpoints); free(consumed);
return -1;
}
for (size_t i = 0; i < edge_count; ++i)
@@ -814,6 +1206,8 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
probe_requires_split(mesh, triangle, side, config);
}
(void)discrete_jacobian(mesh, triangle, &jacobians[i], &parity[i]);
for (unsigned int side = 0; side < 3; ++side)
wanted[i] |= requested[3 * i + side] != 0;
}
}
qsort(edges, edge_count, sizeof *edges, compare_mesh_edge);
@@ -834,6 +1228,7 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
if (allowed[left] && allowed[right]) {
requested[3 * left + edges[first].side] = 1;
requested[3 * right + edges[first + 1].side] = 1;
wanted[left] = wanted[right] = 1;
}
}
}
@@ -864,11 +1259,32 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
size_t split_edges = 0;
for (size_t i = 0; i < edge_count; ++i)
split_edges += requested[3 * edges[i].triangle + edges[i].side] != 0;
/* A triangle whose requested split was fully cancelled by conformity or a
* geometric limit must settle here, or it re-requests the same probes every
* generation forever. A UUD/UDD blocked while geometry still allows is
* counted budget-incomplete by boundary_stats; at the stop scale it is an
* approximate-black boundary. */
for (size_t i = 0; i < mesh->triangle_count; ++i) {
if (!wanted[i] || !allowed[i])
continue;
int still = 0;
for (unsigned int side = 0; side < 3; ++side)
still |= requested[3 * i + side] != 0;
if (!still) {
mesh->triangles[i].evaluated = 1;
mesh->triangles[i].approx_black = 0;
}
}
if (split_edges == 0) {
if (promote_witnesses(mesh, NULL)) {
free(edges); free(requested); free(wanted); free(allowed); free(parity);
free(jacobians); free(side_midpoints); free(consumed);
return -1;
}
mesh->sample_count = 0;
mesh->samples_include_probes = 0;
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
free(side_midpoints);
free(edges); free(requested); free(wanted); free(allowed); free(parity);
free(jacobians); free(side_midpoints); free(consumed);
return 0;
}
/* Allocate a single stable midpoint vertex for each requested edge group. */
@@ -885,8 +1301,8 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
first = last;
}
if (ensure_vertices(mesh, mesh->vertex_count + midpoint_count)) {
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
free(side_midpoints);
free(edges); free(requested); free(wanted); free(allowed); free(parity);
free(jacobians); free(side_midpoints); free(consumed);
return -1;
}
size_t next_vertex = mesh->vertex_count;
@@ -900,21 +1316,40 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
any |= requested[3 * edges[i].triangle + edges[i].side] != 0;
if (any) {
const size_t probe_id = find_probe(mesh, edges[first].a, edges[first].b);
LensVertex midpoint = midpoint_vertex(&mesh->vertices[edges[first].a],
&mesh->vertices[edges[first].b]);
if (probe_id != SIZE_MAX)
midpoint = mesh->samples[probe_id].vertex;
mesh->vertices[next_vertex++] = midpoint;
size_t midpoint_id;
const size_t wid = witness_find(mesh, edges[first].a, edges[first].b);
if (wid != SIZE_MAX && wid < mesh->vertex_count &&
mesh->vertices[wid].diagnostic_probe) {
/* Promote the existing off-mesh witness in place: one physical sample
* keeps a single stable vertex id. This must key on the persistent
* vertex identity, not on the generating sample being a cached PROBE:
* a witness retry is a FRAME_SAMPLE_RETRY and can trigger the split in
* the same generation. */
midpoint_id = wid;
mesh->vertices[wid].diagnostic_probe = 0;
if (probe_id != SIZE_MAX)
consumed[probe_id] = 1;
} else {
LensVertex midpoint = midpoint_vertex(&mesh->vertices[edges[first].a],
&mesh->vertices[edges[first].b]);
if (probe_id != SIZE_MAX) {
midpoint = mesh->samples[probe_id].vertex;
consumed[probe_id] = 1;
}
midpoint.diagnostic_probe = 0;
midpoint_id = next_vertex;
mesh->vertices[next_vertex++] = midpoint;
}
for (size_t i = first; i < last; ++i)
side_midpoints[3 * edges[i].triangle + edges[i].side] = next_vertex - 1;
side_midpoints[3 * edges[i].triangle + edges[i].side] = midpoint_id;
}
first = last;
}
const size_t old_count = mesh->triangle_count;
LensTriangle *children = calloc(old_count * 4, sizeof *children);
if (children == NULL) {
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
free(side_midpoints);
free(edges); free(requested); free(wanted); free(allowed); free(parity);
free(jacobians); free(side_midpoints); free(consumed);
return -1;
}
size_t child_count = 0;
@@ -985,11 +1420,24 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
mesh->triangle_count = child_count;
mesh->triangle_capacity = old_count * 4;
mesh->vertex_count = next_vertex;
const int promote_failed = promote_witnesses(mesh, consumed);
mesh->sample_count = 0;
mesh->samples_include_probes = 0;
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
free(side_midpoints);
return (int)midpoint_count;
free(edges); free(requested); free(wanted); free(allowed); free(parity);
free(jacobians); free(side_midpoints); free(consumed);
return promote_failed ? -1 : (int)midpoint_count;
}
void frame_retry_config_defaults(RefinementConfig *config,
const GeodesicTraceConfig *trace) {
if (config == NULL || trace == NULL)
return;
if (config->retry_step_increment == 0 && config->max_total_steps == 0) {
config->retry_step_increment = trace->max_steps;
config->max_total_steps =
trace->max_steps > (UINT_MAX / 4u) ? trace->max_steps
: trace->max_steps * 4u;
}
}
int frame_lens_mesh_refine(FrameLensMesh *mesh,
@@ -1009,8 +1457,10 @@ int frame_lens_mesh_refine_with_progress(
if (mesh == NULL || spacetime == NULL || observer == NULL || trace == NULL ||
config == NULL)
return -1;
RefinementConfig effective = *config;
frame_retry_config_defaults(&effective, trace);
for (size_t generation = 0;; ++generation) {
const int requested = frame_lens_mesh_prepare_generation(mesh, config);
const int requested = frame_lens_mesh_prepare_generation(mesh, &effective);
if (requested < 0) return -1;
if (requested == 0) return 0;
if (callback != NULL)
@@ -1019,12 +1469,31 @@ int frame_lens_mesh_refine_with_progress(
#pragma omp parallel for schedule(static)
for (size_t i = 0; i < mesh->sample_count; ++i) {
const FrameSample *sample = &mesh->samples[i];
const RayEndpoint endpoint = geodesic_trace_past(
spacetime, observer, sample->vertex.camera_direction, trace);
if (sample->cached) continue;
RayEndpoint endpoint;
if (sample->kind == FRAME_SAMPLE_RETRY) {
const LensVertex *v = &sample->vertex;
GeodesicRayState state = {
.coordinate_time = v->continuation_t,
.x = {v->continuation_x[0], v->continuation_x[1],
v->continuation_x[2]},
.Pi = {v->continuation_Pi[0], v->continuation_Pi[1],
v->continuation_Pi[2]},
.log_alpha_p0 = v->continuation_log_alpha_p0,
.log_alpha_p0_0 = v->continuation_log_alpha_p0_0,
.steps = v->continuation_steps};
GeodesicTraceConfig retry_config = *trace;
retry_config.max_steps = sample->step_limit;
endpoint = geodesic_trace_past_from_state(spacetime, &state,
&retry_config);
} else {
endpoint = geodesic_trace_past(spacetime, observer,
sample->vertex.camera_direction, trace);
}
/* Each request has a distinct destination vertex or probe slot. */
(void)frame_lens_mesh_install_sample(mesh, i, &endpoint);
}
const int added = frame_lens_mesh_finish_generation(mesh, config);
const int added = frame_lens_mesh_finish_generation(mesh, &effective);
if (added < 0) return -1;
if (callback != NULL)
callback(context, generation, 0, mesh->vertex_count, mesh->triangle_count,
@@ -1088,7 +1557,7 @@ static int usable_triangle(const FrameLensMesh *mesh,
const LensVertex *vertices[3]) {
for (int i = 0; i < 3; ++i) {
vertices[i] = &mesh->vertices[triangle->vertex[i]];
if (vertices[i]->status != RAY_ENDPOINT_ESCAPED)
if (vertices[i]->outcome != RAY_OUTCOME_ESCAPED)
return 0;
}
return spherical_area(vertices[0]->n_infinity, vertices[1]->n_infinity,
@@ -1989,5 +2458,7 @@ void frame_lens_mesh_destroy(FrameLensMesh *mesh) {
free(mesh->triangles);
free(mesh->samples);
free(mesh->probe_slots);
free(mesh->witness_slots);
free(mesh->witness_vertices);
*mesh = (FrameLensMesh){0};
}