Phase 2 abgeschlossen: Renderer-State-Sync über echte IPC (§6.2, §6.4)
- RemoteStateMirror (Renderer): Snapshot-Pflicht nach (Re-)Connect, Deltas mit neu/geaendert/geloescht, Deltas vor erstem Snapshot werden abgelehnt (Re-Sync), Duplikate/Veraltete idempotent - RendererStateLink (Control Core): connect_and_sync mit Pflicht-Snapshot, sync_if_changed versendet Deltas seit letzter gesendeter Revision, Reconnect loest immer neuen Snapshot aus (kein halber Zustand) - End-to-End-Integrationstests ueber echtes TCP-Loopback ohne Mocks: Snapshot->Delta-Kette, Reconnect-Re-Sync, Delta-vor-Snapshot-Ablehnung, Duplikat-No-Op - Gesamtsuite 297 gruen, Ruff gruen
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
"""hms_renderer – Render-Worker-Spike (PLAN.md §6.1C, §12, §36 Nr. 4–6).
|
||||
"""hms_renderer – Render-Worker (PLAN.md §6.1C, §12, §36 Nr. 4–6).
|
||||
|
||||
Python orchestriert native GStreamer-Komponenten; keine Pixelverarbeitung
|
||||
in Python (§2.1, §33). Pipelines werden als gst-launch-Strings definiert
|
||||
und auf dem Zielsystem ausgeführt/messbar.
|
||||
und auf dem Zielsystem ausgeführt/messbar. Der RemoteStateMirror hält den
|
||||
über IPC übermittelten Showzustand (§6.2, §6.4).
|
||||
"""
|
||||
|
||||
from hms_renderer.pipelines import (
|
||||
@@ -12,6 +13,7 @@ from hms_renderer.pipelines import (
|
||||
build_single_video_pipeline,
|
||||
gst_available,
|
||||
)
|
||||
from hms_renderer.state_mirror import RemoteStateMirror, apply_envelope
|
||||
|
||||
__all__ = [
|
||||
"D3D11Pipeline",
|
||||
@@ -19,4 +21,6 @@ __all__ = [
|
||||
"build_single_video_pipeline",
|
||||
"build_compositor_pipeline",
|
||||
"gst_available",
|
||||
"RemoteStateMirror",
|
||||
"apply_envelope",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Remote-State-Mirror auf der Renderer-Seite (PLAN.md §6.2, §6.4).
|
||||
|
||||
Der Renderer hält den zuletzt übermittelten Showzustand als lokales
|
||||
Spiegelbild:
|
||||
- nach Verbindung: vollständiger Snapshot (§6.2 Pflicht)
|
||||
- danach inkrementelle Deltas mit monotoner Revision
|
||||
- Deltas werden erst nach erfolgtem Snapshot akzeptiert (§6.2:
|
||||
„Deltas erst nach erfolgreichem Re-Sync")
|
||||
- veraltete/doppelte Deltas sind idempotente No-Ops (§6.5)
|
||||
|
||||
Der Mirror ist reine Zustandslogik ohne Pixelbezug (§33); der Rendergraph
|
||||
liest Werte über get_value() je Frame.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class RemoteStateMirror:
|
||||
"""Spiegel des autoritativen Showzustands im Renderer."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._revision = 0
|
||||
self._values: dict[str, float] = {}
|
||||
self._has_snapshot = False
|
||||
|
||||
@property
|
||||
def revision(self) -> int:
|
||||
return self._revision
|
||||
|
||||
@property
|
||||
def has_snapshot(self) -> bool:
|
||||
"""True, nachdem ein vollständiger Snapshot empfangen wurde."""
|
||||
return self._has_snapshot
|
||||
|
||||
def get_value(self, path: str, default: float | None = None) -> float | None:
|
||||
"""Wirksamer Wert für einen Parameterpfad (§10.2)."""
|
||||
return self._values.get(path, default)
|
||||
|
||||
def all_values(self) -> dict[str, float]:
|
||||
return dict(self._values)
|
||||
|
||||
# ---------- Snapshot / Delta (§6.2, §6.4) ----------
|
||||
|
||||
def apply_snapshot(self, payload: dict) -> None:
|
||||
"""Übernimmt einen vollständigen Snapshot (nach Verbindung/Reconnect)."""
|
||||
values = payload.get("values", {})
|
||||
if not isinstance(values, dict):
|
||||
raise ValueError("Snapshot ohne Werte-Objekt")
|
||||
self._revision = int(payload["state_revision"])
|
||||
self._values = {str(k): float(v) for k, v in values.items()}
|
||||
self._has_snapshot = True
|
||||
|
||||
def apply_delta(self, payload: dict) -> bool:
|
||||
"""Wendet ein Delta an.
|
||||
|
||||
Rückgabe:
|
||||
- True: Delta angewendet ODER als veraltetes Duplikat ignoriert
|
||||
- False: Re-Sync nötig (noch kein Snapshot empfangen, §6.2)
|
||||
|
||||
Gelöschte Pfade sind als None kodiert (StateDelta-Vertrag).
|
||||
"""
|
||||
if not self._has_snapshot:
|
||||
return False # §6.2: Deltas erst nach Snapshot
|
||||
delta_revision = int(payload["state_revision"])
|
||||
if delta_revision <= self._revision:
|
||||
return True # idempotent: Duplikat/veraltet, kein Handlungsbedarf
|
||||
changes = payload.get("changes", {})
|
||||
if not isinstance(changes, dict):
|
||||
raise ValueError("Delta ohne Changes-Objekt")
|
||||
for path, value in changes.items():
|
||||
if value is None:
|
||||
self._values.pop(str(path), None) # gelöscht
|
||||
else:
|
||||
self._values[str(path)] = float(value)
|
||||
self._revision = delta_revision
|
||||
return True
|
||||
|
||||
|
||||
def apply_envelope(mirror: RemoteStateMirror, envelope) -> bool:
|
||||
"""Verarbeitet ein IPC-Envelope in den Mirror.
|
||||
|
||||
- SNAPSHOT: vollständige Übernahme
|
||||
- EVENT mit changes: Delta-Anwendung
|
||||
- alles andere (Heartbeat, Ack, …): keine Zustandswirkung
|
||||
|
||||
Rückgabe: True, wenn der Zustand dadurch (re-)synchronisiert wurde;
|
||||
False, wenn ein Re-Sync (neuer Snapshot) angefordert werden muss.
|
||||
"""
|
||||
from hms_protocol import MessageType
|
||||
|
||||
if envelope.type is MessageType.SNAPSHOT:
|
||||
mirror.apply_snapshot(envelope.payload)
|
||||
return True
|
||||
if envelope.type is MessageType.EVENT and "changes" in envelope.payload:
|
||||
return mirror.apply_delta(envelope.payload)
|
||||
return True # keine Zustandsnachricht: nichts zu tun
|
||||
Reference in New Issue
Block a user