Add SkBlender class; thread into SkVMBlitter.

Like SkColorFilter, SkShader, etc., this has a public-facing component
(SkBlender) and a private subclass (SkBlenderBase) which can be
obtained via a helper function (as_BB). At present there are no public-
facing methods, but the type needs to be exposed to be usable by the
outside world.

These classes exist for SkRuntimeEffect to subclass. The blender base
provides a `program` method with the parameters that blending will use.

Change-Id: I75c772fd4108a9c21fbda84201a8b23d3750a0df
Bug: skia:12080
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/416916
Auto-Submit: John Stiles <johnstiles@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
diff --git a/bench/SkSLBench.cpp b/bench/SkSLBench.cpp
index 154853c..75beb50 100644
--- a/bench/SkSLBench.cpp
+++ b/bench/SkSLBench.cpp
@@ -586,7 +586,7 @@
         SkSL::Compiler compiler(&caps);
         compiler.moduleForProgramKind(SkSL::ProgramKind::kRuntimeColorFilter);
         compiler.moduleForProgramKind(SkSL::ProgramKind::kRuntimeShader);
-        compiler.moduleForProgramKind(SkSL::ProgramKind::kRuntimeBlendFilter);
+        compiler.moduleForProgramKind(SkSL::ProgramKind::kRuntimeBlender);
         int after = heap_bytes_used();
         bench("sksl_compiler_runtimeeffect", after - before);
     }
diff --git a/gn/core.gni b/gn/core.gni
index 1342d5d..e333487 100644
--- a/gn/core.gni
+++ b/gn/core.gni
@@ -11,6 +11,7 @@
   "$_include/core/SkAnnotation.h",
   "$_include/core/SkBBHFactory.h",
   "$_include/core/SkBitmap.h",
+  "$_include/core/SkBlender.h",
   "$_include/core/SkBlendMode.h",
   "$_include/core/SkBlurTypes.h",
   "$_include/core/SkCanvas.h",
@@ -125,6 +126,7 @@
   "$_src/core/SkBitmapProcState.h",
   "$_src/core/SkBitmapProcState_matrixProcs.cpp",
   "$_src/core/SkBlendMode.cpp",
+  "$_src/core/SkBlenderBase.h",
   "$_src/core/SkBlitBWMaskTemplate.h",
   "$_src/core/SkBlitRow.h",
   "$_src/core/SkBlitRow_D32.cpp",
@@ -163,6 +165,7 @@
   "$_src/core/SkCubicClipper.cpp",
   "$_src/core/SkCubicClipper.h",
   "$_src/core/SkCubicMap.cpp",
+  "$_src/core/SkCubicSolver.h",
   "$_src/core/SkData.cpp",
   "$_src/core/SkDataTable.cpp",
   "$_src/core/SkDebug.cpp",
