diff --git a/docs/plugin-sdk/README.md b/docs/plugin-sdk/README.md new file mode 100644 index 0000000..776f316 --- /dev/null +++ b/docs/plugin-sdk/README.md @@ -0,0 +1,75 @@ +# HMS MediaEngine – Plugin-SDK-Dokumentation + +Diese Dokumentation beschreibt das Plugin-System der HMS MediaEngine (PLAN.md §14) und das eingebaute Starterpaket (§15). Sie richtet sich an Entwickler, die eigene Plugins schreiben oder die eingebauten Plugins als Vorlage nutzen. + +## Was ist ein Plugin? + +Ein Plugin ist ein selbstbeschreibendes Paket, das der MediaEngine eine neue Fähigkeit hinzufügt – typischerweise einen GPU-Effekt (Filter/Generator) oder eine Quelle. Jedes Plugin besteht aus einem Manifest (`plugin.json`) und den dazugehörigen Shader-Dateien. Die Engine validiert das Paket, kompiliert die Shader und bindet sie an die Render-Pipeline (PLAN.md §14.1, §14.5). + +## Pluginarten (§14.1) + +| Typ | Aufgabe | +| --- | --- | +| `source` | Video, Bild, Capture, Stream oder andere Texturquelle | +| `generator` | generiert GPU-Inhalt ohne Eingangsbild | +| `filter` | verarbeitet eine Eingangs-Textur | +| `transition` | mischt zwei Quellen zeitabhängig | +| `mixer` | spezielles Layer-Compositing | +| `output` | HDMI, Art-Net-Pixel, später NDI/Spout/Recording | +| `control` | Art-Net, später MIDI, OSC, HTTP/Webhook | +| `automation` | LFO, Audio-Mapping oder spätere KI-Regeln | + +## Verzeichnisstruktur (§14.2) + +Ein Plugin wird als Verzeichnis oder validiertes ZIP installiert: + +```text +com.hms.fx.gaussian_blur/ +├─ plugin.json +├─ shaders/ +│ ├─ d3d11/ +│ │ ├─ horizontal.hlsl +│ │ └─ vertical.hlsl +│ ├─ gl/ +│ │ ├─ horizontal.frag +│ │ └─ vertical.frag +│ └─ gles/ +│ ├─ horizontal.frag +│ └─ vertical.frag +├─ presets/ +├─ thumbnail.png +├─ LICENSE +└─ README.md +``` + +Die Shader liegen je Backend in einem eigenen Unterordner (`d3d11/`, `gl/`, `gles/`). Weitere Dateien wie `presets/`, `thumbnail.png`, `LICENSE` und `README.md` sind optional. + +## Quick Start + +Ein minimales Filter-Plugin besteht aus drei Schritten: + +1. **Manifest anlegen** – `plugin.json` mit Pflichtfeldern (`schema_version`, `id`, `name`, `version`, `api_version`, `kind`, `vendor`, `entrypoints`, `capabilities`, `failure_mode`). Siehe [manifest-reference.md](manifest-reference.md). +2. **Shader schreiben** – je deklariertem Backend eine Shader-Datei, die den Standard-Uniform-Satz (§14.4) und die Backend-Syntaxregeln einhält. Siehe [shader-contract.md](shader-contract.md). +3. **Validieren** – das Manifest wird gegen das Schema und den Validator geprüft (`packages/plugin_sdk/hms_plugin_sdk/manifest.py`). Shader müssen für jedes deklarierte Backend existieren. + +Ein vollständiges, schrittweises Beispiel von null anhand des eingebauten Vignette-Plugins findest du in [example-walkthrough.md](example-walkthrough.md). + +## Dokumente + +| Dokument | Inhalt | +| --- | --- | +| [README.md](README.md) | Übersicht, Pluginarten, Verzeichnisstruktur, Quick Start | +| [manifest-reference.md](manifest-reference.md) | Vollständige `plugin.json`-Feldreferenz | +| [shader-contract.md](shader-contract.md) | Standard-Uniform-Satz, cbuffer-Layout, Backend-Syntax | +| [parameters-and-dmx.md](parameters-and-dmx.md) | Parameter, DMX-Slots, Kurven, gemeinsamer Effektvertrag | +| [lifecycle.md](lifecycle.md) | Lebenszyklus, Quarantäne, Show-Lock | +| [adaptive-quality.md](adaptive-quality.md) | Adaptive-Quality-Varianten und Wechselregeln | +| [example-walkthrough.md](example-walkthrough.md) | Schritt-für-Schritt-Beispiel (Vignette) | + +## Sicherheitsgrenzen (§14.6) + +- Shaderplugins erhalten keinen Dateisystem- oder Netzwerkzugriff. +- Native DLL-Plugins sind vor Version 2 nicht vorgesehen. +- Python-Control-/Automation-Plugins laufen später in einem separaten Prozess mit freigegebener Command-API. +- Kein Plugin greift direkt auf SQLite oder interne Python-Objekte zu. +- Pluginfehler werden einem konkreten Plugin zugeordnet und in der UI angezeigt. diff --git a/docs/plugin-sdk/adaptive-quality.md b/docs/plugin-sdk/adaptive-quality.md new file mode 100644 index 0000000..3ecf335 --- /dev/null +++ b/docs/plugin-sdk/adaptive-quality.md @@ -0,0 +1,84 @@ +# Adaptive Quality + +Dieses Dokument beschreibt Adaptive Quality (AQ) für Plugins. Grundlage sind PLAN.md §14.3 (Beispielmanifest), §15.3 (Bedien- und Presetpflichten), §15.4 (Abnahme) und das JSON-Schema `schemas/plugin/plugin_manifest_v1.schema.json`. + +## Zweck + +Adaptive Quality erlaubt es einem Effekt, die interne Rechenlast an die verfügbare Hardware anzupassen, ohne die semantische Wirkung der Parameter zu verändern. Die Engine wählt automatisch eine Variante oder der Bediener setzt eine feste Qualitätsstufe (`Quality: Auto` oder fest, PLAN.md §14.8). + +## Varianten-Deklaration + +AQ-Varianten werden im Manifest unter `adaptive_quality` deklariert: + +```json +"adaptive_quality": { + "default": "auto", + "variants": [ + {"id": "low", "internal_scale": 0.25, "samples": 5}, + {"id": "medium", "internal_scale": 0.5, "samples": 9}, + {"id": "high", "internal_scale": 1.0, "samples": 17} + ], + "transition_ms": 180, + "semantic_parameters_unchanged": ["radius", "mix"] +} +``` + +### Felder + +| Feld | Typ | Pflicht | Beschreibung | +| --- | --- | --- | --- | +| `default` | string | ja | Standard-Variante, z. B. `auto` | +| `variants` | array, minItems 1 | ja | Liste der Varianten | +| `transition_ms` | number, minimum 0 | nein | Übergangszeit in Millisekunden | +| `semantic_parameters_unchanged` | array of string | nein | Parameter, deren Semantik beim Wechsel unverändert bleibt | + +Jede Variante ist ein Objekt mit: + +| Feld | Typ | Pflicht | Beschreibung | +| --- | --- | --- | --- | +| `id` | string | ja | Varianten-ID, z. B. `low`, `medium`, `high` | +| `internal_scale` | number, exclusiveMinimum 0, maximum 1 | nein | Interne Auflösungsskala | +| `samples` | integer, minimum 1 | nein | Sample-Anzahl | + +## `semantic_parameters_unchanged` + +Dieses Feld listet Parameter, deren **semantische Bedeutung** beim Wechsel der Qualitätsstufe unverändert bleibt. Ein Parameterwert wie `radius` oder `mix` muss auf allen Varianten dasselbe visuelle Ergebnis liefern; nur die interne Abtastung (Auflösung, Sample-Anzahl) darf variieren (PLAN.md §15.3). + +## Kompilierung vor Aktivierung + +Die Validierung verlangt **vollständige, vorab kompilierbare Adaptive-Quality-Varianten** (PLAN.md §14.5). Jede deklarierte Variante muss vor der Aktivierung kompilierbar sein. Ein Plugin, dessen Varianten nicht vollständig kompilierbar sind, wird nicht aktiviert. + +## Wechsel nur an der Framegrenze + +Der Wechsel der Qualitätsstufe erfolgt **atomar an einer Framegrenze** (PLAN.md §14.8). Verschieben, Bypass, Kopieren und Presetwechsel müssen ebenfalls atomar an einer Framegrenze erfolgen. Dadurch wird verhindert, dass ein halber Frame mit gemischten Qualitätsstufen gerendert wird. + +## Beispiel: Vignette + +Das eingebaute Vignette-Plugin deklariert drei Varianten, die alle dieselbe interne Auflösung und Sample-Anzahl verwenden (ein GPU-Pass, PLAN.md §15.2): + +```json +"adaptive_quality": { + "default": "auto", + "variants": [ + {"id": "low", "internal_scale": 1.0, "samples": 1}, + {"id": "medium", "internal_scale": 1.0, "samples": 1}, + {"id": "high", "internal_scale": 1.0, "samples": 1} + ], + "transition_ms": 180, + "semantic_parameters_unchanged": ["mix"] +} +``` + +## Abnahme (§15.4) + +Für jedes eingebaute Plugin sind verpflichtend: + +- HLSL-Implementierung für D3D11 sowie GLSL/GLES-Varianten gemäß Capability; +- Golden Images bei mindestens drei Parametersätzen; +- zeitabhängige Tests mit festem Seed und festem Frameindex; +- Alpha-/Premultiplication-Test und definierter Farbraum; +- Test für Extremwerte, NaN/Infinity und Auflösung 1 × 1; +- atomarer Bypass-, Preset- und Quality-Wechsel; +- dokumentierte GPU-Zeit bei 1080p und, sofern Tier erlaubt, 4K; +- Layer-, Adjustment-/Group- und Master-Scope-Test; +- P1–P8-/G1–G8-Kanalbelegung im generierten Fixture-Handbuch. diff --git a/docs/plugin-sdk/example-walkthrough.md b/docs/plugin-sdk/example-walkthrough.md new file mode 100644 index 0000000..ed6c54d --- /dev/null +++ b/docs/plugin-sdk/example-walkthrough.md @@ -0,0 +1,328 @@ +# Beispiel: Ein Filter-Plugin von null schreiben + +Dieses Dokument führt Schritt für Schritt durch die Erstellung eines neuen Filter-Plugins. Als Vorlage dient das eingebaute Vignette-Plugin (`plugins/builtin/filters/com.hms.fx.vignette/`), das ein GPU-Pass-Filter ist (PLAN.md §15.2). + +## Ziel + +Wir erstellen ein Plugin `com.hms.fx.vignette` – einen Filter, der die Ränder eines Bildes abdunkelt oder einfärbt. Das Plugin unterstützt drei Backends: D3D11 (HLSL), OpenGL (GLSL) und OpenGL ES (GLES). + +## Schritt 1: Verzeichnisstruktur anlegen + +Ein Plugin ist ein Verzeichnis mit `plugin.json` und Shader-Unterordnern je Backend: + +```text +com.hms.fx.vignette/ +├─ plugin.json +└─ shaders/ + ├─ d3d11/ + │ └─ main.hlsl + ├─ gl/ + │ └─ main.frag + └─ gles/ + └─ main.frag +``` + +## Schritt 2: Manifest schreiben + +Lege `plugin.json` an. Die Pflichtfelder sind `schema_version`, `id`, `name`, `version`, `api_version`, `kind`, `vendor`, `entrypoints`, `capabilities`, `failure_mode` (siehe [manifest-reference.md](manifest-reference.md)). + +```json +{ + "schema_version": 1, + "id": "com.hms.fx.vignette", + "name": "Vignette", + "version": "1.0.0", + "api_version": 1, + "kind": "filter", + "vendor": "HMS", + "entrypoints": { + "d3d11": { + "type": "hlsl", + "passes": [ + {"pixel_shader": "shaders/d3d11/main.hlsl"} + ] + }, + "gl": { + "type": "glsl", + "passes": [ + {"fragment": "shaders/gl/main.frag"} + ] + }, + "gles": { + "type": "glsl_es", + "passes": [ + {"fragment": "shaders/gles/main.frag"} + ] + } + }, + "capabilities": { + "minimum_tier": "PI_LITE", + "requires_input_texture": true, + "supported_backends": ["d3d11", "gl", "gles"] + }, + "parameters": [ + {"id": "amount", "label": "Amount", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [1]}, + {"id": "radius", "label": "Radius", "type": "float", "minimum": 0, "maximum": 2, "default": 0.5, "dmx_slots": [2]}, + {"id": "softness", "label": "Softness", "type": "float", "minimum": 0, "maximum": 1, "default": 0.4, "dmx_slots": [3]}, + {"id": "roundness", "label": "Roundness", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [4]}, + {"id": "center_x", "label": "Center X", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [5]}, + {"id": "center_y", "label": "Center Y", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [6]}, + {"id": "color", "label": "Color", "type": "color", "default": [0, 0, 0], "dmx_slots": [7]}, + {"id": "invert", "label": "Invert", "type": "bool", "default": 0, "dmx_slots": [8]}, + {"id": "mix", "label": "Mix", "type": "float", "minimum": 0, "maximum": 1, "default": 1, "dmx_slots": []} + ], + "failure_mode": "bypass", + "adaptive_quality": { + "default": "auto", + "variants": [ + {"id": "low", "internal_scale": 1.0, "samples": 1}, + {"id": "medium", "internal_scale": 1.0, "samples": 1}, + {"id": "high", "internal_scale": 1.0, "samples": 1} + ], + "transition_ms": 180, + "semantic_parameters_unchanged": ["mix"] + } +} +``` + +Wichtige Punkte: + +- `kind: "filter"` – verarbeitet eine Eingangs-Textur. +- `requires_input_texture: true` – der Filter braucht eine Eingangs-Textur. +- `dmx_slots` belegen die Slots 1–8; `mix` belegt keinen Slot (gemeinsamer Effektvertrag, §14.8). +- `failure_mode: "bypass"` – bei Fehler wird der Effekt überbrückt (§3.5). +- `adaptive_quality` deklariert drei Varianten; `semantic_parameters_unchanged: ["mix"]`. + +## Schritt 3: HLSL-Shader schreiben (D3D11) + +Lege `shaders/d3d11/main.hlsl` an. Der Shader deklariert die Textur, den Sampler und den Konstantenpuffer `hms_params` mit dem Standard-Uniform-Satz (§14.4) und den Pluginparametern: + +```hlsl +// HMS MediaEngine - Vignette (PLAN.md §15.2) +// Randabdunklung/-färbung; ein GPU-Pass. + +Texture2D u_input_texture : register(t0); +SamplerState u_sampler : register(s0); + +cbuffer hms_params : register(b0) +{ + float4 u_resolution; // xy = Auflösung in Pixeln + float u_time_seconds; + float u_delta_seconds; + float u_frame_index; + float u_layer_opacity; + float u_audio_rms; + float u_audio_peak; + float u_audio_bass; + float u_audio_mid; + float u_audio_treble; + float u_audio_beat; + float param_amount; + float param_radius; + float param_softness; + float param_roundness; + float param_center_x; + float param_center_y; + float param_color_r; + float param_color_g; + float param_color_b; + float param_invert; // 0 = abdunkeln, 1 = aufhellen + float param_mix; + float _pad0; + float _pad1; + float _pad2; + float _pad3; +}; + +float4 mainPS(float4 pos : SV_POSITION, float2 uv : TEXCOORD0) : SV_Target +{ + float4 src = u_input_texture.Sample(u_sampler, uv); + float m = saturate(param_mix); + if (m < 0.01) + { + return src; // Mix 0 = kostenloser Bypass (§15.3) + } + + float2 c = float2(param_center_x, param_center_y); + float2 d = uv - c; + d.x *= lerp(1.0, u_resolution.x / max(u_resolution.y, 1.0), param_roundness); + float dist = length(d); + + float radius = max(param_radius, 0.0001); + float soft = max(param_softness, 0.0001); + float v = smoothstep(radius, radius + soft, dist); + v = saturate(v * param_amount); + if (param_invert > 0.5) + v = 1.0 - v; + + float3 vignette = lerp(src.rgb, float3(param_color_r, param_color_g, param_color_b), v); + float3 rgb = lerp(src.rgb, vignette, m); + float4 out_c = float4(rgb, src.a); + out_c.rgb *= u_layer_opacity; + out_c.a *= u_layer_opacity; + return out_c; +} +``` + +## Schritt 4: GLSL-Shader schreiben (OpenGL) + +Lege `shaders/gl/main.frag` an. Die Semantik ist identisch zur HLSL-Variante (§12.6, §15.4), nur die Syntax unterscheidet sich: + +```glsl +#version 330 core +// HMS MediaEngine - Vignette (GLSL, Linux x64) +// Semantik identisch zur HLSL-Variante (§12.6, §15.4). + +uniform sampler2D u_input_texture; +uniform vec2 u_resolution; +uniform float u_time_seconds; +uniform float u_delta_seconds; +uniform float u_frame_index; +uniform float u_layer_opacity; +uniform float u_audio_rms; +uniform float u_audio_peak; +uniform float u_audio_bass; +uniform float u_audio_mid; +uniform float u_audio_treble; +uniform float u_audio_beat; +uniform float param_amount; +uniform float param_radius; +uniform float param_softness; +uniform float param_roundness; +uniform float param_center_x; +uniform float param_center_y; +uniform float param_color_r; +uniform float param_color_g; +uniform float param_color_b; +uniform float param_invert; +uniform float param_mix; + +in vec2 v_uv; +out vec4 fragColor; + +void main() +{ + vec4 src = texture(u_input_texture, v_uv); + float m = clamp(param_mix, 0.0, 1.0); + if (m < 0.01) + { + fragColor = src; + return; + } + + vec2 c = vec2(param_center_x, param_center_y); + vec2 d = v_uv - c; + d.x *= mix(1.0, u_resolution.x / max(u_resolution.y, 1.0), param_roundness); + float dist = length(d); + + float radius = max(param_radius, 0.0001); + float soft = max(param_softness, 0.0001); + float v = smoothstep(radius, radius + soft, dist); + v = clamp(v * param_amount, 0.0, 1.0); + if (param_invert > 0.5) + v = 1.0 - v; + + vec3 vignette = mix(src.rgb, vec3(param_color_r, param_color_g, param_color_b), v); + vec3 rgb = mix(src.rgb, vignette, m); + vec4 out_c = vec4(rgb, src.a); + out_c.rgb *= u_layer_opacity; + out_c.a *= u_layer_opacity; + fragColor = out_c; +} +``` + +## Schritt 5: GLES-Shader schreiben (OpenGL ES 2.0) + +Lege `shaders/gles/main.frag` an. GLES 2.0 hat eigene Regeln: `#version 100`, `precision mediump float;`, `texture2D` statt `texture`, `varying` statt `in`/`out`, `gl_FragColor` statt `fragColor` (siehe [shader-contract.md](shader-contract.md)): + +```glsl +#version 100 +// HMS MediaEngine - Vignette (GLES, Raspberry Pi) +// ES 2.0; ein GPU-Pass. +precision mediump float; + +uniform sampler2D u_input_texture; +uniform vec2 u_resolution; +uniform float u_time_seconds; +uniform float u_delta_seconds; +uniform float u_frame_index; +uniform float u_layer_opacity; +uniform float u_audio_rms; +uniform float u_audio_peak; +uniform float u_audio_bass; +uniform float u_audio_mid; +uniform float u_audio_treble; +uniform float u_audio_beat; +uniform float param_amount; +uniform float param_radius; +uniform float param_softness; +uniform float param_roundness; +uniform float param_center_x; +uniform float param_center_y; +uniform float param_color_r; +uniform float param_color_g; +uniform float param_color_b; +uniform float param_invert; +uniform float param_mix; + +varying vec2 v_uv; + +void main() +{ + vec4 src = texture2D(u_input_texture, v_uv); + float m = clamp(param_mix, 0.0, 1.0); + if (m < 0.01) + { + gl_FragColor = src; + return; + } + + vec2 c = vec2(param_center_x, param_center_y); + vec2 d = v_uv - c; + d.x *= mix(1.0, u_resolution.x / max(u_resolution.y, 1.0), param_roundness); + float dist = length(d); + + float radius = max(param_radius, 0.0001); + float soft = max(param_softness, 0.0001); + float v = smoothstep(radius, radius + soft, dist); + v = clamp(v * param_amount, 0.0, 1.0); + if (param_invert > 0.5) + v = 1.0 - v; + + vec3 vignette = mix(src.rgb, vec3(param_color_r, param_color_g, param_color_b), v); + vec3 rgb = mix(src.rgb, vignette, m); + vec4 out_c = vec4(rgb, src.a); + out_c.rgb *= u_layer_opacity; + out_c.a *= u_layer_opacity; + gl_FragColor = out_c; +} +``` + +## Schritt 6: Validieren + +Das Plugin wird über den Lifecycle-Manager validiert (`packages/plugin_sdk/hms_plugin_sdk/lifecycle.py`). `discover()` lädt `plugin.json`, validiert es (inkl. Shader-Existenz) und überführt das Plugin in `validated` oder `quarantined`/`incompatible` (siehe [lifecycle.md](lifecycle.md)). + +Der Validator prüft unter anderem: + +- Manifest-Schema; +- eindeutige Plugin-ID und semantische Version; +- API-Kompatibilität; +- Pfad- und ZIP-Sicherheit; +- erlaubte Dateitypen und Größenlimits; +- Shader-Kompilierung; +- Capability-Prüfung; +- vollständige, vorab kompilierbare Adaptive-Quality-Varianten und unveränderte Parametersemantik; +- Lizenzmetadaten; +- Hash des Pakets. + +## Schritt 7: Aktivieren + +Nach erfolgreicher Validierung durchläuft das Plugin den Lebenszyklus `validated → installed → enabled → compiled → active`. Der Wechsel der Qualitätsstufe erfolgt atomar an einer Framegrenze (siehe [adaptive-quality.md](adaptive-quality.md)). + +## Zusammenfassung + +Ein Filter-Plugin besteht aus: + +1. `plugin.json` mit Pflichtfeldern, Parametern und Adaptive Quality; +2. je deklariertem Backend eine Shader-Datei mit dem Standard-Uniform-Satz (§14.4); +3. semantisch identischen Implementierungen über alle Backends (§12.6, §15.4). diff --git a/docs/plugin-sdk/lifecycle.md b/docs/plugin-sdk/lifecycle.md new file mode 100644 index 0000000..ac8913d --- /dev/null +++ b/docs/plugin-sdk/lifecycle.md @@ -0,0 +1,83 @@ +# Plugin-Lebenszyklus + +Dieses Dokument beschreibt den Lebenszyklus eines Plugins. Grundlage sind PLAN.md §14.5 (Plugin-Lebenszyklus), §14.6 (Sicherheitsgrenze), §26.3 (Show-Lock) und die Implementierung `packages/plugin_sdk/hms_plugin_sdk/lifecycle.py`. + +## Zustände + +Der Lebenszyklus kennt folgende Zustände: + +```text +discovered → validated → installed → enabled → compiled → active + ↘ quarantined / incompatible +``` + +Zusätzlich existiert der Zustand `disabled` (deaktiviert, aber installiert). Die vollständige Zustandsmenge (`lifecycle.py`): + +- `discovered` +- `validated` +- `installed` +- `enabled` +- `compiled` +- `active` +- `quarantined` +- `incompatible` +- `disabled` + +## Übergänge + +Übergänge sind nur entlang definierter Kanten erlaubt; Sprünge sind Fehler (`InvalidTransitionError`). Die erlaubten Übergänge (`lifecycle.py`): + +| Von | Nach | +| --- | --- | +| `discovered` | `validated`, `incompatible`, `quarantined` | +| `validated` | `installed`, `incompatible`, `quarantined` | +| `installed` | `enabled`, `disabled`, `quarantined` | +| `enabled` | `compiled`, `disabled`, `quarantined` | +| `compiled` | `active`, `quarantined`, `disabled` | +| `active` | `disabled`, `quarantined` | +| `disabled` | `enabled`, `quarantined` | +| `quarantined` | – (manuelle Entfernung/Neuinstallation) | +| `incompatible` | – | + +## Discovery und Validierung + +`discover()` scannt ein Verzeichnis mit Plugin-Ordnern, lädt `plugin.json` und validiert es (inkl. Shader-Existenz). Jedes Plugin wird in `validated` oder `incompatible`/`quarantined` überführt (`lifecycle.py`). + +Die Validierung umfasst (PLAN.md §14.5): + +- Manifest-Schema; +- eindeutige Plugin-ID und semantische Version; +- API-Kompatibilität; +- Pfad- und ZIP-Sicherheit; +- erlaubte Dateitypen und Größenlimits; +- Shader-Kompilierung; +- Capability-Prüfung; +- vollständige, vorab kompilierbare Adaptive-Quality-Varianten und unveränderte Parametersemantik; +- Lizenzmetadaten; +- Hash des Pakets. + +## Quarantäne (§3.5, §14.5) + +Ein fehlerhaftes Plugin wird **quarantiniert**, ohne ein Projekt unbrauchbar zu machen. Der Effekt wird überbrückt (`bypass_on_error`). Die Quarantäne speichert die Ursache (`last_error`). Aus `quarantined` gibt es keinen automatischen Übergang; das Plugin muss manuell entfernt oder neu installiert werden. + +## Show-Lock (§26.3) + +Der Show-Lock sperrt strukturelle Änderungen während einer laufenden Show: + +- **Installieren/Updaten ist gesperrt.** Übergänge, die Installation/Update bedeuten (`installed` von `discovered`/`validated`), sind im Show-Lock blockiert. +- **Enable/Disable und Parameter bleiben erlaubt.** Aktivieren bereits installierter Plugins (`enabled`/`compiled`/`active`) bleibt möglich (PLAN.md §17.7 Live-Modus). +- **Versionswechsel sind gesperrt.** Ein Update startet einen neuen Zyklus; im Show-Lock wird ein Versionswechsel abgelehnt. + +Der Show-Lock wird über `set_show_lock(enabled)` gesetzt (`lifecycle.py`). + +## Doppelte Plugin-IDs + +Doppelte Plugin-IDs sind Fehler, keine stillen Überschreibungen. Ein Versionskonflikt wird erkannt; ohne Show-Lock gewinnt der neue Stand, mit Show-Lock wird der Versionswechsel abgelehnt (`lifecycle.py`). + +## Sicherheitsgrenze (§14.6) + +- Shaderplugins erhalten keinen Dateisystem- oder Netzwerkzugriff. +- Native DLL-Plugins sind vor Version 2 nicht vorgesehen. +- Python-Control-/Automation-Plugins laufen später in einem separaten Prozess mit freigegebener Command-API. +- Kein Plugin greift direkt auf SQLite oder interne Python-Objekte zu. +- Pluginfehler werden einem konkreten Plugin zugeordnet und in der UI angezeigt. diff --git a/docs/plugin-sdk/manifest-reference.md b/docs/plugin-sdk/manifest-reference.md new file mode 100644 index 0000000..01b8e8e --- /dev/null +++ b/docs/plugin-sdk/manifest-reference.md @@ -0,0 +1,259 @@ +# `plugin.json` – Manifest-Referenz + +Dieses Dokument beschreibt alle Felder des Plugin-Manifests `plugin.json`. Grundlage sind das JSON-Schema `schemas/plugin/plugin_manifest_v1.schema.json` und der Validator `packages/plugin_sdk/hms_plugin_sdk/manifest.py` (PLAN.md §14.2–14.3). + +## Schema-Version und API-Version + +| Feld | Typ | Pflicht | Wert | +| --- | --- | --- | --- | +| `schema_version` | integer | ja | `1` (konstant) | +| `api_version` | integer | ja | `1` (konstant) | + +`schema_version` und `api_version` müssen exakt `1` sein. Der Validator lehnt jede andere Version ab (`manifest.py`). + +## Pflichtfelder + +Das Schema verlangt folgende Felder auf oberster Ebene: + +```json +["schema_version", "id", "name", "version", "api_version", "kind", "vendor", "entrypoints", "capabilities", "failure_mode"] +``` + +### `id` + +- **Typ:** string +- **Pflicht:** ja +- **Regeln:** Reverse-DNS-Notation, mindestens 5 Zeichen, mindestens zwei durch `.` getrennte Teile, nur Kleinbuchstaben `a–z`, Ziffern `0–9`, `.`, `_`, `-`. Kein `..` erlaubt. +- **Beispiel:** `com.hms.fx.vignette` + +### `name` + +- **Typ:** string, minLength 1 +- **Pflicht:** ja +- **Beispiel:** `Vignette` + +### `version` + +- **Typ:** string +- **Pflicht:** ja +- **Regeln:** Semantische Version `X.Y.Z` (genau drei durch `.` getrennte Ganzzahlen). +- **Beispiel:** `1.0.0` + +### `kind` + +- **Typ:** enum +- **Pflicht:** ja +- **Werte:** `source`, `generator`, `filter`, `transition`, `mixer`, `output`, `control`, `automation` (PLAN.md §14.1) +- **Beispiel:** `filter` + +### `vendor` + +- **Typ:** string, minLength 1 +- **Pflicht:** ja +- **Beispiel:** `HMS` + +### `entrypoints` + +- **Typ:** object, minProperties 1 +- **Pflicht:** ja +- **Beschreibung:** Backend-Entrypoints mit Shader-Pässen. Erlaubte Backend-Schlüssel: `d3d11`, `gl`, `gles`. + +Jeder Entrypoint ist ein Objekt mit: + +| Feld | Typ | Pflicht | Beschreibung | +| --- | --- | --- | --- | +| `type` | string | ja | Entrypoint-Typ, z. B. `hlsl`, `glsl`, `glsl_es`, `hlsl_multipass`, `glsl_multipass`, `glsl_es_multipass` | +| `passes` | array, minItems 1 | ja | Liste der Shader-Pässe | + +Jeder Pass ist ein Objekt mit genau einem Shader-Pfad: + +- **D3D11:** `pixel_shader` (z. B. `shaders/d3d11/main.hlsl`) +- **GL/GLES:** `fragment` (z. B. `shaders/gl/main.frag`) + +**Beispiel (Einpass, Vignette):** + +```json +"entrypoints": { + "d3d11": { + "type": "hlsl", + "passes": [{"pixel_shader": "shaders/d3d11/main.hlsl"}] + }, + "gl": { + "type": "glsl", + "passes": [{"fragment": "shaders/gl/main.frag"}] + }, + "gles": { + "type": "glsl_es", + "passes": [{"fragment": "shaders/gles/main.frag"}] + } +} +``` + +**Beispiel (Multipass, Gaussian Blur, PLAN.md §14.3):** + +```json +"entrypoints": { + "d3d11": { + "type": "hlsl_multipass", + "passes": [ + {"pixel_shader": "shaders/d3d11/horizontal.hlsl"}, + {"pixel_shader": "shaders/d3d11/vertical.hlsl"} + ] + }, + "gl": { + "type": "glsl_multipass", + "passes": [ + {"fragment": "shaders/gl/horizontal.frag"}, + {"fragment": "shaders/gl/vertical.frag"} + ] + }, + "gles": { + "type": "glsl_es_multipass", + "passes": [ + {"fragment": "shaders/gles/horizontal.frag"}, + {"fragment": "shaders/gles/vertical.frag"} + ] + } +} +``` + +Der Validator prüft, dass jeder deklarierte Shader-Pfad relativ und sicher ist (kein absoluter Pfad, kein `..`) und – wenn ein Plugin-Root übergeben wird – tatsächlich existiert (`manifest.py`). + +### `capabilities` + +- **Typ:** object +- **Pflicht:** ja +- **Pflichtfelder:** `minimum_tier`, `supported_backends` + +| Feld | Typ | Pflicht | Beschreibung | +| --- | --- | --- | --- | +| `minimum_tier` | enum | ja | `DESKTOP_FULL`, `DESKTOP_LITE`, `PI_LITE`, `HEADLESS_CONTROL` | +| `requires_input_texture` | boolean | nein | `true`, wenn der Effekt eine Eingangs-Textur braucht (Filter) | +| `supported_backends` | array, minItems 1 | ja | `d3d11`, `gl`, `gles` | + +**Beispiel (Vignette):** + +```json +"capabilities": { + "minimum_tier": "PI_LITE", + "requires_input_texture": true, + "supported_backends": ["d3d11", "gl", "gles"] +} +``` + +### `failure_mode` + +- **Typ:** enum +- **Pflicht:** ja +- **Werte:** `bypass`, `hold`, `black` +- **Beschreibung:** Verhalten bei einem Pluginfehler. `bypass` überbrückt den Effekt (PLAN.md §3.5, §14.5). +- **Beispiel:** `bypass` + +## Optionale Felder + +### `parameters` + +- **Typ:** array +- **Pflicht:** nein (im Schema), aber empfohlen +- **Beschreibung:** Liste der Plugin-Parameter. Details siehe [parameters-and-dmx.md](parameters-and-dmx.md). + +Jeder Parameter ist ein Objekt mit: + +| Feld | Typ | Pflicht | Beschreibung | +| --- | --- | --- | --- | +| `id` | string, minLength 1 | ja | Eindeutige Parameter-ID | +| `label` | string, minLength 1 | ja | Anzeigename | +| `type` | enum | ja | `float`, `int`, `enum`, `bool`, `color` | +| `default` | beliebig | ja | Standardwert | +| `minimum` | number | nein | Untergrenze (für `float`/`int`) | +| `maximum` | number | nein | Obergrenze (für `float`/`int`) | +| `values` | array | nein | Werte für `enum` | +| `dmx_slots` | array, maxItems 8 | nein | DMX-Slot-Belegung (1–8) | +| `curve` | string | nein | Kurvenform, z. B. `quadratic` | + +**Beispiel (Vignette):** + +```json +"parameters": [ + {"id": "amount", "label": "Amount", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [1]}, + {"id": "radius", "label": "Radius", "type": "float", "minimum": 0, "maximum": 2, "default": 0.5, "dmx_slots": [2]}, + {"id": "softness", "label": "Softness", "type": "float", "minimum": 0, "maximum": 1, "default": 0.4, "dmx_slots": [3]}, + {"id": "roundness", "label": "Roundness", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [4]}, + {"id": "center_x", "label": "Center X", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [5]}, + {"id": "center_y", "label": "Center Y", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [6]}, + {"id": "color", "label": "Color", "type": "color", "default": [0, 0, 0], "dmx_slots": [7]}, + {"id": "invert", "label": "Invert", "type": "bool", "default": 0, "dmx_slots": [8]}, + {"id": "mix", "label": "Mix", "type": "float", "minimum": 0, "maximum": 1, "default": 1, "dmx_slots": []} +] +``` + +**Validator-Regeln für Parameter (`manifest.py`):** + +- `id` muss vorhanden und ein String sein; doppelte IDs sind Fehler. +- `type` muss einer von `float`, `int`, `enum`, `bool`, `color` sein. +- Für `float` müssen `minimum`, `maximum` und `default` vorhanden sein. +- `dmx_slots` muss eine Liste von Ganzzahlen sein. +- Die Summe aller `dmx_slots` über alle Parameter darf **8 nicht überschreiten** (PLAN.md §14.7). + +### `adaptive_quality` + +- **Typ:** object +- **Pflicht:** nein (im Schema), aber für qualitätsabhängige Effekte empfohlen +- **Pflichtfelder:** `default`, `variants` +- **Beschreibung:** Adaptive-Quality-Varianten. Details siehe [adaptive-quality.md](adaptive-quality.md). + +| Feld | Typ | Pflicht | Beschreibung | +| --- | --- | --- | --- | +| `default` | string | ja | Standard-Variante, z. B. `auto` | +| `variants` | array, minItems 1 | ja | Liste der Varianten | +| `transition_ms` | number, minimum 0 | nein | Übergangszeit in Millisekunden | +| `semantic_parameters_unchanged` | array of string | nein | Parameter, deren Semantik beim Wechsel unverändert bleibt | + +Jede Variante ist ein Objekt mit: + +| Feld | Typ | Pflicht | Beschreibung | +| --- | --- | --- | --- | +| `id` | string | ja | Varianten-ID, z. B. `low`, `medium`, `high` | +| `internal_scale` | number, exclusiveMinimum 0, maximum 1 | nein | Interne Auflösungsskala | +| `samples` | integer, minimum 1 | nein | Sample-Anzahl | + +**Beispiel (Vignette):** + +```json +"adaptive_quality": { + "default": "auto", + "variants": [ + {"id": "low", "internal_scale": 1.0, "samples": 1}, + {"id": "medium", "internal_scale": 1.0, "samples": 1}, + {"id": "high", "internal_scale": 1.0, "samples": 1} + ], + "transition_ms": 180, + "semantic_parameters_unchanged": ["mix"] +} +``` + +## Validierung + +Die Validierung (`manifest.py`) umfasst (PLAN.md §14.5): + +- Manifest-Schema; +- eindeutige Plugin-ID und semantische Version; +- API-Kompatibilität; +- Pfad- und ZIP-Sicherheit; +- erlaubte Dateitypen und Größenlimits; +- Shader-Kompilierung; +- Capability-Prüfung; +- vollständige, vorab kompilierbare Adaptive-Quality-Varianten und unveränderte Parametersemantik; +- Lizenzmetadaten; +- Hash des Pakets. + +## ZIP-Sicherheit (§27.2) + +Beim Installieren als ZIP gelten zusätzlich (`manifest.py`): + +- maximal 512 Dateien (`MAX_PLUGIN_FILES`); +- maximale entpackte Gesamtgröße 32 MiB (`MAX_TOTAL_UNPACKED`); +- maximale Dateigröße 8 MiB (`MAX_FILE_SIZE`); +- erlaubte Dateiendungen: `.json`, `.hlsl`, `.frag`, `.vert`, `.glsl`, `.png`, `.md`, `.txt`, `.toml`, `.csv`; +- keine absoluten Pfade, kein `..`; +- `plugin.json` muss an der Paketwurzel liegen. diff --git a/docs/plugin-sdk/parameters-and-dmx.md b/docs/plugin-sdk/parameters-and-dmx.md new file mode 100644 index 0000000..6c7a8c3 --- /dev/null +++ b/docs/plugin-sdk/parameters-and-dmx.md @@ -0,0 +1,131 @@ +# Parameter und DMX + +Dieses Dokument beschreibt Parameter-Definitionen, DMX-Slots, Kurven und den gemeinsamen Effektvertrag. Grundlage sind PLAN.md §14.7 (DMX-Parameter-Slots), §14.8 (Gemeinsamer Effektvertrag), das JSON-Schema und der Validator `packages/plugin_sdk/hms_plugin_sdk/manifest.py`. + +## Parameter-Definitionen + +Parameter werden im Manifest unter `parameters` als Liste von Objekten deklariert. Jeder Parameter besitzt folgende Felder: + +| Feld | Typ | Pflicht | Beschreibung | +| --- | --- | --- | --- | +| `id` | string | ja | Eindeutige Parameter-ID | +| `label` | string | ja | Anzeigename | +| `type` | enum | ja | `float`, `int`, `enum`, `bool`, `color` | +| `default` | beliebig | ja | Standardwert | +| `minimum` | number | nein | Untergrenze (für `float`/`int`) | +| `maximum` | number | nein | Obergrenze (für `float`/`int`) | +| `values` | array | nein | Werte für `enum` | +| `dmx_slots` | array | nein | DMX-Slot-Belegung (1–8) | +| `curve` | string | nein | Kurvenform, z. B. `quadratic` | + +### Parametertypen + +Der Validator (`manifest.py`) erlaubt genau fünf Typen: + +- `float` – Gleitkommawert; `minimum`, `maximum` und `default` sind Pflicht. +- `int` – Ganzzahl. +- `enum` – Aufzählung; `values` listet die erlaubten Werte. +- `bool` – Wahrheitswert (`0`/`1`). +- `color` – Farbe, als RGB-Array (z. B. `[0, 0, 0]`). + +### Validator-Regeln + +- `id` muss vorhanden und ein String sein; doppelte IDs sind Fehler. +- `type` muss einer der fünf erlaubten Typen sein. +- Für `float` müssen `minimum`, `maximum` und `default` vorhanden sein. +- `dmx_slots` muss eine Liste von Ganzzahlen sein. +- Die Summe aller `dmx_slots` über alle Parameter darf **8 nicht überschreiten** (PLAN.md §14.7). + +## DMX-Slots (§14.7) + +Ein Effekt besitzt maximal **acht generische DMX-Slots pro Effektinstanz**. Das Manifest bildet reale Parameter darauf ab: + +- acht 8-Bit-Parameter; oder +- vier 16-Bit-Parameter; oder +- eine gemischte, manifestdefinierte Belegung. + +Der DMX-Footprint des Layer-Fixtures bleibt dadurch stabil, auch wenn neue Plugins installiert werden. + +### Slot-Belegung im Manifest + +Jeder Parameter kann über `dmx_slots` auf einen oder mehrere Slots (1–8) abgebildet werden. Das Vignette-Beispiel belegt die Slots 1–8: + +```json +"parameters": [ + {"id": "amount", "label": "Amount", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [1]}, + {"id": "radius", "label": "Radius", "type": "float", "minimum": 0, "maximum": 2, "default": 0.5, "dmx_slots": [2]}, + {"id": "softness", "label": "Softness", "type": "float", "minimum": 0, "maximum": 1, "default": 0.4, "dmx_slots": [3]}, + {"id": "roundness","label": "Roundness","type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [4]}, + {"id": "center_x", "label": "Center X", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [5]}, + {"id": "center_y", "label": "Center Y", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [6]}, + {"id": "color", "label": "Color", "type": "color", "default": [0, 0, 0], "dmx_slots": [7]}, + {"id": "invert", "label": "Invert", "type": "bool", "default": 0, "dmx_slots": [8]}, + {"id": "mix", "label": "Mix", "type": "float", "minimum": 0, "maximum": 1, "default": 1, "dmx_slots": []} +] +``` + +Der `mix`-Parameter belegt keinen DMX-Slot (`dmx_slots: []`), da er Teil des gemeinsamen Effektvertrags ist. + +### 16-Bit-Werte + +Ein Parameter kann über zwei DMX-Slots als 16-Bit-Wert abgebildet werden (vier 16-Bit-Parameter pro Effektinstanz). Die genaue Belegung ist manifestdefiniert. Der Validator zählt jeden Slot in `dmx_slots` einzeln; die Gesamtsumme darf 8 nicht überschreiten. + +## Kurven + +Parameter können eine Kurvenform über `curve` deklarieren. Das Schema erlaubt einen beliebigen String; das Beispiel aus PLAN.md §14.3 verwendet `quadratic`: + +```json +{ + "id": "radius", + "label": "Radius", + "type": "float", + "minimum": 0.0, + "maximum": 40.0, + "default": 0.0, + "dmx_slots": [1], + "curve": "quadratic" +} +``` + +Unterstützte Kurvenformen: + +- `linear` – lineare Abbildung des DMX-/Control-Werts auf den Parameterbereich. +- `quadratic` – quadratische Abbildung, die feine Abstufungen in einem Teilbereich begünstigt. + +## Gemeinsamer Effektvertrag (§14.8) + +Jede Filterinstanz besitzt unabhängig vom konkreten Effekt: + +- Enable/Bypass; +- Mix beziehungsweise Effect Opacity von 0 bis 1; +- eigenen Blend-Modus gegenüber dem unveränderten Eingang; +- deterministische Position in der Effektkette; +- Reset auf Default; +- speicher-, umbenenn- und kopierbares Preset; +- `Quality: Auto` oder feste Qualitätsstufe; +- acht stabile generische DMX-Parameter-Slots `P1` bis `P8`; +- modulatable Parameter über denselben Parameterpfad wie Browser, Art-Net und spätere Automation. + +### `mix` + +Der `mix`-Parameter (Effect Opacity) liegt zwischen 0 und 1. `Mix 0` muss den Effekt kostengünstig bypassen (PLAN.md §15.3). Das Vignette-Beispiel implementiert das im Shader: + +```hlsl +float m = saturate(param_mix); +if (m < 0.01) +{ + return src; // Mix 0 = kostenloser Bypass (§15.3) +} +``` + +### `blend_mode` + +Jeder Effekt besitzt einen eigenen Blend-Modus gegenüber dem unveränderten Eingang. Der Blend-Modus ist Teil des Effektvertrags und wird pro Instanz gesetzt. + +### `quality` + +Jeder Effekt besitzt `Quality: Auto` oder eine feste Qualitätsstufe. Die Qualitätsstufe wählt eine Adaptive-Quality-Variante (siehe [adaptive-quality.md](adaptive-quality.md)). + +### Instanzierungs-Scope + +Filterplugins dürfen – sofern im Manifest freigegeben – auf Source/Clip, Layer, Group, Adjustment Layer, Master oder Output instanziert werden. V1 muss mindestens Layer, Group/Adjustment und Master unterstützen. Die Kettenreihenfolge ist fachlich relevant und wird strikt von oben nach unten ausgewertet. Verschieben, Bypass, Kopieren und Presetwechsel müssen atomar an einer Framegrenze erfolgen (PLAN.md §14.8). diff --git a/docs/plugin-sdk/shader-contract.md b/docs/plugin-sdk/shader-contract.md new file mode 100644 index 0000000..210ca26 --- /dev/null +++ b/docs/plugin-sdk/shader-contract.md @@ -0,0 +1,239 @@ +# Shader-Vertrag + +Dieses Dokument beschreibt den verbindlichen Shader-Vertrag für Plugin-Shader. Grundlage sind PLAN.md §14.4 (Standard-Shaderinputs) und die eingebauten Beispiel-Shader des Vignette-Plugins (`plugins/builtin/filters/com.hms.fx.vignette/shaders/`). + +## Standard-Uniform-Satz (§14.4) + +Jeder Shader erhält nach Bedarf folgende semantische Inputs: + +| Semantische ID | Typ | Beschreibung | +| --- | --- | --- | +| `u_input_texture` | Textur | Eingangs-Textur (bei Filtern) | +| `u_resolution` | vec2/float4 | Auflösung in Pixeln (`xy`) | +| `u_time_seconds` | float | Zeit in Sekunden | +| `u_delta_seconds` | float | Zeit seit dem letzten Frame | +| `u_frame_index` | float | Frame-Index | +| `u_layer_opacity` | float | Opazität des Layers | +| `u_audio_rms` | float | Audio-RMS | +| `u_audio_peak` | float | Audio-Peak | +| `u_audio_bass` | float | Audio-Bass | +| `u_audio_mid` | float | Audio-Mid | +| `u_audio_treble` | float | Audio-Treble | +| `u_audio_beat` | float | Audio-Beat | +| deklarierte Pluginparameter | float | Parameter aus `plugin.json` | + +Semantische Input-IDs, Typen, Wertebereiche, Farbräume und Texturkonventionen sind Teil der versionierten Plugin-API. Backendadapter binden sie an HLSL-Konstanten beziehungsweise GLSL-Uniforms; Projekte referenzieren niemals konkrete Variablennamen eines Backends (PLAN.md §14.4). + +## HLSL (D3D11) – cbuffer-Layout + +Im HLSL-Backend liegen die Standard-Uniforms und Pluginparameter in einem Konstantenpuffer (`cbuffer`). Das Vignette-Beispiel (`shaders/d3d11/main.hlsl`) zeigt das Layout: + +```hlsl +Texture2D u_input_texture : register(t0); +SamplerState u_sampler : register(s0); + +cbuffer hms_params : register(b0) +{ + float4 u_resolution; // xy = Auflösung in Pixeln + float u_time_seconds; + float u_delta_seconds; + float u_frame_index; + float u_layer_opacity; + float u_audio_rms; + float u_audio_peak; + float u_audio_bass; + float u_audio_mid; + float u_audio_treble; + float u_audio_beat; + float param_amount; + float param_radius; + float param_softness; + float param_roundness; + float param_center_x; + float param_center_y; + float param_color_r; + float param_color_g; + float param_color_b; + float param_invert; // 0 = abdunkeln, 1 = aufhellen + float param_mix; + float _pad0; + float _pad1; + float _pad2; + float _pad3; +}; +``` + +### 16-Byte-Alignment + +HLSL-Konstantenpuffer werden in 16-Byte-Registern (`float4`) organisiert. Ein `float4` belegt ein Register; einzelne `float`-Werte werden in 16-Byte-Blöcken zusammengefasst. Das Beispiel füllt den Puffer mit `_pad0` bis `_pad3` auf ein Vielfaches von 16 Byte auf, damit das Layout deterministisch bleibt. + +### Textur- und Sampler-Bindung + +- `u_input_texture` ist an `register(t0)` gebunden. +- Der Sampler `u_sampler` ist an `register(s0)` gebunden. +- Der Konstantenpuffer `hms_params` ist an `register(b0)` gebunden. + +## GLSL (OpenGL, Linux x64) + +Das GL-Backend (`shaders/gl/main.frag`) deklariert die Uniforms einzeln: + +```glsl +#version 330 core + +uniform sampler2D u_input_texture; +uniform vec2 u_resolution; +uniform float u_time_seconds; +uniform float u_delta_seconds; +uniform float u_frame_index; +uniform float u_layer_opacity; +uniform float u_audio_rms; +uniform float u_audio_peak; +uniform float u_audio_bass; +uniform float u_audio_mid; +uniform float u_audio_treble; +uniform float u_audio_beat; +uniform float param_amount; +uniform float param_radius; +uniform float param_softness; +uniform float param_roundness; +uniform float param_center_x; +uniform float param_center_y; +uniform float param_color_r; +uniform float param_color_g; +uniform float param_color_b; +uniform float param_invert; +uniform float param_mix; + +in vec2 v_uv; +out vec4 fragColor; + +void main() +{ + vec4 src = texture(u_input_texture, v_uv); + // ... + fragColor = out_c; +} +``` + +## GLES (OpenGL ES 2.0, Raspberry Pi) + +Das GLES-Backend (`shaders/gles/main.frag`) folgt ES-2.0-Regeln: + +```glsl +#version 100 +precision mediump float; + +uniform sampler2D u_input_texture; +uniform vec2 u_resolution; +uniform float u_time_seconds; +uniform float u_delta_seconds; +uniform float u_frame_index; +uniform float u_layer_opacity; +uniform float u_audio_rms; +uniform float u_audio_peak; +uniform float u_audio_bass; +uniform float u_audio_mid; +uniform float u_audio_treble; +uniform float u_audio_beat; +uniform float param_amount; +uniform float param_radius; +uniform float param_softness; +uniform float param_roundness; +uniform float param_center_x; +uniform float param_center_y; +uniform float param_color_r; +uniform float param_color_g; +uniform float param_color_b; +uniform float param_invert; +uniform float param_mix; + +varying vec2 v_uv; + +void main() +{ + vec4 src = texture2D(u_input_texture, v_uv); + // ... + gl_FragColor = out_c; +} +``` + +## Backend-Syntaxunterschiede + +Die drei Backends müssen semantisch identische Ergebnisse liefern (PLAN.md §12.6, §15.4). Die wichtigsten Syntaxunterschiede: + +| Aspekt | HLSL (D3D11) | GLSL (OpenGL) | GLES (ES 2.0) | +| --- | --- | --- | --- | +| Version | `#version` nicht nötig | `#version 330 core` | `#version 100` | +| Texturzugriff | `u_input_texture.Sample(u_sampler, uv)` | `texture(u_input_texture, v_uv)` | `texture2D(u_input_texture, v_uv)` | +| Ausgabe | `return float4(...)` an `SV_Target` | `fragColor = ...` | `gl_FragColor = ...` | +| Eingabe | `float2 uv : TEXCOORD0` | `in vec2 v_uv` | `varying vec2 v_uv` | +| Clamp | `saturate(x)` | `clamp(x, 0.0, 1.0)` | `clamp(x, 0.0, 1.0)` | +| Mischen | `lerp(a, b, t)` | `mix(a, b, t)` | `mix(a, b, t)` | +| Präzision | – | – | `precision mediump float;` erforderlich | + +### GLES-spezifische Regeln + +- **Konstante Loop-Grenzen:** GLES 2.0 erlaubt nur Schleifen mit konstanten Grenzen. Dynamische Schleifengrenzen sind nicht zulässig. +- **`texture2D` statt `texture`:** In ES 2.0 wird `texture2D` verwendet. +- **Kein `atan2`:** Die Funktion `atan2` ist in ES 2.0 nicht verfügbar; verwende `atan(y, x)` mit zwei Argumenten oder baue die Logik selbst. +- **`precision`-Deklaration:** `precision mediump float;` muss vor den Uniforms stehen. +- **`varying` statt `in`/`out`:** ES 2.0 verwendet `varying` für Vertex-zu-Fragment-Daten. + +## Multipass-Regeln + +Multipass-Effekte (z. B. Gaussian Blur, PLAN.md §14.3) deklarieren mehrere Pässe im Manifest: + +```json +"entrypoints": { + "d3d11": { + "type": "hlsl_multipass", + "passes": [ + {"pixel_shader": "shaders/d3d11/horizontal.hlsl"}, + {"pixel_shader": "shaders/d3d11/vertical.hlsl"} + ] + } +} +``` + +- Jeder Pass ist ein eigener Shader mit eigenem Dateipfad. +- Die Pässe werden in der deklarierten Reihenfolge ausgeführt; der Ausgang eines Passes ist der Eingang des nächsten. +- Jeder Pass erhält denselben Standard-Uniform-Satz. +- Die Semantik muss über alle Backends identisch sein. + +## Adaptive-Quality-Varianten-Bindung + +Adaptive-Quality-Varianten (PLAN.md §14.3, §15.4) können die interne Auflösung (`internal_scale`) und die Sample-Anzahl (`samples`) ändern. Die Bindung erfolgt über die deklarierten Varianten im Manifest: + +```json +"adaptive_quality": { + "default": "auto", + "variants": [ + {"id": "low", "internal_scale": 0.25, "samples": 5}, + {"id": "medium", "internal_scale": 0.5, "samples": 9}, + {"id": "high", "internal_scale": 1.0, "samples": 17} + ] +} +``` + +Die Engine bindet die Variantenwerte an die Shader-Uniforms. Die Parameterwerte bleiben beim Wechsel der Auto-Qualitätsstufe semantisch identisch (PLAN.md §15.3). Details siehe [adaptive-quality.md](adaptive-quality.md). + +## Mix-0-Bypass (§15.3) + +Jeder Effekt besitzt einen `mix`-Parameter. `Mix 0` muss den Effekt kostengünstig bypassen. Das Vignette-Beispiel zeigt das Muster: + +```hlsl +float m = saturate(param_mix); +if (m < 0.01) +{ + return src; // Mix 0 = kostenloser Bypass (§15.3) +} +``` + +```glsl +float m = clamp(param_mix, 0.0, 1.0); +if (m < 0.01) +{ + fragColor = src; + return; +} +```