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
+16
-4
@@ -539,6 +539,7 @@ AsymptoticStatus asymptotic_route_camera(const SpacetimeSource *source,
|
||||
route->Pi[i] = state.Pi[i];
|
||||
}
|
||||
route->log_alpha_p0 = state.log_alpha_p0;
|
||||
route->log_alpha_p0_camera = state.log_alpha_p0;
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
/* A backend that declares ends must describe them consistently and use a
|
||||
@@ -587,6 +588,7 @@ AsymptoticStatus asymptotic_route_camera(const SpacetimeSource *source,
|
||||
route->Pi[k] = state.Pi[k];
|
||||
}
|
||||
route->log_alpha_p0 = state.log_alpha_p0;
|
||||
route->log_alpha_p0_camera = state.log_alpha_p0;
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
}
|
||||
@@ -603,8 +605,13 @@ AsymptoticStatus asymptotic_route_camera(const SpacetimeSource *source,
|
||||
return ASYMPTOTIC_INVALID;
|
||||
if (first_end == SPACETIME_END_NONE)
|
||||
first_end = end.end_id;
|
||||
if (end.exterior_kind == ASYMPTOTIC_EXTERIOR_SCHWARZSCHILD_MONOPOLE)
|
||||
return schwarzschild_route(source, &end, &metric, &state, route);
|
||||
if (end.exterior_kind == ASYMPTOTIC_EXTERIOR_SCHWARZSCHILD_MONOPOLE) {
|
||||
const AsymptoticStatus status =
|
||||
schwarzschild_route(source, &end, &metric, &state, route);
|
||||
if (status == ASYMPTOTIC_OK)
|
||||
route->log_alpha_p0_camera = state.log_alpha_p0;
|
||||
return status;
|
||||
}
|
||||
if (end.exterior_kind != ASYMPTOTIC_EXTERIOR_MINKOWSKI)
|
||||
return ASYMPTOTIC_UNSUPPORTED;
|
||||
if (asymptotic_canonical_from_backend(source, end.end_id, &metric,
|
||||
@@ -639,6 +646,7 @@ AsymptoticStatus asymptotic_route_camera(const SpacetimeSource *source,
|
||||
if (have_entry) {
|
||||
*route = best;
|
||||
route->log_alpha_p0 = state.log_alpha_p0;
|
||||
route->log_alpha_p0_camera = state.log_alpha_p0;
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
if (!have_miss) {
|
||||
@@ -678,8 +686,10 @@ AsymptoticStatus asymptotic_finish_escape(const SpacetimeSource *source,
|
||||
endpoint->n_infinity[i] = n_inf[i];
|
||||
endpoint->frequency_ratio = frequency;
|
||||
endpoint->end_id = end_id;
|
||||
endpoint->status = RAY_ENDPOINT_ESCAPED;
|
||||
endpoint->outcome = RAY_OUTCOME_ESCAPED;
|
||||
endpoint->reason = RAY_REASON_NONE;
|
||||
endpoint->magnification = 1.0;
|
||||
endpoint->threshold_value = NAN;
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
if (end.exterior_kind != ASYMPTOTIC_EXTERIOR_MINKOWSKI)
|
||||
@@ -704,7 +714,9 @@ AsymptoticStatus asymptotic_finish_escape(const SpacetimeSource *source,
|
||||
endpoint->n_infinity[i] = n[i];
|
||||
endpoint->frequency_ratio = 1.0 / energy;
|
||||
endpoint->end_id = end_id;
|
||||
endpoint->status = RAY_ENDPOINT_ESCAPED;
|
||||
endpoint->outcome = RAY_OUTCOME_ESCAPED;
|
||||
endpoint->reason = RAY_REASON_NONE;
|
||||
endpoint->magnification = 1.0;
|
||||
endpoint->threshold_value = NAN;
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
@@ -40,7 +40,11 @@ typedef struct {
|
||||
double activate_t;
|
||||
double x[3];
|
||||
double Pi[3];
|
||||
/* Current L at the activation event (camera when inside, entry event when
|
||||
* externing). `log_alpha_p0_camera` is the reference L at the camera event
|
||||
* used by the camera-relative dark threshold, and must be kept separate. */
|
||||
double log_alpha_p0;
|
||||
double log_alpha_p0_camera;
|
||||
/* Terminal infinity endpoint for ESCAPED. */
|
||||
double n_infinity[3];
|
||||
double frequency_ratio;
|
||||
|
||||
+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};
|
||||
}
|
||||
+73
-2
@@ -14,17 +14,34 @@ typedef struct {
|
||||
double camera_direction[3];
|
||||
double n_infinity[3];
|
||||
double log_frequency_ratio;
|
||||
RayEndpointStatus status;
|
||||
RayOutcome outcome;
|
||||
RayReason reason;
|
||||
/* Asymptotic end this escaped vertex belongs to; a triangle must not
|
||||
* interpolate across two different ends. */
|
||||
SpacetimeEndId end_id;
|
||||
int traced;
|
||||
/* Retry continuation, valid when outcome == RAY_OUTCOME_UNRESOLVED: resume
|
||||
* from this last accepted state instead of replaying the ray. */
|
||||
double continuation_t;
|
||||
double continuation_x[3];
|
||||
double continuation_Pi[3];
|
||||
double continuation_log_alpha_p0;
|
||||
double continuation_log_alpha_p0_0;
|
||||
unsigned int continuation_steps;
|
||||
unsigned int continuation_limit;
|
||||
/* Persistent off-mesh probe witness; also participates in completion checks.
|
||||
* When set, probe_edge holds the sorted edge (a,b) this witness samples. */
|
||||
int diagnostic_probe;
|
||||
size_t probe_edge[2];
|
||||
} LensVertex;
|
||||
|
||||
typedef struct {
|
||||
size_t vertex[3];
|
||||
unsigned int level;
|
||||
int evaluated;
|
||||
/* Set when a boundary triangle containing UNRESOLVED vertices was blackened
|
||||
* as a finite-resolution approximation rather than resolved. */
|
||||
int approx_black;
|
||||
} LensTriangle;
|
||||
|
||||
/* Per-frame staged wall-clock breakdown for one movie frame. All fields are
|
||||
@@ -54,20 +71,51 @@ typedef struct {
|
||||
double jacobian_minimum;
|
||||
double min_edge_pixels;
|
||||
double min_area_pixels2;
|
||||
/* Retry budget for UNRESOLVED vertices. retry_step_increment == 0 disables
|
||||
* retry. max_total_steps is the per-ray hard cap on accepted steps; when a
|
||||
* UUU / escape-containing triangle reaches it, the frame is reported as
|
||||
* budget-incomplete instead of silently blackened. */
|
||||
unsigned int retry_step_increment;
|
||||
unsigned int max_total_steps;
|
||||
} RefinementConfig;
|
||||
|
||||
typedef enum {
|
||||
FRAME_SAMPLE_VERTEX,
|
||||
FRAME_SAMPLE_PROBE
|
||||
FRAME_SAMPLE_PROBE,
|
||||
FRAME_SAMPLE_RETRY
|
||||
} FrameSampleKind;
|
||||
|
||||
typedef struct {
|
||||
FrameSampleKind kind;
|
||||
size_t vertex_id;
|
||||
size_t edge_vertex[2];
|
||||
/* For FRAME_SAMPLE_RETRY: the new total accepted-step budget. */
|
||||
unsigned int step_limit;
|
||||
int cached;
|
||||
LensVertex vertex;
|
||||
} FrameSample;
|
||||
|
||||
/* E/D/U triangle accounting for one finalized mesh. Counts use the
|
||||
* rendering categories: E=ESCAPED, D=DARK, U=UNRESOLVED; triangles
|
||||
* containing an INCOMPLETE vertex are counted as errors and are never
|
||||
* blackened. */
|
||||
typedef struct {
|
||||
size_t escaped_only; /* EEE */
|
||||
size_t dark_only; /* DDD */
|
||||
size_t eed_edd; /* EED / EDD boundary */
|
||||
size_t uud_udd; /* UUD / UDD */
|
||||
size_t u_with_escape; /* UEE / UED / UUE */
|
||||
size_t uuu; /* UUU */
|
||||
size_t error; /* any INCOMPLETE vertex */
|
||||
size_t approx_black_triangles;
|
||||
double approx_black_area_pixels2;
|
||||
double approx_black_max_edge_pixels;
|
||||
double approx_black_max_area_pixels2;
|
||||
size_t approx_black_level_stops;
|
||||
size_t retry_requests;
|
||||
size_t budget_incomplete_triangles;
|
||||
} FrameBoundaryStats;
|
||||
|
||||
typedef struct {
|
||||
LensVertex *vertices;
|
||||
LensTriangle *triangles;
|
||||
@@ -80,6 +128,18 @@ typedef struct {
|
||||
int samples_include_probes;
|
||||
size_t *probe_slots;
|
||||
size_t probe_slot_capacity;
|
||||
/* Cumulative count of retry rays requested across all generations. */
|
||||
size_t retry_requests;
|
||||
/* Persistent off-mesh probe witnesses, keyed by their edge (sorted vertex
|
||||
* ids). Slot value is witness_vertex_id + 1; 0 is empty. Witnesses are
|
||||
* promoted in place to a formal midpoint when their edge is later split, so
|
||||
* one physical sample always has one stable vertex id. */
|
||||
size_t *witness_slots;
|
||||
size_t witness_slot_capacity;
|
||||
/* Compact list of live witness vertex ids for rehashing and accounting. */
|
||||
size_t *witness_vertices;
|
||||
size_t witness_count, witness_capacity;
|
||||
size_t diagnostic_probe_count;
|
||||
} FrameLensMesh;
|
||||
|
||||
typedef enum {
|
||||
@@ -143,6 +203,17 @@ int frame_lens_mesh_refine_with_progress(
|
||||
const ObserverState *observer, const GeodesicTraceConfig *trace,
|
||||
const RefinementConfig *config, FrameRefinementProgressCallback callback,
|
||||
void *context);
|
||||
/* Recompute per-triangle approximate-black provenance and E/D/U accounting
|
||||
* from the finalized mesh. Call after refinement has converged. Leaves the
|
||||
* shared UNRESOLVED vertices untouched. */
|
||||
void frame_lens_mesh_boundary_stats(FrameLensMesh *mesh,
|
||||
const RefinementConfig *config,
|
||||
FrameBoundaryStats *stats);
|
||||
/* Fill retry defaults (derived from the trace step budget) when the caller
|
||||
* did not configure them explicitly. A zero max_total_steps disables retry
|
||||
* only if retry_step_increment is also zero. */
|
||||
void frame_retry_config_defaults(RefinementConfig *config,
|
||||
const GeodesicTraceConfig *trace);
|
||||
|
||||
/* Whether frame_splat_catalog() should run the per-frame catalog prefetch or
|
||||
* rely on a movie-level union prefetch that already completed. */
|
||||
|
||||
+231
-72
@@ -30,14 +30,19 @@ static int invert(double a[3][3], double b[3][3]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Equation (4) and (5) of Bohn et al., arXiv:1410.7775. */
|
||||
static int rhs(const MetricSlab *slab, double t, const State *s,
|
||||
Derivative *out) {
|
||||
/* Equation (4) and (5) of Bohn et al., arXiv:1410.7775. Returns a metric/data
|
||||
* status so the integrator can report why a step failed instead of collapsing
|
||||
* every failure into one generic error. */
|
||||
static SpacetimePointStatus rhs(const MetricSlab *slab, double t,
|
||||
const State *s, Derivative *out) {
|
||||
MetricData m;
|
||||
double inv[3][3], up[3] = {0}, da_pi = 0, k_pi_pi = 0;
|
||||
if (spacetime_slab_eval(slab, t, s->x, &m) || m.alpha <= 0 ||
|
||||
invert(m.gamma, inv))
|
||||
return -1;
|
||||
const SpacetimePointStatus metric_status =
|
||||
spacetime_slab_eval(slab, t, s->x, &m);
|
||||
if (metric_status != SPACETIME_POINT_OK)
|
||||
return metric_status;
|
||||
if (m.alpha <= 0 || invert(m.gamma, inv))
|
||||
return SPACETIME_POINT_INVALID_METRIC;
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
up[i] += inv[i][j] * s->Pi[j];
|
||||
@@ -68,7 +73,7 @@ static int rhs(const MetricSlab *slab, double t, const State *s,
|
||||
db_pi - 0.5 * m.alpha * dg_pi_pi;
|
||||
}
|
||||
out->log_alpha_p0 = -da_pi + m.alpha * k_pi_pi;
|
||||
return 0;
|
||||
return SPACETIME_POINT_OK;
|
||||
}
|
||||
|
||||
static State add(const State *s, const Derivative *d, double h) {
|
||||
@@ -81,20 +86,25 @@ static State add(const State *s, const Derivative *d, double h) {
|
||||
return r;
|
||||
}
|
||||
|
||||
static int rk4(const MetricSlab *slab, double t, double h, State *s) {
|
||||
static SpacetimePointStatus rk4(const MetricSlab *slab, double t, double h,
|
||||
State *s) {
|
||||
Derivative a, b, c, d;
|
||||
State q;
|
||||
if (rhs(slab, t, s, &a))
|
||||
return -1;
|
||||
SpacetimePointStatus status = rhs(slab, t, s, &a);
|
||||
if (status != SPACETIME_POINT_OK)
|
||||
return status;
|
||||
q = add(s, &a, h / 2);
|
||||
if (rhs(slab, t + h / 2, &q, &b))
|
||||
return -1;
|
||||
status = rhs(slab, t + h / 2, &q, &b);
|
||||
if (status != SPACETIME_POINT_OK)
|
||||
return status;
|
||||
q = add(s, &b, h / 2);
|
||||
if (rhs(slab, t + h / 2, &q, &c))
|
||||
return -1;
|
||||
status = rhs(slab, t + h / 2, &q, &c);
|
||||
if (status != SPACETIME_POINT_OK)
|
||||
return status;
|
||||
q = add(s, &c, h);
|
||||
if (rhs(slab, t + h, &q, &d))
|
||||
return -1;
|
||||
status = rhs(slab, t + h, &q, &d);
|
||||
if (status != SPACETIME_POINT_OK)
|
||||
return status;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
s->x[i] += h * (a.x[i] + 2 * b.x[i] + 2 * c.x[i] + d.x[i]) / 6;
|
||||
s->Pi[i] += h * (a.Pi[i] + 2 * b.Pi[i] + 2 * c.Pi[i] + d.Pi[i]) / 6;
|
||||
@@ -103,7 +113,7 @@ static int rk4(const MetricSlab *slab, double t, double h, State *s) {
|
||||
(a.log_alpha_p0 + 2 * b.log_alpha_p0 + 2 * c.log_alpha_p0 +
|
||||
d.log_alpha_p0) /
|
||||
6;
|
||||
return 0;
|
||||
return SPACETIME_POINT_OK;
|
||||
}
|
||||
|
||||
int geodesic_initialize_past_ray_metric(const MetricData *metric,
|
||||
@@ -127,6 +137,7 @@ int geodesic_initialize_past_ray_metric(const MetricData *metric,
|
||||
s->Pi[i] /= m->alpha * k[0];
|
||||
}
|
||||
s->log_alpha_p0 = log(m->alpha * k[0]);
|
||||
s->log_alpha_p0_0 = s->log_alpha_p0;
|
||||
s->coordinate_time = o->coordinate_time;
|
||||
s->steps = 0;
|
||||
return isfinite(s->log_alpha_p0) ? 0 : -1;
|
||||
@@ -218,22 +229,104 @@ static AsymptoticStatus localize_worldtube_crossing(const MetricSlab *slab,
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
|
||||
static GeodesicAdvanceResult legacy_escape_or_capture(const MetricSlab *slab,
|
||||
const State *s,
|
||||
SpacetimeRayStatus status,
|
||||
RayEndpoint *out) {
|
||||
out->status =
|
||||
status == SPACETIME_RAY_ESCAPED ? RAY_ENDPOINT_ESCAPED
|
||||
: RAY_ENDPOINT_CAPTURED;
|
||||
if (out->status == RAY_ENDPOINT_ESCAPED) {
|
||||
if (escaped_direction(slab, s->coordinate_time, s, out->n_infinity) == 0)
|
||||
out->frequency_ratio = exp(-s->log_alpha_p0);
|
||||
else
|
||||
out->status = RAY_ENDPOINT_INTEGRATION_FAILURE;
|
||||
static RayReason reason_from_point_status(SpacetimePointStatus status) {
|
||||
switch (status) {
|
||||
case SPACETIME_POINT_TIME_UNAVAILABLE:
|
||||
return RAY_REASON_TIME_RANGE_EXHAUSTED;
|
||||
case SPACETIME_POINT_OUT_OF_DOMAIN:
|
||||
return RAY_REASON_OUT_OF_DOMAIN;
|
||||
case SPACETIME_POINT_INVALID_METRIC:
|
||||
return RAY_REASON_INVALID_METRIC;
|
||||
case SPACETIME_POINT_INTERNAL_ERROR:
|
||||
return RAY_REASON_PROTOCOL_ERROR;
|
||||
default:
|
||||
return RAY_REASON_INTEGRATION_ERROR;
|
||||
}
|
||||
return out->status == RAY_ENDPOINT_INTEGRATION_FAILURE
|
||||
? GEODESIC_ADVANCE_FAILED
|
||||
: GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
|
||||
static void record_final_state(RayEndpoint *out, const State *s) {
|
||||
if (s == NULL) {
|
||||
out->stop_coordinate_time = NAN;
|
||||
out->accepted_steps = 0;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
out->final_x[i] = NAN;
|
||||
out->final_Pi[i] = NAN;
|
||||
}
|
||||
out->final_log_alpha_p0 = NAN;
|
||||
out->final_log_alpha_p0_0 = NAN;
|
||||
return;
|
||||
}
|
||||
out->stop_coordinate_time = s->coordinate_time;
|
||||
out->accepted_steps = s->steps;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
out->final_x[i] = s->x[i];
|
||||
out->final_Pi[i] = s->Pi[i];
|
||||
}
|
||||
out->final_log_alpha_p0 = s->log_alpha_p0;
|
||||
out->final_log_alpha_p0_0 = s->log_alpha_p0_0;
|
||||
}
|
||||
|
||||
static void set_incomplete(RayEndpoint *out, RayReason reason,
|
||||
const State *last) {
|
||||
out->outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out->reason = reason;
|
||||
out->end_id = SPACETIME_END_NONE;
|
||||
record_final_state(out, last);
|
||||
out->threshold_value = NAN;
|
||||
}
|
||||
|
||||
/* Value of the monitored dark-threshold quantity at a trusted state. */
|
||||
static double monitored_threshold_value(const MetricSlab *slab,
|
||||
ThresholdKind kind, const State *s) {
|
||||
if (kind == THRESHOLD_LOG_ALPHA_P0)
|
||||
return s->log_alpha_p0;
|
||||
if (kind == THRESHOLD_LOG_ENERGY_GROWTH)
|
||||
return s->log_alpha_p0 - s->log_alpha_p0_0;
|
||||
if (kind == THRESHOLD_LOG_P0) {
|
||||
MetricData m;
|
||||
if (spacetime_slab_eval(slab, s->coordinate_time, s->x, &m) !=
|
||||
SPACETIME_POINT_OK ||
|
||||
m.alpha <= 0.0)
|
||||
return NAN;
|
||||
return s->log_alpha_p0 - log(m.alpha);
|
||||
}
|
||||
return NAN;
|
||||
}
|
||||
|
||||
/* Check the dark threshold on one trusted state. Returns nonzero and fills a
|
||||
* DARK endpoint when the monitored quantity has reached the threshold. */
|
||||
static int threshold_reached(const MetricSlab *slab,
|
||||
const GeodesicTraceConfig *config, const State *s,
|
||||
RayEndpoint *out) {
|
||||
if (config->threshold.kind == THRESHOLD_DISABLED)
|
||||
return 0;
|
||||
const double value =
|
||||
monitored_threshold_value(slab, config->threshold.kind, s);
|
||||
if (!isfinite(value) || value < config->threshold.value)
|
||||
return 0;
|
||||
out->outcome = RAY_OUTCOME_DARK;
|
||||
out->reason = RAY_REASON_REDSHIFT_LIMIT;
|
||||
out->end_id = SPACETIME_END_NONE;
|
||||
record_final_state(out, s);
|
||||
out->threshold_value = value;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Legacy no-declared-ends backends may still report a region escape. There
|
||||
* is no position-based physical capture; a failed sky direction is reported
|
||||
* as an integration failure, never as capture. */
|
||||
static GeodesicAdvanceResult legacy_escape(const MetricSlab *slab,
|
||||
const State *s, RayEndpoint *out) {
|
||||
if (escaped_direction(slab, s->coordinate_time, s, out->n_infinity) != 0) {
|
||||
set_incomplete(out, RAY_REASON_INTEGRATION_ERROR, s);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
out->outcome = RAY_OUTCOME_ESCAPED;
|
||||
out->reason = RAY_REASON_NONE;
|
||||
out->frequency_ratio = exp(-s->log_alpha_p0);
|
||||
record_final_state(out, s);
|
||||
out->threshold_value = NAN;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
|
||||
GeodesicAdvanceResult geodesic_advance_past_ray(
|
||||
@@ -241,53 +334,58 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
|
||||
const GeodesicTraceConfig *config, RayEndpoint *out) {
|
||||
if (!slab || !s || !config || !out || config->coordinate_time_step <= 0 ||
|
||||
!config->max_steps || !isfinite(slab_left_time) ||
|
||||
slab_left_time > s->coordinate_time)
|
||||
slab_left_time > s->coordinate_time) {
|
||||
if (out != NULL) {
|
||||
out->outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out->reason = RAY_REASON_PROTOCOL_ERROR;
|
||||
record_final_state(out, s);
|
||||
out->threshold_value = NAN;
|
||||
}
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
const AsymLifecycleMode mode = asym_lifecycle_mode(slab->source);
|
||||
if (mode == ASYM_LIFECYCLE_PROTOCOL_ERROR) {
|
||||
out->status = RAY_ENDPOINT_INVALID;
|
||||
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
const int directed = mode == ASYM_LIFECYCLE_READY;
|
||||
const size_t end_count =
|
||||
directed ? spacetime_asymptotic_end_count(slab->source) : 0;
|
||||
while (s->coordinate_time > slab_left_time) {
|
||||
if (config->capture_log_alpha_p0 > 0.0 &&
|
||||
s->log_alpha_p0 >= config->capture_log_alpha_p0) {
|
||||
out->status = RAY_ENDPOINT_CAPTURED;
|
||||
/* The threshold is checked only on trusted initial/accepted states. A
|
||||
* trial stage that crosses it does not by itself produce DARK. */
|
||||
if (threshold_reached(slab, config, s, out))
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
const SpacetimeRayStatus status =
|
||||
spacetime_slab_classify(slab, s->coordinate_time, s->x);
|
||||
if (status == SPACETIME_RAY_CAPTURED) {
|
||||
out->status = RAY_ENDPOINT_CAPTURED;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
if (!directed && status != SPACETIME_RAY_ACTIVE)
|
||||
return legacy_escape_or_capture(slab, s, status, out);
|
||||
if (!directed &&
|
||||
spacetime_slab_classify(slab, s->coordinate_time, s->x) ==
|
||||
SPACETIME_RAY_ESCAPED)
|
||||
return legacy_escape(slab, s, out);
|
||||
if (s->steps >= config->max_steps) {
|
||||
out->status = RAY_ENDPOINT_MAX_STEPS;
|
||||
/* Trustworthy trajectory, compute budget exhausted: retryable. */
|
||||
out->outcome = RAY_OUTCOME_UNRESOLVED;
|
||||
out->reason = RAY_REASON_BUDGET_EXHAUSTED;
|
||||
out->end_id = SPACETIME_END_NONE;
|
||||
record_final_state(out, s);
|
||||
out->threshold_value = NAN;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
const double h = -fmin(config->coordinate_time_step,
|
||||
s->coordinate_time - slab_left_time);
|
||||
const State before = *s;
|
||||
if (rk4(slab, s->coordinate_time, h, s))
|
||||
const SpacetimePointStatus step_status = rk4(slab, s->coordinate_time, h, s);
|
||||
if (step_status != SPACETIME_POINT_OK) {
|
||||
set_incomplete(out, reason_from_point_status(step_status), &before);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
s->coordinate_time += h;
|
||||
++s->steps;
|
||||
if (!directed)
|
||||
continue;
|
||||
|
||||
if (spacetime_slab_classify(slab, s->coordinate_time, s->x) ==
|
||||
SPACETIME_RAY_CAPTURED) {
|
||||
out->status = RAY_ENDPOINT_CAPTURED;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
for (size_t i = 0; i < end_count; ++i) {
|
||||
SpacetimeAsymptoticEnd end;
|
||||
if (spacetime_asymptotic_end(slab->source, i, &end)) {
|
||||
out->status = RAY_ENDPOINT_INVALID;
|
||||
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
double f_before, f_after;
|
||||
@@ -300,14 +398,14 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
|
||||
after_status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
|
||||
/* A first-class terminal reason, matching pre-route exhaustion:
|
||||
* preserve the end id and install it as terminated provenance. */
|
||||
out->status = RAY_ENDPOINT_TIME_RANGE_EXHAUSTED;
|
||||
set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, &before);
|
||||
out->end_id = end.end_id;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
if (before_status != ASYMPTOTIC_OK || after_status != ASYMPTOTIC_OK) {
|
||||
/* The backend cannot describe its own worldtube; this is an explicit
|
||||
* failure, not a physical escape. */
|
||||
out->status = RAY_ENDPOINT_INVALID;
|
||||
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
/* Strict inside->outside: the step must end strictly outside, so a
|
||||
@@ -320,29 +418,37 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
|
||||
const AsymptoticStatus localized = localize_worldtube_crossing(
|
||||
slab, end.end_id, &before, h, &crossing);
|
||||
if (localized == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
|
||||
out->status = RAY_ENDPOINT_TIME_RANGE_EXHAUSTED;
|
||||
set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, &before);
|
||||
out->end_id = end.end_id;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
if (localized != ASYMPTOTIC_OK) {
|
||||
out->status = RAY_ENDPOINT_INVALID;
|
||||
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
const AsymptoticStatus transfer = asymptotic_finish_escape(
|
||||
slab->source, end.end_id, crossing.coordinate_time, crossing.x,
|
||||
crossing.Pi, crossing.log_alpha_p0, out);
|
||||
if (transfer == ASYMPTOTIC_OK)
|
||||
if (transfer == ASYMPTOTIC_OK) {
|
||||
record_final_state(out, &crossing);
|
||||
out->threshold_value = NAN;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
out->status = transfer == ASYMPTOTIC_TIME_RANGE_EXHAUSTED
|
||||
? RAY_ENDPOINT_TIME_RANGE_EXHAUSTED
|
||||
: RAY_ENDPOINT_INVALID;
|
||||
}
|
||||
if (transfer == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
|
||||
set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, &before);
|
||||
out->end_id = end.end_id;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
}
|
||||
/* The loop can stop exactly at the slab's left boundary, so the final
|
||||
* accepted step must also be checked before reporting ACTIVE; otherwise a
|
||||
* ray that crossed the dark threshold on its last step would be settled as
|
||||
* budget-unresolved and require another slab/retry. */
|
||||
if (threshold_reached(slab, config, s, out))
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
return GEODESIC_ADVANCE_ACTIVE;
|
||||
}
|
||||
|
||||
@@ -353,7 +459,12 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source,
|
||||
RayEndpoint out = {.frequency_ratio = 0,
|
||||
.magnification = 1,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.status = RAY_ENDPOINT_INTEGRATION_FAILURE};
|
||||
.outcome = RAY_OUTCOME_INCOMPLETE,
|
||||
.reason = RAY_REASON_INTEGRATION_ERROR,
|
||||
.stop_coordinate_time = NAN,
|
||||
.accepted_steps = 0,
|
||||
.threshold_value = NAN};
|
||||
record_final_state(&out, NULL);
|
||||
if (!source || !observer || !config || config->coordinate_time_step <= 0 ||
|
||||
!config->max_steps || fabs(dot(n, n) - 1) > 1e-10)
|
||||
return out;
|
||||
@@ -361,16 +472,19 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source,
|
||||
const AsymptoticStatus route_status =
|
||||
asymptotic_route_camera(source, observer, n, &route);
|
||||
if (route_status == ASYMPTOTIC_UNSUPPORTED) {
|
||||
out.status = RAY_ENDPOINT_INVALID;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_UNSUPPORTED;
|
||||
return out;
|
||||
}
|
||||
if (route_status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
|
||||
out.status = RAY_ENDPOINT_TIME_RANGE_EXHAUSTED;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_TIME_RANGE_EXHAUSTED;
|
||||
out.end_id = route.end_id;
|
||||
return out;
|
||||
}
|
||||
if (route_status != ASYMPTOTIC_OK) {
|
||||
out.status = RAY_ENDPOINT_INVALID;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_PROTOCOL_ERROR;
|
||||
return out;
|
||||
}
|
||||
if (route.kind == ASYMPTOTIC_ROUTE_ESCAPED) {
|
||||
@@ -378,34 +492,79 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source,
|
||||
out.n_infinity[i] = route.n_infinity[i];
|
||||
out.frequency_ratio = route.frequency_ratio;
|
||||
out.end_id = route.end_id;
|
||||
out.status = RAY_ENDPOINT_ESCAPED;
|
||||
out.outcome = RAY_OUTCOME_ESCAPED;
|
||||
out.reason = RAY_REASON_NONE;
|
||||
return out;
|
||||
}
|
||||
if (route.kind == ASYMPTOTIC_ROUTE_TIME_RANGE_EXHAUSTED) {
|
||||
out.status = RAY_ENDPOINT_TIME_RANGE_EXHAUSTED;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_TIME_RANGE_EXHAUSTED;
|
||||
out.end_id = route.end_id;
|
||||
return out;
|
||||
}
|
||||
if (route.kind != ASYMPTOTIC_ROUTE_INSIDE &&
|
||||
route.kind != ASYMPTOTIC_ROUTE_ENTRY) {
|
||||
out.status = RAY_ENDPOINT_INVALID;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_PROTOCOL_ERROR;
|
||||
return out;
|
||||
}
|
||||
State state = {.coordinate_time = route.activate_t,
|
||||
.x = {route.x[0], route.x[1], route.x[2]},
|
||||
.Pi = {route.Pi[0], route.Pi[1], route.Pi[2]},
|
||||
.log_alpha_p0 = route.log_alpha_p0,
|
||||
/* Camera-event reference, distinct from the entry-state L for
|
||||
* an external camera. */
|
||||
.log_alpha_p0_0 = route.log_alpha_p0_camera,
|
||||
.steps = 0};
|
||||
const double last_time =
|
||||
route.activate_t - config->coordinate_time_step * config->max_steps;
|
||||
MetricSlab *slab = NULL;
|
||||
if (spacetime_load_slab(source, route.activate_t, last_time - 1.0, &slab)) {
|
||||
out.status = RAY_ENDPOINT_INVALID;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_IO_ERROR;
|
||||
return out;
|
||||
}
|
||||
if (geodesic_advance_past_ray(slab, &state, last_time, config, &out) ==
|
||||
GEODESIC_ADVANCE_ACTIVE)
|
||||
out.status = RAY_ENDPOINT_MAX_STEPS;
|
||||
GEODESIC_ADVANCE_ACTIVE) {
|
||||
out.outcome = RAY_OUTCOME_UNRESOLVED;
|
||||
out.reason = RAY_REASON_BUDGET_EXHAUSTED;
|
||||
record_final_state(&out, &state);
|
||||
}
|
||||
spacetime_free_slab(slab);
|
||||
return out;
|
||||
}
|
||||
|
||||
RayEndpoint geodesic_trace_past_from_state(const SpacetimeSource *source,
|
||||
const GeodesicRayState *state_in,
|
||||
const GeodesicTraceConfig *config) {
|
||||
RayEndpoint out = {.frequency_ratio = 0,
|
||||
.magnification = 1,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.outcome = RAY_OUTCOME_INCOMPLETE,
|
||||
.reason = RAY_REASON_INTEGRATION_ERROR,
|
||||
.stop_coordinate_time = NAN,
|
||||
.accepted_steps = 0,
|
||||
.threshold_value = NAN};
|
||||
record_final_state(&out, NULL);
|
||||
if (!source || !state_in || !config || config->coordinate_time_step <= 0 ||
|
||||
!config->max_steps || state_in->steps >= config->max_steps)
|
||||
return out;
|
||||
State state = *state_in;
|
||||
const double remaining =
|
||||
config->coordinate_time_step * (double)(config->max_steps - state.steps);
|
||||
const double last_time = state.coordinate_time - remaining - 1.0;
|
||||
MetricSlab *slab = NULL;
|
||||
if (spacetime_load_slab(source, state.coordinate_time, last_time, &slab)) {
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_IO_ERROR;
|
||||
return out;
|
||||
}
|
||||
if (geodesic_advance_past_ray(slab, &state, last_time, config, &out) ==
|
||||
GEODESIC_ADVANCE_ACTIVE) {
|
||||
out.outcome = RAY_OUTCOME_UNRESOLVED;
|
||||
out.reason = RAY_REASON_BUDGET_EXHAUSTED;
|
||||
record_final_state(&out, &state);
|
||||
}
|
||||
spacetime_free_slab(slab);
|
||||
return out;
|
||||
}
|
||||
+67
-15
@@ -4,33 +4,75 @@
|
||||
#include "observer.h"
|
||||
#include "spacetime.h"
|
||||
|
||||
/* Rendering/completion category. This is deliberately separate from the
|
||||
* diagnostic reason below, and from the ray-pool lifecycle. */
|
||||
typedef enum {
|
||||
RAY_ENDPOINT_ESCAPED,
|
||||
RAY_ENDPOINT_CAPTURED,
|
||||
RAY_ENDPOINT_MAX_STEPS,
|
||||
RAY_ENDPOINT_INTEGRATION_FAILURE,
|
||||
RAY_ENDPOINT_TIME_RANGE_EXHAUSTED,
|
||||
RAY_ENDPOINT_INVALID
|
||||
} RayEndpointStatus;
|
||||
RAY_OUTCOME_ESCAPED = 0, /* reached an infinity end; carries a payload */
|
||||
RAY_OUTCOME_DARK, /* normal dark terminal (currently redshift limit) */
|
||||
RAY_OUTCOME_UNRESOLVED, /* trustworthy trajectory, compute budget exhausted */
|
||||
RAY_OUTCOME_INCOMPLETE /* history/domain/metric/integration/protocol error */
|
||||
} RayOutcome;
|
||||
|
||||
/* Diagnostic reason. Different DARK reasons must not create a mesh seam; the
|
||||
* reason is for accounting and provenance only. */
|
||||
typedef enum {
|
||||
RAY_REASON_NONE = 0,
|
||||
RAY_REASON_REDSHIFT_LIMIT,
|
||||
RAY_REASON_BUDGET_EXHAUSTED,
|
||||
RAY_REASON_TIME_RANGE_EXHAUSTED,
|
||||
RAY_REASON_OUT_OF_DOMAIN,
|
||||
RAY_REASON_INVALID_METRIC,
|
||||
RAY_REASON_INTEGRATION_ERROR,
|
||||
RAY_REASON_UNSUPPORTED,
|
||||
RAY_REASON_PROTOCOL_ERROR,
|
||||
RAY_REASON_IO_ERROR
|
||||
} RayReason;
|
||||
|
||||
/* Monitored quantity used by the dark-redshift termination policy. `LOG_P0`
|
||||
* is ln(p^0) = L - ln(alpha); it differs from `LOG_ALPHA_P0` by a local
|
||||
* function of position and must not be confused with the true infinity
|
||||
* frequency ratio g. */
|
||||
typedef enum {
|
||||
THRESHOLD_DISABLED = 0,
|
||||
THRESHOLD_LOG_ALPHA_P0, /* absolute L = ln(alpha p^0) */
|
||||
THRESHOLD_LOG_P0, /* ln(p^0) = L - ln(alpha) */
|
||||
THRESHOLD_LOG_ENERGY_GROWTH /* L - L0, local energy growth since the start */
|
||||
} ThresholdKind;
|
||||
|
||||
typedef struct {
|
||||
ThresholdKind kind;
|
||||
double value; /* terminate when the monitored quantity reaches this */
|
||||
unsigned int policy_version;
|
||||
} ThresholdPolicy;
|
||||
|
||||
typedef struct {
|
||||
double n_infinity[3];
|
||||
double frequency_ratio; /* E_camera / E_infinity */
|
||||
double magnification; /* Filled by the future local inverse lens map. */
|
||||
/* Meaningful for RAY_ENDPOINT_ESCAPED and for
|
||||
* RAY_ENDPOINT_TIME_RANGE_EXHAUSTED; SPACETIME_END_NONE otherwise. */
|
||||
/* End this escape belongs to; SPACETIME_END_NONE when not applicable. */
|
||||
SpacetimeEndId end_id;
|
||||
RayEndpointStatus status;
|
||||
RayOutcome outcome;
|
||||
RayReason reason;
|
||||
/* Last trusted state at termination. For an ESCAPED endpoint this is the
|
||||
* (finite) numerical truncation position, not the true parameter end. */
|
||||
double stop_coordinate_time;
|
||||
unsigned int accepted_steps;
|
||||
/* Last trusted continuous state, used to resume an UNRESOLVED ray from its
|
||||
* last accepted step instead of replaying it from the camera. */
|
||||
double final_x[3];
|
||||
double final_Pi[3];
|
||||
double final_log_alpha_p0;
|
||||
double final_log_alpha_p0_0; /* original reference L0 for retries */
|
||||
/* Monitored threshold value at termination, NAN when not applicable. */
|
||||
double threshold_value;
|
||||
} RayEndpoint;
|
||||
|
||||
typedef struct {
|
||||
double coordinate_time_step;
|
||||
unsigned int max_steps;
|
||||
/* A positive value terminates a backwards ray whose horizon redshift has
|
||||
* made log(alpha p^0) reach this value. Zero disables this analytic/demo
|
||||
* criterion; numerical moving-puncture backends use their AH-calibrated
|
||||
* spatial cutoff instead. */
|
||||
double capture_log_alpha_p0;
|
||||
/* Normal dark terminal for every backend. No backend may substitute a
|
||||
* position/horizon cutoff for physical capture. */
|
||||
ThresholdPolicy threshold;
|
||||
} GeodesicTraceConfig;
|
||||
|
||||
typedef struct {
|
||||
@@ -38,6 +80,9 @@ typedef struct {
|
||||
double x[3];
|
||||
double Pi[3];
|
||||
double log_alpha_p0;
|
||||
/* Reference L at the start of this ray's integration, carried unchanged
|
||||
* through retries so THRESHOLD_LOG_ENERGY_GROWTH stays camera-relative. */
|
||||
double log_alpha_p0_0;
|
||||
unsigned int steps;
|
||||
} GeodesicRayState;
|
||||
|
||||
@@ -67,4 +112,11 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
|
||||
const MetricSlab *slab, GeodesicRayState *state,
|
||||
double slab_left_time, const GeodesicTraceConfig *config,
|
||||
RayEndpoint *endpoint);
|
||||
/* Resume a past ray from its last trusted state and integrate to the total
|
||||
* step budget in `config->max_steps` (state->steps counts steps already
|
||||
* consumed). Used to retry UNRESOLVED rays without replaying them from the
|
||||
* camera. */
|
||||
RayEndpoint geodesic_trace_past_from_state(const SpacetimeSource *source,
|
||||
const GeodesicRayState *state,
|
||||
const GeodesicTraceConfig *config);
|
||||
#endif
|
||||
+76
-20
@@ -10,7 +10,10 @@
|
||||
/* All scalar fields are explicitly little-endian; never serialize C structs
|
||||
* because their padding and size_t width are ABI-dependent. */
|
||||
static const unsigned char lens_map_magic[8] = {'G', 'R', 'L', 'E', 'N', 'S', 1, 0};
|
||||
enum { LENS_MAP_VERSION = 1, LENS_MAP_ENDIAN = 0x01020304u };
|
||||
/* Version 2 stores the two-level RayOutcome instead of the removed
|
||||
* RayEndpointStatus. Version 1 files are rejected: their old captured bit
|
||||
* cannot be upgraded into the new dark/unresolved/error provenance. */
|
||||
enum { LENS_MAP_VERSION = 2, LENS_MAP_ENDIAN = 0x01020304u };
|
||||
|
||||
static uint32_t crc32_update(uint32_t crc, const void *data, size_t size) {
|
||||
const unsigned char *bytes = data;
|
||||
@@ -64,11 +67,14 @@ static int valid_mesh(const FrameLensMesh *m) {
|
||||
if (m == NULL || m->vertex_count == 0 || m->triangle_count == 0) return 0;
|
||||
for (size_t i = 0; i < m->vertex_count; ++i) {
|
||||
const LensVertex *v = &m->vertices[i];
|
||||
if (!v->traced || v->status < RAY_ENDPOINT_ESCAPED ||
|
||||
v->status > RAY_ENDPOINT_INTEGRATION_FAILURE || !isfinite(v->image_x) ||
|
||||
if (!v->traced || v->outcome > RAY_OUTCOME_INCOMPLETE ||
|
||||
v->reason > RAY_REASON_IO_ERROR || !isfinite(v->image_x) ||
|
||||
!isfinite(v->image_y) || !isfinite(v->log_frequency_ratio) ||
|
||||
!unit_vector(v->camera_direction) ||
|
||||
(v->status == RAY_ENDPOINT_ESCAPED && !unit_vector(v->n_infinity))) return 0;
|
||||
!unit_vector(v->camera_direction))
|
||||
return 0;
|
||||
if (v->outcome == RAY_OUTCOME_ESCAPED &&
|
||||
(!unit_vector(v->n_infinity) || v->end_id == SPACETIME_END_NONE))
|
||||
return 0;
|
||||
}
|
||||
for (size_t i = 0; i < m->triangle_count; ++i)
|
||||
for (int j = 0; j < 3; ++j)
|
||||
@@ -77,34 +83,51 @@ static int valid_mesh(const FrameLensMesh *m) {
|
||||
}
|
||||
|
||||
int lens_map_write(const char *path, int width, int height, double fov,
|
||||
const LensMapProvenance *provenance,
|
||||
const LensMapFrame *frames, size_t frame_count) {
|
||||
if (path == NULL || frames == NULL || width <= 0 || height <= 0 ||
|
||||
!isfinite(fov) || fov <= 0.0 || fov >= 179.0 || frame_count == 0 ||
|
||||
frame_count > UINT64_MAX) return -1;
|
||||
if (path == NULL || provenance == NULL || frames == NULL || width <= 0 ||
|
||||
height <= 0 || !isfinite(fov) || fov <= 0.0 || fov >= 179.0 ||
|
||||
frame_count == 0 || frame_count > UINT64_MAX)
|
||||
return -1;
|
||||
for (size_t i = 0; i < frame_count; ++i) if (!valid_mesh(&frames[i].mesh)) return -1;
|
||||
FILE *file = fopen(path, "wb"); if (file == NULL) return -1;
|
||||
int failed = write_bytes(file, lens_map_magic, sizeof lens_map_magic, NULL) ||
|
||||
write_u32(file, LENS_MAP_VERSION, NULL) || write_u32(file, LENS_MAP_ENDIAN, NULL) ||
|
||||
write_u32(file, (uint32_t)width, NULL) || write_u32(file, (uint32_t)height, NULL) ||
|
||||
write_double(file, fov, NULL) || write_u64(file, (uint64_t)frame_count, NULL);
|
||||
write_double(file, fov, NULL) || write_u64(file, (uint64_t)frame_count, NULL) ||
|
||||
write_u32(file, provenance->threshold_kind, NULL) ||
|
||||
write_u32(file, provenance->threshold_policy_version, NULL) ||
|
||||
write_double(file, provenance->threshold_value, NULL) ||
|
||||
write_u32(file, provenance->retry_step_increment, NULL) ||
|
||||
write_u32(file, provenance->max_total_steps, NULL) ||
|
||||
write_u32(file, provenance->max_level, NULL) ||
|
||||
write_u32(file, provenance->integrator, NULL) ||
|
||||
write_double(file, provenance->min_edge_pixels, NULL) ||
|
||||
write_double(file, provenance->min_area_pixels2, NULL) ||
|
||||
write_double(file, provenance->coordinate_time_step, NULL) ||
|
||||
write_u32(file, provenance->initial_max_steps, NULL);
|
||||
for (size_t f = 0; !failed && f < frame_count; ++f) {
|
||||
const FrameLensMesh *m = &frames[f].mesh; uint32_t crc = UINT32_MAX;
|
||||
failed = write_u64(file, frames[f].frame_id, NULL) ||
|
||||
write_double(file, frames[f].coordinate_time, NULL) ||
|
||||
write_double(file, frames[f].proper_time, NULL) ||
|
||||
write_u64(file, (uint64_t)m->vertex_count, NULL) ||
|
||||
write_u64(file, (uint64_t)m->triangle_count, NULL);
|
||||
write_u64(file, (uint64_t)m->triangle_count, NULL) ||
|
||||
write_u64(file, (uint64_t)m->retry_requests, NULL);
|
||||
for (size_t i = 0; !failed && i < m->vertex_count; ++i) {
|
||||
const LensVertex *v = &m->vertices[i];
|
||||
failed = write_double(file, v->image_x, &crc) || write_double(file, v->image_y, &crc);
|
||||
for (int j = 0; !failed && j < 3; ++j) failed = write_double(file, v->camera_direction[j], &crc);
|
||||
for (int j = 0; !failed && j < 3; ++j) failed = write_double(file, v->n_infinity[j], &crc);
|
||||
failed = failed || write_double(file, v->log_frequency_ratio, &crc) ||
|
||||
write_u32(file, (uint32_t)v->status, &crc);
|
||||
write_u32(file, (uint32_t)v->end_id, &crc) ||
|
||||
write_u32(file, (uint32_t)v->outcome, &crc) ||
|
||||
write_u32(file, (uint32_t)v->reason, &crc);
|
||||
}
|
||||
for (size_t i = 0; !failed && i < m->triangle_count; ++i) {
|
||||
for (int j = 0; j < 3; ++j) failed = failed || write_u64(file, m->triangles[i].vertex[j], &crc);
|
||||
failed = failed || write_u32(file, m->triangles[i].level, &crc);
|
||||
failed = failed || write_u32(file, m->triangles[i].level, &crc) ||
|
||||
write_u32(file, (uint32_t)m->triangles[i].approx_black, &crc);
|
||||
}
|
||||
failed = failed || write_u32(file, crc ^ UINT32_MAX, NULL);
|
||||
}
|
||||
@@ -118,41 +141,69 @@ void lens_map_destroy(LensMap *map) {
|
||||
free(map->frames); *map = (LensMap){0};
|
||||
}
|
||||
|
||||
int lens_map_read(const char *path, LensMap *map) {
|
||||
int lens_map_read(const char *path, LensMapProvenance *provenance,
|
||||
LensMap *map) {
|
||||
if (path == NULL || map == NULL) return -1;
|
||||
*map = (LensMap){0}; FILE *file = fopen(path, "rb"); if (file == NULL) return -1;
|
||||
unsigned char magic[8]; uint32_t version, endian, width, height; uint64_t count;
|
||||
LensMapProvenance prov = {0};
|
||||
int failed = read_bytes(file, magic, sizeof magic, NULL) || memcmp(magic, lens_map_magic, sizeof magic) ||
|
||||
read_u32(file, &version, NULL) || read_u32(file, &endian, NULL) ||
|
||||
read_u32(file, &width, NULL) || read_u32(file, &height, NULL) ||
|
||||
read_double(file, &map->horizontal_fov_deg, NULL) || read_u64(file, &count, NULL) ||
|
||||
read_u32(file, &prov.threshold_kind, NULL) ||
|
||||
read_u32(file, &prov.threshold_policy_version, NULL) ||
|
||||
read_double(file, &prov.threshold_value, NULL) ||
|
||||
read_u32(file, &prov.retry_step_increment, NULL) ||
|
||||
read_u32(file, &prov.max_total_steps, NULL) ||
|
||||
read_u32(file, &prov.max_level, NULL) ||
|
||||
read_u32(file, &prov.integrator, NULL) ||
|
||||
read_double(file, &prov.min_edge_pixels, NULL) ||
|
||||
read_double(file, &prov.min_area_pixels2, NULL) ||
|
||||
read_double(file, &prov.coordinate_time_step, NULL) ||
|
||||
read_u32(file, &prov.initial_max_steps, NULL) ||
|
||||
version != LENS_MAP_VERSION || endian != LENS_MAP_ENDIAN || width == 0 || height == 0 ||
|
||||
width > INT32_MAX || height > INT32_MAX || !isfinite(map->horizontal_fov_deg) ||
|
||||
map->horizontal_fov_deg <= 0.0 || map->horizontal_fov_deg >= 179.0 || count == 0 ||
|
||||
prov.threshold_kind > THRESHOLD_LOG_ENERGY_GROWTH ||
|
||||
!isfinite(prov.threshold_value) ||
|
||||
prov.coordinate_time_step < 0.0 || !isfinite(prov.coordinate_time_step) ||
|
||||
count > SIZE_MAX / sizeof *map->frames;
|
||||
if (failed) goto done;
|
||||
map->provenance = prov;
|
||||
if (provenance != NULL)
|
||||
*provenance = prov;
|
||||
map->width = (int)width; map->height = (int)height; map->frame_count = (size_t)count;
|
||||
map->frames = calloc(map->frame_count, sizeof *map->frames); if (map->frames == NULL) { failed = 1; goto done; }
|
||||
for (size_t f = 0; !failed && f < map->frame_count; ++f) {
|
||||
LensMapFrame *frame = &map->frames[f]; uint64_t vertices, triangles; uint32_t stored_crc, crc = UINT32_MAX;
|
||||
LensMapFrame *frame = &map->frames[f]; uint64_t vertices, triangles, retry_requests;
|
||||
uint32_t stored_crc, crc = UINT32_MAX;
|
||||
failed = read_u64(file, &frame->frame_id, NULL) || read_double(file, &frame->coordinate_time, NULL) ||
|
||||
read_double(file, &frame->proper_time, NULL) || read_u64(file, &vertices, NULL) || read_u64(file, &triangles, NULL) ||
|
||||
read_u64(file, &retry_requests, NULL) ||
|
||||
!isfinite(frame->coordinate_time) || !isfinite(frame->proper_time) || vertices == 0 || triangles == 0 ||
|
||||
vertices > SIZE_MAX / sizeof *frame->mesh.vertices || triangles > SIZE_MAX / sizeof *frame->mesh.triangles;
|
||||
vertices > SIZE_MAX / sizeof *frame->mesh.vertices || triangles > SIZE_MAX / sizeof *frame->mesh.triangles ||
|
||||
retry_requests > SIZE_MAX;
|
||||
if (failed) break;
|
||||
frame->mesh.vertices = calloc((size_t)vertices, sizeof *frame->mesh.vertices);
|
||||
frame->mesh.triangles = calloc((size_t)triangles, sizeof *frame->mesh.triangles);
|
||||
if (frame->mesh.vertices == NULL || frame->mesh.triangles == NULL) { failed = 1; break; }
|
||||
frame->mesh.vertex_count = frame->mesh.vertex_capacity = (size_t)vertices;
|
||||
frame->mesh.triangle_count = frame->mesh.triangle_capacity = (size_t)triangles;
|
||||
frame->mesh.retry_requests = (size_t)retry_requests;
|
||||
for (size_t i = 0; !failed && i < frame->mesh.vertex_count; ++i) {
|
||||
LensVertex *v = &frame->mesh.vertices[i]; uint32_t status;
|
||||
LensVertex *v = &frame->mesh.vertices[i]; uint32_t end_id, outcome, reason;
|
||||
failed = read_double(file, &v->image_x, &crc) || read_double(file, &v->image_y, &crc);
|
||||
for (int j = 0; !failed && j < 3; ++j) failed = read_double(file, &v->camera_direction[j], &crc);
|
||||
for (int j = 0; !failed && j < 3; ++j) failed = read_double(file, &v->n_infinity[j], &crc);
|
||||
failed = failed || read_double(file, &v->log_frequency_ratio, &crc) || read_u32(file, &status, &crc) ||
|
||||
status > RAY_ENDPOINT_INTEGRATION_FAILURE;
|
||||
v->status = (RayEndpointStatus)status; v->traced = 1;
|
||||
failed = failed || read_double(file, &v->log_frequency_ratio, &crc) ||
|
||||
read_u32(file, &end_id, &crc) || read_u32(file, &outcome, &crc) ||
|
||||
read_u32(file, &reason, &crc) || outcome > RAY_OUTCOME_INCOMPLETE ||
|
||||
reason > RAY_REASON_IO_ERROR;
|
||||
v->end_id = (SpacetimeEndId)end_id;
|
||||
v->outcome = (RayOutcome)outcome;
|
||||
v->reason = (RayReason)reason;
|
||||
v->traced = 1;
|
||||
}
|
||||
for (size_t i = 0; !failed && i < frame->mesh.triangle_count; ++i) {
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
@@ -163,7 +214,12 @@ int lens_map_read(const char *path, LensMap *map) {
|
||||
}
|
||||
frame->mesh.triangles[i].vertex[j] = (size_t)index;
|
||||
}
|
||||
failed = failed || read_u32(file, &frame->mesh.triangles[i].level, &crc); frame->mesh.triangles[i].evaluated = 1;
|
||||
uint32_t approx_black = 0;
|
||||
failed = failed ||
|
||||
read_u32(file, &frame->mesh.triangles[i].level, &crc) ||
|
||||
read_u32(file, &approx_black, &crc);
|
||||
frame->mesh.triangles[i].approx_black = approx_black != 0;
|
||||
frame->mesh.triangles[i].evaluated = 1;
|
||||
}
|
||||
failed = failed || read_u32(file, &stored_crc, NULL) || stored_crc != (crc ^ UINT32_MAX) || !valid_mesh(&frame->mesh);
|
||||
}
|
||||
|
||||
+23
-3
@@ -16,17 +16,37 @@ typedef struct {
|
||||
FrameLensMesh mesh;
|
||||
} LensMapFrame;
|
||||
|
||||
/* File-level provenance. Stored explicitly so a replay can be attributed to
|
||||
* the terminal policy and integration settings that produced it. */
|
||||
typedef struct {
|
||||
uint32_t threshold_kind; /* ThresholdKind */
|
||||
uint32_t threshold_policy_version;
|
||||
double threshold_value;
|
||||
uint32_t retry_step_increment;
|
||||
uint32_t max_total_steps;
|
||||
uint32_t max_level;
|
||||
uint32_t integrator; /* 0 = fixed-step RK4 (transitional) */
|
||||
double min_edge_pixels;
|
||||
double min_area_pixels2;
|
||||
/* Integration source: the coordinate-time step and the initial per-ray
|
||||
* accepted-step budget used for the first trace. */
|
||||
double coordinate_time_step;
|
||||
uint32_t initial_max_steps;
|
||||
} LensMapProvenance;
|
||||
|
||||
typedef struct {
|
||||
int width, height;
|
||||
double horizontal_fov_deg;
|
||||
LensMapProvenance provenance;
|
||||
LensMapFrame *frames;
|
||||
size_t frame_count;
|
||||
} LensMap;
|
||||
|
||||
int lens_map_write(const char *path, int width, int height,
|
||||
double horizontal_fov_deg, const LensMapFrame *frames,
|
||||
size_t frame_count);
|
||||
int lens_map_read(const char *path, LensMap *map);
|
||||
double horizontal_fov_deg,
|
||||
const LensMapProvenance *provenance,
|
||||
const LensMapFrame *frames, size_t frame_count);
|
||||
int lens_map_read(const char *path, LensMapProvenance *provenance, LensMap *map);
|
||||
void lens_map_destroy(LensMap *map);
|
||||
|
||||
#endif
|
||||
+213
-40
@@ -23,9 +23,11 @@ typedef struct {
|
||||
int width, height;
|
||||
int coarse_cell_pixels;
|
||||
int draw_mesh;
|
||||
int allow_incomplete;
|
||||
int psf_direct;
|
||||
int verbose;
|
||||
double horizontal_fov_deg, look_ra_deg, look_dec_deg, exposure;
|
||||
double dark_threshold;
|
||||
double max_magnification;
|
||||
double max_cache_psf_flux;
|
||||
double psf_relative_tail;
|
||||
@@ -98,7 +100,10 @@ static int parse_double(const char *text, double *value) {
|
||||
char *end;
|
||||
errno = 0;
|
||||
*value = strtod(text, &end);
|
||||
return errno || *end || *value <= 0.0 || *value >= 179.0 ? -1 : 0;
|
||||
return errno || *end || !isfinite(*value) || *value <= 0.0 ||
|
||||
*value >= 179.0
|
||||
? -1
|
||||
: 0;
|
||||
}
|
||||
|
||||
static int parse_ra_deg(const char *text, double *value) {
|
||||
@@ -119,7 +124,7 @@ static int parse_positive(const char *text, double *value) {
|
||||
char *end;
|
||||
errno = 0;
|
||||
*value = strtod(text, &end);
|
||||
return errno || *end || *value <= 0.0 ? -1 : 0;
|
||||
return errno || *end || !isfinite(*value) || *value <= 0.0 ? -1 : 0;
|
||||
}
|
||||
|
||||
static int parse_nonnegative(const char *text, double *value) {
|
||||
@@ -379,6 +384,7 @@ static int parse_args(int argc, char **argv, Settings *s,
|
||||
.look_ra_deg = 90.0,
|
||||
.look_dec_deg = -90.0,
|
||||
.exposure = 1e-3,
|
||||
.dark_threshold = 8.0,
|
||||
.max_magnification = INFINITY,
|
||||
.max_cache_psf_flux = 1.0,
|
||||
.psf_relative_tail = 1e-8,
|
||||
@@ -453,6 +459,8 @@ static int parse_args(int argc, char **argv, Settings *s,
|
||||
!parse_positive(argv[++i], &s->refinement.min_area_pixels2)) {
|
||||
} else if (!strcmp(argv[i], "--draw-mesh")) {
|
||||
s->draw_mesh = 1;
|
||||
} else if (!strcmp(argv[i], "--allow-incomplete")) {
|
||||
s->allow_incomplete = 1;
|
||||
} else if (!strcmp(argv[i], "--fov-deg") && i + 1 < argc &&
|
||||
!parse_double(argv[++i], &s->horizontal_fov_deg)) {
|
||||
s->fov_specified = 1;
|
||||
@@ -464,6 +472,8 @@ static int parse_args(int argc, char **argv, Settings *s,
|
||||
s->look_specified = 1;
|
||||
} else if (!strcmp(argv[i], "--exposure") && i + 1 < argc &&
|
||||
!parse_positive(argv[++i], &s->exposure)) {
|
||||
} else if (!strcmp(argv[i], "--dark-threshold") && i + 1 < argc &&
|
||||
!parse_positive(argv[++i], &s->dark_threshold)) {
|
||||
} else if (!strcmp(argv[i], "--tone-map") && i + 1 < argc) {
|
||||
const char *mode = argv[++i];
|
||||
if (!strcmp(mode, "softclip"))
|
||||
@@ -636,6 +646,7 @@ static void print_help(const char *program) {
|
||||
" --look-ra-deg D ICRS look direction right ascension in degrees (default: 90)\n"
|
||||
" --look-dec-deg D ICRS look direction declination in degrees (default: -90)\n"
|
||||
" --exposure E Linear exposure multiplier (default: 1e-3)\n"
|
||||
" --dark-threshold T Camera-relative dark cutoff L-L0 (default: 8)\n"
|
||||
" --tone-map MODE Display transform: softclip or reinhard\n"
|
||||
" (default: softclip)\n"
|
||||
" --tone-map-p P Softclip hardness P >= 1 (default: 2)\n"
|
||||
@@ -706,6 +717,8 @@ static void print_help(const char *program) {
|
||||
fputs(" --draw-mesh Also write the final lens-mesh overlay as <output-stem>_mesh.ppm\n",
|
||||
stdout);
|
||||
#endif
|
||||
fputs(" --allow-incomplete Publish even when unresolved/error rays remain (diagnostic; output is marked incomplete)\n",
|
||||
stdout);
|
||||
#ifdef SPACETIME_ALCUBIERRE
|
||||
fputs(
|
||||
"\nAlcubierre warp bubble (moving x_s(t)=v_s*t; no capture):\n"
|
||||
@@ -806,13 +819,14 @@ static void report_splat_worker_progress(void *context, size_t worker_id,
|
||||
|
||||
static void ray_pool_status_counts(const RayPool *rays, size_t *pending,
|
||||
size_t *active, size_t *terminated,
|
||||
size_t *failed) {
|
||||
*pending = *active = *terminated = *failed = 0;
|
||||
size_t *unresolved, size_t *failed) {
|
||||
*pending = *active = *terminated = *unresolved = *failed = 0;
|
||||
for (size_t i = 0; i < rays->count; ++i)
|
||||
switch (rays->status[i]) {
|
||||
case RAY_POOL_PENDING: ++*pending; break;
|
||||
case RAY_POOL_ACTIVE: ++*active; break;
|
||||
case RAY_POOL_TERMINATED: ++*terminated; break;
|
||||
case RAY_POOL_UNRESOLVED: ++*unresolved; break;
|
||||
case RAY_POOL_FAILED: ++*failed; break;
|
||||
}
|
||||
}
|
||||
@@ -839,7 +853,7 @@ static void report_frame_refinement(void *context, size_t generation,
|
||||
#ifdef SPACETIME_ALCUBIERRE
|
||||
/* Upper bound on the per-ray step budget. Legal parameters whose worst-case
|
||||
* near-comoving ray could need more than this are rejected at startup rather
|
||||
* than silently terminating as RAY_ENDPOINT_MAX_STEPS. */
|
||||
* than silently terminating as UNRESOLVED/BUDGET_EXHAUSTED. */
|
||||
#define ALCUBIERRE_MAX_TRACE_STEPS (1u << 24)
|
||||
|
||||
/* Safety margin over the straight-line worst case: wall-region deflection can
|
||||
@@ -867,16 +881,109 @@ static double alcubierre_step_budget(const Settings *s) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Recompute approximate-black provenance and report E/D/U accounting. The
|
||||
* result is diagnostic for now; production failure gating is layered on top
|
||||
* of the same counters. */
|
||||
static void report_boundary_stats(const Settings *s,
|
||||
FrameLensMesh *const *meshes,
|
||||
size_t frame_count,
|
||||
const RefinementConfig *config,
|
||||
FrameBoundaryStats *out_total) {
|
||||
FrameBoundaryStats total = {0};
|
||||
for (size_t i = 0; i < frame_count; ++i) {
|
||||
FrameBoundaryStats frame_stats;
|
||||
frame_lens_mesh_boundary_stats(meshes[i], config, &frame_stats);
|
||||
total.escaped_only += frame_stats.escaped_only;
|
||||
total.dark_only += frame_stats.dark_only;
|
||||
total.eed_edd += frame_stats.eed_edd;
|
||||
total.uud_udd += frame_stats.uud_udd;
|
||||
total.u_with_escape += frame_stats.u_with_escape;
|
||||
total.uuu += frame_stats.uuu;
|
||||
total.error += frame_stats.error;
|
||||
total.approx_black_triangles += frame_stats.approx_black_triangles;
|
||||
total.approx_black_area_pixels2 += frame_stats.approx_black_area_pixels2;
|
||||
total.approx_black_max_edge_pixels = fmax(total.approx_black_max_edge_pixels,
|
||||
frame_stats.approx_black_max_edge_pixels);
|
||||
total.approx_black_max_area_pixels2 = fmax(total.approx_black_max_area_pixels2,
|
||||
frame_stats.approx_black_max_area_pixels2);
|
||||
total.approx_black_level_stops += frame_stats.approx_black_level_stops;
|
||||
total.retry_requests += frame_stats.retry_requests;
|
||||
total.budget_incomplete_triangles += frame_stats.budget_incomplete_triangles;
|
||||
}
|
||||
if (out_total != NULL)
|
||||
*out_total = total;
|
||||
if (s->verbose && frame_count > 0)
|
||||
fprintf(stderr,
|
||||
"Boundary accounting: EEE=%zu DDD=%zu EED/EDD=%zu UUD/UDD=%zu "
|
||||
"U+E=%zu UUU=%zu error=%zu; approx-black=%zu (%.6g px^2), "
|
||||
"retries=%zu, budget-incomplete=%zu.\n",
|
||||
total.escaped_only, total.dark_only, total.eed_edd, total.uud_udd,
|
||||
total.u_with_escape, total.uuu, total.error,
|
||||
total.approx_black_triangles, total.approx_black_area_pixels2,
|
||||
total.retry_requests, total.budget_incomplete_triangles);
|
||||
if (s->verbose && total.approx_black_triangles)
|
||||
fprintf(stderr, "Approx-black achieved scale: max edge=%.6g px, max area=%.6g px^2, level stops=%zu.\n",
|
||||
total.approx_black_max_edge_pixels, total.approx_black_max_area_pixels2,
|
||||
total.approx_black_level_stops);
|
||||
}
|
||||
|
||||
static LensMapProvenance lens_map_provenance(const Settings *s,
|
||||
const GeodesicTraceConfig *trace) {
|
||||
RefinementConfig rc = s->refinement;
|
||||
frame_retry_config_defaults(&rc, trace);
|
||||
return (LensMapProvenance){.threshold_kind = (uint32_t)trace->threshold.kind,
|
||||
.threshold_policy_version =
|
||||
trace->threshold.policy_version,
|
||||
.threshold_value = trace->threshold.value,
|
||||
.retry_step_increment = rc.retry_step_increment,
|
||||
.max_total_steps = rc.max_total_steps,
|
||||
.max_level = rc.max_level,
|
||||
.integrator = 0,
|
||||
.min_edge_pixels = rc.min_edge_pixels,
|
||||
.min_area_pixels2 = rc.min_area_pixels2,
|
||||
.coordinate_time_step = trace->coordinate_time_step,
|
||||
.initial_max_steps = trace->max_steps};
|
||||
}
|
||||
|
||||
/* Refuse to publish silently on true errors or on unresolved triangles that
|
||||
* exhausted the configured total budget. Approximate-black UUD/UDD boundary
|
||||
* triangles are an accepted finite-resolution error and do not block output.
|
||||
* A diagnostic run may override this, but the incompleteness is reported. */
|
||||
static int boundary_allows_publish(const Settings *s,
|
||||
const FrameBoundaryStats *total) {
|
||||
const size_t blocking = total->error + total->budget_incomplete_triangles;
|
||||
if (blocking == 0)
|
||||
return 1;
|
||||
if (s->allow_incomplete) {
|
||||
fprintf(stderr,
|
||||
"WARNING: publishing incomplete render (error triangles=%zu, "
|
||||
"budget-incomplete unresolved triangles=%zu).\n",
|
||||
total->error, total->budget_incomplete_triangles);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr,
|
||||
"Incomplete render refused: %zu error triangle(s), %zu "
|
||||
"budget-incomplete unresolved triangle(s). Raise the retry budget or "
|
||||
"pass --allow-incomplete for a diagnostic output.\n",
|
||||
total->error, total->budget_incomplete_triangles);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static GeodesicTraceConfig trace_config(const Settings *s) {
|
||||
/* The normal dark terminal is the camera-relative local energy growth
|
||||
* L - L0, independent of the spacetime backend. A constant camera boost
|
||||
* cancels; the photon energy and frequency ratio are never reset. */
|
||||
const ThresholdPolicy threshold = {.kind = THRESHOLD_LOG_ENERGY_GROWTH,
|
||||
.value = s->dark_threshold,
|
||||
.policy_version = 3};
|
||||
#ifdef SPACETIME_SCHWARZSCHILD
|
||||
(void)s;
|
||||
/* The directed worldtube crossing makes an escaping ray traverse the
|
||||
* interior as a round trip from the entry sphere (in, turn, back out),
|
||||
* rather than the old one-way stop at the first outside sample. The step
|
||||
* budget must cover roughly twice the escape sphere plus margin. */
|
||||
return (GeodesicTraceConfig){.coordinate_time_step = 0.1,
|
||||
.max_steps = 65536,
|
||||
.capture_log_alpha_p0 = 8.0};
|
||||
.threshold = threshold};
|
||||
#elif defined(SPACETIME_ALCUBIERRE)
|
||||
const double step = alcubierre_time_step(s);
|
||||
const double budget = alcubierre_step_budget(s);
|
||||
@@ -886,11 +993,12 @@ static GeodesicTraceConfig trace_config(const Settings *s) {
|
||||
if (max_steps < 1024u)
|
||||
max_steps = 1024u;
|
||||
return (GeodesicTraceConfig){.coordinate_time_step = step,
|
||||
.max_steps = max_steps};
|
||||
.max_steps = max_steps,
|
||||
.threshold = threshold};
|
||||
#else
|
||||
(void)s;
|
||||
return (GeodesicTraceConfig){.coordinate_time_step = 1.0,
|
||||
.max_steps = 2048};
|
||||
.max_steps = 2048,
|
||||
.threshold = threshold};
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -947,17 +1055,13 @@ static int build_observer(const Settings *s, const SpacetimeSource *spacetime,
|
||||
camera.position[i] = s->observer_position[i];
|
||||
camera.velocity[i] = s->observer_velocity[i];
|
||||
}
|
||||
const SpacetimeRayStatus camera_status =
|
||||
spacetime_classify(spacetime, camera.coordinate_time, camera.position);
|
||||
if (camera_status == SPACETIME_RAY_CAPTURED) {
|
||||
fputs("Camera position is inside the backend capture cutoff or invalid.\n", stderr);
|
||||
return -1;
|
||||
}
|
||||
/* A camera outside the escape sphere is supported by the asymptotic
|
||||
* exterior module for every declared end kind; unsupported exteriors are
|
||||
* reported through the ray endpoints instead. */
|
||||
/* Camera legality depends only on metric availability, a timelike
|
||||
* four-velocity, time orientation, and an orthonormal tetrad. A camera
|
||||
* inside a horizon or an old spatial cutoff is a normal rendering target;
|
||||
* its position never decides a ray's terminal category. */
|
||||
MetricData metric;
|
||||
if (spacetime_eval(spacetime, camera.coordinate_time, camera.position, &metric)) {
|
||||
if (spacetime_eval(spacetime, camera.coordinate_time, camera.position,
|
||||
&metric) != SPACETIME_POINT_OK) {
|
||||
fputs("Could not evaluate metric at the camera event.\n", stderr);
|
||||
return -1;
|
||||
}
|
||||
@@ -1032,13 +1136,26 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog,
|
||||
"%zu vertices, %zu triangles.\n",
|
||||
omp_get_wtime() - refinement_start, mesh.vertex_count,
|
||||
mesh.triangle_count);
|
||||
FrameBoundaryStats boundary_totals;
|
||||
{
|
||||
RefinementConfig boundary_config = s->refinement;
|
||||
frame_retry_config_defaults(&boundary_config, &trace);
|
||||
FrameLensMesh *meshes[1] = {&mesh};
|
||||
report_boundary_stats(s, meshes, 1, &boundary_config, &boundary_totals);
|
||||
}
|
||||
if (!boundary_allows_publish(s, &boundary_totals)) {
|
||||
frame_lens_mesh_destroy(&mesh);
|
||||
free(hdr);
|
||||
return -1;
|
||||
}
|
||||
if (s->lens_map_output_path != NULL) {
|
||||
const LensMapFrame map_frame = {.frame_id = 0,
|
||||
.coordinate_time = 0.0,
|
||||
.proper_time = 0.0,
|
||||
.mesh = mesh};
|
||||
const LensMapProvenance provenance = lens_map_provenance(s, &trace);
|
||||
if (lens_map_write(s->lens_map_output_path, s->width, s->height,
|
||||
s->horizontal_fov_deg, &map_frame, 1)) {
|
||||
s->horizontal_fov_deg, &provenance, &map_frame, 1)) {
|
||||
fprintf(stderr, "Failed to write lens map: %s\n", s->lens_map_output_path);
|
||||
frame_lens_mesh_destroy(&mesh);
|
||||
free(hdr);
|
||||
@@ -1113,9 +1230,11 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
|
||||
RayPool rays = {0};
|
||||
size_t ray_count = 0;
|
||||
size_t total_added = 0;
|
||||
RefinementConfig effective = s->refinement;
|
||||
frame_retry_config_defaults(&effective, trace);
|
||||
for (size_t f = 0; f < movie->frame_count; ++f) {
|
||||
const int prepared = frame_lens_mesh_prepare_generation(
|
||||
&movie->frames[f].mesh, &s->refinement);
|
||||
&movie->frames[f].mesh, &effective);
|
||||
if (prepared < 0) return -1;
|
||||
ray_count += (size_t)prepared;
|
||||
}
|
||||
@@ -1128,12 +1247,26 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
|
||||
for (size_t f = 0; f < movie->frame_count; ++f) {
|
||||
size_t count = 0;
|
||||
const FrameSample *samples = frame_lens_mesh_samples(&movie->frames[f].mesh, &count);
|
||||
for (size_t sample = 0; sample < count; ++sample)
|
||||
if (ray_pool_append(&rays, &movie->frames[f].observer,
|
||||
samples[sample].vertex.camera_direction, f, sample)) {
|
||||
for (size_t sample = 0; sample < count; ++sample) {
|
||||
const FrameSample *fs = &samples[sample];
|
||||
if (fs->cached) continue;
|
||||
int rc;
|
||||
if (fs->kind == FRAME_SAMPLE_RETRY) {
|
||||
const LensVertex *v = &fs->vertex;
|
||||
rc = ray_pool_append_continuation(
|
||||
&rays, f, sample, v->continuation_t, v->continuation_x,
|
||||
v->continuation_Pi, v->continuation_log_alpha_p0,
|
||||
v->continuation_log_alpha_p0_0, v->continuation_steps,
|
||||
fs->step_limit);
|
||||
} else {
|
||||
rc = ray_pool_append(&rays, &movie->frames[f].observer,
|
||||
fs->vertex.camera_direction, f, sample);
|
||||
}
|
||||
if (rc) {
|
||||
ray_pool_destroy(&rays);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
ray_pool_preroute(&rays, spacetime);
|
||||
double slab_hi = movie->frames[movie->frame_count - 1].coordinate_time;
|
||||
@@ -1141,29 +1274,35 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
|
||||
while (ray_pool_has_live(&rays)) {
|
||||
const double slab_lo = slab_hi - s->slab_duration;
|
||||
MetricSlab *slab = NULL;
|
||||
size_t pending_before, active_before, terminated_before, failed_before;
|
||||
size_t pending_before, active_before, terminated_before, unresolved_before,
|
||||
failed_before;
|
||||
ray_pool_status_counts(&rays, &pending_before, &active_before,
|
||||
&terminated_before, &failed_before);
|
||||
&terminated_before, &unresolved_before,
|
||||
&failed_before);
|
||||
if (spacetime_load_slab(spacetime, slab_hi, slab_lo, &slab)) {
|
||||
ray_pool_destroy(&rays);
|
||||
return -1;
|
||||
}
|
||||
ray_pool_activate_in_time_range(&rays, slab);
|
||||
size_t pending_active, active_active, terminated_active, failed_active;
|
||||
size_t pending_active, active_active, terminated_active, unresolved_active,
|
||||
failed_active;
|
||||
ray_pool_status_counts(&rays, &pending_active, &active_active,
|
||||
&terminated_active, &failed_active);
|
||||
&terminated_active, &unresolved_active,
|
||||
&failed_active);
|
||||
ray_pool_advance_active(&rays, slab, trace);
|
||||
spacetime_free_slab(slab);
|
||||
size_t pending_after, active_after, terminated_after, failed_after;
|
||||
size_t pending_after, active_after, terminated_after, unresolved_after,
|
||||
failed_after;
|
||||
ray_pool_status_counts(&rays, &pending_after, &active_after,
|
||||
&terminated_after, &failed_after);
|
||||
&terminated_after, &unresolved_after, &failed_after);
|
||||
if (s->verbose)
|
||||
fprintf(stderr,
|
||||
"Ray trace generation %zu, slab %zu [%.6g, %.6g]: activated %zu; "
|
||||
"live %zu -> %zu, terminated %zu, failed %zu.\n",
|
||||
"live %zu -> %zu, terminated %zu, unresolved %zu, failed %zu.\n",
|
||||
generation, ++slab_id, slab_hi, slab_lo,
|
||||
active_active - active_before, pending_before + active_before,
|
||||
pending_after + active_after, terminated_after, failed_after);
|
||||
pending_after + active_after, terminated_after, unresolved_after,
|
||||
failed_after);
|
||||
slab_hi = slab_lo;
|
||||
}
|
||||
for (size_t i = 0; i < rays.count; ++i)
|
||||
@@ -1181,7 +1320,7 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
|
||||
if (movie->frames[f].mesh.sample_count == 0)
|
||||
continue;
|
||||
const int added = frame_lens_mesh_finish_generation(&movie->frames[f].mesh,
|
||||
&s->refinement);
|
||||
&effective);
|
||||
if (added < 0) {
|
||||
fprintf(stderr, "Ray trace generation %zu: frame %zu refinement failed.\n",
|
||||
generation, movie->frames[f].frame_id);
|
||||
@@ -1338,6 +1477,21 @@ static int render_movie(const Settings *s, StarCatalog *catalog,
|
||||
if (traced == 0)
|
||||
break;
|
||||
}
|
||||
{
|
||||
RefinementConfig boundary_config = s->refinement;
|
||||
frame_retry_config_defaults(&boundary_config, &trace);
|
||||
FrameLensMesh **meshes = malloc(movie.frame_count * sizeof *meshes);
|
||||
if (meshes == NULL)
|
||||
goto done;
|
||||
for (size_t i = 0; i < movie.frame_count; ++i)
|
||||
meshes[i] = &movie.frames[i].mesh;
|
||||
FrameBoundaryStats boundary_totals;
|
||||
report_boundary_stats(s, meshes, movie.frame_count, &boundary_config,
|
||||
&boundary_totals);
|
||||
free(meshes);
|
||||
if (!boundary_allows_publish(s, &boundary_totals))
|
||||
goto done;
|
||||
}
|
||||
if (s->lens_map_output_path != NULL) {
|
||||
LensMapFrame *map_frames = calloc(movie.frame_count, sizeof *map_frames);
|
||||
if (map_frames == NULL) goto done;
|
||||
@@ -1346,9 +1500,11 @@ static int render_movie(const Settings *s, StarCatalog *catalog,
|
||||
.coordinate_time = movie.frames[i].coordinate_time,
|
||||
.proper_time = movie.frames[i].proper_time,
|
||||
.mesh = movie.frames[i].mesh};
|
||||
const LensMapProvenance provenance = lens_map_provenance(s, &trace);
|
||||
const int write_failed = lens_map_write(s->lens_map_output_path, s->width,
|
||||
s->height, s->horizontal_fov_deg,
|
||||
map_frames, movie.frame_count);
|
||||
&provenance, map_frames,
|
||||
movie.frame_count);
|
||||
free(map_frames);
|
||||
if (write_failed) {
|
||||
fprintf(stderr, "Failed to write lens map: %s\n", s->lens_map_output_path);
|
||||
@@ -1468,10 +1624,26 @@ done:
|
||||
|
||||
static int render_lens_map(const Settings *s, StarCatalog *catalog) {
|
||||
LensMap map = {0};
|
||||
if (lens_map_read(s->lens_map_input_path, &map)) {
|
||||
if (lens_map_read(s->lens_map_input_path, NULL, &map)) {
|
||||
fprintf(stderr, "Failed to read or validate lens map: %s\n", s->lens_map_input_path);
|
||||
return -1;
|
||||
}
|
||||
RefinementConfig replay_config = {
|
||||
.max_level = map.provenance.max_level,
|
||||
.min_edge_pixels = map.provenance.min_edge_pixels,
|
||||
.min_area_pixels2 = map.provenance.min_area_pixels2,
|
||||
.retry_step_increment = map.provenance.retry_step_increment,
|
||||
.max_total_steps = map.provenance.max_total_steps};
|
||||
int replay_incomplete = 0;
|
||||
/* Replay consumes stored decisions, not current CLI refinement defaults. */
|
||||
for (size_t f = 0; f < map.frame_count; ++f) {
|
||||
FrameBoundaryStats completion;
|
||||
frame_lens_mesh_boundary_stats(&map.frames[f].mesh, &replay_config, &completion);
|
||||
replay_incomplete |= completion.error != 0 || completion.budget_incomplete_triangles != 0;
|
||||
if (!boundary_allows_publish(s, &completion)) {
|
||||
lens_map_destroy(&map); return -1;
|
||||
}
|
||||
}
|
||||
if ((s->width_specified && s->width != map.width) ||
|
||||
(s->height_specified && s->height != map.height) ||
|
||||
(s->fov_specified && fabs(s->horizontal_fov_deg - map.horizontal_fov_deg) > 1e-12)) {
|
||||
@@ -1609,7 +1781,7 @@ static int render_lens_map(const Settings *s, StarCatalog *catalog) {
|
||||
if (prepare_movie_output_job(s, &map.frames[i].mesh, hdr, map.width,
|
||||
map.height, &output_paths,
|
||||
(size_t)map.frames[i].frame_id, images,
|
||||
catalog->count, "; imported lens map",
|
||||
catalog->count, replay_incomplete ? "; INCOMPLETE imported lens map" : "; imported lens map",
|
||||
&psf_stats, &frame_timing, &job)) {
|
||||
free(hdr);
|
||||
result = -1;
|
||||
@@ -1632,7 +1804,7 @@ static int render_lens_map(const Settings *s, StarCatalog *catalog) {
|
||||
const int write_result = write_frame_outputs(
|
||||
s, &map.frames[i].mesh, hdr, map.width, map.height,
|
||||
map.horizontal_fov_deg, &output_paths, images, catalog->count,
|
||||
"; imported lens map", NULL);
|
||||
replay_incomplete ? "; INCOMPLETE imported lens map" : "; imported lens map", NULL);
|
||||
free(hdr);
|
||||
report_psf_splat(s, &psf_stats);
|
||||
if (write_result) { result = -1; break; }
|
||||
@@ -1688,7 +1860,8 @@ int main(int argc, char **argv) {
|
||||
"Usage: %s [--catalog PATH | --all-sky-catalog DIR] [--output PATH] [--width N] [--height "
|
||||
"N] [--fov-deg D] [--look-ra-deg D] [--look-dec-deg D] "
|
||||
"[--lens-map-input FILE | --lens-map-output FILE] "
|
||||
"[--exposure E] [--tone-map softclip|reinhard] [--tone-map-p P] "
|
||||
"[--exposure E] [--dark-threshold T] "
|
||||
"[--tone-map softclip|reinhard] [--tone-map-p P] "
|
||||
"[--sensor-bloom-limit E --sensor-bloom-transfer e] "
|
||||
"[--observer-radius R | --observer-position X Y Z] "
|
||||
"[--observer-velocity VX VY VZ] [--camera-roll-deg ANGLE] "
|
||||
@@ -1696,7 +1869,7 @@ int main(int argc, char **argv) {
|
||||
"[--max-magnification M] [--max-cache-psf-flux F] "
|
||||
"[--psf-relative-tail R] [--psf-min-y Y] "
|
||||
"[--psf-direct] [--fast-mode --fast-supersample N "
|
||||
"--fast-deposit nearest|bilinear] [--verbose] "
|
||||
"--fast-deposit nearest|bilinear] [--verbose] [--allow-incomplete] "
|
||||
#ifdef ENABLE_HDR_OUTPUT
|
||||
"[--hdr-output] "
|
||||
#endif
|
||||
|
||||
@@ -14,7 +14,9 @@ int ray_pool_init(RayPool *p, size_t capacity) {
|
||||
if (!(RAY_ALLOC(t) && RAY_ALLOC(x0) && RAY_ALLOC(x1) && RAY_ALLOC(x2) &&
|
||||
RAY_ALLOC(p0) && RAY_ALLOC(p1) && RAY_ALLOC(p2) && RAY_ALLOC(observer) &&
|
||||
RAY_ALLOC(direction0) && RAY_ALLOC(direction1) && RAY_ALLOC(direction2) &&
|
||||
RAY_ALLOC(log_alpha_p0) && RAY_ALLOC(activate_t) && RAY_ALLOC(steps) &&
|
||||
RAY_ALLOC(log_alpha_p0) && RAY_ALLOC(log_alpha_p0_0) &&
|
||||
RAY_ALLOC(activate_t) && RAY_ALLOC(steps) &&
|
||||
RAY_ALLOC(step_limit) && RAY_ALLOC(continuation) &&
|
||||
RAY_ALLOC(frame_id) && RAY_ALLOC(vertex_id) && RAY_ALLOC(status) &&
|
||||
RAY_ALLOC(endpoint))) {
|
||||
ray_pool_destroy(p);
|
||||
@@ -43,7 +45,55 @@ int ray_pool_append(RayPool *p, const ObserverState *observer,
|
||||
p->status[i] = RAY_POOL_PENDING;
|
||||
p->endpoint[i] = (RayEndpoint){.magnification = 1.0,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.status = RAY_ENDPOINT_INTEGRATION_FAILURE};
|
||||
.outcome = RAY_OUTCOME_INCOMPLETE,
|
||||
.reason = RAY_REASON_NONE,
|
||||
.stop_coordinate_time = NAN,
|
||||
.accepted_steps = 0,
|
||||
.final_x = {NAN, NAN, NAN},
|
||||
.final_Pi = {NAN, NAN, NAN},
|
||||
.final_log_alpha_p0 = NAN,
|
||||
.final_log_alpha_p0_0 = NAN,
|
||||
.threshold_value = NAN};
|
||||
p->step_limit[i] = 0;
|
||||
p->continuation[i] = 0;
|
||||
p->log_alpha_p0_0[i] = 0.0;
|
||||
++p->count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ray_pool_append_continuation(RayPool *p, size_t frame_id,
|
||||
size_t vertex_id, double t, const double x[3],
|
||||
const double Pi[3], double log_alpha_p0,
|
||||
double log_alpha_p0_0, unsigned int steps,
|
||||
unsigned int limit) {
|
||||
if (p == NULL || p->count == p->capacity || x == NULL || Pi == NULL)
|
||||
return -1;
|
||||
const size_t i = p->count;
|
||||
p->t[i] = t;
|
||||
p->activate_t[i] = t;
|
||||
p->observer[i] = NULL;
|
||||
p->direction0[i] = p->direction1[i] = p->direction2[i] = 0.0;
|
||||
p->x0[i] = x[0]; p->x1[i] = x[1]; p->x2[i] = x[2];
|
||||
p->p0[i] = Pi[0]; p->p1[i] = Pi[1]; p->p2[i] = Pi[2];
|
||||
p->log_alpha_p0[i] = log_alpha_p0;
|
||||
p->log_alpha_p0_0[i] = log_alpha_p0_0;
|
||||
p->steps[i] = steps;
|
||||
p->step_limit[i] = limit;
|
||||
p->continuation[i] = 1;
|
||||
p->frame_id[i] = frame_id;
|
||||
p->vertex_id[i] = vertex_id;
|
||||
p->status[i] = RAY_POOL_PENDING;
|
||||
p->endpoint[i] = (RayEndpoint){.magnification = 1.0,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.outcome = RAY_OUTCOME_INCOMPLETE,
|
||||
.reason = RAY_REASON_NONE,
|
||||
.stop_coordinate_time = NAN,
|
||||
.accepted_steps = steps,
|
||||
.final_x = {NAN, NAN, NAN},
|
||||
.final_Pi = {NAN, NAN, NAN},
|
||||
.final_log_alpha_p0 = NAN,
|
||||
.final_log_alpha_p0_0 = log_alpha_p0_0,
|
||||
.threshold_value = NAN};
|
||||
++p->count;
|
||||
return 0;
|
||||
}
|
||||
@@ -53,7 +103,7 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
|
||||
return;
|
||||
#pragma omp parallel for schedule(static)
|
||||
for (size_t i = 0; i < p->count; ++i) {
|
||||
if (p->status[i] != RAY_POOL_PENDING)
|
||||
if (p->status[i] != RAY_POOL_PENDING || p->continuation[i])
|
||||
continue;
|
||||
AsymptoticRoute route;
|
||||
const AsymptoticStatus status = asymptotic_route_camera(
|
||||
@@ -61,13 +111,17 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
|
||||
(double[]){p->direction0[i], p->direction1[i], p->direction2[i]},
|
||||
&route);
|
||||
if (status == ASYMPTOTIC_UNSUPPORTED || status == ASYMPTOTIC_INVALID) {
|
||||
p->endpoint[i].status = RAY_ENDPOINT_INVALID;
|
||||
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
p->endpoint[i].reason = status == ASYMPTOTIC_UNSUPPORTED
|
||||
? RAY_REASON_UNSUPPORTED
|
||||
: RAY_REASON_PROTOCOL_ERROR;
|
||||
p->endpoint[i].end_id = route.end_id;
|
||||
p->status[i] = RAY_POOL_FAILED;
|
||||
continue;
|
||||
}
|
||||
if (status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
|
||||
p->endpoint[i].status = RAY_ENDPOINT_TIME_RANGE_EXHAUSTED;
|
||||
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
p->endpoint[i].reason = RAY_REASON_TIME_RANGE_EXHAUSTED;
|
||||
p->endpoint[i].end_id = route.end_id;
|
||||
p->status[i] = RAY_POOL_TERMINATED;
|
||||
continue;
|
||||
@@ -77,19 +131,22 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
|
||||
p->endpoint[i].n_infinity[axis] = route.n_infinity[axis];
|
||||
p->endpoint[i].frequency_ratio = route.frequency_ratio;
|
||||
p->endpoint[i].end_id = route.end_id;
|
||||
p->endpoint[i].status = RAY_ENDPOINT_ESCAPED;
|
||||
p->endpoint[i].outcome = RAY_OUTCOME_ESCAPED;
|
||||
p->endpoint[i].reason = RAY_REASON_NONE;
|
||||
p->status[i] = RAY_POOL_TERMINATED;
|
||||
continue;
|
||||
}
|
||||
if (route.kind == ASYMPTOTIC_ROUTE_TIME_RANGE_EXHAUSTED) {
|
||||
p->endpoint[i].status = RAY_ENDPOINT_TIME_RANGE_EXHAUSTED;
|
||||
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
p->endpoint[i].reason = RAY_REASON_TIME_RANGE_EXHAUSTED;
|
||||
p->endpoint[i].end_id = route.end_id;
|
||||
p->status[i] = RAY_POOL_TERMINATED;
|
||||
continue;
|
||||
}
|
||||
if (route.kind != ASYMPTOTIC_ROUTE_INSIDE &&
|
||||
route.kind != ASYMPTOTIC_ROUTE_ENTRY) {
|
||||
p->endpoint[i].status = RAY_ENDPOINT_INVALID;
|
||||
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
p->endpoint[i].reason = RAY_REASON_PROTOCOL_ERROR;
|
||||
p->status[i] = RAY_POOL_FAILED;
|
||||
continue;
|
||||
}
|
||||
@@ -101,6 +158,8 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
|
||||
p->p1[i] = route.Pi[1];
|
||||
p->p2[i] = route.Pi[2];
|
||||
p->log_alpha_p0[i] = route.log_alpha_p0;
|
||||
/* Camera-event reference, distinct from the entry-state L. */
|
||||
p->log_alpha_p0_0[i] = route.log_alpha_p0_camera;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +169,9 @@ void ray_pool_activate_in_time_range(RayPool *p, const MetricSlab *slab) {
|
||||
p->activate_t[i] <= slab->t_lo)
|
||||
continue;
|
||||
p->t[i] = p->activate_t[i];
|
||||
p->steps[i] = 0;
|
||||
/* Continuation rays keep the accepted-step count they already consumed. */
|
||||
if (!p->continuation[i])
|
||||
p->steps[i] = 0;
|
||||
p->status[i] = RAY_POOL_ACTIVE;
|
||||
}
|
||||
}
|
||||
@@ -129,16 +190,22 @@ void ray_pool_advance_active(RayPool *p, const MetricSlab *slab,
|
||||
.x = {p->x0[i], p->x1[i], p->x2[i]},
|
||||
.Pi = {p->p0[i], p->p1[i], p->p2[i]},
|
||||
.log_alpha_p0 = p->log_alpha_p0[i],
|
||||
.log_alpha_p0_0 = p->log_alpha_p0_0[i],
|
||||
.steps = p->steps[i]};
|
||||
GeodesicTraceConfig per_ray = *config;
|
||||
if (p->step_limit[i] != 0)
|
||||
per_ray.max_steps = p->step_limit[i];
|
||||
const GeodesicAdvanceResult result =
|
||||
geodesic_advance_past_ray(slab, &s, slab->t_lo, config, &p->endpoint[i]);
|
||||
geodesic_advance_past_ray(slab, &s, slab->t_lo, &per_ray, &p->endpoint[i]);
|
||||
p->t[i] = s.coordinate_time;
|
||||
p->x0[i] = s.x[0]; p->x1[i] = s.x[1]; p->x2[i] = s.x[2];
|
||||
p->p0[i] = s.Pi[0]; p->p1[i] = s.Pi[1]; p->p2[i] = s.Pi[2];
|
||||
p->log_alpha_p0[i] = s.log_alpha_p0;
|
||||
p->steps[i] = s.steps;
|
||||
if (result == GEODESIC_ADVANCE_TERMINATED)
|
||||
p->status[i] = RAY_POOL_TERMINATED;
|
||||
p->status[i] = p->endpoint[i].outcome == RAY_OUTCOME_UNRESOLVED
|
||||
? RAY_POOL_UNRESOLVED
|
||||
: RAY_POOL_TERMINATED;
|
||||
else if (result == GEODESIC_ADVANCE_FAILED)
|
||||
p->status[i] = RAY_POOL_FAILED;
|
||||
}
|
||||
@@ -159,7 +226,9 @@ void ray_pool_destroy(RayPool *p) {
|
||||
free(p->t); free(p->x0); free(p->x1); free(p->x2); free(p->observer);
|
||||
free(p->direction0); free(p->direction1); free(p->direction2);
|
||||
free(p->p0); free(p->p1); free(p->p2); free(p->log_alpha_p0);
|
||||
free(p->activate_t); free(p->steps); free(p->frame_id); free(p->vertex_id);
|
||||
free(p->log_alpha_p0_0);
|
||||
free(p->activate_t); free(p->steps); free(p->step_limit);
|
||||
free(p->continuation); free(p->frame_id); free(p->vertex_id);
|
||||
free(p->status);
|
||||
free(p->endpoint);
|
||||
*p = (RayPool){0};
|
||||
|
||||
@@ -10,11 +10,12 @@ typedef enum {
|
||||
RAY_POOL_PENDING,
|
||||
RAY_POOL_ACTIVE,
|
||||
RAY_POOL_TERMINATED,
|
||||
RAY_POOL_UNRESOLVED, /* trustworthy but budget-exhausted; retryable */
|
||||
RAY_POOL_FAILED
|
||||
} RayPoolStatus;
|
||||
|
||||
typedef struct {
|
||||
double *t, *x0, *x1, *x2, *p0, *p1, *p2, *log_alpha_p0;
|
||||
double *t, *x0, *x1, *x2, *p0, *p1, *p2, *log_alpha_p0, *log_alpha_p0_0;
|
||||
/* Coordinate time at which the pre-routed interior state becomes valid.
|
||||
* For a camera inside a worldtube this equals the camera time; for an
|
||||
* exterior hit it is the earlier entry time. */
|
||||
@@ -22,6 +23,11 @@ typedef struct {
|
||||
const ObserverState **observer;
|
||||
double *direction0, *direction1, *direction2;
|
||||
unsigned int *steps;
|
||||
/* Per-ray total accepted-step limit; zero means use the trace config. */
|
||||
unsigned int *step_limit;
|
||||
/* Nonzero for a retry that resumes from a saved state instead of from the
|
||||
* camera; such rays are not pre-routed. */
|
||||
uint8_t *continuation;
|
||||
size_t *frame_id, *vertex_id;
|
||||
uint8_t *status;
|
||||
RayEndpoint *endpoint;
|
||||
@@ -32,6 +38,13 @@ int ray_pool_init(RayPool *pool, size_t capacity);
|
||||
int ray_pool_append(RayPool *pool, const ObserverState *observer,
|
||||
const double direction[3],
|
||||
size_t frame_id, size_t vertex_id);
|
||||
/* Append a retry that resumes an UNRESOLVED ray from its last accepted state.
|
||||
* `limit` is the new total accepted-step budget for this ray. */
|
||||
int ray_pool_append_continuation(RayPool *pool, size_t frame_id,
|
||||
size_t vertex_id, double t, const double x[3],
|
||||
const double Pi[3], double log_alpha_p0,
|
||||
double log_alpha_p0_0, unsigned int steps,
|
||||
unsigned int limit);
|
||||
/* Pre-route every still-PENDING ray once, before the slab sweep. */
|
||||
void ray_pool_preroute(RayPool *pool, const SpacetimeSource *source);
|
||||
void ray_pool_activate_in_time_range(RayPool *pool, const MetricSlab *slab);
|
||||
|
||||
+24
-12
@@ -14,10 +14,23 @@ typedef struct {
|
||||
double d_gamma[3][3][3]; /* d_gamma[spatial derivative][j][k] */
|
||||
} MetricData;
|
||||
|
||||
/* Result of evaluating the metric at one event. `OK` is zero so that legacy
|
||||
* `if (eval(...))` call sites keep working. These codes describe data
|
||||
* availability only; they never express a physical capture. */
|
||||
typedef enum {
|
||||
SPACETIME_POINT_OK = 0,
|
||||
SPACETIME_POINT_TIME_UNAVAILABLE,
|
||||
SPACETIME_POINT_OUT_OF_DOMAIN,
|
||||
SPACETIME_POINT_INVALID_METRIC,
|
||||
SPACETIME_POINT_INTERNAL_ERROR
|
||||
} SpacetimePointStatus;
|
||||
|
||||
/* Optional legacy region query for backends that declare no asymptotic end.
|
||||
* It can only report ACTIVE or ESCAPED; it can never report a physical
|
||||
* capture, and it is not required by spacetime_source_finalize(). */
|
||||
typedef enum {
|
||||
SPACETIME_RAY_ACTIVE,
|
||||
SPACETIME_RAY_ESCAPED,
|
||||
SPACETIME_RAY_CAPTURED
|
||||
SPACETIME_RAY_ESCAPED
|
||||
} SpacetimeRayStatus;
|
||||
|
||||
/* Stable identifier for one asymptotic end (infinity) of a backend. Backends
|
||||
@@ -66,15 +79,15 @@ struct MetricSlab {
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
int (*eval)(const SpacetimeSource *source, double t, const double x[3],
|
||||
MetricData *metric);
|
||||
SpacetimePointStatus (*eval)(const SpacetimeSource *source, double t,
|
||||
const double x[3], MetricData *metric);
|
||||
SpacetimeRayStatus (*classify)(const SpacetimeSource *source, double t,
|
||||
const double x[3]);
|
||||
int (*load_slab)(const SpacetimeSource *source, double t_hi, double t_lo,
|
||||
MetricSlab **out);
|
||||
void (*free_slab)(MetricSlab *slab);
|
||||
int (*eval_slab)(const MetricSlab *slab, double t, const double x[3],
|
||||
MetricData *metric);
|
||||
SpacetimePointStatus (*eval_slab)(const MetricSlab *slab, double t,
|
||||
const double x[3], MetricData *metric);
|
||||
SpacetimeRayStatus (*classify_slab)(const MetricSlab *slab, double t,
|
||||
const double x[3]);
|
||||
/* Declared asymptotic ends and their moving escape worldtubes. Backends
|
||||
@@ -108,8 +121,7 @@ struct SpacetimeSource {
|
||||
int spacetime_create_default(SpacetimeSource *source);
|
||||
int spacetime_create_minkowski(SpacetimeSource *source, double escape_radius);
|
||||
int spacetime_create_schwarzschild_ks(SpacetimeSource *source, double mass,
|
||||
double escape_radius,
|
||||
double capture_radius);
|
||||
double escape_radius);
|
||||
/* Moving Alcubierre bubble with x_s(t) = vs*t and x_s(0) = 0. Requires
|
||||
* |vs| < 1, R > 0, and sigma > 0. */
|
||||
int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
|
||||
@@ -118,15 +130,15 @@ int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
|
||||
* callers size their integration step budget. */
|
||||
double spacetime_alcubierre_escape_radius(double radius, double sigma);
|
||||
void spacetime_destroy(SpacetimeSource *source);
|
||||
int spacetime_eval(const SpacetimeSource *source, double t, const double x[3],
|
||||
MetricData *metric);
|
||||
SpacetimePointStatus spacetime_eval(const SpacetimeSource *source, double t,
|
||||
const double x[3], MetricData *metric);
|
||||
SpacetimeRayStatus spacetime_classify(const SpacetimeSource *source, double t,
|
||||
const double x[3]);
|
||||
int spacetime_load_slab(const SpacetimeSource *source, double t_hi, double t_lo,
|
||||
MetricSlab **out);
|
||||
void spacetime_free_slab(MetricSlab *slab);
|
||||
int spacetime_slab_eval(const MetricSlab *slab, double t, const double x[3],
|
||||
MetricData *metric);
|
||||
SpacetimePointStatus spacetime_slab_eval(const MetricSlab *slab, double t,
|
||||
const double x[3], MetricData *metric);
|
||||
SpacetimeRayStatus spacetime_slab_classify(const MetricSlab *slab, double t,
|
||||
const double x[3]);
|
||||
size_t spacetime_asymptotic_end_count(const SpacetimeSource *source);
|
||||
|
||||
@@ -72,8 +72,9 @@ static double alcubierre_shape_derivative(double r, double radius,
|
||||
* = -v_s (delta_jx d_i f + delta_ix d_j f) / 2,
|
||||
* where d_i differentiates at fixed t (only the spatial argument of f moves
|
||||
* with t). K encodes the time dependence required by the 3+1 null-ray RHS. */
|
||||
static int alcubierre_eval(const SpacetimeSource *source, double t,
|
||||
const double x[3], MetricData *metric) {
|
||||
static SpacetimePointStatus alcubierre_eval(const SpacetimeSource *source,
|
||||
double t, const double x[3],
|
||||
MetricData *metric) {
|
||||
const AlcubierreContext *context = source->context;
|
||||
const double vs = context->vs;
|
||||
const double dx = x[0] - vs * t;
|
||||
@@ -81,7 +82,7 @@ static int alcubierre_eval(const SpacetimeSource *source, double t,
|
||||
double df[3] = {0.0, 0.0, 0.0};
|
||||
double f;
|
||||
if (!isfinite(r2))
|
||||
return -1;
|
||||
return SPACETIME_POINT_INVALID_METRIC;
|
||||
const double r = sqrt(r2);
|
||||
*metric = (MetricData){
|
||||
.alpha = 1.0,
|
||||
@@ -103,7 +104,7 @@ static int alcubierre_eval(const SpacetimeSource *source, double t,
|
||||
metric->K[i][j] =
|
||||
-0.5 * vs * ((j == 0 ? df[i] : 0.0) + (i == 0 ? df[j] : 0.0));
|
||||
}
|
||||
return 0;
|
||||
return SPACETIME_POINT_OK;
|
||||
}
|
||||
|
||||
/* A warp bubble has no curvature singularity or horizon for |v_s| < 1, so
|
||||
|
||||
+18
-13
@@ -9,17 +9,18 @@ void spacetime_destroy(SpacetimeSource *source) {
|
||||
source->ops->destroy(source);
|
||||
}
|
||||
|
||||
int spacetime_eval(const SpacetimeSource *source, double t, const double x[3],
|
||||
MetricData *metric) {
|
||||
return source == NULL || source->ops == NULL
|
||||
? -1
|
||||
SpacetimePointStatus spacetime_eval(const SpacetimeSource *source, double t,
|
||||
const double x[3], MetricData *metric) {
|
||||
return source == NULL || source->ops == NULL || source->ops->eval == NULL
|
||||
? SPACETIME_POINT_INTERNAL_ERROR
|
||||
: source->ops->eval(source, t, x, metric);
|
||||
}
|
||||
|
||||
SpacetimeRayStatus spacetime_classify(const SpacetimeSource *source, double t,
|
||||
const double x[3]) {
|
||||
return source == NULL || source->ops == NULL
|
||||
? SPACETIME_RAY_CAPTURED
|
||||
/* A missing or incomplete source is never reported as escaped. */
|
||||
return source == NULL || source->ops == NULL || source->ops->classify == NULL
|
||||
? SPACETIME_RAY_ACTIVE
|
||||
: source->ops->classify(source, t, x);
|
||||
}
|
||||
|
||||
@@ -47,10 +48,12 @@ void spacetime_free_slab(MetricSlab *slab) {
|
||||
free(slab);
|
||||
}
|
||||
|
||||
int spacetime_slab_eval(const MetricSlab *slab, double t, const double x[3],
|
||||
MetricData *metric) {
|
||||
if (slab == NULL || t < slab->t_lo || t > slab->t_hi)
|
||||
return -1;
|
||||
SpacetimePointStatus spacetime_slab_eval(const MetricSlab *slab, double t,
|
||||
const double x[3], MetricData *metric) {
|
||||
if (slab == NULL || slab->source == NULL || slab->source->ops == NULL)
|
||||
return SPACETIME_POINT_INTERNAL_ERROR;
|
||||
if (t < slab->t_lo || t > slab->t_hi)
|
||||
return SPACETIME_POINT_TIME_UNAVAILABLE;
|
||||
if (slab->source->ops->eval_slab != NULL)
|
||||
return slab->source->ops->eval_slab(slab, t, x, metric);
|
||||
return spacetime_eval(slab->source, t, x, metric);
|
||||
@@ -58,8 +61,10 @@ int spacetime_slab_eval(const MetricSlab *slab, double t, const double x[3],
|
||||
|
||||
SpacetimeRayStatus spacetime_slab_classify(const MetricSlab *slab, double t,
|
||||
const double x[3]) {
|
||||
if (slab == NULL || t < slab->t_lo || t > slab->t_hi)
|
||||
return SPACETIME_RAY_CAPTURED;
|
||||
if (slab == NULL || slab->source == NULL)
|
||||
return SPACETIME_RAY_ACTIVE;
|
||||
if (t < slab->t_lo || t > slab->t_hi)
|
||||
return SPACETIME_RAY_ACTIVE;
|
||||
if (slab->source->ops->classify_slab != NULL)
|
||||
return slab->source->ops->classify_slab(slab, t, x);
|
||||
return spacetime_classify(slab->source, t, x);
|
||||
@@ -102,7 +107,7 @@ int spacetime_source_finalize(SpacetimeSource *source) {
|
||||
if (source == NULL || source->ops == NULL || source->context == NULL)
|
||||
return -1;
|
||||
const SpacetimeOps *ops = source->ops;
|
||||
if (ops->eval == NULL || ops->classify == NULL || ops->destroy == NULL)
|
||||
if (ops->eval == NULL || ops->destroy == NULL)
|
||||
return -1;
|
||||
const size_t count = spacetime_asymptotic_end_count(source);
|
||||
if (count == 0)
|
||||
|
||||
@@ -6,15 +6,16 @@ typedef struct {
|
||||
double escape_radius;
|
||||
} MinkowskiContext;
|
||||
|
||||
static int minkowski_eval(const SpacetimeSource *source, double t,
|
||||
const double x[3], MetricData *metric) {
|
||||
static SpacetimePointStatus minkowski_eval(const SpacetimeSource *source,
|
||||
double t, const double x[3],
|
||||
MetricData *metric) {
|
||||
(void)source;
|
||||
(void)t;
|
||||
(void)x;
|
||||
*metric = (MetricData){
|
||||
.alpha = 1.0,
|
||||
.gamma = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
|
||||
return 0;
|
||||
return SPACETIME_POINT_OK;
|
||||
}
|
||||
|
||||
static SpacetimeRayStatus minkowski_classify(const SpacetimeSource *source,
|
||||
|
||||
@@ -6,22 +6,26 @@
|
||||
typedef struct {
|
||||
double mass;
|
||||
double escape_radius;
|
||||
double capture_radius;
|
||||
} SchwarzschildKsContext;
|
||||
|
||||
/* Schwarzschild in ingoing Cartesian Kerr--Schild coordinates:
|
||||
* g_mu_nu = eta_mu_nu + (2 M / r) l_mu l_nu, l_mu = (1, x_i / r).
|
||||
* These slices are regular at r = 2 M; only the physical r = 0 singularity
|
||||
* is excluded by the conservative capture cutoff. */
|
||||
static int schwarzschild_ks_eval(const SpacetimeSource *source, double t,
|
||||
const double x[3], MetricData *metric) {
|
||||
* These slices are regular at r = 2 M. Only r = 0 is a coordinate
|
||||
* singularity; it is reported as a data/domain status, not as a physical
|
||||
* capture. Normal dark endpoints come from the redshift threshold in the
|
||||
* geodesic layer (see design section 18). */
|
||||
static SpacetimePointStatus schwarzschild_ks_eval(const SpacetimeSource *source,
|
||||
double t, const double x[3],
|
||||
MetricData *metric) {
|
||||
const SchwarzschildKsContext *context = source->context;
|
||||
double r2 = 0.0;
|
||||
(void)t;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
r2 += x[i] * x[i];
|
||||
if (!isfinite(r2) || r2 <= 0.0)
|
||||
return -1;
|
||||
if (!isfinite(r2))
|
||||
return SPACETIME_POINT_INVALID_METRIC;
|
||||
if (r2 <= 0.0)
|
||||
return SPACETIME_POINT_OUT_OF_DOMAIN; /* r = 0 coordinate singularity */
|
||||
const double r = sqrt(r2);
|
||||
const double m = context->mass;
|
||||
const double f = 2.0 * m / r;
|
||||
@@ -79,11 +83,14 @@ static int schwarzschild_ks_eval(const SpacetimeSource *source, double t,
|
||||
}
|
||||
metric->K[i][j] = (d_beta_cov_i_j - connection_term_ij +
|
||||
d_beta_cov_j_i - connection_term_ji) /
|
||||
(2.0 * alpha);
|
||||
(2.0 * alpha);
|
||||
}
|
||||
return 0;
|
||||
return SPACETIME_POINT_OK;
|
||||
}
|
||||
|
||||
/* Optional legacy region test: reports the escape sphere only. It never
|
||||
* reports a physical capture; the normal dark terminal is the redshift
|
||||
* threshold in the geodesic layer. */
|
||||
static SpacetimeRayStatus schwarzschild_ks_classify(
|
||||
const SpacetimeSource *source, double t, const double x[3]) {
|
||||
const SchwarzschildKsContext *context = source->context;
|
||||
@@ -91,8 +98,8 @@ static SpacetimeRayStatus schwarzschild_ks_classify(
|
||||
(void)t;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
r2 += x[i] * x[i];
|
||||
if (!isfinite(r2) || r2 <= context->capture_radius * context->capture_radius)
|
||||
return SPACETIME_RAY_CAPTURED;
|
||||
if (!isfinite(r2))
|
||||
return SPACETIME_RAY_ACTIVE;
|
||||
return r2 >= context->escape_radius * context->escape_radius
|
||||
? SPACETIME_RAY_ESCAPED
|
||||
: SPACETIME_RAY_ACTIVE;
|
||||
@@ -150,16 +157,13 @@ static const SpacetimeOps schwarzschild_ks_ops = {
|
||||
};
|
||||
|
||||
int spacetime_create_schwarzschild_ks(SpacetimeSource *source, double mass,
|
||||
double escape_radius,
|
||||
double capture_radius) {
|
||||
if (source == NULL || mass <= 0.0 || escape_radius <= 2.0 * mass ||
|
||||
capture_radius <= 0.0 || capture_radius >= 2.0 * mass ||
|
||||
capture_radius >= escape_radius)
|
||||
double escape_radius) {
|
||||
if (source == NULL || mass <= 0.0 || escape_radius <= 2.0 * mass)
|
||||
return -1;
|
||||
SchwarzschildKsContext *context = malloc(sizeof *context);
|
||||
if (context == NULL)
|
||||
return -1;
|
||||
*context = (SchwarzschildKsContext){mass, escape_radius, capture_radius};
|
||||
*context = (SchwarzschildKsContext){mass, escape_radius};
|
||||
source->ops = &schwarzschild_ks_ops;
|
||||
source->context = context;
|
||||
if (spacetime_source_finalize(source)) {
|
||||
@@ -170,5 +174,5 @@ int spacetime_create_schwarzschild_ks(SpacetimeSource *source, double mass,
|
||||
}
|
||||
|
||||
int spacetime_create_default(SpacetimeSource *source) {
|
||||
return spacetime_create_schwarzschild_ks(source, 1.0, 256.0, 1.5);
|
||||
return spacetime_create_schwarzschild_ks(source, 1.0, 256.0);
|
||||
}
|
||||
Reference in new issue
Block a user