diff --git a/include/core/SkBlender.h b/include/core/SkBlender.h
new file mode 100644
index 0000000..4e007e3
--- /dev/null
+++ b/include/core/SkBlender.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkBlender_DEFINED
+#define SkBlender_DEFINED
+
+#include "include/core/SkFlattenable.h"
+
+/**
+ * SkBlender represents a custom blend function in the Skia pipeline. When an SkBlender is
+ * present in a paint, the SkBlendMode is ignored. A blender combines a source color (the
+ * result of our paint) and destination color (from the canvas) into a final color.
+ */
+class SK_API SkBlender : public SkFlattenable {
+private:
+    SkBlender() = default;
+    friend class SkBlenderBase;
+
+    using INHERITED = SkFlattenable;
+};
+
+#endif
diff --git a/include/core/SkFlattenable.h b/include/core/SkFlattenable.h
index b105f6d..9de6e7f 100644
--- a/include/core/SkFlattenable.h
+++ b/include/core/SkFlattenable.h
@@ -27,8 +27,9 @@
 public:
     enum Type {
         kSkColorFilter_Type,
+        kSkBlender_Type,
         kSkDrawable_Type,
-        kSkDrawLooper_Type, // no longer used internally by Skia
+        kSkDrawLooper_Type,  // no longer used internally by Skia
         kSkImageFilter_Type,
         kSkMaskFilter_Type,
         kSkPathEffect_Type,
diff --git a/include/effects/SkRuntimeEffect.h b/include/effects/SkRuntimeEffect.h
index 32d0665..c5204a7 100644
--- a/include/effects/SkRuntimeEffect.h
+++ b/include/effects/SkRuntimeEffect.h
@@ -204,7 +204,7 @@
         kUsesSampleCoords_Flag = 0x1,
         kAllowColorFilter_Flag = 0x2,
         kAllowShader_Flag      = 0x4,
-        kAllowBlendFilter_Flag = 0x8,
+        kAllowBlender_Flag     = 0x8,
     };
 
     SkRuntimeEffect(SkString sksl,
@@ -227,7 +227,7 @@
     bool usesSampleCoords() const { return (fFlags & kUsesSampleCoords_Flag); }
     bool allowShader()      const { return (fFlags & kAllowShader_Flag);      }
     bool allowColorFilter() const { return (fFlags & kAllowColorFilter_Flag); }
-    bool allowBlendFilter() const { return (fFlags & kAllowBlendFilter_Flag); }
+    bool allowBlender()     const { return (fFlags & kAllowBlender_Flag);     }
 
     const SkFilterColorProgram* getFilterColorProgram();
 
diff --git a/include/private/SkSLProgramKind.h b/include/private/SkSLProgramKind.h
index e8cc587..5b381d7 100644
--- a/include/private/SkSLProgramKind.h
+++ b/include/private/SkSLProgramKind.h
@@ -22,7 +22,7 @@
     kFragmentProcessor,
     kRuntimeColorFilter,  // Runtime effect only suitable as SkColorFilter
     kRuntimeShader,       //   "       "     "      "     "  SkShader
-    kRuntimeBlendFilter,  //   "       "     "      "     "  SkBlendFilter
+    kRuntimeBlender,      //   "       "     "      "     "  SkBlender
     kGeneric,
 };
 
diff --git a/src/core/SkBlenderBase.h b/src/core/SkBlenderBase.h
new file mode 100644
index 0000000..37a3abc
--- /dev/null
+++ b/src/core/SkBlenderBase.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkBlenderBase_DEFINED
+#define SkBlenderBase_DEFINED
+
+#include "include/core/SkBlender.h"
+#include "include/core/SkColorSpace.h"
+#include "src/core/SkArenaAlloc.h"
+#include "src/core/SkVM.h"
+
+/**
+ * Encapsulates a custom blend function for Runtime Effects. These combine a source color (the
+ * result of our paint) and destination color (from the canvas) into a final color.
+ */
+class SkBlenderBase : public SkBlender {
+public:
+    SK_WARN_UNUSED_RESULT
+    skvm::Color program(skvm::Builder* p, skvm::Color src, skvm::Color dst,
+                        const SkColorInfo& colorInfo, skvm::Uniforms* uniforms,
+                        SkArenaAlloc* alloc) const {
+        return this->onProgram(p, src, dst, colorInfo, uniforms, alloc);
+    }
+
+    static SkFlattenable::Type GetFlattenableType() { return kSkBlender_Type; }
+    Type getFlattenableType() const override { return GetFlattenableType(); }
+
+private:
+    virtual skvm::Color onProgram(skvm::Builder* p, skvm::Color src, skvm::Color dst,
+                                  const SkColorInfo& colorInfo, skvm::Uniforms* uniforms,
+                                  SkArenaAlloc* alloc) const = 0;
+
+    using INHERITED = SkFlattenable;
+};
+
+inline SkBlenderBase* as_BB(SkBlender* blend) {
+    return static_cast<SkBlenderBase*>(blend);
+}
+
+inline const SkBlenderBase* as_BB(const SkBlender* blend) {
+    return static_cast<const SkBlenderBase*>(blend);
+}
+
+inline const SkBlenderBase* as_BB(const sk_sp<SkBlender>& blend) {
+    return static_cast<SkBlenderBase*>(blend.get());
+}
+
+#endif  // SkBlenderBase_DEFINED
diff --git a/src/core/SkRuntimeEffect.cpp b/src/core/SkRuntimeEffect.cpp
index 6b33208..f365183 100644
--- a/src/core/SkRuntimeEffect.cpp
+++ b/src/core/SkRuntimeEffect.cpp
@@ -187,7 +187,7 @@
     switch (kind) {
         case SkSL::ProgramKind::kRuntimeColorFilter: flags |= kAllowColorFilter_Flag; break;
         case SkSL::ProgramKind::kRuntimeShader:      flags |= kAllowShader_Flag;      break;
-        case SkSL::ProgramKind::kRuntimeBlendFilter: flags |= kAllowBlendFilter_Flag; break;
+        case SkSL::ProgramKind::kRuntimeBlender:     flags |= kAllowBlender_Flag;     break;
         default: SkUNREACHABLE;
     }
 
