Support mixing shaders and color filters in runtime effects
Needs more testing, but includes a GM that demonstrates the ultimate
benefit of this: our 3D color LUT demo working as a color filter.
Bug: skia:11813
Change-Id: I97c129c54bcf2cb788c0806b5d9e907ff058bb69
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/406296
Reviewed-by: Mike Klein <mtklein@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Brian Salomon <bsalomon@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/gm/runtimeshader.cpp b/gm/runtimeshader.cpp
index 7c0792e..e96f2de 100644
--- a/gm/runtimeshader.cpp
+++ b/gm/runtimeshader.cpp
@@ -20,8 +20,9 @@
#include "tools/Resources.h"
enum RT_Flags {
- kAnimate_RTFlag = 0x1,
- kBench_RTFlag = 0x2,
+ kAnimate_RTFlag = 0x1,
+ kBench_RTFlag = 0x2,
+ kColorFilter_RTFlag = 0x4,
};
class RuntimeShaderGM : public skiagm::GM {
@@ -30,7 +31,9 @@
: fName(name), fSize(size), fFlags(flags), fSkSL(sksl) {}
void onOnceBeforeDraw() override {
- auto [effect, error] = SkRuntimeEffect::MakeForShader(fSkSL);
+ auto [effect, error] = (fFlags & kColorFilter_RTFlag)
+ ? SkRuntimeEffect::MakeForColorFilter(fSkSL)
+ : SkRuntimeEffect::MakeForShader(fSkSL);
if (!effect) {
SkDebugf("RuntimeShader error: %s\n", error.c_str());
}
@@ -340,6 +343,87 @@
};
DEF_GM(return new ColorCubeRT;)
+// Same as above, but demonstrating how to implement this as a runtime color filter (that samples
+// a shader child for the LUT).
+class ColorCubeColorFilterRT : public RuntimeShaderGM {
+public:
+ ColorCubeColorFilterRT() : RuntimeShaderGM("color_cube_cf_rt", {512, 512}, R"(
+ uniform shader color_cube;
+
+ uniform float rg_scale;
+ uniform float rg_bias;
+ uniform float b_scale;
+ uniform float inv_size;
+
+ half4 main(half4 inColor) {
+ float4 c = unpremul(inColor);
+
+ // Map to cube coords:
+ float3 cubeCoords = float3(c.rg * rg_scale + rg_bias, c.b * b_scale);
+
+ // Compute slice coordinate
+ float2 coords1 = float2((floor(cubeCoords.b) + cubeCoords.r) * inv_size, cubeCoords.g);
+ float2 coords2 = float2(( ceil(cubeCoords.b) + cubeCoords.r) * inv_size, cubeCoords.g);
+
+ // Two bilinear fetches, plus a manual lerp for the third axis:
+ half4 color = mix(sample(color_cube, coords1), sample(color_cube, coords2),
+ fract(cubeCoords.b));
+
+ // Premul again
+ color.rgb *= color.a;
+
+ return color;
+ }
+ )", kColorFilter_RTFlag) {}
+
+ sk_sp<SkImage> fMandrill, fMandrillSepia, fIdentityCube, fSepiaCube;
+
+ void onOnceBeforeDraw() override {
+ fMandrill = GetResourceAsImage("images/mandrill_256.png");
+ fMandrillSepia = GetResourceAsImage("images/mandrill_sepia.png");
+ fIdentityCube = GetResourceAsImage("images/lut_identity.png");
+ fSepiaCube = GetResourceAsImage("images/lut_sepia.png");
+
+ this->RuntimeShaderGM::onOnceBeforeDraw();
+ }
+
+ void onDraw(SkCanvas* canvas) override {
+ // First we draw the unmodified image, and a copy that was sepia-toned in Photoshop:
+ canvas->drawImage(fMandrill, 0, 0);
+ canvas->drawImage(fMandrillSepia, 0, 256);
+
+ // LUT dimensions should be (kSize^2, kSize)
+ constexpr float kSize = 16.0f;
+
+ const SkSamplingOptions sampling(SkFilterMode::kLinear);
+
+ float uniforms[] = {
+ (kSize - 1) / kSize, // rg_scale
+ 0.5f / kSize, // rg_bias
+ kSize - 1, // b_scale
+ 1.0f / kSize, // inv_size
+ };
+
+ SkPaint paint;
+
+ // TODO: Should we add SkImage::makeNormalizedShader() to handle this automatically?
+ SkMatrix normalize = SkMatrix::Scale(1.0f / (kSize * kSize), 1.0f / kSize);
+
+ // Now draw the image with an identity color cube - it should look like the original
+ SkRuntimeEffect::ChildPtr children[] = {fIdentityCube->makeShader(sampling, normalize)};
+ paint.setColorFilter(fEffect->makeColorFilter(
+ SkData::MakeWithCopy(uniforms, sizeof(uniforms)), SkMakeSpan(children)));
+ canvas->drawImage(fMandrill, 256, 0, sampling, &paint);
+
+ // ... and with a sepia-tone color cube. This should match the sepia-toned image.
+ children[0] = fSepiaCube->makeShader(sampling, normalize);
+ paint.setColorFilter(fEffect->makeColorFilter(
+ SkData::MakeWithCopy(uniforms, sizeof(uniforms)), SkMakeSpan(children)));
+ canvas->drawImage(fMandrill, 256, 256, sampling, &paint);
+ }
+};
+DEF_GM(return new ColorCubeColorFilterRT;)
+
class DefaultColorRT : public RuntimeShaderGM {
public:
DefaultColorRT() : RuntimeShaderGM("default_color_rt", {512, 256}, R"(
diff --git a/include/effects/SkRuntimeEffect.h b/include/effects/SkRuntimeEffect.h
index f3c4f06..ac9fe97 100644
--- a/include/effects/SkRuntimeEffect.h
+++ b/include/effects/SkRuntimeEffect.h
@@ -8,10 +8,12 @@
#ifndef SkRuntimeEffect_DEFINED
#define SkRuntimeEffect_DEFINED
+#include "include/core/SkColorFilter.h"
#include "include/core/SkData.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkMatrix.h"
#include "include/core/SkShader.h"
+#include "include/core/SkSpan.h"
#include "include/core/SkString.h"
#include "include/private/SkOnce.h"
#include "include/private/SkSLSampleUsage.h"
@@ -19,10 +21,8 @@
#include <vector>
class GrRecordingContext;
-class SkColorFilter;
class SkFilterColorProgram;
class SkImage;
-class SkShader;
namespace SkSL {
class FunctionDefinition;
@@ -42,6 +42,7 @@
*/
class SK_API SkRuntimeEffect : public SkRefCnt {
public:
+ // Reflected description of a uniform variable in the effect's SkSL
struct Uniform {
enum class Type {
kFloat,
@@ -72,6 +73,7 @@
size_t sizeInBytes() const;
};
+ // Reflected description of a uniform child (shader or colorFilter) in the effect's SkSL
struct Child {
enum class Type {
kShader,
@@ -131,11 +133,23 @@
static Result MakeForShader(std::unique_ptr<SkSL::Program> program);
+ // Object that allows passing either an SkShader or SkColorFilter as a child
+ struct ChildPtr {
+ ChildPtr(sk_sp<SkShader> s) : shader(std::move(s)) {}
+ ChildPtr(sk_sp<SkColorFilter> cf) : colorFilter(std::move(cf)) {}
+ sk_sp<SkShader> shader;
+ sk_sp<SkColorFilter> colorFilter;
+ };
+
sk_sp<SkShader> makeShader(sk_sp<SkData> uniforms,
sk_sp<SkShader> children[],
size_t childCount,
const SkMatrix* localMatrix,
bool isOpaque) const;
+ sk_sp<SkShader> makeShader(sk_sp<SkData> uniforms,
+ SkSpan<ChildPtr> children,
+ const SkMatrix* localMatrix,
+ bool isOpaque) const;
sk_sp<SkImage> makeImage(GrRecordingContext*,
sk_sp<SkData> uniforms,
@@ -149,6 +163,8 @@
sk_sp<SkColorFilter> makeColorFilter(sk_sp<SkData> uniforms,
sk_sp<SkColorFilter> children[],
size_t childCount) const;
+ sk_sp<SkColorFilter> makeColorFilter(sk_sp<SkData> uniforms,
+ SkSpan<ChildPtr> children) const;
const SkString& source() const { return fSkSL; }
diff --git a/src/core/SkRuntimeEffect.cpp b/src/core/SkRuntimeEffect.cpp
index 7646883..db02bfe 100644
--- a/src/core/SkRuntimeEffect.cpp
+++ b/src/core/SkRuntimeEffect.cpp
@@ -442,7 +442,18 @@
// color, an immediate color, or the results of a previous sample call). If the color is none
// 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).
+ if (!std::all_of(effect->fChildren.begin(),
+ effect->fChildren.end(),
+ [](const SkRuntimeEffect::Child& c) {
+ return c.type == SkRuntimeEffect::Child::Type::kColorFilter;
+ })) {
+ return nullptr;
+ }
+
// When we run this program later, these uniform values are replaced with either the results of
// the child (using the SampleCall), or the input color (if the child is nullptr).
// These Uniform ids are loads from the *first* arg ptr.
@@ -605,15 +616,46 @@
return uniforms ? uniforms : baseUniforms;
}
+#if SK_SUPPORT_GPU
+static std::unique_ptr<GrFragmentProcessor> make_effect_fp(
+ sk_sp<SkRuntimeEffect> effect,
+ const char* name,
+ sk_sp<SkData> uniforms,
+ SkSpan<const SkRuntimeEffect::ChildPtr> children,
+ const GrFPArgs& childArgs) {
+ auto fp = GrSkSLFP::Make(std::move(effect), name, std::move(uniforms));
+ for (const auto& child : children) {
+ if (child.shader) {
+ auto childFP = as_SB(child.shader)->asFragmentProcessor(childArgs);
+ if (!childFP) {
+ return nullptr;
+ }
+ fp->addChild(std::move(childFP));
+ } else if (child.colorFilter) {
+ auto [success, childFP] = as_CFB(child.colorFilter)
+ ->asFragmentProcessor(/*inputFP=*/nullptr,
+ childArgs.fContext,
+ *childArgs.fDstColorInfo);
+ if (!success) {
+ return nullptr;
+ }
+ fp->addChild(std::move(childFP));
+ } else {
+ fp->addChild(nullptr);
+ }
+ }
+ return std::move(fp);
+}
+#endif
+
class SkRuntimeColorFilter : public SkColorFilterBase {
public:
SkRuntimeColorFilter(sk_sp<SkRuntimeEffect> effect,
sk_sp<SkData> uniforms,
- sk_sp<SkColorFilter> children[],
- size_t childCount)
+ SkSpan<SkRuntimeEffect::ChildPtr> children)
: fEffect(std::move(effect))
, fUniforms(std::move(uniforms))
- , fChildren(children, children + childCount) {}
+ , fChildren(children.begin(), children.end()) {}
#if SK_SUPPORT_GPU
GrFPResult asFragmentProcessor(std::unique_ptr<GrFragmentProcessor> inputFP,
@@ -623,18 +665,14 @@
get_xformed_uniforms(fEffect.get(), fUniforms, colorInfo.colorSpace());
SkASSERT(uniforms);
- auto fp = GrSkSLFP::Make(fEffect, "Runtime_Color_Filter", std::move(uniforms));
- for (const auto& child : fChildren) {
- std::unique_ptr<GrFragmentProcessor> childFP;
- if (child) {
- bool success;
- std::tie(success, childFP) = as_CFB(child)->asFragmentProcessor(
- /*inputFP=*/nullptr, context, colorInfo);
- if (!success) {
- return GrFPFailure(std::move(inputFP));
- }
- }
- fp->addChild(std::move(childFP));
+ GrFPArgs childArgs(context, SkSimpleMatrixProvider(SkMatrix::I()), &colorInfo);
+ auto fp = make_effect_fp(fEffect,
+ "Runtime_Color_Filter",
+ std::move(uniforms),
+ SkMakeSpan(fChildren),
+ childArgs);
+ if (!fp) {
+ return GrFPFailure(std::move(inputFP));
}
// Runtime effect scripts are written to take an input color, not a fragment processor.
@@ -659,9 +697,13 @@
// something. (Uninitialized values can trigger asserts in skvm::Builder).
skvm::Coord zeroCoord = { p->splat(0.0f), p->splat(0.0f) };
- auto sampleChild = [&](int ix, skvm::Coord /*coord*/, skvm::Color color) {
- if (fChildren[ix]) {
- return as_CFB(fChildren[ix])->program(p, color, dst, uniforms, alloc);
+ auto sampleChild = [&](int ix, skvm::Coord coord, skvm::Color color) {
+ if (fChildren[ix].shader) {
+ SkSimpleMatrixProvider mats{SkMatrix::I()};
+ return as_SB(fChildren[ix].shader)
+ ->program(p, coord, coord, color, mats, nullptr, dst, uniforms, alloc);
+ } else if (fChildren[ix].colorFilter) {
+ return as_CFB(fChildren[ix].colorFilter)->program(p, color, dst, uniforms, alloc);
} else {
return color;
}
@@ -694,8 +736,12 @@
SkASSERT(inputs);
auto evalChild = [&](int index, SkPMColor4f inColor) {
- const SkColorFilter* child = fChildren[index].get();
- return child ? as_CFB(child)->onFilterColor4f(inColor, dstCS) : inColor;
+ const auto& child = fChildren[index];
+
+ // Guaranteed by initFilterColorInfo
+ SkASSERT(!child.shader);
+ return child.colorFilter ? as_CFB(child.colorFilter)->onFilterColor4f(inColor, dstCS)
+ : inColor;
};
return program->eval(color, inputs->data(), evalChild);
@@ -715,7 +761,8 @@
}
buffer.write32(fChildren.size());
for (const auto& child : fChildren) {
- buffer.writeFlattenable(child.get());
+ buffer.writeFlattenable(child.shader ? (const SkFlattenable*)child.shader.get()
+ : child.colorFilter.get());
}
}
@@ -724,7 +771,7 @@
private:
sk_sp<SkRuntimeEffect> fEffect;
sk_sp<SkData> fUniforms;
- std::vector<sk_sp<SkColorFilter>> fChildren;
+ std::vector<SkRuntimeEffect::ChildPtr> fChildren;
};
sk_sp<SkFlattenable> SkRuntimeColorFilter::CreateProc(SkReadBuffer& buffer) {
@@ -742,25 +789,36 @@
return nullptr;
}
- std::vector<sk_sp<SkColorFilter>> children(childCount);
- for (size_t i = 0; i < children.size(); ++i) {
- children[i] = buffer.readColorFilter();
+ SkSTArray<4, SkRuntimeEffect::ChildPtr> children(childCount);
+ for (const auto& child : effect->children()) {
+ if (child.type == SkRuntimeEffect::Child::Type::kShader) {
+ children.emplace_back(buffer.readShader());
+ } else {
+ SkASSERT(child.type == SkRuntimeEffect::Child::Type::kColorFilter);
+ children.emplace_back(buffer.readColorFilter());
+ }
+ }
+ if (!buffer.isValid()) {
+ return nullptr;
}
- return effect->makeColorFilter(std::move(uniforms), children.data(), children.size());
+ return effect->makeColorFilter(std::move(uniforms), SkMakeSpan(children));
}
///////////////////////////////////////////////////////////////////////////////////////////////////
class SkRTShader : public SkShaderBase {
public:
- SkRTShader(sk_sp<SkRuntimeEffect> effect, sk_sp<SkData> uniforms, const SkMatrix* localMatrix,
- sk_sp<SkShader>* children, size_t childCount, bool isOpaque)
+ SkRTShader(sk_sp<SkRuntimeEffect> effect,
+ sk_sp<SkData> uniforms,
+ const SkMatrix* localMatrix,
+ SkSpan<SkRuntimeEffect::ChildPtr> children,
+ bool isOpaque)
: SkShaderBase(localMatrix)
, fEffect(std::move(effect))
, fIsOpaque(isOpaque)
, fUniforms(std::move(uniforms))
- , fChildren(children, children + childCount) {}
+ , fChildren(children.begin(), children.end()) {}
bool isOpaque() const override { return fIsOpaque; }
@@ -780,12 +838,12 @@
GrFPArgs childArgs = args;
childArgs.fInputColorIsOpaque = false;
- auto fp = GrSkSLFP::Make(fEffect, "runtime_shader", std::move(uniforms));
- for (const auto& child : fChildren) {
- auto childFP = child ? as_SB(child)->asFragmentProcessor(childArgs) : nullptr;
- fp->addChild(std::move(childFP));
+ auto result = make_effect_fp(
+ fEffect, "runtime_shader", std::move(uniforms), SkMakeSpan(fChildren), childArgs);
+ if (!result) {
+ return nullptr;
}
- std::unique_ptr<GrFragmentProcessor> result = std::move(fp);
+
// If the shader was created with isOpaque = true, we *force* that result here.
// CPU does the same thing (in SkShaderBase::program).
if (fIsOpaque) {
@@ -826,11 +884,12 @@
local = SkShaderBase::ApplyMatrix(p,inv,local,uniforms);
auto sampleChild = [&](int ix, skvm::Coord coord, skvm::Color color) {
- if (fChildren[ix]) {
+ if (fChildren[ix].shader) {
SkOverrideDeviceMatrixProvider mats{matrices, SkMatrix::I()};
- return as_SB(fChildren[ix])->program(p, device, coord, color,
- mats, nullptr, dst,
- uniforms, alloc);
+ return as_SB(fChildren[ix].shader)
+ ->program(p, device, coord, color, mats, nullptr, dst, uniforms, alloc);
+ } else if (fChildren[ix].colorFilter) {
+ return as_CFB(fChildren[ix].colorFilter)->program(p, color, dst, uniforms, alloc);
} else {
return color;
}
@@ -870,7 +929,8 @@
}
buffer.write32(fChildren.size());
for (const auto& child : fChildren) {
- buffer.writeFlattenable(child.get());
+ buffer.writeFlattenable(child.shader ? (const SkFlattenable*)child.shader.get()
+ : child.colorFilter.get());
}
}
@@ -888,7 +948,7 @@
bool fIsOpaque;
sk_sp<SkData> fUniforms;
- std::vector<sk_sp<SkShader>> fChildren;
+ std::vector<SkRuntimeEffect::ChildPtr> fChildren;
};
sk_sp<SkFlattenable> SkRTShader::CreateProc(SkReadBuffer& buffer) {
@@ -914,43 +974,65 @@
return nullptr;
}
- std::vector<sk_sp<SkShader>> children(childCount);
- for (size_t i = 0; i < children.size(); ++i) {
- children[i] = buffer.readShader();
+ SkSTArray<4, SkRuntimeEffect::ChildPtr> children(childCount);
+ for (const auto& child : effect->children()) {
+ if (child.type == SkRuntimeEffect::Child::Type::kShader) {
+ children.emplace_back(buffer.readShader());
+ } else {
+ SkASSERT(child.type == SkRuntimeEffect::Child::Type::kColorFilter);
+ children.emplace_back(buffer.readColorFilter());
+ }
+ }
+ if (!buffer.isValid()) {
+ return nullptr;
}
- return effect->makeShader(std::move(uniforms), children.data(), children.size(), localMPtr,
- isOpaque);
+ return effect->makeShader(std::move(uniforms), SkMakeSpan(children), localMPtr, isOpaque);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
sk_sp<SkShader> SkRuntimeEffect::makeShader(sk_sp<SkData> uniforms,
- sk_sp<SkShader> children[],
+ sk_sp<SkShader> childShaders[],
size_t childCount,
const SkMatrix* localMatrix,
bool isOpaque) const {
+ SkSTArray<4, ChildPtr> children(childCount);
+ for (size_t i = 0; i < childCount; ++i) {
+ children.emplace_back(childShaders[i]);
+ }
+ return this->makeShader(std::move(uniforms), SkMakeSpan(children), localMatrix, isOpaque);
+}
+
+sk_sp<SkShader> SkRuntimeEffect::makeShader(sk_sp<SkData> uniforms,
+ SkSpan<ChildPtr> children,
+ const SkMatrix* localMatrix,
+ bool isOpaque) const {
if (!this->allowShader()) {
return nullptr;
}
+ if (children.size() != fChildren.size()) {
+ return nullptr;
+ }
+ // Verify that all child objects match the declared type in the SkSL
+ for (size_t i = 0; i < children.size(); ++i) {
+ if (fChildren[i].type == Child::Type::kShader) {
+ if (children[i].colorFilter) {
+ return nullptr;
+ }
+ } else {
+ SkASSERT(fChildren[i].type == Child::Type::kColorFilter);
+ if (children[i].shader) {
+ return nullptr;
+ }
+ }
+ }
if (!uniforms) {
uniforms = SkData::MakeEmpty();
}
- // Verify that all child objects are shaders (to match the C++ types here).
- // TODO(skia:11813) When we support shader and colorFilter children (with different samplng
- // semantics), the 'children' parameter will contain both types, so this will be more complex.
- if (!std::all_of(fChildren.begin(), fChildren.end(), [](const Child& c) {
- return c.type == Child::Type::kShader;
- })) {
- return nullptr;
- }
- return uniforms->size() == this->uniformSize() && childCount == fChildren.size()
- ? sk_sp<SkShader>(new SkRTShader(sk_ref_sp(this),
- std::move(uniforms),
- localMatrix,
- children,
- childCount,
- isOpaque))
+ return uniforms->size() == this->uniformSize()
+ ? sk_sp<SkShader>(new SkRTShader(
+ sk_ref_sp(this), std::move(uniforms), localMatrix, children, isOpaque))
: nullptr;
}
@@ -1033,30 +1115,47 @@
}
sk_sp<SkColorFilter> SkRuntimeEffect::makeColorFilter(sk_sp<SkData> uniforms,
- sk_sp<SkColorFilter> children[],
+ sk_sp<SkColorFilter> childColorFilters[],
size_t childCount) const {
+ SkSTArray<4, ChildPtr> children(childCount);
+ for (size_t i = 0; i < childCount; ++i) {
+ children.emplace_back(childColorFilters[i]);
+ }
+ return this->makeColorFilter(std::move(uniforms), SkMakeSpan(children));
+}
+
+sk_sp<SkColorFilter> SkRuntimeEffect::makeColorFilter(sk_sp<SkData> uniforms,
+ SkSpan<ChildPtr> children) const {
if (!this->allowColorFilter()) {
return nullptr;
}
+ if (children.size() != fChildren.size()) {
+ return nullptr;
+ }
+ // Verify that all child objects match the declared type in the SkSL
+ for (size_t i = 0; i < children.size(); ++i) {
+ if (fChildren[i].type == Child::Type::kShader) {
+ if (children[i].colorFilter) {
+ return nullptr;
+ }
+ } else {
+ SkASSERT(fChildren[i].type == Child::Type::kColorFilter);
+ if (children[i].shader) {
+ return nullptr;
+ }
+ }
+ }
if (!uniforms) {
uniforms = SkData::MakeEmpty();
}
- // Verify that all child objects are color filters (to match the C++ types here).
- // TODO(skia:11813) When we support shader and colorFilter children (with different samplng
- // semantics), the 'children' parameter will contain both types, so this will be more complex.
- if (!std::all_of(fChildren.begin(), fChildren.end(), [](const Child& c) {
- return c.type == Child::Type::kColorFilter;
- })) {
- return nullptr;
- }
- return uniforms->size() == this->uniformSize() && childCount == fChildren.size()
+ return uniforms->size() == this->uniformSize()
? sk_sp<SkColorFilter>(new SkRuntimeColorFilter(
- sk_ref_sp(this), std::move(uniforms), children, childCount))
+ sk_ref_sp(this), std::move(uniforms), children))
: nullptr;
}
sk_sp<SkColorFilter> SkRuntimeEffect::makeColorFilter(sk_sp<SkData> uniforms) const {
- return this->makeColorFilter(std::move(uniforms), nullptr, 0);
+ return this->makeColorFilter(std::move(uniforms), /*children=*/{});
}
///////////////////////////////////////////////////////////////////////////////////////////////////