Implement SkBlender support in Ganesh.
SkBlenderBase::asFragmentProcessor now returns a working runtime-blender
FP. This FP is appended to the end of the paintFP chain by
skpaint_to_grpaint_impl when an SkPaint contains an SkBlender, and the
GrPaint's XferProcessor factory is set to kSrc.
Unit tests have been added to verify basic functionality is working as
expected; more thorough drawing tests will be added once the CPU side is
running as well (since we don't want GMs that render differently on CPU
and GPU).
Change-Id: I255abd057fa75d638a9f2612c1a353be4de9e24c
Bug: skia:12080
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/419358
Reviewed-by: Brian Osman <brianosman@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
Commit-Queue: John Stiles <johnstiles@google.com>
Auto-Submit: John Stiles <johnstiles@google.com>
diff --git a/src/core/SkBlenderBase.h b/src/core/SkBlenderBase.h
index 7d5ef18..1a10fb0 100644
--- a/src/core/SkBlenderBase.h
+++ b/src/core/SkBlenderBase.h
@@ -13,6 +13,8 @@
#include "src/core/SkArenaAlloc.h"
#include "src/core/SkVM.h"
+struct GrFPArgs;
+class GrFragmentProcessor;
class SkRuntimeEffect;
/**
@@ -28,6 +30,15 @@
return this->onProgram(p, src, dst, colorInfo, uniforms, alloc);
}
+#if SK_SUPPORT_GPU
+ /**
+ * Returns a GrFragmentProcessor that implements this blend for the GPU backend.
+ * The GrFragmentProcessor expects a premultiplied input and returns a premultiplied output.
+ */
+ virtual std::unique_ptr<GrFragmentProcessor> asFragmentProcessor(
+ std::unique_ptr<GrFragmentProcessor> inputFP, const GrFPArgs& fpArgs) const = 0;
+#endif
+
virtual SkRuntimeEffect* asRuntimeEffect() const { return nullptr; }
static SkFlattenable::Type GetFlattenableType() { return kSkBlender_Type; }
diff --git a/src/core/SkRuntimeEffect.cpp b/src/core/SkRuntimeEffect.cpp
index 15d961f..e07af38 100644
--- a/src/core/SkRuntimeEffect.cpp
+++ b/src/core/SkRuntimeEffect.cpp
@@ -1033,6 +1033,18 @@
src, dst, /*sampleChild=*/nullptr);
}
+#if SK_SUPPORT_GPU
+ std::unique_ptr<GrFragmentProcessor> asFragmentProcessor(
+ std::unique_ptr<GrFragmentProcessor> inputFP, const GrFPArgs& args) const override {
+ sk_sp<SkData> uniforms = get_xformed_uniforms(fEffect.get(), fUniforms,
+ args.fDstColorInfo->colorSpace());
+ SkASSERT(uniforms);
+
+ return GrSkSLFP::MakeWithData(fEffect, "runtime_blender", std::move(inputFP),
+ std::move(uniforms), /*childFPs=*/{});
+ }
+#endif
+
void flatten(SkWriteBuffer& buffer) const override {
buffer.writeString(fEffect->source().c_str());
buffer.writeDataAsByteArray(fUniforms.get());
diff --git a/src/core/SkXfermodeInterpretation.cpp b/src/core/SkXfermodeInterpretation.cpp
index 95675a5..bae6ca1 100644
--- a/src/core/SkXfermodeInterpretation.cpp
+++ b/src/core/SkXfermodeInterpretation.cpp
@@ -13,6 +13,9 @@
}
SkXfermodeInterpretation SkInterpretXfermode(const SkPaint& paint, bool dstIsOpaque) {
+ if (paint.getBlender()) {
+ return kNormal_SkXfermodeInterpretation;
+ }
switch (paint.getBlendMode()) {
case SkBlendMode::kSrcOver:
return kSrcOver_SkXfermodeInterpretation;
diff --git a/src/gpu/SkGpuDevice_drawTexture.cpp b/src/gpu/SkGpuDevice_drawTexture.cpp
index dc2f2ef..09124a9 100644
--- a/src/gpu/SkGpuDevice_drawTexture.cpp
+++ b/src/gpu/SkGpuDevice_drawTexture.cpp
@@ -319,7 +319,8 @@
*/
static bool can_use_draw_texture(const SkPaint& paint, bool useCubicResampler, SkMipmapMode mm) {
return (!paint.getColorFilter() && !paint.getShader() && !paint.getMaskFilter() &&
- !paint.getImageFilter() && !useCubicResampler && mm == SkMipmapMode::kNone);
+ !paint.getImageFilter() && !paint.getBlender() && !useCubicResampler &&
+ mm == SkMipmapMode::kNone);
}
static SkPMColor4f texture_color(SkColor4f paintColor, float entryAlpha, GrColorType srcColorType,
diff --git a/src/gpu/SkGr.cpp b/src/gpu/SkGr.cpp
index 18113b5..e434d04 100644
--- a/src/gpu/SkGr.cpp
+++ b/src/gpu/SkGr.cpp
@@ -19,6 +19,7 @@
#include "include/private/SkTemplates.h"
#include "src/core/SkAutoMalloc.h"
#include "src/core/SkBlendModePriv.h"
+#include "src/core/SkBlenderBase.h"
#include "src/core/SkColorFilterBase.h"
#include "src/core/SkColorSpacePriv.h"
#include "src/core/SkImagePriv.h"
@@ -438,11 +439,19 @@
}
#endif
- // When the xfermode is null on the SkPaint (meaning kSrcOver) we need the XPFactory field on
- // the GrPaint to also be null (also kSrcOver).
- SkASSERT(!grPaint->getXPFactory());
- if (!skPaint.isSrcOver()) {
- grPaint->setXPFactory(SkBlendMode_AsXPFactory(skPaint.getBlendMode()));
+ SkBlender* blender = skPaint.getBlender();
+ if (blender) {
+ // Apply the custom blend, and force the XP to kSrc. We don't honor the SkBlendMode when a
+ // custom blend is applied.
+ paintFP = as_BB(blender)->asFragmentProcessor(std::move(paintFP), fpArgs);
+ grPaint->setXPFactory(SkBlendMode_AsXPFactory(SkBlendMode::kSrc));
+ } else {
+ // When the xfermode is null on the SkPaint (meaning kSrcOver) we need the XPFactory field
+ // on the GrPaint to also be null (also kSrcOver).
+ SkASSERT(!grPaint->getXPFactory());
+ if (!skPaint.isSrcOver()) {
+ grPaint->setXPFactory(SkBlendMode_AsXPFactory(skPaint.getBlendMode()));
+ }
}
if (GrColorTypeClampType(dstColorInfo.colorType()) == GrClampType::kManual) {
diff --git a/src/gpu/effects/GrSkSLFP.cpp b/src/gpu/effects/GrSkSLFP.cpp
index fbf0d47..77eac35 100644
--- a/src/gpu/effects/GrSkSLFP.cpp
+++ b/src/gpu/effects/GrSkSLFP.cpp
@@ -178,12 +178,24 @@
// Copy the incoming coords to a local variable. Code in main might modify the coords
// parameter. fSampleCoord could be a varying, so writes to it would be illegal.
- SkString coordsVarName = args.fFragBuilder->newTmpVarName("coords");
- const char* coords = coordsVarName.c_str();
+ const char* coords = "float2(0)";
+ SkString coordsVarName;
if (fp.referencesSampleCoords()) {
+ coordsVarName = args.fFragBuilder->newTmpVarName("coords");
+ coords = coordsVarName.c_str();
args.fFragBuilder->codeAppendf("float2 %s = %s;\n", coords, args.fSampleCoord);
}
+ // For blend effects, we need to copy the dest-color to a local variable as well.
+ const char* destColor = "half4(1)";
+ SkString destColorVarName;
+ if (fp.willReadDstColor()) {
+ destColorVarName = args.fFragBuilder->newTmpVarName("destColor");
+ destColor = destColorVarName.c_str();
+ args.fFragBuilder->codeAppendf(
+ "half4 %s = %s;\n", destColor, args.fFragBuilder->dstColor());
+ }
+
FPCallbacks callbacks(this,
args,
inputColorCopy.c_str(),
@@ -191,7 +203,7 @@
fp.uniformData(),
fp.uniformFlags());
SkSL::PipelineStage::ConvertProgram(
- program, coords, args.fInputColor, "half4(1)", &callbacks);
+ program, coords, args.fInputColor, destColor, &callbacks);
}
void onSetData(const GrGLSLProgramDataManager& pdman,
@@ -269,6 +281,10 @@
if (fEffect->usesSampleCoords()) {
this->setUsesSampleCoordsDirectly();
}
+
+ if (fEffect->allowBlender()) {
+ this->setWillReadDstColor();
+ }
}
GrSkSLFP::GrSkSLFP(const GrSkSLFP& other)
@@ -286,6 +302,10 @@
this->setUsesSampleCoordsDirectly();
}
+ if (fEffect->allowBlender()) {
+ this->setWillReadDstColor();
+ }
+
this->cloneAndRegisterAllChildProcessors(other);
}
diff --git a/src/gpu/glsl/GrGLSLShaderBuilder.h b/src/gpu/glsl/GrGLSLShaderBuilder.h
index a047240..af7f249 100644
--- a/src/gpu/glsl/GrGLSLShaderBuilder.h
+++ b/src/gpu/glsl/GrGLSLShaderBuilder.h
@@ -48,8 +48,8 @@
GrGLSLColorSpaceXformHelper* colorXformHelper = nullptr);
/** Does the work of appendTextureLookup and blends the result by dst, treating the texture
- lookup a the src input to the blend. The dst is assumed to be half4 and the result is always
- a half4. If dst is nullptr we use half4(1) as the blend dst. */
+ lookup as the src input to the blend. The dst is assumed to be half4 and the result is
+ always a half4. If dst is nullptr we use half4(1) as the blend dst. */
void appendTextureLookupAndBlend(const char* dst,
SkBlendMode,
SamplerHandle,
diff --git a/tests/SkRuntimeEffectTest.cpp b/tests/SkRuntimeEffectTest.cpp
index 6fccf5d..d0a6bb8 100644
--- a/tests/SkRuntimeEffectTest.cpp
+++ b/tests/SkRuntimeEffectTest.cpp
@@ -304,6 +304,40 @@
"sample(colorFilter, float2, half4)");
}
+using PreTestFn = std::function<void(SkCanvas*, SkPaint*)>;
+
+void paint_canvas(SkCanvas* canvas, SkPaint* paint, const PreTestFn& preTestCallback) {
+ canvas->save();
+ if (preTestCallback) {
+ preTestCallback(canvas, paint);
+ }
+ canvas->drawPaint(*paint);
+ canvas->restore();
+}
+
+static void verify_2x2_surface_results(skiatest::Reporter* r,
+ const SkRuntimeEffect* effect,
+ SkSurface* surface,
+ std::array<GrColor, 4> expected) {
+ std::array<GrColor, 4> actual;
+ SkImageInfo info = surface->imageInfo();
+ if (!surface->readPixels(info, actual.data(), info.minRowBytes(), /*srcX=*/0, /*srcY=*/0)) {
+ REPORT_FAILURE(r, "readPixels", SkString("readPixels failed"));
+ return;
+ }
+
+ if (actual != expected) {
+ REPORT_FAILURE(r, "Runtime effect didn't match expectations",
+ SkStringPrintf("\n"
+ "Expected: [ %08x %08x %08x %08x ]\n"
+ "Got : [ %08x %08x %08x %08x ]\n"
+ "SkSL:\n%s\n",
+ expected[0], expected[1], expected[2], expected[3],
+ actual[0], actual[1], actual[2], actual[3],
+ effect->source().c_str()));
+ }
+}
+
class TestEffect {
public:
TestEffect(skiatest::Reporter* r, sk_sp<SkSurface> surface)
@@ -322,14 +356,12 @@
SkRuntimeShaderBuilder::BuilderUniform uniform(const char* name) {
return fBuilder->uniform(name);
}
+
SkRuntimeShaderBuilder::BuilderChild child(const char* name) {
return fBuilder->child(name);
}
- using PreTestFn = std::function<void(SkCanvas*, SkPaint*)>;
-
- void test(GrColor TL, GrColor TR, GrColor BL, GrColor BR,
- PreTestFn preTestCallback = nullptr) {
+ void test(std::array<GrColor, 4> expected, PreTestFn preTestCallback = nullptr) {
auto shader = fBuilder->makeShader(/*localMatrix=*/nullptr, /*isOpaque=*/false);
if (!shader) {
REPORT_FAILURE(fReporter, "shader", SkString("Effect didn't produce a shader"));
@@ -341,34 +373,13 @@
paint.setShader(std::move(shader));
paint.setBlendMode(SkBlendMode::kSrc);
- canvas->save();
- if (preTestCallback) {
- preTestCallback(canvas, &paint);
- }
- canvas->drawPaint(paint);
- canvas->restore();
+ paint_canvas(canvas, &paint, preTestCallback);
- GrColor actual[4];
- SkImageInfo info = fSurface->imageInfo();
- if (!fSurface->readPixels(info, actual, info.minRowBytes(), 0, 0)) {
- REPORT_FAILURE(fReporter, "readPixels", SkString("readPixels failed"));
- return;
- }
-
- GrColor expected[4] = {TL, TR, BL, BR};
- if (0 != memcmp(actual, expected, sizeof(actual))) {
- REPORT_FAILURE(fReporter, "Runtime effect didn't match expectations",
- SkStringPrintf("\n"
- "Expected: [ %08x %08x %08x %08x ]\n"
- "Got : [ %08x %08x %08x %08x ]\n"
- "SkSL:\n%s\n",
- TL, TR, BL, BR, actual[0], actual[1], actual[2],
- actual[3], fBuilder->effect()->source().c_str()));
- }
+ verify_2x2_surface_results(fReporter, fBuilder->effect(), fSurface.get(), expected);
}
void test(GrColor expected, PreTestFn preTestCallback = nullptr) {
- this->test(expected, expected, expected, expected, preTestCallback);
+ this->test({expected, expected, expected, expected}, preTestCallback);
}
private:
@@ -377,6 +388,52 @@
SkTLazy<SkRuntimeShaderBuilder> fBuilder;
};
+class TestBlend {
+public:
+ TestBlend(skiatest::Reporter* r, sk_sp<SkSurface> surface)
+ : fReporter(r), fSurface(std::move(surface)) {}
+
+ void build(const char* src) {
+ auto [effect, errorText] = SkRuntimeEffect::MakeForBlender(SkString(src));
+ if (!effect) {
+ REPORT_FAILURE(fReporter, "effect",
+ SkStringPrintf("Effect didn't compile: %s", errorText.c_str()));
+ return;
+ }
+ fBuilder.init(std::move(effect));
+ }
+
+ SkRuntimeBlendBuilder::BuilderUniform uniform(const char* name) {
+ return fBuilder->uniform(name);
+ }
+
+ void test(std::array<GrColor, 4> expected, PreTestFn preTestCallback = nullptr) {
+ auto blender = fBuilder->makeBlender();
+ if (!blender) {
+ REPORT_FAILURE(fReporter, "blender", SkString("Effect didn't produce a blender"));
+ return;
+ }
+
+ SkCanvas* canvas = fSurface->getCanvas();
+ SkPaint paint;
+ paint.experimental_setBlender(std::move(blender));
+ paint.setColor(SK_ColorGRAY);
+
+ paint_canvas(canvas, &paint, preTestCallback);
+
+ verify_2x2_surface_results(fReporter, fBuilder->effect(), fSurface.get(), expected);
+ }
+
+ void test(GrColor expected, PreTestFn preTestCallback = nullptr) {
+ this->test({expected, expected, expected, expected}, preTestCallback);
+ }
+
+private:
+ skiatest::Reporter* fReporter;
+ sk_sp<SkSurface> fSurface;
+ SkTLazy<SkRuntimeBlendBuilder> fBuilder;
+};
+
// Produces a 2x2 bitmap shader, with opaque colors:
// [ Red, Green ]
// [ Blue, White ]
@@ -404,7 +461,7 @@
// Local coords
effect.build("half4 main(float2 p) { return half4(half2(p - 0.5), 0, 1); }");
- effect.test(0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF);
+ effect.test({0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF});
// Use of a simple uniform. (Draw twice with two values to ensure it's updated).
effect.build("uniform float4 gColor; half4 main(float2 p) { return half4(gColor); }");
@@ -425,25 +482,25 @@
// make sure we're not saturating unexpectedly.
effect.build(
"half4 main(float2 p) { return half4(0.498 * (half2(sk_FragCoord.xy) - 0.5), 0, 1); }");
- effect.test(0xFF000000, 0xFF00007F, 0xFF007F00, 0xFF007F7F,
+ effect.test({0xFF000000, 0xFF00007F, 0xFF007F00, 0xFF007F7F},
[](SkCanvas* canvas, SkPaint*) { canvas->rotate(45.0f); });
// Runtime effects should use relaxed precision rules by default
effect.build("half4 main(float2 p) { return float4(p - 0.5, 0, 1); }");
- effect.test(0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF);
+ effect.test({0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF});
// ... and support *returning* float4 (aka vec4), not just half4
effect.build("float4 main(float2 p) { return float4(p - 0.5, 0, 1); }");
- effect.test(0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF);
+ effect.test({0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF});
effect.build("vec4 main(float2 p) { return float4(p - 0.5, 0, 1); }");
- effect.test(0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF);
+ effect.test({0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF});
// Mutating coords should work. (skbug.com/10918)
effect.build("vec4 main(vec2 p) { p -= 0.5; return vec4(p, 0, 1); }");
- effect.test(0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF);
+ effect.test({0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF});
effect.build("void moveCoords(inout vec2 p) { p -= 0.5; }"
"vec4 main(vec2 p) { moveCoords(p); return vec4(p, 0, 1); }");
- effect.test(0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF);
+ effect.test({0xFF000000, 0xFF0000FF, 0xFF00FF00, 0xFF00FFFF});
//
// Sampling children
@@ -462,13 +519,13 @@
effect.build("uniform shader child;"
"half4 main(float2 p) { return sample(child, p); }");
effect.child("child") = rgbwShader;
- effect.test(0xFF0000FF, 0xFF00FF00, 0xFFFF0000, 0xFFFFFFFF);
+ effect.test({0xFF0000FF, 0xFF00FF00, 0xFFFF0000, 0xFFFFFFFF});
// Sampling with explicit coordinates (reflecting about the diagonal)
effect.build("uniform shader child;"
"half4 main(float2 p) { return sample(child, p.yx); }");
effect.child("child") = rgbwShader;
- effect.test(0xFF0000FF, 0xFFFF0000, 0xFF00FF00, 0xFFFFFFFF);
+ effect.test({0xFF0000FF, 0xFFFF0000, 0xFF00FF00, 0xFFFFFFFF});
//
// Helper functions
@@ -488,6 +545,73 @@
test_RuntimeEffect_Shaders(r, ctxInfo.directContext());
}
+static void test_RuntimeEffect_Blenders(skiatest::Reporter* r, GrRecordingContext* rContext) {
+ SkImageInfo info = SkImageInfo::Make(2, 2, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
+ sk_sp<SkSurface> surface = rContext
+ ? SkSurface::MakeRenderTarget(rContext, SkBudgeted::kNo, info)
+ : SkSurface::MakeRaster(info);
+ REPORTER_ASSERT(r, surface);
+ TestBlend effect(r, surface);
+
+ using float4 = std::array<float, 4>;
+ using int4 = std::array<int, 4>;
+
+ // Use of a simple uniform. (Draw twice with two values to ensure it's updated).
+ effect.build("uniform float4 gColor; half4 main(half4 s, half4 d) { return half4(gColor); }");
+ effect.uniform("gColor") = float4{ 0.0f, 0.25f, 0.75f, 1.0f };
+ effect.test(0xFFBF4000);
+ effect.uniform("gColor") = float4{ 1.0f, 0.0f, 0.0f, 0.498f };
+ effect.test(0x7F0000FF); // Unlike SkShaders, we don't clamp here
+
+ // Same, with integer uniforms
+ effect.build("uniform int4 gColor;"
+ "half4 main(half4 s, half4 d) { return half4(gColor) / 255.0; }");
+ effect.uniform("gColor") = int4{ 0x00, 0x40, 0xBF, 0xFF };
+ effect.test(0xFFBF4000);
+ effect.uniform("gColor") = int4{ 0xFF, 0x00, 0x00, 0x7F };
+ effect.test(0x7F0000FF); // Unlike SkShaders, we don't clamp here
+
+ // Verify that mutating the source and destination colors is allowed
+ effect.build("half4 main(half4 s, half4 d) { s += d; d += s; return half4(1); }");
+ effect.test(0xFFFFFFFF);
+
+ // Verify that we can write out the source color (ignoring the dest color)
+ // This is equivalent to the kSrc blend mode.
+ effect.build("half4 main(half4 s, half4 d) { return s; }");
+ 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);
+
+ // Verify that we can read back the dest color exactly as-is (ignoring the source color)
+ // This is equivalent to the kDst blend mode.
+ effect.build("half4 main(half4 s, half4 d) { return d; }");
+ effect.test({0xFF0000FF, 0xFF00FF00, 0xFFFF0000, 0xFFFFFFFF});
+
+ // Verify that we can invert the destination color (including the alpha channel).
+ // The expected outputs are the exact inverse of the previous test.
+ effect.build("half4 main(half4 s, half4 d) { return half4(1) - d; }");
+ effect.test({0x00FFFF00, 0x00FF00FF, 0x0000FFFF, 0x00000000});
+
+ // Verify that color values are clamped to 0 and 1.
+ effect.build("half4 main(half4 s, half4 d) { return half4(-1); }");
+ effect.test(0x00000000);
+ effect.build("half4 main(half4 s, half4 d) { return half4(2); }");
+ effect.test(0xFFFFFFFF);
+}
+
+DEF_TEST(SkRuntimeEffect_Blender_CPU, r) {
+ // TODO(skia:12080): add CPU support for SkBlender
+// test_RuntimeEffect_Blenders(r, /*rContext=*/nullptr);
+}
+
+DEF_GPUTEST_FOR_RENDERING_CONTEXTS(SkRuntimeEffect_Blender_GPU, r, ctxInfo) {
+ test_RuntimeEffect_Blenders(r, ctxInfo.directContext());
+}
+
DEF_TEST(SkRuntimeShaderBuilderReuse, r) {
const char* kSource = R"(
uniform half x;