diff --git a/src/frame.h b/src/frame.h index 1132bd9..dec7cfa 100644 --- a/src/frame.h +++ b/src/frame.h @@ -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); diff --git a/src/main.c b/src/main.c index 210e2ac..ae8e5b6 100644 --- a/src/main.c +++ b/src/main.c @@ -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", diff --git a/tests/fixtures/schwarzschild_mixed_refinement.csv b/tests/fixtures/schwarzschild_mixed_refinement.csv new file mode 100644 index 0000000..69a59a5 --- /dev/null +++ b/tests/fixtures/schwarzschild_mixed_refinement.csv @@ -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 diff --git a/tests/test_camera_cli.py b/tests/test_camera_cli.py index 102fd0e..cbed67a 100644 --- a/tests/test_camera_cli.py +++ b/tests/test_camera_cli.py @@ -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)