Allow sampling from SkBlenders.

Runtime shaders, color filters, and blenders are all able to sample from
a blender. These use the blend-function signature; both a src-color and
dst-color must be passed to sample. i.e.: sample(blender, s, d)

Change-Id: I3738e6b0b4af6d1d79e62ca1815c80d6a1ae9d6f
Bug: skia:12257
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/432056
Commit-Queue: John Stiles <johnstiles@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
This commit is contained in:
John Stiles 2021-07-30 11:20:19 -04:00 committed by SkCQ
parent 1a2e7de8ea
commit ce9a5c953d
13 changed files with 238 additions and 59 deletions

View File

@ -82,6 +82,7 @@ public:
enum class Type {
kShader,
kColorFilter,
kBlender,
};
SkString name;
@ -161,13 +162,15 @@ public:
static Result MakeForBlender(std::unique_ptr<SkSL::Program> program, const Options&);
static Result MakeForBlender(std::unique_ptr<SkSL::Program> program);
// Object that allows passing either an SkShader or SkColorFilter as a child
// Object that allows passing a SkShader, SkColorFilter or SkBlender as a child
struct ChildPtr {
ChildPtr() = default;
ChildPtr(sk_sp<SkShader> s) : shader(std::move(s)) {}
ChildPtr(sk_sp<SkColorFilter> cf) : colorFilter(std::move(cf)) {}
ChildPtr(sk_sp<SkBlender> b) : blender(std::move(b)) {}
sk_sp<SkShader> shader;
sk_sp<SkColorFilter> colorFilter;
sk_sp<SkBlender> blender;
};
sk_sp<SkShader> makeShader(sk_sp<SkData> uniforms,

View File

@ -130,7 +130,7 @@ static bool init_uniform_type(const SkSL::Context& ctx,
static SkRuntimeEffect::Child::Type child_type(const SkSL::Type& type) {
switch (type.typeKind()) {
// TODO(skia:12257): add support for kBlender
case SkSL::Type::TypeKind::kBlender: return SkRuntimeEffect::Child::Type::kBlender;
case SkSL::Type::TypeKind::kColorFilter: return SkRuntimeEffect::Child::Type::kColorFilter;
case SkSL::Type::TypeKind::kShader: return SkRuntimeEffect::Child::Type::kShader;
default: SkUNREACHABLE;
@ -147,12 +147,17 @@ static bool verify_child_effects(const std::vector<SkRuntimeEffect::Child>& refl
for (size_t i = 0; i < effectPtrs.size(); ++i) {
switch (reflected[i].type) {
case SkRuntimeEffect::Child::Type::kShader:
if (effectPtrs[i].colorFilter) {
if (effectPtrs[i].colorFilter || effectPtrs[i].blender) {
return false;
}
continue;
case SkRuntimeEffect::Child::Type::kColorFilter:
if (effectPtrs[i].shader) {
if (effectPtrs[i].shader || effectPtrs[i].blender) {
return false;
}
continue;
case SkRuntimeEffect::Child::Type::kBlender:
if (effectPtrs[i].shader || effectPtrs[i].colorFilter) {
return false;
}
continue;
@ -178,6 +183,8 @@ static bool read_child_effects(SkReadBuffer& buffer,
children->emplace_back(buffer.readShader());
} else if (child.type == SkRuntimeEffect::Child::Type::kColorFilter) {
children->emplace_back(buffer.readColorFilter());
} else if (child.type == SkRuntimeEffect::Child::Type::kBlender) {
children->emplace_back(buffer.readBlender());
} else {
return false;
}
@ -190,8 +197,10 @@ static void write_child_effects(SkWriteBuffer& buffer,
const std::vector<SkRuntimeEffect::ChildPtr>& children) {
buffer.write32(children.size());
for (const auto& child : children) {
buffer.writeFlattenable(child.shader ? (const SkFlattenable*)child.shader.get()
: child.colorFilter.get());
buffer.writeFlattenable(
child.shader ? (const SkFlattenable*)child.shader.get() :
child.colorFilter ? (const SkFlattenable*)child.colorFilter.get() :
(const SkFlattenable*)child.blender.get());
}
}
@ -569,9 +578,10 @@ std::unique_ptr<SkFilterColorProgram> SkFilterColorProgram::Make(const SkRuntime
// of those, we are unable to use this per-effect program, and callers will need to fall back
// to another (slower) implementation.
// We also require that any children are *also* color filters (not shaders). In theory we could
// detect the coords being passed to shader children, and replicate those calls, but that's
// very complicated, and has diminishing returns. (eg, for table lookup color filters).
// We also require that any children are *also* color filters (not shaders or blenders). In
// theory we could detect the coords being passed to shader children, and replicate those calls,
// but that's very complicated, and has diminishing returns. (eg, for table lookup color
// filters).
if (!std::all_of(effect->fChildren.begin(),
effect->fChildren.end(),
[](const SkRuntimeEffect::Child& c) {
@ -777,12 +787,14 @@ static GrFPResult make_effect_fp(sk_sp<SkRuntimeEffect> effect,
SkSTArray<8, std::unique_ptr<GrFragmentProcessor>> childFPs;
for (const auto& child : children) {
if (child.shader) {
// Convert a SkShader into a child FP.
auto childFP = as_SB(child.shader)->asFragmentProcessor(childArgs);
if (!childFP) {
return GrFPFailure(std::move(inputFP));
}
childFPs.push_back(std::move(childFP));
} else if (child.colorFilter) {
// Convert a SkColorFilter into a child FP.
auto [success, childFP] = as_CFB(child.colorFilter)
->asFragmentProcessor(/*inputFP=*/nullptr,
childArgs.fContext,
@ -791,7 +803,17 @@ static GrFPResult make_effect_fp(sk_sp<SkRuntimeEffect> effect,
return GrFPFailure(std::move(inputFP));
}
childFPs.push_back(std::move(childFP));
} else if (child.blender) {
// Convert a SkBlender into a child FP.
auto childFP = as_BB(child.blender)->asFragmentProcessor(/*srcFP=*/nullptr,
/*dstFP=*/nullptr,
childArgs);
if (!childFP) {
return GrFPFailure(std::move(inputFP));
}
childFPs.push_back(std::move(childFP));
} else {
// We have a null child effect.
childFPs.push_back(nullptr);
}
}
@ -860,8 +882,10 @@ public:
return color;
};
auto sampleBlender = [&](int ix, skvm::Color src, skvm::Color dst) {
// TODO(skia:12257): implement sample(blender, src, dst)
return src;
if (SkBlender* blender = fChildren[ix].blender.get()) {
return as_BB(blender)->program(p, src, dst, colorInfo, uniforms, alloc);
}
return blend(SkBlendMode::kSrcOver, src, dst);
};
std::vector<skvm::Val> uniform = make_skvm_uniforms(p, uniforms, fEffect->uniformSize(),
@ -891,8 +915,9 @@ public:
auto evalChild = [&](int index, SkPMColor4f inColor) {
const auto& child = fChildren[index];
// Guaranteed by initFilterColorInfo
// SkFilterColorProgram::Make has guaranteed that any children will be color filters.
SkASSERT(!child.shader);
SkASSERT(!child.blender);
return child.colorFilter ? as_CFB(child.colorFilter)->onFilterColor4f(inColor, dstCS)
: inColor;
};
@ -1036,8 +1061,10 @@ public:
return color;
};
auto sampleBlender = [&](int ix, skvm::Color src, skvm::Color dst) {
// TODO(skia:12257): implement sample(blender, src, dst)
return src;
if (SkBlender* blender = fChildren[ix].blender.get()) {
return as_BB(blender)->program(p, src, dst, colorInfo, uniforms, alloc);
}
return blend(SkBlendMode::kSrcOver, src, dst);
};
std::vector<skvm::Val> uniform = make_skvm_uniforms(p, uniforms, fEffect->uniformSize(),
@ -1144,8 +1171,10 @@ public:
return color;
};
auto sampleBlender = [&](int ix, skvm::Color src, skvm::Color dst) {
// TODO(skia:12257): implement sample(blender, src, dst)
return src;
if (SkBlender* blender = fChildren[ix].blender.get()) {
return as_BB(blender)->program(p, src, dst, colorInfo, uniforms, alloc);
}
return blend(SkBlendMode::kSrcOver, src, dst);
};
std::vector<skvm::Val> uniform = make_skvm_uniforms(p, uniforms, fEffect->uniformSize(),

View File

@ -146,8 +146,10 @@ public:
}
String sampleBlender(int index, String src, String dst) override {
// TODO(skia:12257): invokeChild does not yet allow sampling from a blender
return "half4(1)";
if (!fSelf->childProcessor(index)) {
return String::printf("blend_src_over(%s, %s)", src.c_str(), dst.c_str());
}
return String(fSelf->invokeChild(index, src.c_str(), dst.c_str(), fArgs).c_str());
}
GrGLSLSkSLFP* fSelf;

View File

@ -180,6 +180,6 @@ BuiltinTypes::BuiltinTypes()
, fSkCaps(Type::MakeSpecialType("$sk_Caps", "O", Type::TypeKind::kOther))
, fColorFilter(Type::MakeSpecialType("colorFilter", "CF", Type::TypeKind::kColorFilter))
, fShader(Type::MakeSpecialType("shader", "SH", Type::TypeKind::kShader))
, fBlender(Type::MakeSpecialType("$blender", "B", Type::TypeKind::kBlender)) {}
, fBlender(Type::MakeSpecialType("blender", "B", Type::TypeKind::kBlender)) {}
} // namespace SkSL

View File

@ -141,7 +141,7 @@ void PipelineStageCodeGenerator::writeFunctionCall(const FunctionCall& c) {
const FunctionDeclaration& function = c.function();
const ExpressionArray& arguments = c.arguments();
if (function.isBuiltin() && function.name() == "sample") {
SkASSERT(arguments.size() == 2);
SkASSERT(arguments.size() >= 2);
const Expression* child = arguments[0].get();
SkASSERT(child->type().isEffectChild());
SkASSERT(child->is<VariableReference>());
@ -164,20 +164,44 @@ void PipelineStageCodeGenerator::writeFunctionCall(const FunctionCall& c) {
SkASSERT(found);
// Shaders require a coordinate argument. Color filters require a color argument.
// When we call sampleChild, the other value remains empty.
// Blenders require two color arguments.
String sampleOutput;
{
AutoOutputBuffer outputToBuffer(this);
this->writeExpression(*arguments.back(), Precedence::kSequence);
if (child->type().typeKind() == Type::TypeKind::kShader) {
SkASSERT(arguments[1]->type() == *fProgram.fContext->fTypes.fFloat2);
sampleOutput = fCallbacks->sampleShader(index, outputToBuffer.fBuffer.str());
AutoOutputBuffer exprBuffer(this);
this->writeExpression(*arguments[1], Precedence::kSequence);
} else {
SkASSERT(child->type().typeKind() == Type::TypeKind::kColorFilter);
SkASSERT(arguments[1]->type() == *fProgram.fContext->fTypes.fHalf4 ||
arguments[1]->type() == *fProgram.fContext->fTypes.fFloat4);
sampleOutput = fCallbacks->sampleColorFilter(index, outputToBuffer.fBuffer.str());
switch (child->type().typeKind()) {
case Type::TypeKind::kShader: {
SkASSERT(arguments.size() == 2);
SkASSERT(arguments[1]->type() == *fProgram.fContext->fTypes.fFloat2);
sampleOutput = fCallbacks->sampleShader(index, exprBuffer.fBuffer.str());
break;
}
case Type::TypeKind::kColorFilter: {
SkASSERT(arguments.size() == 2);
SkASSERT(arguments[1]->type() == *fProgram.fContext->fTypes.fHalf4 ||
arguments[1]->type() == *fProgram.fContext->fTypes.fFloat4);
sampleOutput = fCallbacks->sampleColorFilter(index, exprBuffer.fBuffer.str());
break;
}
case Type::TypeKind::kBlender: {
SkASSERT(arguments.size() == 3);
SkASSERT(arguments[1]->type() == *fProgram.fContext->fTypes.fHalf4 ||
arguments[1]->type() == *fProgram.fContext->fTypes.fFloat4);
SkASSERT(arguments[2]->type() == *fProgram.fContext->fTypes.fHalf4 ||
arguments[2]->type() == *fProgram.fContext->fTypes.fFloat4);
AutoOutputBuffer exprBuffer2(this);
this->writeExpression(*arguments[2], Precedence::kSequence);
sampleOutput = fCallbacks->sampleBlender(index, exprBuffer.fBuffer.str(),
exprBuffer2.fBuffer.str());
break;
}
default: {
SkDEBUGFAILF("cannot sample from type '%s'",
child->type().description().c_str());
}
}
}
this->write(sampleOutput);

View File

@ -873,7 +873,7 @@ Value SkVMGenerator::writeIntrinsicCall(const FunctionCall& c) {
if (intrinsicKind == k_sample_IntrinsicKind) {
// Sample is very special. The first argument is a child (shader/colorFilter/blender),
// which is opaque and can't be evaluated.
SkASSERT(nargs == 2);
SkASSERT(nargs >= 2);
const Expression* child = c.arguments()[0].get();
SkASSERT(child->type().isEffectChild());
SkASSERT(child->is<VariableReference>());
@ -887,16 +887,43 @@ Value SkVMGenerator::writeIntrinsicCall(const FunctionCall& c) {
Value argVal = this->writeExpression(*arg);
skvm::Color color;
if (child->type().typeKind() == Type::TypeKind::kShader) {
SkASSERT(arg->type() == *fProgram.fContext->fTypes.fFloat2);
skvm::Coord coord = {f32(argVal[0]), f32(argVal[1])};
color = fSampleShader(fp_it->second, coord);
} else {
SkASSERT(child->type().typeKind() == Type::TypeKind::kColorFilter);
SkASSERT(arg->type() == *fProgram.fContext->fTypes.fHalf4 ||
arg->type() == *fProgram.fContext->fTypes.fFloat4);
skvm::Color inColor = {f32(argVal[0]), f32(argVal[1]), f32(argVal[2]), f32(argVal[3])};
color = fSampleColorFilter(fp_it->second, inColor);
switch (child->type().typeKind()) {
case Type::TypeKind::kShader: {
SkASSERT(nargs == 2);
SkASSERT(arg->type() == *fProgram.fContext->fTypes.fFloat2);
skvm::Coord coord = {f32(argVal[0]), f32(argVal[1])};
color = fSampleShader(fp_it->second, coord);
break;
}
case Type::TypeKind::kColorFilter: {
SkASSERT(nargs == 2);
SkASSERT(arg->type() == *fProgram.fContext->fTypes.fHalf4 ||
arg->type() == *fProgram.fContext->fTypes.fFloat4);
skvm::Color inColor = {f32(argVal[0]), f32(argVal[1]),
f32(argVal[2]), f32(argVal[3])};
color = fSampleColorFilter(fp_it->second, inColor);
break;
}
case Type::TypeKind::kBlender: {
SkASSERT(nargs == 3);
SkASSERT(arg->type() == *fProgram.fContext->fTypes.fHalf4 ||
arg->type() == *fProgram.fContext->fTypes.fFloat4);
skvm::Color srcColor = {f32(argVal[0]), f32(argVal[1]),
f32(argVal[2]), f32(argVal[3])};
arg = c.arguments()[2].get();
argVal = this->writeExpression(*arg);
SkASSERT(arg->type() == *fProgram.fContext->fTypes.fHalf4 ||
arg->type() == *fProgram.fContext->fTypes.fFloat4);
skvm::Color dstColor = {f32(argVal[0]), f32(argVal[1]),
f32(argVal[2]), f32(argVal[3])};
color = fSampleBlender(fp_it->second, srcColor, dstColor);
break;
}
default: {
SkDEBUGFAILF("cannot sample from type '%s'", child->type().description().c_str());
}
}
Value result(4);

View File

@ -1,4 +1,4 @@
static uint8_t SKSL_INCLUDE_sksl_rt_blend[] = {56,0,
static uint8_t SKSL_INCLUDE_sksl_rt_blend[] = {74,0,
1,115,
6,115,104,97,100,101,114,
6,99,111,111,114,100,115,
@ -8,7 +8,11 @@ static uint8_t SKSL_INCLUDE_sksl_rt_blend[] = {56,0,
1,102,
11,99,111,108,111,114,70,105,108,116,101,114,
5,99,111,108,111,114,
47,7,0,
1,98,
7,98,108,101,110,100,101,114,
3,115,114,99,
3,100,115,116,
47,12,0,
51,1,0,
16,2,0,
48,2,0,4,0,3,
@ -29,8 +33,24 @@ static uint8_t SKSL_INCLUDE_sksl_rt_blend[] = {56,0,
28,11,0,
16,25,0,2,7,0,9,0,
45,6,0,
45,11,0,1,0,
5,0,
45,11,0,
51,12,0,
16,58,0,
48,13,0,60,0,3,
51,14,0,
16,68,0,
45,6,0,3,
51,15,0,
16,72,0,
45,6,0,3,
50,16,0,3,
45,5,0,
45,11,0,
28,17,0,
16,25,0,3,12,0,14,0,15,0,
45,6,0,
45,17,0,1,0,
10,0,
19,
20,};
static constexpr size_t SKSL_INCLUDE_sksl_rt_blend_LENGTH = sizeof(SKSL_INCLUDE_sksl_rt_blend);

View File

@ -1,4 +1,4 @@
static uint8_t SKSL_INCLUDE_sksl_rt_colorfilter[] = {56,0,
static uint8_t SKSL_INCLUDE_sksl_rt_colorfilter[] = {74,0,
1,115,
6,115,104,97,100,101,114,
6,99,111,111,114,100,115,
@ -8,7 +8,11 @@ static uint8_t SKSL_INCLUDE_sksl_rt_colorfilter[] = {56,0,
1,102,
11,99,111,108,111,114,70,105,108,116,101,114,
5,99,111,108,111,114,
47,7,0,
1,98,
7,98,108,101,110,100,101,114,
3,115,114,99,
3,100,115,116,
47,12,0,
51,1,0,
16,2,0,
48,2,0,4,0,3,
@ -29,8 +33,24 @@ static uint8_t SKSL_INCLUDE_sksl_rt_colorfilter[] = {56,0,
28,11,0,
16,25,0,2,7,0,9,0,
45,6,0,
45,11,0,1,0,
5,0,
45,11,0,
51,12,0,
16,58,0,
48,13,0,60,0,3,
51,14,0,
16,68,0,
45,6,0,3,
51,15,0,
16,72,0,
45,6,0,3,
50,16,0,3,
45,5,0,
45,11,0,
28,17,0,
16,25,0,3,12,0,14,0,15,0,
45,6,0,
45,17,0,1,0,
10,0,
19,
20,};
static constexpr size_t SKSL_INCLUDE_sksl_rt_colorfilter_LENGTH = sizeof(SKSL_INCLUDE_sksl_rt_colorfilter);

View File

@ -1,4 +1,4 @@
static uint8_t SKSL_INCLUDE_sksl_rt_shader[] = {76,0,
static uint8_t SKSL_INCLUDE_sksl_rt_shader[] = {94,0,
12,115,107,95,70,114,97,103,67,111,111,114,100,
6,102,108,111,97,116,52,
1,115,
@ -10,7 +10,11 @@ static uint8_t SKSL_INCLUDE_sksl_rt_shader[] = {76,0,
1,102,
11,99,111,108,111,114,70,105,108,116,101,114,
5,99,111,108,111,114,
47,8,0,
1,98,
7,98,108,101,110,100,101,114,
3,115,114,99,
3,100,115,116,
47,13,0,
51,1,0,
35,
34,0,2,0,0,255,255,255,255,255,15,0,255,255,255,255,0,2,0,
@ -35,8 +39,24 @@ static uint8_t SKSL_INCLUDE_sksl_rt_shader[] = {76,0,
28,13,0,
16,45,0,2,9,0,11,0,
45,8,0,
45,13,0,2,0,
6,0,
45,13,0,
51,14,0,
16,78,0,
48,15,0,80,0,3,
51,16,0,
16,88,0,
45,8,0,3,
51,17,0,
16,92,0,
45,8,0,3,
50,18,0,3,
45,7,0,
45,13,0,
28,19,0,
16,45,0,3,14,0,16,0,17,0,
45,8,0,
45,19,0,2,0,
11,0,
0,0,
19,
53,

View File

@ -1,2 +1,3 @@
half4 sample(shader s, float2 coords);
half4 sample(colorFilter f, half4 color);
half4 sample(blender b, half4 src, half4 dst);

View File

@ -1,2 +1,3 @@
half4 sample(shader s, float2 coords);
half4 sample(colorFilter f, half4 color);
half4 sample(blender b, half4 src, half4 dst);

View File

@ -2,3 +2,4 @@ layout(builtin=15) float4 sk_FragCoord;
half4 sample(shader s, float2 coords);
half4 sample(colorFilter f, half4 color);
half4 sample(blender b, half4 src, half4 dst);

View File

@ -12,6 +12,7 @@
#include "include/core/SkData.h"
#include "include/core/SkPaint.h"
#include "include/core/SkSurface.h"
#include "include/effects/SkBlenders.h"
#include "include/effects/SkRuntimeEffect.h"
#include "include/gpu/GrDirectContext.h"
#include "src/core/SkColorSpacePriv.h"
@ -618,10 +619,10 @@ static void test_RuntimeEffect_Blenders(skiatest::Reporter* r, GrRecordingContex
effect.test(0xFF888888);
// Fill the destination with a variety of colors (using the RGBW shader)
SkPaint paint;
paint.setShader(make_RGBW_shader());
paint.setBlendMode(SkBlendMode::kSrc);
surface->getCanvas()->drawPaint(paint);
SkPaint rgbwPaint;
rgbwPaint.setShader(make_RGBW_shader());
rgbwPaint.setBlendMode(SkBlendMode::kSrc);
surface->getCanvas()->drawPaint(rgbwPaint);
// Verify that we can read back the dest color exactly as-is (ignoring the source color)
// This is equivalent to the kDst blend mode.
@ -643,13 +644,27 @@ static void test_RuntimeEffect_Blenders(skiatest::Reporter* r, GrRecordingContex
// Sampling children
//
// Sampling a null child should return the paint color
// Sampling a null shader/color filter should return the paint color.
effect.build("uniform shader child;"
"half4 main(half4 s, half4 d) { return sample(child, s.rg); }");
effect.child("child") = nullptr;
effect.test(0xFF00FFFF,
[](SkCanvas*, SkPaint* paint) { paint->setColor4f({1.0f, 1.0f, 0.0f, 1.0f}); });
effect.build("uniform colorFilter child;"
"half4 main(half4 s, half4 d) { return sample(child, s); }");
effect.child("child") = nullptr;
effect.test(0xFF00FFFF,
[](SkCanvas*, SkPaint* paint) { paint->setColor4f({1.0f, 1.0f, 0.0f, 1.0f}); });
// Sampling a null blender should do a src-over blend. Draw 50% black over RGBW to verify this.
surface->getCanvas()->drawPaint(rgbwPaint);
effect.build("uniform blender child;"
"half4 main(half4 s, half4 d) { return sample(child, s, d); }");
effect.child("child") = nullptr;
effect.test({0xFF000080, 0xFF008000, 0xFF800000, 0xFF808080},
[](SkCanvas*, SkPaint* paint) { paint->setColor4f({0.0f, 0.0f, 0.0f, 0.497f}); });
// Sampling a shader at various coordinates
effect.build("uniform shader child;"
"uniform half2 pos;"
@ -672,6 +687,22 @@ static void test_RuntimeEffect_Blenders(skiatest::Reporter* r, GrRecordingContex
"half4 main(half4 s, half4 d) { return sample(child, half4(1)); }");
effect.child("child") = SkColorFilters::Blend(0xFF012345, SkBlendMode::kSrc);
effect.test(0xFF452301);
// Sampling a built-in blender
surface->getCanvas()->drawPaint(rgbwPaint);
effect.build("uniform blender child;"
"half4 main(half4 s, half4 d) { return sample(child, s, d); }");
effect.child("child") = SkBlender::Mode(SkBlendMode::kPlus);
effect.test({0xFF4523FF, 0xFF45FF01, 0xFFFF2301, 0xFFFFFFFF},
[](SkCanvas*, SkPaint* paint) { paint->setColor(0xFF012345); });
// Sampling a runtime-effect blender
surface->getCanvas()->drawPaint(rgbwPaint);
effect.build("uniform blender child;"
"half4 main(half4 s, half4 d) { return sample(child, s, d); }");
effect.child("child") = SkBlenders::Arithmetic(0, 1, 1, 0, /*enforcePremul=*/false);
effect.test({0xFF4523FF, 0xFF45FF01, 0xFFFF2301, 0xFFFFFFFF},
[](SkCanvas*, SkPaint* paint) { paint->setColor(0xFF012345); });
}
DEF_TEST(SkRuntimeEffect_Blender_CPU, r) {