Frame: add adaptive mesh refinement

This commit is contained in:
wyj committed 2026-08-28 23:06:53 -04:00
1 parent 4aa5f6666a
commit 48dcf4e707
7 files changed
+768 -72

No files matched your search

+52
View File
@@ -142,6 +142,58 @@ metric or its infinity criterion; frame, observer, and integrator use only
as `--coarse-cell-pixels`; it is a Phase-0 sampling knob, not a settled as `--coarse-cell-pixels`; it is a Phase-0 sampling knob, not a settled
production refinement threshold. production refinement threshold.
### Adaptive image mesh refinement
Adaptive refinement is disabled by default (`--refine-max-level 0`), so the
existing coarse-mesh renders remain unchanged. When enabled, its defaults are
an absolute direction error of `1e-3` degrees, relative error `0.1`, minimum
long edge `0.5` pixels, and minimum area `0.25` pixel-squared. Each value can
be overridden independently:
```text
--refine-max-level N
--refine-angle-abs-deg D
--refine-angle-rel R
--refine-min-edge-pixels P
--refine-min-area-pixels2 A
```
`N` caps the triangle refinement level. Let `e` be the angle between the
traced longest-edge midpoint direction and the normalized endpoint
interpolation, and let `s` be the angle between those two endpoint **camera
directions**. Both are evaluated internally in radians; the absolute CLI
threshold `D` is specified in degrees and converted before comparison. `s`
is the angular geometric size of the image triangle's test edge, not a
source-sky/lens-map length. A locally escaped triangle is split only when
**both** `e > D_rad` (the converted `--refine-angle-abs-deg D`) and
`e / max(s, 1e-15) > --refine-angle-rel`. `P` and `A`
prevent selecting a leaf already at or below the requested image-plane
long-edge and area scales.
Triangles whose three vertices disagree between capture and escape are split
independently of the direction-error thresholds, allowing the mesh to follow a
shadow boundary. This first implementation deliberately does not evaluate
orientation or Jacobian criteria.
For a short Schwarzschild diagnostic that permits at most one actual split
generation, for example:
```sh
./build/schwarzschild_sky --catalog assets/sky_grid_5deg.csv \
--width 48 --height 48 --coarse-cell-pixels 24 --fov-deg 40 \
--refine-max-level 1 --refine-angle-abs-deg 0.001 \
--refine-angle-rel 0.001 --refine-min-edge-pixels 1 \
--refine-min-area-pixels2 1 --draw-mesh \
--output output/imgs/schwarzschild_refinement.png
```
For movies, each refinement generation completes the full newest-to-oldest
time-slab sweep before any probe becomes a mesh vertex. Newly added vertices
are therefore traced only by the next generation; the renderer never returns
to a slab that has already been released. At the start of every generation,
newly inserted vertices and geometry-only longest-edge probes for its new
leaves are collected together, so both ray sets use the same parallel
`RayPool` pass.
`--look-ra-deg` and `--look-dec-deg` rotate that fixed tetrad so its forward `--look-ra-deg` and `--look-dec-deg` rotate that fixed tetrad so its forward
axis is the corresponding catalog direction; their defaults reproduce the axis is the corresponding catalog direction; their defaults reproduce the
original `-Z` view. `--exposure` converts a catalog's physical flux original `-Z` view. `--exposure` converts a catalog's physical flux
+522 -3
View File
@@ -7,6 +7,7 @@
#include <omp.h> #include <omp.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
/* Numerical metric backends may reserve substantial memory for slabs and /* Numerical metric backends may reserve substantial memory for slabs and
* thread-local evaluators, so they retain this private-HDR allocation budget. * thread-local evaluators, so they retain this private-HDR allocation budget.
@@ -76,14 +77,16 @@ 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_left = vertex_index(column, row + 1, columns);
const size_t bottom_right = vertex_index(column + 1, row + 1, columns); const size_t bottom_right = vertex_index(column + 1, row + 1, columns);
triangles[next_triangle++] = triangles[next_triangle++] =
(LensTriangle){{top_left, bottom_left, bottom_right}}; (LensTriangle){{top_left, bottom_left, bottom_right}, 0, 0};
triangles[next_triangle++] = triangles[next_triangle++] =
(LensTriangle){{top_left, bottom_right, top_right}}; (LensTriangle){{top_left, bottom_right, top_right}, 0, 0};
} }
*mesh = (FrameLensMesh){.vertices = vertices, *mesh = (FrameLensMesh){.vertices = vertices,
.triangles = triangles, .triangles = triangles,
.vertex_count = vertex_count, .vertex_count = vertex_count,
.triangle_count = triangle_count}; .vertex_capacity = vertex_count,
.triangle_count = triangle_count,
.triangle_capacity = triangle_count};
return 0; return 0;
} }
@@ -101,6 +104,7 @@ int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime,
RayEndpoint endpoint = geodesic_trace_past(spacetime, observer, RayEndpoint endpoint = geodesic_trace_past(spacetime, observer,
vertex->camera_direction, trace); vertex->camera_direction, trace);
vertex->status = endpoint.status; vertex->status = endpoint.status;
vertex->traced = 1;
if (endpoint.status == RAY_ENDPOINT_ESCAPED) { if (endpoint.status == RAY_ENDPOINT_ESCAPED) {
for (int axis = 0; axis < 3; ++axis) for (int axis = 0; axis < 3; ++axis)
vertex->n_infinity[axis] = endpoint.n_infinity[axis]; vertex->n_infinity[axis] = endpoint.n_infinity[axis];
@@ -110,6 +114,519 @@ int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime,
return 0; return 0;
} }
typedef struct {
size_t a, b, triangle;
unsigned int side;
size_t midpoint;
} MeshEdge;
static int compare_mesh_edge(const void *left, const void *right) {
const MeshEdge *a = left, *b = right;
if (a->a != b->a)
return a->a < b->a ? -1 : 1;
if (a->b != b->b)
return a->b < b->b ? -1 : 1;
return 0;
}
static double image_edge_length(const LensVertex *a, const LensVertex *b) {
return hypot(a->image_x - b->image_x, a->image_y - b->image_y);
}
static double image_triangle_area(const LensVertex *a, const LensVertex *b,
const LensVertex *c) {
return 0.5 * fabs((b->image_x - a->image_x) * (c->image_y - a->image_y) -
(b->image_y - a->image_y) * (c->image_x - a->image_x));
}
static unsigned int longest_side(const FrameLensMesh *mesh,
const LensTriangle *triangle) {
unsigned int best = 0;
double best_length = -1.0;
for (unsigned int side = 0; side < 3; ++side) {
const double length = image_edge_length(
&mesh->vertices[triangle->vertex[side]],
&mesh->vertices[triangle->vertex[(side + 1) % 3]]);
if (length > best_length) {
best_length = length;
best = side;
}
}
return best;
}
static int ensure_samples(FrameLensMesh *mesh, size_t count) {
if (count <= mesh->sample_capacity)
return 0;
size_t capacity = mesh->sample_capacity ? mesh->sample_capacity : 16;
while (capacity < count)
capacity *= 2;
FrameSample *samples = realloc(mesh->samples, capacity * sizeof *samples);
if (samples == NULL)
return -1;
mesh->samples = samples;
mesh->sample_capacity = capacity;
return 0;
}
static int ensure_vertices(FrameLensMesh *mesh, size_t count) {
if (count <= mesh->vertex_capacity)
return 0;
size_t capacity = mesh->vertex_capacity ? mesh->vertex_capacity : 16;
while (capacity < count)
capacity *= 2;
LensVertex *vertices = realloc(mesh->vertices, capacity * sizeof *vertices);
if (vertices == NULL)
return -1;
mesh->vertices = vertices;
mesh->vertex_capacity = capacity;
return 0;
}
static int all_vertices_traced(const FrameLensMesh *mesh) {
for (size_t i = 0; i < mesh->vertex_count; ++i)
if (!mesh->vertices[i].traced)
return 0;
return 1;
}
static int add_sample(FrameLensMesh *mesh, const FrameSample *sample) {
if (ensure_samples(mesh, mesh->sample_count + 1))
return -1;
mesh->samples[mesh->sample_count++] = *sample;
return 0;
}
static size_t probe_hash(size_t a, size_t b) {
uint64_t value = (uint64_t)a * UINT64_C(0x9e3779b185ebca87) ^
(uint64_t)b * UINT64_C(0xc2b2ae3d27d4eb4f);
value ^= value >> 33;
return (size_t)value;
}
static int prepare_probe_index(FrameLensMesh *mesh) {
size_t capacity = 16;
while (capacity < mesh->triangle_count * 2)
capacity *= 2;
if (capacity > mesh->probe_slot_capacity) {
size_t *slots = realloc(mesh->probe_slots, capacity * sizeof *slots);
if (slots == NULL)
return -1;
mesh->probe_slots = slots;
mesh->probe_slot_capacity = capacity;
}
memset(mesh->probe_slots, 0, mesh->probe_slot_capacity * sizeof *mesh->probe_slots);
return 0;
}
static size_t find_probe(const FrameLensMesh *mesh, size_t a, size_t b) {
if (a > b) { size_t swap = a; a = b; b = swap; }
if (mesh->probe_slot_capacity == 0)
return SIZE_MAX;
size_t slot = probe_hash(a, b) & (mesh->probe_slot_capacity - 1);
while (mesh->probe_slots[slot] != 0) {
const size_t sample_id = mesh->probe_slots[slot] - 1;
const FrameSample *sample = &mesh->samples[sample_id];
size_t x = sample->edge_vertex[0], y = sample->edge_vertex[1];
if (x > y) { size_t swap = x; x = y; y = swap; }
if (x == a && y == b)
return sample_id;
slot = (slot + 1) & (mesh->probe_slot_capacity - 1);
}
return SIZE_MAX;
}
static void index_probe(FrameLensMesh *mesh, size_t sample_id) {
FrameSample *sample = &mesh->samples[sample_id];
size_t a = sample->edge_vertex[0], b = sample->edge_vertex[1];
if (a > b) { size_t swap = a; a = b; b = swap; }
size_t slot = probe_hash(a, b) & (mesh->probe_slot_capacity - 1);
while (mesh->probe_slots[slot] != 0)
slot = (slot + 1) & (mesh->probe_slot_capacity - 1);
mesh->probe_slots[slot] = sample_id + 1;
}
int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
const RefinementConfig *config) {
if (mesh == NULL || config == NULL || mesh->sample_count != 0)
return -1;
mesh->samples_include_probes = 0;
if (config->max_level > 0 && prepare_probe_index(mesh))
return -1;
const int has_untraced_vertices = !all_vertices_traced(mesh);
if (has_untraced_vertices) {
for (size_t i = 0; i < mesh->vertex_count; ++i)
if (!mesh->vertices[i].traced &&
add_sample(mesh, &(FrameSample){.kind = FRAME_SAMPLE_VERTEX,
.vertex_id = i,
.vertex = mesh->vertices[i]}))
return -1;
}
if (config->max_level == 0)
return 0;
/* 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. */
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 unsigned int side = longest_side(mesh, triangle);
const size_t a = triangle->vertex[side];
const size_t b = triangle->vertex[(side + 1) % 3];
if (find_probe(mesh, a, b) != SIZE_MAX)
continue;
FrameSample probe = {.kind = FRAME_SAMPLE_PROBE, .edge_vertex = {a, b}};
const LensVertex *left = &mesh->vertices[a];
const LensVertex *right = &mesh->vertices[b];
probe.vertex.image_x = 0.5 * (left->image_x + right->image_x);
probe.vertex.image_y = 0.5 * (left->image_y + right->image_y);
for (int axis = 0; axis < 3; ++axis)
probe.vertex.camera_direction[axis] =
left->camera_direction[axis] + right->camera_direction[axis];
if (normalize(probe.vertex.camera_direction) == 0.0)
return -1;
if (add_sample(mesh, &probe))
return -1;
index_probe(mesh, mesh->sample_count - 1);
}
mesh->samples_include_probes = mesh->sample_count != 0;
return (int)mesh->sample_count;
}
const FrameSample *frame_lens_mesh_samples(const FrameLensMesh *mesh,
size_t *count) {
if (count != NULL)
*count = mesh == NULL ? 0 : mesh->sample_count;
return mesh == NULL ? NULL : mesh->samples;
}
int frame_lens_mesh_install_sample(FrameLensMesh *mesh, size_t sample_id,
const RayEndpoint *endpoint) {
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->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);
}
return 0;
}
static int terminal_mismatch(const LensVertex *a, const LensVertex *b,
const LensVertex *c) {
int escaped = 0, captured = 0;
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;
}
return escaped && captured;
}
static double direction_angle(const double a[3], const double b[3]) {
const double product = fmax(-1.0, fmin(1.0, dot(a, b)));
return acos(product);
}
static int probe_requires_split(const FrameLensMesh *mesh,
const LensTriangle *triangle,
unsigned int side,
const RefinementConfig *config) {
const LensVertex *a = &mesh->vertices[triangle->vertex[side]];
const LensVertex *b = &mesh->vertices[triangle->vertex[(side + 1) % 3]];
const LensVertex *c = &mesh->vertices[triangle->vertex[(side + 2) % 3]];
if (terminal_mismatch(a, b, c))
return 1;
const size_t probe_id = find_probe(mesh, triangle->vertex[side],
triangle->vertex[(side + 1) % 3]);
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)
return 0;
double predicted[3] = {a->n_infinity[0] + b->n_infinity[0],
a->n_infinity[1] + b->n_infinity[1],
a->n_infinity[2] + b->n_infinity[2]};
if (normalize(predicted) == 0.0)
return 1;
const double error = direction_angle(predicted, probe->n_infinity);
/* The relative error is normalized by the image triangle's own angular
* scale (the camera-direction span of its longest edge), not by the
* source-side lens mapping. */
const double scale = fmax(direction_angle(a->camera_direction,
b->camera_direction), 1e-15);
return error > config->angle_absolute_rad &&
error / scale > config->angle_relative;
}
static int triangle_allows_children(const FrameLensMesh *mesh,
const LensTriangle *triangle,
const RefinementConfig *config) {
const LensVertex *a = &mesh->vertices[triangle->vertex[0]];
const LensVertex *b = &mesh->vertices[triangle->vertex[1]];
const LensVertex *c = &mesh->vertices[triangle->vertex[2]];
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;
}
static LensVertex midpoint_vertex(const LensVertex *a, const LensVertex *b) {
LensVertex result = {.image_x = 0.5 * (a->image_x + b->image_x),
.image_y = 0.5 * (a->image_y + b->image_y)};
for (int axis = 0; axis < 3; ++axis)
result.camera_direction[axis] = a->camera_direction[axis] + b->camera_direction[axis];
normalize(result.camera_direction);
return result;
}
static int append_triangle(LensTriangle *triangles, size_t *count,
size_t capacity, size_t a, size_t b, size_t c,
unsigned int level, int evaluated) {
if (*count >= capacity)
return -1;
triangles[(*count)++] = (LensTriangle){{a, b, c}, level, evaluated};
return 0;
}
int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
const RefinementConfig *config) {
if (mesh == NULL || config == NULL || mesh->sample_count == 0)
return -1;
for (size_t i = 0; i < mesh->sample_count; ++i)
if (!mesh->samples[i].vertex.traced &&
mesh->samples[i].kind == FRAME_SAMPLE_PROBE)
return -1;
if (!mesh->samples_include_probes) {
mesh->sample_count = 0;
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;
const size_t edge_count = mesh->triangle_count * 3;
MeshEdge *edges = calloc(edge_count, sizeof *edges);
unsigned char *requested = calloc(edge_count, sizeof *requested);
unsigned char *allowed = calloc(mesh->triangle_count, sizeof *allowed);
if (edges == NULL || requested == NULL || allowed == NULL) {
free(edges); free(requested); free(allowed);
return -1;
}
for (size_t i = 0; i < mesh->triangle_count; ++i) {
const LensTriangle *triangle = &mesh->triangles[i];
allowed[i] = triangle->level < config->max_level &&
triangle_allows_children(mesh, triangle, config);
for (unsigned int side = 0; side < 3; ++side) {
size_t a = triangle->vertex[side], b = triangle->vertex[(side + 1) % 3];
if (a > b) { size_t swap = a; a = b; b = swap; }
edges[3 * i + side] = (MeshEdge){a, b, i, side, SIZE_MAX};
}
if (allowed[i]) {
const unsigned int side = longest_side(mesh, triangle);
requested[3 * i + side] = probe_requires_split(mesh, triangle, side, config);
}
}
qsort(edges, edge_count, sizeof *edges, compare_mesh_edge);
/* A requested interior edge is split by both incident leaves, preserving a
* conforming mesh. If either side has reached its geometric limit, reject
* the whole edge instead of introducing a T-junction. */
for (size_t first = 0; first < edge_count;) {
size_t last = first + 1;
while (last < edge_count && edges[last].a == edges[first].a &&
edges[last].b == edges[first].b)
++last;
int any = 0, possible = 1;
for (size_t i = first; i < last; ++i) {
const MeshEdge *edge = &edges[i];
any |= requested[3 * edge->triangle + edge->side] != 0;
possible &= allowed[edge->triangle] != 0;
}
if (any && possible)
for (size_t i = first; i < last; ++i)
requested[3 * edges[i].triangle + edges[i].side] = 1;
else if (any)
for (size_t i = first; i < last; ++i)
requested[3 * edges[i].triangle + edges[i].side] = 0;
first = last;
}
/* Two requested sides require red refinement. Add the third side, then
* close the new shared edge requests before allocating any vertices. */
for (;;) {
int changed = 0;
for (size_t t = 0; t < mesh->triangle_count; ++t) {
unsigned int count = 0;
for (unsigned int side = 0; side < 3; ++side)
count += requested[3 * t + side] != 0;
if (count >= 2 && allowed[t])
for (unsigned int side = 0; side < 3; ++side)
if (!requested[3 * t + side]) {
requested[3 * t + side] = 1;
changed = 1;
}
}
for (size_t first = 0; first < edge_count;) {
size_t last = first + 1;
while (last < edge_count && edges[last].a == edges[first].a &&
edges[last].b == edges[first].b)
++last;
int any = 0, possible = 1;
for (size_t i = first; i < last; ++i) {
any |= requested[3 * edges[i].triangle + edges[i].side] != 0;
possible &= allowed[edges[i].triangle] != 0;
}
if (any && possible)
for (size_t i = first; i < last; ++i)
if (!requested[3 * edges[i].triangle + edges[i].side]) {
requested[3 * edges[i].triangle + edges[i].side] = 1;
changed = 1;
}
first = last;
}
if (!changed) break;
}
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;
if (split_edges == 0) {
mesh->sample_count = 0;
mesh->samples_include_probes = 0;
free(edges); free(requested); free(allowed);
return 0;
}
/* Allocate a single stable midpoint vertex for each requested edge group. */
size_t midpoint_count = 0;
for (size_t first = 0; first < edge_count;) {
size_t last = first + 1;
while (last < edge_count && edges[last].a == edges[first].a &&
edges[last].b == edges[first].b)
++last;
int any = 0;
for (size_t i = first; i < last; ++i)
any |= requested[3 * edges[i].triangle + edges[i].side] != 0;
if (any) ++midpoint_count;
first = last;
}
if (ensure_vertices(mesh, mesh->vertex_count + midpoint_count)) {
free(edges); free(requested); free(allowed);
return -1;
}
size_t next_vertex = mesh->vertex_count;
for (size_t first = 0; first < edge_count;) {
size_t last = first + 1;
while (last < edge_count && edges[last].a == edges[first].a &&
edges[last].b == edges[first].b)
++last;
int any = 0;
for (size_t i = first; i < last; ++i)
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;
for (size_t i = first; i < last; ++i)
edges[i].midpoint = next_vertex - 1;
}
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);
return -1;
}
size_t child_count = 0;
for (size_t t = 0; t < old_count; ++t) {
const LensTriangle *triangle = &mesh->triangles[t];
size_t middle[3] = {SIZE_MAX, SIZE_MAX, SIZE_MAX};
unsigned int count = 0;
for (unsigned int side = 0; side < 3; ++side) {
if (!requested[3 * t + side]) continue;
++count;
size_t a = triangle->vertex[side], b = triangle->vertex[(side + 1) % 3];
if (a > b) { size_t swap = a; a = b; b = swap; }
for (size_t e = 0; e < edge_count; ++e)
if (edges[e].a == a && edges[e].b == b) { middle[side] = edges[e].midpoint; break; }
}
const size_t a = triangle->vertex[0], b = triangle->vertex[1], c = triangle->vertex[2];
const unsigned int level = triangle->level + 1;
if (count == 0)
append_triangle(children, &child_count, old_count * 4, a, b, c, triangle->level,
triangle->evaluated);
else if (count == 1) {
unsigned int side = middle[0] != SIZE_MAX ? 0 : middle[1] != SIZE_MAX ? 1 : 2;
const size_t v0 = triangle->vertex[side];
const size_t v1 = triangle->vertex[(side + 1) % 3];
const size_t other = triangle->vertex[(side + 2) % 3];
append_triangle(children, &child_count, old_count * 4, v0, middle[side], other, level, 0);
append_triangle(children, &child_count, old_count * 4, middle[side], v1, other, level, 0);
} else { /* Two or three requested edges become conforming red refinement. */
size_t ab = middle[0], bc = middle[1], ca = middle[2];
if (ab == SIZE_MAX || bc == SIZE_MAX || ca == SIZE_MAX) {
/* This cannot be made conforming from one-probe-per-triangle data. */
append_triangle(children, &child_count, old_count * 4, a, b, c, triangle->level, 1);
} else {
append_triangle(children, &child_count, old_count * 4, a, ab, ca, level, 0);
append_triangle(children, &child_count, old_count * 4, ab, b, bc, level, 0);
append_triangle(children, &child_count, old_count * 4, ca, bc, c, level, 0);
append_triangle(children, &child_count, old_count * 4, ab, bc, ca, level, 0);
}
}
}
free(mesh->triangles);
mesh->triangles = children;
mesh->triangle_count = child_count;
mesh->triangle_capacity = old_count * 4;
mesh->vertex_count = next_vertex;
mesh->sample_count = 0;
mesh->samples_include_probes = 0;
free(edges); free(requested); free(allowed);
return (int)midpoint_count;
}
int frame_lens_mesh_refine(FrameLensMesh *mesh,
const SpacetimeSource *spacetime,
const ObserverState *observer,
const GeodesicTraceConfig *trace,
const RefinementConfig *config) {
if (mesh == NULL || spacetime == NULL || observer == NULL || trace == NULL ||
config == NULL)
return -1;
for (;;) {
const int requested = frame_lens_mesh_prepare_generation(mesh, config);
if (requested < 0) return -1;
if (requested == 0) return 0;
#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);
/* Each request has a distinct destination vertex or probe slot. */
(void)frame_lens_mesh_install_sample(mesh, i, &endpoint);
}
if (frame_lens_mesh_finish_generation(mesh, config) < 0) return -1;
}
}
static double spherical_area(const double a[3], const double b[3], static double spherical_area(const double a[3], const double b[3],
const double c[3]) { const double c[3]) {
double b_cross_c[3]; double b_cross_c[3];
@@ -500,5 +1017,7 @@ void frame_lens_mesh_destroy(FrameLensMesh *mesh) {
return; return;
free(mesh->vertices); free(mesh->vertices);
free(mesh->triangles); free(mesh->triangles);
free(mesh->samples);
free(mesh->probe_slots);
*mesh = (FrameLensMesh){0}; *mesh = (FrameLensMesh){0};
} }
+44 -1
View File
@@ -15,16 +15,42 @@ typedef struct {
double n_infinity[3]; double n_infinity[3];
double log_frequency_ratio; double log_frequency_ratio;
RayEndpointStatus status; RayEndpointStatus status;
int traced;
} LensVertex; } LensVertex;
typedef struct { typedef struct {
size_t vertex[3]; size_t vertex[3];
unsigned int level;
int evaluated;
} LensTriangle; } LensTriangle;
typedef struct {
unsigned int max_level;
double angle_absolute_rad;
double angle_relative;
double min_edge_pixels;
double min_area_pixels2;
} RefinementConfig;
typedef enum { FRAME_SAMPLE_VERTEX, FRAME_SAMPLE_PROBE } FrameSampleKind;
typedef struct {
FrameSampleKind kind;
size_t vertex_id;
size_t edge_vertex[2];
LensVertex vertex;
} FrameSample;
typedef struct { typedef struct {
LensVertex *vertices; LensVertex *vertices;
LensTriangle *triangles; LensTriangle *triangles;
size_t vertex_count, triangle_count; size_t vertex_count, vertex_capacity;
size_t triangle_count, triangle_capacity;
FrameSample *samples;
size_t sample_count, sample_capacity;
int samples_include_probes;
size_t *probe_slots;
size_t probe_slot_capacity;
} FrameLensMesh; } FrameLensMesh;
typedef enum { typedef enum {
@@ -48,6 +74,23 @@ int frame_lens_mesh_build_coarse(FrameLensMesh *mesh, int width, int height,
int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime, int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime,
const ObserverState *observer, const ObserverState *observer,
const GeodesicTraceConfig *trace); const GeodesicTraceConfig *trace);
/* Builds exactly one generation of requests. Probe results must be installed
* only after the caller has completed the generation's tracing sweep. */
int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
const RefinementConfig *config);
const FrameSample *frame_lens_mesh_samples(const FrameLensMesh *mesh,
size_t *count);
int frame_lens_mesh_install_sample(FrameLensMesh *mesh, size_t sample_id,
const RayEndpoint *endpoint);
/* Applies the completed generation. Returns the number of newly added
* vertices, zero when no topology change was made, or -1 on failure. */
int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
const RefinementConfig *config);
int frame_lens_mesh_refine(FrameLensMesh *mesh,
const SpacetimeSource *spacetime,
const ObserverState *observer,
const GeodesicTraceConfig *trace,
const RefinementConfig *config);
/* Each locally invertible escaped triangle contributes one image per contained /* Each locally invertible escaped triangle contributes one image per contained
* star. */ * star. */
+96 -66
View File
@@ -40,6 +40,7 @@ typedef struct {
double slab_duration; double slab_duration;
double minkowski_proper_acceleration; double minkowski_proper_acceleration;
int catalog_load_workers; int catalog_load_workers;
RefinementConfig refinement;
} Settings; } Settings;
static int parse_int(const char *text, int *value) { static int parse_int(const char *text, int *value) {
@@ -52,6 +53,16 @@ static int parse_int(const char *text, int *value) {
return 0; return 0;
} }
static int parse_nonnegative_int(const char *text, unsigned int *value) {
char *end;
errno = 0;
unsigned long parsed = strtoul(text, &end, 10);
if (errno || *end || parsed > 1024)
return -1;
*value = (unsigned int)parsed;
return 0;
}
static int parse_double(const char *text, double *value) { static int parse_double(const char *text, double *value) {
char *end; char *end;
errno = 0; errno = 0;
@@ -139,7 +150,12 @@ static int parse_args(int argc, char **argv, Settings *s,
.movie_fps = 30.0, .movie_fps = 30.0,
.slab_duration = 64.0, .slab_duration = 64.0,
.minkowski_proper_acceleration = 1.52, .minkowski_proper_acceleration = 1.52,
.catalog_load_workers = 4}; .catalog_load_workers = 4,
.refinement = {.angle_absolute_rad =
1e-3 * 3.14159265358979323846 / 180.0,
.angle_relative = 0.1,
.min_edge_pixels = 0.5,
.min_area_pixels2 = 0.25}};
*write_path = NULL; *write_path = NULL;
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
if (!strcmp(argv[i], "--catalog") && i + 1 < argc) if (!strcmp(argv[i], "--catalog") && i + 1 < argc)
@@ -158,6 +174,17 @@ static int parse_args(int argc, char **argv, Settings *s,
!parse_int(argv[++i], &s->height)) { !parse_int(argv[++i], &s->height)) {
} else if (!strcmp(argv[i], "--coarse-cell-pixels") && i + 1 < argc && } else if (!strcmp(argv[i], "--coarse-cell-pixels") && i + 1 < argc &&
!parse_int(argv[++i], &s->coarse_cell_pixels)) { !parse_int(argv[++i], &s->coarse_cell_pixels)) {
} else if (!strcmp(argv[i], "--refine-max-level") && i + 1 < argc &&
!parse_nonnegative_int(argv[++i], &s->refinement.max_level)) {
} else if (!strcmp(argv[i], "--refine-angle-abs-deg") && i + 1 < argc &&
!parse_positive(argv[++i], &s->refinement.angle_absolute_rad)) {
s->refinement.angle_absolute_rad *= 3.14159265358979323846 / 180.0;
} else if (!strcmp(argv[i], "--refine-angle-rel") && i + 1 < argc &&
!parse_positive(argv[++i], &s->refinement.angle_relative)) {
} else if (!strcmp(argv[i], "--refine-min-edge-pixels") && i + 1 < argc &&
!parse_positive(argv[++i], &s->refinement.min_edge_pixels)) {
} else if (!strcmp(argv[i], "--refine-min-area-pixels2") && i + 1 < argc &&
!parse_positive(argv[++i], &s->refinement.min_area_pixels2)) {
} else if (!strcmp(argv[i], "--draw-mesh")) { } else if (!strcmp(argv[i], "--draw-mesh")) {
s->draw_mesh = 1; s->draw_mesh = 1;
} else if (!strcmp(argv[i], "--fov-deg") && i + 1 < argc && } else if (!strcmp(argv[i], "--fov-deg") && i + 1 < argc &&
@@ -257,19 +284,6 @@ static void report_splat_progress(void *context, FrameSplatProgressStage stage,
} }
} }
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;
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_FAILED: ++*failed; break;
}
}
static GeodesicTraceConfig trace_config(void) { static GeodesicTraceConfig trace_config(void) {
#ifdef SPACETIME_SCHWARZSCHILD #ifdef SPACETIME_SCHWARZSCHILD
return (GeodesicTraceConfig){.coordinate_time_step = 0.1, return (GeodesicTraceConfig){.coordinate_time_step = 0.1,
@@ -303,7 +317,9 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog,
frame_lens_mesh_build_coarse(&mesh, s->width, s->height, frame_lens_mesh_build_coarse(&mesh, s->width, s->height,
s->coarse_cell_pixels, s->coarse_cell_pixels,
s->horizontal_fov_deg) || s->horizontal_fov_deg) ||
frame_lens_mesh_trace(&mesh, spacetime, observer, &trace)) { frame_lens_mesh_trace(&mesh, spacetime, observer, &trace) ||
frame_lens_mesh_refine(&mesh, spacetime, observer, &trace,
&s->refinement)) {
frame_lens_mesh_destroy(&mesh); frame_lens_mesh_destroy(&mesh);
free(hdr); free(hdr);
return -1; return -1;
@@ -370,11 +386,65 @@ static int frame_output_path(char path[PATH_MAX], const Settings *s,
return written < 0 || written >= PATH_MAX ? -1 : 0; return written < 0 || written >= PATH_MAX ? -1 : 0;
} }
static int trace_movie_generation(Movie *movie, const Settings *s,
const SpacetimeSource *spacetime,
const GeodesicTraceConfig *trace,
size_t generation) {
RayPool rays = {0};
size_t ray_count = 0;
for (size_t f = 0; f < movie->frame_count; ++f) {
const int prepared = frame_lens_mesh_prepare_generation(
&movie->frames[f].mesh, &s->refinement);
if (prepared < 0) return -1;
ray_count += (size_t)prepared;
}
if (ray_count == 0) return 0;
if (ray_pool_init(&rays, ray_count)) return -1;
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)) {
ray_pool_destroy(&rays);
return -1;
}
}
double slab_hi = movie->frames[movie->frame_count - 1].coordinate_time;
size_t slab_id = 0;
while (ray_pool_has_live(&rays)) {
const double slab_lo = slab_hi - s->slab_duration;
MetricSlab *slab = NULL;
if (spacetime_load_slab(spacetime, slab_hi, slab_lo, &slab)) {
ray_pool_destroy(&rays);
return -1;
}
ray_pool_activate_in_time_range(&rays, slab);
ray_pool_advance_active(&rays, slab, trace);
spacetime_free_slab(slab);
if (s->verbose)
fprintf(stderr, "Refinement generation %zu, slab %zu completed.\n",
generation, ++slab_id);
slab_hi = slab_lo;
}
for (size_t i = 0; i < rays.count; ++i)
if (frame_lens_mesh_install_sample(&movie->frames[rays.frame_id[i]].mesh,
rays.vertex_id[i], &rays.endpoint[i])) {
ray_pool_destroy(&rays);
return -1;
}
ray_pool_destroy(&rays);
for (size_t f = 0; f < movie->frame_count; ++f)
if (frame_lens_mesh_finish_generation(&movie->frames[f].mesh,
&s->refinement) < 0)
return -1;
return 1;
}
static int render_movie(const Settings *s, StarCatalog *catalog, static int render_movie(const Settings *s, StarCatalog *catalog,
const SpacetimeSource *spacetime) { const SpacetimeSource *spacetime) {
ObserverTrack track = {0}; ObserverTrack track = {0};
Movie movie = {0}; Movie movie = {0};
RayPool rays = {0};
const GeodesicTraceConfig trace = trace_config(); const GeodesicTraceConfig trace = trace_config();
int result = -1; int result = -1;
if (s->observer_track_path == NULL || if (s->observer_track_path == NULL ||
@@ -384,55 +454,13 @@ static int render_movie(const Settings *s, StarCatalog *catalog,
movie_build_coarse_meshes(&movie, s->width, s->height, movie_build_coarse_meshes(&movie, s->width, s->height,
s->coarse_cell_pixels, s->horizontal_fov_deg)) s->coarse_cell_pixels, s->horizontal_fov_deg))
goto done; goto done;
size_t ray_count = 0; for (size_t generation = 0;; ++generation) {
for (size_t i = 0; i < movie.frame_count; ++i) const int traced = trace_movie_generation(&movie, s, spacetime, &trace,
ray_count += movie.frames[i].mesh.vertex_count; generation);
if (ray_count == 0 || ray_pool_init(&rays, ray_count)) if (traced < 0)
goto done;
for (size_t f = 0; f < movie.frame_count; ++f)
for (size_t v = 0; v < movie.frames[f].mesh.vertex_count; ++v)
if (ray_pool_append(&rays, &movie.frames[f].observer,
movie.frames[f].mesh.vertices[v].camera_direction,
f, v))
goto done;
double slab_hi = movie.frames[movie.frame_count - 1].coordinate_time;
size_t slab_id = 0;
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;
ray_pool_status_counts(&rays, &pending_before, &active_before,
&terminated_before, &failed_before);
if (s->verbose)
fprintf(stderr, "Time slab %zu: loading [%.6g, %.6g] with %zu pending and %zu active rays.\n",
slab_id + 1, slab_hi, slab_lo, pending_before, active_before);
if (spacetime_load_slab(spacetime, slab_hi, slab_lo, &slab))
goto done; goto done;
ray_pool_activate_in_time_range(&rays, slab); if (traced == 0)
size_t pending_active, active_active, terminated_active, failed_active; break;
ray_pool_status_counts(&rays, &pending_active, &active_active,
&terminated_active, &failed_active);
ray_pool_advance_active(&rays, slab, &trace);
spacetime_free_slab(slab);
size_t pending_after, active_after, terminated_after, failed_after;
ray_pool_status_counts(&rays, &pending_after, &active_after,
&terminated_after, &failed_after);
fprintf(stderr,
"Time slab %zu [%.6g, %.6g]: activated %zu; live %zu -> %zu, "
"terminated %zu, failed %zu\n",
++slab_id, slab_hi, slab_lo, active_active - active_before,
pending_before + active_before, pending_after + active_after,
terminated_after, failed_after);
slab_hi = slab_lo;
}
for (size_t i = 0; i < rays.count; ++i) {
LensVertex *vertex = &movie.frames[rays.frame_id[i]].mesh.vertices[rays.vertex_id[i]];
vertex->status = rays.endpoint[i].status;
if (vertex->status == RAY_ENDPOINT_ESCAPED) {
for (int axis = 0; axis < 3; ++axis)
vertex->n_infinity[axis] = rays.endpoint[i].n_infinity[axis];
vertex->log_frequency_ratio = log(rays.endpoint[i].frequency_ratio);
}
} }
for (size_t i = 0; i < movie.frame_count; ++i) { for (size_t i = 0; i < movie.frame_count; ++i) {
char output_path[PATH_MAX]; char output_path[PATH_MAX];
@@ -467,7 +495,6 @@ static int render_movie(const Settings *s, StarCatalog *catalog,
} }
result = 0; result = 0;
done: done:
ray_pool_destroy(&rays);
movie_destroy(&movie); movie_destroy(&movie);
observer_track_destroy(&track); observer_track_destroy(&track);
return result; return result;
@@ -499,7 +526,10 @@ int main(int argc, char **argv) {
#ifdef ENABLE_HDR_DEBUG #ifdef ENABLE_HDR_DEBUG
"[--hdr-output PATH] " "[--hdr-output PATH] "
#endif #endif
"[--coarse-cell-pixels N] [--draw-mesh] [--write-catalog PATH] " "[--coarse-cell-pixels N] [--refine-max-level N "
"--refine-angle-abs-deg D --refine-angle-rel R "
"--refine-min-edge-pixels P --refine-min-area-pixels2 A] "
"[--draw-mesh] [--write-catalog PATH] "
"[--catalog-load-workers N] " "[--catalog-load-workers N] "
"[--observer-track PATH --frames-dir DIR --frames-prefix NAME " "[--observer-track PATH --frames-dir DIR --frames-prefix NAME "
"--start-time T --duration T --fps N] " "--start-time T --duration T --fps N] "
+32
View File
@@ -160,6 +160,38 @@ int main(void) {
goto done; goto done;
} }
frame_lens_mesh_destroy(&fine_mesh); frame_lens_mesh_destroy(&fine_mesh);
/* Refinement probes are temporary until their generation is complete. A
* shared diagonal probe must produce one stable midpoint and conforming
* children only after its endpoint has been installed. */
FrameLensMesh adaptive_mesh = {0};
const RefinementConfig refine = {.max_level = 1,
.angle_absolute_rad = 1e-4,
.angle_relative = 1e-4,
.min_edge_pixels = 1.0,
.min_area_pixels2 = 1.0};
if (frame_lens_mesh_build_coarse(&adaptive_mesh, width, height, 100, 30.0))
goto done;
for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) {
adaptive_mesh.vertices[i].traced = 1;
adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED;
adaptive_mesh.vertices[i].n_infinity[0] = 1.0;
}
if (frame_lens_mesh_prepare_generation(&adaptive_mesh, &refine) != 1) {
fputs("adaptive shared-edge probe setup regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
const RayEndpoint bent_probe = {.n_infinity = {0.0, 1.0, 0.0},
.frequency_ratio = 1.0,
.status = RAY_ENDPOINT_ESCAPED};
if (frame_lens_mesh_install_sample(&adaptive_mesh, 0, &bent_probe) ||
frame_lens_mesh_finish_generation(&adaptive_mesh, &refine) != 1 ||
adaptive_mesh.vertex_count != 5 || adaptive_mesh.triangle_count != 4) {
fputs("adaptive shared-edge split regression failed\n", stderr);
frame_lens_mesh_destroy(&adaptive_mesh);
goto done;
}
frame_lens_mesh_destroy(&adaptive_mesh);
result = 0; result = 0;
done: done:
frame_lens_mesh_destroy(&mesh); frame_lens_mesh_destroy(&mesh);
+20
View File
@@ -1,4 +1,5 @@
#include "geodesic.h" #include "geodesic.h"
#include "frame.h"
#include <math.h> #include <math.h>
#include <stdio.h> #include <stdio.h>
@@ -56,6 +57,25 @@ int main(void) {
central.status, inside_shadow.status, outside_shadow.status); central.status, inside_shadow.status, outside_shadow.status);
goto done; goto done;
} }
/* A coarse field covering the shadow must genuinely refine: its initial
* capture/escape-discontinuous triangles are a separate trigger from the
* smooth direction-error criterion. */
FrameLensMesh mesh = {0};
const RefinementConfig refinement = {.max_level = 1,
.angle_absolute_rad = 1e-5,
.angle_relative = 1e-5,
.min_edge_pixels = 1.0,
.min_area_pixels2 = 1.0};
if (frame_lens_mesh_build_coarse(&mesh, 48, 48, 24, 40.0) ||
frame_lens_mesh_trace(&mesh, &spacetime, &observer, &trace) ||
frame_lens_mesh_refine(&mesh, &spacetime, &observer, &trace,
&refinement) ||
mesh.vertex_count <= 9 || mesh.triangle_count <= 8) {
fputs("Schwarzschild adaptive-refinement regression failed\n", stderr);
frame_lens_mesh_destroy(&mesh);
goto done;
}
frame_lens_mesh_destroy(&mesh);
result = 0; result = 0;
done: done:
spacetime_destroy(&spacetime); spacetime_destroy(&spacetime);
+2 -2
View File
@@ -45,7 +45,7 @@ SampleRequest -> RayPool (SoA, inactive / active / terminated)
| A | 基本完成 | canonical 21 列 observer-track CSV、插值、movie frame schedule、编号 PNG/PPM 序列和相关 CLI 已实现。 | 输出仍由 `optics` 写入,尚未拆出计划中的 `output.h/.c`;CSV 非有限数、非单调时间和 PNG 序号冲突的独立回归仍需补齐。 | | A | 基本完成 | canonical 21 列 observer-track CSV、插值、movie frame schedule、编号 PNG/PPM 序列和相关 CLI 已实现。 | 输出仍由 `optics` 写入,尚未拆出计划中的 `output.h/.c`;CSV 非有限数、非单调时间和 PNG 序号冲突的独立回归仍需补齐。 |
| B | 部分完成 | Minkowski 恒 proper-acceleration 轨迹生成器、`0..2`/30 fps/61 frame 基准和前向解析 Doppler regression 已实现。 | 尚无逐 frame 诊断 CSV;a=0 全序列、后向红移、tetrad 正交性/有限差分速度和图像连续性回归仍需补齐。 | | B | 部分完成 | Minkowski 恒 proper-acceleration 轨迹生成器、`0..2`/30 fps/61 frame 基准和前向解析 Doppler regression 已实现。 | 尚无逐 frame 诊断 CSV;a=0 全序列、后向红移、tetrad 正交性/有限差分速度和图像连续性回归仍需补齐。 |
| C | 核心路径完成,验收未完成 | `MetricSlab` 接口、SoA `RayPool`、slab 到达时的 lazy ray initialization、倒序 slab sweep、OpenMP 批量推进和 `(frame_id, vertex_id)` endpoint 回填已实现。 | 仍未 compact terminated rays 或支持 RayPool 增长;未记录/测试每 generation 每 slab 一次 load/free;scheduler 与 wrapper 的完整 endpoint/image 对比尚未成为当前测试。 | | C | 核心路径完成,验收未完成 | `MetricSlab` 接口、SoA `RayPool`、slab 到达时的 lazy ray initialization、倒序 slab sweep、OpenMP 批量推进和 `(frame_id, vertex_id)` endpoint 回填已实现。 | 仍未 compact terminated rays 或支持 RayPool 增长;未记录/测试每 generation 每 slab 一次 load/free;scheduler 与 wrapper 的完整 endpoint/image 对比尚未成为当前测试。 |
| D | 未开始 | 无。 | multi-pass adaptive refinement、请求队列、nmesh slab I/O/ghost-slice/memory contract。 | | D | 部分完成 | 多代 request/probe mesh、最长边二分、共享边 conforming 闭包、相对+绝对天空方向误差、capture/escape 边界细分,以及单帧/movie 共用 generation 调度已实现。 | nmesh slab I/O/ghost-slice/memory contract、slab instrumentation 和强透镜收敛基准仍未完成。 |
| E | 未开始 | 仅有静态 Schwarzschild 单帧和局部 inward boost。 | `30M -> 20M -> 30M` observer track、movie regression 和连续性检查。 | | E | 未开始 | 仅有静态 Schwarzschild 单帧和局部 inward boost。 | `30M -> 20M -> 30M` observer track、movie regression 和连续性检查。 |
| F | 未开始 | PNG sequence 是默认 movie 输出。 | FFmpeg 编码命令/目标、解码核验和可选 direct-video backend。 | | F | 未开始 | PNG sequence 是默认 movie 输出。 | FFmpeg 编码命令/目标、解码核验和可选 direct-video backend。 |
@@ -88,7 +88,7 @@ SampleRequest -> RayPool (SoA, inactive / active / terminated)
## Phase D:补全多 pass adaptive mesh 与 out-of-core 准备(未开始) ## Phase D:补全多 pass adaptive mesh 与 out-of-core 准备(未开始)
1. 给 `FrameLensMesh` 增加稳定 vertex ID、请求队列、triangle level/flags 和“缺失 endpoint”状态。每 pass 只为需要的 edge midpoint/center 产生 `SampleRequest`;安装结果后再判定 mapping interpolation error、orientation consistency 与局部 Jacobian 奇异性。 1. `FrameLensMesh` 采用 append-only stable vertex ID、generation-local request/probe 队列、triangle level 与“缺失 endpoint”状态。每 pass 的 probe 只放在最长 image-plane 边中点;完整 sweep 后才安装 endpoint 并改变拓扑。细分要求同时超过绝对与相对 `n_infinity` 方向误差阈值,并受最大层数、最小长边和最小面积约束;capture/escape 不一致强制细分。首版不计算 orientation 或 Jacobian。
2. 以迭代 queue(可按 frame/root tile 并行、线程本地 request buffer 后 sort/deduplicate)替代递归 task。每一个 pass 完整执行 Phase C sweep;只有所有 frame 都无新请求才进行 catalog splat。 2. 以迭代 queue(可按 frame/root tile 并行、线程本地 request buffer 后 sort/deduplicate)替代递归 task。每一个 pass 完整执行 Phase C sweep;只有所有 frame 都无新请求才进行 catalog splat。
3. 在 movie 生命周期中及时释放已完成的 RayPool、临时 request 和单帧 HDR;保留最终 mesh/endpoints,或在渲染 PNG 后按明确策略释放,避免视频时无界增长。 3. 在 movie 生命周期中及时释放已完成的 RayPool、临时 request 和单帧 HDR;保留最终 mesh/endpoints,或在渲染 PNG 后按明确策略释放,避免视频时无界增长。
4. 为 nmesh 预留并实现 source-side slab overlap / temporal ghost-slice 契约、可配置 memory budget、slab coverage 日志和线程本地 `MetricWorkspace` ownership。此 phase 不重采样为 Cartesian grid,也不假定相邻时间 slice 的 AMR tree 相同。 4. 为 nmesh 预留并实现 source-side slab overlap / temporal ghost-slice 契约、可配置 memory budget、slab coverage 日志和线程本地 `MetricWorkspace` ownership。此 phase 不重采样为 Cartesian grid,也不假定相邻时间 slice 的 AMR tree 相同。