SkSL runtime effects
RuntimeEffect lets you write a shader in SkSL — Skia's GLSL-flavored shading language — and use it the same way you use any other Shader. This page is a working reference for the language as Skia exposes it through CanvasKit.
The pattern is always the same:
- Write SkSL source as a string.
CK.RuntimeEffect.Make(source)compiles it.effect.makeShader(uniforms)produces aShaderinstance.paint.setShader(shader)and draw.
Minimal program
const sksl = `
half4 main(float2 xy) {
return half4(xy.x / 420.0, xy.y / 256.0, 0.0, 1.0);
}
`;
const effect = CK.RuntimeEffect.Make(sksl);
const paint = new CK.Paint();
const shader = effect.makeShader([]);
paint.setShader(shader);
canvas.drawRect(CK.LTRBRect(0, 0, 420, 256), paint);
surface.flush();
shader.delete();
paint.delete();
effect.delete();
Every SkSL fragment shader has the same entry point: half4 main(float2 xy). xy is the pixel coordinate in the destination's local space — not normalized. Return the color as half4(r, g, b, a) with components in [0,1].
Types
| SkSL type | What it is | Notes |
|---|---|---|
float |
32-bit float | The default scalar. |
half |
16-bit float | Color/uniform values; preferred for the return type. |
int |
32-bit signed int | Loop counters, indices. |
bool |
boolean | true/false. |
float2, float3, float4 |
2/3/4-component float vector | Arithmetic is component-wise. |
half2, half3, half4 |
half vectors | Use in color math. |
float2x2, float3x3, float4x4 |
column-major matrices | Multiplication is mat * vec. |
Vectors swizzle like GLSL: v.xy, v.rgb, v.bgra. Construction: float3(x, y, z), half4(rgb, a) (broadcasting), float4(0) (all zero).
Built-in functions
The full GLSL standard library is available. The ones you'll reach for:
- Trig:
sin,cos,tan,asin,acos,atan,atan2. - Exp / log:
exp,log,exp2,log2,pow,sqrt,inversesqrt. - Common:
abs,sign,floor,ceil,fract,mod,min,max,clamp,mix,step,smoothstep. - Geometric:
length,distance,dot,cross,normalize,reflect,refract. - Vector relational:
lessThan,greaterThan,equal,any,all.
Operate component-wise on vectors: mix(a, b, t) works for floats and float3s.
Uniforms
Declare at the top, pass values to makeShader:
const sksl = `
uniform float u_time;
uniform float2 u_resolution;
uniform float3 u_tint;
half4 main(float2 xy) {
float2 uv = xy / u_resolution;
float ring = sin(length(uv - 0.5) * 30.0 - u_time * 4.0);
return half4(half3(u_tint) * (0.5 + 0.5 * ring), 1.0);
}
`;
const effect = CK.RuntimeEffect.Make(sksl);
const paint = new CK.Paint();
let t = 0;
loop(() => {
t += 0.016;
// Order in the array matches order in the SkSL source: u_time, u_resolution, u_tint.
const shader = effect.makeShader([t, w, h, 0.95, 0.5, 0.2]);
paint.setShader(shader);
canvas.drawRect(CK.LTRBRect(0, 0, w, h), paint);
shader.delete();
surface.flush();
});
Rules:
- The flat
number[]you pass must be the concatenated uniform values, in source order.u_time(1 float) +u_resolution(2 floats) +u_tint(3 floats) = 6 floats. effect.getUniformFloatCount()tells you the total length expected.effect.getUniformName(i)andeffect.getUniform(i)introspect at runtime — useful when a shader gets edited live.
Coordinate space
xy arrives in the local coordinate space of the geometry the paint is drawn against. If you draw a 1000×1000 rect, xy ranges over [0, 1000]. To get a 0–1 normalized UV, divide by your known resolution (passed as a uniform).
Rotations and translations applied to the canvas don't affect xy inside the shader — Skia samples in the geometry's pre-transform space. Pass a localMatrix to makeShader if you need to remap.
Child shaders
A SkSL program can sample other shaders. Declare them with uniform shader name; and call name.eval(coord):
Error: Line 1: Unexpected identifier
makeShaderWithChildren(uniforms, children) binds each uniform shader declaration to a Shader from the children array, in declaration order. Common children: image samplers, gradients, even other runtime shaders.
Control flow
if / else, for, ternary ?: all work. No recursion, no while with non-constant bounds (loops must have a static upper bound the SkSL compiler can unroll). Functions:
float sdCircle(float2 p, float r) {
return length(p) - r;
}
half4 main(float2 xy) {
float d = sdCircle(xy - float2(210.0, 128.0), 60.0);
float a = smoothstep(2.0, -2.0, d);
return half4(half3(0.2, 0.6, 0.9) * a, a);
}
Compile errors
Make(source, errorCallback) returns null on failure and calls the callback with a diagnostic string:
const effect = CK.RuntimeEffect.Make(badSource, (msg) => {
console.error('SkSL error:', msg);
});
if (!effect) {
// bail out — no shader to install
}
The error format is similar to GLSL's: error: <line>: <message>. Common ones in practice:
- Forgetting the
half4return type (usingfloat4works in many drivers but is non-portable). - Calling a function before it's declared (SkSL is single-pass; declare or forward-declare first).
- Mismatched vector dimensions in arithmetic.
What SkSL is not
- Not GLSL: no
gl_FragCoord, novarying/attribute, no preprocessor#versiondirective. - Not WGSL: different type names, different builtins.
- No textures, only child shaders. To sample an image, build an image shader (
image.makeShaderOptions(...)) and pass it as a child. - No multiple render targets, no compute.
RuntimeEffectproduces a singlehalf4per pixel.
See also
RuntimeEffect— the WASM object you compile to.Shader— whatmakeShaderreturns; how to install viapaint.setShader.Image.makeShaderOptions— wrap an image as a child shader.Paint.setBlender,RuntimeEffect.MakeForBlender— the same machinery for blend programs.