Add Infinity_Vis_1 performance core
This commit is contained in:
196
Infinity_Vis_1/infinity_vis_1/engine.py
Normal file
196
Infinity_Vis_1/infinity_vis_1/engine.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import time
|
||||
|
||||
from .models import FrameSnapshot, MappingSpec, SceneState
|
||||
from .output.ddp import DDPClient
|
||||
from .renderer import FastRenderer
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class OutputSettings:
|
||||
output_fps: float = 40.0
|
||||
preview_fps: float = 12.0
|
||||
keepalive_seconds: float = 0.35
|
||||
delta_sending: bool = True
|
||||
preview_enabled: bool = True
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RuntimeDiagnostics:
|
||||
tile_count: int
|
||||
controller_count: int
|
||||
total_leds: int
|
||||
output_frames: int = 0
|
||||
preview_frames: int = 0
|
||||
packets_built: int = 0
|
||||
packets_sent: int = 0
|
||||
changed_controller_frames: int = 0
|
||||
keepalive_controller_frames: int = 0
|
||||
stale_output_frames: int = 0
|
||||
last_render_ms: float = 0.0
|
||||
last_tick_at: float = 0.0
|
||||
last_output_at: float = 0.0
|
||||
last_preview_at: float = 0.0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TickResult:
|
||||
timestamp: float
|
||||
output_due: bool
|
||||
preview_due: bool
|
||||
output_snapshot: FrameSnapshot | None = None
|
||||
preview_snapshot: FrameSnapshot | None = None
|
||||
changed_payloads: dict[str, bytes] = field(default_factory=dict)
|
||||
packet_count: int = 0
|
||||
|
||||
|
||||
class RealtimeEngine:
|
||||
def __init__(
|
||||
self,
|
||||
mapping: MappingSpec,
|
||||
scene: SceneState | None = None,
|
||||
settings: OutputSettings | None = None,
|
||||
ddp_client: DDPClient | None = None,
|
||||
) -> None:
|
||||
self.mapping = mapping
|
||||
self.scene = scene or SceneState()
|
||||
self.settings = settings or OutputSettings()
|
||||
self.renderer = FastRenderer(mapping)
|
||||
self.ddp_client = ddp_client or DDPClient()
|
||||
|
||||
renderer_stats = self.renderer.stats()
|
||||
self.diagnostics = RuntimeDiagnostics(
|
||||
tile_count=renderer_stats.tile_count,
|
||||
controller_count=renderer_stats.controller_count,
|
||||
total_leds=renderer_stats.total_leds,
|
||||
)
|
||||
|
||||
self._last_output_at: float | None = None
|
||||
self._last_preview_at: float | None = None
|
||||
self._last_sent_payloads: dict[str, bytes] = {}
|
||||
self._last_sent_at: dict[str, float] = {}
|
||||
self._latest_snapshot: FrameSnapshot | None = None
|
||||
self._force_output = True
|
||||
self._force_preview = True
|
||||
|
||||
def set_scene(self, scene: SceneState) -> None:
|
||||
self.scene = scene
|
||||
self._force_output = True
|
||||
self._force_preview = True
|
||||
|
||||
def request_preview_refresh(self) -> None:
|
||||
self._force_preview = True
|
||||
|
||||
def latest_snapshot(self) -> FrameSnapshot | None:
|
||||
return self._latest_snapshot
|
||||
|
||||
def tick(self, now: float | None = None) -> TickResult:
|
||||
timestamp = time.perf_counter() if now is None else float(now)
|
||||
output_due = self._is_due(self._last_output_at, self._interval(self.settings.output_fps), timestamp, self._force_output)
|
||||
preview_due = self.settings.preview_enabled and self._is_due(
|
||||
self._last_preview_at,
|
||||
self._interval(self.settings.preview_fps),
|
||||
timestamp,
|
||||
self._force_preview,
|
||||
)
|
||||
if not output_due and not preview_due:
|
||||
return TickResult(timestamp=timestamp, output_due=False, preview_due=False)
|
||||
|
||||
render_started = time.perf_counter()
|
||||
snapshot = self.renderer.render(self.scene, timestamp=timestamp)
|
||||
self.diagnostics.last_render_ms = (time.perf_counter() - render_started) * 1000.0
|
||||
self.diagnostics.last_tick_at = timestamp
|
||||
self._latest_snapshot = snapshot
|
||||
|
||||
changed_payloads: dict[str, bytes] = {}
|
||||
packet_count = 0
|
||||
output_snapshot: FrameSnapshot | None = None
|
||||
preview_snapshot: FrameSnapshot | None = None
|
||||
|
||||
if output_due:
|
||||
changed_payloads, changed_count, keepalive_count = self._select_output_payloads(
|
||||
snapshot.controller_payloads,
|
||||
timestamp,
|
||||
)
|
||||
output_snapshot = snapshot
|
||||
self._last_output_at = timestamp
|
||||
self._force_output = False
|
||||
self.diagnostics.output_frames += 1
|
||||
self.diagnostics.changed_controller_frames += changed_count
|
||||
self.diagnostics.keepalive_controller_frames += keepalive_count
|
||||
self.diagnostics.last_output_at = timestamp
|
||||
if changed_payloads:
|
||||
packet_count = self.ddp_client.count_packets(changed_payloads)
|
||||
self.diagnostics.packets_built += packet_count
|
||||
else:
|
||||
self.diagnostics.stale_output_frames += 1
|
||||
|
||||
if preview_due:
|
||||
preview_snapshot = snapshot
|
||||
self._last_preview_at = timestamp
|
||||
self._force_preview = False
|
||||
self.diagnostics.preview_frames += 1
|
||||
self.diagnostics.last_preview_at = timestamp
|
||||
|
||||
return TickResult(
|
||||
timestamp=timestamp,
|
||||
output_due=output_due,
|
||||
preview_due=preview_due,
|
||||
output_snapshot=output_snapshot,
|
||||
preview_snapshot=preview_snapshot,
|
||||
changed_payloads=changed_payloads,
|
||||
packet_count=packet_count,
|
||||
)
|
||||
|
||||
def send_due(self, now: float | None = None) -> TickResult:
|
||||
result = self.tick(now=now)
|
||||
if result.changed_payloads:
|
||||
sent = self.ddp_client.send(result.changed_payloads)
|
||||
self.diagnostics.packets_sent += sent
|
||||
return result
|
||||
|
||||
def _interval(self, fps: float) -> float:
|
||||
if fps <= 0:
|
||||
return 0.0
|
||||
return 1.0 / float(fps)
|
||||
|
||||
def _is_due(self, last_at: float | None, interval: float, now: float, forced: bool) -> bool:
|
||||
if forced or last_at is None:
|
||||
return True
|
||||
if interval <= 0:
|
||||
return True
|
||||
return (now - last_at) >= max(0.0, interval - 1e-9)
|
||||
|
||||
def _select_output_payloads(self, payloads: dict[str, bytes], timestamp: float) -> tuple[dict[str, bytes], int, int]:
|
||||
if not self.settings.delta_sending:
|
||||
for host, payload in payloads.items():
|
||||
self._last_sent_payloads[host] = payload
|
||||
self._last_sent_at[host] = timestamp
|
||||
return dict(payloads), len(payloads), 0
|
||||
|
||||
changed_payloads: dict[str, bytes] = {}
|
||||
changed_count = 0
|
||||
keepalive_count = 0
|
||||
keepalive_seconds = max(0.0, float(self.settings.keepalive_seconds))
|
||||
|
||||
for host, payload in payloads.items():
|
||||
previous = self._last_sent_payloads.get(host)
|
||||
last_sent_at = self._last_sent_at.get(host)
|
||||
unchanged = previous == payload
|
||||
keepalive_due = (
|
||||
keepalive_seconds > 0.0
|
||||
and unchanged
|
||||
and last_sent_at is not None
|
||||
and (timestamp - last_sent_at) >= keepalive_seconds
|
||||
)
|
||||
if previous is None or not unchanged or keepalive_due:
|
||||
changed_payloads[host] = payload
|
||||
self._last_sent_payloads[host] = payload
|
||||
self._last_sent_at[host] = timestamp
|
||||
if keepalive_due and unchanged:
|
||||
keepalive_count += 1
|
||||
else:
|
||||
changed_count += 1
|
||||
return changed_payloads, changed_count, keepalive_count
|
||||
Reference in New Issue
Block a user