> ## Documentation Index
> Fetch the complete documentation index at: https://docs.framelane.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Runtime shaders

> Write a shader in SkSL, compile it once, and run it in any element's effect chain, on the render farm and in the preview alike.

A runtime shader is your own pixel effect: an SkSL function the API compiles into the
three variants the render engines run (GLSL 330 core on the farm, GLSL ES 3.00 in the
WebGL build, WGSL in the Rust preview engine). You post the source once, get back an id,
and use that id in the `effects` of a video, image, shape, frame or instance.

## The loop

1. `POST /v1/shaders` with the source and its params. The answer is `201` with the
   shader's `id`, or `422 shader_compile_error` with every error's line and column.
2. `POST /v1/shaders/{id}/probe` renders one frame of it on a full-canvas shape (a PNG
   preview, not billed). Poll the job with `GET /v1/renders/{id}`.
3. Put it in a composition: `effects: [{"shader": {"id": "sh_...", "params": {...}}}]`.
4. To change the source, post it again: a new source is a new shader with a new id.

## Compile a shader

```json POST /v1/shaders theme={null}
{
  "name": "tinted glow",
  "source": "half4 main(float2 uv) {\n  half4 c = sampleSource(uv);\n  return half4(mix(c.rgb, u_tint.rgb * c.a, u_amount), c.a);\n}\n",
  "params": [
    { "name": "tint", "type": "color", "default": "#FF8800FF" },
    { "name": "amount", "type": "float", "default": 0.5, "min": 0, "max": 1 }
  ]
}
```

```json 201 theme={null}
{
  "id": "sh_01J8QR2K5VKDGN2T4FBM3CZYX7",
  "name": "tinted glow",
  "lang": "sksl",
  "hash": "623b1272a931ef77d258b8c284a01a70e8a9066c316824ed019fef87d2c63604",
  "jitter": false,
  "status": "compiled",
  "errors": [],
  "params": [
    { "name": "tint", "type": "color", "default": "#FF8800FF" },
    { "name": "amount", "type": "float", "default": 0.5, "min": 0.0, "max": 1.0 }
  ],
  "variant_hashes": { "glsl330": "<64 hex>", "glsles300": "<64 hex>", "wgsl": "<64 hex>" },
  "shared": false,
  "source": null,
  "variants": null,
  "created_at": "2026-10-16T12:00:00Z",
  "updated_at": "2026-10-16T12:00:00Z"
}
```

The entry is `half4 main(float2 uv)`, with uv from `(0, 0)` at the top-left of the
element to `(1, 1)` at its bottom-right. A source that defines Jitter's
`half4 jitterMain(float2 uv)` compiles in Jitter's dialect (`jitter: true`); pass
`"jitter": true` to require it. Do not declare `uniform`s: everything a source reads comes
from the prelude below or from its `params`, each read as `u_<name>`.

`hash` is the SHA-256 of `"sksl\n"` plus the source, the shader's identity. The variants'
text comes back with `GET /v1/shaders/{id}?view=full`, beside the source.

### Compile errors

```json 422 theme={null}
{
  "error": {
    "code": "shader_compile_error",
    "message": "The shader did not compile: 3:1: expected ';', but found '}'",
    "details": {
      "shader_id": "sh_01J8QR2K9P4N6Y0B2C8D1E3F5G",
      "hash": "405d3257eeb6ccdced3f08b4cb5b3d220c294829d8a507e180b14b1c0962d479",
      "errors": [{ "line": 3, "col": 1, "message": "expected ';', but found '}'" }]
    }
  }
}
```

`line` and `col` are 1-based positions in your source. Line `0` marks an error outside
it: a param (`param 'time': u_time is a prelude name`) or a limit. A name the prelude
does not have is reported where it is first used (`unknown prelude symbol 'u_amount'`).
The failed shader is stored too, so `GET /v1/shaders/{id}` keeps its errors; an effect
that names it is refused with `shader_compile_error`.

