diff --git a/tests/fixtures/p23-03/diagnostic_stream.gd b/tests/fixtures/p23-03/diagnostic_stream.gd index fd4992e..7f51e24 100644 --- a/tests/fixtures/p23-03/diagnostic_stream.gd +++ b/tests/fixtures/p23-03/diagnostic_stream.gd @@ -12,25 +12,35 @@ class MotionObserver extends Node: var scaled_elapsed := 0.0 func _ready() -> void: RenderingServer.frame_post_draw.connect(after_draw) + view.draw.connect(on_view_draw) func _process(delta: float) -> void: elapsed += delta var clock := get_node_or_null("/root/GameClock") if clock == null or not bool(clock.get("paused")): scaled_elapsed += delta * maxf(float(clock.get("speed")) if clock != null else 1.0, 0.0) - func after_draw() -> void: - if not is_instance_valid(view): return - var report := view.get_life_observation_report() - if report.is_empty(): return + func on_view_draw() -> void: + # Read the draw-consumed report after the real _draw completes. + after_draw.call_deferred() + func display_sample(report: Dictionary) -> Dictionary: var sample := {} for key: String in ["animation_time", "paused", "layer", "zoom", "pan", "viewport", "actor_tracks", "elevator_tracks", "actor_drawings", "cabins"]: sample[key] = report[key] + return sample + func after_draw() -> void: + if not is_instance_valid(view): return + var frame := Engine.get_process_frames() + if not frames.is_empty() and frame <= int(frames[-1]["frame"]): return + var report := view.get_life_observation_report() + if report.is_empty(): return + var sample := display_sample(report) if not frames.is_empty() and sample == frames[-1]["display"] and not sample["paused"]: return # Keep only two draw frames, never whole Runtime copies per frame. - frames.append({"frame": Engine.get_process_frames(), "delta": elapsed, "scaled_delta": scaled_elapsed, "display": sample}) + frames.append({"frame": frame, "delta": elapsed, "scaled_delta": scaled_elapsed, "display": sample}) if frames.size() > 2: frames.pop_front() elapsed = 0.0 scaled_elapsed = 0.0 func _exit_tree() -> void: + if is_instance_valid(view) and view.draw.is_connected(on_view_draw): view.draw.disconnect(on_view_draw) if RenderingServer.frame_post_draw.is_connected(after_draw): RenderingServer.frame_post_draw.disconnect(after_draw) func watch(view: WorldPreview) -> void: @@ -38,6 +48,19 @@ func watch(view: WorldPreview) -> void: observer.view = view view.add_child(observer) +func await_draws(tree: SceneTree, view: WorldPreview, n: int = 2, bound: int = 120) -> bool: + var start := Engine.get_process_frames() + for unused: int in range(bound): + view.queue_redraw() + await tree.process_frame + if not is_instance_valid(observer): break + var report := view.get_life_observation_report() + if observer.frames.size() == n and not report.is_empty() and int(observer.frames[0]["frame"]) > start and observer.frames[-1]["display"] == observer.display_sample(report): + return true + # Persist timeout as a failure, never a skip. + record("draw-history-timeout", {"start_frame": start, "bound": bound, "required_frames": n}) + return false + func open(path: String) -> bool: directory = path if DirAccess.dir_exists_absolute(path): diff --git a/tests/fixtures/p23-03/layout_comparison.gd b/tests/fixtures/p23-03/layout_comparison.gd index 8c42e50..4a38945 100644 --- a/tests/fixtures/p23-03/layout_comparison.gd +++ b/tests/fixtures/p23-03/layout_comparison.gd @@ -86,10 +86,8 @@ func run(tree: SceneTree, directory: String, layout: String) -> bool: if guest[&"current_cell"] != [GOAL.x, GOAL.y, GOAL.z] or finished.has(guest[&"guest_id"]): continue finished[guest[&"guest_id"]] = offset if offset % 30 == 0: - view.queue_redraw() - await tree.process_frame - await tree.process_frame - ok = stream.capture("frame-%03d" % offset, runtime, view) and ok + var drawn: bool = await stream.await_draws(tree, view) + ok = drawn and stream.capture("frame-%03d" % offset, runtime, view) and ok ok = stream.record("first-arrivals", {&"offsets": finished}) and ok ok = stream.record("final", runtime.create_snapshot()) and ok stream.close() diff --git a/tests/fixtures/p23-03/observation.gd b/tests/fixtures/p23-03/observation.gd index 25383a9..8b9a2ed 100644 --- a/tests/fixtures/p23-03/observation.gd +++ b/tests/fixtures/p23-03/observation.gd @@ -39,6 +39,7 @@ func advance(minutes: int) -> void: await frames() await observe_progress() if clock.absolute_minute % 30 == 0: + expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") expect(stream.capture("minute-%d" % clock.absolute_minute, runtime, main.world_preview), "sparse minute observation") func observe_progress() -> void: @@ -118,8 +119,10 @@ func prepare() -> void: func layer_observations(prefix: String) -> void: await button(main.front_button) + expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") expect(stream.capture(prefix + "-FRONT", runtime, main.world_preview), "FRONT state/report") await button(main.back_button) + expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") expect(stream.capture(prefix + "-BACK", runtime, main.world_preview), "BACK state/report") func writer() -> void: diff --git a/tests/fixtures/p23-03/passive_observation.gd b/tests/fixtures/p23-03/passive_observation.gd index ea1b577..1d9203e 100644 --- a/tests/fixtures/p23-03/passive_observation.gd +++ b/tests/fixtures/p23-03/passive_observation.gd @@ -44,10 +44,12 @@ func motion_observations() -> void: while clock.absolute_minute == minute and not terminated: await tree.process_frame await frames() + expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") expect(stream.capture("active-motion", runtime, main.world_preview), "active interpolation history") await button(main.pause_button) expect(clock.paused, "ordinary pause for stable observation") await frames() + expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") expect(stream.capture("paused-motion", runtime, main.world_preview), "paused interpolation history") class InputObserver extends Node: @@ -93,6 +95,7 @@ func request(action: Dictionary) -> void: DirAccess.make_dir_recursive_absolute(folder) var window: Window = main.get_window() var view: WorldPreview = main.world_preview + expect(await stream.await_draws(tree, view), "real draw history before capture") action["report"] = Codec.new().typed(view.get_life_observation_report()) action["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse()) action["local_to_viewport"] = affine(view.get_global_transform_with_canvas()) diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index d7eecb1..536fa73 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -269,11 +269,15 @@ class InfrastructureTests(unittest.TestCase): audit = dict(frames=[dict(frame=1, delta=.01, scaled_delta=0, display=copy.deepcopy(report)), dict(frame=2, delta=.01, scaled_delta=0, display=copy.deepcopy(report))]) check.compare_motion(state, report, audit) - for change in ('delta', 'frame', 'display'): + for change in ('delta', 'frame', 'decreasing_frame', 'no_frames', 'one_frame', 'display', 'report_binding'): with self.subTest(change=change): bad = copy.deepcopy(audit) if change == 'delta': bad['frames'][1]['scaled_delta'] = 1 if change == 'frame': bad['frames'][1]['frame'] = 1 + if change == 'decreasing_frame': bad['frames'][1]['frame'] = 0 + if change == 'no_frames': bad['frames'].clear() + if change == 'one_frame': bad['frames'].pop() + if change == 'report_binding': bad['frames'][1]['display']['zoom'] = 2 if change == 'display': bad['frames'][1]['display']['animation_time'] = 1 with self.assertRaises(ValueError): check.compare_motion(state, report, bad)