diff --git a/src/core/SkVMBlitter.cpp b/src/core/SkVMBlitter.cpp
index c7e5968..7a94cd3 100644
--- a/src/core/SkVMBlitter.cpp
+++ b/src/core/SkVMBlitter.cpp
@@ -9,6 +9,7 @@
 #include "include/private/SkMacros.h"
 #include "src/core/SkArenaAlloc.h"
 #include "src/core/SkBlendModePriv.h"
+#include "src/core/SkBlenderBase.h"
 #include "src/core/SkColorFilterBase.h"
 #include "src/core/SkColorSpacePriv.h"
 #include "src/core/SkColorSpaceXformSteps.h"
@@ -38,6 +39,7 @@
     struct Params {
         sk_sp<SkShader>         shader;
         sk_sp<SkShader>         clip;
+        sk_sp<SkBlender>        blender;
         SkColorInfo             dst;
         SkBlendMode             blendMode;
         Coverage                coverage;
@@ -55,6 +57,7 @@
     struct Key {
         uint64_t shader,
                  clip,
+                 blender,
                  colorSpace;
         uint8_t  colorType,
                  alphaType,
@@ -66,13 +69,14 @@
         // they'll be folded into the shader key if used.
 
         bool operator==(const Key& that) const {
-            return this->shader     == that.shader
-                && this->clip       == that.clip
-                && this->colorSpace == that.colorSpace
-                && this->colorType  == that.colorType
-                && this->alphaType  == that.alphaType
-                && this->blendMode  == that.blendMode
-                && this->coverage   == that.coverage;
+            return this->shader      == that.shader
+                && this->clip        == that.clip
+                && this->blender     == that.blender
+                && this->colorSpace  == that.colorSpace
+                && this->colorType   == that.colorType
+                && this->alphaType   == that.alphaType
+                && this->blendMode   == that.blendMode
+                && this->coverage    == that.coverage;
         }
 
         Key withCoverage(Coverage c) const {
@@ -84,15 +88,16 @@
     SK_END_REQUIRE_DENSE;
 
     static SkString debug_name(const Key& key) {
-        return SkStringPrintf(
-            "Shader-%" PRIx64 "_Clip-%" PRIx64 "_CS-%" PRIx64 "_CT-%d_AT-%d_Blend-%d_Cov-%d",
-            key.shader,
-            key.clip,
-            key.colorSpace,
-            key.colorType,
-            key.alphaType,
-            key.blendMode,
-            key.coverage);
+        return SkStringPrintf("Shader-%" PRIx64 "_Clip-%" PRIx64 "_Blender-%" PRIx64
+                              "_CS-%" PRIx64 "_CT-%d_AT-%d_Blend-%d_Cov-%d",
+                              key.shader,
+                              key.clip,
+                              key.blender,
+                              key.colorSpace,
+                              key.colorType,
+                              key.alphaType,
+                              key.blendMode,
+                              key.coverage);
     }
 
     static SkLRUCache<Key, skvm::Program>* try_acquire_program_cache() {
@@ -119,6 +124,12 @@
         };
     }
 
+    static skvm::Color dst_color(skvm::Builder* p, const Params& params) {
+        skvm::PixelFormat dstFormat = skvm::SkColorType_to_PixelFormat(params.dst.colorType());
+        skvm::Ptr dst_ptr = p->arg(SkColorTypeBytesPerPixel(params.dst.colorType()));
+        return p->load(dstFormat, dst_ptr);
+    }
+
     // If build_program() can't build this program, cache_key() sets *ok to false.
     static Key cache_key(const Params& params,
                          skvm::Uniforms* uniforms, SkArenaAlloc* alloc, bool* ok) {
@@ -127,7 +138,8 @@
                       g = uniforms->pushF(params.paint.fG),
                       b = uniforms->pushF(params.paint.fB),
                       a = uniforms->pushF(params.paint.fA);
-        auto hash_shader = [&](const sk_sp<SkShader>& shader) {
+
+        auto hash_shader = [&](const sk_sp<SkShader>& shader, skvm::Color* outColor) {
             const SkShaderBase* sb = as_SB(shader);
             skvm::Builder p;
 
@@ -140,16 +152,20 @@
             };
 
             uint64_t hash = 0;
-            if (auto c = sb->program(&p,
-                                     device,/*local=*/device, paint,
-                                     params.matrices, /*localM=*/nullptr,
-                                     params.dst, uniforms,alloc)) {
+            *outColor = sb->program(&p, device, /*local=*/device, paint, params.matrices,
+                                    /*localM=*/nullptr, params.dst, uniforms, alloc);
+            if (*outColor) {
                 hash = p.hash();
                 // p.hash() folds in all instructions to produce r,g,b,a but does not know
                 // precisely which value we'll treat as which channel.  Imagine the shader
                 // called std::swap(*r,*b)... it draws differently, but p.hash() is unchanged.
                 // We'll fold the hash of their IDs in order to disambiguate.
-                const skvm::Val outputs[] = { c.r.id, c.g.id, c.b.id, c.a.id };
+                const skvm::Val outputs[] = {
+                    outColor->r.id,
+                    outColor->g.id,
+                    outColor->b.id,
+                    outColor->a.id
+                };
                 hash ^= SkOpts::hash(outputs, sizeof(outputs));
             } else {
                 *ok = false;
@@ -157,20 +173,52 @@
             return hash;
         };
 
+        // Calculate a hash for the color shader.
         SkASSERT(params.shader);
-        uint64_t shaderHash = hash_shader(params.shader);
+        skvm::Color src;
+        uint64_t shaderHash = hash_shader(params.shader, &src);
 
+        // Calculate a hash for the clip shader, if one exists.
         uint64_t clipHash = 0;
+        skvm::Color cov;
         if (params.clip) {
-            clipHash = hash_shader(params.clip);
+            clipHash = hash_shader(params.clip, &cov);
             if (clipHash == 0) {
                 clipHash = 1;
             }
         }
 
+        // Calculate a hash for the blend shader, if one exists.
+        uint64_t blendHash = 0;
+        if (params.blender) {
+            const SkBlenderBase* blender = as_BB(params.blender);
+            skvm::Builder p;
+
+            skvm::Color dst = dst_color(&p, params);
+            skvm::Color outColor = blender->program(&p, src, dst, params.dst, uniforms, alloc);
+            if (outColor) {
+                blendHash = p.hash();
+                // Like in `hash_shader` above, we must fold the color component IDs into our hash.
+                const skvm::Val outputs[] = {
+                    outColor.r.id,
+                    outColor.g.id,
+                    outColor.b.id,
+                    outColor.a.id
+                };
+                blendHash ^= SkOpts::hash(outputs, sizeof(outputs));
+            } else {
+                *ok = false;
+            }
+
+            if (blendHash == 0) {
+                blendHash = 1;
+            }
+        }
+
         return {
             shaderHash,
               clipHash,
+             blendHash,
             params.dst.colorSpace() ? params.dst.colorSpace()->hash() : 0,
             SkToU8(params.dst.colorType()),
             SkToU8(params.dst.alphaType()),
@@ -196,7 +244,7 @@
         skvm::Color paint = p->uniformColor(params.paint, uniforms);
 
         // See note about arguments above: a SpriteShader will call p->arg() once during program().
-        skvm::Color src = as_SB(params.shader)->program(p, device,/*local=*/device, paint,
+        skvm::Color src = as_SB(params.shader)->program(p, device, /*local=*/device, paint,
                                                         params.matrices, /*localM=*/nullptr,
                                                         params.dst, uniforms, alloc);
         SkASSERT(src);
@@ -266,7 +314,7 @@
             } break;
         }
         if (params.clip) {
-            skvm::Color clip = as_SB(params.clip)->program(p, device,/*local=*/device, paint,
+            skvm::Color clip = as_SB(params.clip)->program(p, device, /*local=*/device, paint,
                                                            params.matrices, /*localM=*/nullptr,
                                                            params.dst, uniforms, alloc);
             SkAssertResult(clip);
@@ -276,19 +324,25 @@
             cov.a *= clip.a;
         }
 
-        // The math for some blend modes lets us fold coverage into src before the blend,
-        // which is simpler than the canonical post-blend lerp().
-        if (SkBlendMode_ShouldPreScaleCoverage(params.blendMode,
+        // The math for some blend modes lets us fold coverage into src before the blend, which is
+        // simpler than the canonical post-blend lerp().
+        bool applyPostBlendCoverage = true;
+        if (!params.blender &&
+            SkBlendMode_ShouldPreScaleCoverage(params.blendMode,
                                                params.coverage == Coverage::MaskLCD16)) {
+            applyPostBlendCoverage = false;
             src.r *= cov.r;
             src.g *= cov.g;
             src.b *= cov.b;
             src.a *= cov.a;
+        }
 
-            src = blend(params.blendMode, src, dst);
-        } else {
-            src = blend(params.blendMode, src, dst);
+        // Apply our blend function to the computed color.
+        src = params.blender
+                      ? as_BB(params.blender)->program(p, src, dst, params.dst, uniforms, alloc)
+                      : blend(params.blendMode, src, dst);
 
+        if (applyPostBlendCoverage) {
             src.r = lerp(dst.r, src.r, cov.r);
             src.g = lerp(dst.g, src.g, cov.g);
             src.b = lerp(dst.b, src.b, cov.b);
@@ -528,6 +582,7 @@
         return {
             std::move(shader),
             std::move(clip),
+            /*blender=*/nullptr,
             { device.colorType(), device.alphaType(), device.refColorSpace() },
             blendMode,
             Coverage::Full,  // Placeholder... withCoverage() will change as needed.
diff --git a/src/sksl/SkSLCompiler.cpp b/src/sksl/SkSLCompiler.cpp
index e653aad..dabebc2 100644
--- a/src/sksl/SkSLCompiler.cpp
+++ b/src/sksl/SkSLCompiler.cpp
@@ -298,13 +298,13 @@
     return fRuntimeShaderModule;
 }
 
-const ParsedModule& Compiler::loadRuntimeBlendFilterModule() {
-    if (!fRuntimeBlendFilterModule.fSymbols) {
-        fRuntimeBlendFilterModule = this->parseModule(
-                ProgramKind::kRuntimeBlendFilter, MODULE_DATA(rt_blend), this->loadPublicModule());
-        add_glsl_type_aliases(fRuntimeBlendFilterModule.fSymbols.get(), fContext->fTypes);
+const ParsedModule& Compiler::loadRuntimeBlenderModule() {
+    if (!fRuntimeBlenderModule.fSymbols) {
+        fRuntimeBlenderModule = this->parseModule(
+                ProgramKind::kRuntimeBlender, MODULE_DATA(rt_blend), this->loadPublicModule());
+        add_glsl_type_aliases(fRuntimeBlenderModule.fSymbols.get(), fContext->fTypes);
     }
-    return fRuntimeBlendFilterModule;
+    return fRuntimeBlenderModule;
 }
 
 const ParsedModule& Compiler::moduleForProgramKind(ProgramKind kind) {
@@ -315,7 +315,7 @@
         case ProgramKind::kFragmentProcessor:  return this->loadFPModule();                 break;
         case ProgramKind::kRuntimeColorFilter: return this->loadRuntimeColorFilterModule(); break;
         case ProgramKind::kRuntimeShader:      return this->loadRuntimeShaderModule();      break;
-        case ProgramKind::kRuntimeBlendFilter: return this->loadRuntimeBlendFilterModule(); break;
+        case ProgramKind::kRuntimeBlender:     return this->loadRuntimeBlenderModule();     break;
         case ProgramKind::kGeneric:            return this->loadPublicModule();             break;
     }
     SkUNREACHABLE;
diff --git a/src/sksl/SkSLCompiler.h b/src/sksl/SkSLCompiler.h
index 543f5ba..decf660 100644
--- a/src/sksl/SkSLCompiler.h
+++ b/src/sksl/SkSLCompiler.h
@@ -208,7 +208,7 @@
     const ParsedModule& loadPublicModule();
     const ParsedModule& loadRuntimeColorFilterModule();
     const ParsedModule& loadRuntimeShaderModule();
-    const ParsedModule& loadRuntimeBlendFilterModule();
+    const ParsedModule& loadRuntimeBlenderModule();
 
     /** Verifies that @if and @switch statements were actually optimized away. */
     void verifyStaticTests(const Program& program);
@@ -248,7 +248,7 @@
     ParsedModule fPublicModule;              // [Root] + Public features
     ParsedModule fRuntimeColorFilterModule;  // [Public] + Runtime shader decls
     ParsedModule fRuntimeShaderModule;       // [Public] + Runtime color filter decls
-    ParsedModule fRuntimeBlendFilterModule;  // [Public] + Runtime blend filter decls
+    ParsedModule fRuntimeBlenderModule;      // [Public] + Runtime blender decls
 
     // holds ModifiersPools belonging to the core includes for lifetime purposes
     ModifiersPool fCoreModifiers;
diff --git a/src/sksl/SkSLMain.cpp b/src/sksl/SkSLMain.cpp
index 1dce022..258ac17 100644
--- a/src/sksl/SkSLMain.cpp
+++ b/src/sksl/SkSLMain.cpp
@@ -292,7 +292,7 @@
     } else if (inputPath.ends_with(".fp")) {
         kind = SkSL::ProgramKind::kFragmentProcessor;
     } else if (inputPath.ends_with(".rtb")) {
-        kind = SkSL::ProgramKind::kRuntimeBlendFilter;
+        kind = SkSL::ProgramKind::kRuntimeBlender;
     } else if (inputPath.ends_with(".rtcf")) {
         kind = SkSL::ProgramKind::kRuntimeColorFilter;
     } else if (inputPath.ends_with(".rts")) {
diff --git a/src/sksl/dsl/DSLFunction.cpp b/src/sksl/dsl/DSLFunction.cpp
index cd8181d..1be891d 100644
--- a/src/sksl/dsl/DSLFunction.cpp
+++ b/src/sksl/dsl/DSLFunction.cpp
@@ -46,7 +46,7 @@
         SkSL::ProgramKind kind = DSLWriter::Context().fConfig->fKind;
         if (isMain && (kind == ProgramKind::kRuntimeColorFilter ||
                        kind == ProgramKind::kRuntimeShader ||
-                       kind == ProgramKind::kRuntimeBlendFilter ||
+                       kind == ProgramKind::kRuntimeBlender ||
                        kind == ProgramKind::kFragmentProcessor)) {
             const SkSL::Type& type = param->fType.skslType();
             // We verify that the signature is fully correct later. For now, if this is an .fp
diff --git a/src/sksl/ir/SkSLFunctionDeclaration.cpp b/src/sksl/ir/SkSLFunctionDeclaration.cpp
index 68b0559..29f8b54 100644
--- a/src/sksl/ir/SkSLFunctionDeclaration.cpp
+++ b/src/sksl/ir/SkSLFunctionDeclaration.cpp
@@ -92,7 +92,7 @@
         ProgramKind kind = context.fConfig->fKind;
         if (isMain && (kind == ProgramKind::kRuntimeColorFilter ||
                        kind == ProgramKind::kRuntimeShader ||
-                       kind == ProgramKind::kRuntimeBlendFilter ||
+                       kind == ProgramKind::kRuntimeBlender ||
                        kind == 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
@@ -178,7 +178,7 @@
             }
             break;
         }
-        case ProgramKind::kRuntimeBlendFilter: {
+        case ProgramKind::kRuntimeBlender: {
             // (half4|float4) main(half4|float4, half4|float4)
             if (!typeIsValidForColor(returnType)) {
                 errors.error(offset, "'main' must return: 'vec4', 'float4', or 'half4'");