## Use it in an effect

```json theme={null}
{
  "type": "shape",
  "id": "backdrop",
  "duration": 4,
  "path": "M0 0 H100 V100 H0 Z",
  "fill": "none",
  "effects": [
    {
      "shader": {
        "id": "sh_01J8QR2K5VKDGN2T4FBM3CZYX7",
        "params": {
          "amount": [{ "time": 0, "value": 0 }, { "time": 2, "value": 1 }],
          "tint": "#33CCFF"
        }
      },
      "intensity": 100
    }
  ]
}
```

An effect takes either a vocabulary `type` or a `shader`, and beside a `shader` only
`intensity` (static or `intensity_keyframes`). A shape with `fill: "none"` and no stroke
is a generator: the shader paints the whole box from nothing. On any other element the
shader reads the element's own pixels through `sampleSource`.

| Param type | Value |
| - | - |
| `float` | a number |
| `color` | `"#RRGGBBAA"` (straight alpha), or `"#RRGGBB"`, which is opaque |
| `vec2` | `[x, y]` |

A param's value is one of these or keyframes, `[{time, value}]` in timeline seconds,
linear between and held outside. A param left out takes its declared `default`; a value
outside the declared `min` and `max` is refused, as are params the shader does not
declare (`shader_param_invalid`). The render carries the compiled variants, so it never
depends on the shader still existing once it is queued.

## Limits

| Limit | Value |
| - | - |
| Source size | 65,536 bytes (`SKSL_SOURCE_MAX_BYTES`) |
| Params per shader | 32, in at most 1,024 bytes of uniforms (float 4, vec2 8, color 16, after 40 of prelude) |
| Loop iterations one pixel runs | 100,000, nested loops multiplied; `while` and `do` are refused |
| Shader effects | 8 per element, 64 per render |
| GPU time | 8 ms per 1920 x 1080 pixels shaded per frame |
| Compile time | 20 s (`SKSL_COMPILE_TIMEOUT_SECONDS`) |

## The prelude

| Uniform | Meaning |
| - | - |
| `u_time`, `u_time_ms` | Timeline time in seconds, and in milliseconds |
| `u_op_time` | Seconds since the effect's first keyframe (else the element's start) |
| `u_resolution` | The source texture in px |
| `u_intensity` | The effect's intensity, 0 to 1 |
| `u_element_size` | The element box in px (design px under a `design_size`) |
| `u_source_size` | The source in px |
| `u_content_uv_min`, `u_content_uv_max` | The content box in uv |

Functions: `sampleSource(float2 uv)` (the source, premultiplied), `uvToLocal` and
`localToUv` (uv into and out of the content box), `transformUv(uv, aspect, pivot,
translate, scale, angleRad)`, `simplexNoise(float2 p, float2 res, float scale)` (in -1
to 1) and Skia's `unpremul`. Jitter's dialect adds `jitterSampleSource`,
`jitterUvToLocal`, `jitterLocalToUv`, `jitterTransformUv`, `jitterSimplexPerlinNoise`,
`u_width`/`u_height` and `u_source_width`/`u_source_height` as aliases of the same.
`GET /v1/capabilities` serves all of this as the `shaders` section.

## Shared shaders

`GET /v1/shaders` lists this workspace's shaders and the shared ones (`shared: true`),
newest first. A shared shader works in any workspace's effects and cannot be deleted
(`403`). Deleting your own shader leaves renders already queued with it untouched; a
document that still names it is refused with `shader_not_found` at its next render.

## Availability

The compiler runs beside the API. A deployment without it answers `POST /v1/shaders` with
`503 shader_compiler_unavailable`, and `shaders.compiler_available` in
`GET /v1/capabilities` is `false`. Rendering a shader needs the v5 render task: while a
deployment does not send it (`shaders.renders` is `false`), an effect with a `shader` is
refused with `unsupported_feature`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.