Add runtime color filter and shader modes to the SkSL compiler

These enforce stricter rules about the signature of main, and each one
uses a separate pre-include module. That prevents color filters from
being able to reference sk_FragCoord (or coords passed to main) at all.
It also limits the versions of sample() that are exposed.

In the new world, an effect created for a specific stage of the Skia
pipeline can only be used to create instances of that stage (SkShader or
SkColorFilter). For now, SkRuntimeEffect::Make uses kRuntimeEffect,
which continues to be more lenient and allow creation of either shaders
or color filters from a single effect. After we migrate all clients, we
can deprecate and then delete that mode.

Bug: skia:11813
Change-Id: I0afd79a72beeec84da42c86146e8fcd8d0e4c09f
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/395716
Reviewed-by: John Stiles <johnstiles@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index cf06273..6d03797 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -673,6 +673,8 @@
       "src/sksl/sksl_geom.sksl",
       "src/sksl/sksl_gpu.sksl",
       "src/sksl/sksl_public.sksl",
+      "src/sksl/sksl_rt_colorfilter.sksl",
+      "src/sksl/sksl_rt_shader.sksl",
       "src/sksl/sksl_runtime.sksl",
       "src/sksl/sksl_vert.sksl",
     ]
diff --git a/bench/SkSLBench.cpp b/bench/SkSLBench.cpp
index 7892ae2..175974a 100644
--- a/bench/SkSLBench.cpp
+++ b/bench/SkSLBench.cpp
@@ -580,12 +580,13 @@
         bench("sksl_compiler_gpu", after - before);
     }
 
-    // Heap used by a compiler with the runtime effect module loaded
+    // Heap used by a compiler with the runtime shader & color filter modules loaded
     {
         int before = heap_bytes_used();
         GrShaderCaps caps(GrContextOptions{});
         SkSL::Compiler compiler(&caps);
-        compiler.moduleForProgramKind(SkSL::ProgramKind::kRuntimeEffect);
+        compiler.moduleForProgramKind(SkSL::ProgramKind::kRuntimeColorFilter);
+        compiler.moduleForProgramKind(SkSL::ProgramKind::kRuntimeShader);
         int after = heap_bytes_used();
         bench("sksl_compiler_runtimeeffect", after - before);
     }
diff --git a/gn/sksl_tests.gni b/gn/sksl_tests.gni
index 40b7c1c..13923eb 100644
--- a/gn/sksl_tests.gni
+++ b/gn/sksl_tests.gni
@@ -579,6 +579,9 @@
   "/sksl/runtime_errors/IllegalOperators.rte",
   "/sksl/runtime_errors/IllegalShaderUse.rte",
   "/sksl/runtime_errors/IllegalStatements.rte",
+  "/sksl/runtime_errors/InvalidColorFilterFeatures.rtcf",
+  "/sksl/runtime_errors/InvalidColorFilterMain.rtcf",
+  "/sksl/runtime_errors/InvalidShaderMain.rts",
   "/sksl/runtime_errors/LoopConditionErrors.rte",
   "/sksl/runtime_errors/LoopExpressionErrors.rte",
   "/sksl/runtime_errors/LoopInitializerErrors.rte",
diff --git a/include/effects/SkRuntimeEffect.h b/include/effects/SkRuntimeEffect.h
index 565dd35..f658bad 100644
--- a/include/effects/SkRuntimeEffect.h
+++ b/include/effects/SkRuntimeEffect.h
@@ -27,6 +27,7 @@
 namespace SkSL {
 class FunctionDefinition;
 struct Program;
+enum class ProgramKind : int8_t;
 }  // namespace SkSL
 
 namespace skvm {
@@ -92,11 +93,38 @@
         SkString errorText;
     };
 
-    static Result Make(SkString sksl, const Options& options);
+    // MakeForColorFilter and MakeForShader verify that the SkSL code is valid for those stages of
+    // the Skia pipeline. In all of the signatures described below, color parameters and return
+    // values are flexible. They are listed as being 'vec4', but they can also be 'half4' or
+    // 'float4'. ('vec4' is an alias for 'float4').
+
+    // Color filter SkSL requires an entry point that looks like:
+    //     vec4 main(vec4 inColor) { ... }
+    static Result MakeForColorFilter(SkString sksl, const Options&);
+
+    // Shader SkSL requires an entry point that looks like:
+    //     vec4 main(vec2 inCoords) { ... }
+    //   -or-
+    //     vec4 main(vec2 inCoords, vec4 inColor) { ... }
+    //
+    // Most shaders don't use the input color, so that parameter is optional.
+    static Result MakeForShader(SkString sksl, const Options&);
+
+    // [DEPRECATED] Make supports SkSL that is legal as either an SkShader or SkColorFilter.
+    // makeColorFilter might return nullptr, if the effect is dependent on position in any way.
+    static Result Make(SkString sksl, const Options&);
 
     // We can't use a default argument for `options` due to a bug in Clang.
     // https://bugs.llvm.org/show_bug.cgi?id=36684
