Fix: Skip empty movie refinement generations

Allow frames to finish refinement independently without aborting the movie. Report genuine refinement errors with generation and frame identifiers, and clarify the sample batch contract.

Add a two-frame Schwarzschild regression covering unequal refinement progress. Validated Release and Debug make test suites and 640x360 rendering of the final eight free-fall frames in both builds.
This commit is contained in:
wyj committed 2026-09-06 08:10:29 -04:00
1 parent b69c9cfd45
commit 8cf6106127
4 files changed
+36 -7

No files matched your search

+7 -3
View File
@@ -51,6 +51,8 @@ typedef struct {
size_t vertex_count, vertex_capacity;
size_t triangle_count, triangle_capacity;
FrameSample *samples;
/* Current generation's vertex/probe requests, retained through result
* installation and cleared by finish_generation; not a pending-ray count. */
size_t sample_count, sample_capacity;
int samples_include_probes;
size_t *probe_slots;
@@ -94,15 +96,17 @@ int frame_lens_mesh_build_coarse(FrameLensMesh *mesh, int width, int height,
int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime,
const ObserverState *observer,
const GeodesicTraceConfig *trace);
/* Builds one generation of vertex/probe ray requests. Results are installed
* only after the caller has completed the generation's sweep. */
/* Builds one generation of vertex/probe ray requests. Returns the request
* count, zero when this frame needs no work, or -1 on failure. Results are
* installed only after the caller has completed the generation's 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
/* Applies a nonempty completed generation; callers skip frames for which
* prepare_generation returned zero. 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);
+7 -1
View File
@@ -782,10 +782,16 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
fprintf(stderr, "Ray trace generation %zu: installing endpoints and refining meshes.\n",
generation);
for (size_t f = 0; f < movie->frame_count; ++f) {
/* Frames converge independently; only finish those traced this pass. */
if (movie->frames[f].mesh.sample_count == 0)
continue;
const int added = frame_lens_mesh_finish_generation(&movie->frames[f].mesh,
&s->refinement);
if (added < 0)
if (added < 0) {
fprintf(stderr, "Ray trace generation %zu: frame %zu refinement failed.\n",
generation, movie->frames[f].frame_id);
return -1;
}
total_added += (size_t)added;
if (s->verbose && added > 0)
fprintf(stderr, "Ray trace generation %zu: frame %zu added %d vertices.\n",
+3
View File
@@ -0,0 +1,3 @@
t,tau,x,y,z,e0t,e0x,e0y,e0z,e1t,e1x,e1y,e1z,e2t,e2x,e2y,e2z,e3t,e3x,e3y,e3z
398.48185980038227,385,6.2695764947414609,0,0,1.1913347333893116,-0.52820527560786401,0,0,0.31666643933096511,-0.97979589711241188,1.1999039092174769e-16,0,0,0,0,1,-3.7996929847120426e-17,1.1756609271804282e-16,1,0
404.71892830936724,390,3.1592785705381488,0,0,1.3415634010738333,-0.7701012396930732,0,0,0.40833373573712628,-0.97979589711788617,1.1999039092174769e-16,0,0,0,0,1,-4.8996124577635443e-17,1.1756609271869967e-16,1,0
1 t tau x y z e0t e0x e0y e0z e1t e1x e1y e1z e2t e2x e2y e2z e3t e3x e3y e3z
2 398.48185980038227 385 6.2695764947414609 0 0 1.1913347333893116 -0.52820527560786401 0 0 0.31666643933096511 -0.97979589711241188 1.1999039092174769e-16 0 0 0 0 1 -3.7996929847120426e-17 1.1756609271804282e-16 1 0
3 404.71892830936724 390 3.1592785705381488 0 0 1.3415634010738333 -0.7701012396930732 0 0 0.40833373573712628 -0.97979589711788617 1.1999039092174769e-16 0 0 0 0 1 -4.8996124577635443e-17 1.1756609271869967e-16 1 0
+19 -3
View File
@@ -20,7 +20,7 @@ def run(binary, *args, ok=True):
return result
def image_payload(path):
def image_payload(path, dimensions=(64, 48), allow_black=False):
data = path.read_bytes()
assert data[:8] == b'\x89PNG\r\n\x1a\n'
offset, compressed = 8, bytearray()
@@ -28,12 +28,13 @@ def image_payload(path):
count, kind = struct.unpack_from('>I4s', data, offset)
payload = data[offset + 8:offset + 8 + count]
if kind == b'IHDR':
assert struct.unpack_from('>II', payload) == (64, 48)
assert struct.unpack_from('>II', payload) == dimensions
if kind == b'IDAT':
compressed.extend(payload)
offset += count + 12
raw = zlib.decompress(compressed)
assert any(raw), f'empty image: {path}'
assert raw, f'missing image data: {path}'
assert allow_black or any(raw), f'empty image: {path}'
return raw
@@ -137,4 +138,19 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
assert max_error < 1e-9, max_error
# A map import must still work without evaluating a camera/metric.
assert single == render('import', '--lens-map-input', single_map)
if backend == 'schwarzschild':
# Two inward-looking free-fall samples at r=6.2696 and r=3.1593.
# At 16:9 the latter frame finishes in generation 0, while the
# former still needs refinement. Finishing the empty batch used
# to abort the whole movie in generation 1.
mixed = Path(__file__).parent / 'fixtures/schwarzschild_mixed_refinement.csv'
result = run(binary, *common, '--height', 36, '--fov-deg', 60,
'--refine-max-level', 3, '--observer-track', mixed,
'--movie-track-samples', '--frames-dir', tmp,
'--frames-prefix', 'mixed', '--verbose')
assert 'Ray trace generation 1: frame 0 added' in result.stderr
assert 'Ray trace generation 0: frame 1 added' not in result.stderr
for frame in range(2):
image_payload(tmp / f'mixed_{frame:06d}.png',
dimensions=(64, 36), allow_black=True)
print(f'{backend}: CLI checks passed; single/movie PNG identical, map max error {max_error:.3g}', flush=True)