#!/usr/bin/env python3 """ Demo script for testing pipeline hot-rebuild and state preservation. Usage: python scripts/demo_hot_rebuild.py python scripts/demo_hot_rebuild.py --viewport 40x15 This script: 1. Creates a small viewport (40x15) for easier capture 2. Uses NullDisplay with recording enabled 3. Runs the pipeline for N frames (capturing initial state) 4. Triggers a "hot-rebuild" (e.g., toggling an effect stage) 5. Runs the pipeline for M more frames 6. Verifies state preservation by comparing frames before/after rebuild 7. Prints visual comparison to stdout """ import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from engine.display import DisplayRegistry from engine.effects import get_registry from engine.fetch import load_cache from engine.pipeline import Pipeline, PipelineConfig, PipelineContext from engine.pipeline.adapters import ( EffectPluginStage, FontStage, SourceItemsToBufferStage, ViewportFilterStage, create_stage_from_display, create_stage_from_effect, ) from engine.pipeline.params import PipelineParams def run_demo(viewport_width: int = 40, viewport_height: int = 15): """Run the hot-rebuild demo.""" print(f"\n{'=' * 60}") print(f"Pipeline Hot-Rebuild Demo") print(f"Viewport: {viewport_width}x{viewport_height}") print(f"{'=' * 60}\n") import engine.effects.plugins as effects_plugins effects_plugins.discover_plugins() print("[1/6] Loading source items...") items = load_cache() if not items: print(" ERROR: No fixture cache available") sys.exit(1) print(f" Loaded {len(items)} items") print("[2/6] Creating NullDisplay with recording...") display = DisplayRegistry.create("null") display.init(viewport_width, viewport_height) display.start_recording() print(" Recording started") print("[3/6] Building pipeline...") params = PipelineParams() params.viewport_width = viewport_width params.viewport_height = viewport_height config = PipelineConfig( source="fixture", display="null", camera="scroll", effects=["noise", "fade"], ) pipeline = Pipeline(config=config, context=PipelineContext()) from engine.data_sources.sources import ListDataSource from engine.pipeline.adapters import DataSourceStage list_source = ListDataSource(items, name="fixture") pipeline.add_stage("source", DataSourceStage(list_source, name="fixture")) pipeline.add_stage("viewport_filter", ViewportFilterStage(name="viewport-filter")) pipeline.add_stage("font", FontStage(name="font")) effect_registry = get_registry() for effect_name in config.effects: effect = effect_registry.get(effect_name) if effect: pipeline.add_stage( f"effect_{effect_name}", create_stage_from_effect(effect, effect_name), ) pipeline.add_stage("display", create_stage_from_display(display, "null")) pipeline.build() if not pipeline.initialize(): print(" ERROR: Failed to initialize pipeline") sys.exit(1) print(" Pipeline built and initialized") ctx = pipeline.context ctx.params = params ctx.set("display", display) ctx.set("items", items) ctx.set("pipeline", pipeline) ctx.set("pipeline_order", pipeline.execution_order) ctx.set("camera_y", 0) print("[4/6] Running pipeline for 10 frames (before rebuild)...") frames_before = [] for frame in range(10): params.frame_number = frame ctx.params = params result = pipeline.execute(items) if result.success: frames_before.append(display._last_buffer) print(f" Captured {len(frames_before)} frames") print("[5/6] Triggering hot-rebuild (toggling 'fade' effect)...") fade_stage = pipeline.get_stage("effect_fade") if fade_stage and isinstance(fade_stage, EffectPluginStage): new_enabled = not fade_stage.is_enabled() fade_stage.set_enabled(new_enabled) fade_stage._effect.config.enabled = new_enabled print(f" Fade effect enabled: {new_enabled}") else: print(" WARNING: Could not find fade effect stage") print("[6/6] Running pipeline for 10 more frames (after rebuild)...") frames_after = [] for frame in range(10, 20): params.frame_number = frame ctx.params = params result = pipeline.execute(items) if result.success: frames_after.append(display._last_buffer) print(f" Captured {len(frames_after)} frames") display.stop_recording() print("\n" + "=" * 60) print("RESULTS") print("=" * 60) print("\n[State Preservation Check]") if frames_before and frames_after: last_before = frames_before[-1] first_after = frames_after[0] if last_before == first_after: print(" PASS: Buffer state preserved across rebuild") else: print(" INFO: Buffer changed after rebuild (expected - effect toggled)") print("\n[Frame Continuity Check]") recorded_frames = display.get_frames() print(f" Total recorded frames: {len(recorded_frames)}") print(f" Frames before rebuild: {len(frames_before)}") print(f" Frames after rebuild: {len(frames_after)}") if len(recorded_frames) == 20: print(" PASS: All frames recorded") else: print(" WARNING: Frame count mismatch") print("\n[Visual Comparison - First frame before vs after rebuild]") print("\n--- Before rebuild (frame 9) ---") for i, line in enumerate(frames_before[0][:viewport_height]): print(f"{i:2}: {line}") print("\n--- After rebuild (frame 10) ---") for i, line in enumerate(frames_after[0][:viewport_height]): print(f"{i:2}: {line}") print("\n[Recording Save/Load Test]") test_file = Path("/tmp/test_recording.json") display.save_recording(test_file) print(f" Saved recording to: {test_file}") display2 = DisplayRegistry.create("null") display2.init(viewport_width, viewport_height) display2.load_recording(test_file) loaded_frames = display2.get_frames() print(f" Loaded {len(loaded_frames)} frames from file") if len(loaded_frames) == len(recorded_frames): print(" PASS: Recording save/load works correctly") else: print(" WARNING: Frame count mismatch after load") test_file.unlink(missing_ok=True) pipeline.cleanup() display.cleanup() print("\n" + "=" * 60) print("Demo complete!") print("=" * 60 + "\n") def main(): viewport_width = 40 viewport_height = 15 if "--viewport" in sys.argv: idx = sys.argv.index("--viewport") if idx + 1 < len(sys.argv): vp = sys.argv[idx + 1] try: viewport_width, viewport_height = map(int, vp.split("x")) except ValueError: print("Error: Invalid viewport format. Use WxH (e.g., 40x15)") sys.exit(1) run_demo(viewport_width, viewport_height) if __name__ == "__main__": main()