Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell. Jetzt ist sauber getrennt: ROOT (was der Nutzer sieht und braucht): - run.py = das Programm - hms_app/ = der Anwendungscode - HMS MediaEngine.app = macOS Doppelklick-Starter - HMS-Start.vbs = Windows Doppelklick-Starter - HMS-Install.vbs = Windows Erst-Installation - HMS-Mac-Install.command = macOS Homebrew-Installation - HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix) - installer_gui.py = grafischer Installer - launcher.pyw + launcher_core.py = interne Start-Logik - LIESMICH.txt = 10-Zeilen-Kurzanleitung - .gitignore _entwicklung/ (alles andere, NICHT benoetigt): - packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/ build/ fixture_profiles/ - PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md - pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann _entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne. Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
11 KiB
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:
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).
{
"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_slotsbelegen die Slots 1–8;mixbelegt keinen Slot (gemeinsamer Effektvertrag, §14.8).failure_mode: "bypass"– bei Fehler wird der Effekt überbrückt (§3.5).adaptive_qualitydeklariert 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:
// 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:
#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):
#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).
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).
Zusammenfassung
Ein Filter-Plugin besteht aus:
plugin.jsonmit Pflichtfeldern, Parametern und Adaptive Quality;- je deklariertem Backend eine Shader-Datei mit dem Standard-Uniform-Satz (§14.4);
- semantisch identischen Implementierungen über alle Backends (§12.6, §15.4).