forked from genewildish/Mainline
feat: Complete pipeline hot-rebuild implementation with acceptance tests
- Implements pipeline hot-rebuild with state preservation (issue #43) - Adds auto-injection of MVP stages for missing capabilities - Adds radial camera mode for polar coordinate scanning - Adds afterimage and motionblur effects using framebuffer history - Adds comprehensive acceptance tests for camera modes and pipeline rebuild - Updates presets.toml with new effect configurations Related to: #35 (Pipeline Mutation API epic) Closes: #43, #44, #45
This commit is contained in:
222
scripts/demo_hot_rebuild.py
Normal file
222
scripts/demo_hot_rebuild.py
Normal file
@@ -0,0 +1,222 @@
|
||||
#!/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()
|
||||
509
scripts/pipeline_demo.py
Normal file
509
scripts/pipeline_demo.py
Normal file
@@ -0,0 +1,509 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pipeline Demo Orchestrator
|
||||
|
||||
Demonstrates all effects and camera modes with gentle oscillation.
|
||||
Runs a comprehensive test of the Mainline pipeline system with proper
|
||||
frame rate control and extended duration for visibility.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from engine.camera import Camera
|
||||
from engine.data_sources.checkerboard import CheckerboardDataSource
|
||||
from engine.data_sources.sources import SourceItem
|
||||
from engine.display import DisplayRegistry, NullDisplay
|
||||
from engine.effects.plugins import discover_plugins
|
||||
from engine.effects import get_registry
|
||||
from engine.effects.types import EffectConfig
|
||||
from engine.frame import FrameTimer
|
||||
from engine.pipeline import Pipeline, PipelineConfig, PipelineContext
|
||||
from engine.pipeline.adapters import (
|
||||
CameraClockStage,
|
||||
CameraStage,
|
||||
DataSourceStage,
|
||||
DisplayStage,
|
||||
EffectPluginStage,
|
||||
SourceItemsToBufferStage,
|
||||
)
|
||||
from engine.pipeline.stages.framebuffer import FrameBufferStage
|
||||
|
||||
|
||||
class GentleOscillator:
|
||||
"""Produces smooth, gentle sinusoidal values."""
|
||||
|
||||
def __init__(
|
||||
self, speed: float = 60.0, amplitude: float = 1.0, offset: float = 0.0
|
||||
):
|
||||
self.speed = speed # Period length in frames
|
||||
self.amplitude = amplitude # Amplitude
|
||||
self.offset = offset # Base offset
|
||||
|
||||
def value(self, frame: int) -> float:
|
||||
"""Get oscillated value for given frame."""
|
||||
return self.offset + self.amplitude * 0.5 * (1 + math.sin(frame / self.speed))
|
||||
|
||||
|
||||
class PipelineDemoOrchestrator:
|
||||
"""Orchestrates comprehensive pipeline demonstrations."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
use_terminal: bool = True,
|
||||
target_fps: float = 30.0,
|
||||
effect_duration: float = 8.0,
|
||||
mode_duration: float = 3.0,
|
||||
enable_fps_switch: bool = False,
|
||||
loop: bool = False,
|
||||
verbose: bool = False,
|
||||
):
|
||||
self.use_terminal = use_terminal
|
||||
self.target_fps = target_fps
|
||||
self.effect_duration = effect_duration
|
||||
self.mode_duration = mode_duration
|
||||
self.enable_fps_switch = enable_fps_switch
|
||||
self.loop = loop
|
||||
self.verbose = verbose
|
||||
self.frame_count = 0
|
||||
self.pipeline = None
|
||||
self.context = None
|
||||
self.framebuffer = None
|
||||
self.camera = None
|
||||
self.timer = None
|
||||
|
||||
def log(self, message: str, verbose: bool = False):
|
||||
"""Print with timestamp if verbose or always-important."""
|
||||
if self.verbose or not verbose:
|
||||
print(f"[{time.strftime('%H:%M:%S')}] {message}")
|
||||
|
||||
def build_base_pipeline(
|
||||
self, camera_type: str = "scroll", camera_speed: float = 0.5
|
||||
):
|
||||
"""Build a base pipeline with all required components."""
|
||||
self.log(f"Building base pipeline: camera={camera_type}, speed={camera_speed}")
|
||||
|
||||
# Camera
|
||||
camera = Camera.scroll(speed=camera_speed)
|
||||
camera.set_canvas_size(200, 200)
|
||||
|
||||
# Context
|
||||
ctx = PipelineContext()
|
||||
|
||||
# Pipeline config
|
||||
config = PipelineConfig(
|
||||
source="empty",
|
||||
display="terminal" if self.use_terminal else "null",
|
||||
camera=camera_type,
|
||||
effects=[],
|
||||
enable_metrics=True,
|
||||
)
|
||||
pipeline = Pipeline(config=config, context=ctx)
|
||||
|
||||
# Use a large checkerboard pattern for visible motion effects
|
||||
source = CheckerboardDataSource(width=200, height=200, square_size=10)
|
||||
pipeline.add_stage("source", DataSourceStage(source, name="checkerboard"))
|
||||
|
||||
# Add camera clock (must run every frame)
|
||||
pipeline.add_stage(
|
||||
"camera_update", CameraClockStage(camera, name="camera-clock")
|
||||
)
|
||||
|
||||
# Add render
|
||||
pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer"))
|
||||
|
||||
# Add camera stage
|
||||
pipeline.add_stage("camera", CameraStage(camera, name="camera"))
|
||||
|
||||
# Add framebuffer (optional for effects that use it)
|
||||
self.framebuffer = FrameBufferStage(name="default", history_depth=5)
|
||||
pipeline.add_stage("framebuffer", self.framebuffer)
|
||||
|
||||
# Add display
|
||||
display_backend = "terminal" if self.use_terminal else "null"
|
||||
display = DisplayRegistry.create(display_backend)
|
||||
if display:
|
||||
pipeline.add_stage("display", DisplayStage(display, name=display_backend))
|
||||
|
||||
# Build and initialize
|
||||
pipeline.build(auto_inject=False)
|
||||
pipeline.initialize()
|
||||
|
||||
self.pipeline = pipeline
|
||||
self.context = ctx
|
||||
self.camera = camera
|
||||
|
||||
self.log("Base pipeline built successfully")
|
||||
return pipeline
|
||||
|
||||
def test_effects_oscillation(self):
|
||||
"""Test each effect with gentle intensity oscillation."""
|
||||
self.log("\n=== EFFECTS OSCILLATION TEST ===")
|
||||
self.log(
|
||||
f"Duration: {self.effect_duration}s per effect at {self.target_fps} FPS"
|
||||
)
|
||||
|
||||
discover_plugins() # Ensure all plugins are registered
|
||||
registry = get_registry()
|
||||
all_effects = registry.list_all()
|
||||
effect_names = [
|
||||
name
|
||||
for name in all_effects.keys()
|
||||
if name not in ("motionblur", "afterimage")
|
||||
]
|
||||
|
||||
# Calculate frames based on duration and FPS
|
||||
frames_per_effect = int(self.effect_duration * self.target_fps)
|
||||
oscillator = GentleOscillator(speed=90, amplitude=0.7, offset=0.3)
|
||||
|
||||
total_effects = len(effect_names) + 2 # +2 for motionblur and afterimage
|
||||
estimated_total = total_effects * self.effect_duration
|
||||
|
||||
self.log(f"Testing {len(effect_names)} regular effects + 2 framebuffer effects")
|
||||
self.log(f"Estimated time: {estimated_total:.0f}s")
|
||||
|
||||
for idx, effect_name in enumerate(sorted(effect_names), 1):
|
||||
try:
|
||||
self.log(f"[{idx}/{len(effect_names)}] Testing effect: {effect_name}")
|
||||
|
||||
effect = registry.get(effect_name)
|
||||
if not effect:
|
||||
self.log(f" Skipped: plugin not found")
|
||||
continue
|
||||
|
||||
stage = EffectPluginStage(effect, name=effect_name)
|
||||
self.pipeline.add_stage(f"effect_{effect_name}", stage)
|
||||
self.pipeline.build(auto_inject=False)
|
||||
|
||||
self._run_frames(
|
||||
frames_per_effect, oscillator=oscillator, effect=effect
|
||||
)
|
||||
|
||||
self.pipeline.remove_stage(f"effect_{effect_name}")
|
||||
self.pipeline.build(auto_inject=False)
|
||||
|
||||
self.log(f" ✓ {effect_name} completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f" ✗ {effect_name} failed: {e}")
|
||||
|
||||
# Test motionblur and afterimage separately with framebuffer
|
||||
for effect_name in ["motionblur", "afterimage"]:
|
||||
try:
|
||||
self.log(
|
||||
f"[{len(effect_names) + 1}/{total_effects}] Testing effect: {effect_name} (with framebuffer)"
|
||||
)
|
||||
|
||||
effect = registry.get(effect_name)
|
||||
if not effect:
|
||||
self.log(f" Skipped: plugin not found")
|
||||
continue
|
||||
|
||||
stage = EffectPluginStage(
|
||||
effect,
|
||||
name=effect_name,
|
||||
dependencies={"framebuffer.history.default"},
|
||||
)
|
||||
self.pipeline.add_stage(f"effect_{effect_name}", stage)
|
||||
self.pipeline.build(auto_inject=False)
|
||||
|
||||
self._run_frames(
|
||||
frames_per_effect, oscillator=oscillator, effect=effect
|
||||
)
|
||||
|
||||
self.pipeline.remove_stage(f"effect_{effect_name}")
|
||||
self.pipeline.build(auto_inject=False)
|
||||
self.log(f" ✓ {effect_name} completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f" ✗ {effect_name} failed: {e}")
|
||||
|
||||
def _run_frames(self, num_frames: int, oscillator=None, effect=None):
|
||||
"""Run a specified number of frames with proper timing."""
|
||||
for frame in range(num_frames):
|
||||
self.frame_count += 1
|
||||
self.context.set("frame_number", frame)
|
||||
|
||||
if oscillator and effect:
|
||||
intensity = oscillator.value(frame)
|
||||
effect.configure(EffectConfig(intensity=intensity))
|
||||
|
||||
dt = self.timer.sleep_until_next_frame()
|
||||
self.camera.update(dt)
|
||||
self.pipeline.execute([])
|
||||
|
||||
def test_framebuffer(self):
|
||||
"""Test framebuffer functionality."""
|
||||
self.log("\n=== FRAMEBUFFER TEST ===")
|
||||
|
||||
try:
|
||||
# Run frames using FrameTimer for consistent pacing
|
||||
self._run_frames(10)
|
||||
|
||||
# Check framebuffer history
|
||||
history = self.context.get("framebuffer.default.history")
|
||||
assert history is not None, "No framebuffer history found"
|
||||
assert len(history) > 0, "Framebuffer history is empty"
|
||||
|
||||
self.log(f"History frames: {len(history)}")
|
||||
self.log(f"Configured depth: {self.framebuffer.config.history_depth}")
|
||||
|
||||
# Check intensity computation
|
||||
intensity = self.context.get("framebuffer.default.current_intensity")
|
||||
assert intensity is not None, "No intensity map found"
|
||||
self.log(f"Intensity map length: {len(intensity)}")
|
||||
|
||||
# Check that frames are being stored correctly
|
||||
recent_frame = self.framebuffer.get_frame(0, self.context)
|
||||
assert recent_frame is not None, "Cannot retrieve recent frame"
|
||||
self.log(f"Recent frame rows: {len(recent_frame)}")
|
||||
|
||||
self.log("✓ Framebuffer test passed")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"✗ Framebuffer test failed: {e}")
|
||||
raise
|
||||
|
||||
def test_camera_modes(self):
|
||||
"""Test each camera mode."""
|
||||
self.log("\n=== CAMERA MODES TEST ===")
|
||||
self.log(f"Duration: {self.mode_duration}s per mode at {self.target_fps} FPS")
|
||||
|
||||
camera_modes = [
|
||||
("feed", 0.1),
|
||||
("scroll", 0.5),
|
||||
("horizontal", 0.3),
|
||||
("omni", 0.3),
|
||||
("floating", 0.5),
|
||||
("bounce", 0.5),
|
||||
("radial", 0.3),
|
||||
]
|
||||
|
||||
frames_per_mode = int(self.mode_duration * self.target_fps)
|
||||
self.log(f"Testing {len(camera_modes)} camera modes")
|
||||
self.log(f"Estimated time: {len(camera_modes) * self.mode_duration:.0f}s")
|
||||
|
||||
for idx, (camera_type, speed) in enumerate(camera_modes, 1):
|
||||
try:
|
||||
self.log(f"[{idx}/{len(camera_modes)}] Testing camera: {camera_type}")
|
||||
|
||||
# Rebuild camera
|
||||
self.camera.reset()
|
||||
cam_class = getattr(Camera, camera_type, Camera.scroll)
|
||||
new_camera = cam_class(speed=speed)
|
||||
new_camera.set_canvas_size(200, 200)
|
||||
|
||||
# Update camera stages
|
||||
clock_stage = CameraClockStage(new_camera, name="camera-clock")
|
||||
self.pipeline.replace_stage("camera_update", clock_stage)
|
||||
|
||||
camera_stage = CameraStage(new_camera, name="camera")
|
||||
self.pipeline.replace_stage("camera", camera_stage)
|
||||
|
||||
self.camera = new_camera
|
||||
|
||||
# Run frames with proper timing
|
||||
self._run_frames(frames_per_mode)
|
||||
|
||||
# Verify camera moved (check final position)
|
||||
x, y = self.camera.x, self.camera.y
|
||||
self.log(f" Final position: ({x:.1f}, {y:.1f})")
|
||||
|
||||
if camera_type == "feed":
|
||||
assert x == 0 and y == 0, "Feed camera should not move"
|
||||
elif camera_type in ("scroll", "horizontal"):
|
||||
assert abs(x) > 0 or abs(y) > 0, "Camera should have moved"
|
||||
else:
|
||||
self.log(f" Position check skipped (mode={camera_type})")
|
||||
|
||||
self.log(f" ✓ {camera_type} completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f" ✗ {camera_type} failed: {e}")
|
||||
|
||||
def test_fps_switch_demo(self):
|
||||
"""Demonstrate the effect of different frame rates on animation smoothness."""
|
||||
if not self.enable_fps_switch:
|
||||
return
|
||||
|
||||
self.log("\n=== FPS SWITCH DEMONSTRATION ===")
|
||||
|
||||
fps_sequence = [
|
||||
(30.0, 5.0), # 30 FPS for 5 seconds
|
||||
(60.0, 5.0), # 60 FPS for 5 seconds
|
||||
(30.0, 5.0), # Back to 30 FPS for 5 seconds
|
||||
(20.0, 3.0), # 20 FPS for 3 seconds
|
||||
(60.0, 3.0), # 60 FPS for 3 seconds
|
||||
]
|
||||
|
||||
original_fps = self.target_fps
|
||||
|
||||
for fps, duration in fps_sequence:
|
||||
self.log(f"\n--- Switching to {fps} FPS for {duration}s ---")
|
||||
self.target_fps = fps
|
||||
self.timer.target_frame_dt = 1.0 / fps
|
||||
|
||||
# Update display FPS if supported
|
||||
display = (
|
||||
self.pipeline.get_stage("display").stage
|
||||
if self.pipeline.get_stage("display")
|
||||
else None
|
||||
)
|
||||
if display and hasattr(display, "target_fps"):
|
||||
display.target_fps = fps
|
||||
display._frame_period = 1.0 / fps if fps > 0 else 0
|
||||
|
||||
frames = int(duration * fps)
|
||||
camera_type = "radial" # Use radial for smooth rotation that's visible at different FPS
|
||||
speed = 0.3
|
||||
|
||||
# Rebuild camera if needed
|
||||
self.camera.reset()
|
||||
new_camera = Camera.radial(speed=speed)
|
||||
new_camera.set_canvas_size(200, 200)
|
||||
clock_stage = CameraClockStage(new_camera, name="camera-clock")
|
||||
self.pipeline.replace_stage("camera_update", clock_stage)
|
||||
camera_stage = CameraStage(new_camera, name="camera")
|
||||
self.pipeline.replace_stage("camera", camera_stage)
|
||||
self.camera = new_camera
|
||||
|
||||
for frame in range(frames):
|
||||
self.context.set("frame_number", frame)
|
||||
dt = self.timer.sleep_until_next_frame()
|
||||
self.camera.update(dt)
|
||||
result = self.pipeline.execute([])
|
||||
|
||||
self.log(f" Completed {frames} frames at {fps} FPS")
|
||||
|
||||
# Restore original FPS
|
||||
self.target_fps = original_fps
|
||||
self.timer.target_frame_dt = 1.0 / original_fps
|
||||
self.log("✓ FPS switch demo completed")
|
||||
|
||||
def run(self):
|
||||
"""Run the complete demo."""
|
||||
start_time = time.time()
|
||||
self.log("Starting Pipeline Demo Orchestrator")
|
||||
self.log("=" * 50)
|
||||
|
||||
# Initialize frame timer
|
||||
self.timer = FrameTimer(target_frame_dt=1.0 / self.target_fps)
|
||||
|
||||
# Build pipeline
|
||||
self.build_base_pipeline()
|
||||
|
||||
try:
|
||||
# Test framebuffer first (needed for motion blur effects)
|
||||
self.test_framebuffer()
|
||||
|
||||
# Test effects
|
||||
self.test_effects_oscillation()
|
||||
|
||||
# Test camera modes
|
||||
self.test_camera_modes()
|
||||
|
||||
# Optional FPS switch demonstration
|
||||
if self.enable_fps_switch:
|
||||
self.test_fps_switch_demo()
|
||||
else:
|
||||
self.log("\n=== FPS SWITCH DEMO ===")
|
||||
self.log("Skipped (enable with --switch-fps)")
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
self.log("\n" + "=" * 50)
|
||||
self.log("Demo completed successfully!")
|
||||
self.log(f"Total frames processed: {self.frame_count}")
|
||||
self.log(f"Total elapsed time: {elapsed:.1f}s")
|
||||
self.log(f"Average FPS: {self.frame_count / elapsed:.1f}")
|
||||
|
||||
finally:
|
||||
# Always cleanup properly
|
||||
self._cleanup()
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up pipeline resources."""
|
||||
self.log("Cleaning up...", verbose=True)
|
||||
if self.pipeline:
|
||||
try:
|
||||
self.pipeline.cleanup()
|
||||
if self.verbose:
|
||||
self.log("Pipeline cleaned up successfully", verbose=True)
|
||||
except Exception as e:
|
||||
self.log(f"Error during pipeline cleanup: {e}", verbose=True)
|
||||
|
||||
# If not looping, clear references
|
||||
if not self.loop:
|
||||
self.pipeline = None
|
||||
self.context = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Pipeline Demo Orchestrator - comprehensive demo of Mainline pipeline"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--null",
|
||||
action="store_true",
|
||||
help="Use null display (no visual output)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fps",
|
||||
type=float,
|
||||
default=30.0,
|
||||
help="Target frame rate (default: 30)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--effect-duration",
|
||||
type=float,
|
||||
default=8.0,
|
||||
help="Duration per effect in seconds (default: 8)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode-duration",
|
||||
type=float,
|
||||
default=3.0,
|
||||
help="Duration per camera mode in seconds (default: 3)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--switch-fps",
|
||||
action="store_true",
|
||||
help="Include FPS switching demonstration",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--loop",
|
||||
action="store_true",
|
||||
help="Run demo in an infinite loop",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Enable verbose output",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
orchestrator = PipelineDemoOrchestrator(
|
||||
use_terminal=not args.null,
|
||||
target_fps=args.fps,
|
||||
effect_duration=args.effect_duration,
|
||||
mode_duration=args.mode_duration,
|
||||
enable_fps_switch=args.switch_fps,
|
||||
loop=args.loop,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
|
||||
try:
|
||||
orchestrator.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted by user")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\nDemo failed: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user