-    static Result Make(SkString sksl) { return Make(std::move(sksl), Options{}); }
+    static Result MakeForColorFilter(SkString sksl) {
+        return MakeForColorFilter(std::move(sksl), Options{});
+    }
+    static Result MakeForShader(SkString sksl) {
+        return MakeForShader(std::move(sksl), Options{});
+    }
+    static Result Make(SkString sksl) {
+        return Make(std::move(sksl), Options{});
+    }
 
     sk_sp<SkShader> makeShader(sk_sp<SkData> uniforms,
                                sk_sp<SkShader> children[],
@@ -159,6 +187,12 @@
 #endif
 
 private:
+    enum Flags {
+        kUsesSampleCoords_Flag = 0x1,
+        kAllowColorFilter_Flag = 0x2,
+        kAllowShader_Flag      = 0x4,
+    };
+
     SkRuntimeEffect(SkString sksl,
                     std::unique_ptr<SkSL::Program> baseProgram,
                     const Options& options,
@@ -167,11 +201,14 @@
                     std::vector<SkString>&& children,
                     std::vector<SkSL::SampleUsage>&& sampleUsages,
                     std::vector<Varying>&& varyings,
-                    bool usesSampleCoords,
-                    bool allowColorFilter);
+                    uint32_t flags);
+
+    static Result Make(SkString sksl, const Options& options, SkSL::ProgramKind kind);
 
     uint32_t hash() const { return fHash; }
-    bool usesSampleCoords() const { return fUsesSampleCoords; }
+    bool usesSampleCoords() const { return (fFlags & kUsesSampleCoords_Flag); }
+    bool allowShader()      const { return (fFlags & kAllowShader_Flag);      }
+    bool allowColorFilter() const { return (fFlags & kAllowColorFilter_Flag); }
 
     struct FilterColorInfo {
         const skvm::Program& program;
@@ -201,8 +238,7 @@
     std::unique_ptr<skvm::Program> fColorFilterProgram;
     bool fColorFilterProgramLeavesAlphaUnchanged;
 
-    bool   fUsesSampleCoords;
-    bool   fAllowColorFilter;
+    uint32_t fFlags;  // Flags
 };
 
 /** Base class for SkRuntimeShaderBuilder, defined below. */
diff --git a/include/private/SkSLProgramKind.h b/include/private/SkSLProgramKind.h
index 0af73f4..107ab2f 100644
--- a/include/private/SkSLProgramKind.h
+++ b/include/private/SkSLProgramKind.h
@@ -20,7 +20,9 @@
     kVertex,
     kGeometry,
     kFragmentProcessor,
-    kRuntimeEffect,
+    kRuntimeEffect,       // Legacy: Generic runtime effect that can be kColorFilter or kShader
+    kRuntimeColorFilter,  // Runtime effect only suitable as SkColorFilter
+    kRuntimeShader,       //   "       "     "      "     "  SkShader
     kGeneric,
 };
 
diff --git a/resources/sksl/runtime_errors/InvalidColorFilterFeatures.rtcf b/resources/sksl/runtime_errors/InvalidColorFilterFeatures.rtcf
new file mode 100644
index 0000000..220da64
--- /dev/null
+++ b/resources/sksl/runtime_errors/InvalidColorFilterFeatures.rtcf
@@ -0,0 +1,9 @@
+// Runtime color filters may not use layout(marker), or sk_FragCoord
+
+// Expect 2 errors
+
+layout(marker=ctm) uniform float4x4 ctm;
+
+half4 main(half4 color) {
+    return sk_FragCoord.xy01;
+}
diff --git a/resources/sksl/runtime_errors/InvalidColorFilterMain.rtcf b/resources/sksl/runtime_errors/InvalidColorFilterMain.rtcf
new file mode 100644
index 0000000..a955a6f
--- /dev/null
+++ b/resources/sksl/runtime_errors/InvalidColorFilterMain.rtcf
@@ -0,0 +1,8 @@
+// Runtime color filters require specific main signatures. Test that older signatures, or those
+// intended for shaders don't work.
+
+// Expect 3 errors
+
+half4 main() { return half(1); }
+half4 main(float2 coord) { return half4(1); }
+half4 main(float2 coord, half4 color) { return color; }
\ No newline at end of file
diff --git a/resources/sksl/runtime_errors/InvalidShaderMain.rts b/resources/sksl/runtime_errors/InvalidShaderMain.rts
new file mode 100644
index 0000000..3d63a36
--- /dev/null
+++ b/resources/sksl/runtime_errors/InvalidShaderMain.rts
@@ -0,0 +1,7 @@
+// Runtime shaders require specific main signatures. Test that older signatures, or those intended
+// for color filters don't work.
+
+// Expect 2 errors
+
+half4 main() { return half4(1); }
+half4 main(half4 color) { return color; }
diff --git a/src/core/SkRuntimeEffect.cpp b/src/core/SkRuntimeEffect.cpp
index f0bcae4..50dc7ae 100644
--- a/src/core/SkRuntimeEffect.cpp
+++ b/src/core/SkRuntimeEffect.cpp
@@ -133,15 +133,16 @@
     return false;
 }
 
