# Shader Package Contract This document explains how to create shaders for the Video Shader runtime. Each shader is a small package under `shaders//`: ```text shaders/my-effect/ shader.json shader.slang optional-texture.png ``` The runtime reads `shader.json`, generates a Slang wrapper from `runtime/templates/shader_wrapper.slang.in`, includes your `shader.slang`, compiles the result to GLSL, and exposes the shader in the local control UI. ## Quick Start Create a folder: ```text shaders/my-effect/ ``` Add `shader.json`: ```json { "id": "my-effect", "name": "My Effect", "description": "A simple starter shader.", "category": "Custom", "entryPoint": "shadeVideo", "parameters": [ { "id": "strength", "label": "Strength", "type": "float", "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01 } ] } ``` Add `shader.slang`: ```slang float4 shadeVideo(ShaderContext context) { float4 color = context.sourceColor; color.rgb = lerp(color.rgb, 1.0 - color.rgb, strength); return saturate(color); } ``` With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically. ## Manifest Fields `shader.json` is the runtime-facing description of the shader. Required fields: - `id`: package ID used by state/presets. Hyphenated names are OK here, for example `my-effect`. - `name`: display name in the UI. - `parameters`: array of exposed controls. Use `[]` if there are no user parameters. Optional fields: - `description`: display/help text for the shader library. - `category`: UI grouping label. - `entryPoint`: Slang function to call. Defaults to `shadeVideo`. - `textures`: texture assets to load and expose as samplers. - `temporal`: history-buffer requirements. Shader-visible identifiers must be valid Slang-style identifiers: - `entryPoint` - parameter `id` - texture `id` Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID. ## Slang Entry Point Your shader file must implement the manifest `entryPoint`. Default: ```slang float4 shadeVideo(ShaderContext context) { return context.sourceColor; } ``` The runtime owns the real fragment shader entry point. Your function is called from the wrapper, and the runtime handles final bypass/mix behavior: ```slang return lerp(context.sourceColor, effectedColor, mixValue); ``` That means: - Return the fully effected color from your function. - Respect alpha if your shader produces an overlay or sprite. - The runtime will blend your result with the source according to `mixAmount` and bypass state. ## ShaderContext Your entry point receives: ```slang struct ShaderContext { float2 uv; float4 sourceColor; float2 inputResolution; float2 outputResolution; float time; float frameCount; float mixAmount; float bypass; int sourceHistoryLength; int temporalHistoryLength; float2 audioRms; float2 audioPeak; float audioMonoRms; float audioMonoPeak; float4 audioBands; }; ``` Fields: - `uv`: normalized texture coordinates, usually `0..1`. - `sourceColor`: decoded RGBA source video at `uv`. - `inputResolution`: decoded input video resolution in pixels. - `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode. - `time`: elapsed runtime time in seconds. - `frameCount`: incrementing frame counter. - `mixAmount`: runtime mix amount. - `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`. - `sourceHistoryLength`: number of usable source-history frames currently available. - `temporalHistoryLength`: number of usable temporal frames currently available for this layer. - `audioRms`: left/right RMS level for the audio block synchronized with the rendered output frame. - `audioPeak`: left/right peak level for the same synchronized audio block. - `audioMonoRms`: mono RMS level derived from left/right. - `audioMonoPeak`: mono peak level derived from left/right. - `audioBands`: four smoothed, normalized low-to-high frequency bands. ## Helper Functions The wrapper provides: ```slang float4 sampleVideo(float2 uv); float4 sampleSourceHistory(int framesAgo, float2 uv); float4 sampleTemporalHistory(int framesAgo, float2 uv); float4 sampleAudioWaveform(float x); float4 sampleAudioSpectrum(float x); ``` `sampleVideo` samples the live decoded source video. `sampleSourceHistory` samples previous decoded source frames. `framesAgo` is clamped into the available range. If no history is available, it falls back to `sampleVideo`. `sampleTemporalHistory` samples previous pre-layer input frames for temporal shaders that request `preLayerInput` history. `framesAgo` is clamped into the available range. If no temporal history is available, it falls back to `sampleVideo`. `sampleAudioWaveform` samples the current synchronized audio waveform texture. `x` is normalized `0..1`; returned waveform channels are encoded from `-1..1` into `0..1`. `sampleAudioSpectrum` samples the current synchronized audio spectrum texture. Values are normalized `0..1`. Example: ```slang float4 shadeVideo(ShaderContext context) { float4 previous = sampleSourceHistory(1, context.uv); return lerp(context.sourceColor, previous, 0.35); } ``` ## Parameters Manifest parameters are exposed to Slang as global values with the same `id`. Supported types: | Manifest type | Slang type | JSON value | | --- | --- | --- | | `float` | `float` | number | | `vec2` | `float2` | `[x, y]` | | `color` | `float4` | `[r, g, b, a]` | | `bool` | `bool` | `true` or `false` | | `enum` | `int` | selected option index | Float example: ```json { "id": "brightness", "label": "Brightness", "type": "float", "default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01 } ``` ```slang color.rgb *= brightness; ``` Vector example: ```json { "id": "offset", "label": "Offset", "type": "vec2", "default": [0.0, 0.0], "min": [-0.2, -0.2], "max": [0.2, 0.2], "step": [0.001, 0.001] } ``` ```slang float2 uv = clamp(context.uv + offset, float2(0.0), float2(1.0)); ``` Color example: ```json { "id": "tint", "label": "Tint", "type": "color", "default": [1.0, 1.0, 1.0, 1.0] } ``` ```slang color *= tint; ``` Boolean example: ```json { "id": "invert", "label": "Invert", "type": "bool", "default": false } ``` ```slang if (invert) color.rgb = 1.0 - color.rgb; ``` Enum example: ```json { "id": "mode", "label": "Mode", "type": "enum", "default": "normal", "options": [ { "value": "normal", "label": "Normal" }, { "value": "luma", "label": "Luma" }, { "value": "posterize", "label": "Posterize" } ] } ``` Enums are stored in presets/state by their string `value`, but exposed to Slang as a zero-based integer index in option order: ```slang if (mode == 1) { float luma = dot(color.rgb, float3(0.2126, 0.7152, 0.0722)); color.rgb = float3(luma); } else if (mode == 2) { color.rgb = floor(color.rgb * 4.0) / 4.0; } ``` Parameter validation: - Float values are clamped to `min`/`max` if provided. - `vec2` must have exactly 2 numbers. - `color` must have exactly 4 numbers. - Enum defaults must match one of the declared option values. - Non-finite numeric values are rejected. ## Texture Assets Declare texture assets in the manifest: ```json { "textures": [ { "id": "logoTexture", "path": "logo.png" } ] } ``` Rules: - `id` must be a valid shader identifier. - `path` is relative to the shader package directory. - The file must exist when the manifest is loaded. - Texture asset changes trigger shader reload. Texture IDs become `Sampler2D` globals: ```slang float4 logo = logoTexture.Sample(logoUv); ``` For sprite or overlay shaders, return premultiplied-looking output if you want clean composition: ```slang float alpha = logo.a; return float4(logo.rgb * alpha, alpha); ``` See `shaders/dvd-bounce/` for a complete texture-driven example. ## Temporal Shaders Temporal shaders can request access to previous frames. Manifest example: ```json { "temporal": { "enabled": true, "historySource": "preLayerInput", "historyLength": 12 } } ``` Supported `historySource` values: - `source`: decoded source-video history from previous frames. - `preLayerInput`: history of the input arriving at this layer before the shader runs. `historyLength` is the requested frame count. The runtime clamps it by `maxTemporalHistoryFrames` in `config/runtime-host.json`. Temporal history resets when: - layers are added, removed, or reordered - a layer bypass state changes - a layer changes shader - a shader is reloaded or recompiled - render dimensions change Use the available history lengths to avoid assuming history is ready on the first frame: ```slang float4 shadeVideo(ShaderContext context) { if (context.temporalHistoryLength <= 0) return context.sourceColor; float4 oldFrame = sampleTemporalHistory(3, context.uv); return lerp(context.sourceColor, oldFrame, 0.4); } ``` See `shaders/temporal-ghost-trail/` and `shaders/temporal-low-fps/` for examples. ## Coordinate And Color Notes - `uv` is normalized. - Use `context.outputResolution` for pixel-sized effects. - Use `context.inputResolution` when sampling source video by input pixel size. - `sourceColor` and `sampleVideo` return RGBA values in normalized `0..1` range. - Prefer `saturate(color)` or explicit `clamp` before returning if your math can overshoot. Pixel-size example: ```slang float2 pixel = 1.0 / max(context.outputResolution, float2(1.0)); float4 right = sampleVideo(context.uv + float2(pixel.x, 0.0)); ``` ## Reload And Generated Files When a shader compiles, the runtime writes generated files under `runtime/shader_cache/`: - `active_shader_wrapper.slang` - `active_shader.raw.frag` - `active_shader.frag` These files are ignored by git and are useful for debugging compiler output. If a shader fails to compile, inspect the wrapper first; it shows the exact generated Slang code including your included shader. ## Common Pitfalls - Do not use hyphens in parameter IDs, texture IDs, or entry point names. - Do not declare your own `ShaderContext`, `GlobalParams`, `sampleVideo`, `sampleSourceHistory`, or `sampleTemporalHistory`. - Do not write a `[shader("fragment")]` entry point in `shader.slang`; the runtime provides it. - Remember enum globals are integer indexes, not strings. - Declare every texture in `shader.json`; undeclared texture samplers will not be bound. - Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config. - If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect. - If a Slang name collides with a generated global, rename your parameter or local symbol. ## Minimal Package Checklist Before committing a new shader package: - `shader.json` is valid JSON. - `id` is unique across `shaders/`. - `entryPoint`, parameter IDs, and texture IDs are valid identifiers. - `shader.slang` implements the configured entry point. - Texture files referenced by `textures` exist. - Enum defaults are present in their `options`. - Temporal shaders handle short or empty history gracefully. - The app can reload and compile the shader without errors.