forked from genewildish/Mainline
Implement pipeline hot-rebuild with state preservation
- Add save_state/restore_state methods to CameraStage - Add save_state/restore_state methods to DisplayStage - Extend Pipeline._copy_stage_state() to preserve camera/display state - Add save_state/restore_state methods to UIPanel for UI state preservation - Update pipeline_runner to preserve UI state across preset changes Camera state preserved: - Position (x, y) - Mode (feed, scroll, horizontal, etc.) - Speed, zoom, canvas dimensions - Internal timing state Display state preserved: - Initialization status - Dimensions - Reuse flag for display reinitialization UI Panel state preserved: - Stage enabled/disabled status - Parameter values - Selected stage and focused parameter - Scroll position This enables manual/event-driven rebuilds when inlet-outlet connections change, while preserving all relevant state across pipeline mutations.
This commit is contained in:
@@ -349,6 +349,9 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
|||||||
|
|
||||||
print(f" \033[38;5;245mSwitching to preset: {preset_name}\033[0m")
|
print(f" \033[38;5;245mSwitching to preset: {preset_name}\033[0m")
|
||||||
|
|
||||||
|
# Save current UI panel state before rebuild
|
||||||
|
ui_state = ui_panel.save_state() if ui_panel else None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Clean up old pipeline
|
# Clean up old pipeline
|
||||||
pipeline.cleanup()
|
pipeline.cleanup()
|
||||||
@@ -558,6 +561,10 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
|||||||
)
|
)
|
||||||
stage_control.effect = effect # type: ignore[attr-defined]
|
stage_control.effect = effect # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
# Restore UI panel state if it was saved
|
||||||
|
if ui_state:
|
||||||
|
ui_panel.restore_state(ui_state)
|
||||||
|
|
||||||
if ui_panel.stages:
|
if ui_panel.stages:
|
||||||
first_stage = next(iter(ui_panel.stages))
|
first_stage = next(iter(ui_panel.stages))
|
||||||
ui_panel.select_stage(first_stage)
|
ui_panel.select_stage(first_stage)
|
||||||
|
|||||||
@@ -16,6 +16,58 @@ class CameraStage(Stage):
|
|||||||
self.optional = True
|
self.optional = True
|
||||||
self._last_frame_time: float | None = None
|
self._last_frame_time: float | None = None
|
||||||
|
|
||||||
|
def save_state(self) -> dict[str, Any]:
|
||||||
|
"""Save camera state for restoration after pipeline rebuild.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing camera state that can be restored
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"x": self._camera.x,
|
||||||
|
"y": self._camera.y,
|
||||||
|
"mode": self._camera.mode.value
|
||||||
|
if hasattr(self._camera.mode, "value")
|
||||||
|
else self._camera.mode,
|
||||||
|
"speed": self._camera.speed,
|
||||||
|
"zoom": self._camera.zoom,
|
||||||
|
"canvas_width": self._camera.canvas_width,
|
||||||
|
"canvas_height": self._camera.canvas_height,
|
||||||
|
"_x_float": getattr(self._camera, "_x_float", 0.0),
|
||||||
|
"_y_float": getattr(self._camera, "_y_float", 0.0),
|
||||||
|
"_time": getattr(self._camera, "_time", 0.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
def restore_state(self, state: dict[str, Any]) -> None:
|
||||||
|
"""Restore camera state from saved state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: Dictionary containing camera state from save_state()
|
||||||
|
"""
|
||||||
|
from engine.camera import CameraMode
|
||||||
|
|
||||||
|
self._camera.x = state.get("x", 0)
|
||||||
|
self._camera.y = state.get("y", 0)
|
||||||
|
|
||||||
|
# Restore mode - handle both enum value and direct enum
|
||||||
|
mode_value = state.get("mode", 0)
|
||||||
|
if isinstance(mode_value, int):
|
||||||
|
self._camera.mode = CameraMode(mode_value)
|
||||||
|
else:
|
||||||
|
self._camera.mode = mode_value
|
||||||
|
|
||||||
|
self._camera.speed = state.get("speed", 1.0)
|
||||||
|
self._camera.zoom = state.get("zoom", 1.0)
|
||||||
|
self._camera.canvas_width = state.get("canvas_width", 200)
|
||||||
|
self._camera.canvas_height = state.get("canvas_height", 200)
|
||||||
|
|
||||||
|
# Restore internal state
|
||||||
|
if hasattr(self._camera, "_x_float"):
|
||||||
|
self._camera._x_float = state.get("_x_float", 0.0)
|
||||||
|
if hasattr(self._camera, "_y_float"):
|
||||||
|
self._camera._y_float = state.get("_y_float", 0.0)
|
||||||
|
if hasattr(self._camera, "_time"):
|
||||||
|
self._camera._time = state.get("_time", 0.0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stage_type(self) -> str:
|
def stage_type(self) -> str:
|
||||||
return "camera"
|
return "camera"
|
||||||
|
|||||||
@@ -13,6 +13,39 @@ class DisplayStage(Stage):
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.category = "display"
|
self.category = "display"
|
||||||
self.optional = False
|
self.optional = False
|
||||||
|
self._initialized = False
|
||||||
|
self._init_width = 80
|
||||||
|
self._init_height = 24
|
||||||
|
|
||||||
|
def save_state(self) -> dict[str, Any]:
|
||||||
|
"""Save display state for restoration after pipeline rebuild.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing display state that can be restored
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"initialized": self._initialized,
|
||||||
|
"init_width": self._init_width,
|
||||||
|
"init_height": self._init_height,
|
||||||
|
"width": getattr(self._display, "width", 80),
|
||||||
|
"height": getattr(self._display, "height", 24),
|
||||||
|
}
|
||||||
|
|
||||||
|
def restore_state(self, state: dict[str, Any]) -> None:
|
||||||
|
"""Restore display state from saved state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: Dictionary containing display state from save_state()
|
||||||
|
"""
|
||||||
|
self._initialized = state.get("initialized", False)
|
||||||
|
self._init_width = state.get("init_width", 80)
|
||||||
|
self._init_height = state.get("init_height", 24)
|
||||||
|
|
||||||
|
# Restore display dimensions if the display supports it
|
||||||
|
if hasattr(self._display, "width"):
|
||||||
|
self._display.width = state.get("width", 80)
|
||||||
|
if hasattr(self._display, "height"):
|
||||||
|
self._display.height = state.get("height", 24)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def capabilities(self) -> set[str]:
|
def capabilities(self) -> set[str]:
|
||||||
@@ -37,7 +70,17 @@ class DisplayStage(Stage):
|
|||||||
def init(self, ctx: PipelineContext) -> bool:
|
def init(self, ctx: PipelineContext) -> bool:
|
||||||
w = ctx.params.viewport_width if ctx.params else 80
|
w = ctx.params.viewport_width if ctx.params else 80
|
||||||
h = ctx.params.viewport_height if ctx.params else 24
|
h = ctx.params.viewport_height if ctx.params else 24
|
||||||
result = self._display.init(w, h, reuse=False)
|
|
||||||
|
# Try to reuse display if already initialized
|
||||||
|
reuse = self._initialized
|
||||||
|
result = self._display.init(w, h, reuse=reuse)
|
||||||
|
|
||||||
|
# Update initialization state
|
||||||
|
if result is not False:
|
||||||
|
self._initialized = True
|
||||||
|
self._init_width = w
|
||||||
|
self._init_height = h
|
||||||
|
|
||||||
return result is not False
|
return result is not False
|
||||||
|
|
||||||
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
||||||
|
|||||||
@@ -204,6 +204,15 @@ class Pipeline:
|
|||||||
if hasattr(old_stage, "_enabled"):
|
if hasattr(old_stage, "_enabled"):
|
||||||
new_stage._enabled = old_stage._enabled
|
new_stage._enabled = old_stage._enabled
|
||||||
|
|
||||||
|
# Preserve camera state
|
||||||
|
if hasattr(old_stage, "save_state") and hasattr(new_stage, "restore_state"):
|
||||||
|
try:
|
||||||
|
state = old_stage.save_state()
|
||||||
|
new_stage.restore_state(state)
|
||||||
|
except Exception:
|
||||||
|
# If state preservation fails, continue without it
|
||||||
|
pass
|
||||||
|
|
||||||
def _rebuild(self) -> None:
|
def _rebuild(self) -> None:
|
||||||
"""Rebuild execution order after mutation without full reinitialization."""
|
"""Rebuild execution order after mutation without full reinitialization."""
|
||||||
self._capability_map = self._build_capability_map()
|
self._capability_map = self._build_capability_map()
|
||||||
|
|||||||
@@ -78,6 +78,58 @@ class UIPanel:
|
|||||||
self._show_panel: bool = True # UI panel visibility
|
self._show_panel: bool = True # UI panel visibility
|
||||||
self._preset_scroll_offset: int = 0 # Scroll in preset list
|
self._preset_scroll_offset: int = 0 # Scroll in preset list
|
||||||
|
|
||||||
|
def save_state(self) -> dict[str, Any]:
|
||||||
|
"""Save UI panel state for restoration after pipeline rebuild.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing UI panel state that can be restored
|
||||||
|
"""
|
||||||
|
# Save stage control states (enabled, params, etc.)
|
||||||
|
stage_states = {}
|
||||||
|
for name, ctrl in self.stages.items():
|
||||||
|
stage_states[name] = {
|
||||||
|
"enabled": ctrl.enabled,
|
||||||
|
"selected": ctrl.selected,
|
||||||
|
"params": dict(ctrl.params), # Copy params dict
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"stage_states": stage_states,
|
||||||
|
"scroll_offset": self.scroll_offset,
|
||||||
|
"selected_stage": self.selected_stage,
|
||||||
|
"_focused_param": self._focused_param,
|
||||||
|
"_show_panel": self._show_panel,
|
||||||
|
"_show_preset_picker": self._show_preset_picker,
|
||||||
|
"_preset_scroll_offset": self._preset_scroll_offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
def restore_state(self, state: dict[str, Any]) -> None:
|
||||||
|
"""Restore UI panel state from saved state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: Dictionary containing UI panel state from save_state()
|
||||||
|
"""
|
||||||
|
# Restore stage control states
|
||||||
|
stage_states = state.get("stage_states", {})
|
||||||
|
for name, stage_state in stage_states.items():
|
||||||
|
if name in self.stages:
|
||||||
|
ctrl = self.stages[name]
|
||||||
|
ctrl.enabled = stage_state.get("enabled", True)
|
||||||
|
ctrl.selected = stage_state.get("selected", False)
|
||||||
|
# Restore params
|
||||||
|
saved_params = stage_state.get("params", {})
|
||||||
|
for param_name, param_value in saved_params.items():
|
||||||
|
if param_name in ctrl.params:
|
||||||
|
ctrl.params[param_name] = param_value
|
||||||
|
|
||||||
|
# Restore UI panel state
|
||||||
|
self.scroll_offset = state.get("scroll_offset", 0)
|
||||||
|
self.selected_stage = state.get("selected_stage")
|
||||||
|
self._focused_param = state.get("_focused_param")
|
||||||
|
self._show_panel = state.get("_show_panel", True)
|
||||||
|
self._show_preset_picker = state.get("_show_preset_picker", False)
|
||||||
|
self._preset_scroll_offset = state.get("_preset_scroll_offset", 0)
|
||||||
|
|
||||||
def register_stage(self, stage: Any, enabled: bool = True) -> StageControl:
|
def register_stage(self, stage: Any, enabled: bool = True) -> StageControl:
|
||||||
"""Register a stage for UI control.
|
"""Register a stage for UI control.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user