-SkRuntimeEffect::Result SkRuntimeEffect::Make(SkString sksl, const Options& options) {
+SkRuntimeEffect::Result SkRuntimeEffect::Make(SkString sksl,
+                                              const Options& options,
+                                              SkSL::ProgramKind kind) {
     SkSL::SharedCompiler compiler;
     SkSL::Program::Settings settings;
     settings.fInlineThreshold = 0;
     settings.fForceNoInline = options.forceNoInline;
     settings.fAllowNarrowingConversions = true;
-    auto program = compiler->convertProgram(SkSL::ProgramKind::kRuntimeEffect,
-                                            SkSL::String(sksl.c_str(), sksl.size()),
-                                            settings);
+    auto program =
+            compiler->convertProgram(kind, SkSL::String(sksl.c_str(), sksl.size()), settings);
     // TODO: Many errors aren't caught until we process the generated Program here. Catching those
     // in the IR generator would provide better errors messages (with locations).
     #define RETURN_FAILURE(...) return Result{nullptr, SkStringPrintf(__VA_ARGS__)}
@@ -151,8 +152,17 @@
     }
 
     const SkSL::FunctionDefinition* main = nullptr;
-    const bool usesSampleCoords = SkSL::Analysis::ReferencesSampleCoords(*program);
-    const bool usesFragCoords   = SkSL::Analysis::ReferencesFragCoords(*program);
+    uint32_t flags = 0;
+    switch (kind) {
+        case SkSL::ProgramKind::kRuntimeColorFilter: flags |= kAllowColorFilter_Flag; break;
+        case SkSL::ProgramKind::kRuntimeShader:      flags |= kAllowShader_Flag;      break;
+        case SkSL::ProgramKind::kRuntimeEffect:      flags |= (kAllowColorFilter_Flag |
+                                                               kAllowShader_Flag);    break;
+        default: SkUNREACHABLE;
+    }
+    if (SkSL::Analysis::ReferencesSampleCoords(*program)) {
+        flags |= kUsesSampleCoords_Flag;
+    }
 
     // Color filters are not allowed to depend on position (local or device) in any way, but they
     // can sample children with matrices or explicit coords. Because the children are color filters,
@@ -160,7 +170,13 @@
     //
     // Further down, we also ensure that color filters can't use varyings or layout(marker), which
     // would allow them to change behavior based on the CTM.
-    bool allowColorFilter = !usesSampleCoords && !usesFragCoords;
+    // TODO(skbug.com/11813): When ProgramKind is always kRuntimeColorFilter or kRuntimeShader,
+    // this can be simpler. There is no way for color filters to refer to sk_FragCoord or sample
+    // coords in that mode.
+    if ((flags & kAllowColorFilter_Flag) &&
+        (SkSL::Analysis::ReferencesFragCoords(*program) || (flags & kUsesSampleCoords_Flag))) {
+        flags &= ~kAllowColorFilter_Flag;
+    }
 
     size_t offset = 0;
     std::vector<Uniform> uniforms;
@@ -181,7 +197,7 @@
 
             // Varyings (only used in conjunction with drawVertices)
             if (var.modifiers().fFlags & SkSL::Modifiers::kVarying_Flag) {
-                allowColorFilter = false;
+                flags &= ~kAllowColorFilter_Flag;
                 varyings.push_back({var.name(),
                                     varType.typeKind() == SkSL::Type::TypeKind::kVector
                                             ? varType.columns()
@@ -213,7 +229,7 @@
                 const SkSL::StringFragment& marker(var.modifiers().fLayout.fMarker);
                 if (marker.fLength) {
                     uni.flags |= Uniform::kMarker_Flag;
-                    allowColorFilter = false;
+                    flags &= ~kAllowColorFilter_Flag;
                     if (!parse_marker(marker, &uni.marker, &uni.flags)) {
                         RETURN_FAILURE("Invalid 'marker' string: '%.*s'", (int)marker.fLength,
                                         marker.fChars);
@@ -255,11 +271,26 @@
                                                       std::move(children),
                                                       std::move(sampleUsages),
                                                       std::move(varyings),
-                                                      usesSampleCoords,
-                                                      allowColorFilter));
+                                                      flags));
     return Result{std::move(effect), SkString()};
 }
 
+SkRuntimeEffect::Result SkRuntimeEffect::Make(SkString sksl, const Options& options) {
+    return Make(std::move(sksl), options, SkSL::ProgramKind::kRuntimeEffect);
+}
+
+SkRuntimeEffect::Result SkRuntimeEffect::MakeForColorFilter(SkString sksl, const Options& options) {
+    auto result = Make(std::move(sksl), options, SkSL::ProgramKind::kRuntimeColorFilter);
+    SkASSERT(!result.effect || result.effect->allowColorFilter());
+    return result;
+}
+
+SkRuntimeEffect::Result SkRuntimeEffect::MakeForShader(SkString sksl, const Options& options) {
+    auto result = Make(std::move(sksl), options, SkSL::ProgramKind::kRuntimeShader);
+    SkASSERT(!result.effect || result.effect->allowShader());
+    return result;
+}
+
 sk_sp<SkRuntimeEffect> SkMakeCachedRuntimeEffect(SkString sksl) {
     SK_BEGIN_REQUIRE_DENSE
     struct Key {
@@ -332,8 +363,7 @@
                                  std::vector<SkString>&& children,
                                  std::vector<SkSL::SampleUsage>&& sampleUsages,
                                  std::vector<Varying>&& varyings,
-                                 bool usesSampleCoords,
-                                 bool allowColorFilter)
+                                 uint32_t flags)
         : fHash(SkGoodHash()(sksl))
         , fSkSL(std::move(sksl))
         , fBaseProgram(std::move(baseProgram))
@@ -342,8 +372,7 @@
         , fChildren(std::move(children))
         , fSampleUsages(std::move(sampleUsages))
         , fVaryings(std::move(varyings))
-        , fUsesSampleCoords(usesSampleCoords)
-        , fAllowColorFilter(allowColorFilter) {
+        , fFlags(flags) {
     SkASSERT(fBaseProgram);
     SkASSERT(fChildren.size() == fSampleUsages.size());
 
@@ -376,7 +405,7 @@
 }
 
 SkRuntimeEffect::FilterColorInfo SkRuntimeEffect::getFilterColorInfo() {
-    SkASSERT(fAllowColorFilter);
+    SkASSERT(this->allowColorFilter());
 
     fColorFilterProgramOnce([&] {
         // Runtime effects are often long lived & cached. So: build and save a program that can
@@ -853,6 +882,9 @@
                                             size_t childCount,
                                             const SkMatrix* localMatrix,
                                             bool isOpaque) const {
+    if (!this->allowShader()) {
+        return nullptr;
+    }
     if (!uniforms) {
         uniforms = SkData::MakeEmpty();
     }
@@ -948,7 +980,7 @@
 sk_sp<SkColorFilter> SkRuntimeEffect::makeColorFilter(sk_sp<SkData> uniforms,
                                                       sk_sp<SkColorFilter> children[],
                                                       size_t childCount) const {
-    if (!fAllowColorFilter) {
+    if (!this->allowColorFilter()) {
         return nullptr;
     }
     if (!uniforms) {
diff --git a/src/gpu/effects/GrSkSLFP.cpp b/src/gpu/effects/GrSkSLFP.cpp
index f03f931..a660536 100644
--- a/src/gpu/effects/GrSkSLFP.cpp
+++ b/src/gpu/effects/GrSkSLFP.cpp
@@ -174,8 +174,8 @@
                    const char* name,
                    sk_sp<SkData> uniforms)
         : INHERITED(kGrSkSLFP_ClassID,
-                    effect->fAllowColorFilter ? kConstantOutputForConstantInput_OptimizationFlag
-                                              : kNone_OptimizationFlags)
+                    effect->allowColorFilter() ? kConstantOutputForConstantInput_OptimizationFlag
+                                               : kNone_OptimizationFlags)
         , fEffect(std::move(effect))
         , fName(name)
         , fUniforms(std::move(uniforms)) {
diff --git a/src/sksl/SkSLCompiler.cpp b/src/sksl/SkSLCompiler.cpp
index d1a562b..7f64d19 100644
--- a/src/sksl/SkSLCompiler.cpp
+++ b/src/sksl/SkSLCompiler.cpp
@@ -63,6 +63,8 @@
 #include "src/sksl/generated/sksl_geom.dehydrated.sksl"
 #include "src/sksl/generated/sksl_gpu.dehydrated.sksl"
 #include "src/sksl/generated/sksl_public.dehydrated.sksl"
+#include "src/sksl/generated/sksl_rt_colorfilter.dehydrated.sksl"
+#include "src/sksl/generated/sksl_rt_shader.dehydrated.sksl"
 #include "src/sksl/generated/sksl_runtime.dehydrated.sksl"
 #include "src/sksl/generated/sksl_vert.dehydrated.sksl"
 
@@ -242,39 +244,63 @@
     return fPublicModule;
 }
 
+static void add_glsl_type_aliases(SkSL::SymbolTable* symbols, const SkSL::BuiltinTypes& types) {
+    // Add some aliases to the runtime effect modules so that it's friendlier, and more like GLSL
+    symbols->addAlias("vec2", types.fFloat2.get());
+    symbols->addAlias("vec3", types.fFloat3.get());
+    symbols->addAlias("vec4", types.fFloat4.get());
+
+    symbols->addAlias("ivec2", types.fInt2.get());
+    symbols->addAlias("ivec3", types.fInt3.get());
+    symbols->addAlias("ivec4", types.fInt4.get());
+
+    symbols->addAlias("bvec2", types.fBool2.get());
+    symbols->addAlias("bvec3", types.fBool3.get());
+    symbols->addAlias("bvec4", types.fBool4.get());
+
+    symbols->addAlias("mat2", types.fFloat2x2.get());
+    symbols->addAlias("mat3", types.fFloat3x3.get());
+    symbols->addAlias("mat4", types.fFloat4x4.get());
+}
+
 const ParsedModule& Compiler::loadRuntimeEffectModule() {
     if (!fRuntimeEffectModule.fSymbols) {
-        fRuntimeEffectModule = this->parseModule(ProgramKind::kRuntimeEffect, MODULE_DATA(runtime),
-                                                 this->loadPublicModule());
-
-        // Add some aliases to the runtime effect module so that it's friendlier, and more like GLSL
-        fRuntimeEffectModule.fSymbols->addAlias("vec2", fContext->fTypes.fFloat2.get());
-        fRuntimeEffectModule.fSymbols->addAlias("vec3", fContext->fTypes.fFloat3.get());
-        fRuntimeEffectModule.fSymbols->addAlias("vec4", fContext->fTypes.fFloat4.get());
-
-        fRuntimeEffectModule.fSymbols->addAlias("ivec2", fContext->fTypes.fInt2.get());
-        fRuntimeEffectModule.fSymbols->addAlias("ivec3", fContext->fTypes.fInt3.get());
-        fRuntimeEffectModule.fSymbols->addAlias("ivec4", fContext->fTypes.fInt4.get());
-
-        fRuntimeEffectModule.fSymbols->addAlias("bvec2", fContext->fTypes.fBool2.get());
-        fRuntimeEffectModule.fSymbols->addAlias("bvec3", fContext->fTypes.fBool3.get());
-        fRuntimeEffectModule.fSymbols->addAlias("bvec4", fContext->fTypes.fBool4.get());
-
-        fRuntimeEffectModule.fSymbols->addAlias("mat2", fContext->fTypes.fFloat2x2.get());
-        fRuntimeEffectModule.fSymbols->addAlias("mat3", fContext->fTypes.fFloat3x3.get());
-        fRuntimeEffectModule.fSymbols->addAlias("mat4", fContext->fTypes.fFloat4x4.get());
+        fRuntimeEffectModule = this->parseModule(
+                ProgramKind::kRuntimeEffect, MODULE_DATA(runtime), this->loadPublicModule());
+        add_glsl_type_aliases(fRuntimeEffectModule.fSymbols.get(), fContext->fTypes);
     }
     return fRuntimeEffectModule;
 }
 
+const ParsedModule& Compiler::loadRuntimeColorFilterModule() {
+    if (!fRuntimeColorFilterModule.fSymbols) {
+        fRuntimeColorFilterModule = this->parseModule(ProgramKind::kRuntimeColorFilter,
+                                                      MODULE_DATA(rt_colorfilter),
+                                                      this->loadPublicModule());
+        add_glsl_type_aliases(fRuntimeColorFilterModule.fSymbols.get(), fContext->fTypes);
+    }
+    return fRuntimeColorFilterModule;
+}
+
+const ParsedModule& Compiler::loadRuntimeShaderModule() {
+    if (!fRuntimeShaderModule.fSymbols) {
+        fRuntimeShaderModule = this->parseModule(
+                ProgramKind::kRuntimeShader, MODULE_DATA(rt_shader), this->loadPublicModule());
+        add_glsl_type_aliases(fRuntimeShaderModule.fSymbols.get(), fContext->fTypes);
+    }
+    return fRuntimeShaderModule;
+}
+
 const ParsedModule& Compiler::moduleForProgramKind(ProgramKind kind) {
     switch (kind) {
-        case ProgramKind::kVertex:            return this->loadVertexModule();        break;
-        case ProgramKind::kFragment:          return this->loadFragmentModule();      break;
-        case ProgramKind::kGeometry:          return this->loadGeometryModule();      break;
-        case ProgramKind::kFragmentProcessor: return this->loadFPModule();            break;
-        case ProgramKind::kRuntimeEffect:     return this->loadRuntimeEffectModule(); break;
-        case ProgramKind::kGeneric:           return this->loadPublicModule();        break;
+        case ProgramKind::kVertex:             return this->loadVertexModule();             break;
+        case ProgramKind::kFragment:           return this->loadFragmentModule();           break;
+        case ProgramKind::kGeometry:           return this->loadGeometryModule();           break;
+        case ProgramKind::kFragmentProcessor:  return this->loadFPModule();                 break;
+        case ProgramKind::kRuntimeEffect:      return this->loadRuntimeEffectModule();      break;
+        case ProgramKind::kRuntimeColorFilter: return this->loadRuntimeColorFilterModule(); break;
+        case ProgramKind::kRuntimeShader:      return this->loadRuntimeShaderModule();      break;
+        case ProgramKind::kGeneric:            return this->loadPublicModule();             break;
     }
     SkUNREACHABLE;
 }
diff --git a/src/sksl/SkSLCompiler.h b/src/sksl/SkSLCompiler.h
index c3dcbba..93f20d5 100644
--- a/src/sksl/SkSLCompiler.h
+++ b/src/sksl/SkSLCompiler.h
@@ -190,6 +190,8 @@
     const ParsedModule& loadGeometryModule();
     const ParsedModule& loadPublicModule();
     const ParsedModule& loadRuntimeEffectModule();
+    const ParsedModule& loadRuntimeColorFilterModule();
+    const ParsedModule& loadRuntimeShaderModule();
 
     /** Verifies that @if and @switch statements were actually optimized away. */
     void verifyStaticTests(const Program& program);
@@ -214,17 +216,19 @@
     std::shared_ptr<SymbolTable> fRootSymbolTable;
     std::shared_ptr<SymbolTable> fPrivateSymbolTable;
 
-    ParsedModule fRootModule;           // Core types
+    ParsedModule fRootModule;                // Core types
 
-    ParsedModule fPrivateModule;        // [Root] + Internal types
-    ParsedModule fGPUModule;            // [Private] + GPU intrinsics, helper functions
-    ParsedModule fVertexModule;         // [GPU] + Vertex stage decls
-    ParsedModule fFragmentModule;       // [GPU] + Fragment stage decls
-    ParsedModule fGeometryModule;       // [GPU] + Geometry stage decls
-    ParsedModule fFPModule;             // [GPU] + FP features
+    ParsedModule fPrivateModule;             // [Root] + Internal types
+    ParsedModule fGPUModule;                 // [Private] + GPU intrinsics, helper functions
+    ParsedModule fVertexModule;              // [GPU] + Vertex stage decls
+    ParsedModule fFragmentModule;            // [GPU] + Fragment stage decls
+    ParsedModule fGeometryModule;            // [GPU] + Geometry stage decls
+    ParsedModule fFPModule;                  // [GPU] + FP features
 
-    ParsedModule fPublicModule;         // [Root] + Public features
-    ParsedModule fRuntimeEffectModule;  // [Public] + Runtime effect decls
+    ParsedModule fPublicModule;              // [Root] + Public features
+    ParsedModule fRuntimeEffectModule;       // [Public] + Runtime effect decls
+    ParsedModule fRuntimeColorFilterModule;  // [Public] + Runtime shader decls
+    ParsedModule fRuntimeShaderModule;       // [Public] + Runtime color filter decls
 
     // holds ModifiersPools belonging to the core includes for lifetime purposes
     std::vector<std::unique_ptr<ModifiersPool>> fModifiers;
diff --git a/src/sksl/SkSLIRGenerator.cpp b/src/sksl/SkSLIRGenerator.cpp
index 149341c..645c594 100644
--- a/src/sksl/SkSLIRGenerator.cpp
+++ b/src/sksl/SkSLIRGenerator.cpp
@@ -293,7 +293,9 @@
                                         "'key' is only permitted within fragment processors");
         }
     }
-    if (this->programKind() == ProgramKind::kRuntimeEffect) {
+    if (this->programKind() == ProgramKind::kRuntimeEffect ||
+        this->programKind() == ProgramKind::kRuntimeColorFilter ||
+        this->programKind() == ProgramKind::kRuntimeShader) {
         if (modifiers.fFlags & Modifiers::kIn_Flag) {
             this->errorReporter().error(offset, "'in' variables not permitted in runtime effects");
         }
@@ -307,9 +309,9 @@
         this->errorReporter().error(offset, "'key' is not permitted on 'uniform' variables");
     }
     if (modifiers.fLayout.fMarker.fLength) {
-        if (this->programKind() != ProgramKind::kRuntimeEffect) {
-            this->errorReporter().error(offset,
-                                        "'marker' is only permitted in runtime effects");
+        if (this->programKind() != ProgramKind::kRuntimeEffect &&
+            this->programKind() != ProgramKind::kRuntimeShader) {
+            this->errorReporter().error(offset, "'marker' is only permitted in runtime shaders");
         }
         if (!(modifiers.fFlags & Modifiers::kUniform_Flag)) {
             this->errorReporter().error(offset,
@@ -321,7 +323,9 @@
         }
     }
     if (modifiers.fLayout.fFlags & Layout::kSRGBUnpremul_Flag) {
-        if (this->programKind() != ProgramKind::kRuntimeEffect) {
+        if (this->programKind() != ProgramKind::kRuntimeEffect &&
+            this->programKind() != ProgramKind::kRuntimeColorFilter &&
+            this->programKind() != ProgramKind::kRuntimeShader) {
             this->errorReporter().error(offset,
                                         "'srgb_unpremul' is only permitted in runtime effects");
         }
@@ -341,8 +345,9 @@
         }
     }
     if (modifiers.fFlags & Modifiers::kVarying_Flag) {
-        if (this->programKind() != ProgramKind::kRuntimeEffect) {
-            this->errorReporter().error(offset, "'varying' is only permitted in runtime effects");
+        if (this->programKind() != ProgramKind::kRuntimeEffect &&
+            this->programKind() != ProgramKind::kRuntimeShader) {
+            this->errorReporter().error(offset, "'varying' is only permitted in runtime shaders");
         }
         if (!baseType->isFloat() &&
             !(baseType->isVector() && baseType->componentType().isFloat())) {
@@ -1061,6 +1066,8 @@
 
         Modifiers m = pd.fModifiers;
         if (isMain && (this->programKind() == ProgramKind::kRuntimeEffect ||
+                       this->programKind() == ProgramKind::kRuntimeColorFilter ||
+                       this->programKind() == ProgramKind::kRuntimeShader ||
                        this->programKind() == ProgramKind::kFragmentProcessor)) {
             // We verify that the signature is fully correct later. For now, if this is an .fp or
             // runtime effect of any flavor, a float2 param is supposed to be the coords, and
@@ -1095,11 +1102,6 @@
         switch (this->programKind()) {
             case ProgramKind::kRuntimeEffect: {
                 // Legacy/generic runtime effects take a wide variety of main() signatures.
-                // TODO(skbug.com/11813): When we have dedicated program kinds for runtime shader
-                // vs color filter, those will only accept the suitable versions. Also, be even
-                // more restrictive: shaders must take coords, and color filters must take input
-                // color, even if unused.
-
                 // (half4|float4) main(float2?, (half4|float4)?)
                 if (!typeIsValidForColor(*returnType)) {
                     this->errorReporter().error(f.fOffset,
@@ -1119,6 +1121,37 @@
                 }
                 break;
             }
+            case ProgramKind::kRuntimeColorFilter: {
+                // (half4|float4) main(half4|float4)
+                if (!typeIsValidForColor(*returnType)) {
+                    this->errorReporter().error(f.fOffset,
+                                                "'main' must return: 'vec4', 'float4', or 'half4'");
+                    return;
+                }
+                bool validParams = (parameters.size() == 1 && paramIsInputColor(0));
+                if (!validParams) {
+                    this->errorReporter().error(
+                            f.fOffset, "'main' parameter must be 'vec4', 'float4', or 'half4'");
+                    return;
+                }
+                break;
+            }
+            case ProgramKind::kRuntimeShader: {
+                // (half4|float4) main(float2)  -or-  (half4|float4) main(float2, half4|float4)
+                if (!typeIsValidForColor(*returnType)) {
+                    this->errorReporter().error(f.fOffset,
+                                                "'main' must return: 'vec4', 'float4', or 'half4'");
+                    return;
+                }
+                bool validParams =
+                        (parameters.size() == 1 && paramIsCoords(0)) ||
+                        (parameters.size() == 2 && paramIsCoords(0) && paramIsInputColor(1));
+                if (!validParams) {
+                    this->errorReporter().error(
+                            f.fOffset, "'main' parameters must be (float2, (vec4|float4|half4)?)");
+                }
+                break;
+            }
             case ProgramKind::kFragmentProcessor: {
                 if (*returnType != *fContext.fTypes.fHalf4) {
                     this->errorReporter().error(f.fOffset, ".fp 'main' must return 'half4'");
diff --git a/src/sksl/SkSLMain.cpp b/src/sksl/SkSLMain.cpp
index d49b5b5..419a8ae 100644
--- a/src/sksl/SkSLMain.cpp
+++ b/src/sksl/SkSLMain.cpp
@@ -284,8 +284,13 @@
         kind = SkSL::ProgramKind::kFragmentProcessor;
     } else if (inputPath.endsWith(".rte")) {
         kind = SkSL::ProgramKind::kRuntimeEffect;
+    } else if (inputPath.endsWith(".rtcf")) {
+        kind = SkSL::ProgramKind::kRuntimeColorFilter;
+    } else if (inputPath.endsWith(".rts")) {
+        kind = SkSL::ProgramKind::kRuntimeShader;
     } else {
-        printf("input filename must end in '.vert', '.frag', '.geom', '.fp', '.rte', or '.sksl'\n");
+        printf("input filename must end in '.vert', '.frag', '.geom', '.fp', '.rte', '.rtcf', "
+               "'.rts', or '.sksl'\n");
         return ResultCode::kInputError;
     }
 
diff --git a/src/sksl/SkSLProgramSettings.h b/src/sksl/SkSLProgramSettings.h
index 66260a1..bc4aad5 100644
--- a/src/sksl/SkSLProgramSettings.h
+++ b/src/sksl/SkSLProgramSettings.h
@@ -74,7 +74,10 @@
     ProgramSettings fSettings;
 
     bool strictES2Mode() const {
-        return fKind == ProgramKind::kRuntimeEffect || fKind == ProgramKind::kGeneric;
+        return fKind == ProgramKind::kRuntimeEffect ||
+               fKind == ProgramKind::kRuntimeColorFilter ||
+               fKind == ProgramKind::kRuntimeShader ||
+               fKind == ProgramKind::kGeneric;
     }
 };
 
diff --git a/src/sksl/generated/sksl_rt_colorfilter.dehydrated.sksl b/src/sksl/generated/sksl_rt_colorfilter.dehydrated.sksl
new file mode 100644
index 0000000..5459f25
--- /dev/null
+++ b/src/sksl/generated/sksl_rt_colorfilter.dehydrated.sksl
@@ -0,0 +1,21 @@
+static uint8_t SKSL_INCLUDE_sksl_rt_colorfilter[] = {36,0,
+1,115,
+6,115,104,97,100,101,114,
+6,99,111,111,114,100,115,
+6,102,108,111,97,116,50,
+6,115,97,109,112,108,101,
+5,104,97,108,102,52,
+49,3,0,
+53,1,0,
+16,2,0,
+50,2,0,4,0,3,
+53,3,0,
+16,11,0,
+50,4,0,18,0,3,
+30,5,0,
+16,25,0,2,1,0,3,0,
+50,6,0,32,0,1,0,
+2,0,
+19,
+20,};
+static constexpr size_t SKSL_INCLUDE_sksl_rt_colorfilter_LENGTH = sizeof(SKSL_INCLUDE_sksl_rt_colorfilter);
diff --git a/src/sksl/generated/sksl_rt_shader.dehydrated.sksl b/src/sksl/generated/sksl_rt_shader.dehydrated.sksl
new file mode 100644
index 0000000..18a60ff
--- /dev/null
+++ b/src/sksl/generated/sksl_rt_shader.dehydrated.sksl
@@ -0,0 +1,57 @@
+static uint8_t SKSL_INCLUDE_sksl_rt_shader[] = {76,0,
+0,
+12,115,107,95,70,114,97,103,67,111,111,114,100,
+6,102,108,111,97,116,52,
+1,115,
+6,115,104,97,100,101,114,
+6,115,97,109,112,108,101,
+5,104,97,108,102,52,
+9,116,114,97,110,115,102,111,114,109,
+8,102,108,111,97,116,51,120,51,
+6,99,111,111,114,100,115,
+6,102,108,111,97,116,50,
+49,11,0,
+53,1,0,
+37,
+36,0,32,0,0,255,255,255,255,255,15,0,255,255,255,255,2,0,2,0,0,0,3,0,
+50,2,0,16,0,0,
+53,3,0,
+16,23,0,
+50,4,0,25,0,3,
+30,5,0,
+16,32,0,1,3,0,
+50,6,0,39,0,
+53,7,0,
+16,23,0,
+47,4,0,3,
+53,8,0,
+16,45,0,
+50,9,0,55,0,3,
+52,10,0,2,
+47,5,0,
+30,11,0,
+16,32,0,2,7,0,8,0,
+47,6,0,
+47,11,0,
+53,12,0,
+16,23,0,
+47,4,0,3,
+53,13,0,
+16,64,0,
+50,14,0,71,0,3,
+52,15,0,3,
+47,5,0,
+47,11,0,
+30,16,0,
+16,32,0,2,12,0,13,0,
+47,6,0,
+47,16,0,2,0,
+9,0,
+0,0,
+19,
+55,
+54,1,0,
+47,2,0,0,
+57,
+20,};
+static constexpr size_t SKSL_INCLUDE_sksl_rt_shader_LENGTH = sizeof(SKSL_INCLUDE_sksl_rt_shader);
diff --git a/src/sksl/sksl_rt_colorfilter.sksl b/src/sksl/sksl_rt_colorfilter.sksl
new file mode 100644
index 0000000..1f6183a
--- /dev/null
+++ b/src/sksl/sksl_rt_colorfilter.sksl
@@ -0,0 +1,5 @@
+half4 sample(shader s, float2 coords);
+
+// TODO(skbug.com/11813): Implement sample() that takes a color
+// half4 sample(colorFilter f, half4 color);
+// half4 sample(shader s, half4 color, float2 coords);
diff --git a/src/sksl/sksl_rt_shader.sksl b/src/sksl/sksl_rt_shader.sksl
new file mode 100644
index 0000000..b386b30
--- /dev/null
+++ b/src/sksl/sksl_rt_shader.sksl
@@ -0,0 +1,11 @@
+layout(builtin=15) float4 sk_FragCoord;
+
+half4 sample(shader s);
+half4 sample(shader s, float3x3 transform);
+half4 sample(shader s, float2 coords);
+
+// TODO(skbug.com/11813): Implement sample() that takes a color
+// half4 sample(colorFilter f, half4 color);
+// half4 sample(shader s, half4 color);
+// half4 sample(shader s, half4 color, float3x3 transform);
+// half4 sample(shader s, half4 color, float2 coords);
diff --git a/tests/sksl/errors/BadModifiers.glsl b/tests/sksl/errors/BadModifiers.glsl
index d0d57bc..39a06ec 100644
--- a/tests/sksl/errors/BadModifiers.glsl
+++ b/tests/sksl/errors/BadModifiers.glsl
@@ -16,7 +16,7 @@
 error: 4: 'inline' is not permitted here
 error: 4: 'noinline' is not permitted here
 error: 6: 'in uniform' variables only permitted within fragment processors
-error: 6: 'varying' is only permitted in runtime effects
+error: 6: 'varying' is only permitted in runtime shaders
 error: 6: 'sk_has_side_effects' is not permitted here
 error: 6: 'inline' is not permitted here
 error: 6: 'noinline' is not permitted here
diff --git a/tests/sksl/runtime_errors/InvalidColorFilterFeatures.skvm b/tests/sksl/runtime_errors/InvalidColorFilterFeatures.skvm
new file mode 100644
index 0000000..2c5d442
--- /dev/null
+++ b/tests/sksl/runtime_errors/InvalidColorFilterFeatures.skvm
@@ -0,0 +1,5 @@
+### Compilation failed:
+
+error: 5: 'marker' is only permitted in runtime shaders
+error: 8: unknown identifier 'sk_FragCoord'
+2 errors
diff --git a/tests/sksl/runtime_errors/InvalidColorFilterMain.skvm b/tests/sksl/runtime_errors/InvalidColorFilterMain.skvm
new file mode 100644
index 0000000..b691376
--- /dev/null
+++ b/tests/sksl/runtime_errors/InvalidColorFilterMain.skvm
@@ -0,0 +1,6 @@
+### Compilation failed:
+
+error: 6: 'main' parameter must be 'vec4', 'float4', or 'half4'
+error: 7: 'main' parameter must be 'vec4', 'float4', or 'half4'
+error: 8: 'main' parameter must be 'vec4', 'float4', or 'half4'
+3 errors
diff --git a/tests/sksl/runtime_errors/InvalidShaderMain.skvm b/tests/sksl/runtime_errors/InvalidShaderMain.skvm
new file mode 100644
index 0000000..7ebeed8
--- /dev/null
+++ b/tests/sksl/runtime_errors/InvalidShaderMain.skvm
@@ -0,0 +1,5 @@
+### Compilation failed:
+
+error: 6: 'main' parameters must be (float2, (vec4|float4|half4)?)
+error: 7: 'main' parameters must be (float2, (vec4|float4|half4)?)
+2 errors
diff --git a/tools/viewer/SkSLSlide.cpp b/tools/viewer/SkSLSlide.cpp
index 8fa3bfc..9d06fce 100644
--- a/tools/viewer/SkSLSlide.cpp
+++ b/tools/viewer/SkSLSlide.cpp
@@ -96,7 +96,7 @@
         fwrite(fSkSL.c_str(), 1, fSkSL.size(), backup);
         fclose(backup);
     }
-    auto [effect, errorText] = SkRuntimeEffect::Make(sksl);
+    auto [effect, errorText] = SkRuntimeEffect::MakeForShader(sksl);
     if (backup) {
         std::remove(kBackupFile);
     }