Files
hms-mediaengine/_entwicklung/docs/plugin-sdk/example-walkthrough.md
T

329 lines
11 KiB
Markdown
Raw Normal View History

# 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 18; `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).