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:
1 parent
7fde49308b
commit
f7380cbf75
31 files changed
+2349
-402
No files matched your search
+540
-69
@@ -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};
|
||||
}
|
||||
Reference in new issue
Block a user